diff --git a/src/compiler/preprocess/index.ts b/src/compiler/preprocess/index.ts index b51b67bb2345..264fbbdbcd42 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); } @@ -160,7 +164,7 @@ function decoded_sourcemap_from_generator(generator: any) { * 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 +175,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 +190,11 @@ 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 = decoded_map.sources.indexOf(file_basename); + if (source_index !== -1) { + 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 +211,9 @@ export default async function preprocess( const filename = (options && options.filename) || preprocessor.filename; // legacy const dependencies = []; + // preprocess source must be relative to itself or equal null + const file_basename = filename == null ? null : get_file_basename(filename); + const preprocessors = preprocessor ? Array.isArray(preprocessor) ? preprocessor : [preprocessor] : []; @@ -246,13 +257,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(); } @@ -265,10 +276,13 @@ export default async function preprocess( attributes: parse_attributes(attributes), filename }); - - if (!processed) return no_change(); - if (processed.dependencies) dependencies.push(...processed.dependencies); - return get_replacement(filename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, ``); + if (processed && processed.dependencies) { + dependencies.push(...processed.dependencies); + } + if (!processed || !processed.map && processed.code === content) { + return no_change(); + } + return get_replacement(file_basename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, ``); } ); source = res.string; @@ -285,7 +299,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 421a0c1fbd97..1766a0db069a 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; - } - // shift lines + if (map.mappings.length == 0) return; 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[1] === source_index) { // also ensures that seg.length >= 4 + // shift column if it points at the first line + if (seg[2] === 0) { + seg[3] += offset.column; + } + // shift line + 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[1] >= 0) seg[1] = new_source_idx[seg[1]]; + if (seg[4] >= 0) 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[1] >= 0) 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[4] >= 0) 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,12 +167,23 @@ export class StringWithSourcemap { } static from_processed(string: string, map?: DecodedSourceMap): StringWithSourcemap { - if (map) return new StringWithSourcemap(string, map); + const line_count = string.split('\n').length; + + 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 = line_count - 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: [] }; // add empty SourceMapSegment[] for every line - const line_count = (string.match(/\n/g) || '').length; for (let i = 0; i < line_count; i++) map.mappings.push([]); return new StringWithSourcemap(string, map); } diff --git a/test/sourcemaps/helpers.ts b/test/sourcemaps/helpers.ts index d0bea310e623..f546566a95ce 100644 --- a/test/sourcemaps/helpers.ts +++ b/test/sourcemaps/helpers.ts @@ -1,4 +1,111 @@ -import MagicString from 'magic-string'; +import * as assert from 'assert'; +import { getLocator } from 'locate-character'; +import MagicString, { Bundle } from 'magic-string'; + +type AssertMappedParameters = { + code: string; + filename?: string; + input: string | ReturnType; + input_code?: string; + preprocessed: any; +}; + +export function assert_mapped( + { code, filename, input, input_code, preprocessed }: AssertMappedParameters +) { + const locate_input = typeof input === 'function' ? input : getLocator(input); + if (filename === undefined) filename = 'input.svelte'; + if (input_code === undefined) input_code = code; + + const source_loc = locate_input(input_code); + assert.notEqual( + source_loc, + undefined, + `failed to locate "${input_code}" in "${filename}"` + ); + + const transformed_loc = preprocessed.locate_1(code); + assert.notEqual( + transformed_loc, + undefined, + `failed to locate "${code}" in transformed "${filename}"` + ); + + assert.deepEqual( + preprocessed.mapConsumer.originalPositionFor(transformed_loc), + { + source: filename, + name: null, + line: source_loc.line + 1, + column: source_loc.column + }, + `incorrect mappings for "${input_code}" in "${filename}"` + ); +} + +type AssertNotMappedParameters = { + code: string; + filename?: string; + preprocessed: any; +}; + +export function assert_not_mapped( + { code, filename, preprocessed }: AssertNotMappedParameters +) { + if (filename === undefined) filename = 'input.svelte'; + + const transformed_loc = preprocessed.locate_1(code); + assert.notEqual( + transformed_loc, + undefined, + `failed to locate "${code}" in transformed "${filename}"` + ); + + assert.deepEqual( + preprocessed.mapConsumer.originalPositionFor(transformed_loc), + { + source: null, + name: null, + line: null, + column: null + }, + `incorrect mappings for "${code}" in "${filename}"` + ); +} + +export function assert_not_located( + code: string, + locate: ReturnType, + filename = 'input.svelte' +) { + assert.equal( + locate(code), + undefined, + `located "${code}" that should be removed from ${filename}` + ); +} + +export function magic_string_bundle( + inputs: Array<{ code: string | MagicString, filename: string }>, + filename = 'bundle.js', + separator = '\n' +) { + const bundle = new Bundle({ separator }); + inputs.forEach(({ code, filename }) => { + bundle.addSource({ + filename, + content: typeof code === 'string' ? new MagicString(code) : code + }); + }); + return { + code: bundle.toString(), + map: bundle.generateMap({ + source: filename, + hires: true, + includeContent: false + }) + }; +} export function magic_string_preprocessor_result(filename: string, src: MagicString) { return { diff --git a/test/sourcemaps/samples/external/_config.js b/test/sourcemaps/samples/external/_config.js new file mode 100644 index 000000000000..400def20ac63 --- /dev/null +++ b/test/sourcemaps/samples/external/_config.js @@ -0,0 +1,24 @@ +import { magic_string_bundle } from '../../helpers'; + +export const COMMON = ':global(html) { height: 100%; }\n'; + +// TODO: removing '\n' breaks test +// - _actual.svelte.map looks correct +// - _actual.css.map adds reference to on input.svelte +// - Most probably caused by bug in current magic-string version (fixed in 0.25.7) +export const STYLES = '.awesome { color: orange; }\n'; + +export default { + css_map_sources: ['common.scss', 'styles.scss'], + js_map_sources: [], + preprocess: [ + { + style: () => { + return magic_string_bundle([ + { filename: 'common.scss', code: COMMON }, + { filename: 'styles.scss', code: STYLES } + ]); + } + } + ] +}; diff --git a/test/sourcemaps/samples/external/input.svelte b/test/sourcemaps/samples/external/input.svelte new file mode 100644 index 000000000000..94d895e4a724 --- /dev/null +++ b/test/sourcemaps/samples/external/input.svelte @@ -0,0 +1,3 @@ + + +
Divs ftw!
\ No newline at end of file diff --git a/test/sourcemaps/samples/external/test.js b/test/sourcemaps/samples/external/test.js new file mode 100644 index 000000000000..5193b62ba1d4 --- /dev/null +++ b/test/sourcemaps/samples/external/test.js @@ -0,0 +1,26 @@ +import { assert_mapped } from '../../helpers'; +import { COMMON, STYLES } from './_config'; + +export function test({ input, preprocessed }) { + // Transformed script, main file + assert_mapped({ + filename: 'input.svelte', + code: 'Divs ftw!', + input: input.locate, + preprocessed + }); + + // External files + assert_mapped({ + filename: 'common.scss', + code: 'height: 100%;', + input: COMMON, + preprocessed + }); + assert_mapped({ + filename: 'styles.scss', + code: 'color: orange;', + input: STYLES, + preprocessed + }); +} diff --git a/test/sourcemaps/samples/preprocessed-no-map/_config.js b/test/sourcemaps/samples/preprocessed-no-map/_config.js new file mode 100644 index 000000000000..9888b56ddf8f --- /dev/null +++ b/test/sourcemaps/samples/preprocessed-no-map/_config.js @@ -0,0 +1,15 @@ +export default { + css_map_sources: [], + preprocess: [ + { + style: ({ content }) => { + // Modified without source map + return { code: content + ' ' }; + }, + script: ({ content }) => { + // Not modified + return { code: content }; + } + } + ] +}; diff --git a/test/sourcemaps/samples/preprocessed-no-map/input.svelte b/test/sourcemaps/samples/preprocessed-no-map/input.svelte new file mode 100644 index 000000000000..febfa5191b4b --- /dev/null +++ b/test/sourcemaps/samples/preprocessed-no-map/input.svelte @@ -0,0 +1,13 @@ + + +
+
{name}
+
+ + diff --git a/test/sourcemaps/samples/preprocessed-no-map/test.js b/test/sourcemaps/samples/preprocessed-no-map/test.js new file mode 100644 index 000000000000..22286ae282b1 --- /dev/null +++ b/test/sourcemaps/samples/preprocessed-no-map/test.js @@ -0,0 +1,37 @@ +import { assert_mapped, assert_not_mapped } from '../../helpers'; + +export function test({ input, preprocessed }) { + // markup (start) + assert_mapped({ + code: ' + +

Hello

\ No newline at end of file diff --git a/test/sourcemaps/samples/sourcemap-concat/test.js b/test/sourcemaps/samples/sourcemap-concat/test.js new file mode 100644 index 000000000000..c68d7175dc23 --- /dev/null +++ b/test/sourcemaps/samples/sourcemap-concat/test.js @@ -0,0 +1,9 @@ +import { assert_mapped } from '../../helpers'; + +export function test({ input, preprocessed }) { + assert_mapped({ + code: 'Target', + input: input.locate, + preprocessed + }); +} diff --git a/test/sourcemaps/samples/sourcemap-offsets/_config.js b/test/sourcemaps/samples/sourcemap-offsets/_config.js new file mode 100644 index 000000000000..392fb68a434f --- /dev/null +++ b/test/sourcemaps/samples/sourcemap-offsets/_config.js @@ -0,0 +1,18 @@ +import { magic_string_bundle } from '../../helpers'; + +export const EXTERNAL = 'span { --external-var: 1px; }'; + +export default { + js_map_sources: [], + css_map_sources: ['input.svelte', 'external.css'], + preprocess: [ + { + style: ({ content, filename }) => { + return magic_string_bundle([ + { code: EXTERNAL, filename: 'external.css' }, + { code: content, filename } + ]); + } + } + ] +}; diff --git a/test/sourcemaps/samples/sourcemap-offsets/input.svelte b/test/sourcemaps/samples/sourcemap-offsets/input.svelte new file mode 100644 index 000000000000..51f7762e2ab7 --- /dev/null +++ b/test/sourcemaps/samples/sourcemap-offsets/input.svelte @@ -0,0 +1,9 @@ + + + + +
Text
\ No newline at end of file diff --git a/test/sourcemaps/samples/sourcemap-offsets/test.js b/test/sourcemaps/samples/sourcemap-offsets/test.js new file mode 100644 index 000000000000..a6c328770c0e --- /dev/null +++ b/test/sourcemaps/samples/sourcemap-offsets/test.js @@ -0,0 +1,19 @@ +import { assert_mapped } from '../../helpers'; +import { EXTERNAL } from './_config'; + +export function test({ input, preprocessed }) { + // Part from component, should be with offset + assert_mapped({ + code: '--component-var', + input: input.locate, + preprocessed + }); + + // Part from external file, should be without offset + assert_mapped({ + filename: 'external.css', + code: '--external-var', + input: EXTERNAL, + preprocessed + }); +} diff --git a/test/sourcemaps/samples/typescript/_config.js b/test/sourcemaps/samples/typescript/_config.js new file mode 100644 index 000000000000..c8a955dfbdf4 --- /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 000000000000..93639544b66e --- /dev/null +++ b/test/sourcemaps/samples/typescript/input.svelte @@ -0,0 +1,18 @@ + + +

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 000000000000..bec397e33c49 --- /dev/null +++ b/test/sourcemaps/samples/typescript/test.js @@ -0,0 +1,21 @@ +import { assert_mapped, assert_not_located } from '../../helpers'; + +export function test({ input, preprocessed }) { + // TS => JS code + assert_mapped({ + code: 'let count = 0;', + input_code: 'let count: number = 0;', + input: input.locate, + preprocessed + }); + + // Markup, not touched + assert_mapped({ + code: '

Hello world!

', + input: input.locate, + preprocessed + }); + + // TS types, removed + assert_not_located('ITimeoutDestroyer', preprocessed.locate_1); +}