Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/social-taxis-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: ensure async `@const` in boundary hydrates correctly
5 changes: 5 additions & 0 deletions .changeset/stale-items-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: parallelize async `@const`s in the template
3 changes: 1 addition & 2 deletions packages/svelte/src/compiler/phases/1-parse/utils/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ export function create_fragment(transparent = false) {
nodes: [],
metadata: {
transparent,
dynamic: false,
has_await: false
dynamic: false
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ export function AwaitExpression(node, context) {
if (context.state.expression) {
context.state.expression.has_await = true;

if (context.state.fragment && context.path.some((node) => node.type === 'ConstTag')) {
context.state.fragment.metadata.has_await = true;
}

suspend = true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly after_update: Statement[];
/** Transformed `{@const }` declarations */
readonly consts: Statement[];
/** Transformed async `{@const }` declarations (if any) and those coming after them */
async_consts?: {
id: Identifier;
thunks: Expression[];
};
/** Transformed `let:` directives */
readonly let_directives: Statement[];
/** Memoized expressions */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ export function ConstTag(node, context) {
expression = b.call('$.tag', expression, b.literal(declaration.id.name));
}

context.state.consts.push(b.const(declaration.id, expression));

context.state.transform[declaration.id.name] = { read: get_value };

// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (dev) {
context.state.consts.push(b.stmt(b.call('$.get', declaration.id)));
}
add_const_declaration(
context.state,
declaration.id,
expression,
node.metadata.expression.has_await,
context.state.scope.get_bindings(declaration)
);
} else {
const identifiers = extract_identifiers(declaration.id);
const tmp = b.id(context.state.scope.generate('computed_const'));
Expand Down Expand Up @@ -69,13 +69,13 @@ export function ConstTag(node, context) {
expression = b.call('$.tag', expression, b.literal('[@const]'));
}

context.state.consts.push(b.const(tmp, expression));

// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (dev) {
context.state.consts.push(b.stmt(b.call('$.get', tmp)));
}
add_const_declaration(
context.state,
tmp,
expression,
node.metadata.expression.has_await,
context.state.scope.get_bindings(declaration)
);

for (const node of identifiers) {
context.state.transform[node.name] = {
Expand All @@ -84,3 +84,39 @@ export function ConstTag(node, context) {
}
}
}

/**
* @param {ComponentContext['state']} state
* @param {import('estree').Identifier} id
* @param {import('estree').Expression} expression
* @param {boolean} has_await
* @param {import('#compiler').Binding[]} bindings
*/
function add_const_declaration(state, id, expression, has_await, bindings) {
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
const after = dev ? [b.stmt(b.call('$.get', id))] : [];

if (has_await || state.async_consts) {
const run = (state.async_consts ??= {
id: b.id(state.scope.generate('promises')),
thunks: []
});

state.consts.push(b.let(id));

const assignment = b.assignment('=', id, expression);
const body = after.length === 0 ? assignment : b.block([b.stmt(assignment), ...after]);

run.thunks.push(b.thunk(body, has_await));

const blocker = b.member(run.id, b.literal(run.thunks.length - 1), true);

for (const binding of bindings) {
binding.blocker = blocker;
}
} else {
state.consts.push(b.const(id, expression));
state.consts.push(...after);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ export function Fragment(node, context) {
const is_single_child_not_needing_template =
trimmed.length === 1 &&
(trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement');
const has_await = context.state.init !== null && (node.metadata.has_await || false);

const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent

/** @type {Statement[]} */
Expand All @@ -72,7 +70,8 @@ export function Fragment(node, context) {
metadata: {
namespace,
bound_contenteditable: context.state.metadata.bound_contenteditable
}
},
async_consts: undefined
};

for (const node of hoisted) {
Expand Down Expand Up @@ -153,8 +152,8 @@ export function Fragment(node, context) {

body.push(...state.let_directives, ...state.consts);

if (has_await) {
body.push(b.if(b.call('$.aborted'), b.return()));
if (state.async_consts && state.async_consts.thunks.length > 0) {
body.push(b.var(state.async_consts.id, b.call('$.run', b.array(state.async_consts.thunks))));
}

if (is_text_first) {
Expand All @@ -177,13 +176,5 @@ export function Fragment(node, context) {
body.push(close);
}

if (has_await) {
return b.block([
b.stmt(
b.call('$.async_body', b.id('$$anchor'), b.arrow([b.id('$$anchor')], b.block(body), true))
)
]);
} else {
return b.block(body);
}
return b.block(body);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ export function SnippetBlock(node, context) {
// TODO hoist where possible
/** @type {(Identifier | AssignmentPattern)[]} */
const args = [b.id('$$anchor')];
const has_await = node.body.metadata.has_await || false;

/** @type {BlockStatement} */
let body;

Expand Down Expand Up @@ -78,12 +76,8 @@ export function SnippetBlock(node, context) {

// in dev we use a FunctionExpression (not arrow function) so we can use `arguments`
let snippet = dev
? b.call(
'$.wrap_snippet',
b.id(context.state.analysis.name),
b.function(null, args, body, has_await)
)
: b.arrow(args, body, has_await);
? b.call('$.wrap_snippet', b.id(context.state.analysis.name), b.function(null, args, body))
: b.arrow(args, body);

const declaration = b.const(node.expression, snippet);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ export function SvelteBoundary(node, context) {
if (child.type === 'ConstTag') {
has_const = true;
if (!context.state.options.experimental.async) {
context.visit(child, { ...context.state, consts: const_tags });
context.visit(child, {
...context.state,
consts: const_tags,
scope: context.state.scopes.get(node.fragment) ?? context.state.scope
});
}
}
}
Expand Down Expand Up @@ -101,7 +105,13 @@ export function SvelteBoundary(node, context) {
nodes.push(child);
}

const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes }));
const block = /** @type {BlockStatement} */ (
context.visit(
{ ...node.fragment, nodes },
// Since we're creating a new fragment the reference in scopes can't match, so we gotta attach the right scope manually
{ ...context.state, scope: context.state.scopes.get(node.fragment) ?? context.state.scope }
)
);

if (!context.state.options.experimental.async) {
block.body.unshift(...const_tags);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export function process_children(nodes, initial, is_element, context) {
is_element &&
// In case it's wrapped in async the async logic will want to skip sibling nodes up until the end, hence we cannot make this controlled
// TODO switch this around and instead optimize for elements with a single block child and not require extra comments (neither for async nor normally)
!(node.body.metadata.has_await || node.metadata.expression.is_async())
!node.metadata.expression.is_async()
) {
node.metadata.is_controlled = true;
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { Expression, Statement, ModuleDeclaration, LabeledStatement } from 'estree';
import type {
Expression,
Statement,
ModuleDeclaration,
LabeledStatement,
Identifier
} from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
Expand All @@ -21,6 +27,11 @@ export interface ComponentServerTransformState extends ServerTransformState {
readonly namespace: Namespace;
readonly preserve_whitespace: boolean;
readonly skip_hydration_boundaries: boolean;
/** Transformed async `{@const }` declarations (if any) and those coming after them */
async_consts?: {
id: Identifier;
thunks: Expression[];
};
}

export type Context = import('zimmerframe').Context<AST.SvelteNode, ServerTransformState>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { extract_identifiers } from '../../../../utils/ast.js';

/**
* @param {AST.ConstTag} node
Expand All @@ -11,6 +12,29 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
const id = /** @type {Pattern} */ (context.visit(declaration.id));
const init = /** @type {Expression} */ (context.visit(declaration.init));
const has_await = node.metadata.expression.has_await;

context.state.init.push(b.const(id, init));
if (has_await || context.state.async_consts) {
const run = (context.state.async_consts ??= {
id: b.id(context.state.scope.generate('promises')),
thunks: []
});

const identifiers = extract_identifiers(declaration.id);
const bindings = context.state.scope.get_bindings(declaration);

for (const identifier of identifiers) {
context.state.init.push(b.let(identifier.name));
}

const assignment = b.assignment('=', id, init);
run.thunks.push(b.thunk(b.block([b.stmt(assignment)]), has_await));

const blocker = b.member(run.id, b.literal(run.thunks.length - 1), true);
for (const binding of bindings) {
binding.blocker = blocker;
}
} else {
context.state.init.push(b.const(id, init));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,7 @@ export function EachBlock(node, context) {

const new_body = /** @type {BlockStatement} */ (context.visit(node.body)).body;

if (node.body)
each.push(
// TODO get rid of fragment.has_await
...(node.body.metadata.has_await ? [create_async_block(b.block(new_body))] : new_body)
);
if (node.body) each.push(...new_body);

const for_loop = b.for(
b.declaration('let', [
Expand All @@ -61,7 +57,7 @@ export function EachBlock(node, context) {
b.if(
b.binary('!==', b.member(array_id, 'length'), b.literal(0)),
b.block([open, for_loop]),
node.fallback.metadata.has_await ? create_async_block(fallback) : fallback
fallback
)
);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export function Fragment(node, context) {
init: [],
template: [],
namespace,
skip_hydration_boundaries: is_standalone
skip_hydration_boundaries: is_standalone,
async_consts: undefined
};

for (const node of hoisted) {
Expand All @@ -42,5 +43,11 @@ export function Fragment(node, context) {

process_children(trimmed, { ...context, state });

if (state.async_consts && state.async_consts.thunks.length > 0) {
state.init.push(
b.var(state.async_consts.id, b.call('$$renderer.run', b.array(state.async_consts.thunks)))
);
}

return b.block([...state.init, ...build_template(state.template)]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,7 @@ export function IfBlock(node, context) {

const is_async = node.metadata.expression.is_async();

const has_await =
node.metadata.expression.has_await ||
// TODO get rid of this stuff
node.consequent.metadata.has_await ||
node.alternate?.metadata.has_await;
const has_await = node.metadata.expression.has_await;

if (is_async || has_await) {
statement = create_async_block(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
/** @import { ComponentContext } from '../types.js' */
import { dev } from '../../../../state.js';
import * as b from '#compiler/builders';
import { create_async_block } from './shared/utils.js';

/**
* @param {AST.SnippetBlock} node
Expand All @@ -16,10 +15,6 @@ export function SnippetBlock(node, context) {
/** @type {BlockStatement} */ (context.visit(node.body))
);

if (node.body.metadata.has_await) {
fn.body = b.block([create_async_block(fn.body)]);
}

// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true;

Expand Down
Loading
Loading