Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eleven-weeks-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

chore: refactor task microtask dispatching + boundary scheduling
2 changes: 2 additions & 0 deletions packages/svelte/src/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,3 +500,5 @@ declare namespace $host {
/** @deprecated */
export const toString: never;
}

declare function $await<V>(value: Promise<V>): [V, undefined | Promise<V>];
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export function Attribute(node, context) {

node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
chunk.metadata.expression.dependencies.forEach((dependency) =>
node.metadata.expression.dependencies.add(dependency)
);
chunk.metadata.expression.async_dependencies.forEach((dependency) =>
node.metadata.expression.async_dependencies.add(dependency)
);
}

if (is_event_attribute(node)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/** @import { Context } from '../types' */
import { get_rune } from '../../scope.js';
import * as e from '../../../errors.js';
import { get_parent, unwrap_optional } from '../../../utils/ast.js';
import { extract_identifiers, get_parent, unwrap_optional } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
import { dev, locate_node, source } from '../../../state.js';
import * as b from '../../../utils/builders.js';
Expand Down Expand Up @@ -123,6 +123,52 @@ export function CallExpression(node, context) {

break;

case '$await': {
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}

const declarator = context.path.at(-1);
const declaration = context.path.at(-2);
const program = context.path.at(-3);

if (context.state.ast_type !== 'instance') {
throw new Error('TODO: $await can only be used at the top-level of a component');
}
if (
declarator?.type !== 'VariableDeclarator' ||
declarator?.id.type !== 'ArrayPattern' ||
declaration?.type !== 'VariableDeclaration' ||
declaration?.declarations.length !== 1 ||
context.state.function_depth !== 1 ||
program?.type !== 'Program'
) {
throw new Error('TODO: invalid usage of $await in component');
}

const [async_derived, derived_promise] = declarator.id.elements;

if (async_derived) {
for (const id of extract_identifiers(async_derived)) {
const binding = context.state.scope.get(id.name);
if (binding !== null) {
binding.kind = 'async_derived';
}
}
}

if (derived_promise) {
for (const id of extract_identifiers(derived_promise)) {
const binding = context.state.scope.get(id.name);
if (binding !== null) {
binding.kind = 'derived';
}
}
}

break;
}

case '$inspect':
if (node.arguments.length < 1) {
e.rune_invalid_arguments_length(node, rune, 'one or more arguments');
Expand Down Expand Up @@ -207,7 +253,7 @@ export function CallExpression(node, context) {
}

// `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning
if (rune === '$inspect' || rune === '$derived') {
if (rune === '$inspect' || rune === '$derived' || rune === '$await') {
context.next({ ...context.state, function_depth: context.state.function_depth + 1 });
} else {
context.next();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,25 @@ export function Identifier(node, context) {
}

if (binding) {
if (context.state.expression) {
context.state.expression.dependencies.add(binding);
context.state.expression.has_state ||= binding.kind !== 'normal';
if (binding.kind === 'async_derived') {
debugger
}

const expression = context.state.expression;

if (expression) {
expression.dependencies.add(binding);

if (
binding.kind === 'async_derived'
) {
expression.async_dependencies.add(binding);
}
expression.has_state ||= binding.kind !== 'normal';

binding.async_dependencies.forEach((dep) => {
expression.async_dependencies.add(dep);
});
}

if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';

const valid = ['onerror', 'failed'];
const valid = ['onerror', 'failed', 'pending'];

/**
* @param {AST.SvelteBoundary} node
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ensure_no_module_import_conflict, validate_identifier_name } from './sh
import * as e from '../../../errors.js';
import { extract_paths } from '../../../utils/ast.js';
import { equal } from '../../../utils/assert.js';
import { create_expression_metadata } from '../../nodes.js';

/**
* @param {VariableDeclarator} node
Expand All @@ -29,21 +30,42 @@ export function VariableDeclarator(node, context) {
rune === '$state.raw' ||
rune === '$derived' ||
rune === '$derived.by' ||
rune === '$props'
rune === '$props' ||
rune === '$await'
) {
for (const path of paths) {
// @ts-ignore this fails in CI for some insane reason
const binding = /** @type {Binding} */ (context.state.scope.get(path.node.name));
binding.kind =
rune === '$state'
? 'state'
: rune === '$state.raw'
? 'raw_state'
: rune === '$derived' || rune === '$derived.by'
? 'derived'
: path.is_rest
? 'rest_prop'
: 'prop';
let metadata = create_expression_metadata();

if (rune !== '$await') {
for (const path of paths) {
// @ts-ignore this fails in CI for some insane reason
const binding = /** @type {Binding} */ (context.state.scope.get(path.node.name));
binding.kind =
rune === '$state'
? 'state'
: rune === '$state.raw'
? 'raw_state'
: rune === '$derived' || rune === '$derived.by'
? 'derived'
: path.is_rest
? 'rest_prop'
: 'prop';
}
}

context.visit(node.id);
if (node.init) {
context.visit(node.init);
if (node.init.type === 'CallExpression' && node.init.arguments.length > 0) {
context.visit(node.init.arguments[0], { ...context.state, expression: metadata });

if (metadata.async_dependencies.size > 0) {
for (const path of paths) {
// @ts-ignore
const binding = /** @type {Binding} */ (context.state.scope.get(path.node.name));
metadata.async_dependencies.forEach((dep) => binding.async_dependencies.add(dep));
}
}
}
}
}

Expand Down Expand Up @@ -103,6 +125,8 @@ export function VariableDeclarator(node, context) {
}
}
}

return;
} else {
if (node.init?.type === 'CallExpression') {
const callee = node.init.callee;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ export function client_component(analysis, options) {
update: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null),
locations: /** @type {any} */ (null)
locations: /** @type {any} */ (null),
target_statements: null
};

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

const target_statements = instance_state.target_statements;

const template = /** @type {ESTree.Program} */ (
walk(
/** @type {AST.SvelteNode} */ (analysis.template.ast),
Expand Down Expand Up @@ -351,17 +354,24 @@ export function client_component(analysis, options) {
const push_args = [b.id('$$props'), b.literal(analysis.runes)];
if (dev) push_args.push(b.id(analysis.name));

const component_block = b.block([
const component_block_statements = [
...store_setup,
...legacy_reactive_declarations,
...group_binding_declarations,
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body),
analysis.runes || !analysis.needs_context
? b.empty
: b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)),
.../** @type {ESTree.Statement[]} */ (template.body)
]);
: b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))
];

if (target_statements === null) {
component_block_statements.push(.../** @type {ESTree.Statement[]} */ (template.body));
} else {
target_statements.push(.../** @type {ESTree.Statement[]} */ (template.body));
}

const component_block = b.block(component_block_statements);

if (!analysis.runes) {
// Bind static exports to props so that people can access them with bind:x
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import type {
Expression,
AssignmentExpression,
UpdateExpression,
VariableDeclaration
VariableDeclaration,
Directive
} from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
Expand Down Expand Up @@ -91,6 +92,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly instance_level_snippets: VariableDeclaration[];
/** Snippets hoisted to the module */
readonly module_level_snippets: VariableDeclaration[];

target_statements: null | Array<ModuleDeclaration | Statement | Directive>;
}

export interface StateField {
Expand Down
107 changes: 106 additions & 1 deletion packages/svelte/src/compiler/phases/3-transform/client/utils.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */
/** @import { ArrowFunctionExpression, Expression, CallExpression, VariableDeclarator, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement, VariableDeclaration, ModuleDeclaration, Directive } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { Analysis } from '../../types.js' */
/** @import { Scope } from '../../scope.js' */
import * as b from '../../../utils/builders.js';
import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js';
import { get_rune } from '../../scope.js';
import {
PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE,
Expand Down Expand Up @@ -312,3 +313,107 @@ export function create_derived_block_argument(node, context) {
export function create_derived(state, arg) {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
}

/**
* @param {(ModuleDeclaration | Statement | Directive)[]} statements
* @param {ComponentContext} context
* @returns {[(ModuleDeclaration | Statement | Directive)[], null | (ModuleDeclaration | Statement | Directive)[]]}
*/
export function wrap_unsafe_async_statements(statements, context) {
/** @type {(ModuleDeclaration | Statement | Directive)[]} */
const new_statements = [];
let target_block_statements = new_statements;
let is_unsafe = true;

const push_unsafe_statement = (/** @type {Statement} */ statement) => {
if (is_unsafe) {
const block_statments = [statement];
const script_template = b.stmt(b.call('$.script_effect', b.thunk(b.block(block_statments))));
target_block_statements.push(script_template);
target_block_statements = block_statments;
is_unsafe = false;
} else {
target_block_statements.push(statement);
}
};

for (const statement of statements) {
const visited = /** @type {Statement} */ (context.visit(statement));

if (
statement.type === 'FunctionDeclaration' ||
statement.type === 'ClassDeclaration' ||
statement.type === 'EmptyStatement' ||
statement.type === 'ImportDeclaration' ||
statement.type === 'ExportNamedDeclaration' ||
statement.type === 'ExportAllDeclaration' ||
statement.type === 'ExportDefaultDeclaration'
) {
target_block_statements.push(visited);
continue;
}

if (statement.type === 'VariableDeclaration') {
if (statement.declarations.length === 1) {
const declarator = statement.declarations[0];
const init = declarator.init;

// Safe declaration
if (
init == null ||
init.type === 'Literal' ||
init.type === 'FunctionExpression' ||
init.type === 'ArrowFunctionExpression' ||
(init.type === 'ArrayExpression' && init.elements.length === 0) ||
(init.type === 'ObjectExpression' && init.properties.length === 0)
) {
target_block_statements.push(visited);
continue;
}
// Handle runes
if (init.type === 'CallExpression') {
const rune = get_rune(init, context.state.scope);

if (rune === '$props' || rune === '$derived' || rune === '$derived.by') {
target_block_statements.push(visited);
continue;
}
if (rune === '$await') {
target_block_statements.push(visited);
is_unsafe = true;
continue;
}
}
}
// TODO: we can probably better handle multiple declarators
push_unsafe_statement(visited);
continue;
}

if (statement.type === 'ExpressionStatement') {
const expression = statement.expression;

// Handle runes
if (expression.type === 'CallExpression') {
const rune = get_rune(expression, context.state.scope);

if (rune === '$effect' || rune === '$effect.pre') {
target_block_statements.push(visited);
continue;
}
}

// Assume all expression statement expressions are unsafe
push_unsafe_statement(visited);
continue;
}

// Assume all other top-level statements are unsafe
push_unsafe_statement(visited);
}

return [
new_statements,
new_statements === target_block_statements ? null : target_block_statements
];
}
Loading
Loading