Skip to content

Commit 68538c6

Browse files
authored
Fix various issues with preprocess source maps (#5754)
1 parent 91376d2 commit 68538c6

File tree

21 files changed

+474
-32
lines changed

21 files changed

+474
-32
lines changed

src/compiler/preprocess/index.ts

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,18 @@ function parse_attributes(str: string) {
3939
return attrs;
4040
}
4141

42+
function get_file_basename(filename: string) {
43+
return filename.split(/[/\\]/).pop();
44+
}
45+
4246
interface Replacement {
4347
offset: number;
4448
length: number;
4549
replacement: StringWithSourcemap;
4650
}
4751

4852
async function replace_async(
49-
filename: string,
53+
file_basename: string,
5054
source: string,
5155
get_location: ReturnType<typeof getLocator>,
5256
re: RegExp,
@@ -73,13 +77,13 @@ async function replace_async(
7377
)) {
7478
// content = unchanged source characters before the replaced segment
7579
const content = StringWithSourcemap.from_source(
76-
filename, source.slice(last_end, offset), get_location(last_end));
80+
file_basename, source.slice(last_end, offset), get_location(last_end));
7781
out.concat(content).concat(replacement);
7882
last_end = offset + length;
7983
}
8084
// final_content = unchanged source characters after last replaced segment
8185
const final_content = StringWithSourcemap.from_source(
82-
filename, source.slice(last_end), get_location(last_end));
86+
file_basename, source.slice(last_end), get_location(last_end));
8387
return out.concat(final_content);
8488
}
8589

@@ -160,7 +164,7 @@ function decoded_sourcemap_from_generator(generator: any) {
160164
* Convert a preprocessor output and its leading prefix and trailing suffix into StringWithSourceMap
161165
*/
162166
function get_replacement(
163-
filename: string,
167+
file_basename: string,
164168
offset: number,
165169
get_location: ReturnType<typeof getLocator>,
166170
original: string,
@@ -171,9 +175,9 @@ function get_replacement(
171175

172176
// Convert the unchanged prefix and suffix to StringWithSourcemap
173177
const prefix_with_map = StringWithSourcemap.from_source(
174-
filename, prefix, get_location(offset));
178+
file_basename, prefix, get_location(offset));
175179
const suffix_with_map = StringWithSourcemap.from_source(
176-
filename, suffix, get_location(offset + prefix.length + original.length));
180+
file_basename, suffix, get_location(offset + prefix.length + original.length));
177181

178182
// Convert the preprocessed code and its sourcemap to a StringWithSourcemap
179183
let decoded_map: DecodedSourceMap;
@@ -186,7 +190,11 @@ function get_replacement(
186190
// import decoded sourcemap from mozilla/source-map/SourceMapGenerator
187191
decoded_map = decoded_sourcemap_from_generator(decoded_map);
188192
}
189-
sourcemap_add_offset(decoded_map, get_location(offset + prefix.length));
193+
// offset only segments pointing at original component source
194+
const source_index = decoded_map.sources.indexOf(file_basename);
195+
if (source_index !== -1) {
196+
sourcemap_add_offset(decoded_map, get_location(offset + prefix.length), source_index);
197+
}
190198
}
191199
const processed_with_map = StringWithSourcemap.from_processed(processed.code, decoded_map);
192200

