diff --git a/.gitignore b/.gitignore index f7fac04ebaa9..a471c3aaa29c 100644 --- a/.gitignore +++ b/.gitignore @@ -18,10 +18,6 @@ node_modules /coverage/ /coverage.lcov /test/*/samples/_ -/test/sourcemaps/samples/*/output.js -/test/sourcemaps/samples/*/output.js.map -/test/sourcemaps/samples/*/output.css -/test/sourcemaps/samples/*/output.css.map /yarn-error.log _actual*.* _output diff --git a/package-lock.json b/package-lock.json index 3ddddec68377..e87e2c0b2341 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 62b15efb5fab..311c484d5309 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 6c1fe898232f..d3dd8d0a0bf2 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -29,7 +29,11 @@ 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_add_tostring_tourl, combine_sourcemaps_map_stats } from '../utils/string_with_sourcemap'; import Element from './nodes/Element'; +import { RawSourceMap } from '@ampproject/remapping/dist/types/types'; +import { encode as encode_mappings, decode as decode_mappings } from 'sourcemap-codec'; + interface ComponentOptions { namespace?: string; @@ -317,12 +321,19 @@ export default class Component { css = compile_options.customElement ? { code: null, map: null } - : result.css; + : result.css; // css.map.mappings are decoded js = print(program, { sourceMapSource: compile_options.filename }); + // TODO remove workaround + // js.map.mappings should be decoded + // https://github.com/Rich-Harris/code-red/issues/50 + if (js.map && typeof (js.map as any).mappings == 'string') { + (js.map as any).mappings = decode_mappings((js.map as any).mappings); + } + js.map.sources = [ compile_options.filename ? get_relative_path(compile_options.outputFilename || '', compile_options.filename) : null ]; @@ -330,6 +341,60 @@ export default class Component { js.map.sourcesContent = [ this.source ]; + + // combine sourcemaps + const map_stats: combine_sourcemaps_map_stats = { + sourcemapWarnLoss: 0, // segment loss is usually high, so we ignore + sourcemapEncodedWarn: true // TODO config + // property `result` is set by combine_sourcemaps + }; + + 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 + ], + map_stats + ) as RawSourceMap; + sourcemap_add_tostring_tourl(js.map); + if (map_stats.result && map_stats.result.maps_encoded && map_stats.result.maps_encoded.length > 0) { + console.log('warning. svelte.compile received encoded script sourcemaps (index '+ + map_stats.result.maps_encoded.join(', ')+'). '+ + 'this is slow. make your sourcemap-generators return decoded mappings '+ + 'or disable this warning with svelte.compile(_, _, { sourcemapEncodedWarn: false })' + ); + } + } + if (css.map) { + css.map = combine_sourcemaps( + this.file, + [ + css.map, // idx 1: internal + compile_options.sourcemap // idx 0: external: svelte.preprocess, etc + ] + ) as RawSourceMap; + sourcemap_add_tostring_tourl(css.map); + if (map_stats.result && map_stats.result.maps_encoded && map_stats.result.maps_encoded.length > 0) { + console.log('warning. svelte.compile received encoded style sourcemaps (index '+ + map_stats.result.maps_encoded.join(', ')+'). '+ + 'this is slow. make your sourcemap-generators return decoded mappings '+ + 'or disable this warning with svelte.compile(_, _, { sourcemapEncodedWarn: false })' + ); + } + } + } + + // encode mappings only once, after all sourcemaps are combined + if (js.map && typeof(js.map.mappings) == 'object') { + (js.map as RawSourceMap).mappings = encode_mappings(js.map.mappings); + } + if (css.map && typeof(css.map.mappings) == 'object') { + (css.map as RawSourceMap).mappings = encode_mappings(css.map.mappings); + } + } return { diff --git a/src/compiler/compile/css/Stylesheet.ts b/src/compiler/compile/css/Stylesheet.ts index dc464d7df806..9d18d642749e 100644 --- a/src/compiler/compile/css/Stylesheet.ts +++ b/src/compiler/compile/css/Stylesheet.ts @@ -410,7 +410,7 @@ export default class Stylesheet { return { code: code.toString(), - map: code.generateMap({ + map: code.generateDecodedMap({ includeContent: true, source: this.filename, file diff --git a/src/compiler/compile/index.ts b/src/compiler/compile/index.ts index 9d56dfae0252..96a67e2a8d15 100644 --- a/src/compiler/compile/index.ts +++ b/src/compiler/compile/index.ts @@ -12,6 +12,7 @@ const valid_options = [ 'format', 'name', 'filename', + 'sourcemap', 'generate', 'outputFilename', 'cssOutputFilename', diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts index ab4ee904e8a8..523141b57b6b 100644 --- a/src/compiler/compile/render_dom/index.ts +++ b/src/compiler/compile/render_dom/index.ts @@ -30,8 +30,12 @@ export default function dom( } const css = component.stylesheet.render(options.filename, !options.customElement); + + // TODO fix css.map.toUrl - stylesheet.render returns decoded mappings, map.toUrl needs encoded mappings + // TODO use combined css.map? see compile/Component.ts const styles = component.stylesheet.has_styles && options.dev - ? `${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */` + //? `${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */` + ? `${css.code}\n/*# sourceMappingURL=TODO_FIXME */` : css.code; const add_css = component.get_unique_name('add_css'); @@ -467,12 +471,15 @@ export default function dom( } if (options.customElement) { + // TODO use combined css.map? see compile/Component.ts + // TODO css.map.toUrl needs encoded mappings + // ${css.code && b`this.shadowRoot.innerHTML = \`\`;`} const declaration = b` class ${name} extends @SvelteElement { constructor(options) { super(); - ${css.code && b`this.shadowRoot.innerHTML = \`\`;`} + ${css.code && b`this.shadowRoot.innerHTML = \`\`;`} @init(this, { target: this.shadowRoot }, ${definition}, ${has_create_fragment ? 'create_fragment': 'null'}, ${not_equal}, ${prop_indexes}, ${dirty}); diff --git a/src/compiler/compile/render_ssr/index.ts b/src/compiler/compile/render_ssr/index.ts index ff45abd78b05..62e2d1831476 100644 --- a/src/compiler/compile/render_ssr/index.ts +++ b/src/compiler/compile/render_ssr/index.ts @@ -137,6 +137,7 @@ export default function ssr( main ].filter(Boolean); + // TODO use combined css.map? see compile/Component.ts const js = b` ${css.code ? b` const #css = { diff --git a/src/compiler/interfaces.ts b/src/compiler/interfaces.ts index 6e96e340adee..263f25d7d759 100644 --- a/src/compiler/interfaces.ts +++ b/src/compiler/interfaces.ts @@ -1,5 +1,7 @@ import { Node, Program } from "estree"; -import { SourceMap } from 'magic-string'; + +// eslint-disable-next-line import/named +import { DecodedSourceMap } from 'magic-string'; interface BaseNode { start: number; @@ -110,6 +112,7 @@ export interface CompileOptions { filename?: string; generate?: 'dom' | 'ssr' | false; + sourcemap?: object | string; outputFilename?: string; cssOutputFilename?: string; sveltePath?: string; @@ -165,5 +168,5 @@ export interface Var { export interface CssResult { code: string; - map: SourceMap; + map: DecodedSourceMap; } diff --git a/src/compiler/preprocess/index.ts b/src/compiler/preprocess/index.ts index 5371baf4b3cd..30f4d08f1b23 100644 --- a/src/compiler/preprocess/index.ts +++ b/src/compiler/preprocess/index.ts @@ -1,6 +1,11 @@ +import { SourceMapInput, 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, combine_sourcemaps_map_stats } from '../utils/string_with_sourcemap'; + export interface Processed { code: string; - map?: object | string; + map?: SourceMapInput; 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,79 +66,200 @@ 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 = source before replacement + 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 = source after last replacement + const final_content = StringWithSourcemap.from_source( + filename, source.slice(last_end), get_location(last_end)); + return out.concat(final_content); +} + +function get_replacement( + filename: string, + offset: number, + get_location: ReturnType, + original: string, + processed: Processed, + prefix: string, + suffix: string +): 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)); + + let decoded_map; + 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); + + return prefix_with_map.concat(processed_with_map).concat(suffix_with_map); } export default async function preprocess( source: string, preprocessor: PreprocessorGroup | PreprocessorGroup[], - options?: { filename?: string } + options?: { + filename?: string, + sourcemapWarnLoss?: number, // default 0.5 + sourcemapEncodedWarn?: boolean // default true + } ) { // @ts-ignore todo: doublecheck const filename = (options && options.filename) || preprocessor.filename; // legacy + const sourcemapWarnLoss = (options && options.sourcemapWarnLoss != undefined) ? options.sourcemapWarnLoss : 0.5; + const sourcemapEncodedWarn = (options && options.sourcemapEncodedWarn != undefined) ? options.sourcemapEncodedWarn : true; + const dependencies = []; - const preprocessors = Array.isArray(preprocessor) ? preprocessor : [preprocessor]; + const preprocessors = preprocessor + ? Array.isArray(preprocessor) ? preprocessor : [preprocessor] + : []; // noop 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) as RawSourceMap + : processed.map as (RawSourceMap | DecodedSourceMap) + ); } + // TODO run script and style in parallel + 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); + } + + const map_stats: combine_sourcemaps_map_stats = { + sourcemapWarnLoss, + sourcemapEncodedWarn + // property `result` is set by combine_sourcemaps + }; + + const map: DecodedSourceMap = combine_sourcemaps( + filename, + sourcemap_list, + map_stats, + true // explicitly decode mappings + // TODO remove this, when `remapping` allows to return decoded mappings, so we skip the unnecessary encode + decode steps + ) as DecodedSourceMap; + + // TODO better than console.log? + + if (map_stats.result && map_stats.result.segments_lost) { + const { segment_loss_per_map, segments_per_map } = map_stats.result; + console.log('warning. svelte.preprocess seems to receive low-resolution sourcemaps. '+ + 'relative segment loss per combine_sourcemaps step: '+ + segment_loss_per_map.map(f => f.toFixed(2)).join(' -> ')+ + '. absolute number of segments per sourcemap: '+ + segments_per_map.join(' -> ')+ + '. make your preprocessors return high-resolution sourcemaps '+ + 'or increase the tolerated loss with svelte.preprocess(_, _, { sourcemapWarnLoss: 0.8 })' + ); + } + + if (map_stats.result && map_stats.result.maps_encoded && map_stats.result.maps_encoded.length > 0) { + console.log('warning. svelte.preprocess received encoded sourcemaps (index '+ + map_stats.result.maps_encoded.join(', ')+'). '+ + 'this is slow. make your sourcemap-generators return decoded mappings '+ + 'or disable this warning with svelte.preprocess(_, _, { sourcemapEncodedWarn: false })' + ); } return { @@ -138,6 +270,7 @@ export default async function preprocess( code: source, dependencies: [...new Set(dependencies)], + map, 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..0e0843609241 --- /dev/null +++ b/src/compiler/utils/string_with_sourcemap.ts @@ -0,0 +1,333 @@ +import { DecodedSourceMap, RawSourceMap, SourceMapSegment, SourceMapLoader } from '@ampproject/remapping/dist/types/types'; +import remapping from '@ampproject/remapping'; +import { decode as decode_mappings } from 'sourcemap-codec'; + +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 type combine_sourcemaps_map_stats = { + sourcemapEncodedWarn?: boolean, + sourcemapWarnLoss?: number, + result?: { + maps_encoded?: number[], + segments_lost?: boolean + segment_loss_per_map?: number[], + segments_per_map?: number[], + } +}; + +export function combine_sourcemaps( + filename: string, + sourcemap_list: Array, + map_stats?: combine_sourcemaps_map_stats, + do_decode_mappings?: boolean +): (RawSourceMap | DecodedSourceMap) { + if (sourcemap_list.length == 0) return null; + + if (map_stats) { + map_stats.result = {}; + const { result } = map_stats; + + const last_map_idx = sourcemap_list.length - 1; + + // TODO allow to set options per preprocessor -> extend preprocessor config object + // some sourcemap-generators produce ultra-high-resolution sourcemaps (1 token = 1 character), so a high segment loss can be tolerable + + // sourcemapEncodedWarn: show warning + // if preprocessors return sourcemaps with encoded mappings + // we need decoded mappings, so that is a waste of time + + if (map_stats.sourcemapEncodedWarn) { + result.maps_encoded = []; + + for (let map_idx = last_map_idx; map_idx >= 0; map_idx--) { + const map = sourcemap_list[map_idx]; + if (typeof(map) == 'string') { + sourcemap_list[map_idx] = JSON.parse(map); + } + if (typeof(map.mappings) == 'string') { + result.maps_encoded.push(last_map_idx - map_idx); // chronological index + } + } + } + + // sourcemapWarnLoss: show warning if source files were lost + // disable warning with `svelte.preprocess(_, _, { sourcemapWarnLoss: false })` + // value 1 : never warn + // value 0.8: seldom warn + // value 0.5: average warn + // value 0.2: often warn + // value 0 : nonsense -> never warn + // -Infinity <= loss <= 1 and 0 < sourcemapWarnLoss <= 1 + + if (map_stats.sourcemapWarnLoss) { + + // guess if segments were lost because of lowres sourcemaps + // assert: typeof(result) == 'object' + result.segments_per_map = []; + result.segment_loss_per_map = []; + result.segments_lost = false; + + let last_num_segments; + + for (let map_idx = last_map_idx; map_idx >= 0; map_idx--) { + + const map = sourcemap_list[map_idx]; + if (typeof(map) == 'string') { + sourcemap_list[map_idx] = JSON.parse(map); + } + if (typeof(map.mappings) == 'string') { + // do this before remapping to avoid double decoding + // remapping does not mutate its input data + map.mappings = decode_mappings(map.mappings); + } + let num_segments = 0; + for (const line of map.mappings) { + num_segments += line.length; + } + // get relative loss, compared to last map + const loss = map_idx == last_map_idx + ? 0 : (last_num_segments - num_segments) / last_num_segments; + if (loss > map_stats.sourcemapWarnLoss) { + result.segments_lost = true; + } + + // chronological index + result.segment_loss_per_map.push(loss); + result.segments_per_map.push(num_segments); + + last_num_segments = num_segments; + } + } + + } + + 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` + + if (do_decode_mappings) { + // explicitly decode mappings + // TODO remove this, when `remapping` allows to return decoded mappings, so we skip the unnecessary encode + decode steps + (map as unknown as DecodedSourceMap).mappings = decode_mappings(map.mappings); + } + + return map; +} + +export function sourcemap_add_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.js b/test/preprocess/index.js index 5d83bb60590d..09ab03fbbd6c 100644 --- a/test/preprocess/index.js +++ b/test/preprocess/index.js @@ -8,17 +8,25 @@ describe('preprocess', () => { const config = loadConfig(`${__dirname}/samples/${dir}/_config.js`); const solo = config.solo || /\.solo/.test(dir); + const skip = config.skip || /\.skip/.test(dir); if (solo && process.env.CI) { throw new Error('Forgot to remove `solo: true` from test'); } - (config.skip ? it.skip : solo ? it.only : it)(dir, async () => { + (skip ? it.skip : solo ? it.only : it)(dir, async () => { const input = fs.readFileSync(`${__dirname}/samples/${dir}/input.svelte`, 'utf-8'); const expected = fs.readFileSync(`${__dirname}/samples/${dir}/output.svelte`, 'utf-8'); - const result = await svelte.preprocess(input, config.preprocess); + const result = await svelte.preprocess( + input, + config.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/preprocess/samples/filename/_config.js b/test/preprocess/samples/filename/_config.js index c71cdafcac9a..1563ac71fd7a 100644 --- a/test/preprocess/samples/filename/_config.js +++ b/test/preprocess/samples/filename/_config.js @@ -1,6 +1,7 @@ export default { preprocess: { - filename: 'file.svelte', + // this is ignored cos filename is set in options + //filename: 'file.svelte', markup: ({ content, filename }) => { return { code: content.replace('__MARKUP_FILENAME__', filename) @@ -16,5 +17,10 @@ export default { code: content.replace('__SCRIPT_FILENAME__', filename) }; } + }, + options: { + // options.filename is preferred over preprocessor.filename + // see function preprocess + filename: 'file.svelte' } -}; \ No newline at end of file +}; diff --git a/test/setup.js b/test/setup.js index 7406a07dd9fb..14e0366387b1 100644 --- a/test/setup.js +++ b/test/setup.js @@ -8,22 +8,28 @@ require.extensions['.js'] = function(module, filename) { const exports = []; let code = fs.readFileSync(filename, 'utf-8') - .replace(/^import \* as (\w+) from ['"]([^'"]+)['"];?/gm, 'var $1 = require("$2");') - .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(/^import\s+\*\s+as\s+(\w+)\s+from\s+(['"])(.+?)\2;?/gm, 'var $1 = require($2$3$2);') + .replace(/^import\s+(\w+)\s+from\s+(['"])(.+?)\2;?/gm, 'var {default: $1} = require($2$3$2);') + .replace(/^import\s+(?:(\w+)\s*,\s*)?{([^}]+)}(?:\s*,\s*(\w+))?\s+from\s+(['"])(.+?)\4;?/gm, + (match, default_name_1, names, default_name_2, quote, source) => { + names = names.replace(/\s+as\s+/g, ': '); + let default_name = default_name_1 || default_name_2; + default_name = default_name ? `default: ${default_name}, ` : ''; + return `var {${default_name}${names}} = require(${quote}${source}${quote});`; + }) + .replace(/^export\s+default\s+/gm, 'exports.default = ') + .replace(/^export\s+(const|let|var|class|function)\s+(\w+)/gm, (match, type, name) => { exports.push(name); return `${type} ${name}`; }) - .replace(/^export \{([^}]+)\}(?: from ['"]([^'"]+)['"];?)?/gm, (match, names, source) => { + .replace(/^export\s+\{([^}]+)\}(?:\s+from\s+(['"])(.+?)\2;?)?/gm, (match, names, quote, source) => { names.split(',').filter(Boolean).forEach(name => { exports.push(name); }); - return source ? `const { ${names} } = require("${source}");` : ''; + return source ? `const { ${names} } = require(${quote}${source}${quote});` : ''; }) - .replace(/^export function (\w+)/gm, 'exports.$1 = function $1'); + .replace(/^export\s+function\s+(\w+)/gm, 'exports.$1 = function $1'); exports.forEach(name => { code += `\nexports.${name} = ${name};`; diff --git a/test/sourcemaps/index.js b/test/sourcemaps/index.js index 0b0424a764ff..b8ba4aaa2a39 100644 --- a/test/sourcemaps/index.js +++ b/test/sourcemaps/index.js @@ -1,73 +1,131 @@ import * as fs from "fs"; import * as path from "path"; import * as assert from "assert"; -import { svelte } from "../helpers.js"; +import { loadConfig, svelte } from "../helpers.js"; +// keep source-map at version 0.7.x +// https://github.com/mozilla/source-map/issues/400 import { SourceMapConsumer } from "source-map"; import { getLocator } from "locate-character"; +import { encode as encode_mappings, decode as decode_mappings } from 'sourcemap-codec'; describe("sourcemaps", () => { fs.readdirSync(`${__dirname}/samples`).forEach(dir => { if (dir[0] === ".") return; + const config = loadConfig(`${__dirname}/samples/${dir}/_config.js`); + // add .solo to a sample directory name to only run that test - const solo = /\.solo/.test(dir); - const skip = /\.skip/.test(dir); + const solo = config.solo || /\.solo/.test(dir); + const skip = config.skip || /\.skip/.test(dir); if (solo && process.env.CI) { throw new Error("Forgot to remove `solo: true` from test"); } (solo ? it.only : skip ? it.skip : it)(dir, async () => { - const filename = path.resolve( - `${__dirname}/samples/${dir}/input.svelte` - ); - const outputFilename = path.resolve( - `${__dirname}/samples/${dir}/output` - ); + const { test } = require(`./samples/${dir}/test.js`); + const inputFile = path.resolve(`${__dirname}/samples/${dir}/input.svelte`); + const outputName = '_actual'; + const outputBase = path.resolve(`${__dirname}/samples/${dir}/${outputName}`); + + const input = {}; + input.code = fs.readFileSync(inputFile, "utf-8"); + input.locate = getLocator(input.code); - const input = fs.readFileSync(filename, "utf-8").replace(/\s+$/, ""); - const { js, css } = svelte.compile(input, { - filename, - outputFilename: `${outputFilename}.js`, - cssOutputFilename: `${outputFilename}.css` + let preprocessed; + try { + preprocessed = await svelte.preprocess( + input.code, + config.preprocess, { + filename: "input.svelte" + }); + } catch (error) { + preprocessed = { error }; + // run test without js, css + return test({ assert, input, preprocessed }); + } + + // preprocessed.map.mappings should be decoded + // to avoid unnecessary encode + decode steps + if (preprocessed.map) { + assert.equal(typeof preprocessed.map.mappings, 'object', 'preprocessed.map.mappings should be decoded'); + assert.equal(Array.isArray(preprocessed.map.mappings), true, 'preprocessed.map.mappings should be decoded'); + } + + const { js, css } = svelte.compile( + preprocessed.code, { + filename: "input.svelte", + sourcemap: preprocessed.map, + // filenames for sourcemaps + outputFilename: `${outputName}.js`, + cssOutputFilename: `${outputName}.css`, }); - const _code = js.code.replace(/Svelte v\d+\.\d+\.\d+/, match => match.replace(/\d/g, 'x')); + js.code = js.code.replace( + /generated by Svelte v\d+\.\d+\.\d+/, + match => match.replace(/\d/g, "x") + ); + fs.writeFileSync(`${outputBase}.svelte`, preprocessed.code); + if (preprocessed.map) { + fs.writeFileSync( + `${outputBase}.svelte.map`, + // TODO encode mappings for output - svelte.preprocess returns decoded mappings + JSON.stringify(preprocessed.map, null, 2) + ); + } fs.writeFileSync( - `${outputFilename}.js`, - `${_code}\n//# sourceMappingURL=output.js.map` + `${outputBase}.js`, + `${js.code}\n//# sourceMappingURL=${outputName}.js.map` ); fs.writeFileSync( - `${outputFilename}.js.map`, - JSON.stringify(js.map, null, " ") + `${outputBase}.js.map`, + JSON.stringify(js.map, 0, 2) ); - if (css.code) { fs.writeFileSync( - `${outputFilename}.css`, - `${css.code}\n/*# sourceMappingURL=output.css.map */` + `${outputBase}.css`, + `${css.code}\n/*# sourceMappingURL=${outputName}.css.map */` ); fs.writeFileSync( - `${outputFilename}.css.map`, - JSON.stringify(css.map, null, " ") + `${outputBase}.css.map`, + JSON.stringify(css.map, 0, 2) ); } - assert.deepEqual(js.map.sources, ["input.svelte"]); - if (css.map) assert.deepEqual(css.map.sources, ["input.svelte"]); + assert.deepEqual( + js.map.sources.slice().sort(), + (config.js_map_sources || ["input.svelte"]).sort() + ); + if (css.map) { + assert.deepEqual( + css.map.sources.slice().sort(), + (config.css_map_sources || ["input.svelte"]).sort() + ); + }; - const { test } = require(`./samples/${dir}/test.js`); + // stupid workaround (unnecessary encode + decode steps) + // TODO find a SourceMapConsumer who also consumes decoded mappings + if (preprocessed.map) { + preprocessed.map.mappings = encode_mappings(preprocessed.map.mappings); + } + + // use locate_1 with mapConsumer: + // lines are one-based, columns are zero-based - const locateInSource = getLocator(input); + preprocessed.mapConsumer = preprocessed.map && await new SourceMapConsumer(preprocessed.map); + preprocessed.locate = getLocator(preprocessed.code); + preprocessed.locate_1 = getLocator(preprocessed.code, { offsetLine: 1 }); - const smc = await new SourceMapConsumer(js.map); - const locateInGenerated = getLocator(_code); + js.mapConsumer = js.map && await new SourceMapConsumer(js.map); + js.locate = getLocator(js.code); + js.locate_1 = getLocator(js.code, { offsetLine: 1 }); - const smcCss = css.map && await new SourceMapConsumer(css.map); - const locateInGeneratedCss = getLocator(css.code || ''); + css.mapConsumer = css.map && await new SourceMapConsumer(css.map); + css.locate = getLocator(css.code || ''); + css.locate_1 = getLocator(css.code || '', { offsetLine: 1 }); - test({ assert, code: _code, map: js.map, smc, smcCss, locateInSource, locateInGenerated, locateInGeneratedCss }); + test({ assert, input, preprocessed, js, css }); }); }); }); diff --git a/test/sourcemaps/samples/basic/test.js b/test/sourcemaps/samples/basic/test.js index 705976167461..34c619c128ca 100644 --- a/test/sourcemaps/samples/basic/test.js +++ b/test/sourcemaps/samples/basic/test.js @@ -1,12 +1,12 @@ -export function test({ assert, smc, locateInSource, locateInGenerated }) { - const expected = locateInSource('foo.bar.baz'); +export function test({ assert, input, js }) { + const expected = input.locate('foo.bar.baz'); let start; let actual; - start = locateInGenerated('ctx[0].bar.baz'); + start = js.locate('ctx[0].bar.baz'); - actual = smc.originalPositionFor({ + actual = js.mapConsumer.originalPositionFor({ line: start.line + 1, column: start.column }); @@ -18,9 +18,9 @@ export function test({ assert, smc, locateInSource, locateInGenerated }) { column: expected.column }); - start = locateInGenerated('ctx[0].bar.baz', start.character + 1); + start = js.locate('ctx[0].bar.baz', start.character + 1); - actual = smc.originalPositionFor({ + actual = js.mapConsumer.originalPositionFor({ line: start.line + 1, column: start.column }); diff --git a/test/sourcemaps/samples/binding-shorthand.skip/test.js b/test/sourcemaps/samples/binding-shorthand.skip/test.js index cdfbbdc091ac..13ecdbf88934 100644 --- a/test/sourcemaps/samples/binding-shorthand.skip/test.js +++ b/test/sourcemaps/samples/binding-shorthand.skip/test.js @@ -1,13 +1,14 @@ -export function test({ assert, smc, locateInSource, locateInGenerated }) { - const expected = locateInSource('potato'); +export function test({ assert, input, js }) { + const expected = input.locate('potato'); let start; - start = locateInGenerated('potato'); - start = locateInGenerated('potato', start.character + 1); - start = locateInGenerated('potato', start.character + 1); // we need the third instance of 'potato' + start = js.locate('potato'); + start = js.locate('potato', start.character + 1); + start = js.locate('potato', start.character + 1); + // we need the third instance of 'potato' - const actual = smc.originalPositionFor({ + const actual = js.mapConsumer.originalPositionFor({ line: start.line + 1, column: start.column }); diff --git a/test/sourcemaps/samples/binding/test.js b/test/sourcemaps/samples/binding/test.js index 31c0e98442d5..3cb3246e5089 100644 --- a/test/sourcemaps/samples/binding/test.js +++ b/test/sourcemaps/samples/binding/test.js @@ -1,12 +1,12 @@ -export function test({ assert, smc, locateInSource, locateInGenerated }) { - const expected = locateInSource('bar.baz'); +export function test({ assert, input, js }) { + const expected = input.locate('bar.baz'); let start; let actual; - start = locateInGenerated('bar.baz'); + start = js.locate('bar.baz'); - actual = smc.originalPositionFor({ + actual = js.mapConsumer.originalPositionFor({ line: start.line + 1, column: start.column }); @@ -18,9 +18,9 @@ export function test({ assert, smc, locateInSource, locateInGenerated }) { column: expected.column }); - start = locateInGenerated('bar.baz', start.character + 1); + start = js.locate('bar.baz', start.character + 1); - actual = smc.originalPositionFor({ + actual = js.mapConsumer.originalPositionFor({ line: start.line + 1, column: start.column }); diff --git a/test/sourcemaps/samples/css/input.svelte b/test/sourcemaps/samples/css/input.svelte index ad0845d15b73..eba8f758a719 100644 --- a/test/sourcemaps/samples/css/input.svelte +++ b/test/sourcemaps/samples/css/input.svelte @@ -4,4 +4,4 @@ .foo { color: red; } - \ No newline at end of file + diff --git a/test/sourcemaps/samples/css/test.js b/test/sourcemaps/samples/css/test.js index 54082bd4ee66..1e0dda1dff43 100644 --- a/test/sourcemaps/samples/css/test.js +++ b/test/sourcemaps/samples/css/test.js @@ -1,14 +1,14 @@ -export function test({ assert, smcCss, locateInSource, locateInGeneratedCss }) { - const expected = locateInSource( '.foo' ); +export function test({ assert, input, css }) { + const expected = input.locate('.foo'); - const start = locateInGeneratedCss( '.foo' ); + const start = css.locate('.foo'); - const actual = smcCss.originalPositionFor({ + const actual = css.mapConsumer.originalPositionFor({ line: start.line + 1, column: start.column }); - assert.deepEqual( actual, { + assert.deepEqual(actual, { source: 'input.svelte', name: null, line: expected.line + 1, 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/each-block/input.svelte b/test/sourcemaps/samples/each-block/input.svelte index bf2f0609bd7e..bf33a18b0d94 100644 --- a/test/sourcemaps/samples/each-block/input.svelte +++ b/test/sourcemaps/samples/each-block/input.svelte @@ -1,3 +1,3 @@ {#each foo as bar} {bar} -{/each} \ No newline at end of file +{/each} diff --git a/test/sourcemaps/samples/each-block/test.js b/test/sourcemaps/samples/each-block/test.js index 35479986a546..08b37686a462 100644 --- a/test/sourcemaps/samples/each-block/test.js +++ b/test/sourcemaps/samples/each-block/test.js @@ -1,10 +1,10 @@ -export function test({ assert, code, smc, map, locateInSource, locateInGenerated }) { - const startIndex = code.indexOf('create_main_fragment'); +export function test({ assert, input, js }) { + const startIndex = js.code.indexOf('create_main_fragment'); - const expected = locateInSource('each'); - const start = locateInGenerated('length', startIndex); + const expected = input.locate('each'); + const start = js.locate('length', startIndex); - const actual = smc.originalPositionFor({ + const actual = js.mapConsumer.originalPositionFor({ line: start.line + 1, column: start.column }); diff --git a/test/sourcemaps/samples/preprocessed-markup/_config.js b/test/sourcemaps/samples/preprocessed-markup/_config.js new file mode 100644 index 000000000000..cb3eb90e01c6 --- /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..d4c1e6cdbeb5 --- /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..8781e9d7f2c1 --- /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..4e199e6d70c4 --- /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/script-after-comment/test.js b/test/sourcemaps/samples/script-after-comment/test.js index b4739701122a..06ecc46929df 100644 --- a/test/sourcemaps/samples/script-after-comment/test.js +++ b/test/sourcemaps/samples/script-after-comment/test.js @@ -1,13 +1,13 @@ -export function test({ assert, smc, locateInSource, locateInGenerated }) { - const expected = locateInSource( 'assertThisLine' ); - const start = locateInGenerated( 'assertThisLine' ); +export function test({ assert, input, js }) { + const expected = input.locate('assertThisLine'); + const start = js.locate('assertThisLine'); - const actual = smc.originalPositionFor({ + const actual = js.mapConsumer.originalPositionFor({ line: start.line + 1, column: start.column }); - assert.deepEqual( actual, { + assert.deepEqual(actual, { source: 'input.svelte', name: null, line: expected.line + 1, diff --git a/test/sourcemaps/samples/script/test.js b/test/sourcemaps/samples/script/test.js index 73971ef48763..e6a91f51e1c1 100644 --- a/test/sourcemaps/samples/script/test.js +++ b/test/sourcemaps/samples/script/test.js @@ -1,8 +1,8 @@ -export function test({ assert, smc, locateInSource, locateInGenerated }) { - const expected = locateInSource( '42' ); - const start = locateInGenerated( '42' ); +export function test({ assert, input, js }) { + const expected = input.locate('42'); + const start = js.locate('42'); - const actual = smc.originalPositionFor({ + const actual = js.mapConsumer.originalPositionFor({ line: start.line + 1, column: start.column }); 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..999fb20dfeff --- /dev/null +++ b/test/sourcemaps/samples/sourcemap-sources/_config.js @@ -0,0 +1,55 @@ +import MagicString, { 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}"` + ); + + }); +} diff --git a/test/sourcemaps/samples/static-no-script/input.svelte b/test/sourcemaps/samples/static-no-script/input.svelte index 1e2d59797188..a6a67113e10f 100644 --- a/test/sourcemaps/samples/static-no-script/input.svelte +++ b/test/sourcemaps/samples/static-no-script/input.svelte @@ -1 +1 @@ -

