From f8ff2b6ea37e8b1ac891e1fe0638eb90ffe0adb1 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 15 Feb 2024 18:03:57 +0100 Subject: [PATCH] chore: source maps for preprocessors + tests (#10459) Add source map merging for preprocessors and get tests passing. - fixed some issues around the `sources` array where they weren't calculated relative to the input correctly - adjusted some CSS tests because due to our own CSS parser the AST is less granular, so there are less mappings now. Don't think this is a problem, but worth thinking about - removed enableSourcemap but only log a warning, the reason this was introduced was to mitigate a bug in Vite which occured when having the source map inlined into the SSR'd output. Since SSR doesn't contain inlined CSS anymore (and if it did, we would omit the source map) the reason for which it was introduced no longer exists - files without js mapping in it have no source mappings yet (originally added in Svelte 4 for #6092) --- .changeset/silly-laws-happen.md | 5 + .../svelte/src/compiler/css/Stylesheet.js | 30 +- .../src/compiler/phases/1-parse/index.js | 17 +- .../compiler/phases/1-parse/read/context.js | 1 + .../phases/1-parse/utils/mapped_code.js | 424 ------------------ .../src/compiler/phases/2-analyze/index.js | 2 +- .../3-transform/client/transform-client.js | 2 +- .../phases/3-transform/client/utils.js | 17 +- .../3-transform/client/visitors/template.js | 15 +- .../src/compiler/phases/3-transform/index.js | 23 +- .../3-transform/server/transform-server.js | 4 +- packages/svelte/src/compiler/phases/scope.js | 2 +- .../svelte/src/compiler/preprocess/index.js | 20 +- packages/svelte/src/compiler/types/index.d.ts | 11 +- .../svelte/src/compiler/types/template.d.ts | 2 +- .../svelte/src/compiler/utils/mapped_code.js | 98 +++- .../svelte/src/compiler/validate-options.js | 14 +- packages/svelte/tests/css/test.ts | 4 +- packages/svelte/tests/helpers.js | 52 ++- packages/svelte/tests/hydration/test.ts | 6 +- .../samples/animation/output.json | 12 + .../samples/await-catch/output.json | 12 + .../samples/await-then-catch/output.json | 24 + .../samples/each-block-else/output.json | 12 + .../samples/each-block-indexed/output.json | 12 + .../samples/each-block-keyed/output.json | 12 + .../samples/each-block/output.json | 12 + .../no-error-if-before-closing/output.json | 24 + .../samples/unusual-identifier/output.json | 12 + .../samples/snippets/output.json | 14 +- .../svelte/tests/runtime-browser/test-ssr.ts | 2 +- .../svelte/tests/runtime-legacy/shared.ts | 6 +- .../tests/server-side-rendering/test.ts | 2 +- packages/svelte/tests/snapshot/test.ts | 8 +- packages/svelte/tests/sourcemaps/helpers.js | 3 +- .../samples/attached-sourcemap/_config.js | 25 +- .../samples/attached-sourcemap/test.js | 44 -- .../tests/sourcemaps/samples/basic/_config.js | 3 +- .../tests/sourcemaps/samples/basic/test.js | 34 -- .../samples/binding-shorthand/_config.js | 9 +- .../samples/binding-shorthand/input.svelte | 3 +- .../samples/binding-shorthand/test.js | 22 - .../sourcemaps/samples/binding/_config.js | 6 + .../tests/sourcemaps/samples/binding/test.js | 34 -- .../samples/compile-option-dev/_config.js | 27 -- .../samples/compile-option-dev/test.js | 40 -- .../samples/css-injected-map/_config.js | 71 +++ .../input.svelte | 0 .../tests/sourcemaps/samples/css/_config.js | 2 +- .../tests/sourcemaps/samples/css/test.js | 17 - .../samples/decoded-sourcemap/_config.js | 7 +- .../samples/decoded-sourcemap/test.js | 17 - .../sourcemaps/samples/each-block/_config.js | 2 +- .../sourcemaps/samples/each-block/test.js | 18 - .../sourcemaps/samples/external/_config.js | 12 +- .../tests/sourcemaps/samples/external/test.js | 26 -- .../sourcemaps/samples/markup/_config.js | 3 +- .../samples/no-sourcemap/_config.js | 9 - .../samples/no-sourcemap/input.svelte | 11 - .../sourcemaps/samples/no-sourcemap/test.js | 4 - .../samples/only-css-sourcemap/_config.js | 9 - .../samples/only-css-sourcemap/input.svelte | 11 - .../samples/only-css-sourcemap/test.js | 4 - .../samples/only-js-sourcemap/_config.js | 9 - .../samples/only-js-sourcemap/input.svelte | 11 - .../samples/only-js-sourcemap/test.js | 4 - .../samples/preprocessed-markup/_config.js | 4 +- .../samples/preprocessed-markup/test.js | 32 -- .../samples/preprocessed-multiple/_config.js | 7 +- .../samples/preprocessed-multiple/test.js | 32 -- .../samples/preprocessed-no-map/_config.js | 15 +- .../samples/preprocessed-no-map/test.js | 37 -- .../samples/preprocessed-script/_config.js | 4 +- .../samples/preprocessed-script/test.js | 40 -- .../samples/preprocessed-styles/_config.js | 4 +- .../samples/preprocessed-styles/test.js | 40 -- .../samples/script-after-comment/_config.js | 3 + .../samples/script-after-comment/test.js | 16 - .../sourcemaps/samples/script/_config.js | 5 + .../tests/sourcemaps/samples/script/test.js | 16 - .../samples/source-map-generator/_config.js | 11 +- .../_config.js | 3 +- .../samples/sourcemap-basename/_config.js | 17 +- .../samples/sourcemap-basename/test.js | 15 - .../samples/sourcemap-concat/_config.js | 6 +- .../samples/sourcemap-concat/test.js | 9 - .../samples/sourcemap-names/_config.js | 21 +- .../samples/sourcemap-offsets/_config.js | 11 +- .../samples/sourcemap-offsets/test.js | 19 - .../samples/sourcemap-sources/_config.js | 51 ++- .../samples/sourcemap-sources/input.svelte | 2 +- .../samples/sourcemap-sources/test.js | 23 - .../samples/static-no-script/_config.js | 8 +- .../samples/static-no-script/test.js | 9 - .../sourcemaps/samples/two-scripts/_config.js | 5 + .../sourcemaps/samples/two-scripts/test.js | 16 - .../sourcemaps/samples/typescript/_config.js | 8 +- .../sourcemaps/samples/typescript/test.js | 21 - packages/svelte/tests/sourcemaps/test.ts | 340 ++++++++++---- packages/svelte/tests/suite.ts | 6 +- packages/svelte/types/index.d.ts | 17 +- .../03-appendix/02-breaking-changes.md | 1 + 102 files changed, 926 insertions(+), 1358 deletions(-) create mode 100644 .changeset/silly-laws-happen.md delete mode 100644 packages/svelte/src/compiler/phases/1-parse/utils/mapped_code.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/attached-sourcemap/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/basic/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/binding-shorthand/test.js create mode 100644 packages/svelte/tests/sourcemaps/samples/binding/_config.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/binding/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/compile-option-dev/_config.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/compile-option-dev/test.js create mode 100644 packages/svelte/tests/sourcemaps/samples/css-injected-map/_config.js rename packages/svelte/tests/sourcemaps/samples/{compile-option-dev => css-injected-map}/input.svelte (100%) delete mode 100644 packages/svelte/tests/sourcemaps/samples/css/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/decoded-sourcemap/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/each-block/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/external/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/no-sourcemap/_config.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/no-sourcemap/input.svelte delete mode 100644 packages/svelte/tests/sourcemaps/samples/no-sourcemap/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/only-css-sourcemap/_config.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/only-css-sourcemap/input.svelte delete mode 100644 packages/svelte/tests/sourcemaps/samples/only-css-sourcemap/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/only-js-sourcemap/_config.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/only-js-sourcemap/input.svelte delete mode 100644 packages/svelte/tests/sourcemaps/samples/only-js-sourcemap/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/preprocessed-markup/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/preprocessed-multiple/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/preprocessed-no-map/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/preprocessed-script/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/preprocessed-styles/test.js create mode 100644 packages/svelte/tests/sourcemaps/samples/script-after-comment/_config.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/script-after-comment/test.js create mode 100644 packages/svelte/tests/sourcemaps/samples/script/_config.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/script/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/sourcemap-basename/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/sourcemap-concat/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/sourcemap-offsets/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/sourcemap-sources/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/static-no-script/test.js create mode 100644 packages/svelte/tests/sourcemaps/samples/two-scripts/_config.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/two-scripts/test.js delete mode 100644 packages/svelte/tests/sourcemaps/samples/typescript/test.js diff --git a/.changeset/silly-laws-happen.md b/.changeset/silly-laws-happen.md new file mode 100644 index 0000000000..76428ef3bf --- /dev/null +++ b/.changeset/silly-laws-happen.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: add proper source map support diff --git a/packages/svelte/src/compiler/css/Stylesheet.js b/packages/svelte/src/compiler/css/Stylesheet.js index e632a3692e..1337cf19e0 100644 --- a/packages/svelte/src/compiler/css/Stylesheet.js +++ b/packages/svelte/src/compiler/css/Stylesheet.js @@ -2,9 +2,8 @@ import MagicString from 'magic-string'; import { walk } from 'zimmerframe'; import { ComplexSelector } from './Selector.js'; import { hash } from './utils.js'; -// import compiler_warnings from '../compiler_warnings.js'; -// import { extract_ignores_above_position } from '../utils/extract_svelte_ignore.js'; import { create_attribute } from '../phases/nodes.js'; // TODO move this +import { merge_with_preprocessor_map } from '../utils/mapped_code.js'; const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/; const regex_name_boundary = /^[\s,;}]$/; @@ -337,7 +336,7 @@ export class Stylesheet { /** @type {import('#compiler').Style | null} */ ast; - /** @type {string} */ + /** @type {string} Path of Svelte file the CSS is in */ filename; /** @type {boolean} */ @@ -471,20 +470,23 @@ export class Stylesheet { } /** - * @param {string} file * @param {string} source - * @param {boolean} dev + * @param {import('#compiler').ValidatedCompileOptions} options */ - render(file, source, dev) { + render(source, options) { // TODO neaten this up if (!this.ast) throw new Error('Unexpected error'); const code = new MagicString(source); + // Generate source mappings for the style sheet nodes we have. + // Note that resolution is a bit more coarse than in Svelte 4 because + // our own CSS AST is not as detailed with regards to the node values. walk(/** @type {import('#compiler').Css.Node} */ (this.ast), null, { - _: (node) => { + _: (node, { next }) => { code.addSourcemapLocation(node.start); code.addSourcemapLocation(node.end); + next(); } }); @@ -495,19 +497,27 @@ export class Stylesheet { code.remove(0, this.ast.content.start); for (const child of this.children) { - child.prune(code, dev); + child.prune(code, options.dev); } code.remove(/** @type {number} */ (this.ast.content.end), source.length); - return { + const css = { code: code.toString(), map: code.generateMap({ + // include source content; makes it easier/more robust looking up the source map code includeContent: true, + // generateMap takes care of calculating source relative to file source: this.filename, - file + file: options.cssOutputFilename || this.filename }) }; + merge_with_preprocessor_map(css, options, css.map.sources[0]); + if (options.dev && options.css === 'injected' && css.code) { + css.code += `\n/*# sourceMappingURL=${css.map.toUrl()} */`; + } + + return css; } /** @param {import('../phases/types.js').ComponentAnalysis} analysis */ diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index 4e94d944c1..5c77d36ea6 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -7,6 +7,7 @@ import full_char_code_at from './utils/full_char_code_at.js'; import { error } from '../../errors.js'; import { create_fragment } from './utils/create.js'; import read_options from './read/options.js'; +import { getLocator } from 'locate-character'; const regex_position_indicator = / \(\d+:\d+\)$/; @@ -41,6 +42,8 @@ export class Parser { /** @type {LastAutoClosedTag | undefined} */ last_auto_closed_tag; + locate; + /** @param {string} template */ constructor(template) { if (typeof template !== 'string') { @@ -48,6 +51,7 @@ export class Parser { } this.template = template.trimEnd(); + this.locate = getLocator(this.template, { offsetLine: 1 }); let match_lang; @@ -133,6 +137,18 @@ export class Parser { } } + /** + * offset -> line/column + * @param {number} start + * @param {number} end + */ + get_location(start, end) { + return { + start: /** @type {import('locate-character').Location_1} */ (this.locate(start)), + end: /** @type {import('locate-character').Location_1} */ (this.locate(end)) + }; + } + current() { return this.stack[this.stack.length - 1]; } @@ -297,7 +313,6 @@ export class Parser { */ export function parse(template) { const parser = new Parser(template); - return parser.root; } diff --git a/packages/svelte/src/compiler/phases/1-parse/read/context.js b/packages/svelte/src/compiler/phases/1-parse/read/context.js index e284af3380..3759dd2049 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/context.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js @@ -28,6 +28,7 @@ export default function read_pattern(parser) { type: 'Identifier', name, start, + loc: parser.get_location(start, parser.index), end: parser.index, typeAnnotation: annotation }; diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/mapped_code.js b/packages/svelte/src/compiler/phases/1-parse/utils/mapped_code.js deleted file mode 100644 index 95f136effa..0000000000 --- a/packages/svelte/src/compiler/phases/1-parse/utils/mapped_code.js +++ /dev/null @@ -1,424 +0,0 @@ -// @ts-nocheck TODO this has a bunch of type errors in strict mode which may or may not hint at bugs - check at some point - -import remapping from '@ampproject/remapping'; -import { push_array } from './push_array.js'; - -/** @param {string} s */ -function last_line_length(s) { - return s.length - s.lastIndexOf('\n') - 1; -} - -// mutate map in-place - -/** - * @param {import('@ampproject/remapping').DecodedSourceMap} map - * @param {SourceLocation} offset - * @param {number} source_index - */ -export function sourcemap_add_offset(map, offset, source_index) { - if (map.mappings.length === 0) return; - for (let line = 0; line < map.mappings.length; line++) { - const segment_list = map.mappings[line]; - for (let segment = 0; segment < segment_list.length; segment++) { - const seg = segment_list[segment]; - // shift only segments that belong to component source file - if (seg[1] === source_index) { - // also ensures that seg.length >= 4 - // shift column if it points at the first line - if (seg[2] === 0) { - seg[3] += offset.column; - } - // shift line - seg[2] += offset.line; - } - } - } -} - -/** - * @template T - * @param {T[]} this_table - * @param {T[]} other_table - * @returns {[T[], number[], boolean, boolean]} - */ -function merge_tables(this_table, other_table) { - const new_table = this_table.slice(); - const idx_map = []; - other_table = other_table || []; - let val_changed = false; - for (const [other_idx, other_val] of other_table.entries()) { - const this_idx = this_table.indexOf(other_val); - if (this_idx >= 0) { - idx_map[other_idx] = this_idx; - } else { - const new_idx = new_table.length; - new_table[new_idx] = other_val; - idx_map[other_idx] = new_idx; - val_changed = true; - } - } - let idx_changed = val_changed; - if (val_changed) { - if ( - idx_map.find( - /** - * @param {any} val - * @param {any} idx - */ (val, idx) => val !== idx - ) === undefined - ) { - // idx_map is identity map [0, 1, 2, 3, 4, ....] - idx_changed = false; - } - } - return [new_table, idx_map, val_changed, idx_changed]; -} - -const regex_line_token = /([^\d\w\s]|\s+)/g; - -export class MappedCode { - /** @type {string} */ - string; - - /** @type {import('@ampproject/remapping').DecodedSourceMap} */ - map; - - /** - * @param {any} string - * @param {import('@ampproject/remapping').DecodedSourceMap} map - */ - constructor(string = '', map = null) { - this.string = string; - if (map) { - this.map = /** @type {import('@ampproject/remapping').DecodedSourceMap} */ (map); - } else { - this.map = { - version: 3, - mappings: [], - sources: [], - names: [] - }; - } - } - - /** - * concat in-place (mutable), return this (chainable) - * will also mutate the `other` object - * @param {MappedCode} other - * @returns {import("C:/repos/svelte/svelte-octane/mapped_code.ts-to-jsdoc").MappedCode} - */ - concat(other) { - // noop: if one is empty, return the other - if (other.string === '') return this; - if (this.string === '') { - this.string = other.string; - this.map = other.map; - return this; - } - - // compute last line length before mutating - const column_offset = last_line_length(this.string); - - this.string += other.string; - - const m1 = this.map; - const m2 = other.map; - - if (m2.mappings.length === 0) return this; - - // combine sources and names - const [sources, new_source_idx, sources_changed, sources_idx_changed] = merge_tables( - m1.sources, - m2.sources - ); - const [names, new_name_idx, names_changed, names_idx_changed] = merge_tables( - m1.names, - m2.names - ); - - if (sources_changed) m1.sources = sources; - if (names_changed) m1.names = names; - - // unswitched loops are faster - if (sources_idx_changed && names_idx_changed) { - for (let line = 0; line < m2.mappings.length; line++) { - const segment_list = m2.mappings[line]; - for (let segment = 0; segment < segment_list.length; segment++) { - const seg = segment_list[segment]; - if (seg[1] >= 0) seg[1] = new_source_idx[seg[1]]; - if (seg[4] >= 0) seg[4] = new_name_idx[seg[4]]; - } - } - } else if (sources_idx_changed) { - for (let line = 0; line < m2.mappings.length; line++) { - const segment_list = m2.mappings[line]; - for (let segment = 0; segment < segment_list.length; segment++) { - const seg = segment_list[segment]; - if (seg[1] >= 0) seg[1] = new_source_idx[seg[1]]; - } - } - } else if (names_idx_changed) { - for (let line = 0; line < m2.mappings.length; line++) { - const segment_list = m2.mappings[line]; - for (let segment = 0; segment < segment_list.length; segment++) { - const seg = segment_list[segment]; - if (seg[4] >= 0) seg[4] = new_name_idx[seg[4]]; - } - } - } - - // combine the mappings - - // combine - // 1. last line of first map - // 2. first line of second map - // columns of 2 must be shifted - - if (m2.mappings.length > 0 && column_offset > 0) { - const first_line = m2.mappings[0]; - for (let i = 0; i < first_line.length; i++) { - first_line[i][0] += column_offset; - } - } - - // combine last line + first line - push_array(m1.mappings[m1.mappings.length - 1], m2.mappings.shift()); - - // append other lines - push_array(m1.mappings, m2.mappings); - - return this; - } - - /** - * @static - * @param {string} string - * @param {import('@ampproject/remapping').DecodedSourceMap} [map] - * @returns {import("C:/repos/svelte/svelte-octane/mapped_code.ts-to-jsdoc").MappedCode} - */ - static from_processed(string, map) { - const line_count = string.split('\n').length; - - if (map) { - // ensure that count of source map mappings lines - // is equal to count of generated code lines - // (some tools may produce less) - const missing_lines = line_count - map.mappings.length; - for (let i = 0; i < missing_lines; i++) { - map.mappings.push([]); - } - return new MappedCode(string, map); - } - - if (string === '') return new MappedCode(); - map = { version: 3, names: [], sources: [], mappings: [] }; - - // add empty SourceMapSegment[] for every line - for (let i = 0; i < line_count; i++) map.mappings.push([]); - return new MappedCode(string, map); - } - - /** - * @static - * @param {import('../preprocess/types.js').Source}params_0 - * @returns {import("C:/repos/svelte/svelte-octane/mapped_code.ts-to-jsdoc").MappedCode} - */ - static from_source({ source, file_basename, get_location }) { - /** @type {SourceLocation} */ - let offset = get_location(0); - - if (!offset) offset = { line: 0, column: 0 }; - - /** @type {import('@ampproject/remapping').DecodedSourceMap} */ - const map = { version: 3, names: [], sources: [file_basename], mappings: [] }; - if (source === '') return new MappedCode(source, map); - - // we create a high resolution identity map here, - // we know that it will eventually be merged with svelte's map, - // at which stage the resolution will decrease. - const line_list = source.split('\n'); - for (let line = 0; line < line_list.length; line++) { - map.mappings.push([]); - const token_list = line_list[line].split(regex_line_token); - for (let token = 0, column = 0; token < token_list.length; token++) { - if (token_list[token] === '') continue; - map.mappings[line].push([column, 0, offset.line + line, column]); - column += token_list[token].length; - } - } - // shift columns in first line - const segment_list = map.mappings[0]; - for (let segment = 0; segment < segment_list.length; segment++) { - segment_list[segment][3] += offset.column; - } - return new MappedCode(source, map); - } -} - -/** - * @param {string} filename - * @param {Array} sourcemap_list - * @returns {import('@ampproject/remapping').RawSourceMap} - */ -export function combine_sourcemaps(filename, sourcemap_list) { - if (sourcemap_list.length === 0) return null; - - let map_idx = 1; - - /** @type {import('@ampproject/remapping').RawSourceMap} */ - const map = - sourcemap_list.slice(0, -1).find(/** @param {any} m */ (m) => m.sources.length !== 1) === - undefined - ? remapping( - // use array interface - // only the oldest sourcemap can have multiple sources - sourcemap_list, - () => null, - true // skip optional field `sourcesContent` - ) - : remapping( - // use loader interface - sourcemap_list[0], // last map - - /** @type {import('@ampproject/remapping').SourceMapLoader} */ ( - (sourcefile) => { - if (sourcefile === filename && sourcemap_list[map_idx]) { - return sourcemap_list[map_idx++]; // idx 1, 2, ... - // bundle file = branch node - } else { - return null; // source file = leaf node - } - } - ), - true - ); - - if (!map.file) delete map.file; // skip optional field `file` - - // When source maps are combined and the leading map is empty, sources is not set. - // Add the filename to the empty array in this case. - // Further improvements to remapping may help address this as well https://github.com/ampproject/remapping/issues/116 - if (!map.sources.length) map.sources = [filename]; - - return map; -} - -// browser vs node.js -const b64enc = - typeof btoa === 'function' - ? btoa /** @param {any} b */ - : (b) => Buffer.from(b).toString('base64'); -const b64dec = - typeof atob === 'function' - ? atob /** @param {any} a */ - : (a) => Buffer.from(a, 'base64').toString(); - -/** - * @param {string} filename - * @param {import('magic-string').SourceMap} svelte_map - * @param {string | import('@ampproject/remapping').DecodedSourceMap | import('@ampproject/remapping').RawSourceMap} preprocessor_map_input - * @returns {import('magic-string').SourceMap} - */ -export function apply_preprocessor_sourcemap(filename, svelte_map, preprocessor_map_input) { - if (!svelte_map || !preprocessor_map_input) return svelte_map; - - const preprocessor_map = - typeof preprocessor_map_input === 'string' - ? JSON.parse(preprocessor_map_input) - : preprocessor_map_input; - - const result_map = /** @type {import('@ampproject/remapping').RawSourceMap} */ ( - combine_sourcemaps(filename, [ - /** @type {import('@ampproject/remapping').RawSourceMap} */ (svelte_map), - preprocessor_map - ]) - ); - - // Svelte expects a SourceMap which includes toUrl and toString. Instead of wrapping our output in a class, - // we just tack on the extra properties. - Object.defineProperties(result_map, { - toString: { - enumerable: false, - value: function toString() { - return JSON.stringify(this); - } - }, - toUrl: { - enumerable: false, - value: function toUrl() { - return 'data:application/json;charset=utf-8;base64,' + b64enc(this.toString()); - } - } - }); - - return /** @type {import('magic-string').SourceMap} */ (result_map); -} - -const regex_data_uri = /data:(?:application|text)\/json;(?:charset[:=]\S+?;)?base64,(\S*)/; - -// parse attached sourcemap in processed.code - -/** - * @param {import('../preprocess/types.js').Processed} processed - * @param {'script' | 'style'} tag_name - * @returns {void} - */ -export function parse_attached_sourcemap(processed, tag_name) { - const r_in = '[#@]\\s*sourceMappingURL\\s*=\\s*(\\S*)'; - const regex = - tag_name === 'script' - ? new RegExp('(?://' + r_in + ')|(?:/\\*' + r_in + '\\s*\\*/)$') - : new RegExp('/\\*' + r_in + '\\s*\\*/$'); - - /** @param {any} message */ - function log_warning(message) { - // code_start: help to find preprocessor - const code_start = - processed.code.length < 100 ? processed.code : processed.code.slice(0, 100) + ' [...]'; - // eslint-disable-next-line no-console - console.warn(`warning: ${message}. processed.code = ${JSON.stringify(code_start)}`); - } - processed.code = processed.code.replace( - regex, - /** - * @param {any} _ - * @param {any} match1 - * @param {any} match2 - */ (_, match1, match2) => { - const map_url = tag_name === 'script' ? match1 || match2 : match1; - const map_data = (map_url.match(regex_data_uri) || [])[1]; - if (map_data) { - // sourceMappingURL is data URL - if (processed.map) { - log_warning( - 'Not implemented. ' + - 'Found sourcemap in both processed.code and processed.map. ' + - 'Please update your preprocessor to return only one sourcemap.' - ); - // ignore attached sourcemap - return ''; - } - processed.map = b64dec(map_data); // use attached sourcemap - return ''; // remove from processed.code - } - // sourceMappingURL is path or URL - if (!processed.map) { - log_warning( - `Found sourcemap path ${JSON.stringify( - map_url - )} in processed.code, but no sourcemap data. ` + - 'Please update your preprocessor to return sourcemap data directly.' - ); - } - // ignore sourcemap path - return ''; // remove from processed.code - } - ); -} - -/** - * @typedef {{ - * line: number; - * column: number; - * }} SourceLocation - */ diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 7ecde4b001..3bbc6819eb 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -343,7 +343,7 @@ export function analyze_component(root, options) { stylesheet: new Stylesheet({ ast: root.css, // TODO are any of these necessary or can we just pass in the whole `analysis` object later? - filename: options.filename ?? '', + filename: options.filename || 'input.svelte', component_name, get_css_hash: options.cssHash }), diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 00a9d1fab8..187329fe2e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -279,7 +279,7 @@ export function client_component(source, analysis, options) { '$.append_styles', b.id('$$anchor'), b.literal(analysis.stylesheet.id), - b.literal(analysis.stylesheet.render(analysis.name, source, options.dev).code) + b.literal(analysis.stylesheet.render(source, options).code) ) ) ) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 903106e7e1..49e5bb1d2d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -75,7 +75,7 @@ export function serialize_get_binding(node, state) { } if (binding.expression) { - return binding.expression; + return typeof binding.expression === 'function' ? binding.expression(node) : binding.expression; } if (binding.kind === 'prop') { @@ -550,6 +550,7 @@ function get_hoistable_params(node, context) { } else if ( // If it's a destructured derived binding, then we can extract the derived signal reference and use that. binding.expression !== null && + typeof binding.expression !== 'function' && binding.expression.type === 'MemberExpression' && binding.expression.object.type === 'CallExpression' && binding.expression.object.callee.type === 'Identifier' && @@ -697,3 +698,17 @@ export function should_proxy_or_freeze(node, scope) { } return true; } + +/** + * Port over the location information from the source to the target identifier. + * but keep the target as-is (i.e. a new id is created). + * This ensures esrap can generate accurate source maps. + * @param {import('estree').Identifier} target + * @param {import('estree').Identifier} source + */ +export function with_loc(target, source) { + if (source.loc) { + return { ...target, loc: source.loc }; + } + return target; +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 5bef734b63..c297a04a68 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -17,6 +17,7 @@ import { is_custom_element_node, is_element_node } from '../../../nodes.js'; import * as b from '../../../../utils/builders.js'; import { error } from '../../../../errors.js'; import { + with_loc, function_visitor, get_assignment_value, serialize_get_binding, @@ -2315,14 +2316,20 @@ export const template_visitors = { each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.index); - const item = b.id(each_node_meta.item_name); + const item = each_node_meta.item; const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(item.name)); - binding.expression = each_item_is_reactive ? b.call('$.unwrap', item) : item; + binding.expression = (id) => { + const item_with_loc = with_loc(item, id); + return each_item_is_reactive ? b.call('$.unwrap', item_with_loc) : item_with_loc; + }; if (node.index) { const index_binding = /** @type {import('#compiler').Binding} */ ( context.state.scope.get(node.index) ); - index_binding.expression = each_item_is_reactive ? b.call('$.unwrap', index) : index; + index_binding.expression = (id) => { + const index_with_loc = with_loc(index, id); + return each_item_is_reactive ? b.call('$.unwrap', index_with_loc) : index_with_loc; + }; } /** @type {import('estree').Statement[]} */ @@ -2337,7 +2344,7 @@ export const template_visitors = { ) ); } else { - const unwrapped = binding.expression; + const unwrapped = binding.expression(binding.node); const paths = extract_paths(node.context); for (const path of paths) { diff --git a/packages/svelte/src/compiler/phases/3-transform/index.js b/packages/svelte/src/compiler/phases/3-transform/index.js index af8de96d70..37e9e57263 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 { merge_with_preprocessor_map, get_source_name } from '../../utils/mapped_code.js'; /** * @param {import('../types').ComponentAnalysis} analysis @@ -41,13 +42,23 @@ export function transform_component(analysis, source, options) { ]; } + const js_source_name = get_source_name(options.filename, options.outputFilename, 'input.svelte'); + const js = print(program, { + // include source content; makes it easier/more robust looking up the source map code + sourceMapContent: source, + sourceMapSource: js_source_name + }); + merge_with_preprocessor_map(js, options, js_source_name); + + const css = + analysis.stylesheet.has_styles && !analysis.inject_styles + ? analysis.stylesheet.render(source, options) + : null; + return { - js: print(program, { sourceMapSource: options.filename }), // TODO needs more logic to apply map from preprocess - css: - analysis.stylesheet.has_styles && !analysis.inject_styles - ? analysis.stylesheet.render(options.filename ?? 'TODO', source, options.dev) - : null, - warnings: transform_warnings(source, options.filename, analysis.warnings), + js, + css, + warnings: transform_warnings(source, options.filename, analysis.warnings), // TODO apply preprocessor sourcemap metadata: { runes: analysis.runes } 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 2cda4ae8f6..7db7cae37c 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 @@ -329,7 +329,7 @@ function serialize_get_binding(node, state) { } if (binding.expression) { - return binding.expression; + return typeof binding.expression === 'function' ? binding.expression(node) : binding.expression; } return node; @@ -1311,7 +1311,7 @@ const template_visitors = { const each_node_meta = node.metadata; const collection = /** @type {import('estree').Expression} */ (context.visit(node.expression)); - const item = b.id(each_node_meta.item_name); + const item = each_node_meta.item; const index = each_node_meta.contains_group_binding || !node.index ? each_node_meta.index diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 5d46c31871..c1ce9f8374 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -556,7 +556,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { contains_group_binding: false, array_name: needs_array_deduplication ? state.scope.root.unique('$$array') : null, index: scope.root.unique('$$index'), - item_name: node.context.type === 'Identifier' ? node.context.name : '$$item', + item: node.context.type === 'Identifier' ? node.context : b.id('$$item'), declarations: scope.declarations, references: [...references_within] .map((id) => /** @type {import('#compiler').Binding} */ (state.scope.get(id.name))) diff --git a/packages/svelte/src/compiler/preprocess/index.js b/packages/svelte/src/compiler/preprocess/index.js index ad1061c788..9b101249a5 100644 --- a/packages/svelte/src/compiler/preprocess/index.js +++ b/packages/svelte/src/compiler/preprocess/index.js @@ -3,27 +3,21 @@ import { MappedCode, parse_attached_sourcemap, sourcemap_add_offset, - combine_sourcemaps + combine_sourcemaps, + get_basename } from '../utils/mapped_code.js'; import { decode_map } from './decode_sourcemap.js'; import { replace_in_code, slice_source } from './replace_in_code.js'; -const regex_filepath_separator = /[/\\]/; - -/** - * @param {string} filename - */ -function get_file_basename(filename) { - return /** @type {string} */ (filename.split(regex_filepath_separator).pop()); -} - /** * Represents intermediate states of the preprocessing. + * Implements the Source interface. */ class PreprocessResult { /** @type {string} */ source; - /** @type {string | undefined} */ + + /** @type {string | undefined} The filename passed as-is to preprocess */ filename; // sourcemap_list is sorted in reverse order from last map (index 0) to first map (index -1) @@ -43,7 +37,7 @@ class PreprocessResult { dependencies = []; /** - * @type {string | null } + * @type {string | null} last part of the filename, as used for `sources` in sourcemaps */ file_basename = /** @type {any} */ (undefined); @@ -61,7 +55,7 @@ class PreprocessResult { this.filename = filename; this.update_source({ string: source }); // preprocess source must be relative to itself or equal null - this.file_basename = filename == null ? null : get_file_basename(filename); + this.file_basename = filename == null ? null : get_basename(filename); } /** diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 8559254b72..fa3f75bd91 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -178,10 +178,6 @@ export interface CompileOptions extends ModuleCompileOptions { * @default null */ cssOutputFilename?: string; - - // Other Svelte 4 compiler options: - // enableSourcemap?: EnableSourcemap; // TODO bring back? https://github.com/sveltejs/svelte/pull/6835 - // legacy?: boolean; // TODO compiler error noting the new purpose? } export interface ModuleCompileOptions { @@ -285,8 +281,11 @@ export interface Binding { legacy_dependencies: Binding[]; /** Legacy props: the `class` in `{ export klass as class}` */ prop_alias: string | null; - /** If this is set, all references should use this expression instead of the identifier name */ - expression: Expression | null; + /** + * If this is set, all references should use this expression instead of the identifier name. + * If a function is given, it will be called with the identifier at that location and should return the new expression. + */ + expression: Expression | ((id: Identifier) => Expression) | null; /** If this is set, all mutations should use this expression */ mutation: ((assignment: AssignmentExpression, context: Context) => Expression) | null; } diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 26b6ee7ca2..0cbe05c57b 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -378,7 +378,7 @@ export interface EachBlock extends BaseNode { /** Set if something in the array expression is shadowed within the each block */ array_name: Identifier | null; index: Identifier; - item_name: string; + item: Identifier; declarations: Map; /** List of bindings that are referenced within the expression */ references: Binding[]; diff --git a/packages/svelte/src/compiler/utils/mapped_code.js b/packages/svelte/src/compiler/utils/mapped_code.js index a438e48fd7..5fe215e435 100644 --- a/packages/svelte/src/compiler/utils/mapped_code.js +++ b/packages/svelte/src/compiler/utils/mapped_code.js @@ -243,8 +243,18 @@ export class MappedCode { } } +// browser vs node.js +const b64enc = + typeof window !== 'undefined' && typeof btoa === 'function' + ? /** @param {string} str */ (str) => btoa(unescape(encodeURIComponent(str))) + : /** @param {string} str */ (str) => Buffer.from(str).toString('base64'); +const b64dec = + typeof window !== 'undefined' && typeof atob === 'function' + ? atob + : /** @param {any} a */ (a) => Buffer.from(a, 'base64').toString(); + /** - * @param {string} filename + * @param {string} filename Basename of the input file * @param {Array} sourcemap_list */ export function combine_sourcemaps(filename, sourcemap_list) { @@ -263,6 +273,10 @@ export function combine_sourcemaps(filename, sourcemap_list) { // use loader interface sourcemap_list[0], // last map (sourcefile) => { + // TODO the equality check assumes that the preprocessor map has the input file as a relative path in sources, + // e.g. when the input file is `src/foo/bar.svelte`, then sources is expected to contain just `bar.svelte`. + // Therefore filename also needs to be the basename of the path. This feels brittle, investigate how we can + // harden this (without breaking other tooling that assumes this behavior). if (sourcefile === filename && sourcemap_list[map_idx]) { return sourcemap_list[map_idx++]; // idx 1, 2, ... // bundle file = branch node @@ -286,7 +300,7 @@ export function combine_sourcemaps(filename, sourcemap_list) { * @param {string | import('@ampproject/remapping').DecodedSourceMap | import('@ampproject/remapping').RawSourceMap} preprocessor_map_input * @returns {import('magic-string').SourceMap} */ -export function apply_preprocessor_sourcemap(filename, svelte_map, preprocessor_map_input) { +function apply_preprocessor_sourcemap(filename, svelte_map, preprocessor_map_input) { if (!svelte_map || !preprocessor_map_input) return svelte_map; const preprocessor_map = typeof preprocessor_map_input === 'string' @@ -305,18 +319,7 @@ export function apply_preprocessor_sourcemap(filename, svelte_map, preprocessor_ toUrl: { enumerable: false, value: function toUrl() { - let b64 = ''; - if (typeof window !== 'undefined' && window.btoa) { - // btoa doesn't support multi-byte characters - b64 = window.btoa(unescape(encodeURIComponent(this.toString()))); - } else if (typeof Buffer !== 'undefined') { - b64 = Buffer.from(this.toString(), 'utf8').toString('base64'); - } else { - throw new Error( - 'Unsupported environment: `window.btoa` or `Buffer` should be present to use toUrl.' - ); - } - return 'data:application/json;charset=utf-8;base64,' + b64; + return 'data:application/json;charset=utf-8;base64,' + b64enc(this.toString()); } } }); @@ -361,7 +364,7 @@ export function parse_attached_sourcemap(processed, tag_name) { // ignore attached sourcemap return ''; } - processed.map = atob(map_data); // use attached sourcemap + processed.map = b64dec(map_data); // use attached sourcemap return ''; // remove from processed.code } // sourceMappingURL is path or URL @@ -377,3 +380,68 @@ export function parse_attached_sourcemap(processed, tag_name) { return ''; // remove from processed.code }); } + +/** + * @param {{ code: string, map: import('magic-string').SourceMap}} result + * @param {import('#compiler').ValidatedCompileOptions} options + * @param {string} source_name + */ +export function merge_with_preprocessor_map(result, options, source_name) { + if (options.sourcemap) { + const file_basename = get_basename(options.filename || 'input.svelte'); + // The preprocessor map is expected to contain `sources: [basename_of_filename]`, but our own + // map may contain a different file name. Patch our map beforehand to align sources so merging + // with the preprocessor map works correctly. + result.map.sources = [file_basename]; + result.map = apply_preprocessor_sourcemap( + file_basename, + result.map, + /** @type {any} */ (options.sourcemap) + ); + // After applying the preprocessor map, we need to do the inverse and make the sources + // relative to the input file again in case the output code is in a different directory. + if (file_basename !== source_name) { + result.map.sources = result.map.sources.map( + /** @param {string} source */ (source) => get_relative_path(source_name, source) + ); + } + } +} + +/** + * @param {string} from + * @param {string} to + */ +export function get_relative_path(from, to) { + // Don't use node's utils here to ensure the compiler is usable in a browser environment + const from_parts = from.split(/[/\\]/); + const to_parts = to.split(/[/\\]/); + from_parts.pop(); // get dirname + while (from_parts[0] === to_parts[0]) { + from_parts.shift(); + to_parts.shift(); + } + if (from_parts.length) { + let i = from_parts.length; + while (i--) from_parts[i] = '..'; + } + return from_parts.concat(to_parts).join('/'); +} + +/** + * Like node's `basename`, but doesn't use it to ensure the compiler is usable in a browser environment + * @param {string} filename + */ +export function get_basename(filename) { + return /** @type {string} */ (filename.split(/[/\\]/).pop()); +} + +/** + * @param {string | undefined} filename + * @param {string | undefined} output_filename + * @param {string} fallback + */ +export function get_source_name(filename, output_filename, fallback) { + if (!filename) return fallback; + return output_filename ? get_relative_path(output_filename, filename) : get_basename(filename); +} diff --git a/packages/svelte/src/compiler/validate-options.js b/packages/svelte/src/compiler/validate-options.js index 5752435178..1151ad0dc1 100644 --- a/packages/svelte/src/compiler/validate-options.js +++ b/packages/svelte/src/compiler/validate-options.js @@ -87,7 +87,6 @@ export const validate_component_options = namespace: list(['html', 'svg', 'foreign']), - // TODO this is a sourcemap option, would be good to put under a sourcemap namespace outputFilename: string(undefined), preserveComments: boolean(false), @@ -96,16 +95,15 @@ export const validate_component_options = runes: boolean(undefined), - sourcemap: validator(undefined, (input, keypath) => { - // TODO - return input; - }), - - enableSourcemap: validator(undefined, (input, keypath) => { - // TODO decide if we want to keep this + sourcemap: validator(undefined, (input) => { + // Source maps can take on a variety of values, including string, JSON, map objects from magic-string and source-map, + // so there's no good way to check type validity here return input; }), + enableSourcemap: warn_removed( + 'The enableSourcemap option has been removed. Source maps are always generated now, and tooling can choose to ignore them.' + ), hydratable: warn_removed( 'The hydratable option has been removed. Svelte components are always hydratable now.' ), diff --git a/packages/svelte/tests/css/test.ts b/packages/svelte/tests/css/test.ts index 1d8dc5c563..3eb54e160d 100644 --- a/packages/svelte/tests/css/test.ts +++ b/packages/svelte/tests/css/test.ts @@ -25,8 +25,8 @@ const { test, run } = suite(async (config, cwd) => { // TODO // const expected_warnings = (config.warnings || []).map(normalize_warning); - compile_directory(cwd, 'client', { cssHash: () => 'svelte-xyz', ...config.compileOptions }); - compile_directory(cwd, 'server', { cssHash: () => 'svelte-xyz', ...config.compileOptions }); + await compile_directory(cwd, 'client', { cssHash: () => 'svelte-xyz', ...config.compileOptions }); + await compile_directory(cwd, 'server', { cssHash: () => 'svelte-xyz', ...config.compileOptions }); const dom_css = fs.readFileSync(`${cwd}/_output/client/input.svelte.css`, 'utf-8').trim(); const ssr_css = fs.readFileSync(`${cwd}/_output/server/input.svelte.css`, 'utf-8').trim(); diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js index a8e3d605d9..d08b81b570 100644 --- a/packages/svelte/tests/helpers.js +++ b/packages/svelte/tests/helpers.js @@ -1,7 +1,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import glob from 'tiny-glob/sync.js'; -import { compile, compileModule } from 'svelte/compiler'; +import { VERSION, compile, compileModule, preprocess } from 'svelte/compiler'; /** * @param {string} file @@ -54,8 +54,16 @@ export function create_deferred() { * @param {string} cwd * @param {'client' | 'server'} generate * @param {Partial} compileOptions + * @param {boolean} [output_map] + * @param {any} [preprocessor] */ -export function compile_directory(cwd, generate, compileOptions = {}) { +export async function compile_directory( + cwd, + generate, + compileOptions = {}, + output_map = false, + preprocessor +) { const output_dir = `${cwd}/_output/${generate}`; fs.rmSync(output_dir, { recursive: true, force: true }); @@ -63,8 +71,12 @@ export function compile_directory(cwd, generate, compileOptions = {}) { for (const file of glob('**', { cwd, filesOnly: true })) { if (file.startsWith('_')) continue; - const text = fs.readFileSync(`${cwd}/${file}`, 'utf-8'); - const opts = { filename: path.join(cwd, file), ...compileOptions, generate }; + let text = fs.readFileSync(`${cwd}/${file}`, 'utf-8'); + let opts = { + filename: path.join(cwd, file), + ...compileOptions, + generate + }; if (file.endsWith('.js')) { const out = `${output_dir}/${file}`; @@ -85,12 +97,42 @@ export function compile_directory(cwd, generate, compileOptions = {}) { write(out, result); } } else if (file.endsWith('.svelte')) { - const compiled = compile(text, opts); + if (preprocessor?.preprocess) { + const preprocessed = await preprocess( + text, + preprocessor.preprocess, + preprocessor.options || { + filename: opts.filename + } + ); + text = preprocessed.code; + opts = { ...opts, sourcemap: preprocessed.map }; + write(`${output_dir}/${file.slice(0, -7)}.preprocessed.svelte`, text); + if (output_map) { + write( + `${output_dir}/${file.slice(0, -7)}.preprocessed.svelte.map`, + JSON.stringify(preprocessed.map, null, '\t') + ); + } + } + + const compiled = compile(text, { + outputFilename: `${output_dir}/${file}${file.endsWith('.js') ? '' : '.js'}`, + cssOutputFilename: `${output_dir}/${file}.css`, + ...opts + }); + compiled.js.code = compiled.js.code.replace(`v${VERSION}`, 'VERSION'); write(`${output_dir}/${file}.js`, compiled.js.code); + if (output_map) { + write(`${output_dir}/${file}.js.map`, JSON.stringify(compiled.js.map, null, '\t')); + } if (compiled.css) { write(`${output_dir}/${file}.css`, compiled.css.code); + if (output_map) { + write(`${output_dir}/${file}.css.map`, JSON.stringify(compiled.css.map, null, '\t')); + } } } } diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts index d10612ef9b..3f7d2e5815 100644 --- a/packages/svelte/tests/hydration/test.ts +++ b/packages/svelte/tests/hydration/test.ts @@ -1,7 +1,7 @@ // @vitest-environment jsdom import * as fs from 'node:fs'; -import { assert, expect } from 'vitest'; +import { assert } from 'vitest'; import { compile_directory, should_update_expected } from '../helpers.js'; import { assert_html_equal } from '../html_equal.js'; import { suite, assert_ok } from '../suite.js'; @@ -46,8 +46,8 @@ const { test, run } = suite(async (config, cwd) => { } if (!config.load_compiled) { - compile_directory(cwd, 'client', { accessors: true, ...config.compileOptions }); - compile_directory(cwd, 'server', config.compileOptions); + await compile_directory(cwd, 'client', { accessors: true, ...config.compileOptions }); + await compile_directory(cwd, 'server', config.compileOptions); } const target = window.document.body; diff --git a/packages/svelte/tests/parser-legacy/samples/animation/output.json b/packages/svelte/tests/parser-legacy/samples/animation/output.json index 6a43c13c2f..0d82cb2bb9 100644 --- a/packages/svelte/tests/parser-legacy/samples/animation/output.json +++ b/packages/svelte/tests/parser-legacy/samples/animation/output.json @@ -39,6 +39,18 @@ "type": "Identifier", "name": "thing", "start": 17, + "loc": { + "start": { + "line": 1, + "column": 17, + "character": 17 + }, + "end": { + "line": 1, + "column": 22, + "character": 22 + } + }, "end": 22 }, "expression": { diff --git a/packages/svelte/tests/parser-legacy/samples/await-catch/output.json b/packages/svelte/tests/parser-legacy/samples/await-catch/output.json index 06a73d522c..5572d573f8 100644 --- a/packages/svelte/tests/parser-legacy/samples/await-catch/output.json +++ b/packages/svelte/tests/parser-legacy/samples/await-catch/output.json @@ -29,6 +29,18 @@ "type": "Identifier", "name": "theError", "start": 47, + "loc": { + "start": { + "line": 3, + "column": 8, + "character": 47 + }, + "end": { + "line": 3, + "column": 16, + "character": 55 + } + }, "end": 55 }, "pending": { diff --git a/packages/svelte/tests/parser-legacy/samples/await-then-catch/output.json b/packages/svelte/tests/parser-legacy/samples/await-then-catch/output.json index a2ccb995e0..b71365f39d 100644 --- a/packages/svelte/tests/parser-legacy/samples/await-then-catch/output.json +++ b/packages/svelte/tests/parser-legacy/samples/await-then-catch/output.json @@ -28,12 +28,36 @@ "type": "Identifier", "name": "theValue", "start": 46, + "loc": { + "start": { + "line": 3, + "column": 7, + "character": 46 + }, + "end": { + "line": 3, + "column": 15, + "character": 54 + } + }, "end": 54 }, "error": { "type": "Identifier", "name": "theError", "start": 96, + "loc": { + "start": { + "line": 5, + "column": 8, + "character": 96 + }, + "end": { + "line": 5, + "column": 16, + "character": 104 + } + }, "end": 104 }, "pending": { diff --git a/packages/svelte/tests/parser-legacy/samples/each-block-else/output.json b/packages/svelte/tests/parser-legacy/samples/each-block-else/output.json index 5af3bff86d..a6db309edb 100644 --- a/packages/svelte/tests/parser-legacy/samples/each-block-else/output.json +++ b/packages/svelte/tests/parser-legacy/samples/each-block-else/output.json @@ -44,6 +44,18 @@ "type": "Identifier", "name": "animal", "start": 18, + "loc": { + "start": { + "line": 1, + "column": 18, + "character": 18 + }, + "end": { + "line": 1, + "column": 24, + "character": 24 + } + }, "end": 24 }, "expression": { diff --git a/packages/svelte/tests/parser-legacy/samples/each-block-indexed/output.json b/packages/svelte/tests/parser-legacy/samples/each-block-indexed/output.json index 915dd6228b..bce7fd81a2 100644 --- a/packages/svelte/tests/parser-legacy/samples/each-block-indexed/output.json +++ b/packages/svelte/tests/parser-legacy/samples/each-block-indexed/output.json @@ -72,6 +72,18 @@ "type": "Identifier", "name": "animal", "start": 18, + "loc": { + "start": { + "line": 1, + "column": 18, + "character": 18 + }, + "end": { + "line": 1, + "column": 24, + "character": 24 + } + }, "end": 24 }, "expression": { diff --git a/packages/svelte/tests/parser-legacy/samples/each-block-keyed/output.json b/packages/svelte/tests/parser-legacy/samples/each-block-keyed/output.json index 9582d45465..2f6206a6cb 100644 --- a/packages/svelte/tests/parser-legacy/samples/each-block-keyed/output.json +++ b/packages/svelte/tests/parser-legacy/samples/each-block-keyed/output.json @@ -44,6 +44,18 @@ "type": "Identifier", "name": "todo", "start": 16, + "loc": { + "start": { + "line": 1, + "column": 16, + "character": 16 + }, + "end": { + "line": 1, + "column": 20, + "character": 20 + } + }, "end": 20 }, "expression": { diff --git a/packages/svelte/tests/parser-legacy/samples/each-block/output.json b/packages/svelte/tests/parser-legacy/samples/each-block/output.json index 1b418dac5a..f26f557958 100644 --- a/packages/svelte/tests/parser-legacy/samples/each-block/output.json +++ b/packages/svelte/tests/parser-legacy/samples/each-block/output.json @@ -44,6 +44,18 @@ "type": "Identifier", "name": "animal", "start": 18, + "loc": { + "start": { + "line": 1, + "column": 18, + "character": 18 + }, + "end": { + "line": 1, + "column": 24, + "character": 24 + } + }, "end": 24 }, "expression": { diff --git a/packages/svelte/tests/parser-legacy/samples/no-error-if-before-closing/output.json b/packages/svelte/tests/parser-legacy/samples/no-error-if-before-closing/output.json index 79a26426de..c60efd4fba 100644 --- a/packages/svelte/tests/parser-legacy/samples/no-error-if-before-closing/output.json +++ b/packages/svelte/tests/parser-legacy/samples/no-error-if-before-closing/output.json @@ -119,6 +119,18 @@ "type": "Identifier", "name": "f", "start": 97, + "loc": { + "start": { + "line": 13, + "column": 7, + "character": 97 + }, + "end": { + "line": 13, + "column": 8, + "character": 98 + } + }, "end": 98 }, "error": null, @@ -207,6 +219,18 @@ "type": "Identifier", "name": "f", "start": 137, + "loc": { + "start": { + "line": 18, + "column": 7, + "character": 137 + }, + "end": { + "line": 18, + "column": 8, + "character": 138 + } + }, "end": 138 }, "error": null, diff --git a/packages/svelte/tests/parser-legacy/samples/unusual-identifier/output.json b/packages/svelte/tests/parser-legacy/samples/unusual-identifier/output.json index 96e6c0a135..9081b7cb92 100644 --- a/packages/svelte/tests/parser-legacy/samples/unusual-identifier/output.json +++ b/packages/svelte/tests/parser-legacy/samples/unusual-identifier/output.json @@ -44,6 +44,18 @@ "type": "Identifier", "name": "𐊧", "start": 17, + "loc": { + "start": { + "line": 1, + "column": 17, + "character": 17 + }, + "end": { + "line": 1, + "column": 19, + "character": 19 + } + }, "end": 19 }, "expression": { diff --git a/packages/svelte/tests/parser-modern/samples/snippets/output.json b/packages/svelte/tests/parser-modern/samples/snippets/output.json index 732ba5888a..c8f6b2bcda 100644 --- a/packages/svelte/tests/parser-modern/samples/snippets/output.json +++ b/packages/svelte/tests/parser-modern/samples/snippets/output.json @@ -27,9 +27,21 @@ "parameters": [ { "type": "Identifier", + "name": "msg", "start": 43, + "loc": { + "start": { + "line": 3, + "column": 14, + "character": 43 + }, + "end": { + "line": 3, + "column": 25, + "character": 54 + } + }, "end": 54, - "name": "msg", "typeAnnotation": { "type": "TSTypeAnnotation", "start": 46, diff --git a/packages/svelte/tests/runtime-browser/test-ssr.ts b/packages/svelte/tests/runtime-browser/test-ssr.ts index c94db0a55e..27cf2334c3 100644 --- a/packages/svelte/tests/runtime-browser/test-ssr.ts +++ b/packages/svelte/tests/runtime-browser/test-ssr.ts @@ -17,7 +17,7 @@ export async function run_ssr_test( test_dir: string ) { try { - compile_directory(test_dir, 'server', { + await compile_directory(test_dir, 'server', { ...config.compileOptions, runes: test_dir.includes('runtime-runes') }); diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 8c04e91f6c..93b3552493 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -122,7 +122,7 @@ export function runtime_suite(runes: boolean) { ); } -function common_setup(cwd: string, runes: boolean | undefined, config: RuntimeTest) { +async function common_setup(cwd: string, runes: boolean | undefined, config: RuntimeTest) { const compileOptions: CompileOptions = { generate: 'client', ...config.compileOptions, @@ -134,8 +134,8 @@ function common_setup(cwd: string, runes: boolean | undefined, config: RuntimeTe // load_compiled can be used for debugging a test. It means the compiler will not run on the input // so you can manipulate the output manually to see what fixes it, adding console.logs etc. if (!config.load_compiled) { - compile_directory(cwd, 'client', compileOptions); - compile_directory(cwd, 'server', compileOptions); + await compile_directory(cwd, 'client', compileOptions); + await compile_directory(cwd, 'server', compileOptions); } return compileOptions; diff --git a/packages/svelte/tests/server-side-rendering/test.ts b/packages/svelte/tests/server-side-rendering/test.ts index 72843c98bd..fdd1f9993c 100644 --- a/packages/svelte/tests/server-side-rendering/test.ts +++ b/packages/svelte/tests/server-side-rendering/test.ts @@ -18,7 +18,7 @@ interface SSRTest extends BaseTest { } const { test, run } = suite(async (config, test_dir) => { - compile_directory(test_dir, 'server', config.compileOptions); + await compile_directory(test_dir, 'server', config.compileOptions); const Component = (await import(`${test_dir}/_output/server/main.svelte.js`)).default; const expected_html = try_read_file(`${test_dir}/_expected.html`); diff --git a/packages/svelte/tests/snapshot/test.ts b/packages/svelte/tests/snapshot/test.ts index 26f3a1a1b7..88cf9193c3 100644 --- a/packages/svelte/tests/snapshot/test.ts +++ b/packages/svelte/tests/snapshot/test.ts @@ -10,17 +10,13 @@ interface SnapshotTest extends BaseTest { } const { test, run } = suite(async (config, cwd) => { - compile_directory(cwd, 'client', config.compileOptions); - compile_directory(cwd, 'server', config.compileOptions); + await compile_directory(cwd, 'client', config.compileOptions); + await compile_directory(cwd, 'server', config.compileOptions); // run `UPDATE_SNAPSHOTS=true pnpm test snapshot` to update snapshot tests if (process.env.UPDATE_SNAPSHOTS) { fs.rmSync(`${cwd}/_expected`, { recursive: true, force: true }); fs.cpSync(`${cwd}/_output`, `${cwd}/_expected`, { recursive: true, force: true }); - - for (const file of glob(`${cwd}/_expected/**`, { filesOnly: true })) { - fs.writeFileSync(file, fs.readFileSync(file, 'utf-8').replace(`v${VERSION}`, 'VERSION')); - } } else { const actual = glob('**', { cwd: `${cwd}/_output`, filesOnly: true }); const expected = glob('**', { cwd: `${cwd}/_expected`, filesOnly: true }); diff --git a/packages/svelte/tests/sourcemaps/helpers.js b/packages/svelte/tests/sourcemaps/helpers.js index f4944296c4..4a2a3ccb9f 100644 --- a/packages/svelte/tests/sourcemaps/helpers.js +++ b/packages/svelte/tests/sourcemaps/helpers.js @@ -1,4 +1,5 @@ import * as assert from 'node:assert'; +import * as path from 'node:path'; import { getLocator } from 'locate-character'; import MagicString, { Bundle } from 'magic-string'; @@ -111,7 +112,7 @@ export function magic_string_preprocessor_result(filename, src) { return { code: src.toString(), map: src.generateMap({ - source: filename, + source: path.basename(filename), // preprocessors are expected to return `sources: [file_basename]` hires: true, includeContent: false }) diff --git a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js index 7cd63837db..187eb4ade5 100644 --- a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js @@ -1,3 +1,4 @@ +import * as path from 'node:path'; import MagicString from 'magic-string'; import { test } from '../../test'; @@ -13,7 +14,7 @@ let comment_multi = true; */ function get_processor(tag_name, search, replace) { /** @type {import('../../../../src/compiler/public').Preprocessor} */ - const preprocessor = ({ content, filename }) => { + const preprocessor = ({ content, filename = '' }) => { let code = content.slice(); const ms = new MagicString(code); @@ -25,7 +26,7 @@ function get_processor(tag_name, search, replace) { const indent = Array.from({ length: indent_size }).join(' '); ms.prependLeft(idx, '\n' + indent); - const map_opts = { source: filename, hires: true, includeContent: false }; + const map_opts = { source: path.basename(filename), hires: true, includeContent: false }; const map = ms.generateMap(map_opts); const attach_line = tag_name == 'style' || comment_multi @@ -44,12 +45,28 @@ function get_processor(tag_name, search, replace) { } export default test({ - skip: true, preprocess: [ get_processor('script', 'replace_me_script', 'done_replace_script_1'), get_processor('script', 'done_replace_script_1', 'done_replace_script_2'), get_processor('style', '.replace_me_style', '.done_replace_style_1'), get_processor('style', '.done_replace_style_1', '.done_replace_style_2') - ] + ], + client: [ + { str: 'replace_me_script', strGenerated: 'done_replace_script_2' }, + { str: 'done_replace_script_2', idxGenerated: 1 } + ], + css: [{ str: '.replace_me_style', strGenerated: '.done_replace_style_2.svelte-o6vre' }], + test({ assert, code_preprocessed, code_css }) { + assert.equal( + code_preprocessed.includes('\n/*# sourceMappingURL=data:application/json;base64,'), + false, + 'magic-comment attachments were NOT removed' + ); + assert.equal( + code_css.includes('\n/*# sourceMappingURL=data:application/json;base64,'), + false, + 'magic-comment attachments were NOT removed' + ); + } }); diff --git a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/test.js b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/test.js deleted file mode 100644 index 1dfb3caeea..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/test.js +++ /dev/null @@ -1,44 +0,0 @@ -import * as assert from 'node:assert'; - -const get_line_column = (obj) => ({ line: obj.line, column: obj.column }); - -export function test({ input, css, js }) { - let out_obj, loc_output, actual, loc_input, expected; - - out_obj = js; - // we need the second occurrence of 'done_replace_script_2' in output.js - // the first occurrence is mapped back to markup '{done_replace_script_2}' - loc_output = out_obj.locate_1('done_replace_script_2'); - loc_output = out_obj.locate_1('done_replace_script_2', loc_output.character + 1); - actual = out_obj.mapConsumer.originalPositionFor(loc_output); - loc_input = input.locate_1('replace_me_script'); - expected = { - source: 'input.svelte', - name: 'replace_me_script', - ...get_line_column(loc_input) - }; - assert.deepEqual(actual, expected); - - out_obj = css; - loc_output = out_obj.locate_1('.done_replace_style_2'); - actual = out_obj.mapConsumer.originalPositionFor(loc_output); - loc_input = input.locate_1('.replace_me_style'); - expected = { - source: 'input.svelte', - name: '.replace_me_style', - ...get_line_column(loc_input) - }; - assert.deepEqual(actual, expected); - - assert.equal( - js.code.indexOf('\n/*# sourceMappingURL=data:application/json;base64,'), - -1, - 'magic-comment attachments were NOT removed' - ); - - assert.equal( - css.code.indexOf('\n/*# sourceMappingURL=data:application/json;base64,'), - -1, - 'magic-comment attachments were NOT removed' - ); -} diff --git a/packages/svelte/tests/sourcemaps/samples/basic/_config.js b/packages/svelte/tests/sourcemaps/samples/basic/_config.js index f965e04b02..591a614397 100644 --- a/packages/svelte/tests/sourcemaps/samples/basic/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/basic/_config.js @@ -1,5 +1,6 @@ import { test } from '../../test'; export default test({ - skip: true + client: ['foo.bar.baz'], + server: ['foo.bar.baz'] }); diff --git a/packages/svelte/tests/sourcemaps/samples/basic/test.js b/packages/svelte/tests/sourcemaps/samples/basic/test.js deleted file mode 100644 index 34c619c128..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/basic/test.js +++ /dev/null @@ -1,34 +0,0 @@ -export function test({ assert, input, js }) { - const expected = input.locate('foo.bar.baz'); - - let start; - let actual; - - start = js.locate('ctx[0].bar.baz'); - - actual = js.mapConsumer.originalPositionFor({ - line: start.line + 1, - column: start.column - }); - - assert.deepEqual(actual, { - source: 'input.svelte', - name: null, - line: expected.line + 1, - column: expected.column - }); - - start = js.locate('ctx[0].bar.baz', start.character + 1); - - actual = js.mapConsumer.originalPositionFor({ - line: start.line + 1, - column: start.column - }); - - assert.deepEqual(actual, { - source: 'input.svelte', - name: null, - line: expected.line + 1, - column: expected.column - }); -} diff --git a/packages/svelte/tests/sourcemaps/samples/binding-shorthand/_config.js b/packages/svelte/tests/sourcemaps/samples/binding-shorthand/_config.js index 64fdc120d6..c4eab5b039 100644 --- a/packages/svelte/tests/sourcemaps/samples/binding-shorthand/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/binding-shorthand/_config.js @@ -1,3 +1,10 @@ import { test } from '../../test'; -export default test({ skip: true }); +export default test({ + skip: true, // No source map for binding in template because there's no loc property for it; skipped in Svelte 4, too + client: [ + 'potato', + { str: 'potato', idxOriginal: 1, idxGenerated: 3 }, + { str: 'potato', idxOriginal: 1, idxGenerated: 5 } + ] +}); diff --git a/packages/svelte/tests/sourcemaps/samples/binding-shorthand/input.svelte b/packages/svelte/tests/sourcemaps/samples/binding-shorthand/input.svelte index 7ba7c7c10d..53829fb4f0 100644 --- a/packages/svelte/tests/sourcemaps/samples/binding-shorthand/input.svelte +++ b/packages/svelte/tests/sourcemaps/samples/binding-shorthand/input.svelte @@ -1,7 +1,6 @@ +{potato} diff --git a/packages/svelte/tests/sourcemaps/samples/binding-shorthand/test.js b/packages/svelte/tests/sourcemaps/samples/binding-shorthand/test.js deleted file mode 100644 index 13ecdbf889..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/binding-shorthand/test.js +++ /dev/null @@ -1,22 +0,0 @@ -export function test({ assert, input, js }) { - const expected = input.locate('potato'); - - let start; - - start = js.locate('potato'); - start = js.locate('potato', start.character + 1); - start = js.locate('potato', start.character + 1); - // we need the third instance of 'potato' - - const actual = js.mapConsumer.originalPositionFor({ - line: start.line + 1, - column: start.column - }); - - assert.deepEqual(actual, { - source: 'input.svelte', - name: null, - line: expected.line + 1, - column: expected.column - }); -} diff --git a/packages/svelte/tests/sourcemaps/samples/binding/_config.js b/packages/svelte/tests/sourcemaps/samples/binding/_config.js new file mode 100644 index 0000000000..8c14a698f7 --- /dev/null +++ b/packages/svelte/tests/sourcemaps/samples/binding/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +export default test({ + client: ['bar.baz'], + server: ['bar.baz'] +}); diff --git a/packages/svelte/tests/sourcemaps/samples/binding/test.js b/packages/svelte/tests/sourcemaps/samples/binding/test.js deleted file mode 100644 index 3cb3246e50..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/binding/test.js +++ /dev/null @@ -1,34 +0,0 @@ -export function test({ assert, input, js }) { - const expected = input.locate('bar.baz'); - - let start; - let actual; - - start = js.locate('bar.baz'); - - actual = js.mapConsumer.originalPositionFor({ - line: start.line + 1, - column: start.column - }); - - assert.deepEqual(actual, { - source: 'input.svelte', - name: null, - line: expected.line + 1, - column: expected.column - }); - - start = js.locate('bar.baz', start.character + 1); - - actual = js.mapConsumer.originalPositionFor({ - line: start.line + 1, - column: start.column - }); - - assert.deepEqual(actual, { - source: 'input.svelte', - name: null, - line: expected.line + 1, - column: expected.column - }); -} diff --git a/packages/svelte/tests/sourcemaps/samples/compile-option-dev/_config.js b/packages/svelte/tests/sourcemaps/samples/compile-option-dev/_config.js deleted file mode 100644 index cd0505c2d0..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/compile-option-dev/_config.js +++ /dev/null @@ -1,27 +0,0 @@ -import MagicString from 'magic-string'; -import { test } from '../../test'; -import { magic_string_preprocessor_result, magic_string_replace_all } from '../../helpers.js'; - -export default test({ - skip: true, - compileOptions: { - dev: true - }, - preprocess: [ - { - style: ({ content, filename = '' }) => { - const src = new MagicString(content); - magic_string_replace_all(src, '--replace-me-once', '\n --done-replace-once'); - magic_string_replace_all(src, '--replace-me-twice', '\n--almost-done-replace-twice'); - return magic_string_preprocessor_result(filename, src); - } - }, - { - style: ({ content, filename = '' }) => { - const src = new MagicString(content); - magic_string_replace_all(src, '--almost-done-replace-twice', '\n --done-replace-twice'); - return magic_string_preprocessor_result(filename, src); - } - } - ] -}); diff --git a/packages/svelte/tests/sourcemaps/samples/compile-option-dev/test.js b/packages/svelte/tests/sourcemaps/samples/compile-option-dev/test.js deleted file mode 100644 index 16477061f8..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/compile-option-dev/test.js +++ /dev/null @@ -1,40 +0,0 @@ -import { SourceMapConsumer } from 'source-map'; - -const b64dec = (s) => Buffer.from(s, 'base64').toString(); - -export async function test({ assert, css, js }) { - // We check that the css source map embedded in the js is accurate - const match = js.code.match( - /\tappend_styles\(target, "svelte-.{6}", "(.*?)(?:\\n\/\*# sourceMappingURL=data:(.*?);charset=(.*?);base64,(.*?) \*\/)?"\);\n/ - ); - - assert.notEqual(match, null); - - const [mime_type, encoding, css_map_base64] = match.slice(2); - assert.equal(mime_type, 'application/json'); - assert.equal(encoding, 'utf-8'); - - const css_map_json = b64dec(css_map_base64); - css.mapConsumer = await new SourceMapConsumer(css_map_json); - - // TODO make util fn + move to test index.js - const sourcefile = 'input.svelte'; - [ - // TODO: get line and col num from input.svelte rather than hardcoding here - [css, '--keep-me', 13, 2], - [css, '--keep-me', null, 13, 2], - [css, '--done-replace-once', '--replace-me-once', 7, 2], - [css, '--done-replace-twice', '--replace-me-twice', 10, 2] - ].forEach(([where, content, name, line, column]) => { - assert.deepEqual( - where.mapConsumer.originalPositionFor(where.locate_1(content)), - { - source: sourcefile, - name, - line, - column - }, - `failed to locate "${content}" from "${sourcefile}"` - ); - }); -} diff --git a/packages/svelte/tests/sourcemaps/samples/css-injected-map/_config.js b/packages/svelte/tests/sourcemaps/samples/css-injected-map/_config.js new file mode 100644 index 0000000000..2699d350a8 --- /dev/null +++ b/packages/svelte/tests/sourcemaps/samples/css-injected-map/_config.js @@ -0,0 +1,71 @@ +import MagicString from 'magic-string'; +import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping'; +import { test } from '../../test'; +import { magic_string_preprocessor_result, magic_string_replace_all } from '../../helpers.js'; +import { getLocator } from 'locate-character'; + +export default test({ + compileOptions: { + dev: true, + css: 'injected' + }, + preprocess: [ + { + style: ({ content, filename = '' }) => { + const src = new MagicString(content); + magic_string_replace_all(src, '--replace-me-once', '\n --done-replace-once'); + magic_string_replace_all(src, '--replace-me-twice', '\n--almost-done-replace-twice'); + return magic_string_preprocessor_result(filename, src); + } + }, + { + style: ({ content, filename = '' }) => { + const src = new MagicString(content); + magic_string_replace_all(src, '--almost-done-replace-twice', '\n --done-replace-twice'); + return magic_string_preprocessor_result(filename, src); + } + } + ], + async test({ assert, code_client }) { + // Check that the css source map embedded in the js is accurate + const match = code_client.match( + /append_styles\(\$\$anchor, "svelte-.{6}", "(.*?)(?:\\n\/\*# sourceMappingURL=data:(.*?);charset=(.*?);base64,(.*?) \*\/)?"\);/ + ); + + assert.notEqual(match, null); + + const [css, mime_type, encoding, css_map_base64] = /** @type {RegExpMatchArray} */ ( + match + ).slice(1); + assert.equal(mime_type, 'application/json'); + assert.equal(encoding, 'utf-8'); + + const css_map_json = Buffer.from(css_map_base64, 'base64').toString(); + const map = new TraceMap(css_map_json); + const sourcefile = '../../input.svelte'; + const locate = getLocator( + css.replace(/\\r/g, '\r').replace(/\\n/g, '\n').replace(/\\t/g, '\t'), + { offsetLine: 1 } + ); + + /** @type {const} */ ([ + ['--keep-me: blue', null, 13, 2], + ['--done-replace-once: red', '--replace-me-once', 7, 2], + ['--done-replace-twice: green', '--replace-me-twice', 10, 2] + ]).forEach(([content, name, line, column]) => { + assert.deepEqual( + originalPositionFor( + map, + /** @type {import('locate-character').Location_1} */ (locate(content)) + ), + { + source: sourcefile, + name, + line, + column + }, + `failed to locate "${content}" from "${sourcefile}"` + ); + }); + } +}); diff --git a/packages/svelte/tests/sourcemaps/samples/compile-option-dev/input.svelte b/packages/svelte/tests/sourcemaps/samples/css-injected-map/input.svelte similarity index 100% rename from packages/svelte/tests/sourcemaps/samples/compile-option-dev/input.svelte rename to packages/svelte/tests/sourcemaps/samples/css-injected-map/input.svelte diff --git a/packages/svelte/tests/sourcemaps/samples/css/_config.js b/packages/svelte/tests/sourcemaps/samples/css/_config.js index f965e04b02..df3c83c703 100644 --- a/packages/svelte/tests/sourcemaps/samples/css/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/css/_config.js @@ -1,5 +1,5 @@ import { test } from '../../test'; export default test({ - skip: true + css: [{ str: '.foo', strGenerated: '.foo.svelte-sg04hs' }] }); diff --git a/packages/svelte/tests/sourcemaps/samples/css/test.js b/packages/svelte/tests/sourcemaps/samples/css/test.js deleted file mode 100644 index 1e0dda1dff..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/css/test.js +++ /dev/null @@ -1,17 +0,0 @@ -export function test({ assert, input, css }) { - const expected = input.locate('.foo'); - - const start = css.locate('.foo'); - - const actual = css.mapConsumer.originalPositionFor({ - line: start.line + 1, - column: start.column - }); - - assert.deepEqual(actual, { - source: 'input.svelte', - name: null, - line: expected.line + 1, - column: expected.column - }); -} diff --git a/packages/svelte/tests/sourcemaps/samples/decoded-sourcemap/_config.js b/packages/svelte/tests/sourcemaps/samples/decoded-sourcemap/_config.js index ae58033c35..d0b5c45ef0 100644 --- a/packages/svelte/tests/sourcemaps/samples/decoded-sourcemap/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/decoded-sourcemap/_config.js @@ -3,14 +3,13 @@ import { test } from '../../test'; import { magic_string_preprocessor_result, magic_string_replace_all } from '../../helpers.js'; export default test({ - skip: true, - js_map_sources: ['input.svelte'], - preprocess: { markup: ({ content, filename = '' }) => { const src = new MagicString(content); magic_string_replace_all(src, 'replace me', 'success'); return magic_string_preprocessor_result(filename, src); } - } + }, + client: [], + preprocessed: [{ str: 'replace me', strGenerated: 'success' }] }); diff --git a/packages/svelte/tests/sourcemaps/samples/decoded-sourcemap/test.js b/packages/svelte/tests/sourcemaps/samples/decoded-sourcemap/test.js deleted file mode 100644 index fe54a570b8..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/decoded-sourcemap/test.js +++ /dev/null @@ -1,17 +0,0 @@ -export function test({ assert, input, preprocessed }) { - const expected = input.locate('replace me'); - - const start = preprocessed.locate('success'); - - const actualbar = preprocessed.mapConsumer.originalPositionFor({ - line: start.line + 1, - column: start.column - }); - - assert.deepEqual(actualbar, { - source: 'input.svelte', - name: 'replace me', - line: expected.line + 1, - column: expected.column - }); -} diff --git a/packages/svelte/tests/sourcemaps/samples/each-block/_config.js b/packages/svelte/tests/sourcemaps/samples/each-block/_config.js index f965e04b02..600ae7cbef 100644 --- a/packages/svelte/tests/sourcemaps/samples/each-block/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/each-block/_config.js @@ -1,5 +1,5 @@ import { test } from '../../test'; export default test({ - skip: true + client: ['foo', 'bar', { str: 'bar', idxGenerated: 1, idxOriginal: 1 }] }); diff --git a/packages/svelte/tests/sourcemaps/samples/each-block/test.js b/packages/svelte/tests/sourcemaps/samples/each-block/test.js deleted file mode 100644 index 7a811a0748..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/each-block/test.js +++ /dev/null @@ -1,18 +0,0 @@ -export function test({ assert, input, js }) { - const start_index = js.code.indexOf('create_main_fragment'); - - const expected = input.locate('each'); - const start = js.locate('length', start_index); - - const actual = js.mapConsumer.originalPositionFor({ - line: start.line + 1, - column: start.column - }); - - assert.deepEqual(actual, { - source: 'input.svelte', - name: null, - line: expected.line + 1, - column: expected.column - }); -} diff --git a/packages/svelte/tests/sourcemaps/samples/external/_config.js b/packages/svelte/tests/sourcemaps/samples/external/_config.js index e89ebc27a4..d5ce837b03 100644 --- a/packages/svelte/tests/sourcemaps/samples/external/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/external/_config.js @@ -1,18 +1,16 @@ import { test } from '../../test'; import { magic_string_bundle } from '../../helpers.js'; -export const COMMON = ':global(html) { height: 100%; }\n'; +const COMMON = ':global(html) { height: 100%; }\n'; // TODO: removing '\n' breaks test // - _actual.svelte.map looks correct // - _actual.css.map adds reference to on input.svelte // - Most probably caused by bug in current magic-string version (fixed in 0.25.7) -export const STYLES = '.awesome { color: orange; }\n'; +const STYLES = '.awesome { color: orange; }\n'; export default test({ - skip: true, css_map_sources: ['common.scss', 'styles.scss'], - js_map_sources: ['input.svelte'], preprocess: [ { style: () => { @@ -22,5 +20,11 @@ export default test({ ]); } } + ], + client: [], + preprocessed: [ + 'Divs ftw!', + { code: COMMON, str: 'height: 100%;' }, + { code: STYLES, str: 'color: orange;' } ] }); diff --git a/packages/svelte/tests/sourcemaps/samples/external/test.js b/packages/svelte/tests/sourcemaps/samples/external/test.js deleted file mode 100644 index b4b14b7cf5..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/external/test.js +++ /dev/null @@ -1,26 +0,0 @@ -import { assert_mapped } from '../../helpers.js'; -import { COMMON, STYLES } from './_config'; - -export function test({ input, preprocessed }) { - // Transformed script, main file - assert_mapped({ - filename: 'input.svelte', - code: 'Divs ftw!', - input: input.locate, - preprocessed - }); - - // External files - assert_mapped({ - filename: 'common.scss', - code: 'height: 100%;', - input: COMMON, - preprocessed - }); - assert_mapped({ - filename: 'styles.scss', - code: 'color: orange;', - input: STYLES, - preprocessed - }); -} diff --git a/packages/svelte/tests/sourcemaps/samples/markup/_config.js b/packages/svelte/tests/sourcemaps/samples/markup/_config.js index f965e04b02..7f1ff86258 100644 --- a/packages/svelte/tests/sourcemaps/samples/markup/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/markup/_config.js @@ -1,5 +1,6 @@ import { test } from '../../test'; export default test({ - skip: true + skip: true, // TODO no source maps here; Svelte 4 added some for static templates due to https://github.com/sveltejs/svelte/issues/6092 + client: [] }); diff --git a/packages/svelte/tests/sourcemaps/samples/no-sourcemap/_config.js b/packages/svelte/tests/sourcemaps/samples/no-sourcemap/_config.js deleted file mode 100644 index 29ce2336dc..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/no-sourcemap/_config.js +++ /dev/null @@ -1,9 +0,0 @@ -import { test } from '../../test'; - -export default test({ - skip: true, - compileOptions: { - // @ts-expect-error - enableSourcemap: false - } -}); diff --git a/packages/svelte/tests/sourcemaps/samples/no-sourcemap/input.svelte b/packages/svelte/tests/sourcemaps/samples/no-sourcemap/input.svelte deleted file mode 100644 index 6d39eaad0e..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/no-sourcemap/input.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - -

{foo}

- - diff --git a/packages/svelte/tests/sourcemaps/samples/no-sourcemap/test.js b/packages/svelte/tests/sourcemaps/samples/no-sourcemap/test.js deleted file mode 100644 index 127459a54e..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/no-sourcemap/test.js +++ /dev/null @@ -1,4 +0,0 @@ -export function test({ assert, js, css }) { - assert.equal(js.map, null); - assert.equal(css.map, null); -} diff --git a/packages/svelte/tests/sourcemaps/samples/only-css-sourcemap/_config.js b/packages/svelte/tests/sourcemaps/samples/only-css-sourcemap/_config.js deleted file mode 100644 index b2351ed4be..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/only-css-sourcemap/_config.js +++ /dev/null @@ -1,9 +0,0 @@ -import { test } from '../../test'; - -export default test({ - skip: true, - compileOptions: { - // @ts-expect-error - enableSourcemap: { css: true } - } -}); diff --git a/packages/svelte/tests/sourcemaps/samples/only-css-sourcemap/input.svelte b/packages/svelte/tests/sourcemaps/samples/only-css-sourcemap/input.svelte deleted file mode 100644 index 6d39eaad0e..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/only-css-sourcemap/input.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - -

{foo}

- - diff --git a/packages/svelte/tests/sourcemaps/samples/only-css-sourcemap/test.js b/packages/svelte/tests/sourcemaps/samples/only-css-sourcemap/test.js deleted file mode 100644 index a7ac6a9b0b..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/only-css-sourcemap/test.js +++ /dev/null @@ -1,4 +0,0 @@ -export function test({ assert, js, css }) { - assert.equal(js.map, null); - assert.notEqual(css.map, null); -} diff --git a/packages/svelte/tests/sourcemaps/samples/only-js-sourcemap/_config.js b/packages/svelte/tests/sourcemaps/samples/only-js-sourcemap/_config.js deleted file mode 100644 index f82c278566..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/only-js-sourcemap/_config.js +++ /dev/null @@ -1,9 +0,0 @@ -import { test } from '../../test'; - -export default test({ - skip: true, - compileOptions: { - // @ts-expect-error - enableSourcemap: { js: true } - } -}); diff --git a/packages/svelte/tests/sourcemaps/samples/only-js-sourcemap/input.svelte b/packages/svelte/tests/sourcemaps/samples/only-js-sourcemap/input.svelte deleted file mode 100644 index 6d39eaad0e..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/only-js-sourcemap/input.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - -

{foo}

- - diff --git a/packages/svelte/tests/sourcemaps/samples/only-js-sourcemap/test.js b/packages/svelte/tests/sourcemaps/samples/only-js-sourcemap/test.js deleted file mode 100644 index b150653c3d..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/only-js-sourcemap/test.js +++ /dev/null @@ -1,4 +0,0 @@ -export function test({ assert, js, css }) { - assert.notEqual(js.map, null); - assert.equal(css.map, null); -} diff --git a/packages/svelte/tests/sourcemaps/samples/preprocessed-markup/_config.js b/packages/svelte/tests/sourcemaps/samples/preprocessed-markup/_config.js index 0bda3ef69c..69f4161892 100644 --- a/packages/svelte/tests/sourcemaps/samples/preprocessed-markup/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/preprocessed-markup/_config.js @@ -3,12 +3,12 @@ import { test } from '../../test'; import { magic_string_preprocessor_result, magic_string_replace_all } from '../../helpers.js'; export default test({ - skip: true, preprocess: { markup: ({ content, filename = '' }) => { const src = new MagicString(content); magic_string_replace_all(src, 'baritone', 'bar'); return magic_string_preprocessor_result(filename, src); } - } + }, + client: [{ str: 'baritone', strGenerated: 'bar' }, 'baz'] }); diff --git a/packages/svelte/tests/sourcemaps/samples/preprocessed-markup/test.js b/packages/svelte/tests/sourcemaps/samples/preprocessed-markup/test.js deleted file mode 100644 index 2db7ab16a2..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/preprocessed-markup/test.js +++ /dev/null @@ -1,32 +0,0 @@ -export function test({ assert, input, js }) { - const expected_bar = input.locate('baritone.baz'); - const expected_baz = input.locate('.baz'); - - let start = js.locate('bar.baz'); - - const actualbar = js.mapConsumer.originalPositionFor({ - line: start.line + 1, - column: start.column - }); - - assert.deepEqual(actualbar, { - source: 'input.svelte', - name: 'baritone', - line: expected_bar.line + 1, - column: expected_bar.column - }); - - start = js.locate('.baz'); - - const actualbaz = js.mapConsumer.originalPositionFor({ - line: start.line + 1, - column: start.column - }); - - assert.deepEqual(actualbaz, { - source: 'input.svelte', - name: null, - line: expected_baz.line + 1, - column: expected_baz.column - }); -} diff --git a/packages/svelte/tests/sourcemaps/samples/preprocessed-multiple/_config.js b/packages/svelte/tests/sourcemaps/samples/preprocessed-multiple/_config.js index 33a294a258..f61c27ceda 100644 --- a/packages/svelte/tests/sourcemaps/samples/preprocessed-multiple/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/preprocessed-multiple/_config.js @@ -3,7 +3,6 @@ import { test } from '../../test'; import { magic_string_preprocessor_result, magic_string_replace_all } from '../../helpers.js'; export default test({ - skip: true, preprocess: { markup: ({ content, filename = '' }) => { const src = new MagicString(content); @@ -23,5 +22,9 @@ export default test({ src.prependLeft(idx, ' '); return magic_string_preprocessor_result(filename, src); } - } + }, + client: [{ str: 'baritone', strGenerated: 'bar' }], + css: [ + { str: 'background-color: var(--bazitone)', strGenerated: 'background-color: var( --baz)' } + ] }); diff --git a/packages/svelte/tests/sourcemaps/samples/preprocessed-multiple/test.js b/packages/svelte/tests/sourcemaps/samples/preprocessed-multiple/test.js deleted file mode 100644 index 996fabd721..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/preprocessed-multiple/test.js +++ /dev/null @@ -1,32 +0,0 @@ -export function test({ assert, input, js, css }) { - const expected_bar = input.locate('baritone'); - const expected_baz = input.locate('--bazitone'); - - let start = js.locate('bar'); - - const actualbar = js.mapConsumer.originalPositionFor({ - line: start.line + 1, - column: start.column - }); - - assert.deepEqual(actualbar, { - source: 'input.svelte', - name: 'baritone', - line: expected_bar.line + 1, - column: expected_bar.column - }); - - start = css.locate('--baz'); - - const actualbaz = css.mapConsumer.originalPositionFor({ - line: start.line + 1, - column: start.column - }); - - assert.deepEqual(actualbaz, { - source: 'input.svelte', - name: '--bazitone', - line: expected_baz.line + 1, - column: expected_baz.column - }); -} diff --git a/packages/svelte/tests/sourcemaps/samples/preprocessed-no-map/_config.js b/packages/svelte/tests/sourcemaps/samples/preprocessed-no-map/_config.js index d15472d67c..41b0deb72b 100644 --- a/packages/svelte/tests/sourcemaps/samples/preprocessed-no-map/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/preprocessed-no-map/_config.js @@ -1,8 +1,6 @@ import { test } from '../../test'; export default test({ - skip: true, - css_map_sources: ['input.svelte'], preprocess: [ { style: ({ content }) => { @@ -14,5 +12,18 @@ export default test({ return { code: content }; } } + ], + client: [], + preprocessed: [ + // markup (start) + ' -

sourcemap-sources

+

sourcemap-sources {name}

diff --git a/packages/svelte/tests/sourcemaps/samples/sourcemap-sources/test.js b/packages/svelte/tests/sourcemaps/samples/sourcemap-sources/test.js deleted file mode 100644 index d79ca9a56e..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/sourcemap-sources/test.js +++ /dev/null @@ -1,23 +0,0 @@ -export function test({ assert, preprocessed, js }) { - assert.equal(preprocessed.error, undefined); - - // sourcemap stores location only for 'answer = 42;' - // not for 'var answer = 42;' - [ - [js, 'foo.js', 'answer = 42;', 4], - [js, 'bar.js', 'console.log(answer);', 0], - [js, 'foo2.js', 'answer2 = 84;', 4], - [js, 'bar2.js', 'console.log(answer2);', 0] - ].forEach(([where, sourcefile, content, column]) => { - assert.deepEqual( - where.mapConsumer.originalPositionFor(where.locate_1(content)), - { - source: sourcefile, - name: null, - line: 1, - column - }, - `failed to locate "${content}" from "${sourcefile}"` - ); - }); -} diff --git a/packages/svelte/tests/sourcemaps/samples/static-no-script/_config.js b/packages/svelte/tests/sourcemaps/samples/static-no-script/_config.js index f965e04b02..5cd3b335c9 100644 --- a/packages/svelte/tests/sourcemaps/samples/static-no-script/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/static-no-script/_config.js @@ -1,5 +1,11 @@ import { test } from '../../test'; export default test({ - skip: true + test({ assert, map_client }) { + assert.deepEqual(map_client.sources, ['../../input.svelte']); + // TODO do we need to set sourcesContent? We did it in Svelte 4, but why? + // assert.deepEqual(js.map.sourcesContent, [ + // fs.readFileSync(path.join(__dirname, 'input.svelte'), 'utf-8') + // ]); + } }); diff --git a/packages/svelte/tests/sourcemaps/samples/static-no-script/test.js b/packages/svelte/tests/sourcemaps/samples/static-no-script/test.js deleted file mode 100644 index f153c38ece..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/static-no-script/test.js +++ /dev/null @@ -1,9 +0,0 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; - -export function test({ assert, js }) { - assert.deepEqual(js.map.sources, ['input.svelte']); - assert.deepEqual(js.map.sourcesContent, [ - fs.readFileSync(path.join(__dirname, 'input.svelte'), 'utf-8') - ]); -} diff --git a/packages/svelte/tests/sourcemaps/samples/two-scripts/_config.js b/packages/svelte/tests/sourcemaps/samples/two-scripts/_config.js new file mode 100644 index 0000000000..184da2d8c0 --- /dev/null +++ b/packages/svelte/tests/sourcemaps/samples/two-scripts/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + client: ['first', 'assertThisLine'] +}); diff --git a/packages/svelte/tests/sourcemaps/samples/two-scripts/test.js b/packages/svelte/tests/sourcemaps/samples/two-scripts/test.js deleted file mode 100644 index 06ecc46929..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/two-scripts/test.js +++ /dev/null @@ -1,16 +0,0 @@ -export function test({ assert, input, js }) { - const expected = input.locate('assertThisLine'); - const start = js.locate('assertThisLine'); - - const actual = js.mapConsumer.originalPositionFor({ - line: start.line + 1, - column: start.column - }); - - assert.deepEqual(actual, { - source: 'input.svelte', - name: null, - line: expected.line + 1, - column: expected.column - }); -} diff --git a/packages/svelte/tests/sourcemaps/samples/typescript/_config.js b/packages/svelte/tests/sourcemaps/samples/typescript/_config.js index 17c5e27827..f5e46a0648 100644 --- a/packages/svelte/tests/sourcemaps/samples/typescript/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/typescript/_config.js @@ -2,8 +2,6 @@ import * as ts from 'typescript'; import { test } from '../../test'; export default test({ - skip: true, - js_map_sources: ['input.svelte'], preprocess: [ { script: ({ content, filename }) => { @@ -22,5 +20,11 @@ export default test({ }; } } + ], + client: ['count', 'setInterval'], + preprocessed: [ + { str: 'let count: number = 0;', strGenerated: 'let count = 0;' }, + { str: 'ITimeoutDestroyer', strGenerated: null }, + '

Hello world!

' ] }); diff --git a/packages/svelte/tests/sourcemaps/samples/typescript/test.js b/packages/svelte/tests/sourcemaps/samples/typescript/test.js deleted file mode 100644 index a1ff5c350d..0000000000 --- a/packages/svelte/tests/sourcemaps/samples/typescript/test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { assert_mapped, assert_not_located } from '../../helpers.js'; - -export function test({ input, preprocessed }) { - // TS => JS code - assert_mapped({ - code: 'let count = 0;', - input_code: 'let count: number = 0;', - input: input.locate, - preprocessed - }); - - // Markup, not touched - assert_mapped({ - code: '

Hello world!

', - input: input.locate, - preprocessed - }); - - // TS types, removed - assert_not_located('ITimeoutDestroyer', preprocessed.locate_1); -} diff --git a/packages/svelte/tests/sourcemaps/test.ts b/packages/svelte/tests/sourcemaps/test.ts index 252122be83..c654258b34 100644 --- a/packages/svelte/tests/sourcemaps/test.ts +++ b/packages/svelte/tests/sourcemaps/test.ts @@ -1,12 +1,24 @@ -// @ts-nocheck TODO - import * as fs from 'node:fs'; -import * as path from 'node:path'; -import * as svelte from 'svelte/compiler'; import { assert } from 'vitest'; -import { getLocator } from 'locate-character'; -import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping'; +import { getLocator, locate } from 'locate-character'; import { suite, type BaseTest } from '../suite.js'; +import { compile_directory } from '../helpers.js'; +import { decode } from '@jridgewell/sourcemap-codec'; + +type SourceMapEntry = + | string + | { + /** If not the first occurence, but the nth should be found */ + idxOriginal?: number; + /** If not the first occurence, but the nth should be found */ + idxGenerated?: number; + /** The original string to find */ + str: string; + /** The generated string to find. You can omit this if it's the same as the original string */ + strGenerated?: string | null; + /** If the original code lives in a different file, pass its source code here */ + code?: string; + }; interface SourcemapTest extends BaseTest { options?: { filename: string }; @@ -14,109 +26,259 @@ interface SourcemapTest extends BaseTest { preprocess?: | import('../../src/compiler/public').PreprocessorGroup | import('../../src/compiler/public').PreprocessorGroup[]; + /** The expected `sources` array in the source map */ js_map_sources?: string[]; + /** The expected `sources` array in the source map */ css_map_sources?: string[]; + test?: (obj: { + assert: typeof assert; + input: string; + map_preprocessed: any; + code_preprocessed: string; + map_css: any; + code_css: string; + map_client: any; + code_client: string; + }) => void; + /** Mappings to check in generated client code */ + client?: SourceMapEntry[] | null; + /** Mappings to check in generated server code. If left out, will use the client code checks */ + server?: SourceMapEntry[]; + /** Mappings to check in generated css code */ + css?: SourceMapEntry[] | null; + /** Mappings to check in preprocessed Svelte code */ + preprocessed?: SourceMapEntry[]; } const { test, run } = suite(async (config, cwd) => { - const { test } = await import(`${cwd}/test.js`); - - const input_file = path.resolve(`${cwd}/input.svelte`); - const output_name = '_actual'; - const output_base = path.resolve(`${cwd}/${output_name}`); - - const input_code = fs.readFileSync(input_file, 'utf-8'); - const input = { - code: input_code, - locate: getLocator(input_code), - locate_1: getLocator(input_code, { offsetLine: 1 }) - }; - const preprocessed = await svelte.preprocess( - input.code, - config.preprocess || {}, - config.options || { - filename: 'input.svelte' - } - ); - let { js, css } = svelte.compile(preprocessed.code, { - filename: 'input.svelte', - // filenames for sourcemaps - sourcemap: preprocessed.map, - outputFilename: `${output_name}.js`, - cssOutputFilename: `${output_name}.css`, - ...(config.compile_options || {}) + await compile_directory(cwd, 'client', config.compileOptions, true, { + preprocess: config.preprocess, + options: config.options + }); + await compile_directory(cwd, 'server', config.compileOptions, true, { + preprocess: config.preprocess, + options: config.options }); - if (css === null) { - css = { code: '', map: /** @type {any} */ null }; - } - js.code = js.code.replace(/\(Svelte v\d+\.\d+\.\d+(-next\.\d+)?/, (match) => - match.replace(/\d/g, 'x') - ); + const input = fs.readFileSync(`${cwd}/input.svelte`, 'utf-8'); - fs.writeFileSync(`${output_base}.svelte`, preprocessed.code); - if (preprocessed.map) { - fs.writeFileSync( - `${output_base}.svelte.map`, - // TODO encode mappings for output - svelte.preprocess returns decoded mappings - JSON.stringify(preprocessed.map, null, 2) - ); - } - fs.writeFileSync(`${output_base}.js`, `${js.code}\n//# sourceMappingURL=${output_name}.js.map`); - fs.writeFileSync(`${output_base}.js.map`, JSON.stringify(js.map, null, 2)); - if (css.code) { - fs.writeFileSync( - `${output_base}.css`, - `${css.code}\n/*# sourceMappingURL=${output_name}.css.map */` - ); - fs.writeFileSync(`${output_base}.css.map`, JSON.stringify(css.map, null, ' ')); + function compare(info: string, output: string, map: any, entries: SourceMapEntry[]) { + const output_locator = getLocator(output); + + /** Find line/column of string in original code */ + function find_original(entry: SourceMapEntry, idx = 0) { + let str; + let source; + if (typeof entry === 'string') { + str = entry; + source = input; + } else if (entry.code) { + str = entry.str; + source = entry.code; + } else { + str = entry.str; + source = input; + } + + const original = locate(source, source.indexOf(str, idx)); + if (!original) + throw new Error(`Could not find '${str}'${idx > 0 ? ` after index ${idx}` : ''} in input`); + return original; + } + + /** Find line/column of string in generated code */ + function find_generated(str: string, idx = 0) { + const generated = output_locator(output.indexOf(str, idx)); + if (!generated) + throw new Error(`Could not find '${str}'${idx > 0 ? ` after index ${idx}` : ''} in output`); + return generated; + } + + const decoded = decode(map.mappings); + + try { + for (let entry of entries) { + entry = typeof entry === 'string' ? { str: entry } : entry; + + const str = entry.str; + + // Find generated line/column + const generated_str = entry.strGenerated ?? str; + if (entry.strGenerated === null) { + if (!output.includes(generated_str)) continue; + } + let generated = find_generated(generated_str); + if (entry.idxGenerated) { + let i = entry.idxGenerated; + while (i-- > 0) { + generated = find_generated(generated_str, generated.character + 1); + } + } + + // Find segment in source map pointing from generated to original + const segments = decoded[generated.line]; + const segment = segments.find((segment) => segment[0] === generated.column); + if (!segment && entry.strGenerated !== null) { + throw new Error( + `Could not find segment for '${str}' in sourcemap (${generated.line}:${generated.column})` + ); + } else if (segment && entry.strGenerated === null) { + throw new Error( + `Found segment for '${str}' in sourcemap (${generated.line}:${generated.column}) but should not` + ); + } else if (!segment) { + continue; + } + + // Find original line/column + let original = find_original(entry); + if (entry.idxOriginal) { + let i = entry.idxOriginal; + while (i-- > 0) { + original = find_original(entry, original.character + 1); + } + } + + // Check that segment points to expected original + assert.equal(segment[2], original.line, `mapped line did not match for '${str}'`); + assert.equal(segment[3], original.column, `mapped column did not match for '${str}'`); + + // Same for end of string + const generated_end = generated.column + generated_str.length; + const end_segment = segments.find((segment) => segment[0] === generated_end); + if (!end_segment) { + // If the string is the last segment and it's the end of the line, + // it's okay if there's no end segment (source maps save space by omitting it in that case) + if ( + segments.at(-1)![0] > generated_end || + !/[\r\n]/.test(output[generated.character + generated_str.length]) + ) { + console.log(segments.at(-1)![0] < generated_end, segments.at(-1)![0], generated_end); + console.log( + /[\r\n]/.test(output[generated.character + generated_str.length]), + output[generated.character + generated_str.length] + + '::' + + output.slice( + generated.character + generated_str.length - 10, + generated.character + generated_str.length + 10 + ) + ); + throw new Error( + `Could not find end segment for '${str}' in sourcemap (${generated.line}:${generated_end})` + ); + } else { + continue; + } + } + + assert.equal(end_segment[2], original.line, `mapped line end did not match for '${str}'`); + assert.equal( + end_segment[3], + original.column + str.length, + `mapped column end did not match for '${str}'` + ); + } + } catch (e) { + console.log(`Source map ${info}:\n`); + console.log(decoded); + throw e; + } } - if (js.map) { - assert.deepEqual( - js.map.sources.slice().sort(), - (config.js_map_sources || ['input.svelte']).sort(), - 'js.map.sources is wrong' + let map_client = null; + let code_client = fs.readFileSync(`${cwd}/_output/client/input.svelte.js`, 'utf-8'); + + if (config.client === null) { + assert.equal( + fs.existsSync(`${cwd}/_output/client/input.svelte.js.map`), + false, + 'Expected no source map' ); - } - if (css.map) { + } else { + map_client = JSON.parse(fs.readFileSync(`${cwd}/_output/client/input.svelte.js.map`, 'utf-8')); assert.deepEqual( - css.map.sources.slice().sort(), - (config.css_map_sources || ['input.svelte']).sort(), - 'css.map.sources is wrong' + map_client.sources.slice().sort(), + (config.js_map_sources || ['../../input.svelte']).sort(), + 'js.map.sources is wrong' ); + + if (config.client) { + compare('client', code_client, map_client, config.client); + } } - // use locate_1 with mapConsumer: - // lines are one-based, columns are zero-based + if (config.client || config.server) { + const output_server = fs.readFileSync(`${cwd}/_output/server/input.svelte.js`, 'utf-8'); + const map_server = JSON.parse( + fs.readFileSync(`${cwd}/_output/server/input.svelte.js.map`, 'utf-8') + ); - preprocessed.mapConsumer = preprocessed.map && new TraceMap(preprocessed.map); - preprocessed.locate = getLocator(preprocessed.code); - preprocessed.locate_1 = getLocator(preprocessed.code, { offsetLine: 1 }); + compare( + 'server', + output_server, + map_server, + config.server ?? + // Reuse client sourcemap test for server + config.client ?? + [] + ); + } - if (js.map) { - const map = new TraceMap(js.map); - js.mapConsumer = { - originalPositionFor(loc) { - return originalPositionFor(map, loc); - } - }; + let map_css = null; + let code_css = ''; + if (config.css !== undefined) { + if (config.css === null) { + assert.equal( + fs.existsSync(`${cwd}/_output/client/input.svelte.css.map`), + false, + 'Expected no source map' + ); + } else { + code_css = fs.readFileSync(`${cwd}/_output/client/input.svelte.css`, 'utf-8'); + map_css = JSON.parse(fs.readFileSync(`${cwd}/_output/client/input.svelte.css.map`, 'utf-8')); + assert.deepEqual( + map_css.sources.slice().sort(), + (config.css_map_sources || ['../../input.svelte']).sort(), + 'css.map.sources is wrong' + ); + compare('css', code_css, map_css, config.css); + } } - js.locate = getLocator(js.code); - js.locate_1 = getLocator(js.code, { offsetLine: 1 }); - - if (css.map) { - const map = new TraceMap(css.map); - css.mapConsumer = { - originalPositionFor(loc) { - return originalPositionFor(map, loc); - } - }; + + let map_preprocessed = null; + let code_preprocessed = ''; + if (config.preprocessed !== undefined) { + if (config.preprocessed === null) { + assert.equal( + fs.existsSync(`${cwd}/_output/client/input.preprocessed.svelte.map`), + false, + 'Expected no source map' + ); + } else { + code_preprocessed = fs.readFileSync( + `${cwd}/_output/client/input.preprocessed.svelte`, + 'utf-8' + ); + map_preprocessed = JSON.parse( + fs.readFileSync(`${cwd}/_output/client/input.preprocessed.svelte.map`, 'utf-8') + ); + compare('preprocessed', code_preprocessed, map_preprocessed, config.preprocessed); + } } - css.locate = getLocator(css.code || ''); - css.locate_1 = getLocator(css.code || '', { offsetLine: 1 }); - await test({ assert, input, preprocessed, js, css }); + if (config.test) { + // TODO figure out for which tests we still need this + config.test({ + assert, + input, + map_client, + code_client, + map_preprocessed, + code_preprocessed, + code_css, + map_css + }); + } }); export { test }; diff --git a/packages/svelte/tests/suite.ts b/packages/svelte/tests/suite.ts index b694e4cd95..9cacea8d63 100644 --- a/packages/svelte/tests/suite.ts +++ b/packages/svelte/tests/suite.ts @@ -36,7 +36,7 @@ export function suite(fn: (config: Test, test_dir: string export function suite_with_variants( variants: Variants[], should_skip_variant: (variant: Variants, config: Test) => boolean | 'no-test', - common_setup: (config: Test, test_dir: string) => Common, + common_setup: (config: Test, test_dir: string) => Promise | Common, fn: (config: Test, test_dir: string, variant: Variants, common: Common) => void ) { return { @@ -54,10 +54,10 @@ export function suite_with_variants { + it_fn(`${dir} (${variant})`, async () => { if (!called_common) { called_common = true; - common = common_setup(config, `${cwd}/${samples_dir}/${dir}`); + common = await common_setup(config, `${cwd}/${samples_dir}/${dir}`); } return fn(config, `${cwd}/${samples_dir}/${dir}`, variant, common); }); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 314ec9dd7f..04692e2bb9 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -672,10 +672,6 @@ declare module 'svelte/compiler' { * @default null */ cssOutputFilename?: string; - - // Other Svelte 4 compiler options: - // enableSourcemap?: EnableSourcemap; // TODO bring back? https://github.com/sveltejs/svelte/pull/6835 - // legacy?: boolean; // TODO compiler error noting the new purpose? } interface ModuleCompileOptions { @@ -754,8 +750,11 @@ declare module 'svelte/compiler' { legacy_dependencies: Binding[]; /** Legacy props: the `class` in `{ export klass as class}` */ prop_alias: string | null; - /** If this is set, all references should use this expression instead of the identifier name */ - expression: Expression | null; + /** + * If this is set, all references should use this expression instead of the identifier name. + * If a function is given, it will be called with the identifier at that location and should return the new expression. + */ + expression: Expression | ((id: Identifier) => Expression) | null; /** If this is set, all mutations should use this expression */ mutation: ((assignment: AssignmentExpression, context: Context) => Expression) | null; } @@ -1407,7 +1406,7 @@ declare module 'svelte/compiler' { /** Set if something in the array expression is shadowed within the each block */ array_name: Identifier | null; index: Identifier; - item_name: string; + item: Identifier; declarations: Map; /** List of bindings that are referenced within the expression */ references: Binding[]; @@ -2403,10 +2402,6 @@ declare module 'svelte/types/compiler/interfaces' { * @default null */ cssOutputFilename?: string; - - // Other Svelte 4 compiler options: - // enableSourcemap?: EnableSourcemap; // TODO bring back? https://github.com/sveltejs/svelte/pull/6835 - // legacy?: boolean; // TODO compiler error noting the new purpose? } interface ModuleCompileOptions { diff --git a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md index b783041266..340200e76a 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md @@ -61,6 +61,7 @@ Svelte now use Mutation Observers instead of IFrames to measure dimensions for ` - The `false`/`true` (already deprecated previously) and the `"none"` values were removed as valid values from the `css` option - The `legacy` option was repurposed - The `hydratable` option has been removed. Svelte components are always hydratable now +- The `enableSourcemap` option has been removed. Source maps are always generated now, tooling can choose to ignore it - The `tag` option was removed. Use `` inside the component instead - The `loopGuardTimeout`, `format`, `sveltePath`, `errorMode` and `varsReport` options were removed