chore: source maps for preprocessors + tests (#10459)

Add source map merging for preprocessors and get tests passing.

- fixed some issues around the `sources` array where they weren't calculated relative to the input correctly
- adjusted some CSS tests because due to our own CSS parser the AST is less granular, so there are less mappings now. Don't think this is a problem, but worth thinking about
- removed enableSourcemap but only log a warning, the reason this was introduced was to mitigate a bug in Vite which occured when having the source map inlined into the SSR'd output. Since SSR doesn't contain inlined CSS anymore (and if it did, we would omit the source map) the reason for which it was introduced no longer exists
- files without js mapping in it have no source mappings yet (originally added in Svelte 4 for #6092)
pull/10479/head
Simon H 2 years ago committed by GitHub
parent 4b274dd00d
commit f8ff2b6ea3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: add proper source map support

@ -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 */

@ -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;
}

@ -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
};

@ -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<import('@ampproject/remapping').DecodedSourceMap | import('@ampproject/remapping').RawSourceMap>} 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
*/

@ -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 ?? '<unknown>',
filename: options.filename || 'input.svelte',
component_name,
get_css_hash: options.cssHash
}),

@ -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)
)
)
)

@ -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;
}

@ -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) {

@ -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
}

@ -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

@ -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)))

@ -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);
}
/**

@ -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<any, any>) => Expression) | null;
}

@ -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<string, Binding>;
/** List of bindings that are referenced within the expression */
references: Binding[];

@ -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<import('@ampproject/remapping').DecodedSourceMap | import('@ampproject/remapping').RawSourceMap>} 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);
}

@ -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.'
),

@ -25,8 +25,8 @@ const { test, run } = suite<CssTest>(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();

@ -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<import('#compiler').CompileOptions>} 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'));
}
}
}
}

@ -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<HydrationTest>(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;

@ -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": {

@ -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": {

@ -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": {

@ -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": {

@ -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": {

@ -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": {

@ -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": {

@ -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,

@ -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": {

@ -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,

@ -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')
});

@ -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;

@ -18,7 +18,7 @@ interface SSRTest extends BaseTest {
}
const { test, run } = suite<SSRTest>(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`);

@ -10,17 +10,13 @@ interface SnapshotTest extends BaseTest {
}
const { test, run } = suite<SnapshotTest>(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 });

@ -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
})

@ -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'
);
}
});

@ -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'
);
}

@ -1,5 +1,6 @@
import { test } from '../../test';
export default test({
skip: true
client: ['foo.bar.baz'],
server: ['foo.bar.baz']
});

@ -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
});
}

@ -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 }
]
});

@ -1,7 +1,6 @@
<script>
import Widget from 'wherever';
export let potato;
</script>
{potato}
<Widget bind:potato/>

@ -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
});
}

@ -0,0 +1,6 @@
import { test } from '../../test';
export default test({
client: ['bar.baz'],
server: ['bar.baz']
});

@ -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
});
}

@ -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);
}
}
]
});

@ -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}"`
);
});
}

@ -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}"`
);
});
}
});

@ -1,5 +1,5 @@
import { test } from '../../test';
export default test({
skip: true
css: [{ str: '.foo', strGenerated: '.foo.svelte-sg04hs' }]
});

@ -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
});
}

@ -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' }]
});

@ -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
});
}

@ -1,5 +1,5 @@
import { test } from '../../test';
export default test({
skip: true
client: ['foo', 'bar', { str: 'bar', idxGenerated: 1, idxOriginal: 1 }]
});

@ -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
});
}

@ -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 </style> 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;' }
]
});

@ -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
});
}

@ -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: []
});

@ -1,9 +0,0 @@
import { test } from '../../test';
export default test({
skip: true,
compileOptions: {
// @ts-expect-error
enableSourcemap: false
}
});

@ -1,11 +0,0 @@
<script>
export let foo;
</script>
<p>{foo}</p>
<style>
p {
color: red;
}
</style>

@ -1,4 +0,0 @@
export function test({ assert, js, css }) {
assert.equal(js.map, null);
assert.equal(css.map, null);
}

