From baba2638c9a9a06190a8c08150cdf8641be60969 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Jan 2025 22:07:47 -0500 Subject: [PATCH] render tags --- .../src/compiler/phases/1-parse/state/tag.js | 2 +- .../src/compiler/phases/2-analyze/index.js | 2 - .../src/compiler/phases/2-analyze/types.d.ts | 2 - .../2-analyze/visitors/AwaitExpression.js | 8 ++- .../2-analyze/visitors/CallExpression.js | 14 +--- .../phases/2-analyze/visitors/RenderTag.js | 16 ++++- .../3-transform/client/visitors/RenderTag.js | 69 +++++++++++++++---- .../svelte/src/compiler/types/template.d.ts | 2 +- .../samples/async-render-tag/_config.js | 2 - 9 files changed, 78 insertions(+), 39 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 78820d0fa1..c57b445d34 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -715,7 +715,7 @@ function special(parser) { expression: /** @type {AST.RenderTag['expression']} */ (expression), metadata: { dynamic: false, - args_with_call_expression: new Set(), + arguments: [], path: [], snippets: new Set() } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 1712702157..4fc43151ec 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -618,7 +618,6 @@ export function analyze_component(root, source, options) { has_props_rune: false, component_slots: new Set(), expression: null, - render_tag: null, private_derived_state: [], function_depth: scope.function_depth, instance_scope: instance.scope, @@ -690,7 +689,6 @@ export function analyze_component(root, source, options) { reactive_statements: analysis.reactive_statements, component_slots: new Set(), expression: null, - render_tag: null, private_derived_state: [], function_depth: scope.function_depth }; diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index b4ca4dc262..1e71accb9f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -19,8 +19,6 @@ export interface AnalysisState { component_slots: Set; /** Information about the current expression/directive/block value */ expression: ExpressionMetadata | null; - /** The current {@render ...} tag, if any */ - render_tag: null | AST.RenderTag; private_derived_state: string[]; function_depth: number; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index c176eec3f4..178b817903 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -20,8 +20,12 @@ export function AwaitExpression(node, context) { while (i--) { const parent = context.path[i]; - // @ts-expect-error we could probably use a neater/more robust mechanism - if (parent.metadata?.expression === context.state.expression) { + if ( + // @ts-expect-error we could probably use a neater/more robust mechanism + parent.metadata?.expression === context.state.expression || + // @ts-expect-error + parent.metadata?.arguments?.includes(context.state.expression) + ) { break; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 6755193d3c..c7bbb61542 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -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 { get_parent } 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'; @@ -187,18 +187,6 @@ export function CallExpression(node, context) { break; } - if (context.state.render_tag) { - // Find out which of the render tag arguments contains this call expression - const arg_idx = unwrap_optional(context.state.render_tag.expression).arguments.findIndex( - (arg) => arg === node || context.path.includes(arg) - ); - - // -1 if this is the call expression of the render tag itself - if (arg_idx !== -1) { - context.state.render_tag.metadata.args_with_call_expression.add(arg_idx); - } - } - if (node.callee.type === 'Identifier') { const binding = context.state.scope.get(node.callee.name); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js index 045224276a..a8c9d408bd 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js @@ -5,6 +5,7 @@ import * as e from '../../../errors.js'; import { validate_opening_tag } from './shared/utils.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; import { is_resolved_snippet } from './shared/snippets.js'; +import { create_expression_metadata } from '../../nodes.js'; /** * @param {AST.RenderTag} node @@ -15,7 +16,8 @@ export function RenderTag(node, context) { node.metadata.path = [...context.path]; - const callee = unwrap_optional(node.expression).callee; + const expression = unwrap_optional(node.expression); + const callee = expression.callee; const binding = callee.type === 'Identifier' ? context.state.scope.get(callee.name) : null; @@ -52,5 +54,15 @@ export function RenderTag(node, context) { mark_subtree_dynamic(context.path); - context.next({ ...context.state, render_tag: node }); + context.visit(callee); + + for (const arg of expression.arguments) { + const metadata = create_expression_metadata(); + node.metadata.arguments.push(metadata); + + context.visit(arg, { + ...context.state, + expression: metadata + }); + } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index 7da987f6cc..615cd0097f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -1,8 +1,10 @@ -/** @import { Expression } from 'estree' */ +/** @import { Expression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ -/** @import { ComponentContext } from '../types' */ +/** @import { ComponentContext, MemoizedExpression } from '../types' */ import { unwrap_optional } from '../../../../utils/ast.js'; import * as b from '../../../../utils/builders.js'; +import { create_derived } from '../utils.js'; +import { get_expression_id } from './shared/utils.js'; /** * @param {AST.RenderTag} node @@ -10,23 +12,44 @@ import * as b from '../../../../utils/builders.js'; */ export function RenderTag(node, context) { context.state.template.push(''); - const callee = unwrap_optional(node.expression).callee; - const raw_args = unwrap_optional(node.expression).arguments; + + const expression = unwrap_optional(node.expression); + + const callee = expression.callee; + const raw_args = expression.arguments; /** @type {Expression[]} */ let args = []; + + /** @type {MemoizedExpression[]} */ + const expressions = []; + + /** @type {MemoizedExpression[]} */ + const async_expressions = []; + for (let i = 0; i < raw_args.length; i++) { - const raw = raw_args[i]; - const arg = /** @type {Expression} */ (context.visit(raw)); - if (node.metadata.args_with_call_expression.has(i)) { - const id = b.id(context.state.scope.generate('render_arg')); - context.state.init.push(b.var(id, b.call('$.derived_safe_equal', b.thunk(arg)))); - args.push(b.thunk(b.call('$.get', id))); - } else { - args.push(b.thunk(arg)); + let expression = /** @type {Expression} */ (context.visit(raw_args[i])); + const { has_call, is_async } = node.metadata.arguments[i]; + + if (is_async || has_call) { + expression = b.call( + '$.get', + get_expression_id(is_async ? async_expressions : expressions, expression) + ); } + + args.push(b.thunk(expression)); } + [...async_expressions, ...expressions].forEach((memo, i) => { + memo.id.name = `$${i}`; + }); + + /** @type {Statement[]} */ + const statements = expressions.map((memo, i) => + b.var(memo.id, create_derived(context.state, b.thunk(memo.expression))) + ); + let snippet_function = /** @type {Expression} */ (context.visit(callee)); if (node.metadata.dynamic) { @@ -35,11 +58,11 @@ export function RenderTag(node, context) { snippet_function = b.logical('??', snippet_function, b.id('$.noop')); } - context.state.init.push( + statements.push( b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args)) ); } else { - context.state.init.push( + statements.push( b.stmt( (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( snippet_function, @@ -49,4 +72,22 @@ export function RenderTag(node, context) { ) ); } + + if (async_expressions.length > 0) { + context.state.init.push( + b.stmt( + b.call( + '$.async', + context.state.node, + b.array(async_expressions.map((memo) => b.thunk(memo.expression, true))), + b.arrow( + [context.state.node, ...async_expressions.map((memo) => memo.id)], + b.block(statements) + ) + ) + ) + ); + } else { + context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements)); + } } diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index c16c161e86..6bc1329d70 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -166,7 +166,7 @@ export namespace AST { /** @internal */ metadata: { dynamic: boolean; - args_with_call_expression: Set; + arguments: ExpressionMetadata[]; path: SvelteNode[]; /** The set of locally-defined snippets that this render tag could correspond to, * used for CSS pruning purposes */ diff --git a/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js index 04f5cc71a0..566bd2210b 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js @@ -6,8 +6,6 @@ import { test } from '../../test'; let d; export default test({ - skip: true, - html: `

pending

`, get props() {