From ea139370de0ed0d04a05f9d87ea18e07cc97b723 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 15 Jan 2025 12:13:46 -0500 Subject: [PATCH] WIP --- .../src/compiler/phases/2-analyze/index.js | 2 ++ .../2-analyze/visitors/AwaitExpression.js | 14 ++++++++++ .../3-transform/client/transform-client.js | 2 ++ .../client/visitors/AwaitExpression.js | 16 +++++++++++ .../client/visitors/shared/fragment.js | 4 +-- .../client/visitors/shared/utils.js | 13 +++++---- packages/svelte/src/compiler/phases/nodes.js | 3 ++- packages/svelte/src/compiler/types/index.d.ts | 2 ++ packages/svelte/src/index-client.js | 2 -- .../internal/client/dom/blocks/boundary.js | 27 +++++++++++++++++++ packages/svelte/src/internal/client/index.js | 2 +- packages/svelte/types/index.d.ts | 1 + 12 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 76c1e94277..7557b62a8e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -20,6 +20,7 @@ import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js'; import { AssignmentExpression } from './visitors/AssignmentExpression.js'; import { Attribute } from './visitors/Attribute.js'; import { AwaitBlock } from './visitors/AwaitBlock.js'; +import { AwaitExpression } from './visitors/AwaitExpression.js'; import { BindDirective } from './visitors/BindDirective.js'; import { CallExpression } from './visitors/CallExpression.js'; import { ClassBody } from './visitors/ClassBody.js'; @@ -133,6 +134,7 @@ const visitors = { AssignmentExpression, Attribute, AwaitBlock, + AwaitExpression, BindDirective, CallExpression, ClassBody, diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js new file mode 100644 index 0000000000..633a496e05 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -0,0 +1,14 @@ +/** @import { AwaitExpression } from 'estree' */ +/** @import { Context } from '../types' */ + +/** + * @param {AwaitExpression} node + * @param {Context} context + */ +export function AwaitExpression(node, context) { + if (context.state.expression) { + context.state.expression.is_async = true; + } + + context.next(); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 582c32b534..822dfe6e5b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -12,6 +12,7 @@ import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js'; import { AssignmentExpression } from './visitors/AssignmentExpression.js'; import { Attribute } from './visitors/Attribute.js'; import { AwaitBlock } from './visitors/AwaitBlock.js'; +import { AwaitExpression } from './visitors/AwaitExpression.js'; import { BinaryExpression } from './visitors/BinaryExpression.js'; import { BindDirective } from './visitors/BindDirective.js'; import { BlockStatement } from './visitors/BlockStatement.js'; @@ -87,6 +88,7 @@ const visitors = { AssignmentExpression, Attribute, AwaitBlock, + AwaitExpression, BinaryExpression, BindDirective, BlockStatement, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js new file mode 100644 index 0000000000..8d819b7ed2 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -0,0 +1,16 @@ +/** @import { AwaitExpression, Expression } from 'estree' */ +/** @import { ComponentContext } from '../types' */ +import * as b from '../../../../utils/builders.js'; + +/** + * @param {AwaitExpression} node + * @param {ComponentContext} context + */ +export function AwaitExpression(node, context) { + return b.await( + b.call( + '$.preserve_context', + node.argument && /** @type {Expression} */ (context.visit(node.argument)) + ) + ); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js index 7674fd1eb2..f74fbfcf76 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js @@ -69,7 +69,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) { state.template.push(' '); - const { has_state, has_call, value } = build_template_chunk(sequence, visit, state); + const { has_state, has_call, is_async, value } = build_template_chunk(sequence, visit, state); // if this is a standalone `{expression}`, make sure we handle the case where // no text node was created because the expression was empty during SSR @@ -79,7 +79,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) { const update = b.stmt(b.call('$.set_text', id, value)); if (has_call && !within_bound_contenteditable) { - state.init.push(build_update(update)); + state.init.push(build_update(update, is_async)); } else if (has_state && !within_bound_contenteditable) { state.update.push(update); } else { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 1854baa1e9..f5b1abce39 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -14,7 +14,7 @@ import { locator } from '../../../../../state.js'; * @param {Array} values * @param {(node: AST.SvelteNode, state: any) => any} visit * @param {ComponentClientTransformState} state - * @returns {{ value: Expression, has_state: boolean, has_call: boolean }} + * @returns {{ value: Expression, has_state: boolean, has_call: boolean, is_async: boolean }} */ export function build_template_chunk(values, visit, state) { /** @type {Expression[]} */ @@ -25,6 +25,7 @@ export function build_template_chunk(values, visit, state) { let has_call = false; let has_state = false; + let is_async = false; let contains_multiple_call_expression = false; for (const node of values) { @@ -34,6 +35,7 @@ export function build_template_chunk(values, visit, state) { contains_multiple_call_expression ||= has_call && metadata.has_call; has_call ||= metadata.has_call; has_state ||= metadata.has_state; + is_async ||= metadata.is_async; } } @@ -68,7 +70,7 @@ export function build_template_chunk(values, visit, state) { } else if (values.length === 1) { // If we have a single expression, then pass that in directly to possibly avoid doing // extra work in the template_effect (instead we do the work in set_text). - return { value: visit(node.expression, state), has_state, has_call }; + return { value: visit(node.expression, state), has_state, has_call, is_async }; } else { expressions.push(b.logical('??', visit(node.expression, state), b.literal(''))); } @@ -84,17 +86,18 @@ export function build_template_chunk(values, visit, state) { const value = b.template(quasis, expressions); - return { value, has_state, has_call }; + return { value, has_state, has_call, is_async }; } /** * @param {Statement} statement + * @param {boolean} is_async */ -export function build_update(statement) { +export function build_update(statement, is_async) { const body = statement.type === 'ExpressionStatement' ? statement.expression : b.block([statement]); - return b.stmt(b.call('$.template_effect', b.thunk(body))); + return b.stmt(b.call('$.template_effect', b.thunk(body, is_async))); } /** diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index 5066833feb..22306989c8 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -58,6 +58,7 @@ export function create_expression_metadata() { return { dependencies: new Set(), has_state: false, - has_call: false + has_call: false, + is_async: false }; } diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index b80b717e42..2f5ec226bf 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -318,6 +318,8 @@ export interface ExpressionMetadata { has_state: boolean; /** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */ has_call: boolean; + /** True if the expression contains `await` */ + is_async: boolean; } export * from './template.js'; diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 2fdc8de0ba..587d766233 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -191,5 +191,3 @@ export { } from './internal/client/runtime.js'; export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; - -export { create_suspense } from './internal/client/dom/blocks/boundary.js'; diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index e2ed644699..9dcb54f05d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -248,3 +248,30 @@ export function create_suspense() { return [suspend, unsuspend]; } + +/** + * @template T + * @param {Promise} promise + * @returns {Promise} + */ +export async function preserve_context(promise) { + if (!active_effect) { + return promise; + } + + var previous_effect = active_effect; + var previous_reaction = active_reaction; + var previous_component_context = component_context; + + const [suspend, unsuspend] = create_suspense(); + + try { + suspend(); + return await promise; + } finally { + set_active_effect(previous_effect); + set_active_reaction(previous_reaction); + set_component_context(previous_component_context); + unsuspend(); + } +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 2bf58c51f7..5d852b6a13 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -129,7 +129,7 @@ export { update_store, mark_store_binding } from './reactivity/store.js'; -export { boundary } from './dom/blocks/boundary.js'; +export { boundary, preserve_context } from './dom/blocks/boundary.js'; export { set_text } from './render.js'; export { get, diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index d00b2b01ed..b65ab758ca 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -419,6 +419,7 @@ declare module 'svelte' { render: () => string; setup?: (element: Element) => void | (() => void); }): Snippet; + export function create_suspense(): (() => void)[]; /** Anything except a function */ type NotFunction = T extends Function ? never : T; /**