@ -1,9 +0,0 @@
import { test } from '../../test';
export default test({
skip: true,
compileOptions: {
// @ts-expect-error
enableSourcemap: { css: true }
}
});

@ -1,11 +0,0 @@
<script>
export let foo;
</script>
<p>{foo}</p>
<style>
p {
color: red;
}
</style>

@ -1,4 +0,0 @@
export function test({ assert, js, css }) {
assert.equal(js.map, null);
assert.notEqual(css.map, null);
}

@ -1,9 +0,0 @@
import { test } from '../../test';
export default test({
skip: true,
compileOptions: {
// @ts-expect-error
enableSourcemap: { js: true }
}
});

@ -1,11 +0,0 @@
<script>
export let foo;
</script>
<p>{foo}</p>
<style>
p {
color: red;
}
</style>

@ -1,4 +0,0 @@
export function test({ assert, js, css }) {
assert.notEqual(js.map, null);
assert.equal(css.map, null);
}

@ -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']
});

@ -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
});
}

@ -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)' }
]
});

@ -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
});
}

@ -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)
'<script>',
// script content (preprocessed without map, content not changed)
'console.log(name);',
// markup (middle)
'<div>{name}</div>',
// style content (preprocessed without map, content changed)
{ str: 'font-weight: bold;', strGenerated: null },
// markup (end)
'</style>'
]
});

@ -1,37 +0,0 @@
import { assert_mapped, assert_not_mapped } from '../../helpers.js';
export function test({ input, preprocessed }) {
// markup (start)
assert_mapped({
code: '<script>',
input: input.locate,
preprocessed
});
// script content (preprocessed without map, content not changed)
assert_mapped({
code: 'console.log(name);',
input: input.locate,
preprocessed
});
// markup (middle)
assert_mapped({
code: '<div>{name}</div>',
input: input.locate,
preprocessed
});
// style content (preprocessed without map, content changed)
assert_not_mapped({
code: 'font-weight: bold;',
preprocessed
});
// markup (end)
assert_mapped({
code: '</style>',
input: input.locate,
preprocessed
});
}

@ -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: {
script: ({ 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']
});

@ -1,40 +0,0 @@
export function test({ assert, input, js }) {
const expected_bar = input.locate('baritone:');
const expected_baz = input.locate('baz:');
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
},
"couldn't find bar: in source"
);
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
},
"couldn't find baz: in source"
);
}

@ -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: {
style: ({ content, filename = '' }) => {
const src = new MagicString(content);
magic_string_replace_all(src, 'baritone', 'bar');
return magic_string_preprocessor_result(filename, src);
}
}
},
css: [{ str: '--baritone: red', strGenerated: '--bar: red' }, '--baz: blue']
});

@ -1,40 +0,0 @@
export function test({ assert, input, css }) {
const expected_bar = input.locate('--baritone');
const expected_baz = input.locate('--baz');
let start = css.locate('--bar');
const actualbar = css.mapConsumer.originalPositionFor({
line: start.line + 1,
column: start.column
});
assert.deepEqual(
actualbar,
{
source: 'input.svelte',
name: null,
line: expected_bar.line + 1,
column: expected_bar.column
},
"couldn't find bar in source"
);
start = css.locate('--baz');
const actualbaz = css.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
},
"couldn't find baz in source"
);
}

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({ client: ['assertThisLine'] });

@ -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
});
}

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
client: ['42']
});

@ -1,16 +0,0 @@
export function test({ assert, input, js }) {
const expected = input.locate('42');
const start = js.locate('42');
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
});
}

