Preprocessor sourcemap support (#5584)

Co-authored-by: Milan Hauth <milahu@gmail.com>
Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
pull/5698/head
halfnelson 4 years ago committed by GitHub
parent 1fa46fde4f
commit dcfbd69516
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

22
package-lock.json generated

@ -4,6 +4,16 @@
"lockfileVersion": 1,
"requires": true,
"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": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
@ -36,6 +46,12 @@
"integrity": "sha512-KioOCsSvSvXx6xUNLiJz+P+VMb7NRcePjoefOr74Y5P6lEKsiOn35eZyZzgpK4XCNJdXTDR7+zykj0lwxRvZ2g==",
"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": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.0.0.tgz",
@ -3737,9 +3753,9 @@
}
},
"sourcemap-codec": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz",
"integrity": "sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==",
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
"spdx-correct": {

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

@ -29,7 +29,9 @@ import add_to_set from './utils/add_to_set';
import check_graph_for_cycles from './utils/check_graph_for_cycles';
import { print, x, b } from 'code-red';
import { is_reserved_keyword } from './utils/reserved_keywords';
import { apply_preprocessor_sourcemap } from '../utils/string_with_sourcemap';
import Element from './nodes/Element';
import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping/dist/types/types';
interface ComponentOptions {
namespace?: string;
@ -326,6 +328,8 @@ export default class Component {
js.map.sourcesContent = [
this.source
];
js.map = apply_preprocessor_sourcemap(this.file, js.map, compile_options.sourcemap as (string | RawSourceMap | DecodedSourceMap));
}
return {

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

@ -7,6 +7,8 @@ import { extract_names, Scope } from '../utils/scope';
import { invalidate } from './invalidate';
import Block from './Block';
import { ClassDeclaration, FunctionExpression, Node, Statement, ObjectExpression, Expression } from 'estree';
import { apply_preprocessor_sourcemap } from '../../utils/string_with_sourcemap';
import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types';
export default function dom(
component: Component,
@ -30,6 +32,9 @@ export default function dom(
}
const css = component.stylesheet.render(options.filename, !options.customElement);
css.map = apply_preprocessor_sourcemap(options.filename, css.map, options.sourcemap as string | RawSourceMap | DecodedSourceMap);
const styles = component.stylesheet.has_styles && options.dev
? `${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */`
: css.code;

@ -110,6 +110,7 @@ export interface CompileOptions {
filename?: string;
generate?: 'dom' | 'ssr' | false;
sourcemap?: object | string;
outputFilename?: string;
cssOutputFilename?: 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 {
code: string;
map?: object | string;
map?: string | object; // we are opaque with the type here to avoid dependency on the remapping module for our public types.
dependencies?: string[];
}
@ -37,12 +42,18 @@ function parse_attributes(str: string) {
interface Replacement {
offset: 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>> = [];
str.replace(re, (...args) => {
source.replace(re, (...args) => {
replacements.push(
func(...args).then(
res =>
@ -55,16 +66,55 @@ async function replace_async(str: string, re: RegExp, func: (...any) => Promise<
);
return '';
});
let out = '';
const out = new StringWithSourcemap();
let last_end = 0;
for (const { offset, length, replacement } of await Promise.all(
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;
}
out += str.slice(last_end);
return out;
// final_content = unchanged source characters after last replaced segment
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(
@ -76,60 +126,92 @@ export default async function preprocess(
const filename = (options && options.filename) || preprocessor.filename; // legacy
const dependencies = [];
const preprocessors = Array.isArray(preprocessor) ? preprocessor : [preprocessor];
const preprocessors = preprocessor
? Array.isArray(preprocessor) ? preprocessor : [preprocessor]
: [];
const markup = preprocessors.map(p => p.markup).filter(Boolean);
const script = preprocessors.map(p => p.script).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) {
// run markup preprocessor
const processed = await fn({
content: source,
filename
});
if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
source = processed ? processed.code : source;
if (!processed) continue;
if (processed.dependencies) dependencies.push(...processed.dependencies);
source = processed.code;
if (processed.map) {
sourcemap_list.unshift(
typeof(processed.map) === 'string'
? JSON.parse(processed.map)
: processed.map
);
}
}
for (const fn of script) {
source = await replace_async(
async function preprocess_tag_content(tag_name: 'style' | 'script', preprocessor: Preprocessor) {
const get_location = getLocator(source);
const tag_regex = tag_name == 'style'
? /<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi
: /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi;
const res = await replace_async(
filename,
source,
/<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi,
async (match, attributes = '', content = '') => {
get_location,
tag_regex,
async (match, attributes = '', content = '', offset) => {
const no_change = () => StringWithSourcemap.from_source(
filename, match, get_location(offset));
if (!attributes && !content) {
return match;
return no_change();
}
attributes = attributes || '';
const processed = await fn({
content = content || '';
// run script preprocessor
const processed = await preprocessor({
content,
attributes: parse_attributes(attributes),
filename
});
if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
return processed ? `<script${attributes}>${processed.code}</script>` : match;
if (!processed) return no_change();
if (processed.dependencies) dependencies.push(...processed.dependencies);
return get_replacement(filename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, `</${tag_name}>`);
}
);
source = res.string;
sourcemap_list.unshift(res.map);
}
for (const fn of script) {
await preprocess_tag_content('script', fn);
}
for (const fn of style) {
source = await replace_async(
source,
/<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi,
async (match, attributes = '', content = '') => {
if (!attributes && !content) {
return match;
}
const processed: Processed = await fn({
content,
attributes: parse_attributes(attributes),
filename
});
if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
return processed ? `<style${attributes}>${processed.code}</style>` : match;
}
);
await preprocess_tag_content('style', fn);
}
// Combine all the source maps for each preprocessor function into one
const map: RawSourceMap = combine_sourcemaps(
filename,
sourcemap_list
);
return {
// TODO return separated output, in future version where svelte.compile supports it:
// style: { code: styleCode, map: styleMap },
@ -138,7 +220,7 @@ export default async function preprocess(
code: source,
dependencies: [...new Set(dependencies)],
map: (map as object),
toString() {
return source;
}

@ -0,0 +1,275 @@
import { DecodedSourceMap, RawSourceMap, SourceMapLoader } from '@ampproject/remapping/dist/types/types';
import remapping from '@ampproject/remapping';
import { SourceMap } from 'magic-string';
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
) {
if (map.mappings.length == 0) return map;
// shift columns in first line
const segment_list = map.mappings[0];
for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[segment];
if (seg[3]) seg[3] += offset.column;
}
// shift lines
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];
if (seg[2]) seg[2] += offset.line;
}
}
}
function merge_tables<T>(this_table: T[], other_table: T[]): [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[]) {
// We use push to mutate in place for memory and perf reasons
// We use the for loop instead of _this.push(...other) to avoid the JS engine's function argument limit (65,535 in JavascriptCore)
for (let i = 0; i < other.length; i++) {
_this.push(other[i]);
}
}
export class StringWithSourcemap {
string: string;
map: DecodedSourceMap;
constructor(string = '', map: DecodedSourceMap = 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;
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]) seg[1] = new_source_idx[seg[1]];
if (seg[4]) 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]) 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]) 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) {
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
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);
if (string == '') return new StringWithSourcemap();
map = { version: 3, names: [], sources: [], mappings: [] };
// add empty SourceMapSegment[] for every line
const line_count = (string.match(/\n/g) || '').length;
for (let i = 0; i < line_count; i++) map.mappings.push([]);
return new StringWithSourcemap(string, map);
}
static from_source(
source_file: string, source: string, offset?: SourceLocation
): StringWithSourcemap {
if (!offset) offset = { line: 0, column: 0 };
const map: DecodedSourceMap = { version: 3, names: [], sources: [source_file], mappings: [] };
if (source == '') 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.
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(/([^\d\w\s]|\s+)/g);
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 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;
}
// browser vs node.js
const b64enc = typeof btoa == 'function' ? btoa : b => Buffer.from(b).toString('base64');
export function apply_preprocessor_sourcemap(filename: string, svelte_map: SourceMap, preprocessor_map_input: string | DecodedSourceMap | RawSourceMap): SourceMap {
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 = combine_sourcemaps(
filename,
[
svelte_map as RawSourceMap,
preprocessor_map
]
) as RawSourceMap;
// 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 result_map as SourceMap;
}

@ -24,6 +24,9 @@ describe('preprocess', () => {
config.options || { filename: 'input.svelte' }
);
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);

@ -12,7 +12,7 @@ require.extensions['.js'] = function(module, filename) {
.replace(/^import (\w+) from ['"]([^'"]+)['"];?/gm, 'var {default: $1} = require("$2");')
.replace(/^import {([^}]+)} from ['"](.+)['"];?/gm, 'var {$1} = require("$2");')
.replace(/^export default /gm, 'exports.default = ')
.replace(/^export (const|let|var|class|function) (\w+)/gm, (match, type, name) => {
.replace(/^export (const|let|var|class|function|async\s+function) (\w+)/gm, (match, type, name) => {
exports.push(name);
return `${type} ${name}`;
})

@ -0,0 +1,20 @@
import MagicString from 'magic-string';
export function magic_string_preprocessor_result(filename: string, src: MagicString) {
return {
code: src.toString(),
map: src.generateMap({
source: filename,
hires: true,
includeContent: false
})
};
}
export function magic_string_replace_all(src: MagicString, search: string, replace: string) {
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, { storeName: true });
} while ((idx = src.original.indexOf(search, idx + 1)) != -1);
}

@ -37,7 +37,7 @@ describe('sourcemaps', () => {
const preprocessed = await svelte.preprocess(
input.code,
config.preprocess || {},
{
config.options || {
filename: 'input.svelte'
}
);
@ -46,8 +46,10 @@ describe('sourcemaps', () => {
preprocessed.code, {
filename: 'input.svelte',
// filenames for sourcemaps
sourcemap: preprocessed.map,
outputFilename: `${outputName}.js`,
cssOutputFilename: `${outputName}.css`
cssOutputFilename: `${outputName}.css`,
...(config.compile_options || {})
});
js.code = js.code.replace(
@ -107,7 +109,7 @@ describe('sourcemaps', () => {
css.mapConsumer = css.map && await new SourceMapConsumer(css.map);
css.locate = getLocator(css.code || '');
css.locate_1 = getLocator(css.code || '', { offsetLine: 1 });
test({ assert, input, preprocessed, js, css });
await test({ assert, input, preprocessed, js, css });
});
});
});

@ -0,0 +1,21 @@
import MagicString from 'magic-string';
import { magic_string_preprocessor_result, magic_string_replace_all } from '../../helpers';
export default {
compile_options: {
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);
} }
]
};

@ -0,0 +1,15 @@
<h1>Testing Styles</h1>
<h2>Testing Styles 2</h2>
<div>Testing Styles 3</div>
<script>export const b = 2;</script>
<style>
h1 {
--replace-me-once: red;
}
h2 {
--replace-me-twice: green;
}
div {
--keep-me: blue;
}
</style>

@ -0,0 +1,40 @@
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(/\tstyle\.textContent = "(.*?)(?:\\n\/\*# sourceMappingURL=data:(.*?);charset=(.*?);base64,(.*?) \*\/)?";\n/);
assert.notEqual(match, null);
const [mimeType, encoding, cssMapBase64] = match.slice(2);
assert.equal(mimeType, 'application/json');
assert.equal(encoding, 'utf-8');
const cssMapJson = b64dec(cssMapBase64);
css.mapConsumer = await new SourceMapConsumer(cssMapJson);
// TODO make util fn + move to test index.js
const sourcefile = 'input.svelte';
[
// TODO how to get line + column numbers?
[css, '--keep-me', 13, 2],
[css, '--done-replace-once', 6, 5],
[css, '--done-replace-twice', 9, 5]
]
.forEach(([where, content, line, column]) => {
assert.deepEqual(
where.mapConsumer.originalPositionFor(
where.locate_1(content)
),
{
source: sourcefile,
name: null,
line,
column
},
`failed to locate "${content}" from "${sourcefile}"`
);
});
}

@ -0,0 +1,15 @@
import MagicString from 'magic-string';
import { magic_string_preprocessor_result, magic_string_replace_all } from '../../helpers';
export default {
js_map_sources: [], // test component has no scripts
preprocess: {
markup: ({ content, filename }) => {
const src = new MagicString(content);
magic_string_replace_all(src, 'replace me', 'success');
return magic_string_preprocessor_result(filename, src);
}
}
};

@ -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: 'replace me',
line: expected.line + 1,
column: expected.column
});
}

@ -0,0 +1,12 @@
import MagicString from 'magic-string';
import { magic_string_preprocessor_result, magic_string_replace_all } from '../../helpers';
export default {
preprocess: {
markup: ({ content, filename }) => {
const src = new MagicString(content);
magic_string_replace_all(src, 'baritone', 'bar');
return magic_string_preprocessor_result(filename, src);
}
}
};

@ -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: 'baritone',
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,25 @@
import MagicString from 'magic-string';
import { magic_string_preprocessor_result, magic_string_replace_all } from '../../helpers';
export default {
preprocess: {
markup: ({ content, filename }) => {
const src = new MagicString(content);
magic_string_replace_all(src, 'baritone', 'bar');
magic_string_replace_all(src, '--bazitone', '--baz');
return magic_string_preprocessor_result(filename, src);
},
script: ({ content, filename }) => {
const src = new MagicString(content);
const idx = content.indexOf('bar');
src.prependLeft(idx, ' ');
return magic_string_preprocessor_result(filename, src);
},
style: ({ content, filename }) => {
const src = new MagicString(content);
const idx = content.indexOf('--baz');
src.prependLeft(idx, ' ');
return magic_string_preprocessor_result(filename, src);
}
}
};

@ -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,32 @@
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: 'baritone',
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: '--bazitone',
line: expectedBaz.line + 1,
column: expectedBaz.column
});
}

@ -0,0 +1,12 @@
import MagicString from 'magic-string';
import { magic_string_preprocessor_result, magic_string_replace_all } from '../../helpers';
export default {
preprocess: {
script: ({ content, filename }) => {
const src = new MagicString(content);
magic_string_replace_all(src, 'baritone', 'bar');
return magic_string_preprocessor_result(filename, src);
}
}
};

@ -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: 'baritone',
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,12 @@
import MagicString from 'magic-string';
import { magic_string_preprocessor_result, magic_string_replace_all } from '../../helpers';
export default {
preprocess: {
style: ({ content, filename }) => {
const src = new MagicString(content);
magic_string_replace_all(src, 'baritone', 'bar');
return magic_string_preprocessor_result(filename, src);
}
}
};

@ -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,33 @@
import MagicString from 'magic-string';
import { magic_string_preprocessor_result, magic_string_replace_all } from '../../helpers';
export default {
preprocess: [
{
markup: ({ content, filename }) => {
const src = new MagicString(content);
magic_string_replace_all(src, 'baritone', 'bar');
magic_string_replace_all(src,'--bazitone', '--baz');
magic_string_replace_all(src,'old_name_1', 'temp_new_name_1');
magic_string_replace_all(src,'old_name_2', 'temp_new_name_2');
return magic_string_preprocessor_result(filename, src);
}
},
{
markup: ({ content, filename }) => {
const src = new MagicString(content);
magic_string_replace_all(src, 'temp_new_name_1', 'temp_temp_new_name_1');
magic_string_replace_all(src, 'temp_new_name_2', 'temp_temp_new_name_2');
return magic_string_preprocessor_result(filename, src);
}
},
{
markup: ({ content, filename }) => {
const src = new MagicString(content);
magic_string_replace_all(src, 'temp_temp_new_name_1', 'new_name_1');
magic_string_replace_all(src, 'temp_temp_new_name_2', 'new_name_2');
return magic_string_preprocessor_result(filename, src);
}
}
]
};

@ -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,42 @@
// needed for workaround, TODO remove
import { getLocator } from 'locate-character';
export function test({ assert, preprocessed, js, css }) {
assert.deepEqual(
preprocessed.map.names.sort(),
['baritone', '--bazitone', 'old_name_1', 'old_name_2'].sort()
);
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