From 100eba35d9af209f607a932e0efe1ace00bb7ef1 Mon Sep 17 00:00:00 2001 From: dmitrage Date: Tue, 8 Dec 2020 13:30:36 +0300 Subject: [PATCH] Fix some issues with preprocess source maps --- src/compiler/preprocess/index.ts | 54 +++++++++++++++---- src/compiler/utils/string_with_sourcemap.ts | 43 +++++++++------ .../samples/script-and-style/_config.js | 22 ++++++++ .../samples/script-and-style/input.svelte | 19 +++++++ .../samples/script-and-style/test.js | 17 ++++++ test/sourcemaps/samples/typescript/_config.js | 26 +++++++++ .../samples/typescript/input.svelte | 13 +++++ test/sourcemaps/samples/typescript/test.js | 17 ++++++ 8 files changed, 185 insertions(+), 26 deletions(-) create mode 100644 test/sourcemaps/samples/script-and-style/_config.js create mode 100644 test/sourcemaps/samples/script-and-style/input.svelte create mode 100644 test/sourcemaps/samples/script-and-style/test.js create mode 100644 test/sourcemaps/samples/typescript/_config.js create mode 100644 test/sourcemaps/samples/typescript/input.svelte create mode 100644 test/sourcemaps/samples/typescript/test.js diff --git a/src/compiler/preprocess/index.ts b/src/compiler/preprocess/index.ts index b51b67bb23..f8e2f95c89 100644 --- a/src/compiler/preprocess/index.ts +++ b/src/compiler/preprocess/index.ts @@ -39,6 +39,10 @@ function parse_attributes(str: string) { return attrs; } +function get_file_basename(filename: string) { + return filename.split(/[/\\]/).pop(); +} + interface Replacement { offset: number; length: number; @@ -46,7 +50,7 @@ interface Replacement { } async function replace_async( - filename: string, + file_basename: string, source: string, get_location: ReturnType, re: RegExp, @@ -73,13 +77,13 @@ async function replace_async( )) { // content = unchanged source characters before the replaced segment const content = StringWithSourcemap.from_source( - filename, source.slice(last_end, offset), get_location(last_end)); + file_basename, source.slice(last_end, offset), get_location(last_end)); out.concat(content).concat(replacement); last_end = offset + length; } // final_content = unchanged source characters after last replaced segment const final_content = StringWithSourcemap.from_source( - filename, source.slice(last_end), get_location(last_end)); + file_basename, source.slice(last_end), get_location(last_end)); return out.concat(final_content); } @@ -156,11 +160,34 @@ function decoded_sourcemap_from_generator(generator: any) { return map; } +/** + * Heuristic used to find index of component source inside source map sources. + */ +function guess_source_index( + file_basename: string, + decoded_map: DecodedSourceMap +): number { + if (!file_basename) { + return decoded_map.sources.findIndex(source => !source); + } + // different tools produce different sources + // (file name, relative path, absolute path) + const index = decoded_map.sources.findIndex(source => { + const source_basename = source && get_file_basename(source); + return source_basename === file_basename; + }); + if (index !== -1) { + // also normalize it in on source map + decoded_map.sources[index] = file_basename; + } + return index; +} + /** * Convert a preprocessor output and its leading prefix and trailing suffix into StringWithSourceMap */ function get_replacement( - filename: string, + file_basename: string, offset: number, get_location: ReturnType, original: string, @@ -171,9 +198,9 @@ function get_replacement( // Convert the unchanged prefix and suffix to StringWithSourcemap const prefix_with_map = StringWithSourcemap.from_source( - filename, prefix, get_location(offset)); + file_basename, prefix, get_location(offset)); const suffix_with_map = StringWithSourcemap.from_source( - filename, suffix, get_location(offset + prefix.length + original.length)); + file_basename, suffix, get_location(offset + prefix.length + original.length)); // Convert the preprocessed code and its sourcemap to a StringWithSourcemap let decoded_map: DecodedSourceMap; @@ -186,7 +213,9 @@ function get_replacement( // import decoded sourcemap from mozilla/source-map/SourceMapGenerator decoded_map = decoded_sourcemap_from_generator(decoded_map); } - sourcemap_add_offset(decoded_map, get_location(offset + prefix.length)); + // offset only segments pointing at original component source + const source_index = guess_source_index(file_basename, decoded_map); + sourcemap_add_offset(decoded_map, get_location(offset + prefix.length), source_index); } const processed_with_map = StringWithSourcemap.from_processed(processed.code, decoded_map); @@ -203,6 +232,9 @@ export default async function preprocess( const filename = (options && options.filename) || preprocessor.filename; // legacy const dependencies = []; + // preprocess source must be relative to itself + const file_basename = filename && get_file_basename(filename); + const preprocessors = preprocessor ? Array.isArray(preprocessor) ? preprocessor : [preprocessor] : []; @@ -246,13 +278,13 @@ export default async function preprocess( : /|([^]*?)<\/script>|\/>)/gi; const res = await replace_async( - filename, + file_basename, source, get_location, tag_regex, async (match, attributes = '', content = '', offset) => { const no_change = () => StringWithSourcemap.from_source( - filename, match, get_location(offset)); + file_basename, match, get_location(offset)); if (!attributes && !content) { return no_change(); } @@ -268,7 +300,7 @@ export default async function preprocess( if (!processed) return no_change(); if (processed.dependencies) dependencies.push(...processed.dependencies); - return get_replacement(filename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, ``); + return get_replacement(file_basename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, ``); } ); source = res.string; @@ -285,7 +317,7 @@ export default async function preprocess( // Combine all the source maps for each preprocessor function into one const map: RawSourceMap = combine_sourcemaps( - filename, + file_basename, sourcemap_list ); diff --git a/src/compiler/utils/string_with_sourcemap.ts b/src/compiler/utils/string_with_sourcemap.ts index 421a0c1fbd..3dbd0a674a 100644 --- a/src/compiler/utils/string_with_sourcemap.ts +++ b/src/compiler/utils/string_with_sourcemap.ts @@ -13,21 +13,22 @@ function last_line_length(s: string) { // mutate map in-place export function sourcemap_add_offset( - map: DecodedSourceMap, offset: SourceLocation + map: DecodedSourceMap, offset: SourceLocation, source_index: number ) { - if (map.mappings.length == 0) return map; - // shift columns in first line - const segment_list = map.mappings[0]; - for (let segment = 0; segment < segment_list.length; segment++) { - const seg = segment_list[segment]; - if (seg[3]) seg[3] += offset.column; - } + if (map.mappings.length == 0 || source_index < 0) return; // shift lines for (let line = 0; line < map.mappings.length; line++) { const segment_list = map.mappings[line]; for (let segment = 0; segment < segment_list.length; segment++) { const seg = segment_list[segment]; - if (seg[2]) seg[2] += offset.line; + // shift only segments that belong to component source file + if (seg.length >= 4 && seg[1] === source_index) { + // shift columns pointing at the first line + if (seg[2] === 0) { + seg[3] += offset.column; + } + seg[2] += offset.line; + } } } } @@ -97,6 +98,9 @@ export class StringWithSourcemap { return this; } + // compute last line length before mutating + const column_offset = last_line_length(this.string); + this.string += other.string; const m1 = this.map; @@ -117,8 +121,8 @@ export class StringWithSourcemap { 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]]; + if (seg.length > 1) seg[1] = new_source_idx[seg[1]]; + if (seg.length > 4) seg[4] = new_name_idx[seg[4]]; } } } else if (sources_idx_changed) { @@ -126,7 +130,7 @@ export class StringWithSourcemap { 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.length > 1) seg[1] = new_source_idx[seg[1]]; } } } else if (names_idx_changed) { @@ -134,7 +138,7 @@ export class StringWithSourcemap { 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]]; + if (seg.length > 4) seg[4] = new_name_idx[seg[4]]; } } } @@ -146,7 +150,6 @@ export class StringWithSourcemap { // 2. first line of second map // columns of 2 must be shifted - const column_offset = last_line_length(this.string); if (m2.mappings.length > 0 && column_offset > 0) { const first_line = m2.mappings[0]; for (let i = 0; i < first_line.length; i++) { @@ -164,7 +167,17 @@ export class StringWithSourcemap { } static from_processed(string: string, map?: DecodedSourceMap): StringWithSourcemap { - if (map) return new StringWithSourcemap(string, map); + if (map) { + // ensure that count of source map mappings lines + // is equal to count of generated code lines + // (some tools may produce less) + const missing_lines = string.split('\n').length - map.mappings.length; + for (let i = 0; i < missing_lines; i++) { + map.mappings.push([]); + } + return new StringWithSourcemap(string, map); + } + if (string == '') return new StringWithSourcemap(); map = { version: 3, names: [], sources: [], mappings: [] }; diff --git a/test/sourcemaps/samples/script-and-style/_config.js b/test/sourcemaps/samples/script-and-style/_config.js new file mode 100644 index 0000000000..50795b2d90 --- /dev/null +++ b/test/sourcemaps/samples/script-and-style/_config.js @@ -0,0 +1,22 @@ +import MagicString from 'magic-string'; +import { magic_string_preprocessor_result } from '../../helpers'; + +export default { + js_map_sources: [ + 'input.svelte' + ], + preprocess: [ + { + script: ({ content, filename }) => { + const s = new MagicString(content); + s.prepend('// This script code is approved\n'); + return magic_string_preprocessor_result(filename, s); + }, + style: ({ content, filename }) => { + const s = new MagicString(content); + s.prepend('/* This style code is approved */\n'); + return magic_string_preprocessor_result(filename, s); + } + } + ] +}; diff --git a/test/sourcemaps/samples/script-and-style/input.svelte b/test/sourcemaps/samples/script-and-style/input.svelte new file mode 100644 index 0000000000..489f128ef1 --- /dev/null +++ b/test/sourcemaps/samples/script-and-style/input.svelte @@ -0,0 +1,19 @@ + + + + +

Hello world!

+
Counter value: {count}
diff --git a/test/sourcemaps/samples/script-and-style/test.js b/test/sourcemaps/samples/script-and-style/test.js new file mode 100644 index 0000000000..473d7aaa1b --- /dev/null +++ b/test/sourcemaps/samples/script-and-style/test.js @@ -0,0 +1,17 @@ +export function test({ assert, input, preprocessed }) { + const content = '

Hello world!

'; + + const original = input.locate(content); + const transformed = preprocessed.locate_1('

Hello world!

'); + + assert.deepEqual( + preprocessed.mapConsumer.originalPositionFor(transformed), + { + source: 'input.svelte', + name: null, + line: original.line + 1, + column: original.column + }, + `failed to locate "${content}"` + ); +} diff --git a/test/sourcemaps/samples/typescript/_config.js b/test/sourcemaps/samples/typescript/_config.js new file mode 100644 index 0000000000..c8a955dfbd --- /dev/null +++ b/test/sourcemaps/samples/typescript/_config.js @@ -0,0 +1,26 @@ +import * as ts from 'typescript'; + +export default { + js_map_sources: [ + 'input.svelte' + ], + preprocess: [ + { + script: ({ content, filename }) => { + const { outputText, sourceMapText } = ts.transpileModule(content, { + fileName: filename, + compilerOptions: { + target: ts.ScriptTarget.ES2015, + module: ts.ModuleKind.ES2015, + sourceMap: true + } + }); + + return { + code: outputText, + map: sourceMapText + }; + } + } + ] +}; diff --git a/test/sourcemaps/samples/typescript/input.svelte b/test/sourcemaps/samples/typescript/input.svelte new file mode 100644 index 0000000000..718336f346 --- /dev/null +++ b/test/sourcemaps/samples/typescript/input.svelte @@ -0,0 +1,13 @@ + + +

Hello world!

+
Counter value: {count}
diff --git a/test/sourcemaps/samples/typescript/test.js b/test/sourcemaps/samples/typescript/test.js new file mode 100644 index 0000000000..473d7aaa1b --- /dev/null +++ b/test/sourcemaps/samples/typescript/test.js @@ -0,0 +1,17 @@ +export function test({ assert, input, preprocessed }) { + const content = '

Hello world!

'; + + const original = input.locate(content); + const transformed = preprocessed.locate_1('

Hello world!

'); + + assert.deepEqual( + preprocessed.mapConsumer.originalPositionFor(transformed), + { + source: 'input.svelte', + name: null, + line: original.line + 1, + column: original.column + }, + `failed to locate "${content}"` + ); +}