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

@ -8,6 +8,7 @@ export interface AnalysisState {
analysis: ComponentAnalysis;
options: ValidatedCompileOptions;
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.
* 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;
}
export interface FragmentAnalysis {
has_await: boolean;
node: AST.Fragment | null;
}
export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<
AST.SvelteNode,
State

@ -11,6 +11,9 @@ export function AwaitExpression(node, context) {
if (context.state.expression) {
context.state.expression.has_await = true;
if (context.state.fragment.node) {
context.state.fragment.has_await = 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
init: /** @type {any} */ (null),
consts: /** @type {any} */ (null),
update: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null),

@ -6,7 +6,8 @@ import type {
Expression,
AssignmentExpression,
UpdateExpression,
VariableDeclaration
VariableDeclaration,
Declaration
} from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
@ -57,6 +58,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly update: Statement[];
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[];
/** Transformed `{#const }` declarations */
readonly consts: Statement[];
/** Memoized expressions */
readonly memoizer: Memoizer;
/** 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
* @param {ComponentClientTransformState} state
* @param {Expression} arg
* @param {boolean} [async]
*/
export function create_derived(state, arg) {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', 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);
}
}

@ -15,7 +15,11 @@ export function AwaitExpression(node, context) {
// preserve context for
// a) top-level await and
// 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)));
}

@ -16,21 +16,29 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
// TODO we can almost certainly share some code with $derived(...)
if (declaration.id.type === 'Identifier') {
const init = build_expression(context, declaration.init, node.metadata.expression);
let expression = create_derived(context.state, b.thunk(init));
const init = build_expression(
{ ...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) {
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 };
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
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 {
const identifiers = extract_identifiers(declaration.id);
@ -44,7 +52,11 @@ export function ConstTag(node, context) {
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`
// 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) {
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
// 'Cannot access x before initialization' errors
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) {

@ -48,8 +48,10 @@ 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
const unsuspend = b.id('$$unsuspend');
/** @type {Statement[]} */
const body = [];
@ -61,6 +63,7 @@ export function Fragment(node, context) {
const state = {
...context.state,
init: [],
consts: [],
update: [],
after_update: [],
memoizer: new Memoizer(),
@ -78,7 +81,7 @@ export function Fragment(node, context) {
if (is_text_first) {
// skip over inserted comment
body.push(b.stmt(b.call('$.next')));
state.init.unshift(b.stmt(b.call('$.next')));
}
if (is_single_element) {
@ -96,13 +99,13 @@ export function Fragment(node, context) {
const template = transform_template(state, namespace, flags);
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));
} else if (is_single_child_not_needing_template) {
context.visit(trimmed[0], state);
} else if (trimmed.length === 1 && trimmed[0].type === '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));
} else if (trimmed.length > 0) {
const id = b.id(context.state.scope.generate('fragment'));
@ -120,7 +123,7 @@ export function Fragment(node, context) {
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));
} else {
if (is_standalone) {
@ -140,12 +143,12 @@ export function Fragment(node, context) {
if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') {
// 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 {
const template = transform_template(state, namespace, flags);
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));
@ -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);
if (state.update.length > 0) {
@ -168,5 +181,9 @@ export function Fragment(node, context) {
body.push(close);
}
if (has_await) {
body.push(b.stmt(b.call(unsuspend)));
}
return b.block(body);
}

@ -14,6 +14,7 @@ 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;
@ -21,10 +22,6 @@ export function SnippetBlock(node, context) {
/** @type {Statement[]} */
const declarations = [];
if (dev) {
declarations.push(b.stmt(b.call('$.validate_snippet_args', b.spread(b.id('arguments')))));
}
const transform = { ...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([
dev ? b.stmt(b.call('$.validate_snippet_args', b.spread(b.id('arguments')))) : b.empty,
...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`
let snippet = dev
? b.call('$.wrap_snippet', b.id(context.state.analysis.name), b.function(null, args, body))
: b.arrow(args, body);
? b.call(
'$.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);

@ -19,6 +19,9 @@ import type {
} from 'estree';
import type { Scope } from '../phases/scope';
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>`
@ -45,7 +48,7 @@ export namespace AST {
type: 'Fragment';
nodes: Array<Text | Tag | ElementLike | Block | Comment>;
/** @internal */
metadata: {
metadata: Partial<FragmentMetadata> & {
/**
* 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),

Loading…
Cancel
Save