Add sourcemap support to preprocessors

Co-authored-by: Milan Hauth <milahu@gmail.com>
pull/5584/head
halfnelson 5 years ago
parent 6fa3e91b5d
commit a2bef2f7b9

22
package-lock.json generated

@ -4,6 +4,16 @@
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-0.3.0.tgz",
"integrity": "sha512-dqmASpaTCavldZqwdEpokgG4yOXmEiEGPP3ATTsBbdXXSKf6kx8jt2fPcKhodABdZlYe82OehR2oFK1y9gwZxw==",
"dev": true,
"requires": {
"@jridgewell/resolve-uri": "1.0.0",
"sourcemap-codec": "1.4.8"
}
},
"@babel/code-frame": { "@babel/code-frame": {
"version": "7.10.1", "version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
@ -36,6 +46,12 @@
"integrity": "sha512-KioOCsSvSvXx6xUNLiJz+P+VMb7NRcePjoefOr74Y5P6lEKsiOn35eZyZzgpK4XCNJdXTDR7+zykj0lwxRvZ2g==", "integrity": "sha512-KioOCsSvSvXx6xUNLiJz+P+VMb7NRcePjoefOr74Y5P6lEKsiOn35eZyZzgpK4XCNJdXTDR7+zykj0lwxRvZ2g==",
"dev": true "dev": true
}, },
"@jridgewell/resolve-uri": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-1.0.0.tgz",
"integrity": "sha512-9oLAnygRMi8Q5QkYEU4XWK04B+nuoXoxjRvRxgjuChkLZFBja0YPSgdZ7dZtwhncLBcQe/I/E+fLuk5qxcYVJA==",
"dev": true
},
"@rollup/plugin-commonjs": { "@rollup/plugin-commonjs": {
"version": "11.0.0", "version": "11.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.0.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.0.0.tgz",
@ -3737,9 +3753,9 @@
} }
}, },
"sourcemap-codec": { "sourcemap-codec": {
"version": "1.4.6", "version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true "dev": true
}, },
"spdx-correct": { "spdx-correct": {

@ -56,6 +56,7 @@
}, },
"homepage": "https://github.com/sveltejs/svelte#README", "homepage": "https://github.com/sveltejs/svelte#README",
"devDependencies": { "devDependencies": {
"@ampproject/remapping": "^0.3.0",
"@rollup/plugin-commonjs": "^11.0.0", "@rollup/plugin-commonjs": "^11.0.0",
"@rollup/plugin-json": "^4.0.1", "@rollup/plugin-json": "^4.0.1",
"@rollup/plugin-node-resolve": "^6.0.0", "@rollup/plugin-node-resolve": "^6.0.0",
@ -89,6 +90,7 @@
"rollup": "^1.27.14", "rollup": "^1.27.14",
"source-map": "^0.7.3", "source-map": "^0.7.3",
"source-map-support": "^0.5.13", "source-map-support": "^0.5.13",
"sourcemap-codec": "^1.4.8",
"tiny-glob": "^0.2.6", "tiny-glob": "^0.2.6",
"tslib": "^1.10.0", "tslib": "^1.10.0",
"typescript": "^3.5.3" "typescript": "^3.5.3"

@ -29,6 +29,7 @@ import add_to_set from './utils/add_to_set';
import check_graph_for_cycles from './utils/check_graph_for_cycles'; import check_graph_for_cycles from './utils/check_graph_for_cycles';
import { print, x, b } from 'code-red'; import { print, x, b } from 'code-red';
import { is_reserved_keyword } from './utils/reserved_keywords'; import { is_reserved_keyword } from './utils/reserved_keywords';
import { combine_sourcemaps, sourcemap_define_tostring_tourl } from '../utils/string_with_sourcemap';
import Element from './nodes/Element'; import Element from './nodes/Element';
interface ComponentOptions { interface ComponentOptions {
@ -330,6 +331,29 @@ export default class Component {
js.map.sourcesContent = [ js.map.sourcesContent = [
this.source this.source
]; ];
if (compile_options.sourcemap) {
if (js.map) {
js.map = combine_sourcemaps(
this.file,
[
js.map, // idx 1: internal
compile_options.sourcemap // idx 0: external: svelte.preprocess, etc
]
);
sourcemap_define_tostring_tourl(js.map);
}
if (css.map) {
css.map = combine_sourcemaps(
this.file,
[
css.map, // idx 1: internal
compile_options.sourcemap // idx 0: external: svelte.preprocess, etc
]
);
sourcemap_define_tostring_tourl(css.map);
}
}
} }
return { return {

@ -11,6 +11,7 @@ const valid_options = [
'format', 'format',
'name', 'name',
'filename', 'filename',
'sourcemap',
'generate', 'generate',
'outputFilename', 'outputFilename',
'cssOutputFilename', 'cssOutputFilename',

@ -110,6 +110,7 @@ export interface CompileOptions {
filename?: string; filename?: string;
generate?: 'dom' | 'ssr' | false; generate?: 'dom' | 'ssr' | false;
sourcemap?: object | string;
outputFilename?: string; outputFilename?: string;
cssOutputFilename?: string; cssOutputFilename?: string;
sveltePath?: string; sveltePath?: string;

@ -1,6 +1,11 @@
import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types';
import { decode as decode_mappings } from 'sourcemap-codec';
import { getLocator } from 'locate-character';
import { StringWithSourcemap, sourcemap_add_offset, combine_sourcemaps } from '../utils/string_with_sourcemap';
export interface Processed { export interface Processed {
code: string; code: string;
map?: object | string; map?: string | object; // we be opaque with the type here to avoid dependency on the remapping module for our public types.
dependencies?: string[]; dependencies?: string[];
} }
@ -37,12 +42,18 @@ function parse_attributes(str: string) {
interface Replacement { interface Replacement {
offset: number; offset: number;
length: number; length: number;
replacement: string; replacement: StringWithSourcemap;
} }
async function replace_async(str: string, re: RegExp, func: (...any) => Promise<string>) { async function replace_async(
filename: string,
source: string,
get_location: ReturnType<typeof getLocator>,
re: RegExp,
func: (...any) => Promise<StringWithSourcemap>
): Promise<StringWithSourcemap> {
const replacements: Array<Promise<Replacement>> = []; const replacements: Array<Promise<Replacement>> = [];
str.replace(re, (...args) => { source.replace(re, (...args) => {
replacements.push( replacements.push(
func(...args).then( func(...args).then(
res => res =>
@ -55,16 +66,52 @@ async function replace_async(str: string, re: RegExp, func: (...any) => Promise<
); );
return ''; return '';
}); });
let out = ''; const out = new StringWithSourcemap();
let last_end = 0; let last_end = 0;
for (const { offset, length, replacement } of await Promise.all( for (const { offset, length, replacement } of await Promise.all(
replacements replacements
)) { )) {
out += str.slice(last_end, offset) + replacement; // content = unchanged source characters before the replaced segment
const content = StringWithSourcemap.from_source(
filename, source.slice(last_end, offset), get_location(last_end));
out.concat(content).concat(replacement);
last_end = offset + length; last_end = offset + length;
} }
out += str.slice(last_end); // final_content = unchanged source characters after last replaced segment
return out; const final_content = StringWithSourcemap.from_source(
filename, source.slice(last_end), get_location(last_end));
return out.concat(final_content);
}
// Convert a preprocessor output and its leading prefix and trailing suffix into StringWithSourceMap
function get_replacement(
filename: string,
offset: number,
get_location: ReturnType<typeof getLocator>,
original: string,
processed: Processed,
prefix: string,
suffix: string
): StringWithSourcemap {
// Convert the unchanged prefix and suffix to StringWithSourcemap
const prefix_with_map = StringWithSourcemap.from_source(
filename, prefix, get_location(offset));
const suffix_with_map = StringWithSourcemap.from_source(
filename, suffix, get_location(offset + prefix.length + original.length));
// Convert the preprocessed code and its sourcemap to a StringWithSourcemap
let decoded_map: DecodedSourceMap;
if (processed.map) {
decoded_map = typeof processed.map === 'string' ? JSON.parse(processed.map) : processed.map;
if (typeof(decoded_map.mappings) === 'string')
decoded_map.mappings = decode_mappings(decoded_map.mappings);
sourcemap_add_offset(decoded_map, get_location(offset + prefix.length));
}
const processed_with_map = StringWithSourcemap.from_processed(processed.code, decoded_map);
// Surround the processed code with the prefix and suffix, retaining valid sourcemappings
return prefix_with_map.concat(processed_with_map).concat(suffix_with_map);
} }
export default async function preprocess( export default async function preprocess(
@ -76,60 +123,107 @@ export default async function preprocess(
const filename = (options && options.filename) || preprocessor.filename; // legacy const filename = (options && options.filename) || preprocessor.filename; // legacy
const dependencies = []; const dependencies = [];
const preprocessors = Array.isArray(preprocessor) ? preprocessor : [preprocessor]; const preprocessors = Array.isArray(preprocessor) ? preprocessor : [preprocessor || {}];
const markup = preprocessors.map(p => p.markup).filter(Boolean); const markup = preprocessors.map(p => p.markup).filter(Boolean);
const script = preprocessors.map(p => p.script).filter(Boolean); const script = preprocessors.map(p => p.script).filter(Boolean);
const style = preprocessors.map(p => p.style).filter(Boolean); const style = preprocessors.map(p => p.style).filter(Boolean);
// sourcemap_list is sorted in reverse order from last map (index 0) to first map (index -1)
// so we use sourcemap_list.unshift() to add new maps
// https://github.com/ampproject/remapping#multiple-transformations-of-a-file
const sourcemap_list: Array<DecodedSourceMap | RawSourceMap> = [];
// TODO keep track: what preprocessor generated what sourcemap? to make debugging easier = detect low-resolution sourcemaps in fn combine_mappings
for (const fn of markup) { for (const fn of markup) {
// run markup preprocessor
const processed = await fn({ const processed = await fn({
content: source, content: source,
filename filename
}); });
if (processed && processed.dependencies) dependencies.push(...processed.dependencies); if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
source = processed ? processed.code : source; source = processed ? processed.code : source;
if (processed && processed.map)
sourcemap_list.unshift(
typeof(processed.map) === 'string'
? JSON.parse(processed.map)
: processed.map
);
} }
for (const fn of script) { for (const fn of script) {
source = await replace_async( const get_location = getLocator(source);
const res = await replace_async(
filename,
source, source,
get_location,
/<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi, /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi,
async (match, attributes = '', content = '') => { async (match, attributes = '', content = '', offset) => {
const no_change = () => StringWithSourcemap.from_source(
filename, match, get_location(offset));
if (!attributes && !content) { if (!attributes && !content) {
return match; return no_change();
} }
attributes = attributes || ''; attributes = attributes || '';
content = content || '';
// run script preprocessor
const processed = await fn({ const processed = await fn({
content, content,
attributes: parse_attributes(attributes), attributes: parse_attributes(attributes),
filename filename
}); });
if (processed && processed.dependencies) dependencies.push(...processed.dependencies); if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
return processed ? `<script${attributes}>${processed.code}</script>` : match; return processed
? get_replacement(filename, offset, get_location, content, processed, `<script${attributes}>`, '</script>')
: no_change();
} }
); );
source = res.string;
sourcemap_list.unshift(res.map);
} }
for (const fn of style) { for (const fn of style) {
source = await replace_async( const get_location = getLocator(source);
const res = await replace_async(
filename,
source, source,
get_location,
/<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi, /<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi,
async (match, attributes = '', content = '') => { async (match, attributes = '', content = '', offset) => {
const no_change = () => StringWithSourcemap.from_source(
filename, match, get_location(offset));
if (!attributes && !content) { if (!attributes && !content) {
return match; return no_change();
} }
attributes = attributes || '';
content = content || '';
// run style preprocessor
const processed: Processed = await fn({ const processed: Processed = await fn({
content, content,
attributes: parse_attributes(attributes), attributes: parse_attributes(attributes),
filename filename
}); });
if (processed && processed.dependencies) dependencies.push(...processed.dependencies); if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
return processed ? `<style${attributes}>${processed.code}</style>` : match; return processed
? get_replacement(filename, offset, get_location, content, processed, `<style${attributes}>`, '</style>')
: no_change();
} }
); );
source = res.string;
sourcemap_list.unshift(res.map);
} }
// Combine all the source maps for each preprocessor function into one
const map: RawSourceMap = combine_sourcemaps(
filename,
sourcemap_list
);
return { return {
// TODO return separated output, in future version where svelte.compile supports it: // TODO return separated output, in future version where svelte.compile supports it:
// style: { code: styleCode, map: styleMap }, // style: { code: styleCode, map: styleMap },
@ -138,7 +232,7 @@ export default async function preprocess(
code: source, code: source,
dependencies: [...new Set(dependencies)], dependencies: [...new Set(dependencies)],
map: (map as object),
toString() { toString() {
return source; return source;
} }

@ -0,0 +1,235 @@
import { DecodedSourceMap, RawSourceMap, SourceMapSegment, SourceMapLoader } from '@ampproject/remapping/dist/types/types';
import remapping from '@ampproject/remapping';
type SourceLocation = {
line: number;
column: number;
};
function last_line_length(s: string) {
return s.length - s.lastIndexOf('\n') - 1;
}
// mutate map in-place
export function sourcemap_add_offset(
map: DecodedSourceMap, offset: SourceLocation
) {
// shift columns in first line
const m = map.mappings;
m[0].forEach(seg => {
if (seg[3]) seg[3] += offset.column;
});
// shift lines
m.forEach(line => {
line.forEach(seg => {
if (seg[2]) seg[2] += offset.line;
});
});
}
function merge_tables<T>(this_table: T[], other_table): [T[], number[], boolean, boolean] {
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((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];
}
function pushArray<T>(_this: T[], other: T[]) {
for (let i = 0; i < other.length; i++)
_this.push(other[i]);
}
export class StringWithSourcemap {
string: string;
map: DecodedSourceMap;
constructor(string = '', map = null) {
this.string = string;
if (map)
this.map = map as DecodedSourceMap;
else
this.map = {
version: 3,
mappings: [],
sources: [],
names: []
};
}
// concat in-place (mutable), return this (chainable)
// will also mutate the `other` object
concat(other: StringWithSourcemap): StringWithSourcemap {
// 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;
}
this.string += other.string;
const m1 = this.map;
const m2 = other.map;
// 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) {
m2.mappings.forEach(line => {
line.forEach(seg => {
if (seg[1]) seg[1] = new_source_idx[seg[1]];
if (seg[4]) seg[4] = new_name_idx[seg[4]];
});
});
} else if (sources_idx_changed) {
m2.mappings.forEach(line => {
line.forEach(seg => {
if (seg[1]) seg[1] = new_source_idx[seg[1]];
});
});
} else if (names_idx_changed) {
m2.mappings.forEach(line => {
line.forEach(seg => {
if (seg[4]) 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
const column_offset = last_line_length(this.string);
if (m2.mappings.length > 0 && column_offset > 0) {
// shift columns in first line
m2.mappings[0].forEach(seg => {
seg[0] += column_offset;
});
}
// combine last line + first line
pushArray(m1.mappings[m1.mappings.length - 1], m2.mappings.shift());
// append other lines
pushArray(m1.mappings, m2.mappings);
return this;
}
static from_processed(string: string, map?: DecodedSourceMap): StringWithSourcemap {
if (map) return new StringWithSourcemap(string, map);
map = { version: 3, names: [], sources: [], mappings: [] };
if (string == '') return new StringWithSourcemap(string, map);
// add empty SourceMapSegment[] for every line
const lineCount = string.split('\n').length;
map.mappings = Array.from({length: lineCount}).map(_ => []);
return new StringWithSourcemap(string, map);
}
static from_source(
source_file: string, source: string, offset_in_source?: SourceLocation
): StringWithSourcemap {
const offset = offset_in_source || { line: 0, column: 0 };
const map: DecodedSourceMap = { version: 3, names: [], sources: [source_file], mappings: [] };
if (source.length == 0) return new StringWithSourcemap(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.
map.mappings = source.split('\n').map((line, line_idx) => {
let pos = 0;
const segs = line.split(/([^\d\w\s]|\s+)/g)
.filter(s => s !== '').map(s => {
const seg: SourceMapSegment = [
pos, 0,
line_idx + offset.line,
pos + (line_idx == 0 ? offset.column : 0) // shift first line
];
pos = pos + s.length;
return seg;
});
return segs;
});
return new StringWithSourcemap(source, map);
}
}
export function combine_sourcemaps(
filename: string,
sourcemap_list: Array<DecodedSourceMap | RawSourceMap>
): RawSourceMap {
if (sourcemap_list.length == 0) return null;
let map_idx = 1;
const map: RawSourceMap =
sourcemap_list.slice(0, -1)
.find(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
function loader(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
} as SourceMapLoader,
true
);
if (!map.file) delete map.file; // skip optional field `file`
return map;
}
export function sourcemap_define_tostring_tourl(map) {
Object.defineProperties(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,' + btoa(this.toString());
}
}
});
}

@ -24,6 +24,9 @@ describe('preprocess', () => {
config.options || { filename: 'input.svelte' } config.options || { filename: 'input.svelte' }
); );
fs.writeFileSync(`${__dirname}/samples/${dir}/_actual.html`, result.code); fs.writeFileSync(`${__dirname}/samples/${dir}/_actual.html`, result.code);
if (result.map) {
fs.writeFileSync(`${__dirname}/samples/${dir}/_actual.html.map`, JSON.stringify(result.map, null, 2));
}
assert.equal(result.code, expected); assert.equal(result.code, expected);

@ -37,7 +37,7 @@ describe('sourcemaps', () => {
const preprocessed = await svelte.preprocess( const preprocessed = await svelte.preprocess(
input.code, input.code,
config.preprocess || {}, config.preprocess || {},
{ config.options || {
filename: 'input.svelte' filename: 'input.svelte'
} }
); );
@ -46,6 +46,7 @@ describe('sourcemaps', () => {
preprocessed.code, { preprocessed.code, {
filename: 'input.svelte', filename: 'input.svelte',
// filenames for sourcemaps // filenames for sourcemaps
sourcemap: preprocessed.map,
outputFilename: `${outputName}.js`, outputFilename: `${outputName}.js`,
cssOutputFilename: `${outputName}.css` cssOutputFilename: `${outputName}.css`
}); });

@ -0,0 +1,32 @@
import MagicString from 'magic-string';
function replace(search, replace, content, src, options = {}) {
let idx = -1;
while ((idx = content.indexOf(search, idx + 1)) != -1) {
src.overwrite(idx, idx + search.length, replace, options);
}
}
function result(src, filename) {
return {
code: src.toString(),
map: src.generateDecodedMap({ // return decoded sourcemap
source: filename,
hires: true,
includeContent: false
})
};
}
export default {
js_map_sources: [], // test component has no scripts
preprocess: {
markup: ({ content, filename }) => {
const src = new MagicString(content);
replace('replace me', 'success', content, src);
return result(src, filename);
}
}
};

@ -0,0 +1,2 @@
<h1>decoded-sourcemap</h1>
<div>replace me</div>

@ -0,0 +1,19 @@
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: null,
line: expected.line + 1,
column: expected.column
});
}

@ -0,0 +1,54 @@
import MagicString from 'magic-string';
// TODO move util fns to test index.js
function result(filename, src, extraOptions = {}) {
return {
code: src.toString(),
map: src.generateDecodedMap({
source: filename,
hires: true,
includeContent: false,
...extraOptions
})
};
}
function replace_all(src, search, replace) {
let idx = src.original.indexOf(search);
if (idx == -1) throw new Error('search not found in src');
do {
src.overwrite(idx, idx + search.length, replace);
} while ((idx = src.original.indexOf(search, idx + 1)) != -1);
}
function replace_first(src, search, replace) {
const idx = src.original.indexOf(search);
if (idx == -1) throw new Error('search not found in src');
src.overwrite(idx, idx + search.length, replace);
}
export default {
preprocess_options: {
sourcemapLossWarn: 0.9 // warn often
},
js_map_sources: [], // test component has no scripts
preprocess: [
{ markup: ({ content, filename }) => {
const src = new MagicString(content);
replace_all(src, 'replace_me', 'done_replace');
return result(filename, src, { hires: true });
} },
{ markup: ({ content, filename }) => {
const src = new MagicString(content);
replace_first(src, 'done_replace', 'version_3');
// return low-resolution sourcemap
// this should make previous mappings unreachable
return result(filename, src, { hires: false });
} }
]
};

@ -0,0 +1,10 @@
replace_me
replace_me
replace_me
replace_me
replace_me
replace_me
replace_me
replace_me
replace_me
replace_me

@ -0,0 +1,10 @@
export function test({ assert, preprocessed, js }) {
assert.equal(preprocessed.error, undefined);
// TODO can we automate this test?
// we need the output of console.log
// to test the warning message.
// or use a different method for warnings?
}

@ -0,0 +1,18 @@
import MagicString from 'magic-string';
export default {
preprocess: {
markup: ({ content, filename }) => {
const src = new MagicString(content);
const idx = content.indexOf('baritone');
src.overwrite(idx, idx+'baritone'.length, 'bar');
return {
code: src.toString(),
map: src.generateDecodedMap({
source: filename,
includeContent: false
})
};
}
}
};

@ -0,0 +1,5 @@
<script>
export let foo;
</script>
{foo.baritone.baz}

@ -0,0 +1,32 @@
export function test({ assert, input, js }) {
const expectedBar = input.locate('baritone.baz');
const expectedBaz = 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: null,
line: expectedBar.line + 1,
column: expectedBar.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: expectedBaz.line + 1,
column: expectedBaz.column
});
}

@ -0,0 +1,48 @@
import MagicString from 'magic-string';
export default {
preprocess: {
markup: ({ content, filename }) => {
const src = new MagicString(content);
const idx = content.indexOf('baritone');
src.overwrite(idx, idx + 'baritone'.length, 'bar');
const css_idx = content.indexOf('--bazitone');
src.overwrite(css_idx, css_idx + '--bazitone'.length, '--baz');
return {
code: src.toString(),
map: src.generateDecodedMap({
source: filename,
hires: true,
includeContent: false
})
};
},
script: ({ content, filename }) => {
const src = new MagicString(content);
const idx = content.indexOf('bar');
src.prependLeft(idx, ' ');
return {
code: src.toString(),
map: src.generateDecodedMap({
source: filename,
hires: true,
includeContent: false
})
};
},
style: ({ content, filename }) => {
const src = new MagicString(content);
const idx = content.indexOf('--baz');
src.prependLeft(idx, ' ');
return {
code: src.toString(),
map: src.generateDecodedMap({
source: filename,
hires: true,
includeContent: false
})
};
}
}
};

@ -0,0 +1,9 @@
<script>
export let foo = { baritone: 5 }
</script>
<style>
h1 {
background-color: var(--bazitone);
}
</style>
<h1>multiple {foo}</h1>

@ -0,0 +1,37 @@
export function test({ assert, input, js, css }) {
const expectedBar = input.locate('baritone');
const expectedBaz = 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: null,
line: expectedBar.line + 1,
column: expectedBar.column
});
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: expectedBaz.line + 1,
column: expectedBaz.column
}, `\
couldn't find baz in css,
gen: ${JSON.stringify(start)}
actual: ${JSON.stringify(actualbaz)}
expected: ${JSON.stringify(expectedBaz)}\
`);
}

@ -0,0 +1,19 @@
import MagicString from 'magic-string';
export default {
preprocess: {
script: ({ content, filename }) => {
const src = new MagicString(content);
const idx = content.indexOf('baritone');
src.overwrite(idx, idx+'baritone'.length, 'bar');
return {
code: src.toString(),
map: src.generateMap({
source: filename,
hires: true,
includeContent: false
})
};
}
}
};

@ -0,0 +1,9 @@
<style>
h1 {
color: red;
}
</style>
<script>
export let foo = { baritone: { baz: 5 } }
</script>
<h1>{foo.bar.baz}</h1>

@ -0,0 +1,32 @@
export function test({ assert, input, js }) {
const expectedBar = input.locate('baritone:');
const expectedBaz = 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: null,
line: expectedBar.line + 1,
column: expectedBar.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: expectedBaz.line + 1,
column: expectedBaz.column
}, "couldn't find baz: in source");
}

@ -0,0 +1,19 @@
import MagicString from 'magic-string';
export default {
preprocess: {
style: ({ content, filename }) => {
const src = new MagicString(content);
const idx = content.indexOf('baritone');
src.overwrite(idx, idx+'baritone'.length, 'bar');
return {
code: src.toString(),
map: src.generateMap({
source: filename,
hires: true,
includeContent: false
})
};
}
}
};

@ -0,0 +1,12 @@
<h1>Testing Styles</h1>
<h2>Testing Styles 2</h2>
<script>export const b = 2;</script>
<style>
h1 {
--baritone: red;
}
h2 {
--baz: blue;
}
</style>

@ -0,0 +1,32 @@
export function test({ assert, input, css }) {
const expectedBar = input.locate('--baritone');
const expectedBaz = 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: expectedBar.line + 1,
column: expectedBar.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: expectedBaz.line + 1,
column: expectedBaz.column
}, "couldn't find baz in source");
}

@ -0,0 +1,50 @@
import MagicString from 'magic-string';
function replace(search, replace, content, src, options = { storeName: true }) {
let idx = -1;
while ((idx = content.indexOf(search, idx + 1)) != -1) {
src.overwrite(idx, idx + search.length, replace, options);
}
}
function result(src, filename) {
return {
code: src.toString(),
map: src.generateDecodedMap({
source: filename,
hires: true,
includeContent: false
})
};
}
export default {
preprocess: [
{
markup: ({ content, filename }) => {
const src = new MagicString(content);
replace('baritone', 'bar', content, src);
replace('--bazitone', '--baz', content, src);
replace('old_name_1', 'temp_new_name_1', content, src);
replace('old_name_2', 'temp_new_name_2', content, src);
return result(src, filename);
}
},
{
markup: ({ content, filename }) => {
const src = new MagicString(content);
replace('temp_new_name_1', 'temp_temp_new_name_1', content, src);
replace('temp_new_name_2', 'temp_temp_new_name_2', content, src);
return result(src, filename);
}
},
{
markup: ({ content, filename }) => {
const src = new MagicString(content);
replace('temp_temp_new_name_1', 'new_name_1', content, src);
replace('temp_temp_new_name_2', 'new_name_2', content, src);
return result(src, filename);
}
}
]
};

@ -0,0 +1,12 @@
<script>
export let old_name_1 = { baritone: 5 };
let old_name_2 = 'value_2';
</script>
<style>
div {
background-color: var(--bazitone);
}
</style>
<h1>use-names</h1>
<div>{old_name_1.baritone}</div>
<pre style="color: var(--bazitone)">{old_name_2}</pre>

@ -0,0 +1,43 @@
// needed for workaround, TODO remove
import { getLocator } from 'locate-character';
export function test({ assert, input, preprocessed, js, css }) {
assert.deepEqual(
preprocessed.map.names.sort(),
['baritone', '--bazitone', 'old_name_1', 'old_name_2'].sort()
);
// TODO move fn test_name to test/sourcemaps/index.js and use in samples/*/test.js
function test_name(old_name, new_name, where) {
let loc = { character: -1 };
while (loc = where.locate(new_name, loc.character + 1)) {
const actualMapping = where.mapConsumer.originalPositionFor({
line: loc.line + 1, column: loc.column
});
if (actualMapping.line === null) {
// location is not mapped - ignore
continue;
}
assert.equal(actualMapping.name, old_name);
}
if (loc === undefined) {
// workaround for bug in locate-character, TODO remove
// https://github.com/Rich-Harris/locate-character/pull/5
where.locate = getLocator(where.code);
}
}
test_name('baritone', 'bar', js);
test_name('baritone', 'bar', preprocessed);
test_name('--bazitone', '--baz', css);
test_name('--bazitone', '--baz', preprocessed);
test_name('old_name_1', 'new_name_1', js);
test_name('old_name_1', 'new_name_1', preprocessed);
test_name('old_name_2', 'new_name_2', js);
test_name('old_name_2', 'new_name_2', preprocessed);
}

@ -0,0 +1,60 @@
/* eslint-disable import/no-duplicates */
/* the code that transforms these to commonjs, can't handle "MagicString, { Bundle } from.." */
import MagicString from 'magic-string';
import { Bundle } from 'magic-string';
function add(bundle, filename, source) {
bundle.addSource({
filename,
content: new MagicString(source),
separator: '\n'
//separator: '' // ERROR. probably a bug in magic-string
});
}
function result(bundle, filename) {
return {
code: bundle.toString(),
map: bundle.generateMap({
file: filename,
includeContent: false,
hires: true // required for remapping
})
};
}
export default {
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');
return result(bundle, 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');
return result(bundle, filename);
}
}
]
};

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

@ -0,0 +1,29 @@
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}"`
);
});
}
Loading…
Cancel
Save