From a2bef2f7b9fc311b3de92391671b7d20e440010e Mon Sep 17 00:00:00 2001 From: halfnelson Date: Sun, 25 Oct 2020 18:51:00 +1000 Subject: [PATCH 1/8] 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 e04cc22175ee..9c19e27a7e02 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 a0f0b9e455b8..19d1f715d69c 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 d2542c9830f0..340c1cacb7b3 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 1faa33ee1e7f..842539fcde88 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 5249c2fd48b7..689b59529d90 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 1d7d74ceac03..12cc8b00cd3f 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 000000000000..470364e84597 --- /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 60d3acbabb6c..be898bbbfdc0 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 7659948744be..c657ab8f47b2 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 000000000000..fc4d2a03c90e --- /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 000000000000..b233d7f670d7 --- /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 000000000000..54d930cb9714 --- /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 000000000000..b11594dd855f --- /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 000000000000..2b3afd881b3f --- /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 000000000000..0f63efb358d0 --- /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 000000000000..67ed18e9b515 --- /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 000000000000..ee4b90372acd --- /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 000000000000..9c3f0ef06d6c --- /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 000000000000..4e7247cca2f0 --- /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 000000000000..e656d399ae04 --- /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 000000000000..64b215677306 --- /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 000000000000..da71bf195aa2 --- /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 000000000000..11586619e1a0 --- /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 000000000000..a7e53a96e70b --- /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 000000000000..0c2f0ee95836 --- /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 000000000000..0d942390f4b8 --- /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 000000000000..5b28a1251481 --- /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 000000000000..35c7badb296a --- /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 000000000000..b62715a85713 --- /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 000000000000..85f4b1afdba5 --- /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 000000000000..97024cea5f59 --- /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 000000000000..33c8a9d9a66b --- /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 000000000000..78a4c80a1748 --- /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}"` + ); + + }); +} From f0b9a292063e3514bf51002c8c83849ef7c1c4a6 Mon Sep 17 00:00:00 2001 From: halfnelson Date: Mon, 26 Oct 2020 14:56:51 +1000 Subject: [PATCH 2/8] Performance improvements, mild refactoring, and better css map support Co-authored-by: Milan Hauth --- src/compiler/compile/Component.ts | 26 +-- src/compiler/compile/render_dom/index.ts | 5 + src/compiler/utils/string_with_sourcemap.ts | 160 +++++++++++------- test/setup.js | 2 +- test/sourcemaps/index.ts | 5 +- .../samples/compile-option-dev/_config.js | 41 +++++ .../samples/compile-option-dev/input.svelte | 15 ++ .../samples/compile-option-dev/test.js | 40 +++++ 8 files changed, 205 insertions(+), 89 deletions(-) create mode 100644 test/sourcemaps/samples/compile-option-dev/_config.js create mode 100644 test/sourcemaps/samples/compile-option-dev/input.svelte create mode 100644 test/sourcemaps/samples/compile-option-dev/test.js diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index 340c1cacb7b3..6a70190e68e2 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -29,8 +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 { combine_sourcemaps, sourcemap_define_tostring_tourl } from '../utils/string_with_sourcemap'; +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; @@ -332,28 +333,7 @@ export default class Component { 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); - } - } + js.map = apply_preprocessor_sourcemap(this.file, js.map, compile_options.sourcemap as (string | RawSourceMap | DecodedSourceMap)); } return { diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts index 024aafde14a0..4a767dfed120 100644 --- a/src/compiler/compile/render_dom/index.ts +++ b/src/compiler/compile/render_dom/index.ts @@ -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; diff --git a/src/compiler/utils/string_with_sourcemap.ts b/src/compiler/utils/string_with_sourcemap.ts index 470364e84597..845d46ec7df2 100644 --- a/src/compiler/utils/string_with_sourcemap.ts +++ b/src/compiler/utils/string_with_sourcemap.ts @@ -1,5 +1,6 @@ -import { DecodedSourceMap, RawSourceMap, SourceMapSegment, SourceMapLoader } from '@ampproject/remapping/dist/types/types'; +import { DecodedSourceMap, RawSourceMap, SourceMapLoader } from '@ampproject/remapping/dist/types/types'; import remapping from '@ampproject/remapping'; +import { SourceMap } from 'magic-string'; type SourceLocation = { line: number; @@ -14,17 +15,21 @@ function last_line_length(s: string) { export function sourcemap_add_offset( map: DecodedSourceMap, offset: SourceLocation ) { + if (map.mappings.length == 0) return map; // shift columns in first line - const m = map.mappings; - m[0].forEach(seg => { + 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 - m.forEach(line => { - line.forEach(seg => { + 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(this_table: T[], other_table): [T[], number[], boolean, boolean] { @@ -91,6 +96,8 @@ export class StringWithSourcemap { 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); @@ -100,24 +107,30 @@ export class StringWithSourcemap { // unswitched loops are faster if (sources_idx_changed && names_idx_changed) { - m2.mappings.forEach(line => { - line.forEach(seg => { + 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) { - m2.mappings.forEach(line => { - line.forEach(seg => { + 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) { - m2.mappings.forEach(line => { - line.forEach(seg => { + 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 @@ -129,10 +142,10 @@ export class StringWithSourcemap { 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; - }); + 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 @@ -146,38 +159,40 @@ export class StringWithSourcemap { 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: [] }; - 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(_ => []); + 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_in_source?: SourceLocation + source_file: string, source: string, offset?: SourceLocation ): StringWithSourcemap { - const offset = offset_in_source || { line: 0, column: 0 }; + if (!offset) offset = { line: 0, column: 0 }; const map: DecodedSourceMap = { version: 3, names: [], sources: [source_file], mappings: [] }; - if (source.length == 0) return new StringWithSourcemap(source, map); + 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. - 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; - }); + 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); } } @@ -191,34 +206,51 @@ export function combine_sourcemaps( let map_idx = 1; const map: RawSourceMap = sourcemap_list.slice(0, -1) - .find(m => m.sources.length !== 1) === undefined + .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` - ) + // 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 - ); + 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, { +// 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 using the magic-string constructor that takes a decoded map + //we just tack on the extra properties. + Object.defineProperties(result_map, { toString: { enumerable: false, value: function toString() { @@ -228,8 +260,10 @@ export function sourcemap_define_tostring_tourl(map) { toUrl: { enumerable: false, value: function toUrl() { - return 'data:application/json;charset=utf-8;base64,' + btoa(this.toString()); + return 'data:application/json;charset=utf-8;base64,' + b64enc(this.toString()); } } }); + + return result_map as SourceMap; } diff --git a/test/setup.js b/test/setup.js index 7406a07dd9fb..74250c10ebe0 100644 --- a/test/setup.js +++ b/test/setup.js @@ -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}`; }) diff --git a/test/sourcemaps/index.ts b/test/sourcemaps/index.ts index c657ab8f47b2..4122c3a41969 100644 --- a/test/sourcemaps/index.ts +++ b/test/sourcemaps/index.ts @@ -48,7 +48,8 @@ describe('sourcemaps', () => { // filenames for sourcemaps sourcemap: preprocessed.map, outputFilename: `${outputName}.js`, - cssOutputFilename: `${outputName}.css` + cssOutputFilename: `${outputName}.css`, + ...(config.compile_options || {}) }); js.code = js.code.replace( @@ -108,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 }); }); }); }); diff --git a/test/sourcemaps/samples/compile-option-dev/_config.js b/test/sourcemaps/samples/compile-option-dev/_config.js new file mode 100644 index 000000000000..5c638f5a1023 --- /dev/null +++ b/test/sourcemaps/samples/compile-option-dev/_config.js @@ -0,0 +1,41 @@ +import MagicString from 'magic-string'; + +// TODO move util fns to test index.js + +function result(filename, src) { + return { + code: src.toString(), + map: src.generateMap({ + source: filename, + hires: true, + includeContent: false + }) + }; +} + +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); +} + +export default { + compile_options: { + dev: true + }, + preprocess: [ + { style: ({ content, filename }) => { + const src = new MagicString(content); + replace_all(src, '--replace-me-once', '\n --done-replace-once'); + replace_all(src, '--replace-me-twice', '\n--almost-done-replace-twice'); + return result(filename, src); + } }, + { style: ({ content, filename }) => { + const src = new MagicString(content); + replace_all(src, '--almost-done-replace-twice', '\n --done-replace-twice'); + return result(filename, src); + } } + ] +}; diff --git a/test/sourcemaps/samples/compile-option-dev/input.svelte b/test/sourcemaps/samples/compile-option-dev/input.svelte new file mode 100644 index 000000000000..f16bf0d5d857 --- /dev/null +++ b/test/sourcemaps/samples/compile-option-dev/input.svelte @@ -0,0 +1,15 @@ +

