From 75ea6da9cd7a74b8eb54ca4009d08beb9667c66b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 25 Jul 2024 09:51:11 -0400 Subject: [PATCH] chore: modularize server code (#12596) * start modularizing server code * more * more * more * more * alphabetize * start on JS visitors * more * more * more * more * less * more * alphabetize * lint * combine into single visitors folder * alphabetize --- .../3-transform/server/transform-server.js | 2023 +---------------- .../server/visitors/AssignmentExpression.js | 11 + .../3-transform/server/visitors/AwaitBlock.js | 33 + .../server/visitors/CallExpression.js | 44 + .../3-transform/server/visitors/ClassBody.js | 118 + .../3-transform/server/visitors/Component.js | 12 + .../3-transform/server/visitors/ConstTag.js | 16 + .../3-transform/server/visitors/DebugTag.js | 24 + .../3-transform/server/visitors/EachBlock.js | 63 + .../server/visitors/ExpressionStatement.js | 29 + .../3-transform/server/visitors/Fragment.js | 46 + .../3-transform/server/visitors/HtmlTag.js | 13 + .../3-transform/server/visitors/Identifier.js | 19 + .../3-transform/server/visitors/IfBlock.js | 28 + .../3-transform/server/visitors/KeyBlock.js | 16 + .../server/visitors/LabeledStatement.js | 23 + .../server/visitors/LetDirective.js | 40 + .../server/visitors/MemberExpression.js | 19 + .../server/visitors/PropertyDefinition.js | 36 + .../server/visitors/RegularElement.js | 106 + .../3-transform/server/visitors/RenderTag.js | 35 + .../server/visitors/SlotElement.js | 60 + .../server/visitors/SnippetBlock.js | 22 + .../server/visitors/SpreadAttribute.js | 10 + .../server/visitors/SvelteComponent.js | 16 + .../server/visitors/SvelteElement.js | 60 + .../server/visitors/SvelteFragment.js | 26 + .../3-transform/server/visitors/SvelteHead.js | 16 + .../3-transform/server/visitors/SvelteSelf.js | 12 + .../server/visitors/TitleElement.js | 17 + .../server/visitors/UpdateExpression.js | 26 + .../server/visitors/VariableDeclaration.js | 172 ++ .../server/visitors/shared/component.js | 235 ++ .../server/visitors/shared/element.js | 442 ++++ .../server/visitors/shared/utils.js | 365 +++ 35 files changed, 2306 insertions(+), 1927 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/Component.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/DebugTag.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/ExpressionStatement.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/Identifier.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/KeyBlock.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/LabeledStatement.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/LetDirective.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/PropertyDefinition.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/SpreadAttribute.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteComponent.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteFragment.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteSelf.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/TitleElement.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/UpdateExpression.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 913206ab13..f18df26eb4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -1,1926 +1,98 @@ -/** @import { AssignmentExpression, AssignmentOperator, BinaryOperator, BlockStatement, CallExpression, Expression, ExpressionStatement, Identifier, Literal, MethodDefinition, Node, Pattern, Program, Property, PropertyDefinition, Statement, TemplateElement, VariableDeclarator } from 'estree' */ -/** @import { Location } from 'locate-character' */ -/** @import { Attribute, Binding, ClassDirective, Comment, Component, ExpressionTag, Namespace, RegularElement, SpreadAttribute, StyleDirective, SvelteComponent, SvelteElement, SvelteNode, SvelteSelf, TemplateNode, Text, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ -/** @import { ComponentContext, ComponentServerTransformState, ComponentVisitors, ServerTransformState, Visitors } from './types.js' */ +/** @import { Program, Property, Statement, VariableDeclarator } from 'estree' */ +/** @import { SvelteNode, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ +/** @import { ComponentServerTransformState, ComponentVisitors, ServerTransformState, Visitors } from './types.js' */ /** @import { Analysis, ComponentAnalysis } from '../../types.js' */ -/** @import { Scope } from '../../scope.js' */ -/** @import { StateField } from '../../3-transform/client/types.js' */ // TODO move this type import { walk } from 'zimmerframe'; -import { set_scope, get_rune } from '../../scope.js'; -import { - extract_identifiers, - extract_paths, - get_attribute_chunks, - is_event_attribute, - is_expression_async, - is_text_attribute, - unwrap_optional -} from '../../../utils/ast.js'; -import * as b from '../../../utils/builders.js'; -import is_reference from 'is-reference'; -import { - ContentEditableBindings, - LoadErrorElements, - VoidElements, - WhitespaceInsensitiveAttributes -} from '../../constants.js'; -import { - clean_nodes, - determine_namespace_for_children, - infer_namespace, - transform_inspect_rune -} from '../utils.js'; -import { create_attribute, is_custom_element_node, is_element_node } from '../../nodes.js'; -import { binding_properties } from '../../bindings.js'; -import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patterns.js'; -import { - DOMBooleanAttributes, - ELEMENT_IS_NAMESPACED, - ELEMENT_PRESERVE_ATTRIBUTE_CASE -} from '../../../../constants.js'; -import { escape_html } from '../../../../escaping.js'; -import { sanitize_template_string } from '../../../utils/sanitize_template_string.js'; -import { - EMPTY_COMMENT, - BLOCK_CLOSE, - BLOCK_OPEN, - BLOCK_OPEN_ELSE -} from '../../../../internal/server/hydration.js'; -import { filename, locator } from '../../../state.js'; -import { render_stylesheet } from '../css/index.js'; - -/** Opens an if/each block, so that we can remove nodes in the case of a mismatch */ -const block_open = b.literal(BLOCK_OPEN); - -/** Closes an if/each block, so that we can remove nodes in the case of a mismatch. Also serves as an anchor for these blocks */ -const block_close = b.literal(BLOCK_CLOSE); - -/** Empty comment to keep text nodes separate, or provide an anchor node for blocks */ -const empty_comment = b.literal(EMPTY_COMMENT); - -/** - * @param {Node} node - * @returns {node is Statement} - */ -function is_statement(node) { - return node.type.endsWith('Statement') || node.type.endsWith('Declaration'); -} - -/** - * @param {Array} template - * @param {Identifier} out - * @param {AssignmentOperator} operator - * @returns {Statement[]} - */ -function serialize_template(template, out = b.id('$$payload.out'), operator = '+=') { - /** @type {TemplateElement[]} */ - let quasis = []; - - /** @type {Expression[]} */ - let expressions = []; - - /** @type {Statement[]} */ - const statements = []; - - const flush = () => { - statements.push(b.stmt(b.assignment(operator, out, b.template(quasis, expressions)))); - quasis = []; - expressions = []; - }; - - for (let i = 0; i < template.length; i++) { - const node = template[i]; - - if (is_statement(node)) { - if (quasis.length !== 0) { - flush(); - } - - statements.push(node); - } else { - let last = quasis.at(-1); - if (!last) quasis.push((last = b.quasi('', false))); - - if (node.type === 'Literal') { - last.value.raw += - typeof node.value === 'string' ? sanitize_template_string(node.value) : node.value; - } else if (node.type === 'TemplateLiteral') { - last.value.raw += node.quasis[0].value.raw; - quasis.push(...node.quasis.slice(1)); - expressions.push(...node.expressions); - } else { - expressions.push(node); - quasis.push(b.quasi('', i + 1 === template.length || is_statement(template[i + 1]))); - } - } - } - - if (quasis.length !== 0) { - flush(); - } - - return statements; -} - -/** - * Processes an array of template nodes, joining sibling text/expression nodes and - * recursing into child nodes. - * @param {Array} nodes - * @param {ComponentContext} context - */ -function process_children(nodes, { visit, state }) { - /** @type {Array} */ - let sequence = []; - - function flush() { - let quasi = b.quasi('', false); - const quasis = [quasi]; - - /** @type {Expression[]} */ - const expressions = []; - - for (let i = 0; i < sequence.length; i++) { - const node = sequence[i]; - - if (node.type === 'Text' || node.type === 'Comment') { - quasi.value.raw += sanitize_template_string( - node.type === 'Comment' ? `` : escape_html(node.data) - ); - } else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') { - if (node.expression.value != null) { - quasi.value.raw += sanitize_template_string(escape_html(node.expression.value + '')); - } - } else { - expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression)))); - - quasi = b.quasi('', i + 1 === sequence.length); - quasis.push(quasi); - } - } - - state.template.push(b.template(quasis, expressions)); - } - - for (let i = 0; i < nodes.length; i += 1) { - const node = nodes[i]; - - if (node.type === 'Text' || node.type === 'Comment' || node.type === 'ExpressionTag') { - sequence.push(node); - } else { - if (sequence.length > 0) { - flush(); - sequence = []; - } - - visit(node, { ...state }); - } - } - - if (sequence.length > 0) { - flush(); - } -} - -/** - * @param {VariableDeclarator} declarator - * @param {Scope} scope - * @param {Expression} value - * @returns {VariableDeclarator[]} - */ -function create_state_declarators(declarator, scope, value) { - if (declarator.id.type === 'Identifier') { - return [b.declarator(declarator.id, value)]; - } - - const tmp = scope.generate('tmp'); - const paths = extract_paths(declarator.id); - return [ - b.declarator(b.id(tmp), value), // TODO inject declarator for opts, so we can use it below - ...paths.map((path) => { - const value = path.expression?.(b.id(tmp)); - return b.declarator(path.node, value); - }) - ]; -} - -/** - * @param {Identifier} node - * @param {ServerTransformState} state - * @returns {Expression} - */ -function serialize_get_binding(node, state) { - const binding = state.scope.get(node.name); - - if (binding === null || node === binding.node) { - // No associated binding or the declaration itself which shouldn't be transformed - return node; - } - - if (binding.kind === 'store_sub') { - const store_id = b.id(node.name.slice(1)); - return b.call( - '$.store_get', - b.assignment('??=', b.id('$$store_subs'), b.object([])), - b.literal(node.name), - serialize_get_binding(store_id, state) - ); - } - - if (Object.hasOwn(state.getters, node.name)) { - const getter = state.getters[node.name]; - return typeof getter === 'function' ? getter(node) : getter; - } - - return node; -} - -/** - * @param {AssignmentExpression} node - * @param {Pick, 'visit' | 'state'>} context - */ -function get_assignment_value(node, { state, visit }) { - if (node.left.type === 'Identifier') { - const operator = node.operator; - return operator === '=' - ? /** @type {Expression} */ (visit(node.right)) - : // turn something like x += 1 into x = x + 1 - b.binary( - /** @type {BinaryOperator} */ (operator.slice(0, -1)), - serialize_get_binding(node.left, state), - /** @type {Expression} */ (visit(node.right)) - ); - } - - return /** @type {Expression} */ (visit(node.right)); -} - -/** - * @param {string} name - */ -function is_store_name(name) { - return name[0] === '$' && /[A-Za-z_]/.test(name[1]); -} - -/** - * @param {AssignmentExpression} node - * @param {import('zimmerframe').Context} context - * @param {() => any} fallback - * @returns {Expression} - */ -function serialize_set_binding(node, context, fallback) { - const { state, visit } = context; - - if ( - node.left.type === 'ArrayPattern' || - node.left.type === 'ObjectPattern' || - node.left.type === 'RestElement' - ) { - // Turn assignment into an IIFE, so that `$.set` calls etc don't produce invalid code - const tmp_id = context.state.scope.generate('tmp'); - - /** @type {AssignmentExpression[]} */ - const original_assignments = []; - - /** @type {Expression[]} */ - const assignments = []; - - const paths = extract_paths(node.left); - - for (const path of paths) { - const value = path.expression?.(b.id(tmp_id)); - const assignment = b.assignment('=', path.node, value); - original_assignments.push(assignment); - assignments.push(serialize_set_binding(assignment, context, () => assignment)); - } - - if (assignments.every((assignment, i) => assignment === original_assignments[i])) { - // No change to output -> nothing to transform -> we can keep the original assignment - return fallback(); - } - - return b.call( - b.thunk( - b.block([ - b.const(tmp_id, /** @type {Expression} */ (visit(node.right))), - b.stmt(b.sequence(assignments)), - b.return(b.id(tmp_id)) - ]) - ) - ); - } - - if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') { - throw new Error(`Unexpected assignment type ${node.left.type}`); - } - - let left = node.left; - - while (left.type === 'MemberExpression') { - // @ts-expect-error - left = left.object; - } - - if (left.type !== 'Identifier') { - return fallback(); - } - - const is_store = is_store_name(left.name); - const left_name = is_store ? left.name.slice(1) : left.name; - const binding = state.scope.get(left_name); - - if (!binding) return fallback(); - - if (binding.mutation !== null) { - return binding.mutation(node, context); - } - - if ( - binding.kind !== 'state' && - binding.kind !== 'frozen_state' && - binding.kind !== 'prop' && - binding.kind !== 'bindable_prop' && - binding.kind !== 'each' && - binding.kind !== 'legacy_reactive' && - !is_store - ) { - // TODO error if it's a computed (or rest prop)? or does that already happen elsewhere? - return fallback(); - } - - const value = get_assignment_value(node, { state, visit }); - if (left === node.left) { - if (is_store) { - return b.call('$.store_set', b.id(left_name), /** @type {Expression} */ (visit(node.right))); - } - return fallback(); - } else if (is_store) { - return b.call( - '$.mutate_store', - b.assignment('??=', b.id('$$store_subs'), b.object([])), - b.literal(left.name), - b.id(left_name), - b.assignment(node.operator, /** @type {Pattern} */ (visit(node.left)), value) - ); - } - return fallback(); -} - -/** - * @param {RegularElement | SvelteElement} element - * @param {Attribute} attribute - * @param {{ state: { namespace: Namespace }}} context - */ -function get_attribute_name(element, attribute, context) { - let name = attribute.name; - if (!element.metadata.svg && !element.metadata.mathml && context.state.namespace !== 'foreign') { - name = name.toLowerCase(); - // don't lookup boolean aliases here, the server runtime function does only - // check for the lowercase variants of boolean attributes - } - return name; -} - -/** @type {Visitors} */ -const global_visitors = { - Identifier(node, { path, state }) { - if (is_reference(node, /** @type {Node} */ (path.at(-1)))) { - if (node.name === '$$props') { - return b.id('$$sanitized_props'); - } - return serialize_get_binding(node, state); - } - }, - AssignmentExpression(node, context) { - return serialize_set_binding(node, context, context.next); - }, - UpdateExpression(node, context) { - const { state, next } = context; - const argument = node.argument; - - if (argument.type === 'Identifier' && state.scope.get(argument.name)?.kind === 'store_sub') { - return b.call( - node.prefix ? '$.update_store_pre' : '$.update_store', - b.assignment('??=', b.id('$$store_subs'), b.object([])), - b.literal(argument.name), - b.id(argument.name.slice(1)), - node.operator === '--' && b.literal(-1) - ); - } - - return next(); - }, - CallExpression(node, context) { - const rune = get_rune(node, context.state.scope); - - if (rune === '$host') { - return b.id('undefined'); - } - - if (rune === '$effect.tracking') { - return b.literal(false); - } - - if (rune === '$effect.root') { - // ignore $effect.root() calls, just return a noop which mimics the cleanup function - return b.arrow([], b.block([])); - } - - if (rune === '$state.snapshot') { - return b.call('$.snapshot', /** @type {Expression} */ (context.visit(node.arguments[0]))); - } - - if (rune === '$state.is') { - return b.call( - 'Object.is', - /** @type {Expression} */ (context.visit(node.arguments[0])), - /** @type {Expression} */ (context.visit(node.arguments[1])) - ); - } - - if (rune === '$inspect' || rune === '$inspect().with') { - return transform_inspect_rune(node, context); - } - - context.next(); - } -}; - -/** @type {Visitors} */ -const javascript_visitors_runes = { - ClassBody(node, { state, visit }) { - /** @type {Map} */ - const public_derived = new Map(); - - /** @type {Map} */ - const private_derived = new Map(); - - /** @type {string[]} */ - const private_ids = []; - - for (const definition of node.body) { - if ( - definition.type === 'PropertyDefinition' && - (definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier') - ) { - const { type, name } = definition.key; - - const is_private = type === 'PrivateIdentifier'; - if (is_private) private_ids.push(name); - - if (definition.value?.type === 'CallExpression') { - const rune = get_rune(definition.value, state.scope); - if (rune === '$derived' || rune === '$derived.by') { - /** @type {StateField} */ - const field = { - kind: rune === '$derived.by' ? 'derived_call' : 'derived', - // @ts-expect-error this is set in the next pass - id: is_private ? definition.key : null - }; - - if (is_private) { - private_derived.set(name, field); - } else { - public_derived.set(name, field); - } - } - } - } - } - - // each `foo = $derived()` needs a backing `#foo` field - for (const [name, field] of public_derived) { - let deconflicted = name; - while (private_ids.includes(deconflicted)) { - deconflicted = '_' + deconflicted; - } - - private_ids.push(deconflicted); - field.id = b.private_id(deconflicted); - } - - /** @type {Array} */ - const body = []; - - const child_state = { ...state, private_derived }; - - // Replace parts of the class body - for (const definition of node.body) { - if ( - definition.type === 'PropertyDefinition' && - (definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier') - ) { - const name = definition.key.name; - - const is_private = definition.key.type === 'PrivateIdentifier'; - const field = (is_private ? private_derived : public_derived).get(name); - - if (definition.value?.type === 'CallExpression' && field !== undefined) { - const init = /** @type {Expression} **/ ( - visit(definition.value.arguments[0], child_state) - ); - const value = - field.kind === 'derived_call' - ? b.call('$.once', init) - : b.call('$.once', b.thunk(init)); - - if (is_private) { - body.push(b.prop_def(field.id, value)); - } else { - // #foo; - const member = b.member(b.this, field.id); - body.push(b.prop_def(field.id, value)); - - // get foo() { return this.#foo; } - body.push(b.method('get', definition.key, [], [b.return(b.call(member))])); - - if ((field.kind === 'derived' || field.kind === 'derived_call') && state.options.dev) { - body.push( - b.method( - 'set', - definition.key, - [b.id('_')], - [b.throw_error(`Cannot update a derived property ('${name}')`)] - ) - ); - } - } - - continue; - } - } - - body.push(/** @type {MethodDefinition} **/ (visit(definition, child_state))); - } - - return { ...node, body }; - }, - PropertyDefinition(node, { state, next, visit }) { - if (node.value != null && node.value.type === 'CallExpression') { - const rune = get_rune(node.value, state.scope); - - if (rune === '$state' || rune === '$state.frozen' || rune === '$derived') { - return { - ...node, - value: - node.value.arguments.length === 0 - ? null - : /** @type {Expression} */ (visit(node.value.arguments[0])) - }; - } - if (rune === '$derived.by') { - return { - ...node, - value: - node.value.arguments.length === 0 - ? null - : b.call(/** @type {Expression} */ (visit(node.value.arguments[0]))) - }; - } - } - next(); - }, - VariableDeclaration(node, { state, visit }) { - const declarations = []; - - for (const declarator of node.declarations) { - const init = declarator.init; - const rune = get_rune(init, state.scope); - if (!rune || rune === '$effect.tracking' || rune === '$inspect' || rune === '$effect.root') { - declarations.push(/** @type {VariableDeclarator} */ (visit(declarator))); - continue; - } - - if (rune === '$props') { - // remove $bindable() from props declaration - const id = walk(declarator.id, null, { - AssignmentPattern(node) { - if ( - node.right.type === 'CallExpression' && - get_rune(node.right, state.scope) === '$bindable' - ) { - const right = node.right.arguments.length - ? /** @type {Expression} */ (visit(node.right.arguments[0])) - : b.id('undefined'); - return b.assignment_pattern(node.left, right); - } - } - }); - declarations.push(b.declarator(id, b.id('$$props'))); - continue; - } - - const args = /** @type {CallExpression} */ (init).arguments; - const value = - args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (visit(args[0])); - - if (rune === '$derived.by') { - declarations.push( - b.declarator(/** @type {Pattern} */ (visit(declarator.id)), b.call(value)) - ); - continue; - } - - if (declarator.id.type === 'Identifier') { - declarations.push(b.declarator(declarator.id, value)); - continue; - } - - if (rune === '$derived') { - declarations.push(b.declarator(/** @type {Pattern} */ (visit(declarator.id)), value)); - continue; - } - - declarations.push(...create_state_declarators(declarator, state.scope, value)); - } - - return { - ...node, - declarations - }; - }, - ExpressionStatement(node, context) { - const expression = node.expression; - if (expression.type === 'CallExpression') { - const callee = expression.callee; - - if (callee.type === 'Identifier' && callee.name === '$effect') { - return b.empty; - } - - if ( - callee.type === 'MemberExpression' && - callee.object.type === 'Identifier' && - callee.object.name === '$effect' - ) { - return b.empty; - } - } - context.next(); - }, - MemberExpression(node, context) { - if (node.object.type === 'ThisExpression' && node.property.type === 'PrivateIdentifier') { - const field = context.state.private_derived.get(node.property.name); - if (field) { - return b.call(node); - } - } - - context.next(); - } -}; - -/** - * - * @param {Attribute['value']} value - * @param {ComponentContext} context - * @param {boolean} trim_whitespace - * @param {boolean} is_component - * @returns {Expression} - */ -function serialize_attribute_value(value, context, trim_whitespace = false, is_component = false) { - if (value === true) { - return b.true; - } - - if (!Array.isArray(value) || value.length === 1) { - const chunk = Array.isArray(value) ? value[0] : value; - - if (chunk.type === 'Text') { - const data = trim_whitespace - ? chunk.data.replace(regex_whitespaces_strict, ' ').trim() - : chunk.data; - - return b.literal(is_component ? data : escape_html(data, true)); - } - - return /** @type {Expression} */ (context.visit(chunk.expression)); - } - - let quasi = b.quasi('', false); - const quasis = [quasi]; - - /** @type {Expression[]} */ - const expressions = []; - - for (let i = 0; i < value.length; i++) { - const node = value[i]; - - if (node.type === 'Text') { - quasi.value.raw += trim_whitespace - ? node.data.replace(regex_whitespaces_strict, ' ') - : node.data; - } else { - expressions.push( - b.call('$.stringify', /** @type {Expression} */ (context.visit(node.expression))) - ); - - quasi = b.quasi('', i + 1 === value.length); - quasis.push(quasi); - } - } - - return b.template(quasis, expressions); -} - -/** - * - * @param {RegularElement | SvelteElement} element - * @param {Array} attributes - * @param {StyleDirective[]} style_directives - * @param {ClassDirective[]} class_directives - * @param {ComponentContext} context - */ -function serialize_element_spread_attributes( - element, - attributes, - style_directives, - class_directives, - context -) { - let classes; - let styles; - let flags = 0; - - if (class_directives.length > 0 || context.state.analysis.css.hash) { - const properties = class_directives.map((directive) => - b.init( - directive.name, - directive.expression.type === 'Identifier' && directive.expression.name === directive.name - ? b.id(directive.name) - : /** @type {Expression} */ (context.visit(directive.expression)) - ) - ); - - if (context.state.analysis.css.hash) { - properties.unshift(b.init(context.state.analysis.css.hash, b.literal(true))); - } - - classes = b.object(properties); - } - - if (style_directives.length > 0) { - const properties = style_directives.map((directive) => - b.init( - directive.name, - directive.value === true - ? b.id(directive.name) - : serialize_attribute_value(directive.value, context, true) - ) - ); - - styles = b.object(properties); - } - - if (element.metadata.svg || element.metadata.mathml) { - flags |= ELEMENT_IS_NAMESPACED | ELEMENT_PRESERVE_ATTRIBUTE_CASE; - } else if (is_custom_element_node(element)) { - flags |= ELEMENT_PRESERVE_ATTRIBUTE_CASE; - } - - const object = b.object( - attributes.map((attribute) => { - if (attribute.type === 'Attribute') { - const name = get_attribute_name(element, attribute, context); - const value = serialize_attribute_value( - attribute.value, - context, - WhitespaceInsensitiveAttributes.includes(name) - ); - return b.prop('init', b.key(name), value); - } - - return b.spread(/** @type {Expression} */ (context.visit(attribute))); - }) - ); - - const args = [object, classes, styles, flags ? b.literal(flags) : undefined]; - context.state.template.push(b.call('$.spread_attributes', ...args)); -} - -/** - * @param {Component | SvelteComponent | SvelteSelf} node - * @param {Expression} expression - * @param {ComponentContext} context - */ -function serialize_inline_component(node, expression, context) { - /** @type {Array} */ - const props_and_spreads = []; - - /** @type {Property[]} */ - const custom_css_props = []; - - /** @type {ExpressionStatement[]} */ - const lets = []; - - /** @type {Record} */ - const children = {}; - - /** - * If this component has a slot property, it is a named slot within another component. In this case - * the slot scope applies to the component itself, too, and not just its children. - */ - let slot_scope_applies_to_itself = false; - - /** - * Components may have a children prop and also have child nodes. In this case, we assume - * that the child component isn't using render tags yet and pass the slot as $$slots.default. - * We're not doing it for spread attributes, as this would result in too many false positives. - */ - let has_children_prop = false; - - /** - * @param {Property} prop - */ - function push_prop(prop) { - const current = props_and_spreads.at(-1); - const current_is_props = Array.isArray(current); - const props = current_is_props ? current : []; - props.push(prop); - if (!current_is_props) { - props_and_spreads.push(props); - } - } - for (const attribute of node.attributes) { - if (attribute.type === 'LetDirective') { - lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); - } else if (attribute.type === 'SpreadAttribute') { - props_and_spreads.push(/** @type {Expression} */ (context.visit(attribute))); - } else if (attribute.type === 'Attribute') { - if (attribute.name.startsWith('--')) { - const value = serialize_attribute_value(attribute.value, context, false, true); - custom_css_props.push(b.init(attribute.name, value)); - continue; - } - - if (attribute.name === 'slot') { - slot_scope_applies_to_itself = true; - } - - if (attribute.name === 'children') { - has_children_prop = true; - } - - const value = serialize_attribute_value(attribute.value, context, false, true); - push_prop(b.prop('init', b.key(attribute.name), value)); - } else if (attribute.type === 'BindDirective' && attribute.name !== 'this') { - // TODO this needs to turn the whole thing into a while loop because the binding could be mutated eagerly in the child - push_prop( - b.get(attribute.name, [ - b.return(/** @type {Expression} */ (context.visit(attribute.expression))) - ]) - ); - push_prop( - b.set(attribute.name, [ - b.stmt( - /** @type {Expression} */ ( - context.visit(b.assignment('=', attribute.expression, b.id('$$value'))) - ) - ), - b.stmt(b.assignment('=', b.id('$$settled'), b.false)) - ]) - ); - } - } - - if (slot_scope_applies_to_itself) { - context.state.init.push(...lets); - } - - /** @type {Statement[]} */ - const snippet_declarations = []; - - // Group children by slot - for (const child of node.fragment.nodes) { - if (child.type === 'SnippetBlock') { - // the SnippetBlock visitor adds a declaration to `init`, but if it's directly - // inside a component then we want to hoist them into a block so that they - // can be used as props without creating conflicts - context.visit(child, { - ...context.state, - init: snippet_declarations - }); - - push_prop(b.prop('init', child.expression, child.expression)); - - continue; - } - - let slot_name = 'default'; - if (is_element_node(child)) { - const attribute = /** @type {Attribute | undefined} */ ( - child.attributes.find( - (attribute) => attribute.type === 'Attribute' && attribute.name === 'slot' - ) - ); - if (attribute !== undefined) { - slot_name = /** @type {Text[]} */ (attribute.value)[0].data; - } - } - - children[slot_name] = children[slot_name] || []; - children[slot_name].push(child); - } - - // Serialize each slot - /** @type {Property[]} */ - const serialized_slots = []; - - for (const slot_name of Object.keys(children)) { - const block = /** @type {BlockStatement} */ ( - context.visit( - { - ...node.fragment, - // @ts-expect-error - nodes: children[slot_name] - }, - { - ...context.state, - scope: - context.state.scopes.get(slot_name === 'default' ? children[slot_name][0] : node) ?? - context.state.scope - } - ) - ); - - if (block.body.length === 0) continue; - - const slot_fn = b.arrow( - [b.id('$$payload'), b.id('$$slotProps')], - b.block([ - ...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []), - ...block.body - ]) - ); - - if (slot_name === 'default' && !has_children_prop) { - if (lets.length === 0 && children.default.every((node) => node.type !== 'SvelteFragment')) { - // create `children` prop... - push_prop(b.prop('init', b.id('children'), slot_fn)); - - // and `$$slots.default: true` so that `` on the child works - serialized_slots.push(b.init(slot_name, b.true)); - } else { - // create `$$slots.default`... - serialized_slots.push(b.init(slot_name, slot_fn)); - - // and a `children` prop that errors - push_prop(b.init('children', b.id('$.invalid_default_snippet'))); - } - } else { - serialized_slots.push(b.init(slot_name, slot_fn)); - } - } - - if (serialized_slots.length > 0) { - push_prop(b.prop('init', b.id('$$slots'), b.object(serialized_slots))); - } - - const props_expression = - props_and_spreads.length === 0 || - (props_and_spreads.length === 1 && Array.isArray(props_and_spreads[0])) - ? b.object(/** @type {Property[]} */ (props_and_spreads[0] || [])) - : b.call( - '$.spread_props', - b.array(props_and_spreads.map((p) => (Array.isArray(p) ? b.object(p) : p))) - ); - - /** @type {Statement} */ - let statement = b.stmt( - (node.type === 'SvelteComponent' ? b.maybe_call : b.call)( - expression, - b.id('$$payload'), - props_expression - ) - ); - - if (snippet_declarations.length > 0) { - statement = b.block([...snippet_declarations, statement]); - } - - const dynamic = - node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic); - - if (custom_css_props.length > 0) { - context.state.template.push( - b.stmt( - b.call( - '$.css_props', - b.id('$$payload'), - b.literal(context.state.namespace === 'svg' ? false : true), - b.object(custom_css_props), - b.thunk(b.block([statement])), - dynamic && b.true - ) - ) - ); - } else { - if (dynamic) { - context.state.template.push(empty_comment); - } +import { set_scope } from '../../scope.js'; +import { extract_identifiers } from '../../../utils/ast.js'; +import * as b from '../../../utils/builders.js'; +import { filename } from '../../../state.js'; +import { render_stylesheet } from '../css/index.js'; +import { AssignmentExpression } from './visitors/AssignmentExpression.js'; +import { AwaitBlock } from './visitors/AwaitBlock.js'; +import { CallExpression } from './visitors/CallExpression.js'; +import { ClassBodyRunes } from './visitors/ClassBody.js'; +import { Component } from './visitors/Component.js'; +import { ConstTag } from './visitors/ConstTag.js'; +import { DebugTag } from './visitors/DebugTag.js'; +import { EachBlock } from './visitors/EachBlock.js'; +import { ExpressionStatementRunes } from './visitors/ExpressionStatement.js'; +import { Fragment } from './visitors/Fragment.js'; +import { HtmlTag } from './visitors/HtmlTag.js'; +import { Identifier } from './visitors/Identifier.js'; +import { IfBlock } from './visitors/IfBlock.js'; +import { KeyBlock } from './visitors/KeyBlock.js'; +import { LabeledStatementLegacy } from './visitors/LabeledStatement.js'; +import { LetDirective } from './visitors/LetDirective.js'; +import { MemberExpressionRunes } from './visitors/MemberExpression.js'; +import { PropertyDefinitionRunes } from './visitors/PropertyDefinition.js'; +import { RegularElement } from './visitors/RegularElement.js'; +import { RenderTag } from './visitors/RenderTag.js'; +import { SlotElement } from './visitors/SlotElement.js'; +import { SnippetBlock } from './visitors/SnippetBlock.js'; +import { SpreadAttribute } from './visitors/SpreadAttribute.js'; +import { SvelteComponent } from './visitors/SvelteComponent.js'; +import { SvelteElement } from './visitors/SvelteElement.js'; +import { SvelteFragment } from './visitors/SvelteFragment.js'; +import { SvelteHead } from './visitors/SvelteHead.js'; +import { SvelteSelf } from './visitors/SvelteSelf.js'; +import { TitleElement } from './visitors/TitleElement.js'; +import { UpdateExpression } from './visitors/UpdateExpression.js'; +import { + VariableDeclarationLegacy, + VariableDeclarationRunes +} from './visitors/VariableDeclaration.js'; - context.state.template.push(statement); +/** @type {Visitors} */ +const global_visitors = { + AssignmentExpression, + CallExpression, + Identifier, + UpdateExpression +}; - if (!context.state.skip_hydration_boundaries) { - context.state.template.push(empty_comment); - } - } -} +/** @type {Visitors} */ +const javascript_visitors_runes = { + ...global_visitors, + ClassBody: ClassBodyRunes, + ExpressionStatement: ExpressionStatementRunes, + MemberExpression: MemberExpressionRunes, + PropertyDefinition: PropertyDefinitionRunes, + VariableDeclaration: VariableDeclarationRunes +}; /** @type {Visitors} */ const javascript_visitors_legacy = { - VariableDeclaration(node, { state, visit }) { - /** @type {VariableDeclarator[]} */ - const declarations = []; - - for (const declarator of node.declarations) { - const bindings = /** @type {Binding[]} */ (state.scope.get_bindings(declarator)); - const has_state = bindings.some((binding) => binding.kind === 'state'); - const has_props = bindings.some((binding) => binding.kind === 'bindable_prop'); - - if (!has_state && !has_props) { - declarations.push(/** @type {VariableDeclarator} */ (visit(declarator))); - continue; - } - - if (has_props) { - if (declarator.id.type !== 'Identifier') { - // Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = .. - // means that foo and bar are the props (i.e. the leafs are the prop names), not x and z. - const tmp = state.scope.generate('tmp'); - const paths = extract_paths(declarator.id); - declarations.push( - b.declarator( - b.id(tmp), - /** @type {Expression} */ (visit(/** @type {Expression} */ (declarator.init))) - ) - ); - for (const path of paths) { - const value = path.expression?.(b.id(tmp)); - const name = /** @type {Identifier} */ (path.node).name; - const binding = /** @type {Binding} */ (state.scope.get(name)); - const prop = b.member(b.id('$$props'), b.literal(binding.prop_alias ?? name), true); - declarations.push( - b.declarator(path.node, b.call('$.value_or_fallback', prop, b.thunk(value))) - ); - } - continue; - } - - const binding = /** @type {Binding} */ (state.scope.get(declarator.id.name)); - const prop = b.member( - b.id('$$props'), - b.literal(binding.prop_alias ?? declarator.id.name), - true - ); - - /** @type {Expression} */ - let init = prop; - if (declarator.init) { - const default_value = /** @type {Expression} */ (visit(declarator.init)); - init = is_expression_async(default_value) - ? b.await(b.call('$.value_or_fallback_async', prop, b.thunk(default_value, true))) - : b.call('$.value_or_fallback', prop, b.thunk(default_value)); - } - - declarations.push(b.declarator(declarator.id, init)); - - continue; - } - - declarations.push( - ...create_state_declarators( - declarator, - state.scope, - /** @type {Expression} */ (declarator.init && visit(declarator.init)) - ) - ); - } - - return { - ...node, - declarations - }; - }, - LabeledStatement(node, context) { - if (context.path.length > 1) return; - if (node.label.name !== '$') return; - - // TODO bail out if we're in module context - - // these statements will be topologically ordered later - context.state.legacy_reactive_statements.set( - node, - // people could do "break $" inside, so we need to keep the label - b.labeled('$', /** @type {ExpressionStatement} */ (context.visit(node.body))) - ); - - return b.empty; - } + ...global_visitors, + LabeledStatement: LabeledStatementLegacy, + VariableDeclaration: VariableDeclarationLegacy }; /** @type {ComponentVisitors} */ const template_visitors = { - Fragment(node, context) { - const parent = context.path.at(-1) ?? node; - const namespace = infer_namespace(context.state.namespace, parent, node.nodes); - - const { hoisted, trimmed, is_standalone, is_text_first } = clean_nodes( - parent, - node.nodes, - context.path, - namespace, - context.state, - context.state.preserve_whitespace, - context.state.options.preserveComments - ); - - /** @type {ComponentServerTransformState} */ - const state = { - ...context.state, - init: [], - template: [], - namespace, - skip_hydration_boundaries: is_standalone - }; - - for (const node of hoisted) { - context.visit(node, state); - } - - if (is_text_first) { - // insert `` to prevent this from being glued to the previous fragment - state.template.push(empty_comment); - } - - process_children(trimmed, { ...context, state }); - - return b.block([...state.init, ...serialize_template(state.template)]); - }, - HtmlTag(node, context) { - const expression = /** @type {Expression} */ (context.visit(node.expression)); - context.state.template.push(b.call('$.html', expression)); - }, - ConstTag(node, { state, visit }) { - const declaration = node.declaration.declarations[0]; - const pattern = /** @type {Pattern} */ (visit(declaration.id)); - const init = /** @type {Expression} */ (visit(declaration.init)); - state.init.push(b.declaration('const', pattern, init)); - }, - DebugTag(node, { state, visit }) { - state.template.push( - b.stmt( - b.call( - 'console.log', - b.object( - node.identifiers.map((identifier) => - b.prop('init', identifier, /** @type {Expression} */ (visit(identifier))) - ) - ) - ) - ), - b.debugger - ); - }, - RenderTag(node, context) { - const callee = unwrap_optional(node.expression).callee; - const raw_args = unwrap_optional(node.expression).arguments; - - const snippet_function = /** @type {Expression} */ (context.visit(callee)); - - const snippet_args = raw_args.map((arg) => { - return /** @type {Expression} */ (context.visit(arg)); - }); - - context.state.template.push( - b.stmt( - (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( - snippet_function, - b.id('$$payload'), - ...snippet_args - ) - ) - ); - - if (!context.state.skip_hydration_boundaries) { - context.state.template.push(empty_comment); - } - }, - ClassDirective() { - throw new Error('Node should have been handled elsewhere'); - }, - StyleDirective() { - throw new Error('Node should have been handled elsewhere'); - }, - RegularElement(node, context) { - const namespace = determine_namespace_for_children(node, context.state.namespace); - - /** @type {ComponentServerTransformState} */ - const state = { - ...context.state, - getters: { ...context.state.getters }, - namespace, - preserve_whitespace: - context.state.preserve_whitespace || - ((node.name === 'pre' || node.name === 'textarea') && namespace !== 'foreign') - }; - - context.state.template.push(b.literal(`<${node.name}`)); - const body = serialize_element_attributes(node, { ...context, state }); - context.state.template.push(b.literal('>')); - - if ((node.name === 'script' || node.name === 'style') && node.fragment.nodes.length === 1) { - context.state.template.push( - b.literal(/** @type {Text} */ (node.fragment.nodes[0]).data), - b.literal(``) - ); - - return; - } - - const { hoisted, trimmed } = clean_nodes( - node, - node.fragment.nodes, - context.path, - namespace, - { - ...state, - scope: /** @type {Scope} */ (state.scopes.get(node.fragment)) - }, - state.preserve_whitespace, - state.options.preserveComments - ); - - for (const node of hoisted) { - context.visit(node, state); - } - - if (state.options.dev) { - const location = /** @type {Location} */ (locator(node.start)); - state.template.push( - b.stmt( - b.call( - '$.push_element', - b.id('$$payload'), - b.literal(node.name), - b.literal(location.line), - b.literal(location.column) - ) - ) - ); - } - - if (body === null) { - process_children(trimmed, { ...context, state }); - } else { - let id = body; - - if (body.type !== 'Identifier') { - id = b.id(state.scope.generate('$$body')); - state.template.push(b.const(id, body)); - } - - // if this is a `