pull/17038/head
Rich Harris 1 week ago
parent b18a5e310b
commit 60370e67cd

@ -21,9 +21,6 @@ export interface ClientTransformState extends TransformState {
*/
readonly in_constructor: boolean;
/** `true` if we're transforming the contents of `<script>` */
readonly is_instance: boolean;
readonly transform: Record<
string,
{

@ -1,4 +1,4 @@
/** @import { Program, Property, Statement, VariableDeclarator } from 'estree' */
/** @import * as ESTree from 'estree' */
/** @import { AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { ComponentServerTransformState, ComponentVisitors, ServerTransformState, Visitors } from './types.js' */
/** @import { Analysis, ComponentAnalysis } from '../../types.js' */
@ -25,6 +25,7 @@ import { IfBlock } from './visitors/IfBlock.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { Program } from './visitors/Program.js';
import { PropertyDefinition } from './visitors/PropertyDefinition.js';
import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
@ -53,6 +54,7 @@ const global_visitors = {
Identifier,
LabeledStatement,
MemberExpression,
Program,
PropertyDefinition,
UpdateExpression,
VariableDeclaration
@ -86,7 +88,7 @@ const template_visitors = {
/**
* @param {ComponentAnalysis} analysis
* @param {ValidatedCompileOptions} options
* @returns {Program}
* @returns {ESTree.Program}
*/
export function server_component(analysis, options) {
/** @type {ComponentServerTransformState} */
@ -103,17 +105,18 @@ export function server_component(analysis, options) {
namespace: options.namespace,
preserve_whitespace: options.preserveWhitespace,
state_fields: new Map(),
skip_hydration_boundaries: false
skip_hydration_boundaries: false,
is_instance: false
};
const module = /** @type {Program} */ (
const module = /** @type {ESTree.Program} */ (
walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors)
);
const instance = /** @type {Program} */ (
const instance = /** @type {ESTree.Program} */ (
walk(
/** @type {AST.SvelteNode} */ (analysis.instance.ast),
{ ...state, scopes: analysis.instance.scopes },
{ ...state, scopes: analysis.instance.scopes, is_instance: true },
{
...global_visitors,
ImportDeclaration(node) {
@ -131,7 +134,7 @@ export function server_component(analysis, options) {
)
);
const template = /** @type {Program} */ (
const template = /** @type {ESTree.Program} */ (
walk(
/** @type {AST.SvelteNode} */ (analysis.template.ast),
{ ...state, scopes: analysis.template.scopes },
@ -140,7 +143,7 @@ export function server_component(analysis, options) {
)
);
/** @type {VariableDeclarator[]} */
/** @type {ESTree.VariableDeclarator[]} */
const legacy_reactive_declarations = [];
for (const [node] of analysis.reactive_statements) {
@ -192,7 +195,7 @@ export function server_component(analysis, options) {
b.function_declaration(
b.id('$$render_inner'),
[b.id('$$renderer')],
b.block(/** @type {Statement[]} */ (rest))
b.block(/** @type {ESTree.Statement[]} */ (rest))
),
b.do_while(
b.unary('!', b.id('$$settled')),
@ -219,7 +222,7 @@ export function server_component(analysis, options) {
// Propagate values of bound props upwards if they're undefined in the parent and have a value.
// Don't do this as part of the props retrieval because people could eagerly mutate the prop in the instance script.
/** @type {Property[]} */
/** @type {ESTree.Property[]} */
const props = [];
for (const [name, binding] of analysis.instance.scope.declarations) {
@ -239,14 +242,10 @@ export function server_component(analysis, options) {
}
let component_block = b.block([
.../** @type {Statement[]} */ (instance.body),
.../** @type {Statement[]} */ (template.body)
.../** @type {ESTree.Statement[]} */ (instance.body),
.../** @type {ESTree.Statement[]} */ (template.body)
]);
if (analysis.instance.has_await) {
component_block = b.block([create_async_block(component_block)]);
}
// trick esrap into including comments
component_block.loc = instance.loc;
@ -395,7 +394,7 @@ export function server_component(analysis, options) {
/**
* @param {Analysis} analysis
* @param {ValidatedModuleCompileOptions} options
* @returns {Program}
* @returns {ESTree.Program}
*/
export function server_module(analysis, options) {
/** @type {ServerTransformState} */
@ -411,7 +410,7 @@ export function server_module(analysis, options) {
state_fields: new Map()
};
const module = /** @type {Program} */ (
const module = /** @type {ESTree.Program} */ (
walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors)
);

@ -0,0 +1,219 @@
/** @import { BlockStatement, ClassDeclaration, ClassExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Program, Statement, VariableDeclaration, VariableDeclarator } from 'estree' */
/** @import { Context, ComponentContext } from '../types' */
/** @import { AwaitedStatement } from '../../../types' */
import * as b from '#compiler/builders';
import { runes } from '../../../../state.js';
/**
* @param {Program} node
* @param {Context} context
*/
export function Program(node, context) {
if (context.state.is_instance && runes) {
return {
...node,
// @ts-ignore wtf
body: transform_body(node, /** @type {ComponentContext} */ (context))
};
}
context.next();
}
/**
* @param {Program} program
* @param {ComponentContext} context
*/
function transform_body(program, context) {
/** @type {Statement[]} */
const out = [];
/** @type {Identifier[]} */
const ids = [];
/** @type {AwaitedStatement[]} */
const statements = [];
/** @type {AwaitedStatement[]} */
const deriveds = [];
const { awaited_statements } = context.state.analysis;
let awaited = false;
/**
* @param {Statement | VariableDeclarator | ClassDeclaration | FunctionDeclaration} node
*/
const push = (node) => {
const statement = awaited_statements.get(node);
awaited ||= !!statement?.has_await;
if (!awaited || !statement || node.type === 'FunctionDeclaration') {
if (node.type === 'VariableDeclarator') {
out.push(/** @type {VariableDeclaration} */ (context.visit(b.var(node.id, node.init))));
} else {
out.push(/** @type {Statement} */ (context.visit(node)));
}
return;
}
// TODO put deriveds into a separate array, and group them immediately
// after their latest dependency. for now, to avoid having to figure
// out the intricacies of dependency tracking, just let 'em waterfall
// if (node.type === 'VariableDeclarator') {
// const rune = get_rune(node.init, context.state.scope);
// if (rune === '$derived' || rune === '$derived.by') {
// deriveds.push(statement);
// return;
// }
// }
statements.push(statement);
};
for (let node of program.body) {
if (node.type === 'ImportDeclaration') {
// TODO we can get rid of the visitor
context.state.hoisted.push(node);
continue;
}
if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') {
// this can't happen, but it's useful for TypeScript to understand that
continue;
}
if (node.type === 'ExportNamedDeclaration') {
if (node.declaration) {
// TODO ditto — no visitor needed
node = node.declaration;
} else {
continue;
}
}
if (node.type === 'VariableDeclaration') {
for (const declarator of node.declarations) {
push(declarator);
}
} else {
push(node);
}
}
for (const derived of deriveds) {
// find the earliest point we can insert this derived
let index = -1;
for (const binding of derived.dependencies) {
index = Math.max(
index,
statements.findIndex((s) => s.declarations.includes(binding))
);
}
if (index === -1 && !derived.has_await) {
const node = /** @type {VariableDeclarator} */ (derived.node);
out.push(/** @type {VariableDeclaration} */ (context.visit(b.var(node.id, node.init))));
} else {
// TODO combine deriveds with Promise.all where necessary
statements.splice(index + 1, 0, derived);
}
}
if (statements.length > 0) {
var declarations = statements.map((s) => s.declarations).flat();
if (declarations.length > 0) {
out.push(
b.declaration(
'var',
declarations.map((d) => b.declarator(d.node))
)
);
}
const thunks = statements.map((s) => {
if (s.node.type === 'VariableDeclarator') {
const visited = /** @type {VariableDeclaration} */ (
context.visit(b.var(s.node.id, s.node.init))
);
if (visited.declarations.length === 1) {
return b.thunk(
b.assignment('=', s.node.id, visited.declarations[0].init ?? b.void0),
s.has_await
);
}
// if we have multiple declarations, it indicates destructuring
return b.thunk(
b.block([
b.var(visited.declarations[0].id, visited.declarations[0].init),
...visited.declarations
.slice(1)
.map((d) => b.stmt(b.assignment('=', d.id, d.init ?? b.void0)))
]),
s.has_await
);
}
if (s.node.type === 'ClassDeclaration') {
return b.thunk(
b.assignment(
'=',
s.node.id,
/** @type {ClassExpression} */ ({ ...s.node, type: 'ClassExpression' })
),
s.has_await
);
}
if (s.node.type === 'FunctionDeclaration') {
return b.thunk(
b.assignment(
'=',
s.node.id,
/** @type {FunctionExpression} */ ({ ...s.node, type: 'FunctionExpression' })
),
s.has_await
);
}
if (s.node.type === 'ExpressionStatement') {
return b.thunk(
b.unary('void', /** @type {Expression} */ (context.visit(s.node.expression))),
s.has_await
);
}
return b.thunk(b.block([/** @type {Statement} */ (context.visit(s.node))]), s.has_await);
});
var id = b.id('$$promises'); // TODO if we use this technique for fragments, need to deconflict
out.push(b.var(id, b.call('$$renderer.run', b.array(thunks))));
for (let i = 0; i < statements.length; i += 1) {
const s = statements[i];
var blocker = b.member(id, b.literal(i), true);
for (const binding of s.declarations) {
binding.blocker = blocker;
}
}
// TODO we likely need to account for updates that happen after the declaration,
// e.g. `let obj = $state()` followed by a later `obj = {...}`, otherwise
// a synchronous `{obj.foo}` will fail
}
// console.log('statements', statements);
// console.log('deriveds', deriveds);
return out;
}

@ -8,5 +8,8 @@ export interface TransformState {
readonly scope: Scope;
readonly scopes: Map<AST.SvelteNode, Scope>;
/** `true` if we're transforming the contents of `<script>` */
readonly is_instance: boolean;
readonly state_fields: Map<string, StateField>;
}

@ -108,6 +108,11 @@ export class Renderer {
this.#out.push(BLOCK_CLOSE);
}
/**
* @param {Array<() => void>} thunks
*/
run(thunks) {}
/**
* Create a child renderer. The child renderer inherits the state from the parent,
* but has its own content.

Loading…
Cancel
Save