pull/16542/head
ComputerGuy 1 month ago
parent 8067841170
commit d92901edd1

@ -37,6 +37,7 @@ import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js';
import { ExportSpecifier } from './visitors/ExportSpecifier.js'; import { ExportSpecifier } from './visitors/ExportSpecifier.js';
import { ExpressionStatement } from './visitors/ExpressionStatement.js'; import { ExpressionStatement } from './visitors/ExpressionStatement.js';
import { ExpressionTag } from './visitors/ExpressionTag.js'; import { ExpressionTag } from './visitors/ExpressionTag.js';
import { Fragment } from './visitors/Fragment.js';
import { FunctionDeclaration } from './visitors/FunctionDeclaration.js'; import { FunctionDeclaration } from './visitors/FunctionDeclaration.js';
import { FunctionExpression } from './visitors/FunctionExpression.js'; import { FunctionExpression } from './visitors/FunctionExpression.js';
import { HtmlTag } from './visitors/HtmlTag.js'; import { HtmlTag } from './visitors/HtmlTag.js';
@ -156,6 +157,7 @@ const visitors = {
ExportSpecifier, ExportSpecifier,
ExpressionStatement, ExpressionStatement,
ExpressionTag, ExpressionTag,
Fragment,
FunctionDeclaration, FunctionDeclaration,
FunctionExpression, FunctionExpression,
HtmlTag, HtmlTag,
@ -300,6 +302,10 @@ export function analyze_module(source, options) {
function_depth: 0, function_depth: 0,
has_props_rune: false, has_props_rune: false,
options: /** @type {ValidatedCompileOptions} */ (options), options: /** @type {ValidatedCompileOptions} */ (options),
fragment: {
has_await: false,
node: null
},
parent_element: null, parent_element: null,
reactive_statement: null reactive_statement: null
}, },
@ -688,6 +694,10 @@ export function analyze_component(root, source, options) {
analysis, analysis,
options, options,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
fragment: {
has_await: false,
node: ast === template.ast ? template.ast : null
},
parent_element: null, parent_element: null,
has_props_rune: false, has_props_rune: false,
component_slots: new Set(), component_slots: new Set(),
@ -753,6 +763,10 @@ export function analyze_component(root, source, options) {
scopes, scopes,
analysis, analysis,
options, options,
fragment: {
has_await: false,
node: ast === template.ast ? template.ast : null
},
parent_element: null, parent_element: null,
has_props_rune: false, has_props_rune: false,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',

@ -8,6 +8,7 @@ export interface AnalysisState {
analysis: ComponentAnalysis; analysis: ComponentAnalysis;
options: ValidatedCompileOptions; options: ValidatedCompileOptions;
ast_type: 'instance' | 'template' | 'module'; ast_type: 'instance' | 'template' | 'module';
fragment: FragmentAnalysis;
/** /**
* Tag name of the parent element. `null` if the parent is `svelte:element`, `#snippet`, a component or the root. * Tag name of the parent element. `null` if the parent is `svelte:element`, `#snippet`, a component or the root.
* Parent doesn't necessarily mean direct path predecessor because there could be `#each`, `#if` etc in-between. * Parent doesn't necessarily mean direct path predecessor because there could be `#each`, `#if` etc in-between.
@ -28,6 +29,11 @@ export interface AnalysisState {
reactive_statement: null | ReactiveStatement; reactive_statement: null | ReactiveStatement;
} }
export interface FragmentAnalysis {
has_await: boolean;
node: AST.Fragment | null;
}
export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context< export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<
AST.SvelteNode, AST.SvelteNode,
State State

@ -11,6 +11,9 @@ export function AwaitExpression(node, context) {
if (context.state.expression) { if (context.state.expression) {
context.state.expression.has_await = true; context.state.expression.has_await = true;
if (context.state.fragment.node) {
context.state.fragment.has_await = true;
}
suspend = true; suspend = true;
} }

@ -0,0 +1,29 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types.js' */
/**
* @param {AST.Fragment} node
* @param {Context} context
*/
export function Fragment(node, context) {
const parent = /** @type {AST.TemplateNode} */ (context.path.at(-1));
if (
!parent ||
parent.type === 'Component' ||
parent.type === 'Root' ||
parent.type === 'IfBlock' ||
parent.type === 'KeyBlock' ||
parent.type === 'EachBlock' ||
parent.type === 'SnippetBlock' ||
parent.type === 'AwaitBlock'
) {
const fragment_metadata = {
has_await: false,
node
};
context.next({ ...context.state, fragment: fragment_metadata });
node.metadata.has_await = fragment_metadata.has_await;
} else {
context.next();
}
}

@ -170,6 +170,7 @@ export function client_component(analysis, options) {
// these are set inside the `Fragment` visitor, and cannot be used until then // these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null), init: /** @type {any} */ (null),
consts: /** @type {any} */ (null),
update: /** @type {any} */ (null), update: /** @type {any} */ (null),
after_update: /** @type {any} */ (null), after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null), template: /** @type {any} */ (null),

@ -6,7 +6,8 @@ import type {
Expression, Expression,
AssignmentExpression, AssignmentExpression,
UpdateExpression, UpdateExpression,
VariableDeclaration VariableDeclaration,
Declaration
} from 'estree'; } from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler'; import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js'; import type { TransformState } from '../types.js';
@ -57,6 +58,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly update: Statement[]; readonly update: Statement[];
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */ /** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[]; readonly after_update: Statement[];
/** Transformed `{#const }` declarations */
readonly consts: Statement[];
/** Memoized expressions */ /** Memoized expressions */
readonly memoizer: Memoizer; readonly memoizer: Memoizer;
/** The HTML template string */ /** The HTML template string */

@ -290,7 +290,19 @@ export function should_proxy(node, scope) {
* Svelte legacy mode should use safe equals in most places, runes mode shouldn't * Svelte legacy mode should use safe equals in most places, runes mode shouldn't
* @param {ComponentClientTransformState} state * @param {ComponentClientTransformState} state
* @param {Expression} arg * @param {Expression} arg
* @param {boolean} [async]
*/ */
export function create_derived(state, arg) { export function create_derived(state, arg, async = false) {
if (async) {
return b.call(
b.await(
b.call(
'$.save',
b.call('$.async_derived', arg.type === 'ArrowFunctionExpression' ? b.async(arg) : arg)
)
)
);
} else {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg); return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
}
} }

@ -15,7 +15,11 @@ export function AwaitExpression(node, context) {
// preserve context for // preserve context for
// a) top-level await and // a) top-level await and
// b) awaits that precede other expressions in template or `$derived(...)` // b) awaits that precede other expressions in template or `$derived(...)`
if (tla || (is_reactive_expression(context) && !is_last_evaluated_expression(context, node))) { if (
tla ||
(is_reactive_expression(context) &&
(!is_last_evaluated_expression(context, node) || context.path.at(-1)?.type === 'ConstTag'))
) {
return b.call(b.await(b.call('$.save', argument))); return b.call(b.await(b.call('$.save', argument)));
} }

@ -16,21 +16,29 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0]; const declaration = node.declaration.declarations[0];
// TODO we can almost certainly share some code with $derived(...) // TODO we can almost certainly share some code with $derived(...)
if (declaration.id.type === 'Identifier') { if (declaration.id.type === 'Identifier') {
const init = build_expression(context, declaration.init, node.metadata.expression); const init = build_expression(
let expression = create_derived(context.state, b.thunk(init)); { ...context, state: { ...context.state, in_derived: true } },
declaration.init,
node.metadata.expression
);
let expression = create_derived(
context.state,
b.thunk(init),
node.metadata.expression.has_await
);
if (dev) { if (dev) {
expression = b.call('$.tag', expression, b.literal(declaration.id.name)); expression = b.call('$.tag', expression, b.literal(declaration.id.name));
} }
context.state.init.push(b.const(declaration.id, expression)); context.state.consts.push(b.const(declaration.id, expression));
context.state.transform[declaration.id.name] = { read: get_value }; context.state.transform[declaration.id.name] = { read: get_value };
// we need to eagerly evaluate the expression in order to hit any // we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors // 'Cannot access x before initialization' errors
if (dev) { if (dev) {
context.state.init.push(b.stmt(b.call('$.get', declaration.id))); context.state.consts.push(b.stmt(b.call('$.get', declaration.id)));
} }
} else { } else {
const identifiers = extract_identifiers(declaration.id); const identifiers = extract_identifiers(declaration.id);
@ -44,7 +52,11 @@ export function ConstTag(node, context) {
delete transform[node.name]; delete transform[node.name];
} }
const child_state = { ...context.state, transform }; const child_state = /** @type {ComponentContext['state']} */ ({
...context.state,
transform,
in_derived: true
});
// TODO optimise the simple `{ x } = y` case — we can just return `y` // TODO optimise the simple `{ x } = y` case — we can just return `y`
// instead of destructuring it only to return a new object // instead of destructuring it only to return a new object
@ -61,18 +73,18 @@ export function ConstTag(node, context) {
]) ])
); );
let expression = create_derived(context.state, fn); let expression = create_derived(context.state, fn, node.metadata.expression.has_await);
if (dev) { if (dev) {
expression = b.call('$.tag', expression, b.literal('[@const]')); expression = b.call('$.tag', expression, b.literal('[@const]'));
} }
context.state.init.push(b.const(tmp, expression)); context.state.consts.push(b.const(tmp, expression));
// we need to eagerly evaluate the expression in order to hit any // we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors // 'Cannot access x before initialization' errors
if (dev) { if (dev) {
context.state.init.push(b.stmt(b.call('$.get', tmp))); context.state.consts.push(b.stmt(b.call('$.get', tmp)));
} }
for (const node of identifiers) { for (const node of identifiers) {

@ -48,8 +48,10 @@ export function Fragment(node, context) {
const is_single_child_not_needing_template = const is_single_child_not_needing_template =
trimmed.length === 1 && trimmed.length === 1 &&
(trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement'); (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 const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
const unsuspend = b.id('$$unsuspend');
/** @type {Statement[]} */ /** @type {Statement[]} */
const body = []; const body = [];
@ -61,6 +63,7 @@ export function Fragment(node, context) {
const state = { const state = {
...context.state, ...context.state,
init: [], init: [],
consts: [],
update: [], update: [],
after_update: [], after_update: [],
memoizer: new Memoizer(), memoizer: new Memoizer(),
@ -78,7 +81,7 @@ export function Fragment(node, context) {
if (is_text_first) { if (is_text_first) {
// skip over inserted comment // skip over inserted comment
body.push(b.stmt(b.call('$.next'))); state.init.unshift(b.stmt(b.call('$.next')));
} }
if (is_single_element) { if (is_single_element) {
@ -96,13 +99,13 @@ export function Fragment(node, context) {
const template = transform_template(state, namespace, flags); const template = transform_template(state, namespace, flags);
state.hoisted.push(b.var(template_name, template)); state.hoisted.push(b.var(template_name, template));
body.push(b.var(id, b.call(template_name))); state.init.unshift(b.var(id, b.call(template_name)));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else if (is_single_child_not_needing_template) { } else if (is_single_child_not_needing_template) {
context.visit(trimmed[0], state); context.visit(trimmed[0], state);
} else if (trimmed.length === 1 && trimmed[0].type === 'Text') { } else if (trimmed.length === 1 && trimmed[0].type === 'Text') {
const id = b.id(context.state.scope.generate('text')); const id = b.id(context.state.scope.generate('text'));
body.push(b.var(id, b.call('$.text', b.literal(trimmed[0].data)))); state.init.unshift(b.var(id, b.call('$.text', b.literal(trimmed[0].data))));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else if (trimmed.length > 0) { } else if (trimmed.length > 0) {
const id = b.id(context.state.scope.generate('fragment')); const id = b.id(context.state.scope.generate('fragment'));
@ -120,7 +123,7 @@ export function Fragment(node, context) {
state state
}); });
body.push(b.var(id, b.call('$.text'))); state.init.unshift(b.var(id, b.call('$.text')));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else { } else {
if (is_standalone) { if (is_standalone) {
@ -140,12 +143,12 @@ export function Fragment(node, context) {
if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') { if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') {
// special case — we can use `$.comment` instead of creating a unique template // special case — we can use `$.comment` instead of creating a unique template
body.push(b.var(id, b.call('$.comment'))); state.init.unshift(b.var(id, b.call('$.comment')));
} else { } else {
const template = transform_template(state, namespace, flags); const template = transform_template(state, namespace, flags);
state.hoisted.push(b.var(template_name, template)); state.hoisted.push(b.var(template_name, template));
body.push(b.var(id, b.call(template_name))); state.init.unshift(b.var(id, b.call(template_name)));
} }
close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
@ -153,6 +156,16 @@ export function Fragment(node, context) {
} }
} }
if (has_await) {
body.push(b.var(unsuspend, b.call('$.suspend')));
}
body.push(...state.consts);
if (has_await) {
body.push(b.if(b.call('$.aborted'), b.return()));
}
body.push(...state.init); body.push(...state.init);
if (state.update.length > 0) { if (state.update.length > 0) {
@ -168,5 +181,9 @@ export function Fragment(node, context) {
body.push(close); body.push(close);
} }
if (has_await) {
body.push(b.stmt(b.call(unsuspend)));
}
return b.block(body); return b.block(body);
} }

@ -14,6 +14,7 @@ export function SnippetBlock(node, context) {
// TODO hoist where possible // TODO hoist where possible
/** @type {(Identifier | AssignmentPattern)[]} */ /** @type {(Identifier | AssignmentPattern)[]} */
const args = [b.id('$$anchor')]; const args = [b.id('$$anchor')];
const has_await = node.body.metadata.has_await || false;
/** @type {BlockStatement} */ /** @type {BlockStatement} */
let body; let body;
@ -21,10 +22,6 @@ export function SnippetBlock(node, context) {
/** @type {Statement[]} */ /** @type {Statement[]} */
const declarations = []; const declarations = [];
if (dev) {
declarations.push(b.stmt(b.call('$.validate_snippet_args', b.spread(b.id('arguments')))));
}
const transform = { ...context.state.transform }; const transform = { ...context.state.transform };
const child_state = { ...context.state, transform }; const child_state = { ...context.state, transform };
@ -72,16 +69,21 @@ export function SnippetBlock(node, context) {
} }
} }
} }
const block = /** @type {BlockStatement} */ (context.visit(node.body, child_state)).body;
body = b.block([ body = b.block([
dev ? b.stmt(b.call('$.validate_snippet_args', b.spread(b.id('arguments')))) : b.empty,
...declarations, ...declarations,
.../** @type {BlockStatement} */ (context.visit(node.body, child_state)).body ...block
]); ]);
// in dev we use a FunctionExpression (not arrow function) so we can use `arguments` // in dev we use a FunctionExpression (not arrow function) so we can use `arguments`
let snippet = dev let snippet = dev
? b.call('$.wrap_snippet', b.id(context.state.analysis.name), b.function(null, args, body)) ? b.call(
: b.arrow(args, body); '$.wrap_snippet',
b.id(context.state.analysis.name),
has_await ? b.async(b.function(null, args, body)) : b.function(null, args, body)
)
: b.arrow(args, body, has_await);
const declaration = b.const(node.expression, snippet); const declaration = b.const(node.expression, snippet);

@ -19,6 +19,9 @@ import type {
} from 'estree'; } from 'estree';
import type { Scope } from '../phases/scope'; import type { Scope } from '../phases/scope';
import type { _CSS } from './css'; import type { _CSS } from './css';
import type { FragmentAnalysis } from '../phases/2-analyze/types';
type FragmentMetadata = Omit<FragmentAnalysis, 'node'>;
/** /**
* - `html` the default, for e.g. `<div>` or `<span>` * - `html` the default, for e.g. `<div>` or `<span>`
@ -45,7 +48,7 @@ export namespace AST {
type: 'Fragment'; type: 'Fragment';
nodes: Array<Text | Tag | ElementLike | Block | Comment>; nodes: Array<Text | Tag | ElementLike | Block | Comment>;
/** @internal */ /** @internal */
metadata: { metadata: Partial<FragmentMetadata> & {
/** /**
* Fragments declare their own scopes. A transparent fragment is one whose scope * Fragments declare their own scopes. A transparent fragment is one whose scope
* is not represented by a scope in the resulting JavaScript (e.g. an element scope), * is not represented by a scope in the resulting JavaScript (e.g. an element scope),

Loading…
Cancel
Save