Fix some issues with preprocess source maps

pull/5754/head
dmitrage 5 years ago
parent e5aa04ed49
commit 100eba35d9

@ -39,6 +39,10 @@ function parse_attributes(str: string) {
return attrs; return attrs;
} }
function get_file_basename(filename: string) {
return filename.split(/[/\\]/).pop();
}
interface Replacement { interface Replacement {
offset: number; offset: number;
length: number; length: number;
@ -46,7 +50,7 @@ interface Replacement {
} }
async function replace_async( async function replace_async(
filename: string, file_basename: string,
source: string, source: string,
get_location: ReturnType<typeof getLocator>, get_location: ReturnType<typeof getLocator>,
re: RegExp, re: RegExp,
@ -73,13 +77,13 @@ async function replace_async(
)) { )) {
// content = unchanged source characters before the replaced segment // content = unchanged source characters before the replaced segment
const content = StringWithSourcemap.from_source( 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); out.concat(content).concat(replacement);
last_end = offset + length; last_end = offset + length;
} }
// final_content = unchanged source characters after last replaced segment // final_content = unchanged source characters after last replaced segment
const final_content = StringWithSourcemap.from_source( 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); return out.concat(final_content);
} }
@ -156,11 +160,34 @@ function decoded_sourcemap_from_generator(generator: any) {
return map; 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 * Convert a preprocessor output and its leading prefix and trailing suffix into StringWithSourceMap
*/ */
function get_replacement( function get_replacement(
filename: string, file_basename: string,
offset: number, offset: number,
get_location: ReturnType<typeof getLocator>, get_location: ReturnType<typeof getLocator>,
original: string, original: string,
@ -171,9 +198,9 @@ function get_replacement(
// Convert the unchanged prefix and suffix to StringWithSourcemap // Convert the unchanged prefix and suffix to StringWithSourcemap
const prefix_with_map = StringWithSourcemap.from_source( 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( 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 // Convert the preprocessed code and its sourcemap to a StringWithSourcemap
let decoded_map: DecodedSourceMap; let decoded_map: DecodedSourceMap;
@ -186,7 +213,9 @@ function get_replacement(
// import decoded sourcemap from mozilla/source-map/SourceMapGenerator // import decoded sourcemap from mozilla/source-map/SourceMapGenerator
decoded_map = decoded_sourcemap_from_generator(decoded_map); 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); 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 filename = (options && options.filename) || preprocessor.filename; // legacy
const dependencies = []; const dependencies = [];
// preprocess source must be relative to itself
const file_basename = filename && get_file_basename(filename);
const preprocessors = preprocessor const preprocessors = preprocessor
? Array.isArray(preprocessor) ? preprocessor : [preprocessor] ? Array.isArray(preprocessor) ? preprocessor : [preprocessor]
: []; : [];
@ -246,13 +278,13 @@ export default async function preprocess(
: /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi; : /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi;
const res = await replace_async( const res = await replace_async(
filename, file_basename,
source, source,
get_location, get_location,
tag_regex, tag_regex,
async (match, attributes = '', content = '', offset) => { async (match, attributes = '', content = '', offset) => {
const no_change = () => StringWithSourcemap.from_source( const no_change = () => StringWithSourcemap.from_source(
filename, match, get_location(offset)); file_basename, match, get_location(offset));
if (!attributes && !content) { if (!attributes && !content) {
return no_change(); return no_change();
} }
@ -268,7 +300,7 @@ export default async function preprocess(
if (!processed) return no_change(); if (!processed) return no_change();
if (processed.dependencies) dependencies.push(...processed.dependencies); if (processed.dependencies) dependencies.push(...processed.dependencies);
return get_replacement(filename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, `</${tag_name}>`); return get_replacement(file_basename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, `</${tag_name}>`);
} }
); );
source = res.string; source = res.string;
@ -285,7 +317,7 @@ export default async function preprocess(
// Combine all the source maps for each preprocessor function into one // Combine all the source maps for each preprocessor function into one
const map: RawSourceMap = combine_sourcemaps( const map: RawSourceMap = combine_sourcemaps(
filename, file_basename,
sourcemap_list sourcemap_list
); );

@ -13,21 +13,22 @@ function last_line_length(s: string) {
// mutate map in-place // mutate map in-place
export function sourcemap_add_offset( export function sourcemap_add_offset(
map: DecodedSourceMap, offset: SourceLocation map: DecodedSourceMap, offset: SourceLocation, source_index: number
) { ) {
if (map.mappings.length == 0) return map; if (map.mappings.length == 0 || source_index < 0) return;
// 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 // shift lines
for (let line = 0; line < map.mappings.length; line++) { for (let line = 0; line < map.mappings.length; line++) {
const segment_list = map.mappings[line]; const segment_list = map.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) { for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[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; return this;
} }
// compute last line length before mutating
const column_offset = last_line_length(this.string);
this.string += other.string; this.string += other.string;
const m1 = this.map; const m1 = this.map;
@ -117,8 +121,8 @@ export class StringWithSourcemap {
const segment_list = m2.mappings[line]; const segment_list = m2.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) { for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[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]];
if (seg[4]) seg[4] = new_name_idx[seg[4]]; if (seg.length > 4) seg[4] = new_name_idx[seg[4]];
} }
} }
} else if (sources_idx_changed) { } else if (sources_idx_changed) {
@ -126,7 +130,7 @@ export class StringWithSourcemap {
const segment_list = m2.mappings[line]; const segment_list = m2.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) { for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[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) { } else if (names_idx_changed) {
@ -134,7 +138,7 @@ export class StringWithSourcemap {
const segment_list = m2.mappings[line]; const segment_list = m2.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) { for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[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 // 2. first line of second map
// columns of 2 must be shifted // columns of 2 must be shifted
const column_offset = last_line_length(this.string);
if (m2.mappings.length > 0 && column_offset > 0) { if (m2.mappings.length > 0 && column_offset > 0) {
const first_line = m2.mappings[0]; const first_line = m2.mappings[0];
for (let i = 0; i < first_line.length; i++) { for (let i = 0; i < first_line.length; i++) {
@ -164,7 +167,17 @@ export class StringWithSourcemap {
} }
static from_processed(string: string, map?: DecodedSourceMap): 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(); if (string == '') return new StringWithSourcemap();
map = { version: 3, names: [], sources: [], mappings: [] }; map = { version: 3, names: [], sources: [], mappings: [] };

@ -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);
}
}
]
};

@ -0,0 +1,19 @@
<script>
import { onMount } from 'svelte';
let count = 0;
onMount(() => {
const id = setInterval(() => count++, 1000);
return () => clearInterval(id);
});
</script>
<style>
h1 {
color: orange;
}
</style>
<h1>Hello world!</h1>
<div>Counter value: {count}</div>

@ -0,0 +1,17 @@
export function test({ assert, input, preprocessed }) {
const content = '<h1>Hello world!</h1>';
const original = input.locate(content);
const transformed = preprocessed.locate_1('<h1>Hello world!</h1>');
assert.deepEqual(
preprocessed.mapConsumer.originalPositionFor(transformed),
{
source: 'input.svelte',
name: null,
line: original.line + 1,
column: original.column
},
`failed to locate "${content}"`
);
}

@ -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
};
}
}
]
};

@ -0,0 +1,13 @@
<script lang="ts">
import { onMount } from 'svelte';
let count: number = 0;
onMount(() => {
const id = setInterval(() => count++, 1000);
return () => clearInterval(id);
});
</script>
<h1>Hello world!</h1>
<div>Counter value: {count}</div>

@ -0,0 +1,17 @@
export function test({ assert, input, preprocessed }) {
const content = '<h1>Hello world!</h1>';
const original = input.locate(content);
const transformed = preprocessed.locate_1('<h1>Hello world!</h1>');
assert.deepEqual(
preprocessed.mapConsumer.originalPositionFor(transformed),
{
source: 'input.svelte',
name: null,
line: original.line + 1,
column: original.column
},
`failed to locate "${content}"`
);
}
Loading…
Cancel
Save