diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js index dcdf4032ae..e8ee57dd3c 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -6,8 +6,93 @@ import { getLocator } from 'locate-character'; import { walk } from 'zimmerframe'; import { validate_component_options, validate_module_options } from './validate-options.js'; import { convert } from './legacy.js'; +import { transform_warnings } from './utils/warnings.js'; export { default as preprocess } from './preprocess/index.js'; +/** + * @param {string} source + * @param {string | undefined} filename + * @param {Function} fn + */ +function handle_error(source, filename, fn) { + try { + return fn(); + } catch (e) { + if (/** @type {any} */ (e).name === 'CompileError') { + const error = /** @type {import('#compiler').CompileError} */ (e); + + error.filename = filename; + + if (error.position) { + // TODO this is reused with warnings — DRY out + const locator = getLocator(source, { offsetLine: 1 }); + const start = locator(error.position[0]); + const end = locator(error.position[1]); + + error.start = start; + error.end = end; + } + } + + throw e; + } +} + +/** + * The parse function parses a component, returning only its abstract syntax tree. + * + * The `modern` option (`false` by default in Svelte 5) makes the parser return a modern AST instead of the legacy AST. + * `modern` will become `true` by default in Svelte 6, and the option will be removed in Svelte 7. + * + * https://svelte.dev/docs/svelte-compiler#svelte-parse + * @param {string} source + * @param {{ filename?: string; modern?: boolean }} [options] + * @returns {import('#compiler').SvelteNode | import('./types/legacy-nodes.js').LegacySvelteNode} + */ +export function parse(source, options = {}) { + return handle_error(source, undefined, () => { + /** @type {import('#compiler').Root} */ + const ast = _parse(source); + + if (options.modern) { + // remove things that we don't want to treat as public API + return walk(/** @type {import('#compiler').SvelteNode} */ (ast), null, { + _(node, { next }) { + // @ts-ignore + delete node.parent; + // @ts-ignore + delete node.metadata; + next(); + } + }); + } + + return convert(source, ast); + }); +} + +/** + * @param {string} source + * @param {TODO} options + */ +export function analyze(source, options = {}) { + return handle_error(source, options.filename, () => { + const validated = validate_component_options(options, ''); + const parsed = _parse(source); + + const combined_options = /** @type {import('#compiler').ValidatedCompileOptions} */ ({ + ...validated, + ...parsed.options + }); + + const analysis = analyze_component(parsed, combined_options); + + return { + warnings: transform_warnings(source, options.filename, analysis.warnings) + }; + }); +} + /** * `compile` converts your `.svelte` source code into a JavaScript module that exports a component * @@ -17,7 +102,7 @@ export { default as preprocess } from './preprocess/index.js'; * @returns {import('#compiler').CompileResult} */ export function compile(source, options) { - try { + return handle_error(source, options.filename, () => { const validated = validate_component_options(options, ''); const parsed = _parse(source); @@ -29,17 +114,7 @@ export function compile(source, options) { const analysis = analyze_component(parsed, combined_options); const result = transform_component(analysis, source, combined_options); return result; - } catch (e) { - if (/** @type {any} */ (e).name === 'CompileError') { - handle_compile_error( - /** @type {import('#compiler').CompileError} */ (e), - options.filename, - source - ); - } - - throw e; - } + }); } /** @@ -51,86 +126,11 @@ export function compile(source, options) { * @returns {import('#compiler').CompileResult} */ export function compileModule(source, options) { - try { + return handle_error(source, options.filename, () => { const validated = validate_module_options(options, ''); const analysis = analyze_module(parse_acorn(source), validated); return transform_module(analysis, source, validated); - } catch (e) { - if (/** @type {any} */ (e).name === 'CompileError') { - handle_compile_error( - /** @type {import('#compiler').CompileError} */ (e), - options.filename, - source - ); - } - - throw e; - } -} - -/** - * @param {import('#compiler').CompileError} error - * @param {string | undefined} filename - * @param {string} source - */ -function handle_compile_error(error, filename, source) { - error.filename = filename; - - if (error.position) { - // TODO this is reused with warnings — DRY out - const locator = getLocator(source, { offsetLine: 1 }); - const start = locator(error.position[0]); - const end = locator(error.position[1]); - - error.start = start; - error.end = end; - } - - throw error; -} - -/** - * The parse function parses a component, returning only its abstract syntax tree. - * - * The `modern` option (`false` by default in Svelte 5) makes the parser return a modern AST instead of the legacy AST. - * `modern` will become `true` by default in Svelte 6, and the option will be removed in Svelte 7. - * - * https://svelte.dev/docs/svelte-compiler#svelte-parse - * @param {string} source - * @param {{ filename?: string; modern?: boolean }} [options] - * @returns {import('#compiler').SvelteNode | import('./types/legacy-nodes.js').LegacySvelteNode} - */ -export function parse(source, options = {}) { - /** @type {import('#compiler').Root} */ - let ast; - try { - ast = _parse(source); - } catch (e) { - if (/** @type {any} */ (e).name === 'CompileError') { - handle_compile_error( - /** @type {import('#compiler').CompileError} */ (e), - options.filename, - source - ); - } - - throw e; - } - - if (options.modern) { - // remove things that we don't want to treat as public API - return walk(/** @type {import('#compiler').SvelteNode} */ (ast), null, { - _(node, { next }) { - // @ts-ignore - delete node.parent; - // @ts-ignore - delete node.metadata; - next(); - } - }); - } - - return convert(source, ast); + }); } /** diff --git a/packages/svelte/src/compiler/phases/3-transform/index.js b/packages/svelte/src/compiler/phases/3-transform/index.js index 66459d60d6..a8ee9640b5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/index.js @@ -3,6 +3,7 @@ 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'; import { getLocator } from 'locate-character'; +import { transform_warnings } from '../../utils/warnings.js'; /** * @param {import('../types').ComponentAnalysis} analysis @@ -102,38 +103,3 @@ export function transform_module(analysis, source, options) { } }; } - -/** - * @param {string} source - * @param {string | undefined} name - * @param {import('../types').RawWarning[]} warnings - * @returns {import('#compiler').Warning[]} - */ -function transform_warnings(source, name, warnings) { - if (warnings.length === 0) return []; - - const locate = getLocator(source, { offsetLine: 1 }); - - /** @type {import('#compiler').Warning[]} */ - const result = []; - - for (const warning of warnings) { - const start = - warning.position && - /** @type {import('locate-character').Location} */ (locate(warning.position[0])); - - const end = - warning.position && - /** @type {import('locate-character').Location} */ (locate(warning.position[1])); - - result.push({ - start, - end, - filename: name, - message: warning.message, - code: warning.code - }); - } - - return result; -} diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 8ee2cce170..5f9b4f68c6 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -2,6 +2,7 @@ import type { BindDirective, Binding, Fragment, + RawWarning, RegularElement, SvelteElement, SvelteNode, @@ -33,12 +34,6 @@ export interface BindingGroup { directives: BindDirective[]; } -export interface RawWarning { - code: string; - message: string; - position: [number, number] | undefined; -} - /** * Analysis common to modules and components */ diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 2c5480eb7f..660466de31 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -285,3 +285,9 @@ export { Css }; // TODO this chain is a bit weird export { ReactiveStatement } from '../phases/types.js'; + +export interface RawWarning { + code: string; + message: string; + position: [number, number] | undefined; +} diff --git a/packages/svelte/src/compiler/utils/warnings.js b/packages/svelte/src/compiler/utils/warnings.js new file mode 100644 index 0000000000..b0decf7b4b --- /dev/null +++ b/packages/svelte/src/compiler/utils/warnings.js @@ -0,0 +1,36 @@ +import { getLocator } from 'locate-character'; + +/** + * @param {string} source + * @param {string | undefined} name + * @param {import('#compiler').RawWarning[]} warnings + * @returns {import('#compiler').Warning[]} + */ +export function transform_warnings(source, name, warnings) { + if (warnings.length === 0) return []; + + const locate = getLocator(source, { offsetLine: 1 }); + + /** @type {import('#compiler').Warning[]} */ + const result = []; + + for (const warning of warnings) { + const start = + warning.position && + /** @type {import('locate-character').Location} */ (locate(warning.position[0])); + + const end = + warning.position && + /** @type {import('locate-character').Location} */ (locate(warning.position[1])); + + result.push({ + start, + end, + filename: name, + message: warning.message, + code: warning.code + }); + } + + return result; +}