feat: await async blocks if render static

hydratable-flag
paoloricciuti 10 months ago
parent e9d38b52ce
commit 0b2b980eae

@ -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
)
)
),

@ -6,7 +6,7 @@ const BLOCK_OPEN_ELSE = `<!--${HYDRATION_START_ELSE}-->`;
const BLOCK_CLOSE = `<!--${HYDRATION_END}-->`;
const EMPTY_COMMENT = `<!---->`;
let hydratable = true;
export let hydratable = true;
/**
* @param {string} [hash]

@ -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<string, any>} Props
* @param {import('svelte').Component<Props> | ComponentType<SvelteComponent<Props>>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any> }} [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<string, any>} Props
* @param {import('svelte').Component<Props> | ComponentType<SvelteComponent<Props>>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any> }} [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<string, any>} Props
* @param {import('svelte').Component<Props> | ComponentType<SvelteComponent<Props>>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any> }} [options]
* @returns {RenderOutput}
* @returns {Promise<RenderOutput>}
*/
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 += `<style id="${hash}">${code}</style>`;
}
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<V>} 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);

@ -18,6 +18,8 @@ export interface Payload {
title: string;
out: string;
};
async?: Array<() => Promise<void>>;
current_async_level: string;
}
export interface RenderOutput {

@ -38,4 +38,4 @@ export function renderStaticHTML<
component: Comp extends SvelteComponent<any> ? ComponentType<Comp> : Comp,
options: { props: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any> }
]
): RenderOutput;
): Promise<RenderOutput>;

@ -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 ?? {}
});

@ -0,0 +1 @@
if 10 1000 nested nested multiple multiple nested nested nested cool cool

@ -0,0 +1,46 @@
<script lang="ts">
import Component from "./Component.svelte";
</script>
{#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()}
<Component />

@ -1,28 +0,0 @@
<script lang="ts">
import Component from "./Component.svelte";
</script>
{#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()}
<Component />

@ -35,7 +35,7 @@ const { test, run } = suite<SSRTest>(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);

@ -9,6 +9,16 @@ export default function Await_block_scope($$payload) {
}
$$payload.out += `<button>clicks: ${$.escape(counter.count)}</button> ${$.empty()}`;
$.await(promise, () => {}, (counter) => {}, () => {});
$.await(
$$payload,
promise,
() => {},
(counter, $$async_payload = $$payload) => {
const $$payload = $$async_payload;
},
() => {}
);
$$payload.out += `${$.empty()} ${$.escape(counter.count)}`;
}
Loading…
Cancel
Save