Testing Styles

+

Testing Styles 2

+
Testing Styles 3
+ + \ No newline at end of file diff --git a/test/sourcemaps/samples/compile-option-dev/test.js b/test/sourcemaps/samples/compile-option-dev/test.js new file mode 100644 index 000000000000..97a4931597f9 --- /dev/null +++ b/test/sourcemaps/samples/compile-option-dev/test.js @@ -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}"` + ); + }); +} From 07f646a5da2abc8730d08863d1f8c0e314bd6ef7 Mon Sep 17 00:00:00 2001 From: halfnelson Date: Mon, 26 Oct 2020 15:08:15 +1000 Subject: [PATCH 3/8] Remove invalid test --- .../detect-lowres-sourcemaps/_config.js | 54 ------------------- .../detect-lowres-sourcemaps/input.svelte | 10 ---- .../samples/detect-lowres-sourcemaps/test.js | 10 ---- 3 files changed, 74 deletions(-) delete mode 100644 test/sourcemaps/samples/detect-lowres-sourcemaps/_config.js delete mode 100644 test/sourcemaps/samples/detect-lowres-sourcemaps/input.svelte delete mode 100644 test/sourcemaps/samples/detect-lowres-sourcemaps/test.js diff --git a/test/sourcemaps/samples/detect-lowres-sourcemaps/_config.js b/test/sourcemaps/samples/detect-lowres-sourcemaps/_config.js deleted file mode 100644 index b11594dd855f..000000000000 --- a/test/sourcemaps/samples/detect-lowres-sourcemaps/_config.js +++ /dev/null @@ -1,54 +0,0 @@ -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 deleted file mode 100644 index 2b3afd881b3f..000000000000 --- a/test/sourcemaps/samples/detect-lowres-sourcemaps/input.svelte +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 0f63efb358d0..000000000000 --- a/test/sourcemaps/samples/detect-lowres-sourcemaps/test.js +++ /dev/null @@ -1,10 +0,0 @@ -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? - -} From c7f8749b928b6592d9d54021e38660e6df9d10a9 Mon Sep 17 00:00:00 2001 From: halfnelson Date: Mon, 26 Oct 2020 15:41:07 +1000 Subject: [PATCH 4/8] fix typo in src/compiler/preprocess/index.ts Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- src/compiler/preprocess/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/preprocess/index.ts b/src/compiler/preprocess/index.ts index 12cc8b00cd3f..27c1c471c141 100644 --- a/src/compiler/preprocess/index.ts +++ b/src/compiler/preprocess/index.ts @@ -5,7 +5,7 @@ import { StringWithSourcemap, sourcemap_add_offset, combine_sourcemaps } from '. export interface Processed { code: string; - map?: string | object; // we be opaque with the type here to avoid dependency on the remapping module for our public types. + map?: string | object; // we are opaque with the type here to avoid dependency on the remapping module for our public types. dependencies?: string[]; } From 9583d72c9279c0ca4ea38fbb8b6445b8ecfa0e6f Mon Sep 17 00:00:00 2001 From: halfnelson Date: Thu, 29 Oct 2020 22:29:14 +1000 Subject: [PATCH 5/8] Extract test helpers in sourcemap tests --- test/sourcemaps/helpers.ts | 20 +++++++++ .../samples/compile-option-dev/_config.js | 32 +++------------ .../samples/compile-option-dev/test.js | 2 +- .../samples/decoded-sourcemap/_config.js | 23 ++--------- .../samples/decoded-sourcemap/test.js | 2 +- .../samples/preprocessed-markup/_config.js | 12 ++---- .../samples/preprocessed-markup/test.js | 2 +- .../samples/preprocessed-multiple/_config.js | 35 +++------------- .../samples/preprocessed-multiple/test.js | 11 ++--- .../samples/preprocessed-script/_config.js | 13 ++---- .../samples/preprocessed-script/test.js | 2 +- .../samples/preprocessed-styles/_config.js | 13 ++---- .../samples/sourcemap-names/_config.js | 41 ++++++------------- .../samples/sourcemap-names/test.js | 3 +- 14 files changed, 64 insertions(+), 147 deletions(-) create mode 100644 test/sourcemaps/helpers.ts diff --git a/test/sourcemaps/helpers.ts b/test/sourcemaps/helpers.ts new file mode 100644 index 000000000000..d0bea310e623 --- /dev/null +++ b/test/sourcemaps/helpers.ts @@ -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); +} diff --git a/test/sourcemaps/samples/compile-option-dev/_config.js b/test/sourcemaps/samples/compile-option-dev/_config.js index 5c638f5a1023..b6ea851b6dd6 100644 --- a/test/sourcemaps/samples/compile-option-dev/_config.js +++ b/test/sourcemaps/samples/compile-option-dev/_config.js @@ -1,25 +1,5 @@ import MagicString from 'magic-string'; - -// TODO move util fns to test index.js - -function result(filename, src) { - return { - code: src.toString(), - map: src.generateMap({ - source: filename, - hires: true, - includeContent: false - }) - }; -} - -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); -} +import { magic_string_preprocessor_result, magic_string_replace_all } from '../../helpers'; export default { compile_options: { @@ -28,14 +8,14 @@ export default { preprocess: [ { style: ({ content, filename }) => { const src = new MagicString(content); - replace_all(src, '--replace-me-once', '\n --done-replace-once'); - replace_all(src, '--replace-me-twice', '\n--almost-done-replace-twice'); - return result(filename, src); + 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); - replace_all(src, '--almost-done-replace-twice', '\n --done-replace-twice'); - return result(filename, src); + magic_string_replace_all(src, '--almost-done-replace-twice', '\n --done-replace-twice'); + return magic_string_preprocessor_result(filename, src); } } ] }; diff --git a/test/sourcemaps/samples/compile-option-dev/test.js b/test/sourcemaps/samples/compile-option-dev/test.js index 97a4931597f9..bf240a5a8927 100644 --- a/test/sourcemaps/samples/compile-option-dev/test.js +++ b/test/sourcemaps/samples/compile-option-dev/test.js @@ -4,7 +4,7 @@ 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 + // 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); diff --git a/test/sourcemaps/samples/decoded-sourcemap/_config.js b/test/sourcemaps/samples/decoded-sourcemap/_config.js index fc4d2a03c90e..dd4eee9461e0 100644 --- a/test/sourcemaps/samples/decoded-sourcemap/_config.js +++ b/test/sourcemaps/samples/decoded-sourcemap/_config.js @@ -1,22 +1,5 @@ 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 - }) - }; -} +import { magic_string_preprocessor_result, magic_string_replace_all } from '../../helpers'; export default { @@ -25,8 +8,8 @@ export default { preprocess: { markup: ({ content, filename }) => { const src = new MagicString(content); - replace('replace me', 'success', content, src); - return result(src, filename); + magic_string_replace_all(src, 'replace me', 'success'); + return magic_string_preprocessor_result(filename, src); } } }; diff --git a/test/sourcemaps/samples/decoded-sourcemap/test.js b/test/sourcemaps/samples/decoded-sourcemap/test.js index 54d930cb9714..5a44ee83cf7b 100644 --- a/test/sourcemaps/samples/decoded-sourcemap/test.js +++ b/test/sourcemaps/samples/decoded-sourcemap/test.js @@ -11,7 +11,7 @@ export function test({ assert, input, preprocessed }) { assert.deepEqual(actualbar, { source: 'input.svelte', - name: null, + name: 'replace me', line: expected.line + 1, column: expected.column }); diff --git a/test/sourcemaps/samples/preprocessed-markup/_config.js b/test/sourcemaps/samples/preprocessed-markup/_config.js index 67ed18e9b515..0b2baeb49c05 100644 --- a/test/sourcemaps/samples/preprocessed-markup/_config.js +++ b/test/sourcemaps/samples/preprocessed-markup/_config.js @@ -1,18 +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); - const idx = content.indexOf('baritone'); - src.overwrite(idx, idx+'baritone'.length, 'bar'); - return { - code: src.toString(), - map: src.generateDecodedMap({ - source: filename, - includeContent: false - }) - }; + magic_string_replace_all(src, 'baritone', 'bar'); + return magic_string_preprocessor_result(filename, src); } } }; diff --git a/test/sourcemaps/samples/preprocessed-markup/test.js b/test/sourcemaps/samples/preprocessed-markup/test.js index 9c3f0ef06d6c..b58710396917 100644 --- a/test/sourcemaps/samples/preprocessed-markup/test.js +++ b/test/sourcemaps/samples/preprocessed-markup/test.js @@ -11,7 +11,7 @@ export function test({ assert, input, js }) { assert.deepEqual(actualbar, { source: 'input.svelte', - name: null, + name: 'baritone', line: expectedBar.line + 1, column: expectedBar.column }); diff --git a/test/sourcemaps/samples/preprocessed-multiple/_config.js b/test/sourcemaps/samples/preprocessed-multiple/_config.js index 4e7247cca2f0..39259a02a8e3 100644 --- a/test/sourcemaps/samples/preprocessed-multiple/_config.js +++ b/test/sourcemaps/samples/preprocessed-multiple/_config.js @@ -1,48 +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); - 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 - }) - }; + 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 { - code: src.toString(), - map: src.generateDecodedMap({ - source: filename, - hires: true, - includeContent: false - }) - }; + return magic_string_preprocessor_result(filename, src); }, 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 - }) - }; + return magic_string_preprocessor_result(filename, src); } } }; diff --git a/test/sourcemaps/samples/preprocessed-multiple/test.js b/test/sourcemaps/samples/preprocessed-multiple/test.js index 64b215677306..a0cfe1fa8a4f 100644 --- a/test/sourcemaps/samples/preprocessed-multiple/test.js +++ b/test/sourcemaps/samples/preprocessed-multiple/test.js @@ -11,7 +11,7 @@ export function test({ assert, input, js, css }) { assert.deepEqual(actualbar, { source: 'input.svelte', - name: null, + name: 'baritone', line: expectedBar.line + 1, column: expectedBar.column }); @@ -25,13 +25,8 @@ export function test({ assert, input, js, css }) { assert.deepEqual(actualbaz, { source: 'input.svelte', - name: null, + name: '--bazitone', 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 index da71bf195aa2..94bd83c22ff6 100644 --- a/test/sourcemaps/samples/preprocessed-script/_config.js +++ b/test/sourcemaps/samples/preprocessed-script/_config.js @@ -1,19 +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); - 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 - }) - }; + magic_string_replace_all(src, 'baritone', 'bar'); + return magic_string_preprocessor_result(filename, src); } } }; diff --git a/test/sourcemaps/samples/preprocessed-script/test.js b/test/sourcemaps/samples/preprocessed-script/test.js index a7e53a96e70b..20a366f6d290 100644 --- a/test/sourcemaps/samples/preprocessed-script/test.js +++ b/test/sourcemaps/samples/preprocessed-script/test.js @@ -11,7 +11,7 @@ export function test({ assert, input, js }) { assert.deepEqual(actualbar, { source: 'input.svelte', - name: null, + name: 'baritone', line: expectedBar.line + 1, column: expectedBar.column }, "couldn't find bar: in source"); diff --git a/test/sourcemaps/samples/preprocessed-styles/_config.js b/test/sourcemaps/samples/preprocessed-styles/_config.js index 0c2f0ee95836..04c8bcda7a54 100644 --- a/test/sourcemaps/samples/preprocessed-styles/_config.js +++ b/test/sourcemaps/samples/preprocessed-styles/_config.js @@ -1,19 +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); - 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 - }) - }; + magic_string_replace_all(src, 'baritone', 'bar'); + return magic_string_preprocessor_result(filename, src); } } }; diff --git a/test/sourcemaps/samples/sourcemap-names/_config.js b/test/sourcemaps/samples/sourcemap-names/_config.js index 35c7badb296a..c8557ba46528 100644 --- a/test/sourcemaps/samples/sourcemap-names/_config.js +++ b/test/sourcemaps/samples/sourcemap-names/_config.js @@ -1,49 +1,32 @@ 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 - }) - }; -} +import { magic_string_preprocessor_result, magic_string_replace_all } from '../../helpers'; 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); + 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); - 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); + 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); - 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); + 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); } } ] diff --git a/test/sourcemaps/samples/sourcemap-names/test.js b/test/sourcemaps/samples/sourcemap-names/test.js index 85f4b1afdba5..cd70bd25ce90 100644 --- a/test/sourcemaps/samples/sourcemap-names/test.js +++ b/test/sourcemaps/samples/sourcemap-names/test.js @@ -1,14 +1,13 @@ // needed for workaround, TODO remove import { getLocator } from 'locate-character'; -export function test({ assert, input, preprocessed, js, css }) { +export function test({ assert, 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 }; From 713827126a98a7f017049084fdca129e4660ec7f Mon Sep 17 00:00:00 2001 From: halfnelson Date: Thu, 29 Oct 2020 22:38:46 +1000 Subject: [PATCH 6/8] rebase and fix lint --- src/compiler/preprocess/index.ts | 6 ++++-- src/compiler/utils/string_with_sourcemap.ts | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/compiler/preprocess/index.ts b/src/compiler/preprocess/index.ts index 27c1c471c141..3e2bf99c7c5d 100644 --- a/src/compiler/preprocess/index.ts +++ b/src/compiler/preprocess/index.ts @@ -104,8 +104,9 @@ function get_replacement( 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') + 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); @@ -146,12 +147,13 @@ export default async function preprocess( if (processed && processed.dependencies) dependencies.push(...processed.dependencies); source = processed ? processed.code : source; - if (processed && processed.map) + if (processed && processed.map) { sourcemap_list.unshift( typeof(processed.map) === 'string' ? JSON.parse(processed.map) : processed.map ); + } } for (const fn of script) { diff --git a/src/compiler/utils/string_with_sourcemap.ts b/src/compiler/utils/string_with_sourcemap.ts index 845d46ec7df2..ebd44221e19f 100644 --- a/src/compiler/utils/string_with_sourcemap.ts +++ b/src/compiler/utils/string_with_sourcemap.ts @@ -59,8 +59,9 @@ function merge_tables(this_table: T[], other_table): [T[], number[], boolean, } function pushArray(_this: T[], other: T[]) { - for (let i = 0; i < other.length; i++) + for (let i = 0; i < other.length; i++) { _this.push(other[i]); + } } export class StringWithSourcemap { @@ -69,15 +70,16 @@ export class StringWithSourcemap { constructor(string = '', map = null) { this.string = string; - if (map) + if (map) { this.map = map as DecodedSourceMap; - else + } else { this.map = { version: 3, mappings: [], sources: [], names: [] }; + } } // concat in-place (mutable), return this (chainable) From 21f73389ff69d1899600a001eca9af436495cffb Mon Sep 17 00:00:00 2001 From: halfnelson Date: Sat, 7 Nov 2020 11:36:49 +1000 Subject: [PATCH 7/8] Extract tag preprocessor method Tidy code for consistency --- src/compiler/preprocess/index.ts | 78 ++++++++----------- src/compiler/utils/string_with_sourcemap.ts | 14 ++-- .../samples/compile-option-dev/input.svelte | 2 +- 3 files changed, 41 insertions(+), 53 deletions(-) diff --git a/src/compiler/preprocess/index.ts b/src/compiler/preprocess/index.ts index 3e2bf99c7c5d..5f3605b04b61 100644 --- a/src/compiler/preprocess/index.ts +++ b/src/compiler/preprocess/index.ts @@ -83,7 +83,9 @@ async function replace_async( return out.concat(final_content); } -// Convert a preprocessor output and its leading prefix and trailing suffix into StringWithSourceMap +/** + * Convert a preprocessor output and its leading prefix and trailing suffix into StringWithSourceMap + */ function get_replacement( filename: string, offset: number, @@ -124,7 +126,9 @@ 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); @@ -145,24 +149,30 @@ export default async function preprocess( 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 - ); + if (processed) { + 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) { + async function preprocess_tag_content(tag_name: 'style' | 'script', preprocessor: Preprocessor) { const get_location = getLocator(source); + const tag_regex = tag_name == 'style' + ? /|([^]*?)<\/style>|\/>)/gi + : /|([^]*?)<\/script>|\/>)/gi; + const res = await replace_async( filename, source, get_location, - /|([^]*?)<\/script>|\/>)/gi, + tag_regex, async (match, attributes = '', content = '', offset) => { const no_change = () => StringWithSourcemap.from_source( filename, match, get_location(offset)); @@ -173,51 +183,27 @@ export default async function preprocess( content = content || ''; // run script preprocessor - const processed = await fn({ + const processed = await preprocessor({ content, attributes: parse_attributes(attributes), filename }); - if (processed && processed.dependencies) dependencies.push(...processed.dependencies); - return processed - ? get_replacement(filename, offset, get_location, content, processed, ``, '') - : no_change(); + + if (!processed) return no_change(); + if (processed.dependencies) dependencies.push(...processed.dependencies); + return get_replacement(filename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, ``); } ); source = res.string; sourcemap_list.unshift(res.map); } - for (const fn of style) { - const get_location = getLocator(source); - const res = await replace_async( - filename, - source, - get_location, - /|([^]*?)<\/style>|\/>)/gi, - async (match, attributes = '', content = '', offset) => { - const no_change = () => StringWithSourcemap.from_source( - filename, match, get_location(offset)); - if (!attributes && !content) { - return no_change(); - } - attributes = attributes || ''; - content = content || ''; + for (const fn of script) { + await preprocess_tag_content('script', fn); + } - // run style preprocessor - const processed: Processed = await fn({ - content, - attributes: parse_attributes(attributes), - filename - }); - if (processed && processed.dependencies) dependencies.push(...processed.dependencies); - 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) { + await preprocess_tag_content('style', fn); } // Combine all the source maps for each preprocessor function into one diff --git a/src/compiler/utils/string_with_sourcemap.ts b/src/compiler/utils/string_with_sourcemap.ts index ebd44221e19f..7fb38323d46a 100644 --- a/src/compiler/utils/string_with_sourcemap.ts +++ b/src/compiler/utils/string_with_sourcemap.ts @@ -32,7 +32,7 @@ export function sourcemap_add_offset( } } -function merge_tables(this_table: T[], other_table): [T[], number[], boolean, boolean] { +function merge_tables(this_table: T[], other_table: T[]): [T[], number[], boolean, boolean] { const new_table = this_table.slice(); const idx_map = []; other_table = other_table || []; @@ -68,7 +68,7 @@ export class StringWithSourcemap { string: string; map: DecodedSourceMap; - constructor(string = '', map = null) { + constructor(string = '', map: DecodedSourceMap = null) { this.string = string; if (map) { this.map = map as DecodedSourceMap; @@ -82,8 +82,10 @@ export class StringWithSourcemap { } } - // concat in-place (mutable), return this (chainable) - // will also mutate the `other` object + /** + * 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; @@ -250,8 +252,8 @@ export function apply_preprocessor_sourcemap(filename: string, svelte_map: Sourc ] ) as RawSourceMap; - //Svelte expects a SourceMap which includes toUrl and toString. Instead of using the magic-string constructor that takes a decoded map - //we just tack on the extra properties. + // 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, diff --git a/test/sourcemaps/samples/compile-option-dev/input.svelte b/test/sourcemaps/samples/compile-option-dev/input.svelte index f16bf0d5d857..6d5f91158dbe 100644 --- a/test/sourcemaps/samples/compile-option-dev/input.svelte +++ b/test/sourcemaps/samples/compile-option-dev/input.svelte @@ -12,4 +12,4 @@ div { --keep-me: blue; } - \ No newline at end of file + From b07e99c351c696154bf639d3c4c40c5f430ed01c Mon Sep 17 00:00:00 2001 From: halfnelson Date: Sat, 7 Nov 2020 15:53:27 +1000 Subject: [PATCH 8/8] add comments to explain perf sensitive code --- src/compiler/preprocess/index.ts | 20 ++++++++++---------- src/compiler/utils/string_with_sourcemap.ts | 2 ++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/compiler/preprocess/index.ts b/src/compiler/preprocess/index.ts index 5f3605b04b61..1de41cf9bf2f 100644 --- a/src/compiler/preprocess/index.ts +++ b/src/compiler/preprocess/index.ts @@ -149,16 +149,16 @@ export default async function preprocess( filename }); - if (processed) { - 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 - ); - } + 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 + ); } } diff --git a/src/compiler/utils/string_with_sourcemap.ts b/src/compiler/utils/string_with_sourcemap.ts index 7fb38323d46a..7f8a0ec1eba0 100644 --- a/src/compiler/utils/string_with_sourcemap.ts +++ b/src/compiler/utils/string_with_sourcemap.ts @@ -59,6 +59,8 @@ function merge_tables(this_table: T[], other_table: T[]): [T[], number[], boo } function pushArray(_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]); }