diff --git a/.changeset/silly-laws-happen.md b/.changeset/silly-laws-happen.md new file mode 100644 index 000000000000..76428ef3bf86 --- /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 e632a3692e7c..1337cf19e032 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 4e94d944c130..5c77d36ea65f 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 e284af338089..3759dd2049ca 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 95f136effa72..000000000000 --- 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 7ecde4b001b9..3bbc6819eb5c 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 00a9d1fab83d..187329fe2e97 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 903106e7e14f..49e5bb1d2d52 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 5bef734b6329..c297a04a68be 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 af8de96d70bb..37e9e5726382 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 2cda4ae8f656..7db7cae37c80 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 5d46c3187158..c1ce9f837498 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 ad1061c78847..9b101249a509 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 8559254b72a5..fa3f75bd9123 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 26b6ee7ca2d9..0cbe05c57b4c 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 a438e48fd710..5fe215e435a2 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 5752435178e4..1151ad0dc11e 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 1d8dc5c5636f..3eb54e160da8 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 a8e3d605d961..d08b81b5701c 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 d10612ef9beb..3f7d2e58155e 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 6a43c13c2f42..0d82cb2bb917 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 06a73d522cbe..5572d573f89e 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 a2ccb995e018..b71365f39deb 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 5af3bff86df3..a6db309edb08 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 915dd6228b64..bce7fd81a211 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 9582d4546564..2f6206a6cbeb 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 1b418dac5a67..f26f557958db 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 79a26426ded9..c60efd4fbaf0 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 96e6c0a13540..9081b7cb926d 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 732ba5888aa9..c8f6b2bcda78 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 c94db0a55e7f..27cf2334c391 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 8c04e91f6cf7..93b3552493f6 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 72843c98bde1..fdd1f9993cd8 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 26f3a1a1b7ee..88cf9193c3f7 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 f4944296c48e..4a2a3ccb9f98 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 7cd63837db53..187eb4ade52d 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 1dfb3caeea04..000000000000 --- 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 f965e04b0263..591a61439702 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 34c619c128ca..000000000000 --- 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 64fdc120d62a..c4eab5b03963 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 7ba7c7c10d90..53829fb4f0e4 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 13ecdbf88934..000000000000 --- 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 000000000000..8c14a698f7ab --- /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 3cb3246e5089..000000000000 --- 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 cd0505c2d067..000000000000 --- 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 16477061f86e..000000000000 --- 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 000000000000..2699d350a8b8 --- /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 f965e04b0263..df3c83c7032d 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 1e0dda1dff43..000000000000 --- 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 ae58033c3586..d0b5c45ef01f 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 fe54a570b860..000000000000 --- 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 f965e04b0263..600ae7cbef68 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 7a811a0748bb..000000000000 --- 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 e89ebc27a448..d5ce837b0342 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 b4b14b7cf539..000000000000 --- 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 f965e04b0263..7f1ff8625891 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 29ce2336dc69..000000000000 --- 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 6d39eaad0e79..000000000000 --- 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 127459a54e6e..000000000000 --- 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 b2351ed4be5b..000000000000 --- 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 6d39eaad0e79..000000000000 --- 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 a7ac6a9b0bdd..000000000000 --- 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 f82c278566d2..000000000000 --- 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 6d39eaad0e79..000000000000 --- 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 b150653c3d72..000000000000 --- 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 0bda3ef69cf4..69f416189200 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 2db7ab16a2b2..000000000000 --- 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 33a294a258f0..f61c27ceda11 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 996fabd7217d..000000000000 --- 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 d15472d67ca2..41b0deb72b08 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 d79ca9a56e48..000000000000 --- 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 f965e04b0263..5cd3b335c993 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 f153c38ecea3..000000000000 --- 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 000000000000..184da2d8c0aa --- /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 06ecc46929df..000000000000 --- 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 17c5e2782705..f5e46a0648c5 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 a1ff5c350dce..000000000000 --- 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 252122be831d..c654258b34fd 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 b694e4cd9578..9cacea8d6362 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 314ec9dd7fed..04692e2bb954 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 b783041266ed..340200e76a98 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