From a2bef2f7b9fc311b3de92391671b7d20e440010e Mon Sep 17 00:00:00 2001 From: halfnelson Date: Sun, 25 Oct 2020 18:51:00 +1000 Subject: [PATCH] Add sourcemap support to preprocessors Co-authored-by: Milan Hauth --- package-lock.json | 22 +- package.json | 2 + src/compiler/compile/Component.ts | 24 ++ src/compiler/compile/index.ts | 1 + src/compiler/interfaces.ts | 1 + src/compiler/preprocess/index.ts | 130 ++++++++-- src/compiler/utils/string_with_sourcemap.ts | 235 ++++++++++++++++++ test/preprocess/index.ts | 3 + test/sourcemaps/index.ts | 3 +- .../samples/decoded-sourcemap/_config.js | 32 +++ .../samples/decoded-sourcemap/input.svelte | 2 + .../samples/decoded-sourcemap/test.js | 19 ++ .../detect-lowres-sourcemaps/_config.js | 54 ++++ .../detect-lowres-sourcemaps/input.svelte | 10 + .../samples/detect-lowres-sourcemaps/test.js | 10 + .../samples/preprocessed-markup/_config.js | 18 ++ .../samples/preprocessed-markup/input.svelte | 5 + .../samples/preprocessed-markup/test.js | 32 +++ .../samples/preprocessed-multiple/_config.js | 48 ++++ .../preprocessed-multiple/input.svelte | 9 + .../samples/preprocessed-multiple/test.js | 37 +++ .../samples/preprocessed-script/_config.js | 19 ++ .../samples/preprocessed-script/input.svelte | 9 + .../samples/preprocessed-script/test.js | 32 +++ .../samples/preprocessed-styles/_config.js | 19 ++ .../samples/preprocessed-styles/input.svelte | 12 + .../samples/preprocessed-styles/test.js | 32 +++ .../samples/sourcemap-names/_config.js | 50 ++++ .../samples/sourcemap-names/input.svelte | 12 + .../samples/sourcemap-names/test.js | 43 ++++ .../samples/sourcemap-sources/_config.js | 60 +++++ .../samples/sourcemap-sources/input.svelte | 4 + .../samples/sourcemap-sources/test.js | 29 +++ 33 files changed, 996 insertions(+), 22 deletions(-) create mode 100644 src/compiler/utils/string_with_sourcemap.ts create mode 100644 test/sourcemaps/samples/decoded-sourcemap/_config.js create mode 100644 test/sourcemaps/samples/decoded-sourcemap/input.svelte create mode 100644 test/sourcemaps/samples/decoded-sourcemap/test.js create mode 100644 test/sourcemaps/samples/detect-lowres-sourcemaps/_config.js create mode 100644 test/sourcemaps/samples/detect-lowres-sourcemaps/input.svelte create mode 100644 test/sourcemaps/samples/detect-lowres-sourcemaps/test.js create mode 100644 test/sourcemaps/samples/preprocessed-markup/_config.js create mode 100644 test/sourcemaps/samples/preprocessed-markup/input.svelte create mode 100644 test/sourcemaps/samples/preprocessed-markup/test.js create mode 100644 test/sourcemaps/samples/preprocessed-multiple/_config.js create mode 100644 test/sourcemaps/samples/preprocessed-multiple/input.svelte create mode 100644 test/sourcemaps/samples/preprocessed-multiple/test.js create mode 100644 test/sourcemaps/samples/preprocessed-script/_config.js create mode 100644 test/sourcemaps/samples/preprocessed-script/input.svelte create mode 100644 test/sourcemaps/samples/preprocessed-script/test.js create mode 100644 test/sourcemaps/samples/preprocessed-styles/_config.js create mode 100644 test/sourcemaps/samples/preprocessed-styles/input.svelte create mode 100644 test/sourcemaps/samples/preprocessed-styles/test.js create mode 100644 test/sourcemaps/samples/sourcemap-names/_config.js create mode 100644 test/sourcemaps/samples/sourcemap-names/input.svelte create mode 100644 test/sourcemaps/samples/sourcemap-names/test.js create mode 100644 test/sourcemaps/samples/sourcemap-sources/_config.js create mode 100644 test/sourcemaps/samples/sourcemap-sources/input.svelte create mode 100644 test/sourcemaps/samples/sourcemap-sources/test.js diff --git a/package-lock.json b/package-lock.json index e04cc22175..9c19e27a7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index a0f0b9e455..19d1f715d6 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,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", @@ -89,6 +90,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" diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index d2542c9830..340c1cacb7 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -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 { print, x, b } from 'code-red'; 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'; interface ComponentOptions { @@ -330,6 +331,29 @@ export default class Component { js.map.sourcesContent = [ 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 { diff --git a/src/compiler/compile/index.ts b/src/compiler/compile/index.ts index 1faa33ee1e..842539fcde 100644 --- a/src/compiler/compile/index.ts +++ b/src/compiler/compile/index.ts @@ -11,6 +11,7 @@ const valid_options = [ 'format', 'name', 'filename', + 'sourcemap', 'generate', 'outputFilename', 'cssOutputFilename', diff --git a/src/compiler/interfaces.ts b/src/compiler/interfaces.ts index 5249c2fd48..689b59529d 100644 --- a/src/compiler/interfaces.ts +++ b/src/compiler/interfaces.ts @@ -110,6 +110,7 @@ export interface CompileOptions { filename?: string; generate?: 'dom' | 'ssr' | false; + sourcemap?: object | string; outputFilename?: string; cssOutputFilename?: string; sveltePath?: string; diff --git a/src/compiler/preprocess/index.ts b/src/compiler/preprocess/index.ts index 1d7d74ceac..12cc8b00cd 100644 --- a/src/compiler/preprocess/index.ts +++ b/src/compiler/preprocess/index.ts @@ -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 be 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) { +async function replace_async( + filename: string, + source: string, + get_location: ReturnType, + re: RegExp, + func: (...any) => Promise +): Promise { const replacements: Array> = []; - str.replace(re, (...args) => { + source.replace(re, (...args) => { replacements.push( func(...args).then( res => @@ -55,16 +66,52 @@ 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, + 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 +123,107 @@ export default async function preprocess( const filename = (options && options.filename) || preprocessor.filename; // legacy 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 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 = []; + + // 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 && processed.map) + sourcemap_list.unshift( + typeof(processed.map) === 'string' + ? JSON.parse(processed.map) + : processed.map + ); } for (const fn of script) { - source = await replace_async( + const get_location = getLocator(source); + const res = await replace_async( + filename, source, + get_location, /|([^]*?)<\/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) { - return match; + return no_change(); } attributes = attributes || ''; + content = content || ''; + + // run script preprocessor const processed = await fn({ content, attributes: parse_attributes(attributes), filename }); if (processed && processed.dependencies) dependencies.push(...processed.dependencies); - return processed ? `${processed.code}` : match; + return processed + ? get_replacement(filename, offset, get_location, content, processed, ``, '') + : no_change(); } ); + source = res.string; + sourcemap_list.unshift(res.map); } for (const fn of style) { - source = await replace_async( + const get_location = getLocator(source); + const res = await replace_async( + filename, source, + get_location, /|([^]*?)<\/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) { - return match; + return no_change(); } + attributes = attributes || ''; + content = content || ''; + + // run style preprocessor const processed: Processed = await fn({ content, attributes: parse_attributes(attributes), filename }); if (processed && processed.dependencies) dependencies.push(...processed.dependencies); - return processed ? `${processed.code}` : match; + return processed + ? get_replacement(filename, offset, get_location, content, processed, ``, '') + : 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 { // TODO return separated output, in future version where svelte.compile supports it: // style: { code: styleCode, map: styleMap }, @@ -138,7 +232,7 @@ export default async function preprocess( code: source, dependencies: [...new Set(dependencies)], - + map: (map as object), toString() { return source; } diff --git a/src/compiler/utils/string_with_sourcemap.ts b/src/compiler/utils/string_with_sourcemap.ts new file mode 100644 index 0000000000..470364e845 --- /dev/null +++ b/src/compiler/utils/string_with_sourcemap.ts @@ -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(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(_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 +): 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()); + } + } + }); +} diff --git a/test/preprocess/index.ts b/test/preprocess/index.ts index 60d3acbabb..be898bbbfd 100644 --- a/test/preprocess/index.ts +++ b/test/preprocess/index.ts @@ -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); diff --git a/test/sourcemaps/index.ts b/test/sourcemaps/index.ts index 7659948744..c657ab8f47 100644 --- a/test/sourcemaps/index.ts +++ b/test/sourcemaps/index.ts @@ -37,7 +37,7 @@ describe('sourcemaps', () => { const preprocessed = await svelte.preprocess( input.code, config.preprocess || {}, - { + config.options || { filename: 'input.svelte' } ); @@ -46,6 +46,7 @@ describe('sourcemaps', () => { preprocessed.code, { filename: 'input.svelte', // filenames for sourcemaps + sourcemap: preprocessed.map, outputFilename: `${outputName}.js`, cssOutputFilename: `${outputName}.css` }); diff --git a/test/sourcemaps/samples/decoded-sourcemap/_config.js b/test/sourcemaps/samples/decoded-sourcemap/_config.js new file mode 100644 index 0000000000..fc4d2a03c9 --- /dev/null +++ b/test/sourcemaps/samples/decoded-sourcemap/_config.js @@ -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); + } + } +}; diff --git a/test/sourcemaps/samples/decoded-sourcemap/input.svelte b/test/sourcemaps/samples/decoded-sourcemap/input.svelte new file mode 100644 index 0000000000..b233d7f670 --- /dev/null +++ b/test/sourcemaps/samples/decoded-sourcemap/input.svelte @@ -0,0 +1,2 @@ +

