From a24175ce20703d55ce3eea71ae12808ab9204e1f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 24 Jul 2024 17:23:44 -0400 Subject: [PATCH] start modularizing server code --- .../3-transform/server/transform-server.js | 206 ++---------------- .../server/visitors/template/ConstTag.js | 16 ++ .../server/visitors/template/Fragment.js | 46 ++++ .../server/visitors/template/HtmlTag.js | 13 ++ .../server/visitors/template/shared/utils.js | 143 ++++++++++++ 5 files changed, 233 insertions(+), 191 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/template/ConstTag.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/template/Fragment.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/template/HtmlTag.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/template/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..bc1aa7a9fe 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 @@ -24,12 +24,7 @@ import { VoidElements, WhitespaceInsensitiveAttributes } from '../../constants.js'; -import { - clean_nodes, - determine_namespace_for_children, - infer_namespace, - transform_inspect_rune -} from '../utils.js'; +import { clean_nodes, determine_namespace_for_children, 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'; @@ -39,147 +34,19 @@ import { 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 { 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(); - } -} +import { ConstTag } from './visitors/template/ConstTag.js'; +import { Fragment } from './visitors/template/Fragment.js'; +import { HtmlTag } from './visitors/template/HtmlTag.js'; +import { + block_close, + block_open, + empty_comment, + process_children, + serialize_template +} from './visitors/template/shared/utils.js'; /** * @param {VariableDeclarator} declarator @@ -1120,52 +987,9 @@ const javascript_visitors_legacy = { /** @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)); - }, + Fragment, + HtmlTag, + ConstTag, DebugTag(node, { state, visit }) { state.template.push( b.stmt( diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/template/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/template/ConstTag.js new file mode 100644 index 0000000000..4c3622ff02 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/template/ConstTag.js @@ -0,0 +1,16 @@ +/** @import { Expression, Pattern } from 'estree' */ +/** @import { ConstTag } from '#compiler' */ +/** @import { ComponentContext } from '../../types' */ +import * as b from '../../../../../utils/builders.js'; + +/** + * @param {ConstTag} node + * @param {ComponentContext} context + */ +export function ConstTag(node, context) { + const declaration = node.declaration.declarations[0]; + const id = /** @type {Pattern} */ (context.visit(declaration.id)); + const init = /** @type {Expression} */ (context.visit(declaration.init)); + + context.state.init.push(b.const(id, init)); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/template/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/template/Fragment.js new file mode 100644 index 0000000000..28f747501a --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/template/Fragment.js @@ -0,0 +1,46 @@ +/** @import { Fragment } from '#compiler' */ +/** @import { ComponentContext, ComponentServerTransformState } from '../../types' */ +import { clean_nodes, infer_namespace } from '../../../utils.js'; +import * as b from '../../../../../utils/builders.js'; +import { empty_comment, process_children, serialize_template } from './shared/utils.js'; + +/** + * @param {Fragment} node + * @param {ComponentContext} context + */ +export function 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)]); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/template/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/template/HtmlTag.js new file mode 100644 index 0000000000..75df6b7546 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/template/HtmlTag.js @@ -0,0 +1,13 @@ +/** @import { Expression } from 'estree' */ +/** @import { HtmlTag } from '#compiler' */ +/** @import { ComponentContext } from '../../types' */ +import * as b from '../../../../../utils/builders.js'; + +/** + * @param {HtmlTag} node + * @param {ComponentContext} context + */ +export function HtmlTag(node, context) { + const expression = /** @type {Expression} */ (context.visit(node.expression)); + context.state.template.push(b.call('$.html', expression)); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/template/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/template/shared/utils.js new file mode 100644 index 0000000000..ac30539539 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/template/shared/utils.js @@ -0,0 +1,143 @@ +/** @import { AssignmentOperator, Expression, Identifier, Node, Statement, TemplateElement } from 'estree' */ +/** @import { Comment, ExpressionTag, SvelteNode, Text } from '#compiler' */ +/** @import { ComponentContext } from '../../../types.js' */ +import { escape_html } from '../../../../../../../escaping.js'; +import { + BLOCK_CLOSE, + BLOCK_OPEN, + EMPTY_COMMENT +} from '../../../../../../../internal/server/hydration.js'; +import * as b from '../../../../../../utils/builders.js'; +import { sanitize_template_string } from '../../../../../../utils/sanitize_template_string.js'; + +/** Opens an if/each block, so that we can remove nodes in the case of a mismatch */ +export 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 */ +export const block_close = b.literal(BLOCK_CLOSE); + +/** Empty comment to keep text nodes separate, or provide an anchor node for blocks */ +export const empty_comment = b.literal(EMPTY_COMMENT); + +/** + * Processes an array of template nodes, joining sibling text/expression nodes and + * recursing into child nodes. + * @param {Array} nodes + * @param {ComponentContext} context + */ +export 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 {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[]} + */ +export 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; +}