@ -1,20 +1,22 @@
import MagicString from 'magic-string';
import { test } from '../../test';
import { SourceMapConsumer, SourceMapGenerator } from 'source-map';
import * as path from 'node:path';
export default test({
skip: true,
preprocess: {
style: async ({ content, filename }) => {
style: async ({ content, filename = '' }) => {
const src = new MagicString(content);
const idx = content.indexOf('baritone');
src.overwrite(idx, idx + 'baritone'.length, 'bar');
// This test checks that the sourcemap format from source-map
// also works with our preprocessor merging.
const map = SourceMapGenerator.fromSourceMap(
await new SourceMapConsumer(
// sourcemap must be encoded for SourceMapConsumer
src.generateMap({
source: filename,
source: path.basename(filename),
hires: true,
includeContent: false
})
@ -23,5 +25,6 @@ export default test({
return { code: src.toString(), map };
}
}
},
css: [{ str: '--baritone: red', strGenerated: '--bar: red' }, '--baz: blue']
});

@ -8,7 +8,6 @@ export const css_file_basename = 'input.css';
const input_css = ' h1 {color: blue;}';
export default test({
skip: true,
preprocess: [
{
style: ({ content, filename }) => {
@ -30,7 +29,7 @@ export default test({
},
compileOptions: {
filename: component_filepath,
// ../../index.ts initializes output filenames, reset to undefined for this test
// compile_directory initializes output filenames, reset to undefined for this test
outputFilename: undefined,
cssOutputFilename: undefined
}

@ -15,9 +15,7 @@ span {
`;
export default test({
skip: true,
css_map_sources: [external_relative_filename],
js_map_sources: ['input.svelte'],
preprocess: [
{
style: ({ content, filename = '' }) => {
@ -31,5 +29,20 @@ export default test({
],
options: {
filename: component_filepath
},
client: [],
preprocessed: ['Hello world'],
test({ assert, code_preprocessed, map_preprocessed }) {
assert.include(
code_preprocessed,
`/* Filename from preprocess: ${component_filepath} */`,
'Preprocessor should receive same value for filename as passed to preprocess function'
);
assert.deepEqual(
map_preprocessed.sources.slice().sort(),
[external_relative_filename, component_file_basename].sort(),
'Preprocessed map should contain sources relative to filepath'
);
}
});

@ -1,15 +0,0 @@
import { component_filepath, component_file_basename, external_relative_filename } from './_config';
export function test({ assert, preprocessed }) {
assert.notEqual(
preprocessed.locate(`/* Filename from preprocess: ${component_filepath} */`),
undefined,
'Preprocessor should receive same value for filename as passed to preprocess function'
);
assert.deepEqual(
preprocessed.map.sources.slice().sort(),
[external_relative_filename, component_file_basename].sort(),
'Preprocessed map should contain sources relative to filepath'
);
}

@ -3,8 +3,6 @@ import { test } from '../../test';
import { magic_string_preprocessor_result } from '../../helpers.js';
export default test({
skip: true,
js_map_sources: ['input.svelte'],
preprocess: [
{
script: ({ content }) => {
@ -13,5 +11,7 @@ export default test({
return magic_string_preprocessor_result('input.svelte', src);
}
}
]
],
client: ["'Target'"],
preprocessed: ["'Target'"]
});

@ -1,9 +0,0 @@
import { assert_mapped } from '../../helpers.js';
export function test({ input, preprocessed }) {
assert_mapped({
code: 'Target',
input: input.locate,
preprocessed
});
}

@ -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 = '' }) => {
@ -31,5 +30,23 @@ export default test({
return magic_string_preprocessor_result(filename, src);
}
}
]
],
client: [
{ str: 'baritone', strGenerated: 'bar' },
{ str: 'old_name_1', strGenerated: 'new_name_1' },
{ str: 'old_name_2', strGenerated: 'new_name_2' }
],
preprocessed: [
{ str: 'baritone', strGenerated: 'bar' },
{ str: 'old_name_1', strGenerated: 'new_name_1' },
{ str: 'old_name_2', strGenerated: 'new_name_2' },
{ str: '--bazitone', strGenerated: '--baz' }
],
css: [{ str: 'background-color: var(--bazitone)', strGenerated: 'background-color: var(--baz)' }],
test({ assert, map_preprocessed }) {
assert.deepEqual(
map_preprocessed.names.sort(),
['baritone', '--bazitone', 'old_name_1', 'old_name_2'].sort()
);
}
});

@ -1,20 +1,19 @@
import { test } from '../../test';
import { magic_string_bundle } from '../../helpers.js';
export const EXTERNAL = 'span { --external-var: 1px; }';
const EXTERNAL = 'span { --external-var: 1px; }';
export default test({
skip: true,
js_map_sources: ['input.svelte'],
css_map_sources: ['input.svelte', 'external.css'],
preprocess: [
{
style: ({ content, filename = '' }) => {
style: ({ content }) => {
return magic_string_bundle([
{ code: EXTERNAL, filename: 'external.css' },
{ code: content, filename }
{ code: content, filename: 'input.svelte' }
]);
}
}
]
],
preprocessed: [{ str: '--component-var: 2px' }, { code: EXTERNAL, str: '--external-var: 1px' }]
});

@ -1,19 +0,0 @@
import { assert_mapped } from '../../helpers.js';
import { EXTERNAL } from './_config';
export function test({ input, preprocessed }) {
// Part from component, should be with offset
assert_mapped({
code: '--component-var',
input: input.locate,
preprocessed
});
// Part from external file, should be without offset
assert_mapped({
filename: 'external.css',
code: '--external-var',
input: EXTERNAL,
preprocessed
});
}

@ -1,4 +1,5 @@
import MagicString, { Bundle } from 'magic-string';
import * as path from 'node:path';
import { test } from '../../test';
/**
@ -31,31 +32,61 @@ function result(bundle, filename) {
};
}
const FOO = 'var answer = 42; // foo.js\n';
const BAR = 'console.log(answer); // bar.js\n';
const FOO2 = 'var answer2 = 84; // foo2.js\n';
const BAR2 = 'console.log(answer2); // bar2.js\n';
export default test({
skip: true,
js_map_sources: ['input.svelte', 'foo.js', 'bar.js', 'foo2.js', 'bar2.js'],
js_map_sources: [
'../../input.svelte',
'../../foo.js',
'../../bar.js',
'../../foo2.js',
'../../bar2.js'
],
preprocess: [
{
script: ({ content, filename = '' }) => {
const bundle = new Bundle();
add(bundle, filename, content);
add(bundle, 'foo.js', 'var answer = 42; // foo.js\n');
add(bundle, 'bar.js', 'console.log(answer); // bar.js\n');
add(bundle, path.basename(filename), content);
add(bundle, 'foo.js', FOO);
add(bundle, 'bar.js', BAR);
return result(bundle, filename);
return result(bundle, path.basename(filename));
}
},
{
script: ({ content, filename = '' }) => {
const bundle = new Bundle();
add(bundle, filename, content);
add(bundle, 'foo2.js', 'var answer2 = 84; // foo2.js\n');
add(bundle, 'bar2.js', 'console.log(answer2); // bar2.js\n');
add(bundle, path.basename(filename), content);
add(bundle, 'foo2.js', FOO2);
add(bundle, 'bar2.js', BAR2);
return result(bundle, filename);
return result(bundle, path.basename(filename));
}
}
],
client: [
{
code: FOO,
str: 'answer'
},
{
code: BAR,
str: 'answer',
idxGenerated: 1
},
{
code: FOO2,
str: 'answer2'
},
{
code: BAR2,
str: 'answer2',
idxGenerated: 1
}
]
});

@ -1,4 +1,4 @@
<script>
export let name;
</script>
<h1>sourcemap-sources</h1>
<h1>sourcemap-sources {name}</h1>

@ -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}"`
);
});
}

@ -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')
// ]);
}
});

@ -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')
]);
}

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
client: ['first', 'assertThisLine']
});

@ -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
});
}

@ -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 },
'<h1>Hello world!</h1>'
]
});

@ -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: '<h1>Hello world!</h1>',
input: input.locate,
preprocessed
});
// TS types, removed
assert_not_located('ITimeoutDestroyer', preprocessed.locate_1);
}

@ -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<SourcemapTest>(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 };

@ -36,7 +36,7 @@ export function suite<Test extends BaseTest>(fn: (config: Test, test_dir: string
export function suite_with_variants<Test extends BaseTest, Variants extends string, Common>(
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> | Common,
fn: (config: Test, test_dir: string, variant: Variants, common: Common) => void
) {
return {
@ -54,10 +54,10 @@ export function suite_with_variants<Test extends BaseTest, Variants extends stri
const solo = config.solo;
let it_fn = skip ? it.skip : solo ? it.only : it;
it_fn(`${dir} (${variant})`, () => {
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);
});

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save