decoded-sourcemap

+
replace me
diff --git a/test/sourcemaps/samples/decoded-sourcemap/test.js b/test/sourcemaps/samples/decoded-sourcemap/test.js new file mode 100644 index 0000000000..54d930cb97 --- /dev/null +++ b/test/sourcemaps/samples/decoded-sourcemap/test.js @@ -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 + }); + +} diff --git a/test/sourcemaps/samples/detect-lowres-sourcemaps/_config.js b/test/sourcemaps/samples/detect-lowres-sourcemaps/_config.js new file mode 100644 index 0000000000..b11594dd85 --- /dev/null +++ b/test/sourcemaps/samples/detect-lowres-sourcemaps/_config.js @@ -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 }); + } } + ] + +}; diff --git a/test/sourcemaps/samples/detect-lowres-sourcemaps/input.svelte b/test/sourcemaps/samples/detect-lowres-sourcemaps/input.svelte new file mode 100644 index 0000000000..2b3afd881b --- /dev/null +++ b/test/sourcemaps/samples/detect-lowres-sourcemaps/input.svelte @@ -0,0 +1,10 @@ +replace_me +replace_me +replace_me +replace_me +replace_me +replace_me +replace_me +replace_me +replace_me +replace_me diff --git a/test/sourcemaps/samples/detect-lowres-sourcemaps/test.js b/test/sourcemaps/samples/detect-lowres-sourcemaps/test.js new file mode 100644 index 0000000000..0f63efb358 --- /dev/null +++ b/test/sourcemaps/samples/detect-lowres-sourcemaps/test.js @@ -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? + +} diff --git a/test/sourcemaps/samples/preprocessed-markup/_config.js b/test/sourcemaps/samples/preprocessed-markup/_config.js new file mode 100644 index 0000000000..67ed18e9b5 --- /dev/null +++ b/test/sourcemaps/samples/preprocessed-markup/_config.js @@ -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 + }) + }; + } + } +}; diff --git a/test/sourcemaps/samples/preprocessed-markup/input.svelte b/test/sourcemaps/samples/preprocessed-markup/input.svelte new file mode 100644 index 0000000000..ee4b90372a --- /dev/null +++ b/test/sourcemaps/samples/preprocessed-markup/input.svelte @@ -0,0 +1,5 @@ + + +{foo.baritone.baz} diff --git a/test/sourcemaps/samples/preprocessed-markup/test.js b/test/sourcemaps/samples/preprocessed-markup/test.js new file mode 100644 index 0000000000..9c3f0ef06d --- /dev/null +++ b/test/sourcemaps/samples/preprocessed-markup/test.js @@ -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 + }); +} diff --git a/test/sourcemaps/samples/preprocessed-multiple/_config.js b/test/sourcemaps/samples/preprocessed-multiple/_config.js new file mode 100644 index 0000000000..4e7247cca2 --- /dev/null +++ b/test/sourcemaps/samples/preprocessed-multiple/_config.js @@ -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 + }) + }; + } + } +}; diff --git a/test/sourcemaps/samples/preprocessed-multiple/input.svelte b/test/sourcemaps/samples/preprocessed-multiple/input.svelte new file mode 100644 index 0000000000..e656d399ae --- /dev/null +++ b/test/sourcemaps/samples/preprocessed-multiple/input.svelte @@ -0,0 +1,9 @@ + + +

