diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js index 8fc82b8905..a08ebe08cd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js @@ -9,22 +9,53 @@ import { empty_comment } from './shared/utils.js'; * @param {ComponentContext} context */ export function AwaitBlock(node, context) { + const then_block = node.then + ? /** @type {BlockStatement} */ (context.visit(node.then)) + : b.block([]); + if (node.value && node.then) { + then_block.body.unshift( + b.declaration('const', [ + b.declarator(b.assignment_pattern(b.id('$$payload'), b.id('$$async_payload'))) + ]) + ); + } + const catch_block = node.catch + ? /** @type {BlockStatement} */ (context.visit(node.catch)) + : b.block([]); + if (node.value && node.catch) { + catch_block.body.unshift( + b.declaration('const', [ + b.declarator(b.assignment_pattern(b.id('$$payload'), b.id('$$async_payload'))) + ]) + ); + } context.state.template.push( empty_comment, b.stmt( b.call( '$.await', + b.id('$$payload'), /** @type {Expression} */ (context.visit(node.expression)), b.thunk( node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([]) ), b.arrow( - node.value ? [/** @type {Pattern} */ (context.visit(node.value))] : [], - node.then ? /** @type {BlockStatement} */ (context.visit(node.then)) : b.block([]) + node.value + ? [ + /** @type {Pattern} */ (context.visit(node.value)), + b.assignment_pattern(b.id('$$async_payload'), b.id('$$payload')) + ] + : [], + then_block ), b.arrow( - node.error ? [/** @type {Pattern} */ (context.visit(node.error))] : [], - node.catch ? /** @type {BlockStatement} */ (context.visit(node.catch)) : b.block([]) + node.error + ? [ + /** @type {Pattern} */ (context.visit(node.error)), + b.assignment_pattern(b.id('$$payload'), b.id('$$payload')) + ] + : [], + catch_block ) ) ), diff --git a/packages/svelte/src/internal/server/hydration.js b/packages/svelte/src/internal/server/hydration.js index bc34f267bc..47a3f69e64 100644 --- a/packages/svelte/src/internal/server/hydration.js +++ b/packages/svelte/src/internal/server/hydration.js @@ -6,7 +6,7 @@ const BLOCK_OPEN_ELSE = ``; const BLOCK_CLOSE = ``; const EMPTY_COMMENT = ``; -let hydratable = true; +export let hydratable = true; /** * @param {string} [hash] diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index d2a8d98ae3..9d205f1526 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -17,7 +17,7 @@ import { is_boolean_attribute, is_void } from '../../utils.js'; import { validate_store } from '../shared/validate.js'; import { current_component, pop, push } from './context.js'; import { reset_elements } from './dev.js'; -import { close, empty, open, open_else, set_hydratable } from './hydration.js'; +import { close, empty, hydratable, open, open_else, set_hydratable } from './hydration.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://infra.spec.whatwg.org/#noncharacter @@ -31,14 +31,16 @@ const RAW_TEXT_ELEMENTS = ['textarea', 'script', 'style', 'title']; * @param {Payload} to_copy * @returns {Payload} */ -export function copy_payload({ out, css, head }) { +export function copy_payload({ out, css, head, async, current_async_level }) { return { out, css: new Set(css), head: { title: head.title, out: head.out - } + }, + async: async ? [...async] : [], + current_async_level }; } @@ -92,11 +94,16 @@ export let on_destroy = []; * @template {Record} Props * @param {import('svelte').Component | ComponentType>} component * @param {{ props?: Omit; context?: Map }} [options] - * @returns {RenderOutput} + * @returns {Payload} */ -export function render(component, options = {}) { +function render_payload(component, options = {}) { /** @type {Payload} */ - const payload = { out: '', css: new Set(), head: { title: '', out: '' } }; + const payload = { + out: '', + css: new Set(), + head: { title: '', out: '' }, + current_async_level: '' + }; const prev_on_destroy = on_destroy; on_destroy = []; @@ -128,6 +135,19 @@ export function render(component, options = {}) { payload.out += close(); for (const cleanup of on_destroy) cleanup(); on_destroy = prev_on_destroy; + return payload; +} + +/** + * Only available on the server and when compiling with the `server` option. + * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. + * @template {Record} Props + * @param {import('svelte').Component | ComponentType>} component + * @param {{ props?: Omit; context?: Map }} [options] + * @returns {RenderOutput} + */ +export function render(component, options = {}) { + const payload = render_payload(component, options); let head = payload.head.out + payload.head.title; @@ -149,18 +169,33 @@ export function render(component, options = {}) { * @template {Record} Props * @param {import('svelte').Component | ComponentType>} component * @param {{ props?: Omit; context?: Map }} [options] - * @returns {RenderOutput} + * @returns {Promise} */ -export function renderStaticHTML(component, options) { +export async function renderStaticHTML(component, options) { set_hydratable(false); let payload; try { - payload = render(component, options); + payload = render_payload(component, options); + if (payload.async) { + for (let async_fn of payload.async) { + await async_fn(); + } + } + + let head = payload.head.out + payload.head.title; + + for (const { hash, code } of payload.css) { + head += ``; + } + + return { + head, + html: payload.out, + body: payload.out + }; } finally { set_hydratable(true); } - - return payload; } /** @@ -493,16 +528,58 @@ export function bind_props(props_parent, props_now) { /** * @template V + * @param {Payload} $$payload * @param {Promise} promise * @param {null | (() => void)} pending_fn - * @param {(value: V) => void} then_fn + * @param {(value: V, $$payload?: Payload) => void} then_fn + * @param {(error: any, $$payload?: Payload) => void} [catch_fn] * @returns {void} */ -function await_block(promise, pending_fn, then_fn) { +function await_block($$payload, promise, pending_fn, then_fn, catch_fn) { if (is_promise(promise)) { promise.then(null, noop); - if (pending_fn !== null) { - pending_fn(); + if (!hydratable) { + let level = $$payload.current_async_level + ($$payload.async?.length ?? 0); + const replace_marker = '\ufff0\ufff0\ufff0' + level; + $$payload.out += replace_marker; + ($$payload.async ??= []).push(async () => { + /** + * @type {Payload} + */ + const new_payload = { + css: new Set(), + current_async_level: level + '.', + head: { + out: '', + title: '' + }, + out: '', + async: [] + }; + try { + const result = await promise; + then_fn(result, new_payload); + } catch (e) { + if (catch_fn) { + catch_fn(e, new_payload); + } + } + if ($$payload.async && new_payload.async) { + for (let async_replace of new_payload.async) { + await async_replace(); + } + } + $$payload.out = $$payload.out.replace(replace_marker, new_payload.out); + $$payload.head.out = $$payload.head.out.replace(replace_marker, new_payload.head.out); + $$payload.head.title = $$payload.head.title.replace(replace_marker, new_payload.head.title); + for (let css_part of new_payload.css) { + $$payload.css.add(css_part); + } + }); + } else { + if (pending_fn !== null) { + pending_fn(); + } } } else if (then_fn !== null) { then_fn(promise); diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts index e6c235147b..0576826d1f 100644 --- a/packages/svelte/src/internal/server/types.d.ts +++ b/packages/svelte/src/internal/server/types.d.ts @@ -18,6 +18,8 @@ export interface Payload { title: string; out: string; }; + async?: Array<() => Promise>; + current_async_level: string; } export interface RenderOutput { diff --git a/packages/svelte/src/server/index.d.ts b/packages/svelte/src/server/index.d.ts index e3d81838b8..3dcd89713d 100644 --- a/packages/svelte/src/server/index.d.ts +++ b/packages/svelte/src/server/index.d.ts @@ -38,4 +38,4 @@ export function renderStaticHTML< component: Comp extends SvelteComponent ? ComponentType : Comp, options: { props: Omit; context?: Map } ] -): RenderOutput; +): Promise; diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index b14c0bdf4b..4ae0f6e748 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -272,7 +272,9 @@ async function run_test_variant( if (variant === 'hydrate' || variant === 'ssr') { config.before_test?.(); // ssr into target - const SsrSvelteComponent = (await import(`${cwd}/_output/server/main.svelte.js`)).default; + const SsrSvelteComponent = ( + await import(`${cwd}/_output/server/main.svelte.js`).catch((e) => if(cwd.includes("transition-js-await-block")){ console.log(e.stack)}) + ).default; const { html, head } = render(SsrSvelteComponent, { props: config.server_props ?? config.props ?? {} }); diff --git a/packages/svelte/tests/server-side-rendering/samples/static-true/Component.svelte b/packages/svelte/tests/server-side-rendering/samples/hydratable-false/Component.svelte similarity index 100% rename from packages/svelte/tests/server-side-rendering/samples/static-true/Component.svelte rename to packages/svelte/tests/server-side-rendering/samples/hydratable-false/Component.svelte diff --git a/packages/svelte/tests/server-side-rendering/samples/static-true/_config.js b/packages/svelte/tests/server-side-rendering/samples/hydratable-false/_config.js similarity index 100% rename from packages/svelte/tests/server-side-rendering/samples/static-true/_config.js rename to packages/svelte/tests/server-side-rendering/samples/hydratable-false/_config.js diff --git a/packages/svelte/tests/server-side-rendering/samples/hydratable-false/_expected.html b/packages/svelte/tests/server-side-rendering/samples/hydratable-false/_expected.html new file mode 100644 index 0000000000..656cb88025 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/hydratable-false/_expected.html @@ -0,0 +1 @@ +if 10 1000 nested nested multiple multiple nested nested nested cool cool \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/hydratable-false/main.svelte b/packages/svelte/tests/server-side-rendering/samples/hydratable-false/main.svelte new file mode 100644 index 0000000000..0403d3df5a --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/hydratable-false/main.svelte @@ -0,0 +1,46 @@ + + + +{#if true} + if +{/if} + +{#each [] as i} + {i} +{/each} + +{#await Promise.resolve(10) then x} + {x} +{/await} + +{#await new Promise((res)=>setTimeout(res,1000,1000)) then x} + {x} + {#await Promise.resolve("nested") then value} + {value} + {/await} + + {#await Promise.resolve("nested multiple") then value} + {value} + {/await} + + {#await Promise.resolve("multiple nested") then value} + {value} + {#await Promise.resolve("nested nested") then value} + {value} + {/await} + {/await} +{/await} + +{#key true} + cool +{/key} + +{#snippet to_render()} + cool +{/snippet} + +{@render to_render()} + + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/static-true/_expected.html b/packages/svelte/tests/server-side-rendering/samples/static-true/_expected.html deleted file mode 100644 index aec87597e5..0000000000 --- a/packages/svelte/tests/server-side-rendering/samples/static-true/_expected.html +++ /dev/null @@ -1 +0,0 @@ -if cool cool \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/static-true/main.svelte b/packages/svelte/tests/server-side-rendering/samples/static-true/main.svelte deleted file mode 100644 index d71e423044..0000000000 --- a/packages/svelte/tests/server-side-rendering/samples/static-true/main.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - - -{#if true} - if -{/if} - -{#each [] as i} - {i} -{/each} - -{#await Promise.resolve() then x} - {x} -{/await} - -{#key true} - cool -{/key} - -{#snippet to_render()} - cool -{/snippet} - -{@render to_render()} - - \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/test.ts b/packages/svelte/tests/server-side-rendering/test.ts index 199022927a..14bf17cb65 100644 --- a/packages/svelte/tests/server-side-rendering/test.ts +++ b/packages/svelte/tests/server-side-rendering/test.ts @@ -35,7 +35,7 @@ const { test, run } = suite(async (config, test_dir) => { const Component = (await import(`${test_dir}/_output/server/main.svelte.js`)).default; const expected_html = try_read_file(`${test_dir}/_expected.html`); let fn = config.static === true ? renderStaticHTML : render; - const rendered = fn(Component, { props: config.props || {} }); + const rendered = await fn(Component, { props: config.props || {} } as any); const { body, head } = rendered; fs.writeFileSync(`${test_dir}/_output/rendered.html`, body); diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js index bb64227f4e..38b3a23534 100644 --- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js @@ -9,6 +9,16 @@ export default function Await_block_scope($$payload) { } $$payload.out += ` ${$.empty()}`; - $.await(promise, () => {}, (counter) => {}, () => {}); + + $.await( + $$payload, + promise, + () => {}, + (counter, $$async_payload = $$payload) => { + const $$payload = $$async_payload; + }, + () => {} + ); + $$payload.out += `${$.empty()} ${$.escape(counter.count)}`; } \ No newline at end of file