mirror of
https://github.com/withastro/astro.git
synced 2025-03-10 23:01:26 -05:00
Fix Astro components parent-child render order (#8187)
This commit is contained in:
parent
1e8942c438
commit
273335cb01
11 changed files with 123 additions and 27 deletions
5
.changeset/mean-snakes-play.md
Normal file
5
.changeset/mean-snakes-play.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix Astro components parent-child render order
|
|
@ -2,6 +2,7 @@ import { escapeHTML, isHTMLString, markHTMLString } from '../escape.js';
|
||||||
import { isAstroComponentInstance, isRenderTemplateResult } from './astro/index.js';
|
import { isAstroComponentInstance, isRenderTemplateResult } from './astro/index.js';
|
||||||
import { isRenderInstance, type RenderDestination } from './common.js';
|
import { isRenderInstance, type RenderDestination } from './common.js';
|
||||||
import { SlotString } from './slot.js';
|
import { SlotString } from './slot.js';
|
||||||
|
import { renderToBufferDestination } from './util.js';
|
||||||
|
|
||||||
export async function renderChild(destination: RenderDestination, child: any) {
|
export async function renderChild(destination: RenderDestination, child: any) {
|
||||||
child = await child;
|
child = await child;
|
||||||
|
@ -10,8 +11,14 @@ export async function renderChild(destination: RenderDestination, child: any) {
|
||||||
} else if (isHTMLString(child)) {
|
} else if (isHTMLString(child)) {
|
||||||
destination.write(child);
|
destination.write(child);
|
||||||
} else if (Array.isArray(child)) {
|
} else if (Array.isArray(child)) {
|
||||||
for (const c of child) {
|
// Render all children eagerly and in parallel
|
||||||
await renderChild(destination, c);
|
const childRenders = child.map((c) => {
|
||||||
|
return renderToBufferDestination((bufferDestination) => {
|
||||||
|
return renderChild(bufferDestination, c);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
for (const childRender of childRenders) {
|
||||||
|
await childRender.renderToFinalDestination(destination);
|
||||||
}
|
}
|
||||||
} else if (typeof child === 'function') {
|
} else if (typeof child === 'function') {
|
||||||
// Special: If a child is a function, call it automatically.
|
// Special: If a child is a function, call it automatically.
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { markHTMLString } from '../../escape.js';
|
||||||
import { isPromise } from '../../util.js';
|
import { isPromise } from '../../util.js';
|
||||||
import { renderChild } from '../any.js';
|
import { renderChild } from '../any.js';
|
||||||
import type { RenderDestination } from '../common.js';
|
import type { RenderDestination } from '../common.js';
|
||||||
|
import { renderToBufferDestination } from '../util.js';
|
||||||
|
|
||||||
const renderTemplateResultSym = Symbol.for('astro.renderTemplateResult');
|
const renderTemplateResultSym = Symbol.for('astro.renderTemplateResult');
|
||||||
|
|
||||||
|
@ -32,14 +33,23 @@ export class RenderTemplateResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
async render(destination: RenderDestination) {
|
async render(destination: RenderDestination) {
|
||||||
|
// Render all expressions eagerly and in parallel
|
||||||
|
const expRenders = this.expressions.map((exp) => {
|
||||||
|
return renderToBufferDestination((bufferDestination) => {
|
||||||
|
// Skip render if falsy, except the number 0
|
||||||
|
if (exp || exp === 0) {
|
||||||
|
return renderChild(bufferDestination, exp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
for (let i = 0; i < this.htmlParts.length; i++) {
|
for (let i = 0; i < this.htmlParts.length; i++) {
|
||||||
const html = this.htmlParts[i];
|
const html = this.htmlParts[i];
|
||||||
const exp = this.expressions[i];
|
const expRender = expRenders[i];
|
||||||
|
|
||||||
destination.write(markHTMLString(html));
|
destination.write(markHTMLString(html));
|
||||||
// Skip render if falsy, except the number 0
|
if (expRender) {
|
||||||
if (exp || exp === 0) {
|
await expRender.renderToFinalDestination(destination);
|
||||||
await renderChild(destination, exp);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,9 +37,11 @@ export interface RenderDestination {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RenderInstance {
|
export interface RenderInstance {
|
||||||
render(destination: RenderDestination): Promise<void> | void;
|
render: RenderFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RenderFunction = (destination: RenderDestination) => Promise<void> | void;
|
||||||
|
|
||||||
export const Fragment = Symbol.for('astro:fragment');
|
export const Fragment = Symbol.for('astro:fragment');
|
||||||
export const Renderer = Symbol.for('astro:renderer');
|
export const Renderer = Symbol.for('astro:renderer');
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,6 @@ import {
|
||||||
Renderer,
|
Renderer,
|
||||||
chunkToString,
|
chunkToString,
|
||||||
type RenderDestination,
|
type RenderDestination,
|
||||||
type RenderDestinationChunk,
|
|
||||||
type RenderInstance,
|
type RenderInstance,
|
||||||
} from './common.js';
|
} from './common.js';
|
||||||
import { componentIsHTMLElement, renderHTMLElement } from './dom.js';
|
import { componentIsHTMLElement, renderHTMLElement } from './dom.js';
|
||||||
|
@ -421,27 +420,12 @@ function renderAstroComponent(
|
||||||
slots: any = {}
|
slots: any = {}
|
||||||
): RenderInstance {
|
): RenderInstance {
|
||||||
const instance = createAstroComponentInstance(result, displayName, Component, props, slots);
|
const instance = createAstroComponentInstance(result, displayName, Component, props, slots);
|
||||||
|
|
||||||
// Eagerly render the component so they are rendered in parallel.
|
|
||||||
// Render to buffer for now until our returned render function is called.
|
|
||||||
const bufferChunks: RenderDestinationChunk[] = [];
|
|
||||||
const bufferDestination: RenderDestination = {
|
|
||||||
write: (chunk) => bufferChunks.push(chunk),
|
|
||||||
};
|
|
||||||
// Don't await for the render to finish to not block streaming
|
|
||||||
const renderPromise = instance.render(bufferDestination);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async render(destination) {
|
async render(destination) {
|
||||||
// Write the buffered chunks to the real destination
|
// NOTE: This render call can't be pre-invoked outside of this function as it'll also initialize the slots
|
||||||
for (const chunk of bufferChunks) {
|
// recursively, which causes each Astro components in the tree to be called bottom-up, and is incorrect.
|
||||||
destination.write(chunk);
|
// The slots are initialized eagerly for head propagation.
|
||||||
}
|
await instance.render(destination);
|
||||||
// Free memory
|
|
||||||
bufferChunks.length = 0;
|
|
||||||
// Re-assign the real destination so `instance.render` will continue and write to the new destination
|
|
||||||
bufferDestination.write = (chunk) => destination.write(chunk);
|
|
||||||
await renderPromise;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { SSRElement } from '../../../@types/astro';
|
import type { SSRElement } from '../../../@types/astro';
|
||||||
|
import type { RenderDestination, RenderDestinationChunk, RenderFunction } from './common.js';
|
||||||
|
|
||||||
import { HTMLString, markHTMLString } from '../escape.js';
|
import { HTMLString, markHTMLString } from '../escape.js';
|
||||||
import { serializeListValue } from '../util.js';
|
import { serializeListValue } from '../util.js';
|
||||||
|
@ -145,3 +146,56 @@ export function renderElement(
|
||||||
}
|
}
|
||||||
return `<${name}${internalSpreadAttributes(props, shouldEscape)}>${children}</${name}>`;
|
return `<${name}${internalSpreadAttributes(props, shouldEscape)}>${children}</${name}>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the `bufferRenderFunction` to prerender it into a buffer destination, and return a promise
|
||||||
|
* with an object containing the `renderToFinalDestination` function to flush the buffer to the final
|
||||||
|
* destination.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // Render components in parallel ahead of time
|
||||||
|
* const finalRenders = [ComponentA, ComponentB].map((comp) => {
|
||||||
|
* return renderToBufferDestination(async (bufferDestination) => {
|
||||||
|
* await renderComponentToDestination(bufferDestination);
|
||||||
|
* });
|
||||||
|
* });
|
||||||
|
* // Render array of components serially
|
||||||
|
* for (const finalRender of finalRenders) {
|
||||||
|
* await finalRender.renderToFinalDestination(finalDestination);
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function renderToBufferDestination(bufferRenderFunction: RenderFunction): {
|
||||||
|
renderToFinalDestination: RenderFunction;
|
||||||
|
} {
|
||||||
|
// Keep chunks in memory
|
||||||
|
const bufferChunks: RenderDestinationChunk[] = [];
|
||||||
|
const bufferDestination: RenderDestination = {
|
||||||
|
write: (chunk) => bufferChunks.push(chunk),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Don't await for the render to finish to not block streaming
|
||||||
|
const renderPromise = bufferRenderFunction(bufferDestination);
|
||||||
|
|
||||||
|
// Return a closure that writes the buffered chunk
|
||||||
|
return {
|
||||||
|
async renderToFinalDestination(destination) {
|
||||||
|
// Write the buffered chunks to the real destination
|
||||||
|
for (const chunk of bufferChunks) {
|
||||||
|
destination.write(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: We don't empty `bufferChunks` after it's written as benchmarks show
|
||||||
|
// that it causes poorer performance, likely due to forced memory re-allocation,
|
||||||
|
// instead of letting the garbage collector handle it automatically.
|
||||||
|
// (Unsure how this affects on limited memory machines)
|
||||||
|
|
||||||
|
// Re-assign the real destination so `instance.render` will continue and write to the new destination
|
||||||
|
bufferDestination.write = (chunk) => destination.write(chunk);
|
||||||
|
|
||||||
|
// Wait for render to finish entirely
|
||||||
|
await renderPromise;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -102,6 +102,12 @@ describe('Astro basics', () => {
|
||||||
// will have already erred by now, but add test anyway
|
// will have already erred by now, but add test anyway
|
||||||
expect(await fixture.readFile('/special-“characters” -in-file/index.html')).to.be.ok;
|
expect(await fixture.readFile('/special-“characters” -in-file/index.html')).to.be.ok;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders the components top-down', async () => {
|
||||||
|
const html = await fixture.readFile('/order/index.html');
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
expect($('#rendered-order').text()).to.eq('Rendered order: A, B')
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Supports void elements whose name is a string (#2062)', async () => {
|
it('Supports void elements whose name is a string (#2062)', async () => {
|
||||||
|
|
7
packages/astro/test/fixtures/astro-basic/src/components/OrderA.astro
vendored
Normal file
7
packages/astro/test/fixtures/astro-basic/src/components/OrderA.astro
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
globalThis.__ASTRO_TEST_ORDER__ ??= []
|
||||||
|
globalThis.__ASTRO_TEST_ORDER__.push('A')
|
||||||
|
---
|
||||||
|
|
||||||
|
<p>A</p>
|
||||||
|
<slot />
|
7
packages/astro/test/fixtures/astro-basic/src/components/OrderB.astro
vendored
Normal file
7
packages/astro/test/fixtures/astro-basic/src/components/OrderB.astro
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
globalThis.__ASTRO_TEST_ORDER__ ??= []
|
||||||
|
globalThis.__ASTRO_TEST_ORDER__.push('B')
|
||||||
|
---
|
||||||
|
|
||||||
|
<p>B</p>
|
||||||
|
<slot />
|
1
packages/astro/test/fixtures/astro-basic/src/components/OrderLast.astro
vendored
Normal file
1
packages/astro/test/fixtures/astro-basic/src/components/OrderLast.astro
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<p id="rendered-order">Rendered order: {() => (globalThis.__ASTRO_TEST_ORDER__ ?? []).join(', ')}</p>
|
13
packages/astro/test/fixtures/astro-basic/src/pages/order.astro
vendored
Normal file
13
packages/astro/test/fixtures/astro-basic/src/pages/order.astro
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
import OrderA from "../components/OrderA.astro";
|
||||||
|
import OrderB from "../components/OrderB.astro";
|
||||||
|
import OrderLast from "../components/OrderLast.astro";
|
||||||
|
|
||||||
|
globalThis.__ASTRO_TEST_ORDER__ = [];
|
||||||
|
---
|
||||||
|
|
||||||
|
<OrderA>
|
||||||
|
<OrderB>
|
||||||
|
<OrderLast />
|
||||||
|
</OrderB>
|
||||||
|
</OrderA>
|
Loading…
Add table
Reference in a new issue