no moving parts

\ No newline at end of file +

no moving parts

diff --git a/test/sourcemaps/samples/static-no-script/test.js b/test/sourcemaps/samples/static-no-script/test.js index a8f4d89ab822..c683c94d6b31 100644 --- a/test/sourcemaps/samples/static-no-script/test.js +++ b/test/sourcemaps/samples/static-no-script/test.js @@ -1,9 +1,9 @@ -const fs = require( 'fs' ); -const path = require( 'path' ); +const fs = require('fs'); +const path = require('path'); -export function test({ assert, map }) { - assert.deepEqual( map.sources, [ 'input.svelte' ]); - assert.deepEqual( map.sourcesContent, [ - fs.readFileSync( path.join( __dirname, 'input.svelte' ), 'utf-8' ) +export function test({ assert, js }) { + assert.deepEqual(js.map.sources, ['input.svelte']); + assert.deepEqual(js.map.sourcesContent, [ + fs.readFileSync(path.join(__dirname, 'input.svelte'), 'utf-8') ]); } diff --git a/test/sourcemaps/samples/two-scripts/test.js b/test/sourcemaps/samples/two-scripts/test.js index b4739701122a..70901af8c9d2 100644 --- a/test/sourcemaps/samples/two-scripts/test.js +++ b/test/sourcemaps/samples/two-scripts/test.js @@ -1,8 +1,8 @@ -export function test({ assert, smc, locateInSource, locateInGenerated }) { - const expected = locateInSource( 'assertThisLine' ); - const start = locateInGenerated( 'assertThisLine' ); +export function test({ assert, input, js }) { + const expected = input.locate( 'assertThisLine' ); + const start = js.locate( 'assertThisLine' ); - const actual = smc.originalPositionFor({ + const actual = js.mapConsumer.originalPositionFor({ line: start.line + 1, column: start.column }); diff --git a/test/sourcemaps/samples/warn-on-encoded-mappings/_config.js b/test/sourcemaps/samples/warn-on-encoded-mappings/_config.js new file mode 100644 index 000000000000..d33573e27d72 --- /dev/null +++ b/test/sourcemaps/samples/warn-on-encoded-mappings/_config.js @@ -0,0 +1,58 @@ +import MagicString from 'magic-string'; + +// TODO move util fns to test index.js + +function result(filename, src, options = {}) { + const map_fn = options.encodeMappings ? src.generateMap : src.generateDecodedMap; + delete options.encodeMappings; + return { + code: src.toString(), + map: map_fn.apply(src, [{ + source: filename, + hires: true, + includeContent: false, + ...options + }]) + }; +} + +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 { + + js_map_sources: [], // test component has no scripts + + preprocess: [ + // preprocessor 0 + { markup: ({ content, filename }) => { + const src = new MagicString(content); + replace_all(src, 'replace_me', 'version_1'); + return result(filename, src, { encodeMappings: true }); + } }, + // 1 + { markup: ({ content, filename }) => { + const src = new MagicString(content); + replace_all(src, 'version_1', 'version_2'); + return result(filename, src); + } }, + // 2 + { markup: ({ content, filename }) => { + const src = new MagicString(content); + replace_all(src, 'version_2', 'version_3'); + return result(filename, src, { encodeMappings: true }); + } }, + // 3 + { markup: ({ content, filename }) => { + const src = new MagicString(content); + replace_all(src, 'version_3', 'version_4'); + return result(filename, src); + } } + ] + +}; diff --git a/test/sourcemaps/samples/warn-on-encoded-mappings/input.svelte b/test/sourcemaps/samples/warn-on-encoded-mappings/input.svelte new file mode 100644 index 000000000000..ac8f9b30ab75 --- /dev/null +++ b/test/sourcemaps/samples/warn-on-encoded-mappings/input.svelte @@ -0,0 +1,2 @@ +replace_me +replace_me diff --git a/test/sourcemaps/samples/warn-on-encoded-mappings/test.js b/test/sourcemaps/samples/warn-on-encoded-mappings/test.js new file mode 100644 index 000000000000..9e25f21e55c7 --- /dev/null +++ b/test/sourcemaps/samples/warn-on-encoded-mappings/test.js @@ -0,0 +1,13 @@ +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? + + // expected warning message: + // warning. svelte.preprocess received encoded sourcemaps (index 0, 2). [....] + +}