Skip to content

Commit de48a77

Browse files
feat: hoisting
1 parent 64f9110 commit de48a77

File tree

42 files changed

+442
-35
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+442
-35
lines changed

packages/svelte/src/compiler/phases/1-parse/state/element.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ export default function element(parser) {
177177
mathml: false,
178178
scoped: false,
179179
has_spread: false,
180-
path: []
180+
path: [],
181+
synthetic_value_node: null
181182
}
182183
}
183184
: /** @type {AST.ElementLike} */ ({

packages/svelte/src/compiler/phases/1-parse/utils/create.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ export function create_fragment(transparent = false) {
1111
metadata: {
1212
transparent,
1313
dynamic: false,
14-
has_await: false
14+
has_await: false,
15+
// name is added later, after we've done scope analysis
16+
hoisted_promises: { name: '', promises: [] }
1517
}
1618
};
1719
}

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ const visitors = {
131131
ignore_map.set(node, structuredClone(ignore_stack));
132132

133133
const scope = state.scopes.get(node);
134+
if (node.type === 'Fragment') {
135+
node.metadata.hoisted_promises.name = state.scope.generate('promises');
136+
}
134137
next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state);
135138

136139
if (ignores.length > 0) {
@@ -307,7 +310,8 @@ export function analyze_module(source, options) {
307310
title: null,
308311
boundary: null,
309312
parent_element: null,
310-
reactive_statement: null
313+
reactive_statement: null,
314+
async_hoist_boundary: null
311315
},
312316
visitors
313317
);
@@ -535,7 +539,8 @@ export function analyze_component(root, source, options) {
535539
snippet_renderers: new Map(),
536540
snippets: new Set(),
537541
async_deriveds: new Set(),
538-
has_blocking_await: false
542+
has_blocking_await: false,
543+
hoisted_promises: new Map()
539544
};
540545

541546
state.adjust({
@@ -704,7 +709,8 @@ export function analyze_component(root, source, options) {
704709
expression: null,
705710
state_fields: new Map(),
706711
function_depth: scope.function_depth,
707-
reactive_statement: null
712+
reactive_statement: null,
713+
async_hoist_boundary: ast === template.ast ? ast : null
708714
};
709715

710716
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
@@ -774,7 +780,8 @@ export function analyze_component(root, source, options) {
774780
component_slots: new Set(),
775781
expression: null,
776782
state_fields: new Map(),
777-
function_depth: scope.function_depth
783+
function_depth: scope.function_depth,
784+
async_hoist_boundary: ast === template.ast ? ast : null
778785
};
779786

780787
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);

packages/svelte/src/compiler/phases/2-analyze/types.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ export interface AnalysisState {
1212
snippet: AST.SnippetBlock | null;
1313
title: AST.TitleElement | null;
1414
boundary: AST.SvelteBoundary | null;
15+
/**
16+
* The "anchor" fragment for any hoisted promises. This is the root fragment when
17+
* walking starts and until another boundary fragment is encountered, like a
18+
* consequent or alternate of an `#if` or `#each` block. When this fragment is emitted
19+
* during server transformation, the promise expressions will be hoisted out of the fragment
20+
* and placed right above it in an array.
21+
*/
22+
async_hoist_boundary: AST.Fragment | null;
1523
/**
1624
* Tag name of the parent element. `null` if the parent is `svelte:element`, `#snippet`, a component or the root.
1725
* Parent doesn't necessarily mean direct path predecessor because there could be `#each`, `#if` etc in-between.

packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,25 @@ export function AwaitBlock(node, context) {
4141

4242
mark_subtree_dynamic(context.path);
4343

44+
// this one doesn't get the new state because it still hoists to the existing scope
4445
context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
45-
if (node.pending) context.visit(node.pending);
46-
if (node.then) context.visit(node.then);
47-
if (node.catch) context.visit(node.catch);
46+
47+
if (node.pending) {
48+
context.visit(node.pending, {
49+
...context.state,
50+
async_hoist_boundary: node.pending
51+
});
52+
}
53+
if (node.then) {
54+
context.visit(node.then, {
55+
...context.state,
56+
async_hoist_boundary: node.then
57+
});
58+
}
59+
if (node.catch) {
60+
context.visit(node.catch, {
61+
...context.state,
62+
async_hoist_boundary: node.catch
63+
});
64+
}
4865
}

packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** @import { AwaitExpression } from 'estree' */
22
/** @import { Context } from '../types' */
33
import * as e from '../../../errors.js';
4+
import * as b from '#compiler/builders';
45

56
/**
67
* @param {AwaitExpression} node
@@ -19,8 +20,21 @@ export function AwaitExpression(node, context) {
1920
suspend = true;
2021
}
2122

22-
if (context.state.snippet) {
23-
context.state.snippet.metadata.has_await = true;
23+
// Only set has_await on the boundary when we're in a template expression context
24+
// (not in event handlers or other non-template contexts)
25+
if (context.state.async_hoist_boundary && context.state.expression) {
26+
context.state.async_hoist_boundary.metadata.is_async = true;
27+
const len = context.state.async_hoist_boundary.metadata.hoisted_promises.promises.push(
28+
node.argument
29+
);
30+
context.state.analysis.hoisted_promises.set(
31+
node.argument,
32+
b.member(
33+
b.id(context.state.async_hoist_boundary.metadata.hoisted_promises.name),
34+
b.literal(len - 1),
35+
true
36+
)
37+
);
2438
}
2539

2640
if (context.state.title) {

packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,17 @@ export function EachBlock(node, context) {
3535
scope: /** @type {Scope} */ (context.state.scope.parent)
3636
});
3737

38-
context.visit(node.body);
38+
context.visit(node.body, {
39+
...context.state,
40+
async_hoist_boundary: node.body
41+
});
3942
if (node.key) context.visit(node.key);
40-
if (node.fallback) context.visit(node.fallback);
43+
if (node.fallback) {
44+
context.visit(node.fallback, {
45+
...context.state,
46+
async_hoist_boundary: node.fallback
47+
});
48+
}
4149

4250
if (!context.state.analysis.runes) {
4351
let mutated =

packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ export function IfBlock(node, context) {
2222
expression: node.metadata.expression
2323
});
2424

25-
context.visit(node.consequent);
26-
if (node.alternate) context.visit(node.alternate);
25+
context.visit(node.consequent, {
26+
...context.state,
27+
async_hoist_boundary: node.consequent
28+
});
29+
if (node.alternate) {
30+
context.visit(node.alternate, {
31+
...context.state,
32+
async_hoist_boundary: node.alternate
33+
});
34+
}
2735
}

packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,9 @@ export function KeyBlock(node, context) {
1717
mark_subtree_dynamic(context.path);
1818

1919
context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
20-
context.visit(node.fragment);
20+
21+
context.visit(node.fragment, {
22+
...context.state,
23+
async_hoist_boundary: node.fragment
24+
});
2125
}

packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ export function SnippetBlock(node, context) {
2323
}
2424
}
2525

26-
context.next({ ...context.state, parent_element: null, snippet: node });
26+
context.next({
27+
...context.state,
28+
parent_element: null,
29+
snippet: node,
30+
async_hoist_boundary: node.body
31+
});
2732

2833
const can_hoist =
2934
context.path.length === 1 &&

0 commit comments

Comments
 (0)