first crack

pull/16762/head
S. Elliott Johnson 4 weeks ago
parent f0e46729ce
commit c5b639c108

@ -426,6 +426,7 @@ function open(parser) {
body: create_fragment(),
metadata: {
can_hoist: false,
has_await: false,
sites: new Set()
}
});

@ -303,6 +303,7 @@ export function analyze_module(source, options) {
has_props_rune: false,
options: /** @type {ValidatedCompileOptions} */ (options),
fragment: null,
snippet: null,
parent_element: null,
reactive_statement: null
},
@ -531,7 +532,8 @@ export function analyze_component(root, source, options) {
source,
snippet_renderers: new Map(),
snippets: new Set(),
async_deriveds: new Set()
async_deriveds: new Set(),
suspends: false
};
state.adjust({
@ -691,6 +693,7 @@ export function analyze_component(root, source, options) {
options,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
fragment: ast === template.ast ? ast : null,
snippet: null,
parent_element: null,
has_props_rune: false,
component_slots: new Set(),
@ -757,6 +760,7 @@ export function analyze_component(root, source, options) {
analysis,
options,
fragment: ast === template.ast ? ast : null,
snippet: null,
parent_element: null,
has_props_rune: false,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',

@ -9,6 +9,7 @@ export interface AnalysisState {
options: ValidatedCompileOptions;
ast_type: 'instance' | 'template' | 'module';
fragment: AST.Fragment | null;
snippet: AST.SnippetBlock | 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.

@ -23,6 +23,10 @@ export function AwaitExpression(node, context) {
suspend = true;
}
if (context.state.snippet) {
context.state.snippet.metadata.has_await = true;
}
// disallow top-level `await` or `await` in template expressions
// unless a) in runes mode and b) opted into `experimental.async`
if (suspend) {
@ -33,6 +37,8 @@ export function AwaitExpression(node, context) {
if (!context.state.analysis.runes) {
e.legacy_await_invalid(node);
}
context.state.analysis.suspends = true;
}
context.next();

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

@ -10,7 +10,6 @@ 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';
@ -45,7 +44,6 @@ import { SvelteBoundary } from './visitors/SvelteBoundary.js';
const global_visitors = {
_: set_scope,
AssignmentExpression,
AwaitExpression,
CallExpression,
ClassBody,
ExpressionStatement,
@ -240,8 +238,19 @@ export function server_component(analysis, options) {
}
const component_block = b.block([
.../** @type {Statement[]} */ (instance.body),
.../** @type {Statement[]} */ (template.body)
b.stmt(
b.call(
'$$payload.child',
b.arrow(
[],
b.block([
.../** @type {Statement[]} */ (instance.body),
.../** @type {Statement[]} */ (template.body)
]),
analysis.suspends
)
)
)
]);
// trick esrap into including comments

@ -1,25 +0,0 @@
/** @import { AwaitExpression } from 'estree' */
/** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js';
/**
* @param {AwaitExpression} node
* @param {Context} context
*/
export function AwaitExpression(node, context) {
// if `await` is inside a function, or inside `<script module>`,
// allow it, otherwise error
if (
context.state.scope.function_depth === 0 ||
context.path.some(
(node) =>
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression'
)
) {
return context.next();
}
return b.call('$.await_outside_boundary');
}

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

@ -227,7 +227,21 @@ export function build_inline_component(node, expression, context) {
params.push(pattern);
}
const slot_fn = b.arrow(params, b.block(block.body));
const slot_fn = b.arrow(
params,
// TODO: This will always produce correct results because it will always produce async functions
// if the current component is an async component, but it may produce async functions where they're
// not necessary -- eg. when the component is asynchronous but the child content is not.
// May or may not be worth optimizing.
b.block([
b.stmt(
b.call(
'$$payload.child',
b.arrow([], b.block(block.body), context.state.analysis.suspends)
)
)
])
);
if (slot_name === 'default' && !has_children_prop) {
if (

@ -106,6 +106,8 @@ export interface ComponentAnalysis extends Analysis {
* Every snippet that is declared locally
*/
snippets: Set<AST.SnippetBlock>;
/** Whether the component uses `await` in a context that would cause suspense. */
suspends: boolean;
}
declare module 'estree' {

@ -513,6 +513,7 @@ 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>;

@ -101,7 +101,7 @@ export function render(component, options = {}) {
for (const cleanup of on_destroy) cleanup();
on_destroy = prev_on_destroy;
let head = payload.head.out.join('') + payload.head.title;
let head = payload.head.collect() + payload.head.title;
for (const { hash, code } of payload.css) {
head += `<style id="${hash}">${code}</style>`;

@ -1,131 +1,195 @@
import { deferred } from '../shared/utils';
export class HeadPayload {
/** @type {Set<{ hash: string; code: string }>} */
css = new Set();
/** @type {string[]} */
/**
* A base class for payloads. Payloads are basically a tree of `string | Payload`s, where each
* `Payload` in the tree represents work that may or may not have completed. A payload can be
* {@link collect}ed to aggregate the content from itself and all of its children, but this will
* throw if any of the children are performing asynchronous work. A payload can also be collected
* asynchronously with {@link collect_async}, which will wait for all children to complete before
* collecting their contents.
*
* @template {Record<PropertyKey, unknown>} TState
*/
class BasePayload {
/**
* The contents of the payload.
* @type {(string | BasePayload<TState>)[]}
*/
out = [];
uid = () => '';
title = '';
constructor(
/** @type {Set<{ hash: string; code: string }>} */ css = new Set(),
/** @type {string[]} */ out = [],
title = '',
uid = () => ''
) {
this.css = css;
this.out = out;
this.title = title;
this.uid = uid;
}
}
export class Payload {
/** @type {Set<{ hash: string; code: string }>} */
css;
/** @type {(string | ChildPayload)[]} */
out = [];
/** @type {() => string} */
uid;
/** @type {string | undefined} */
select_value = undefined;
/** @type {HeadPayload} */
head;
/** @type {'sync' | 'async'} */
mode;
/** @type {Promise<string>[]} */
tail = [];
/**
* A promise that resolves when this payload's blocking asynchronous work is done.
* If this promise is not resolved, it is not safe to collect the payload from `out`.
* @type {Promise<void> | undefined}
*/
promise;
/**
* @param {{ id_prefix?: string, mode?: 'sync' | 'async', head?: HeadPayload, uid?: () => string, out?: (string | ChildPayload)[], css?: Set<{ hash: string; code: string }>, select_value?: any }} args
* Internal state. This is the easiest way to represent the additional state each payload kind
* needs to add to itself while still giving the base payload the ability to copy itself.
* @protected
* @type {TState}
*/
constructor({
id_prefix = '',
mode = 'sync',
head = new HeadPayload(),
uid = props_id_generator(id_prefix),
css = new Set()
} = {}) {
this.uid = uid;
this.head = head;
this.mode = mode;
this.css = css;
_state;
/**
* Create a new payload, copying the state from the parent payload.
* @param {TState} parent_state
*/
constructor(parent_state) {
this._state = parent_state;
}
/**
* Create a child scope. `front` represents the initial, synchronous code, and `back` represents all code from the first `await` onwards.
* Typically a child will be created for each component.
* @param {{ front: (args: { payload: Payload }) => void, back: (args: { payload: Payload }) => Promise<void> }} args
* Create a child payload. The child payload inherits the state from the parent,
* but has its own `out` array and `promise` property. The child payload is automatically
* inserted into the parent payload's `out` array.
* @param {(args: { $$payload: BasePayload<TState> }) => void | Promise<void>} render
* @returns {void}
*/
child({ front, back }) {
const child = new ChildPayload(this);
front({ payload: child });
// TODO: boundary stuff? Or does this go inside the `back` function?
back({ payload: child }).then(() => child.deferred.resolve());
child(render) {
const child = new BasePayload(this._state);
this.out.push(child);
const result = render({ $$payload: child });
if (result instanceof Promise) {
child.promise = result;
}
}
/**
* Waits for all child payloads to finish their blocking asynchronous work, then returns the generated HTML.
* Waits for all child payloads to finish their blocking asynchronous work, then returns the generated content.
* @returns {Promise<string>}
*/
async collect_async() {
// TODO throw in `sync` mode
/** @type {Promise<void>[]} */
const promises = [];
/**
* @param {(string | ChildPayload)[]} items
*/
function collect_promises(items) {
for (const item of items) {
if (item instanceof ChildPayload) {
promises.push(item.deferred.promise);
collect_promises(item.out);
}
}
// TODO: Should probably use `Promise.allSettled` here just so we can report detailed errors
await Promise.all(this.#collect_promises(this.out));
return this.#collect_content();
}
/**
* Collect all of the code from the `out` array and return it as a string.
* @returns {string}
*/
collect() {
const promises = this.#collect_promises(this.out);
if (promises.length > 0) {
// TODO is there a good way to report where this is? Probably by using some sort of loc or stack trace in `child` creation
throw new Error('Encountered an asynchronous component while rendering synchronously');
}
collect_promises(this.out);
await Promise.all(promises);
return this.collect();
return this.#collect_content();
}
/**
* @param {(string | BasePayload<TState>)[]} items
* @param {Promise<void>[]} [promises]
* @returns {Promise<void>[]}
*/
#collect_promises(items, promises = this.promise ? [this.promise] : []) {
for (const item of items) {
if (item instanceof BasePayload && item.promise) {
promises.push(item.promise);
this.#collect_promises(item.out, promises);
}
}
return promises;
}
/**
* Collect all of the code from the `out` array and return it as a string. If in `async` mode, wait on
* `finished` prior to collecting.
* Collect all of the code from the `out` array and return it as a string.
* @returns {string}
*/
collect() {
#collect_content() {
// TODO throw in `async` mode
let html = '';
let content = '';
for (const item of this.out) {
if (typeof item === 'string') {
html += item;
content += item;
} else {
html += item.collect();
content += item.#collect_content();
}
}
return html;
return content;
}
}
class ChildPayload extends Payload {
deferred = /** @type {ReturnType<typeof deferred<void>>} */ (deferred());
/**
* @extends {BasePayload<{
* css: Set<{ hash: string; code: string }>,
* title: { value: string },
* uid: () => string
* }>}
*/
export class HeadPayload extends BasePayload {
get css() {
return this._state.css;
}
get uid() {
return this._state.uid;
}
// title is boxed so that it gets globally shared between all parent/child heads
get title() {
return this._state.title.value;
}
set title(value) {
this._state.title.value = value;
}
/**
* @param {Payload} parent
* @param {{ css?: Set<{ hash: string; code: string }>, title?: { value: string }, uid?: () => string }} args
*/
constructor(parent) {
constructor({ css = new Set(), title = { value: '' }, uid = () => '' } = {}) {
super({
mode: parent.mode,
head: parent.head,
uid: parent.uid,
css: parent.css
css,
title,
uid
});
}
}
/**
* @extends {BasePayload<{
* css: Set<{ hash: string; code: string }>,
* uid: () => string,
* select_value: any,
* head: HeadPayload,
* }>}
*/
export class Payload extends BasePayload {
get css() {
return this._state.css;
}
get uid() {
return this._state.uid;
}
get head() {
return this._state.head;
}
get select_value() {
return this._state.select_value;
}
set select_value(value) {
this._state.select_value = value;
}
/**
* @param {{ id_prefix?: string, head?: HeadPayload, uid?: () => string, css?: Set<{ hash: string; code: string }>, select_value?: any }} args
*/
constructor({
id_prefix = '',
head = new HeadPayload(),
uid = props_id_generator(id_prefix),
css = new Set()
} = {}) {
super({
uid,
head,
css,
select_value: undefined
});
this.root = parent;
parent.out.push(this);
}
}
@ -135,17 +199,18 @@ class ChildPayload extends Payload {
* @returns {Payload}
*/
export function copy_payload({ out, css, head, uid }) {
const payload = new Payload();
const payload = new Payload({
css: new Set(css),
uid,
head: new HeadPayload({
css: new Set(head.css),
title: head.title,
uid: head.uid
})
});
payload.out = [...out];
payload.css = new Set(css);
payload.uid = uid;
payload.head = new HeadPayload();
payload.head.out = [...head.out];
payload.head.css = new Set(head.css);
payload.head.title = head.title;
payload.head.uid = head.uid;
return payload;
}
@ -158,9 +223,13 @@ export function copy_payload({ out, css, head, uid }) {
*/
export function assign_payload(p1, p2) {
p1.out = [...p2.out];
p1.css = p2.css;
p1.head = p2.head;
p1.uid = p2.uid;
// this is all legacy code so typescript can go cry in a corner -- I don't want to write setters for all of these because they really shouldn't be settable
// @ts-expect-error
p1._state.css = p2.css;
// @ts-expect-error
p1._state.head = p2.head;
// @ts-expect-error
p1._state.uid = p2.uid;
}
/**

Loading…
Cancel
Save