diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index f36790eb20..caf0a42a8f 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -335,7 +335,7 @@ export default class Component { if (compile_options.sourcemap) { if (js.map) { const pre_remap_sources = js.map.sources; - js.map = remapping([js.map, compile_options.sourcemap], () => null); + js.map = remapping([js.map, compile_options.sourcemap], () => null, true); // remapper can remove our source if it isn't used (no segments map back to it). It is still handy to have a source // so we add it back if (js.map.sources && js.map.sources.length == 0) { @@ -357,7 +357,7 @@ export default class Component { }); } if (css.map) { - css.map = remapping([css.map, compile_options.sourcemap], () => null); + css.map = remapping([css.map, compile_options.sourcemap], () => null, true); } } } diff --git a/src/compiler/preprocess/index.ts b/src/compiler/preprocess/index.ts index d72eb73945..c27c658919 100644 --- a/src/compiler/preprocess/index.ts +++ b/src/compiler/preprocess/index.ts @@ -1,12 +1,12 @@ -import remapper from '@ampproject/remapping'; -import { decode as sourcemap_decode } from 'sourcemap-codec'; +import remapping from '@ampproject/remapping'; +import { SourceMapInput, SourceMapLoader, 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 } from '../utils/string_with_sourcemap'; - export interface Processed { code: string; - map?: object | string; + map?: SourceMapInput; dependencies?: string[]; } @@ -102,7 +102,7 @@ function get_replacement( if (processed.map) { decoded_map = typeof processed.map === "string" ? JSON.parse(processed.map) : processed.map; if (typeof(decoded_map.mappings) === 'string') - decoded_map.mappings = sourcemap_decode(decoded_map.mappings); + 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); @@ -130,7 +130,7 @@ export default async function preprocess( // 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 = []; + const sourcemap_list: (DecodedSourceMap | RawSourceMap)[] = []; for (const fn of markup) { @@ -142,7 +142,12 @@ export default async function preprocess( if (processed && processed.dependencies) dependencies.push(...processed.dependencies); source = processed ? processed.code : source; - if (processed && processed.map) sourcemap_list.unshift(processed.map); + if (processed && processed.map) + sourcemap_list.unshift( + typeof(processed.map) === 'string' + ? JSON.parse(processed.map) as RawSourceMap + : processed.map as (RawSourceMap | DecodedSourceMap) + ); } for (const fn of script) { @@ -211,20 +216,31 @@ export default async function preprocess( sourcemap_list.unshift(res.map); } - // remapper can throw error - // `Transformation map ${i} must have exactly one source file.` - // for 0 <= i <= (sourcemap_list.length - 2) - - let map: ReturnType; + let map: RawSourceMap; + let map_idx = 0; try { map = sourcemap_list.length == 0 ? null - : remapper(sourcemap_list as any, () => null, true); // true: skip optional field `sourcesContent` + : sourcemap_list.slice(0, -1).find(m => m.sources.length !== 1) === undefined + ? remapping( // use array interface + sourcemap_list, + () => null, + true // skip optional field `sourcesContent` + ) + : remapping( // use loader interface + sourcemap_list[map_idx++], + function loader(sourcefile) { + if (sourcefile === filename) + return sourcemap_list[map_idx++] || null; + // bundle file = branch node + else return null; // source file = leaf node + } as SourceMapLoader + ); } catch (error) { throw { ...error, message: error.message + '\n\ncould not combine sourcemaps:\n' + - JSON.stringify((sourcemap_list as any).map(m => { + JSON.stringify(sourcemap_list.map(m => { return { ...m, mappings: JSON.stringify(m.mappings).slice(0, 100)+' ....'}; }), null, 2) }; diff --git a/src/compiler/utils/string_with_sourcemap.ts b/src/compiler/utils/string_with_sourcemap.ts index a5a89eea15..9dcc51afe3 100644 --- a/src/compiler/utils/string_with_sourcemap.ts +++ b/src/compiler/utils/string_with_sourcemap.ts @@ -1,14 +1,4 @@ -type MappingSegment = - | [number] - | [number, number, number, number] - | [number, number, number, number, number]; - -type SourceMappings = { - version: number; - sources: string[]; - names: string[]; - mappings: MappingSegment[][]; -}; +import { DecodedSourceMap, SourceMapSegment } from '@ampproject/remapping/dist/types/types'; type SourceLocation = { line: number; @@ -21,10 +11,10 @@ function last_line_length(s: string) { // mutate map in-place export function sourcemap_add_offset( - map: SourceMappings, offset: SourceLocation + map: DecodedSourceMap, offset: SourceLocation ) { // shift columns in first line - const m = map.mappings as any; + const m = map.mappings; m[0].forEach(seg => { if (seg[3]) seg[3] += offset.column; }); @@ -69,12 +59,12 @@ function pushArray(_this: T[], other: T[]) { export class StringWithSourcemap { string: string; - map: SourceMappings; + map: DecodedSourceMap; constructor(string = '', map = null) { this.string = string; if (map) - this.map = map as SourceMappings; + this.map = map as DecodedSourceMap; else this.map = { version: 3, @@ -97,8 +87,8 @@ export class StringWithSourcemap { this.string += other.string; - const m1 = this.map as any; - const m2 = other.map as any; + 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); @@ -109,20 +99,20 @@ export class StringWithSourcemap { // unswitched loops are faster if (sources_idx_changed && names_idx_changed) { - m2.forEach(line => { + 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.forEach(line => { + m2.mappings.forEach(line => { line.forEach(seg => { if (seg[1]) seg[1] = new_source_idx[seg[1]]; }); }); } else if (names_idx_changed) { - m2.forEach(line => { + m2.mappings.forEach(line => { line.forEach(seg => { if (seg[4]) seg[4] = new_name_idx[seg[4]]; }); @@ -137,9 +127,9 @@ export class StringWithSourcemap { // columns of 2 must be shifted const column_offset = last_line_length(this.string); - if (m2.length > 0 && column_offset > 0) { + if (m2.mappings.length > 0 && column_offset > 0) { // shift columns in first line - m2[0].forEach(seg => { + m2.mappings[0].forEach(seg => { seg[0] += column_offset; }); } @@ -153,11 +143,11 @@ export class StringWithSourcemap { return this; } - static from_processed(string: string, map?: SourceMappings): StringWithSourcemap { + 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 MappingSegment[] for every line + // add empty SourceMapSegment[] for every line const lineCount = string.split('\n').length; map.mappings = Array.from({length: lineCount}).map(_ => []); return new StringWithSourcemap(string, map); @@ -167,7 +157,7 @@ export class StringWithSourcemap { source_file: string, source: string, offset_in_source?: SourceLocation ): StringWithSourcemap { const offset = offset_in_source || { line: 0, column: 0 }; - const map: SourceMappings = { version: 3, names: [], sources: [source_file], mappings: [] }; + 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, @@ -177,7 +167,7 @@ export class StringWithSourcemap { let pos = 0; const segs = line.split(/([^\d\w\s]|\s+)/g) .filter(s => s !== "").map(s => { - const seg: MappingSegment = [ + const seg: SourceMapSegment = [ pos, 0, line_idx + offset.line, pos + (line_idx == 0 ? offset.column : 0) // shift first line diff --git a/test/sourcemaps/index.js b/test/sourcemaps/index.js index 7a2bf60982..ca0bdbfca3 100644 --- a/test/sourcemaps/index.js +++ b/test/sourcemaps/index.js @@ -81,17 +81,37 @@ describe("sourcemaps", () => { ); } - 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() + ); + }; - preprocessed.mapConsumer = preprocessed.map && await new SourceMapConsumer(preprocessed.map); - preprocessed.locate = getLocator(preprocessed.code); + // use locate_1 with mapConsumer: + // lines are one-based, columns are zero-based - js.mapConsumer = js.map && await new SourceMapConsumer(js.map); - js.locate = getLocator(js.code); + if (preprocessed.map) { + preprocessed.mapConsumer = await new SourceMapConsumer(preprocessed.map); + preprocessed.locate = getLocator(preprocessed.code); + preprocessed.locate_1 = getLocator(preprocessed.code, { offsetLine: 1 }); + } - css.mapConsumer = css.map && await new SourceMapConsumer(css.map); - css.locate = getLocator(css.code || ""); + if (js.map) { + js.mapConsumer = await new SourceMapConsumer(js.map); + js.locate = getLocator(js.code); + js.locate_1 = getLocator(js.code, { offsetLine: 1 }); + } + + if (css.map) { + css.mapConsumer = await new SourceMapConsumer(css.map); + css.locate = getLocator(css.code); + css.locate_1 = getLocator(css.code, { offsetLine: 1 }); + } test({ assert, input, preprocessed, js, css }); }); diff --git a/test/sourcemaps/samples/sourcemap-sources/_config.js b/test/sourcemaps/samples/sourcemap-sources/_config.js index 0c413c9300..2c3e66c3b4 100644 --- a/test/sourcemaps/samples/sourcemap-sources/_config.js +++ b/test/sourcemaps/samples/sourcemap-sources/_config.js @@ -3,7 +3,9 @@ import MagicString, { Bundle } from 'magic-string'; function add(bundle, filename, source) { bundle.addSource({ filename, - content: new MagicString(source) + content: new MagicString(source), + separator: '\n', + //separator: '', // ERROR. probably a bug in magic-string }); } @@ -12,13 +14,20 @@ function result(bundle, filename) { code: bundle.toString(), map: bundle.generateMap({ file: filename, - includeContent: true, - hires: true + includeContent: false, + hires: false }) }; } export default { + js_map_sources: [ + 'input.svelte', + 'foo.js', + 'bar.js', + 'foo2.js', + 'bar2.js', + ], preprocess: [ { script: ({ content, filename }) => { diff --git a/test/sourcemaps/samples/sourcemap-sources/test.js b/test/sourcemaps/samples/sourcemap-sources/test.js index e35618e18a..98fb64fc6a 100644 --- a/test/sourcemaps/samples/sourcemap-sources/test.js +++ b/test/sourcemaps/samples/sourcemap-sources/test.js @@ -1,12 +1,29 @@ -export function test({ assert, preprocessed, js, css }) { +export function test({ assert, preprocessed, js }) { - assert.notEqual(preprocessed.error, undefined, 'expected preprocessed.error'); + assert.equal(preprocessed.error, undefined); - const msg_expected_prefix = 'Transformation map 0 must have exactly one source file.'; + // sourcemap stores location only for 'answer = 42;' + // not for 'var answer = 42;' + [ + [js, 'foo.js', 'answer = 42;'], + [js, 'bar.js', 'console.log(answer);'], + [js, 'foo2.js', 'answer2 = 84;'], + [js, 'bar2.js', 'console.log(answer2);'], + ] + .forEach(([where, sourcefile, content]) => { - assert.equal( - preprocessed.error.message.slice(0, msg_expected_prefix.length), - msg_expected_prefix - ); + assert.deepEqual( + where.mapConsumer.originalPositionFor( + where.locate_1(content) + ), + { + source: sourcefile, + name: null, + line: 1, + column: 0 + }, + `failed to locate "${content}" from "${sourcefile}"` + ); + }); }