UNPKG

svelte

Version:

Cybernetically enhanced web apps

629 lines (585 loc) 16.5 kB
/** @import { Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import * as Legacy from './types/legacy-nodes.js' */ import { walk } from 'zimmerframe'; import { regex_ends_with_whitespaces, regex_not_whitespace, regex_starts_with_whitespaces } from './phases/patterns.js'; import { extract_svelte_ignore } from './utils/extract_svelte_ignore.js'; /** * Some of the legacy Svelte AST nodes remove whitespace from the start and end of their children. * @param {AST.TemplateNode[]} nodes */ function remove_surrounding_whitespace_nodes(nodes) { const first = nodes.at(0); const last = nodes.at(-1); if (first?.type === 'Text') { if (!regex_not_whitespace.test(first.data)) { nodes.shift(); } else { first.data = first.data.replace(regex_starts_with_whitespaces, ''); } } if (last?.type === 'Text') { if (!regex_not_whitespace.test(last.data)) { nodes.pop(); } else { last.data = last.data.replace(regex_ends_with_whitespaces, ''); } } } /** * Transform our nice modern AST into the monstrosity emitted by Svelte 4 * @param {string} source * @param {AST.Root} ast * @returns {Legacy.LegacyRoot} */ export function convert(source, ast) { const root = /** @type {AST.SvelteNode | Legacy.LegacySvelteNode} */ (ast); return /** @type {Legacy.LegacyRoot} */ ( walk(root, null, { _(node, { next }) { // @ts-ignore delete node.metadata; next(); }, // @ts-ignore Root(node, { visit }) { const { instance, module, options } = node; // Insert svelte:options back into the root nodes if (/** @type {any} */ (options)?.__raw__) { let idx = node.fragment.nodes.findIndex((node) => options.end <= node.start); if (idx === -1) { idx = node.fragment.nodes.length; } node.fragment.nodes.splice(idx, 0, /** @type {any} */ (options).__raw__); } /** @type {number | null} */ let start = null; /** @type {number | null} */ let end = null; if (node.fragment.nodes.length > 0) { const first = /** @type {AST.BaseNode} */ (node.fragment.nodes.at(0)); const last = /** @type {AST.BaseNode} */ (node.fragment.nodes.at(-1)); start = first.start; end = last.end; while (/\s/.test(source[start])) start += 1; while (/\s/.test(source[end - 1])) end -= 1; } if (instance) { // @ts-ignore delete instance.attributes; } if (module) { // @ts-ignore delete module.attributes; } return { html: { type: 'Fragment', start, end, children: node.fragment.nodes.map((child) => visit(child)) }, instance, module, css: ast.css ? visit(ast.css) : undefined }; }, AnimateDirective(node) { return { ...node, type: 'Animation' }; }, // @ts-ignore AwaitBlock(node, { visit }) { let pendingblock = { type: 'PendingBlock', /** @type {number | null} */ start: null, /** @type {number | null} */ end: null, children: node.pending?.nodes.map((child) => visit(child)) ?? [], skip: true }; let thenblock = { type: 'ThenBlock', /** @type {number | null} */ start: null, /** @type {number | null} */ end: null, children: node.then?.nodes.map((child) => visit(child)) ?? [], skip: true }; let catchblock = { type: 'CatchBlock', /** @type {number | null} */ start: null, /** @type {number | null} */ end: null, children: node.catch?.nodes.map((child) => visit(child)) ?? [], skip: true }; if (node.pending) { const first = node.pending.nodes.at(0); const last = node.pending.nodes.at(-1); pendingblock.start = first?.start ?? source.indexOf('}', node.expression.end) + 1; pendingblock.end = last?.end ?? pendingblock.start; pendingblock.skip = false; } if (node.then) { const first = node.then.nodes.at(0); const last = node.then.nodes.at(-1); thenblock.start = pendingblock.end ?? first?.start ?? source.indexOf('}', node.expression.end) + 1; thenblock.end = last?.end ?? source.lastIndexOf('}', pendingblock.end ?? node.expression.end) + 1; thenblock.skip = false; } if (node.catch) { const first = node.catch.nodes.at(0); const last = node.catch.nodes.at(-1); catchblock.start = thenblock.end ?? pendingblock.end ?? first?.start ?? source.indexOf('}', node.expression.end) + 1; catchblock.end = last?.end ?? source.lastIndexOf('}', thenblock.end ?? pendingblock.end ?? node.expression.end) + 1; catchblock.skip = false; } return { type: 'AwaitBlock', start: node.start, end: node.end, expression: node.expression, value: node.value, error: node.error, pending: pendingblock, then: thenblock, catch: catchblock }; }, BindDirective(node) { return { ...node, type: 'Binding' }; }, ClassDirective(node) { return { ...node, type: 'Class' }; }, Comment(node) { return { ...node, ignores: extract_svelte_ignore(node.start, node.data, false) }; }, ComplexSelector(node, { next }) { next(); // delete inner metadata/parent properties const children = []; for (const child of node.children) { if (child.combinator) { children.push(child.combinator); } children.push(...child.selectors); } return { type: 'Selector', start: node.start, end: node.end, children }; }, Component(node, { visit }) { return { type: 'InlineComponent', start: node.start, end: node.end, name: node.name, attributes: node.attributes.map( (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child)) ), children: node.fragment.nodes.map( (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child)) ) }; }, // @ts-ignore ConstTag(node) { if (/** @type {Legacy.LegacyConstTag} */ (node).expression !== undefined) { return node; } const modern_node = /** @type {AST.ConstTag} */ (node); const { id: left } = { ...modern_node.declaration.declarations[0] }; // @ts-ignore delete left.typeAnnotation; return { type: 'ConstTag', start: modern_node.start, end: node.end, expression: { type: 'AssignmentExpression', start: (modern_node.declaration.start ?? 0) + 'const '.length, end: modern_node.declaration.end ?? 0, operator: '=', left, right: modern_node.declaration.declarations[0].init } }; }, // @ts-ignore KeyBlock(node, { visit }) { remove_surrounding_whitespace_nodes(node.fragment.nodes); return { type: 'KeyBlock', start: node.start, end: node.end, expression: node.expression, children: node.fragment.nodes.map( (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child)) ) }; }, // @ts-ignore EachBlock(node, { visit }) { let elseblock = undefined; if (node.fallback) { const first = node.fallback.nodes.at(0); const end = source.lastIndexOf('{', /** @type {number} */ (node.end) - 1); const start = first?.start ?? end; remove_surrounding_whitespace_nodes(node.fallback.nodes); elseblock = { type: 'ElseBlock', start, end, children: node.fallback.nodes.map((child) => visit(child)) }; } remove_surrounding_whitespace_nodes(node.body.nodes); return { type: 'EachBlock', start: node.start, end: node.end, children: node.body.nodes.map((child) => visit(child)), context: node.context, expression: node.expression, index: node.index, key: node.key, else: elseblock }; }, ExpressionTag(node, { path }) { const parent = path.at(-1); if (parent?.type === 'Attribute') { if (source[parent.start] === '{') { return { type: 'AttributeShorthand', start: node.start, end: node.end, expression: node.expression }; } } return { type: 'MustacheTag', start: node.start, end: node.end, expression: node.expression }; }, HtmlTag(node) { return { ...node, type: 'RawMustacheTag' }; }, // @ts-ignore IfBlock(node, { visit }) { let elseblock = undefined; if (node.alternate) { let nodes = node.alternate.nodes; if (nodes.length === 1 && nodes[0].type === 'IfBlock' && nodes[0].elseif) { nodes = nodes[0].consequent.nodes; } const end = source.lastIndexOf('{', /** @type {number} */ (node.end) - 1); const start = nodes.at(0)?.start ?? end; remove_surrounding_whitespace_nodes(node.alternate.nodes); elseblock = { type: 'ElseBlock', start, end: end, children: node.alternate.nodes.map( (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child)) ) }; } const start = node.elseif ? node.consequent.nodes[0]?.start ?? source.lastIndexOf('{', /** @type {number} */ (node.end) - 1) : node.start; remove_surrounding_whitespace_nodes(node.consequent.nodes); return { type: 'IfBlock', start, end: node.end, expression: node.test, children: node.consequent.nodes.map( (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child)) ), else: elseblock, elseif: node.elseif ? true : undefined }; }, OnDirective(node) { return { ...node, type: 'EventHandler' }; }, // @ts-expect-error SnippetBlock(node, { visit }) { remove_surrounding_whitespace_nodes(node.body.nodes); return { type: 'SnippetBlock', start: node.start, end: node.end, expression: node.expression, parameters: node.parameters, children: node.body.nodes.map((child) => visit(child)) }; }, // @ts-expect-error SvelteBoundary(node, { visit }) { remove_surrounding_whitespace_nodes(node.fragment.nodes); return { type: 'SvelteBoundary', name: 'svelte:boundary', start: node.start, end: node.end, attributes: node.attributes.map( (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child)) ), children: node.fragment.nodes.map((child) => visit(child)) }; }, RegularElement(node, { visit }) { return { type: 'Element', start: node.start, end: node.end, name: node.name, attributes: node.attributes.map((child) => visit(child)), children: node.fragment.nodes.map((child) => visit(child)) }; }, SlotElement(node, { visit }) { return { type: 'Slot', start: node.start, end: node.end, name: node.name, attributes: node.attributes.map( (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child)) ), children: node.fragment.nodes.map( (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child)) ) }; }, Attribute(node, { visit, next, path }) { if (node.value !== true && !Array.isArray(node.value)) { path.push(node); const value = /** @type {Legacy.LegacyAttribute['value']} */ ([visit(node.value)]); path.pop(); return { ...node, value }; } else { return next(); } }, StyleDirective(node, { visit, next, path }) { if (node.value !== true && !Array.isArray(node.value)) { path.push(node); const value = /** @type {Legacy.LegacyStyleDirective['value']} */ ([visit(node.value)]); path.pop(); return { ...node, value }; } else { return next(); } }, SpreadAttribute(node) { return { ...node, type: 'Spread' }; }, StyleSheet(node, context) { return { ...node, ...context.next(), type: 'Style' }; }, SvelteBody(node, { visit }) { return { type: 'Body', name: 'svelte:body', start: node.start, end: node.end, attributes: node.attributes.map( (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child)) ), children: node.fragment.nodes.map( (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child)) ) }; }, SvelteComponent(node, { visit }) { return { type: 'InlineComponent', name: 'svelte:component', start: node.start, end: node.end, expression: node.expression, attributes: node.attributes.map( (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child)) ), children: node.fragment.nodes.map( (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child)) ) }; }, SvelteDocument(node, { visit }) { return { type: 'Document', name: 'svelte:document', start: node.start, end: node.end, attributes: node.attributes.map( (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child)) ), children: node.fragment.nodes.map( (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child)) ) }; }, SvelteElement(node, { visit }) { /** @type {Expression | string} */ let tag = node.tag; if ( tag.type === 'Literal' && typeof tag.value === 'string' && source[/** @type {number} */ (node.tag.start) - 1] !== '{' ) { tag = tag.value; } return { type: 'Element', name: 'svelte:element', start: node.start, end: node.end, tag, attributes: node.attributes.map((child) => visit(child)), children: node.fragment.nodes.map((child) => visit(child)) }; }, SvelteFragment(node, { visit }) { return { type: 'SlotTemplate', name: 'svelte:fragment', start: node.start, end: node.end, attributes: node.attributes.map( (a) => /** @type {Legacy.LegacyAttributeLike} */ (visit(a)) ), children: node.fragment.nodes.map( (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child)) ) }; }, SvelteHead(node, { visit }) { return { type: 'Head', name: 'svelte:head', start: node.start, end: node.end, attributes: node.attributes.map( (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child)) ), children: node.fragment.nodes.map( (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child)) ) }; }, SvelteOptions(node, { visit }) { return { type: 'Options', name: 'svelte:options', start: node.start, end: node.end, attributes: node.attributes.map( (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child)) ) }; }, SvelteSelf(node, { visit }) { return { type: 'InlineComponent', name: 'svelte:self', start: node.start, end: node.end, attributes: node.attributes.map( (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child)) ), children: node.fragment.nodes.map( (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child)) ) }; }, SvelteWindow(node, { visit }) { return { type: 'Window', name: 'svelte:window', start: node.start, end: node.end, attributes: node.attributes.map( (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child)) ), children: node.fragment.nodes.map( (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child)) ) }; }, Text(node, { path }) { const parent = path.at(-1); if (parent?.type === 'RegularElement' && parent.name === 'style') { // these text nodes are missing `raw` for some dumb reason return /** @type {AST.Text} */ ({ type: 'Text', start: node.start, end: node.end, data: node.data }); } }, TitleElement(node, { visit }) { return { type: 'Title', name: 'title', start: node.start, end: node.end, attributes: node.attributes.map( (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child)) ), children: node.fragment.nodes.map( (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child)) ) }; }, TransitionDirective(node) { return { ...node, type: 'Transition' }; }, UseDirective(node) { return { ...node, type: 'Action' }; }, LetDirective(node) { return { ...node, type: 'Let' }; } }) ); }