multiple {foo}

diff --git a/test/sourcemaps/samples/preprocessed-multiple/test.js b/test/sourcemaps/samples/preprocessed-multiple/test.js new file mode 100644 index 0000000000..64b2156773 --- /dev/null +++ b/test/sourcemaps/samples/preprocessed-multiple/test.js @@ -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)}\ +`); +} diff --git a/test/sourcemaps/samples/preprocessed-script/_config.js b/test/sourcemaps/samples/preprocessed-script/_config.js new file mode 100644 index 0000000000..da71bf195a --- /dev/null +++ b/test/sourcemaps/samples/preprocessed-script/_config.js @@ -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 + }) + }; + } + } +}; diff --git a/test/sourcemaps/samples/preprocessed-script/input.svelte b/test/sourcemaps/samples/preprocessed-script/input.svelte new file mode 100644 index 0000000000..11586619e1 --- /dev/null +++ b/test/sourcemaps/samples/preprocessed-script/input.svelte @@ -0,0 +1,9 @@ + + +

{foo.bar.baz}

diff --git a/test/sourcemaps/samples/preprocessed-script/test.js b/test/sourcemaps/samples/preprocessed-script/test.js new file mode 100644 index 0000000000..a7e53a96e7 --- /dev/null +++ b/test/sourcemaps/samples/preprocessed-script/test.js @@ -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"); +} diff --git a/test/sourcemaps/samples/preprocessed-styles/_config.js b/test/sourcemaps/samples/preprocessed-styles/_config.js new file mode 100644 index 0000000000..0c2f0ee958 --- /dev/null +++ b/test/sourcemaps/samples/preprocessed-styles/_config.js @@ -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 + }) + }; + } + } +}; diff --git a/test/sourcemaps/samples/preprocessed-styles/input.svelte b/test/sourcemaps/samples/preprocessed-styles/input.svelte new file mode 100644 index 0000000000..0d942390f4 --- /dev/null +++ b/test/sourcemaps/samples/preprocessed-styles/input.svelte @@ -0,0 +1,12 @@ +

Testing Styles

+

Testing Styles 2

+ + diff --git a/test/sourcemaps/samples/preprocessed-styles/test.js b/test/sourcemaps/samples/preprocessed-styles/test.js new file mode 100644 index 0000000000..5b28a12514 --- /dev/null +++ b/test/sourcemaps/samples/preprocessed-styles/test.js @@ -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"); +} diff --git a/test/sourcemaps/samples/sourcemap-names/_config.js b/test/sourcemaps/samples/sourcemap-names/_config.js new file mode 100644 index 0000000000..35c7badb29 --- /dev/null +++ b/test/sourcemaps/samples/sourcemap-names/_config.js @@ -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); + } + } + ] +}; diff --git a/test/sourcemaps/samples/sourcemap-names/input.svelte b/test/sourcemaps/samples/sourcemap-names/input.svelte new file mode 100644 index 0000000000..b62715a857 --- /dev/null +++ b/test/sourcemaps/samples/sourcemap-names/input.svelte @@ -0,0 +1,12 @@ + + +

use-names

+
{old_name_1.baritone}
+
{old_name_2}
diff --git a/test/sourcemaps/samples/sourcemap-names/test.js b/test/sourcemaps/samples/sourcemap-names/test.js new file mode 100644 index 0000000000..85f4b1afdb --- /dev/null +++ b/test/sourcemaps/samples/sourcemap-names/test.js @@ -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); +} diff --git a/test/sourcemaps/samples/sourcemap-sources/_config.js b/test/sourcemaps/samples/sourcemap-sources/_config.js new file mode 100644 index 0000000000..97024cea5f --- /dev/null +++ b/test/sourcemaps/samples/sourcemap-sources/_config.js @@ -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); + } + } + ] +}; diff --git a/test/sourcemaps/samples/sourcemap-sources/input.svelte b/test/sourcemaps/samples/sourcemap-sources/input.svelte new file mode 100644 index 0000000000..33c8a9d9a6 --- /dev/null +++ b/test/sourcemaps/samples/sourcemap-sources/input.svelte @@ -0,0 +1,4 @@ + +

sourcemap-sources

diff --git a/test/sourcemaps/samples/sourcemap-sources/test.js b/test/sourcemaps/samples/sourcemap-sources/test.js new file mode 100644 index 0000000000..78a4c80a17 --- /dev/null +++ b/test/sourcemaps/samples/sourcemap-sources/test.js @@ -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}"` + ); + + }); +}