feat: hoisting

pull/16762/head
S. Elliott Johnson 2 weeks ago
parent 64f91100de
commit de48a77cb2

@ -177,7 +177,8 @@ export default function element(parser) {
mathml: false,
scoped: false,
has_spread: false,
path: []
path: [],
synthetic_value_node: null
}
}
: /** @type {AST.ElementLike} */ ({

@ -11,7 +11,9 @@ export function create_fragment(transparent = false) {
metadata: {
transparent,
dynamic: false,
has_await: false
has_await: false,
// name is added later, after we've done scope analysis
hoisted_promises: { name: '', promises: [] }
}
};
}

@ -131,6 +131,9 @@ const visitors = {
ignore_map.set(node, structuredClone(ignore_stack));
const scope = state.scopes.get(node);
if (node.type === 'Fragment') {
node.metadata.hoisted_promises.name = state.scope.generate('promises');
}
next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state);
if (ignores.length > 0) {
@ -307,7 +310,8 @@ export function analyze_module(source, options) {
title: null,
boundary: null,
parent_element: null,
reactive_statement: null
reactive_statement: null,
async_hoist_boundary: null
},
visitors
);
@ -535,7 +539,8 @@ export function analyze_component(root, source, options) {
snippet_renderers: new Map(),
snippets: new Set(),
async_deriveds: new Set(),
has_blocking_await: false
has_blocking_await: false,
hoisted_promises: new Map()
};
state.adjust({
@ -704,7 +709,8 @@ export function analyze_component(root, source, options) {
expression: null,
state_fields: new Map(),
function_depth: scope.function_depth,
reactive_statement: null
reactive_statement: null,
async_hoist_boundary: ast === template.ast ? ast : null
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
@ -774,7 +780,8 @@ export function analyze_component(root, source, options) {
component_slots: new Set(),
expression: null,
state_fields: new Map(),
function_depth: scope.function_depth
function_depth: scope.function_depth,
async_hoist_boundary: ast === template.ast ? ast : null
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);

@ -12,6 +12,14 @@ export interface AnalysisState {
snippet: AST.SnippetBlock | null;
title: AST.TitleElement | null;
boundary: AST.SvelteBoundary | null;
/**
* The "anchor" fragment for any hoisted promises. This is the root fragment when
* walking starts and until another boundary fragment is encountered, like a
* consequent or alternate of an `#if` or `#each` block. When this fragment is emitted
* during server transformation, the promise expressions will be hoisted out of the fragment
* and placed right above it in an array.
*/
async_hoist_boundary: AST.Fragment | null;
/**
* 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.

@ -41,8 +41,25 @@ export function AwaitBlock(node, context) {
mark_subtree_dynamic(context.path);
// this one doesn't get the new state because it still hoists to the existing scope
context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
if (node.pending) context.visit(node.pending);
if (node.then) context.visit(node.then);
if (node.catch) context.visit(node.catch);
if (node.pending) {
context.visit(node.pending, {
...context.state,
async_hoist_boundary: node.pending
});
}
if (node.then) {
context.visit(node.then, {
...context.state,
async_hoist_boundary: node.then
});
}
if (node.catch) {
context.visit(node.catch, {
...context.state,
async_hoist_boundary: node.catch
});
}
}

@ -1,6 +1,7 @@
/** @import { AwaitExpression } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import * as b from '#compiler/builders';
/**
* @param {AwaitExpression} node
@ -19,8 +20,21 @@ export function AwaitExpression(node, context) {
suspend = true;
}
if (context.state.snippet) {
context.state.snippet.metadata.has_await = true;
// Only set has_await on the boundary when we're in a template expression context
// (not in event handlers or other non-template contexts)
if (context.state.async_hoist_boundary && context.state.expression) {
context.state.async_hoist_boundary.metadata.is_async = true;
const len = context.state.async_hoist_boundary.metadata.hoisted_promises.promises.push(
node.argument
);
context.state.analysis.hoisted_promises.set(
node.argument,
b.member(
b.id(context.state.async_hoist_boundary.metadata.hoisted_promises.name),
b.literal(len - 1),
true
)
);
}
if (context.state.title) {

@ -35,9 +35,17 @@ export function EachBlock(node, context) {
scope: /** @type {Scope} */ (context.state.scope.parent)
});
context.visit(node.body);
context.visit(node.body, {
...context.state,
async_hoist_boundary: node.body
});
if (node.key) context.visit(node.key);
if (node.fallback) context.visit(node.fallback);
if (node.fallback) {
context.visit(node.fallback, {
...context.state,
async_hoist_boundary: node.fallback
});
}
if (!context.state.analysis.runes) {
let mutated =

@ -22,6 +22,14 @@ export function IfBlock(node, context) {
expression: node.metadata.expression
});
context.visit(node.consequent);
if (node.alternate) context.visit(node.alternate);
context.visit(node.consequent, {
...context.state,
async_hoist_boundary: node.consequent
});
if (node.alternate) {
context.visit(node.alternate, {
...context.state,
async_hoist_boundary: node.alternate
});
}
}

@ -17,5 +17,9 @@ export function KeyBlock(node, context) {
mark_subtree_dynamic(context.path);
context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
context.visit(node.fragment);
context.visit(node.fragment, {
...context.state,
async_hoist_boundary: node.fragment
});
}

@ -23,7 +23,12 @@ export function SnippetBlock(node, context) {
}
}
context.next({ ...context.state, parent_element: null, snippet: node });
context.next({
...context.state,
parent_element: null,
snippet: node,
async_hoist_boundary: node.body
});
const can_hoist =
context.path.length === 1 &&

@ -34,5 +34,9 @@ export function SvelteBoundary(node, context) {
)
) ?? null;
context.next({ ...context.state, boundary: node });
context.next({
...context.state,
boundary: node,
async_hoist_boundary: node.fragment
});
}

