diff --git a/packages/svelte/scripts/process-messages/index.js b/packages/svelte/scripts/process-messages/index.js index 81c59271de..d246cfbba5 100644 --- a/packages/svelte/scripts/process-messages/index.js +++ b/packages/svelte/scripts/process-messages/index.js @@ -4,6 +4,7 @@ import fs from 'node:fs'; import * as acorn from 'acorn'; import { walk } from 'zimmerframe'; import * as esrap from 'esrap'; +import ts from 'esrap/languages/ts'; const DIR = '../../documentation/docs/98-reference/.generated'; @@ -98,55 +99,18 @@ function run() { .replace(/\r\n/g, '\n'); /** - * @type {Array<{ - * type: string; - * value: string; - * start: number; - * end: number - * }>} + * @type {any[]} */ const comments = []; let ast = acorn.parse(source, { ecmaVersion: 'latest', sourceType: 'module', - onComment: (block, value, start, end) => { - if (block && /\n/.test(value)) { - let a = start; - while (a > 0 && source[a - 1] !== '\n') a -= 1; - - let b = a; - while (/[ \t]/.test(source[b])) b += 1; - - const indentation = source.slice(a, b); - value = value.replace(new RegExp(`^${indentation}`, 'gm'), ''); - } - - comments.push({ type: block ? 'Block' : 'Line', value, start, end }); - } + locations: true, + onComment: comments }); ast = walk(ast, null, { - _(node, { next }) { - let comment; - - while (comments[0] && comments[0].start < node.start) { - comment = comments.shift(); - // @ts-expect-error - (node.leadingComments ||= []).push(comment); - } - - next(); - - if (comments[0]) { - const slice = source.slice(node.end, comments[0].start); - - if (/^[,) \t]*$/.test(slice)) { - // @ts-expect-error - node.trailingComments = [comments.shift()]; - } - } - }, // @ts-expect-error Identifier(node, context) { if (node.name === 'CODES') { @@ -161,11 +125,6 @@ function run() { } }); - if (comments.length > 0) { - // @ts-expect-error - (ast.trailingComments ||= []).push(...comments); - } - const category = messages[name]; // find the `export function CODE` node @@ -184,6 +143,16 @@ function run() { const template_node = ast.body[index]; ast.body.splice(index, 1); + const jsdoc = comments.findLast((comment) => comment.start < template_node.start); + + const printed = esrap.print( + ast, + // @ts-expect-error + ts({ + comments: comments.filter((comment) => comment !== jsdoc) + }) + ); + for (const code in category) { const { messages } = category[code]; /** @type {string[]} */ @@ -273,41 +242,6 @@ function run() { } const clone = walk(/** @type {import('estree').Node} */ (template_node), null, { - // @ts-expect-error Block is a block comment, which is not recognised - Block(node, context) { - if (!node.value.includes('PARAMETER')) return; - - const value = /** @type {string} */ (node.value) - .split('\n') - .map((line) => { - if (line === ' * MESSAGE') { - return messages[messages.length - 1] - .split('\n') - .map((line) => ` * ${line}`) - .join('\n'); - } - - if (line.includes('PARAMETER')) { - return vars - .map((name, i) => { - const optional = i >= group[0].vars.length; - - return optional - ? ` * @param {string | undefined | null} [${name}]` - : ` * @param {string} ${name}`; - }) - .join('\n'); - } - - return line; - }) - .filter((x) => x !== '') - .join('\n'); - - if (value !== node.value) { - return { ...node, value }; - } - }, FunctionDeclaration(node, context) { if (node.id.name !== 'CODE') return; @@ -394,16 +328,49 @@ function run() { } }); + const jsdoc_clone = { + ...jsdoc, + value: /** @type {string} */ (jsdoc.value) + .split('\n') + .map((line) => { + if (line === ' * MESSAGE') { + return messages[messages.length - 1] + .split('\n') + .map((line) => ` * ${line}`) + .join('\n'); + } + + if (line.includes('PARAMETER')) { + return vars + .map((name, i) => { + const optional = i >= group[0].vars.length; + + return optional + ? ` * @param {string | undefined | null} [${name}]` + : ` * @param {string} ${name}`; + }) + .join('\n'); + } + + return line; + }) + .filter((x) => x !== '') + .join('\n') + }; + + // @ts-expect-error + const block = esrap.print({ ...ast, body: [clone] }, ts({ comments: [jsdoc_clone] })).code; + + printed.code += `\n\n${block}`; + // @ts-expect-error ast.body.push(clone); } - const module = esrap.print(ast); - fs.writeFileSync( dest, `/* This file is generated by scripts/process-messages/index.js. Do not edit! */\n\n` + - module.code, + printed.code, 'utf-8' ); } diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 25e72340c6..ec71fc85a2 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -4,21 +4,25 @@ import { CompileDiagnostic } from './utils/compile_diagnostic.js'; /** @typedef {{ start?: number, end?: number }} NodeLike */ class InternalCompileError extends Error { - message = ''; // ensure this property is enumerable + message = ''; + + // ensure this property is enumerable #diagnostic; /** - * @param {string} code - * @param {string} message - * @param {[number, number] | undefined} position - */ + * @param {string} code + * @param {string} message + * @param {[number, number] | undefined} position + */ constructor(code, message, position) { super(message); this.stack = ''; // avoid unnecessary noise; don't set it as a class property or it becomes enumerable + // We want to extend from Error so that various bundler plugins properly handle it. // But we also want to share the same object shape with that of warnings, therefore // we create an instance of the shared class an copy over its properties. this.#diagnostic = new CompileDiagnostic(code, message, position); + Object.assign(this, this.#diagnostic); this.name = 'CompileError'; } @@ -816,7 +820,9 @@ export function bind_invalid_expression(node) { * @returns {never} */ export function bind_invalid_name(node, name, explanation) { - e(node, 'bind_invalid_name', `${explanation ? `\`bind:${name}\` is not a valid binding. ${explanation}` : `\`bind:${name}\` is not a valid binding`}\nhttps://svelte.dev/e/bind_invalid_name`); + e(node, 'bind_invalid_name', `${explanation + ? `\`bind:${name}\` is not a valid binding. ${explanation}` + : `\`bind:${name}\` is not a valid binding`}\nhttps://svelte.dev/e/bind_invalid_name`); } /** diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js index 756a88a824..ac0797927e 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -11,6 +11,7 @@ import { transform_component, transform_module } from './phases/3-transform/inde import { validate_component_options, validate_module_options } from './validate-options.js'; import * as state from './state.js'; export { default as preprocess } from './preprocess/index.js'; +export { print } from './print/index.js'; /** * `compile` converts your `.svelte` source code into a JavaScript module that exports a component diff --git a/packages/svelte/src/compiler/legacy.js b/packages/svelte/src/compiler/legacy.js index f6b7e4b054..85345bca4a 100644 --- a/packages/svelte/src/compiler/legacy.js +++ b/packages/svelte/src/compiler/legacy.js @@ -451,6 +451,7 @@ export function convert(source, ast) { SpreadAttribute(node) { return { ...node, type: 'Spread' }; }, + // @ts-ignore StyleSheet(node, context) { return { ...node, diff --git a/packages/svelte/src/compiler/phases/3-transform/index.js b/packages/svelte/src/compiler/phases/3-transform/index.js index f96fd64ec7..2d045bf363 100644 --- a/packages/svelte/src/compiler/phases/3-transform/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/index.js @@ -1,6 +1,7 @@ /** @import { ValidatedCompileOptions, CompileResult, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { ComponentAnalysis, Analysis } from '../types' */ import { print } from 'esrap'; +import ts from 'esrap/languages/ts'; import { VERSION } from '../../../version.js'; import { server_component, server_module } from './server/transform-server.js'; import { client_component, client_module } from './client/transform-client.js'; @@ -34,7 +35,8 @@ export function transform_component(analysis, source, options) { const js_source_name = get_source_name(options.filename, options.outputFilename, 'input.svelte'); - const js = print(program, { + // @ts-ignore TODO + const js = print(program, ts(), { // include source content; makes it easier/more robust looking up the source map code // (else esrap does return null for source and sourceMapContent which may trip up tooling) sourceMapContent: source, @@ -94,7 +96,8 @@ export function transform_module(analysis, source, options) { } return { - js: print(program, { + // @ts-expect-error + js: print(program, ts(), { // include source content; makes it easier/more robust looking up the source map code // (else esrap does return null for source and sourceMapContent which may trip up tooling) sourceMapContent: source, diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js new file mode 100644 index 0000000000..d3c6f31c77 --- /dev/null +++ b/packages/svelte/src/compiler/print/index.js @@ -0,0 +1,133 @@ +/** @import { AST } from '#compiler'; */ +/** @import { Visitors } from 'esrap' */ +import * as esrap from 'esrap'; +import ts from 'esrap/languages/ts'; + +/** + * @param {AST.SvelteNode} ast + */ +export function print(ast) { + // @ts-expect-error some bullshit + return esrap.print(ast, { + ...ts(), + ...visitors + }); +} + +/** @type {Visitors} */ +const visitors = { + Root(node, context) { + if (node.options) { + throw new Error('TODO'); + } + + for (const item of [node.module, node.instance, node.fragment, node.css]) { + if (!item) continue; + + context.margin(); + context.newline(); + context.visit(item); + } + }, + Script(node, context) { + context.write(''); + + context.indent(); + context.newline(); + context.visit(node.content); + context.dedent(); + context.newline(); + + context.write(''); + }, + Fragment(node, context) { + for (let i = 0; i < node.nodes.length; i += 1) { + const child = node.nodes[i]; + + if (child.type === 'Text') { + let data = child.data; + + if (i === 0) data = data.trimStart(); + if (i === node.nodes.length - 1) data = data.trimEnd(); + + context.write(data); + } else { + context.visit(child); + } + } + }, + Attribute(node, context) { + context.write(node.name); + + if (node.value === true) return; + + context.write('='); + + if (Array.isArray(node.value)) { + if (node.value.length > 1) { + context.write('"'); + } + + for (const chunk of node.value) { + context.visit(chunk); + } + + if (node.value.length > 1) { + context.write('"'); + } + } else { + context.visit(node.value); + } + }, + Text(node, context) { + context.write(node.data); + }, + ExpressionTag(node, context) { + context.write('{'); + context.visit(node.expression); + context.write('}'); + }, + IfBlock(node, context) { + context.write('{#if '); + context.visit(node.test); + context.write('}'); + + context.visit(node.consequent); + + // TODO handle alternate/else if + + context.write('{/if}'); + }, + RegularElement(node, context) { + context.write('<' + node.name); + + for (const attribute of node.attributes) { + // TODO handle multiline + context.write(' '); + context.visit(attribute); + } + + context.write('>'); + + // TODO handle void elements + if (node.fragment) { + context.visit(node.fragment); + } + + context.write(``); + }, + TransitionDirective(node, context) { + // TODO + } +}; diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index cefc7fa7a2..7d2a0397b1 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -572,7 +572,7 @@ export namespace AST { | AST.Comment | Block; - export type SvelteNode = Node | TemplateNode | AST.Fragment | _CSS.Node; + export type SvelteNode = Node | TemplateNode | AST.Fragment | _CSS.Node | Script; export type { _CSS as CSS }; } diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index 1226190891..e6bc7adf9c 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -1,12 +1,6 @@ /* This file is generated by scripts/process-messages/index.js. Do not edit! */ -import { - warnings, - ignore_stack, - ignore_map, - warning_filter -} from './state.js'; - +import { warnings, ignore_stack, ignore_map, warning_filter } from './state.js'; import { CompileDiagnostic } from './utils/compile_diagnostic.js'; /** @typedef {{ start?: number, end?: number }} NodeLike */ @@ -14,10 +8,10 @@ class InternalCompileWarning extends CompileDiagnostic { name = 'CompileWarning'; /** - * @param {string} code - * @param {string} message - * @param {[number, number] | undefined} position - */ + * @param {string} code + * @param {string} message + * @param {[number, number] | undefined} position + */ constructor(code, message, position) { super(code, message, position); } @@ -40,6 +34,7 @@ function w(node, code, message) { const warning = new InternalCompileWarning(code, message, node && node.start !== undefined ? [node.start, node.end ?? node.start] : undefined); if (!warning_filter(warning)) return; + warnings.push(warning); } @@ -496,7 +491,9 @@ export function a11y_role_supports_aria_props_implicit(node, attribute, role, na * @param {string | undefined | null} [suggestion] */ export function a11y_unknown_aria_attribute(node, attribute, suggestion) { - w(node, 'a11y_unknown_aria_attribute', `${suggestion ? `Unknown aria attribute 'aria-${attribute}'. Did you mean '${suggestion}'?` : `Unknown aria attribute 'aria-${attribute}'`}\nhttps://svelte.dev/e/a11y_unknown_aria_attribute`); + w(node, 'a11y_unknown_aria_attribute', `${suggestion + ? `Unknown aria attribute 'aria-${attribute}'. Did you mean '${suggestion}'?` + : `Unknown aria attribute 'aria-${attribute}'`}\nhttps://svelte.dev/e/a11y_unknown_aria_attribute`); } /** @@ -506,7 +503,9 @@ export function a11y_unknown_aria_attribute(node, attribute, suggestion) { * @param {string | undefined | null} [suggestion] */ export function a11y_unknown_role(node, role, suggestion) { - w(node, 'a11y_unknown_role', `${suggestion ? `Unknown role '${role}'. Did you mean '${suggestion}'?` : `Unknown role '${role}'`}\nhttps://svelte.dev/e/a11y_unknown_role`); + w(node, 'a11y_unknown_role', `${suggestion + ? `Unknown role '${role}'. Did you mean '${suggestion}'?` + : `Unknown role '${role}'`}\nhttps://svelte.dev/e/a11y_unknown_role`); } /** @@ -534,7 +533,9 @@ export function legacy_code(node, code, suggestion) { * @param {string | undefined | null} [suggestion] */ export function unknown_code(node, code, suggestion) { - w(node, 'unknown_code', `${suggestion ? `\`${code}\` is not a recognised code (did you mean \`${suggestion}\`?)` : `\`${code}\` is not a recognised code`}\nhttps://svelte.dev/e/unknown_code`); + w(node, 'unknown_code', `${suggestion + ? `\`${code}\` is not a recognised code (did you mean \`${suggestion}\`?)` + : `\`${code}\` is not a recognised code`}\nhttps://svelte.dev/e/unknown_code`); } /** diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 429dd99da9..042cd9132e 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -11,6 +11,7 @@ export function bind_invalid_checkbox_value() { const error = new Error(`bind_invalid_checkbox_value\nUsing \`bind:value\` together with a checkbox input is not allowed. Use \`bind:checked\` instead\nhttps://svelte.dev/e/bind_invalid_checkbox_value`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/bind_invalid_checkbox_value`); @@ -29,6 +30,7 @@ export function bind_invalid_export(component, key, name) { const error = new Error(`bind_invalid_export\nComponent ${component} has an export named \`${key}\` that a consumer component is trying to access using \`bind:${key}\`, which is disallowed. Instead, use \`bind:this\` (e.g. \`<${name} bind:this={component} />\`) and then access the property on the bound component instance (e.g. \`component.${key}\`)\nhttps://svelte.dev/e/bind_invalid_export`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/bind_invalid_export`); @@ -47,6 +49,7 @@ export function bind_not_bindable(key, component, name) { const error = new Error(`bind_not_bindable\nA component is attempting to bind to a non-bindable property \`${key}\` belonging to ${component} (i.e. \`<${name} bind:${key}={...}>\`). To mark a property as bindable: \`let { ${key} = $bindable() } = $props()\`\nhttps://svelte.dev/e/bind_not_bindable`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/bind_not_bindable`); @@ -64,6 +67,7 @@ export function component_api_changed(method, component) { const error = new Error(`component_api_changed\nCalling \`${method}\` on a component instance (of ${component}) is no longer valid in Svelte 5\nhttps://svelte.dev/e/component_api_changed`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/component_api_changed`); @@ -81,6 +85,7 @@ export function component_api_invalid_new(component, name) { const error = new Error(`component_api_invalid_new\nAttempted to instantiate ${component} with \`new ${name}\`, which is no longer valid in Svelte 5. If this component is not under your control, set the \`compatibility.componentApi\` compiler option to \`4\` to keep it working.\nhttps://svelte.dev/e/component_api_invalid_new`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/component_api_invalid_new`); @@ -96,6 +101,7 @@ export function derived_references_self() { const error = new Error(`derived_references_self\nA derived value cannot reference itself recursively\nhttps://svelte.dev/e/derived_references_self`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/derived_references_self`); @@ -111,9 +117,12 @@ export function derived_references_self() { */ export function each_key_duplicate(a, b, value) { if (DEV) { - const error = new Error(`each_key_duplicate\n${value ? `Keyed each block has duplicate key \`${value}\` at indexes ${a} and ${b}` : `Keyed each block has duplicate key at indexes ${a} and ${b}`}\nhttps://svelte.dev/e/each_key_duplicate`); + const error = new Error(`each_key_duplicate\n${value + ? `Keyed each block has duplicate key \`${value}\` at indexes ${a} and ${b}` + : `Keyed each block has duplicate key at indexes ${a} and ${b}`}\nhttps://svelte.dev/e/each_key_duplicate`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/each_key_duplicate`); @@ -130,6 +139,7 @@ export function effect_in_teardown(rune) { const error = new Error(`effect_in_teardown\n\`${rune}\` cannot be used inside an effect cleanup function\nhttps://svelte.dev/e/effect_in_teardown`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/effect_in_teardown`); @@ -145,6 +155,7 @@ export function effect_in_unowned_derived() { const error = new Error(`effect_in_unowned_derived\nEffect cannot be created inside a \`$derived\` value that was not itself created inside an effect\nhttps://svelte.dev/e/effect_in_unowned_derived`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/effect_in_unowned_derived`); @@ -161,6 +172,7 @@ export function effect_orphan(rune) { const error = new Error(`effect_orphan\n\`${rune}\` can only be used inside an effect (e.g. during component initialisation)\nhttps://svelte.dev/e/effect_orphan`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/effect_orphan`); @@ -176,6 +188,7 @@ export function effect_update_depth_exceeded() { const error = new Error(`effect_update_depth_exceeded\nMaximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops\nhttps://svelte.dev/e/effect_update_depth_exceeded`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/effect_update_depth_exceeded`); @@ -191,6 +204,7 @@ export function hydration_failed() { const error = new Error(`hydration_failed\nFailed to hydrate the application\nhttps://svelte.dev/e/hydration_failed`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/hydration_failed`); @@ -206,6 +220,7 @@ export function invalid_snippet() { const error = new Error(`invalid_snippet\nCould not \`{@render}\` snippet due to the expression being \`null\` or \`undefined\`. Consider using optional chaining \`{@render snippet?.()}\`\nhttps://svelte.dev/e/invalid_snippet`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/invalid_snippet`); @@ -222,6 +237,7 @@ export function lifecycle_legacy_only(name) { const error = new Error(`lifecycle_legacy_only\n\`${name}(...)\` cannot be used in runes mode\nhttps://svelte.dev/e/lifecycle_legacy_only`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/lifecycle_legacy_only`); @@ -238,6 +254,7 @@ export function props_invalid_value(key) { const error = new Error(`props_invalid_value\nCannot do \`bind:${key}={undefined}\` when \`${key}\` has a fallback value\nhttps://svelte.dev/e/props_invalid_value`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/props_invalid_value`); @@ -254,6 +271,7 @@ export function props_rest_readonly(property) { const error = new Error(`props_rest_readonly\nRest element properties of \`$props()\` such as \`${property}\` are readonly\nhttps://svelte.dev/e/props_rest_readonly`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/props_rest_readonly`); @@ -270,6 +288,7 @@ export function rune_outside_svelte(rune) { const error = new Error(`rune_outside_svelte\nThe \`${rune}\` rune is only available inside \`.svelte\` and \`.svelte.js/ts\` files\nhttps://svelte.dev/e/rune_outside_svelte`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/rune_outside_svelte`); @@ -285,6 +304,7 @@ export function state_descriptors_fixed() { const error = new Error(`state_descriptors_fixed\nProperty descriptors defined on \`$state\` objects must contain \`value\` and always be \`enumerable\`, \`configurable\` and \`writable\`.\nhttps://svelte.dev/e/state_descriptors_fixed`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/state_descriptors_fixed`); @@ -300,6 +320,7 @@ export function state_prototype_fixed() { const error = new Error(`state_prototype_fixed\nCannot set prototype of \`$state\` object\nhttps://svelte.dev/e/state_prototype_fixed`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/state_prototype_fixed`); @@ -315,6 +336,7 @@ export function state_unsafe_mutation() { const error = new Error(`state_unsafe_mutation\nUpdating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without \`$state\`\nhttps://svelte.dev/e/state_unsafe_mutation`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/state_unsafe_mutation`); diff --git a/packages/svelte/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js index e07892a4b0..74f9041b91 100644 --- a/packages/svelte/src/internal/client/warnings.js +++ b/packages/svelte/src/internal/client/warnings.js @@ -25,7 +25,13 @@ export function assignment_value_stale(property, location) { */ export function binding_property_non_reactive(binding, location) { if (DEV) { - console.warn(`%c[svelte] binding_property_non_reactive\n%c${location ? `\`${binding}\` (${location}) is binding to a non-reactive property` : `\`${binding}\` is binding to a non-reactive property`}\nhttps://svelte.dev/e/binding_property_non_reactive`, bold, normal); + console.warn( + `%c[svelte] binding_property_non_reactive\n%c${location + ? `\`${binding}\` (${location}) is binding to a non-reactive property` + : `\`${binding}\` is binding to a non-reactive property`}\nhttps://svelte.dev/e/binding_property_non_reactive`, + bold, + normal + ); } else { console.warn(`https://svelte.dev/e/binding_property_non_reactive`); } @@ -76,7 +82,13 @@ export function hydration_attribute_changed(attribute, html, value) { */ export function hydration_html_changed(location) { if (DEV) { - console.warn(`%c[svelte] hydration_html_changed\n%c${location ? `The value of an \`{@html ...}\` block ${location} changed between server and client renders. The client value will be ignored in favour of the server value` : 'The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value'}\nhttps://svelte.dev/e/hydration_html_changed`, bold, normal); + console.warn( + `%c[svelte] hydration_html_changed\n%c${location + ? `The value of an \`{@html ...}\` block ${location} changed between server and client renders. The client value will be ignored in favour of the server value` + : 'The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value'}\nhttps://svelte.dev/e/hydration_html_changed`, + bold, + normal + ); } else { console.warn(`https://svelte.dev/e/hydration_html_changed`); } @@ -88,7 +100,13 @@ export function hydration_html_changed(location) { */ export function hydration_mismatch(location) { if (DEV) { - console.warn(`%c[svelte] hydration_mismatch\n%c${location ? `Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near ${location}` : 'Hydration failed because the initial UI does not match what was rendered on the server'}\nhttps://svelte.dev/e/hydration_mismatch`, bold, normal); + console.warn( + `%c[svelte] hydration_mismatch\n%c${location + ? `Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near ${location}` + : 'Hydration failed because the initial UI does not match what was rendered on the server'}\nhttps://svelte.dev/e/hydration_mismatch`, + bold, + normal + ); } else { console.warn(`https://svelte.dev/e/hydration_mismatch`); } diff --git a/packages/svelte/src/internal/server/errors.js b/packages/svelte/src/internal/server/errors.js index 38c545c84e..e47530c9aa 100644 --- a/packages/svelte/src/internal/server/errors.js +++ b/packages/svelte/src/internal/server/errors.js @@ -1,5 +1,7 @@ /* This file is generated by scripts/process-messages/index.js. Do not edit! */ + + /** * `%name%(...)` is not available on the server * @param {string} name @@ -9,5 +11,6 @@ export function lifecycle_function_unavailable(name) { const error = new Error(`lifecycle_function_unavailable\n\`${name}(...)\` is not available on the server\nhttps://svelte.dev/e/lifecycle_function_unavailable`); error.name = 'Svelte error'; + throw error; } \ No newline at end of file diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index b8606fbf6f..6bcc35016a 100644 --- a/packages/svelte/src/internal/shared/errors.js +++ b/packages/svelte/src/internal/shared/errors.js @@ -11,6 +11,7 @@ export function invalid_default_snippet() { const error = new Error(`invalid_default_snippet\nCannot use \`{@render children(...)}\` if the parent component uses \`let:\` directives. Consider using a named snippet instead\nhttps://svelte.dev/e/invalid_default_snippet`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/invalid_default_snippet`); @@ -26,6 +27,7 @@ export function invalid_snippet_arguments() { const error = new Error(`invalid_snippet_arguments\nA snippet function was passed invalid arguments. Snippets should only be instantiated via \`{@render ...}\`\nhttps://svelte.dev/e/invalid_snippet_arguments`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/invalid_snippet_arguments`); @@ -42,6 +44,7 @@ export function lifecycle_outside_component(name) { const error = new Error(`lifecycle_outside_component\n\`${name}(...)\` can only be used during component initialisation\nhttps://svelte.dev/e/lifecycle_outside_component`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/lifecycle_outside_component`); @@ -57,6 +60,7 @@ export function snippet_without_render_tag() { const error = new Error(`snippet_without_render_tag\nAttempted to render a snippet without a \`{@render}\` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change \`{snippet}\` to \`{@render snippet()}\`.\nhttps://svelte.dev/e/snippet_without_render_tag`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/snippet_without_render_tag`); @@ -73,6 +77,7 @@ export function store_invalid_shape(name) { const error = new Error(`store_invalid_shape\n\`${name}\` is not a store with a \`subscribe\` method\nhttps://svelte.dev/e/store_invalid_shape`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/store_invalid_shape`); @@ -88,6 +93,7 @@ export function svelte_element_invalid_this_value() { const error = new Error(`svelte_element_invalid_this_value\nThe \`this\` prop on \`\` must be a string, if defined\nhttps://svelte.dev/e/svelte_element_invalid_this_value`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/svelte_element_invalid_this_value`); diff --git a/packages/svelte/src/internal/shared/warnings.js b/packages/svelte/src/internal/shared/warnings.js index 281be08382..0acca44184 100644 --- a/packages/svelte/src/internal/shared/warnings.js +++ b/packages/svelte/src/internal/shared/warnings.js @@ -25,11 +25,15 @@ export function dynamic_void_element_content(tag) { */ export function state_snapshot_uncloneable(properties) { if (DEV) { - console.warn(`%c[svelte] state_snapshot_uncloneable\n%c${properties - ? `The following properties cannot be cloned with \`$state.snapshot\` — the return value contains the originals: + console.warn( + `%c[svelte] state_snapshot_uncloneable\n%c${properties + ? `The following properties cannot be cloned with \`$state.snapshot\` — the return value contains the originals: ${properties}` - : 'Value cannot be cloned with `$state.snapshot` — the original value was returned'}\nhttps://svelte.dev/e/state_snapshot_uncloneable`, bold, normal); + : 'Value cannot be cloned with `$state.snapshot` — the original value was returned'}\nhttps://svelte.dev/e/state_snapshot_uncloneable`, + bold, + normal + ); } else { console.warn(`https://svelte.dev/e/state_snapshot_uncloneable`); } diff --git a/packages/svelte/tests/parser-modern/test.ts b/packages/svelte/tests/parser-modern/test.ts index b47d4a4879..08d8aafeab 100644 --- a/packages/svelte/tests/parser-modern/test.ts +++ b/packages/svelte/tests/parser-modern/test.ts @@ -1,8 +1,9 @@ import * as fs from 'node:fs'; import { assert, it } from 'vitest'; -import { parse } from 'svelte/compiler'; +import { parse, print } from 'svelte/compiler'; import { try_load_json } from '../helpers.js'; import { suite, type BaseTest } from '../suite.js'; +import { walk } from 'zimmerframe'; interface ParserTest extends BaseTest {} @@ -30,6 +31,28 @@ const { test, run } = suite(async (config, cwd) => { const expected = try_load_json(`${cwd}/output.json`); assert.deepEqual(actual, expected); } + + const printed = print(actual); + const reparsed = JSON.parse( + JSON.stringify( + parse(printed.code, { + modern: true, + loose: cwd.split('/').pop()!.startsWith('loose-') + }) + ) + ); + + fs.writeFileSync(`${cwd}/_actual.svelte`, JSON.stringify(printed.code, null, '\t')); + + const actual_cleaned = walk(actual, null, { + _(node, context) {} + }); + + const reparsed_cleaned = walk(actual, null, { + _(node, context) {} + }); + + assert.deepEqual(actual_cleaned, reparsed_cleaned); }); export { test }; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 1a83e0d0f1..b8dbc96a11 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1493,7 +1493,7 @@ declare module 'svelte/compiler' { | AST.Comment | Block; - export type SvelteNode = Node | TemplateNode | AST.Fragment | _CSS.Node; + export type SvelteNode = Node | TemplateNode | AST.Fragment | _CSS.Node | Script; export type { _CSS as CSS }; } @@ -1505,6 +1505,10 @@ declare module 'svelte/compiler' { export function preprocess(source: string, preprocessor: PreprocessorGroup | PreprocessorGroup[], options?: { filename?: string; } | undefined): Promise; + export function print(ast: AST.SvelteNode): { + code: string; + map: any; + }; /** * The current version, as set in package.json. * */ diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 2029937f52..348e43b9c0 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -3,7 +3,7 @@ import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseArgs } from 'node:util'; import { globSync } from 'tinyglobby'; -import { compile, compileModule, parse, migrate } from 'svelte/compiler'; +import { compile, compileModule, parse, print, migrate } from 'svelte/compiler'; const argv = parseArgs({ options: { runes: { type: 'boolean' } }, args: process.argv.slice(2) }); @@ -70,6 +70,10 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { } catch (e) { console.warn(`Error migrating ${file}`, e); } + + const printed = print(ast); + + write(`${cwd}/output/printed/${file}`, printed.code); } const compiled = compile(source, {