Skip to content

Commit 8687643

Browse files
committed
WIP
1 parent 0dcc250 commit 8687643

23 files changed

+473
-71
lines changed

packages/svelte/src/ambient.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -500,3 +500,5 @@ declare namespace $host {
500500
/** @deprecated */
501501
export const toString: never;
502502
}
503+
504+
declare function $await<V>(value: Promise<V>): [V, undefined | Promise<V>];

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

+6
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ export function Attribute(node, context) {
6464

6565
node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
6666
node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
67+
chunk.metadata.expression.dependencies.forEach((dependency) =>
68+
node.metadata.expression.dependencies.add(dependency)
69+
);
70+
chunk.metadata.expression.async_dependencies.forEach((dependency) =>
71+
node.metadata.expression.async_dependencies.add(dependency)
72+
);
6773
}
6874

6975
if (is_event_attribute(node)) {

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

+48-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/** @import { Context } from '../types' */
44
import { get_rune } from '../../scope.js';
55
import * as e from '../../../errors.js';
6-
import { get_parent, unwrap_optional } from '../../../utils/ast.js';
6+
import { extract_identifiers, get_parent, unwrap_optional } from '../../../utils/ast.js';
77
import { is_pure, is_safe_identifier } from './shared/utils.js';
88
import { dev, locate_node, source } from '../../../state.js';
99
import * as b from '../../../utils/builders.js';
@@ -123,6 +123,52 @@ export function CallExpression(node, context) {
123123

124124
break;
125125

126+
case '$await': {
127+
if (node.arguments.length !== 1) {
128+
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
129+
}
130+
131+
const declarator = context.path.at(-1);
132+
const declaration = context.path.at(-2);
133+
const program = context.path.at(-3);
134+
135+
if (context.state.ast_type !== 'instance') {
136+
throw new Error('TODO: $await can only be used at the top-level of a component');
137+
}
138+
if (
139+
declarator?.type !== 'VariableDeclarator' ||
140+
declarator?.id.type !== 'ArrayPattern' ||
141+
declaration?.type !== 'VariableDeclaration' ||
142+
declaration?.declarations.length !== 1 ||
143+
context.state.function_depth !== 1 ||
144+
program?.type !== 'Program'
145+
) {
146+
throw new Error('TODO: invalid usage of $await in component');
147+
}
148+
149+
const [async_derived, derived_promise] = declarator.id.elements;
150+
151+
if (async_derived) {
152+
for (const id of extract_identifiers(async_derived)) {
153+
const binding = context.state.scope.get(id.name);
154+
if (binding !== null) {
155+
binding.kind = 'async_derived';
156+
}
157+
}
158+
}
159+
160+
if (derived_promise) {
161+
for (const id of extract_identifiers(derived_promise)) {
162+
const binding = context.state.scope.get(id.name);
163+
if (binding !== null) {
164+
binding.kind = 'derived';
165+
}
166+
}
167+
}
168+
169+
break;
170+
}
171+
126172
case '$inspect':
127173
if (node.arguments.length < 1) {
128174
e.rune_invalid_arguments_length(node, rune, 'one or more arguments');
@@ -207,7 +253,7 @@ export function CallExpression(node, context) {
207253
}
208254

209255
// `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning
210-
if (rune === '$inspect' || rune === '$derived') {
256+
if (rune === '$inspect' || rune === '$derived' || rune === '$await') {
211257
context.next({ ...context.state, function_depth: context.state.function_depth + 1 });
212258
} else {
213259
context.next();

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

+19-3
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,25 @@ export function Identifier(node, context) {
8888
}
8989

9090
if (binding) {
91-
if (context.state.expression) {
92-
context.state.expression.dependencies.add(binding);
93-
context.state.expression.has_state ||= binding.kind !== 'normal';
91+
if (binding.kind === 'async_derived') {
92+
debugger
93+
}
94+
95+
const expression = context.state.expression;
96+
97+
if (expression) {
98+
expression.dependencies.add(binding);
99+
100+
if (
101+
binding.kind === 'async_derived'
102+
) {
103+
expression.async_dependencies.add(binding);
104+
}
105+
expression.has_state ||= binding.kind !== 'normal';
106+
107+
binding.async_dependencies.forEach((dep) => {
108+
expression.async_dependencies.add(dep);
109+
});
94110
}
95111

96112
if (

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

+38-14
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ensure_no_module_import_conflict, validate_identifier_name } from './sh
66
import * as e from '../../../errors.js';
77
import { extract_paths } from '../../../utils/ast.js';
88
import { equal } from '../../../utils/assert.js';
9+
import { create_expression_metadata } from '../../nodes.js';
910

1011
/**
1112
* @param {VariableDeclarator} node
@@ -29,21 +30,42 @@ export function VariableDeclarator(node, context) {
2930
rune === '$state.raw' ||
3031
rune === '$derived' ||
3132
rune === '$derived.by' ||
32-
rune === '$props'
33+
rune === '$props' ||
34+
rune === '$await'
3335
) {
34-
for (const path of paths) {
35-
// @ts-ignore this fails in CI for some insane reason
36-
const binding = /** @type {Binding} */ (context.state.scope.get(path.node.name));
37-
binding.kind =
38-
rune === '$state'
39-
? 'state'
40-
: rune === '$state.raw'
41-
? 'raw_state'
42-
: rune === '$derived' || rune === '$derived.by'
43-
? 'derived'
44-
: path.is_rest
45-
? 'rest_prop'
46-
: 'prop';
36+
let metadata = create_expression_metadata();
37+
38+
if (rune !== '$await') {
39+
for (const path of paths) {
40+
// @ts-ignore this fails in CI for some insane reason
41+
const binding = /** @type {Binding} */ (context.state.scope.get(path.node.name));
42+
binding.kind =
43+
rune === '$state'
44+
? 'state'
45+
: rune === '$state.raw'
46+
? 'raw_state'
47+
: rune === '$derived' || rune === '$derived.by'
48+
? 'derived'
49+
: path.is_rest
50+
? 'rest_prop'
51+
: 'prop';
52+
}
53+
}
54+
55+
context.visit(node.id);
56+
if (node.init) {
57+
context.visit(node.init);
58+
if (node.init.type === 'CallExpression' && node.init.arguments.length > 0) {
59+
context.visit(node.init.arguments[0], { ...context.state, expression: metadata });
60+
61+
if (metadata.async_dependencies.size > 0) {
62+
for (const path of paths) {
63+
// @ts-ignore
64+
const binding = /** @type {Binding} */ (context.state.scope.get(path.node.name));
65+
metadata.async_dependencies.forEach((dep) => binding.async_dependencies.add(dep));
66+
}
67+
}
68+
}
4769
}
4870
}
4971

@@ -103,6 +125,8 @@ export function VariableDeclarator(node, context) {
103125
}
104126
}
105127
}
128+
129+
return;
106130
} else {
107131
if (node.init?.type === 'CallExpression') {
108132
const callee = node.init.callee;

packages/svelte/src/compiler/phases/3-transform/client/transform-client.js

+15-5
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,8 @@ export function client_component(analysis, options) {
174174
update: /** @type {any} */ (null),
175175
after_update: /** @type {any} */ (null),
176176
template: /** @type {any} */ (null),
177-
locations: /** @type {any} */ (null)
177+
locations: /** @type {any} */ (null),
178+
target_statements: null
178179
};
179180

180181
const module = /** @type {ESTree.Program} */ (
@@ -193,6 +194,8 @@ export function client_component(analysis, options) {
193194
walk(/** @type {AST.SvelteNode} */ (analysis.instance.ast), instance_state, visitors)
194195
);
195196

197+
const target_statements = instance_state.target_statements;
198+
196199
const template = /** @type {ESTree.Program} */ (
197200
walk(
198201
/** @type {AST.SvelteNode} */ (analysis.template.ast),
@@ -351,17 +354,24 @@ export function client_component(analysis, options) {
351354
const push_args = [b.id('$$props'), b.literal(analysis.runes)];
352355
if (dev) push_args.push(b.id(analysis.name));
353356

354-
const component_block = b.block([
357+
const component_block_statements = [
355358
...store_setup,
356359
...legacy_reactive_declarations,
357360
...group_binding_declarations,
358361
...state.instance_level_snippets,
359362
.../** @type {ESTree.Statement[]} */ (instance.body),
360363
analysis.runes || !analysis.needs_context
361364
? b.empty
362-
: b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)),
363-
.../** @type {ESTree.Statement[]} */ (template.body)
364-
]);
365+
: b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))
366+
];
367+
368+
if (target_statements === null) {
369+
component_block_statements.push(.../** @type {ESTree.Statement[]} */ (template.body));
370+
} else {
371+
target_statements.push(.../** @type {ESTree.Statement[]} */ (template.body));
372+
}
373+
374+
const component_block = b.block(component_block_statements);
365375

366376
if (!analysis.runes) {
367377
// Bind static exports to props so that people can access them with bind:x

packages/svelte/src/compiler/phases/3-transform/client/types.d.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import type {
77
Expression,
88
AssignmentExpression,
99
UpdateExpression,
10-
VariableDeclaration
10+
VariableDeclaration,
11+
Directive
1112
} from 'estree';
1213
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
1314
import type { TransformState } from '../types.js';
@@ -91,6 +92,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
9192
readonly instance_level_snippets: VariableDeclaration[];
9293
/** Snippets hoisted to the module */
9394
readonly module_level_snippets: VariableDeclaration[];
95+
96+
target_statements: null | Array<ModuleDeclaration | Statement | Directive>;
9497
}
9598

9699
export interface StateField {

packages/svelte/src/compiler/phases/3-transform/client/utils.js

+106-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */
1+
/** @import { ArrowFunctionExpression, Expression, CallExpression, VariableDeclarator, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement, VariableDeclaration, ModuleDeclaration, Directive } from 'estree' */
22
/** @import { AST, Binding } from '#compiler' */
33
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
44
/** @import { Analysis } from '../../types.js' */
55
/** @import { Scope } from '../../scope.js' */
66
import * as b from '../../../utils/builders.js';
77
import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js';
8+
import { get_rune } from '../../scope.js';
89
import {
910
PROPS_IS_LAZY_INITIAL,
1011
PROPS_IS_IMMUTABLE,
@@ -312,3 +313,107 @@ export function create_derived_block_argument(node, context) {
312313
export function create_derived(state, arg) {
313314
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
314315
}
316+
317+
/**
318+
* @param {(ModuleDeclaration | Statement | Directive)[]} statements
319+
* @param {ComponentContext} context
320+
* @returns {[(ModuleDeclaration | Statement | Directive)[], null | (ModuleDeclaration | Statement | Directive)[]]}
321+
*/
322+
export function wrap_unsafe_async_statements(statements, context) {
323+
/** @type {(ModuleDeclaration | Statement | Directive)[]} */
324+
const new_statements = [];
325+
let target_block_statements = new_statements;
326+
let is_unsafe = true;
327+
328+
const push_unsafe_statement = (/** @type {Statement} */ statement) => {
329+
if (is_unsafe) {
330+
const block_statments = [statement];
331+
const script_template = b.stmt(b.call('$.script_effect', b.thunk(b.block(block_statments))));
332+
target_block_statements.push(script_template);
333+
target_block_statements = block_statments;
334+
is_unsafe = false;
335+
} else {
336+
target_block_statements.push(statement);
337+
}
338+
};
339+
340+
for (const statement of statements) {
341+
const visited = /** @type {Statement} */ (context.visit(statement));
342+
343+
if (
344+
statement.type === 'FunctionDeclaration' ||
345+
statement.type === 'ClassDeclaration' ||
346+
statement.type === 'EmptyStatement' ||
347+
statement.type === 'ImportDeclaration' ||
348+
statement.type === 'ExportNamedDeclaration' ||
349+
statement.type === 'ExportAllDeclaration' ||
350+
statement.type === 'ExportDefaultDeclaration'
351+
) {
352+
target_block_statements.push(visited);
353+
continue;
354+
}
355+
356+
if (statement.type === 'VariableDeclaration') {
357+
if (statement.declarations.length === 1) {
358+
const declarator = statement.declarations[0];
359+
const init = declarator.init;
360+
361+
// Safe declaration
362+
if (
363+
init == null ||
364+
init.type === 'Literal' ||
365+
init.type === 'FunctionExpression' ||
366+
init.type === 'ArrowFunctionExpression' ||
367+
(init.type === 'ArrayExpression' && init.elements.length === 0) ||
368+
(init.type === 'ObjectExpression' && init.properties.length === 0)
369+
) {
370+
target_block_statements.push(visited);
371+
continue;
372+
}
373+
// Handle runes
374+
if (init.type === 'CallExpression') {
375+
const rune = get_rune(init, context.state.scope);
376+
377+
if (rune === '$props' || rune === '$derived' || rune === '$derived.by') {
378+
target_block_statements.push(visited);
379+
continue;
380+
}
381+
if (rune === '$await') {
382+
target_block_statements.push(visited);
383+
is_unsafe = true;
384+
continue;
385+
}
386+
}
387+
}
388+
// TODO: we can probably better handle multiple declarators
389+
push_unsafe_statement(visited);
390+
continue;
391+
}
392+
393+
if (statement.type === 'ExpressionStatement') {
394+
const expression = statement.expression;
395+
396+
// Handle runes
397+
if (expression.type === 'CallExpression') {
398+
const rune = get_rune(expression, context.state.scope);
399+
400+
if (rune === '$effect' || rune === '$effect.pre') {
401+
target_block_statements.push(visited);
402+
continue;
403+
}
404+
}
405+
406+
// Assume all expression statement expressions are unsafe
407+
push_unsafe_statement(visited);
408+
continue;
409+
}
410+
411+
// Assume all other top-level statements are unsafe
412+
push_unsafe_statement(visited);
413+
}
414+
415+
return [
416+
new_statements,
417+
new_statements === target_block_statements ? null : target_block_statements
418+
];
419+
}

0 commit comments

Comments
 (0)