@@ -203,6 +211,9 @@ export default async function preprocess(
203211
const filename = (options && options.filename) || preprocessor.filename; // legacy
204212
const dependencies = [];
205213

214+
// preprocess source must be relative to itself or equal null
215+
const file_basename = filename == null ? null : get_file_basename(filename);
216+
206217
const preprocessors = preprocessor
207218
? Array.isArray(preprocessor) ? preprocessor : [preprocessor]
208219
: [];
@@ -246,13 +257,13 @@ export default async function preprocess(
246257
: /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi;
247258

248259
const res = await replace_async(
249-
filename,
260+
file_basename,
250261
source,
251262
get_location,
252263
tag_regex,
253264
async (match, attributes = '', content = '', offset) => {
254265
const no_change = () => StringWithSourcemap.from_source(
255-
filename, match, get_location(offset));
266+
file_basename, match, get_location(offset));
256267
if (!attributes && !content) {
257268
return no_change();
258269
}
@@ -265,10 +276,13 @@ export default async function preprocess(
265276
attributes: parse_attributes(attributes),
266277
filename
267278
});
268-
269-
if (!processed) return no_change();
270-
if (processed.dependencies) dependencies.push(...processed.dependencies);
271-
return get_replacement(filename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, `</${tag_name}>`);
279+
if (processed && processed.dependencies) {
280+
dependencies.push(...processed.dependencies);
281+
}
282+
if (!processed || !processed.map && processed.code === content) {
283+
return no_change();
284+
}
285+
return get_replacement(file_basename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, `</${tag_name}>`);
272286
}
273287
);
274288
source = res.string;
@@ -285,7 +299,7 @@ export default async function preprocess(
285299

286300
// Combine all the source maps for each preprocessor function into one
287301
const map: RawSourceMap = combine_sourcemaps(
288-
filename,
302+
file_basename,
289303
sourcemap_list
290304
);
291305

src/compiler/utils/string_with_sourcemap.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,22 @@ function last_line_length(s: string) {
1313

1414
// mutate map in-place
1515
export function sourcemap_add_offset(
16-
map: DecodedSourceMap, offset: SourceLocation
16+
map: DecodedSourceMap, offset: SourceLocation, source_index: number
1717
) {
18-
if (map.mappings.length == 0) return map;
19-
// shift columns in first line
20-
const segment_list = map.mappings[0];
21-
for (let segment = 0; segment < segment_list.length; segment++) {
22-
const seg = segment_list[segment];
23-
if (seg[3]) seg[3] += offset.column;
24-
}
25-
// shift lines
18+
if (map.mappings.length == 0) return;
2619
for (let line = 0; line < map.mappings.length; line++) {
2720
const segment_list = map.mappings[line];
2821
for (let segment = 0; segment < segment_list.length; segment++) {
2922
const seg = segment_list[segment];
30-
if (seg[2]) seg[2] += offset.line;
23+
// shift only segments that belong to component source file
24+
if (seg[1] === source_index) { // also ensures that seg.length >= 4
25+
// shift column if it points at the first line
26+
if (seg[2] === 0) {
27+
seg[3] += offset.column;
28+
}
29+
// shift line
30+
seg[2] += offset.line;
31+
}
3132
}
3233
}
3334
}
@@ -97,6 +98,9 @@ export class StringWithSourcemap {
9798
return this;
9899
}
99100

101+
// compute last line length before mutating
102+
const column_offset = last_line_length(this.string);
103+
100104
this.string += other.string;
101105

102106
const m1 = this.map;
@@ -117,24 +121,24 @@ export class StringWithSourcemap {
117121
const segment_list = m2.mappings[line];
118122
for (let segment = 0; segment < segment_list.length; segment++) {
119123
const seg = segment_list[segment];
120-
if (seg[1]) seg[1] = new_source_idx[seg[1]];
121-
if (seg[4]) seg[4] = new_name_idx[seg[4]];
124+
if (seg[1] >= 0) seg[1] = new_source_idx[seg[1]];
125+
if (seg[4] >= 0) seg[4] = new_name_idx[seg[4]];
122126
}
123127
}
124128
} else if (sources_idx_changed) {
125129
for (let line = 0; line < m2.mappings.length; line++) {
126130
const segment_list = m2.mappings[line];
127131
for (let segment = 0; segment < segment_list.length; segment++) {
128132
const seg = segment_list[segment];
129-
if (seg[1]) seg[1] = new_source_idx[seg[1]];
133+
if (seg[1] >= 0) seg[1] = new_source_idx[seg[1]];
130134
}
131135
}
132136
} else if (names_idx_changed) {
133137
for (let line = 0; line < m2.mappings.length; line++) {
134138
const segment_list = m2.mappings[line];
135139
for (let segment = 0; segment < segment_list.length; segment++) {
136140
const seg = segment_list[segment];
137-
if (seg[4]) seg[4] = new_name_idx[seg[4]];
141+
if (seg[4] >= 0) seg[4] = new_name_idx[seg[4]];
138142
}
139143
}
140144
}
@@ -146,7 +150,6 @@ export class StringWithSourcemap {
146150
// 2. first line of second map
147151
// columns of 2 must be shifted
148152

149-
const column_offset = last_line_length(this.string);
150153
if (m2.mappings.length > 0 && column_offset > 0) {
151154
const first_line = m2.mappings[0];
152155
for (let i = 0; i < first_line.length; i++) {
@@ -164,12 +167,23 @@ export class StringWithSourcemap {
164167
}
165168

166169
static from_processed(string: string, map?: DecodedSourceMap): StringWithSourcemap {
167-
if (map) return new StringWithSourcemap(string, map);
170+
const line_count = string.split('\n').length;
171+
172+
if (map) {
173+
// ensure that count of source map mappings lines
174+
// is equal to count of generated code lines
175+
// (some tools may produce less)
176+
const missing_lines = line_count - map.mappings.length;
177+
for (let i = 0; i < missing_lines; i++) {
178+
map.mappings.push([]);
179+
}
180+
return new StringWithSourcemap(string, map);
181+
}
182+
168183
if (string == '') return new StringWithSourcemap();
169184
map = { version: 3, names: [], sources: [], mappings: [] };
170185

171186
// add empty SourceMapSegment[] for every line
172-
const line_count = (string.match(/\n/g) || '').length;
173187
for (let i = 0; i < line_count; i++) map.mappings.push([]);
174188
return new StringWithSourcemap(string, map);
175189
}

test/sourcemaps/helpers.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,111 @@
1-
import MagicString from 'magic-string';
1+
import * as assert from 'assert';
2+
import { getLocator } from 'locate-character';
3+
import MagicString, { Bundle } from 'magic-string';
4+
5+
type AssertMappedParameters = {
6+
code: string;
7+
filename?: string;
8+
input: string | ReturnType<typeof getLocator>;
9+
input_code?: string;
10+
preprocessed: any;
11+
};
12+
13+
export function assert_mapped(
14+
{ code, filename, input, input_code, preprocessed }: AssertMappedParameters
15+
) {
16+
const locate_input = typeof input === 'function' ? input : getLocator(input);
17+
if (filename === undefined) filename = 'input.svelte';
18+
if (input_code === undefined) input_code = code;
19+
20+
const source_loc = locate_input(input_code);
21+
assert.notEqual(
22+
source_loc,
23+
undefined,
24+
`failed to locate "${input_code}" in "${filename}"`
25+
);
26+
27+
const transformed_loc = preprocessed.locate_1(code);
28+
assert.notEqual(
29+
transformed_loc,
30+
undefined,
31+
`failed to locate "${code}" in transformed "${filename}"`
32+
);
33+
34+
assert.deepEqual(
35+
preprocessed.mapConsumer.originalPositionFor(transformed_loc),
36+
{
37+
source: filename,
38+
name: null,
39+
line: source_loc.line + 1,
40+
column: source_loc.column
41+
},
42+
`incorrect mappings for "${input_code}" in "${filename}"`
43+
);
44+
}
45+
46+
type AssertNotMappedParameters = {
47+
code: string;
48+
filename?: string;
49+
preprocessed: any;
50+
};
51+
52+
export function assert_not_mapped(
53+
{ code, filename, preprocessed }: AssertNotMappedParameters
54+
) {
55+
if (filename === undefined) filename = 'input.svelte';
56+
57+
const transformed_loc = preprocessed.locate_1(code);
58+
assert.notEqual(
59+
transformed_loc,
60+
undefined,
61+
`failed to locate "${code}" in transformed "${filename}"`
62+
);
63+
64+
assert.deepEqual(
65+
preprocessed.mapConsumer.originalPositionFor(transformed_loc),
66+
{
67+
source: null,
68+
name: null,
69+
line: null,
70+
column: null
71+
},
72+
`incorrect mappings for "${code}" in "${filename}"`
73+
);
74+
}
75+
76+
export function assert_not_located(
77+
code: string,
78+
locate: ReturnType<typeof getLocator>,
79+
filename = 'input.svelte'
80+
) {
81+
assert.equal(
82+
locate(code),
83+
undefined,
84+
`located "${code}" that should be removed from ${filename}`
85+
);
86+
}
87+
88+
export function magic_string_bundle(
89+
inputs: Array<{ code: string | MagicString, filename: string }>,
90+
filename = 'bundle.js',
91+
separator = '\n'
92+
) {
93+
const bundle = new Bundle({ separator });
94+
inputs.forEach(({ code, filename }) => {
95+
bundle.addSource({
96+
filename,
97+
content: typeof code === 'string' ? new MagicString(code) : code
98+
});
99+
});
100+
return {
101+
code: bundle.toString(),
102+
map: bundle.generateMap({
103+
source: filename,
104+
hires: true,
105+
includeContent: false
106+
})
107+
};
108+
}
2109

3110
export function magic_string_preprocessor_result(filename: string, src: MagicString) {
4111
return {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { magic_string_bundle } from '../../helpers';
2+
3+
export const COMMON = ':global(html) { height: 100%; }\n';
4+
5+
// TODO: removing '\n' breaks test
6+
// - _actual.svelte.map looks correct
7+
// - _actual.css.map adds reference to </style> on input.svelte
8+
// - Most probably caused by bug in current magic-string version (fixed in 0.25.7)
9+
export const STYLES = '.awesome { color: orange; }\n';
10+
11+
export default {
12+
css_map_sources: ['common.scss', 'styles.scss'],
13+
js_map_sources: [],
14+
preprocess: [
15+
{
16+
style: () => {
17+
return magic_string_bundle([
18+
{ filename: 'common.scss', code: COMMON },
19+
{ filename: 'styles.scss', code: STYLES }
20+
]);
21+
}
22+
}
23+
]
24+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<style lang="scss" src="./styles.scss"></style>
2+
3+
<div class="awesome">Divs ftw!</div>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { assert_mapped } from '../../helpers';
2+
import { COMMON, STYLES } from './_config';
3+
4+
export function test({ input, preprocessed }) {
5+
// Transformed script, main file
6+
assert_mapped({
7+
filename: 'input.svelte',
8+
code: 'Divs ftw!',
9+
input: input.locate,
10+
preprocessed
11+
});
12+
13+
// External files
14+
assert_mapped({
15+
filename: 'common.scss',
16+
code: 'height: 100%;',
17+
input: COMMON,
18+
preprocessed
19+
});
20+
assert_mapped({
21+
filename: 'styles.scss',
22+
code: 'color: orange;',
23+
input: STYLES,
24+
preprocessed
25+
});
26+
}

0 commit comments

Comments
 (0)