@ -10,6 +10,7 @@ import { dev, filename } from '../../../state.js';
import { render_stylesheet } from '../css/index.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { AwaitExpression } from './visitors/AwaitExpression.js';
import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.js';
import { Component } from './visitors/Component.js';
@ -57,6 +58,7 @@ const global_visitors = {
/** @type {ComponentVisitors} */
const template_visitors = {
AwaitExpression,
AwaitBlock,
Component,
ConstTag,

@ -0,0 +1,14 @@
/** @import { AwaitExpression } from 'estree' */
/** @import { ComponentContext } from '../types.js' */
/**
* This is only registered for components, currently.
* @param {AwaitExpression} node
* @param {ComponentContext} context
*/
export function AwaitExpression(node, context) {
const hoisted = context.state.analysis.hoisted_promises.get(node.argument);
if (hoisted) {
node.argument = hoisted;
}
}

@ -42,5 +42,25 @@ export function Fragment(node, context) {
process_children(trimmed, { ...context, state });
if (node.metadata.hoisted_promises.promises.length > 0) {
return b.block([
b.const(
node.metadata.hoisted_promises.name,
b.array(node.metadata.hoisted_promises.promises)
),
...state.init,
b.stmt(
b.call(
'$$payload.child',
b.arrow(
[b.id('$$payload')],
b.block(build_template(state.template)),
node.metadata.is_async
)
)
)
]);
}
return b.block([...state.init, ...build_template(state.template)]);
}

@ -12,18 +12,7 @@ export function SnippetBlock(node, context) {
let fn = b.function_declaration(
node.expression,
[b.id('$$payload'), ...node.parameters],
b.block([
b.stmt(
b.call(
'$$payload.child',
b.arrow(
[b.id('$$payload')],
/** @type {BlockStatement} */ (context.visit(node.body)),
node.metadata.has_await
)
)
)
])
/** @type {BlockStatement} */ (context.visit(node.body))
);
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone

@ -2,9 +2,10 @@ import type { AST, Binding, StateField } from '#compiler';
import type {
CallExpression,
ClassBody,
Expression,
Identifier,
LabeledStatement,
Node,
MemberExpression,
Program
} from 'estree';
import type { Scope, ScopeRoot } from './scope.js';
@ -108,6 +109,7 @@ export interface ComponentAnalysis extends Analysis {
snippets: Set<AST.SnippetBlock>;
/** Whether the component uses `await` in a context that would require an `await` on the server. */
has_blocking_await: boolean;
hoisted_promises: Map<Expression, MemberExpression>;
}
declare module 'estree' {

@ -57,6 +57,11 @@ export namespace AST {
*/
dynamic: boolean;
has_await: boolean;
/**
* True when this fragment has a top-level `await` expression.
*/
is_async: boolean;
hoisted_promises: { name: string; promises: Expression[] };
};
}
@ -523,7 +528,6 @@ export namespace AST {
/** @internal */
metadata: {
can_hoist: boolean;
has_await: boolean;
/** The set of components/render tags that could render this snippet,
* used for CSS pruning */
sites: Set<Component | SvelteComponent | SvelteSelf | RenderTag>;

@ -0,0 +1,5 @@
{#each await Promise.resolve([]) as item}
{await Promise.reject('This should never be reached')}
{:else}
{await Promise.resolve(4)}
{/each}

@ -0,0 +1,9 @@
<script lang="ts">
const first = Promise.resolve(1);
const second = Promise.resolve(2);
const third = Promise.resolve(3);
</script>
{#each await Promise.resolve([first, second, third]) as item}
{await item}
{/each}

@ -0,0 +1,5 @@
{#if await Promise.resolve(false)}
{await Promise.reject('no no no')}
{:else}
{await Promise.resolve('yes yes yes')}
{/if}

@ -0,0 +1,5 @@
{#if await Promise.resolve(true)}
{await Promise.resolve('yes yes yes')}
{:else}
{await Promise.reject('no no no')}
{/if}

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({ compileOptions: { experimental: { async: true } } });

@ -0,0 +1,35 @@
import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/client';
export default function Async_each_fallback_hoisting($$anchor) {
var fragment = $.comment();
var node = $.first_child(fragment);
$.async(node, [() => Promise.resolve([])], (node, $$collection) => {
$.each(
node,
16,
() => $.get($$collection),
$.index,
($$anchor, item) => {
$.next();
var text = $.text();
$.template_effect(($0) => $.set_text(text, $0), undefined, [() => Promise.reject('This should never be reached')]);
$.append($$anchor, text);
},
($$anchor) => {
$.next();
var text_1 = $.text();
$.template_effect(($0) => $.set_text(text_1, $0), undefined, [() => Promise.resolve(4)]);
$.append($$anchor, text_1);
}
);
});
$.append($$anchor, fragment);
}

@ -0,0 +1,33 @@
import * as $ from 'svelte/internal/server';
export default function Async_each_fallback_hoisting($$payload) {
$$payload.child(async ($$payload) => {
const promises = [Promise.resolve([])];
const each_array = $.ensure_array_like(await promises[0]);
$$payload.child(async ($$payload) => {
if (each_array.length !== 0) {
$$payload.push('<!--[-->');
for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
let item = each_array[$$index];
const promises_1 = [Promise.reject('This should never be reached')];
$$payload.child(async ($$payload) => {
$$payload.push(`<!---->${$.escape(await promises_1[0])}`);
});
}
} else {
$$payload.push('<!--[!-->');
const promises_2 = [Promise.resolve(4)];
$$payload.child(async ($$payload) => {
$$payload.push(`<!---->${$.escape(await promises_2[0])}`);
});
}
$$payload.push(`<!--]-->`);
});
});
}

@ -0,0 +1,5 @@
{#each await Promise.resolve([]) as item}
{await Promise.reject('This should never be reached')}
{:else}
{await Promise.resolve(4)}
{/each}

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({ compileOptions: { experimental: { async: true } } });

@ -0,0 +1,24 @@
import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/client';
export default function Async_each_hoisting($$anchor) {
const first = Promise.resolve(1);
const second = Promise.resolve(2);
const third = Promise.resolve(3);
var fragment = $.comment();
var node = $.first_child(fragment);
$.async(node, [() => Promise.resolve([first, second, third])], (node, $$collection) => {
$.each(node, 17, () => $.get($$collection), $.index, ($$anchor, item) => {
$.next();
var text = $.text();
$.template_effect(($0) => $.set_text(text, $0), undefined, [() => $.get(item)]);
$.append($$anchor, text);
});
});
$.append($$anchor, fragment);
}

@ -0,0 +1,26 @@
import * as $ from 'svelte/internal/server';
export default function Async_each_hoisting($$payload) {
$$payload.child(async ($$payload) => {
const first = Promise.resolve(1);
const second = Promise.resolve(2);
const third = Promise.resolve(3);
const promises = [Promise.resolve([first, second, third])];
const each_array = $.ensure_array_like(await promises[0]);
$$payload.child(async ($$payload) => {
$$payload.push(`<!--[-->`);
for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
let item = each_array[$$index];
const promises_1 = [item];
$$payload.child(async ($$payload) => {
$$payload.push(`<!---->${$.escape(await promises_1[0])}`);
});
}
$$payload.push(`<!--]-->`);
});
});
}

@ -0,0 +1,9 @@
<script lang="ts">
const first = Promise.resolve(1);
const second = Promise.resolve(2);
const third = Promise.resolve(3);
</script>
{#each await Promise.resolve([first, second, third]) as item}
{await item}
{/each}

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({ compileOptions: { experimental: { async: true } } });

@ -0,0 +1,30 @@
import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/client';
export default function Async_if_alternate_hoisting($$anchor) {
var fragment = $.comment();
var node = $.first_child(fragment);
$.async(node, [() => Promise.resolve(false)], (node, $$condition) => {
var consequent = ($$anchor) => {
var text = $.text();
$.template_effect(($0) => $.set_text(text, $0), undefined, [() => Promise.reject('no no no')]);
$.append($$anchor, text);
};
var alternate = ($$anchor) => {
var text_1 = $.text();
$.template_effect(($0) => $.set_text(text_1, $0), undefined, [() => Promise.resolve('yes yes yes')]);
$.append($$anchor, text_1);
};
$.if(node, ($$render) => {
if ($.get($$condition)) $$render(consequent); else $$render(alternate, false);
});
});
$.append($$anchor, fragment);
}

@ -0,0 +1,29 @@
import * as $ from 'svelte/internal/server';
export default function Async_if_alternate_hoisting($$payload) {
$$payload.child(async ($$payload) => {
const promises = [Promise.resolve(false)];
$$payload.child(async ($$payload) => {
if (await promises[0]) {
$$payload.push('<!--[-->');
const promises_1 = [Promise.reject('no no no')];
$$payload.child(async ($$payload) => {
$$payload.push(`${$.escape(await promises_1[0])}`);
});
} else {
$$payload.push('<!--[!-->');
const promises_2 = [Promise.resolve('yes yes yes')];
$$payload.child(async ($$payload) => {
$$payload.push(`${$.escape(await promises_2[0])}`);
});
}
$$payload.push(`<!--]-->`);
});
});
}

@ -0,0 +1,5 @@
{#if await Promise.resolve(false)}
{await Promise.reject('no no no')}
{:else}
{await Promise.resolve('yes yes yes')}
{/if}

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({ compileOptions: { experimental: { async: true } } });

@ -0,0 +1,30 @@
import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/client';
export default function Async_if_hoisting($$anchor) {
var fragment = $.comment();
var node = $.first_child(fragment);
$.async(node, [() => Promise.resolve(true)], (node, $$condition) => {
var consequent = ($$anchor) => {
var text = $.text();
$.template_effect(($0) => $.set_text(text, $0), undefined, [() => Promise.resolve('yes yes yes')]);
$.append($$anchor, text);
};
var alternate = ($$anchor) => {
var text_1 = $.text();
$.template_effect(($0) => $.set_text(text_1, $0), undefined, [() => Promise.reject('no no no')]);
$.append($$anchor, text_1);
};
$.if(node, ($$render) => {
if ($.get($$condition)) $$render(consequent); else $$render(alternate, false);
});
});
$.append($$anchor, fragment);
}

@ -0,0 +1,29 @@
import * as $ from 'svelte/internal/server';
export default function Async_if_hoisting($$payload) {
$$payload.child(async ($$payload) => {
const promises = [Promise.resolve(true)];
$$payload.child(async ($$payload) => {
if (await promises[0]) {
$$payload.push('<!--[-->');
const promises_1 = [Promise.resolve('yes yes yes')];
$$payload.child(async ($$payload) => {
$$payload.push(`${$.escape(await promises_1[0])}`);
});
} else {
$$payload.push('<!--[!-->');
const promises_2 = [Promise.reject('no no no')];
$$payload.child(async ($$payload) => {
$$payload.push(`${$.escape(await promises_2[0])}`);
});
}
$$payload.push(`<!--]-->`);
});
});
}

@ -0,0 +1,5 @@
{#if await Promise.resolve(true)}
{await Promise.resolve('yes yes yes')}
{:else}
{await Promise.reject('no no no')}
{/if}

@ -2,9 +2,7 @@ import * as $ from 'svelte/internal/server';
import TextInput from './Child.svelte';
function snippet($$payload) {
$$payload.child(($$payload) => {
$$payload.push(`<!---->Something`);
});
}
export default function Bind_component_snippet($$payload) {

Loading…
Cancel
Save