Skip to content

Commit 0d19f67

Browse files
authored
Parse attached sourcemap from preprocessor (#5854)
1 parent dbd184c commit 0d19f67

File tree

6 files changed

+156
-7
lines changed

6 files changed

+156
-7
lines changed

src/compiler/preprocess/index.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types';
22
import { decode as decode_mappings } from 'sourcemap-codec';
33
import { getLocator } from 'locate-character';
4-
import { StringWithSourcemap, sourcemap_add_offset, combine_sourcemaps } from '../utils/string_with_sourcemap';
4+
import {
5+
StringWithSourcemap,
6+
sourcemap_add_offset,
7+
combine_sourcemaps,
8+
parse_attached_sourcemap
9+
} from '../utils/string_with_sourcemap';
510

611
export interface Processed {
712
code: string;
@@ -170,7 +175,8 @@ function get_replacement(
170175
original: string,
171176
processed: Processed,
172177
prefix: string,
173-
suffix: string
178+
suffix: string,
179+
tag_name: 'script' | 'style'
174180
): StringWithSourcemap {
175181

176182
// Convert the unchanged prefix and suffix to StringWithSourcemap
@@ -179,6 +185,8 @@ function get_replacement(
179185
const suffix_with_map = StringWithSourcemap.from_source(
180186
file_basename, suffix, get_location(offset + prefix.length + original.length));
181187

188+
parse_attached_sourcemap(processed, tag_name);
189+
182190
// Convert the preprocessed code and its sourcemap to a StringWithSourcemap
183191
let decoded_map: DecodedSourceMap;
184192
if (processed.map) {
@@ -282,7 +290,7 @@ export default async function preprocess(
282290
if (!processed || !processed.map && processed.code === content) {
283291
return no_change();
284292
}
285-
return get_replacement(file_basename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, `</${tag_name}>`);
293+
return get_replacement(file_basename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, `</${tag_name}>`, tag_name);
286294
}
287295
);
288296
source = res.string;

src/compiler/utils/string_with_sourcemap.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { DecodedSourceMap, RawSourceMap, SourceMapLoader } from '@ampproject/remapping/dist/types/types';
22
import remapping from '@ampproject/remapping';
33
import { SourceMap } from 'magic-string';
4+
import { Processed } from '../preprocess';
45

56
type SourceLocation = {
67
line: number;
@@ -255,6 +256,7 @@ export function combine_sourcemaps(
255256

256257
// browser vs node.js
257258
const b64enc = typeof btoa == 'function' ? btoa : b => Buffer.from(b).toString('base64');
259+
const b64dec = typeof atob == 'function' ? atob : a => Buffer.from(a, 'base64').toString();
258260

259261
export function apply_preprocessor_sourcemap(filename: string, svelte_map: SourceMap, preprocessor_map_input: string | DecodedSourceMap | RawSourceMap): SourceMap {
260262
if (!svelte_map || !preprocessor_map_input) return svelte_map;
@@ -288,3 +290,39 @@ export function apply_preprocessor_sourcemap(filename: string, svelte_map: Sourc
288290

289291
return result_map as SourceMap;
290292
}
293+
294+
// parse attached sourcemap in processed.code
295+
export function parse_attached_sourcemap(processed: Processed, tag_name: 'script' | 'style'): void {
296+
const r_in = '[#@]\\s*sourceMappingURL\\s*=\\s*(\\S*)';
297+
const regex = (tag_name == 'script')
298+
? new RegExp('(?://'+r_in+')|(?:/\\*'+r_in+'\\s*\\*/)$')
299+
: new RegExp('/\\*'+r_in+'\\s*\\*/$');
300+
function log_warning(message) {
301+
// code_start: help to find preprocessor
302+
const code_start = processed.code.length < 100 ? processed.code : (processed.code.slice(0, 100) + ' [...]');
303+
console.warn(`warning: ${message}. processed.code = ${JSON.stringify(code_start)}`);
304+
}
305+
processed.code = processed.code.replace(regex, (_, match1, match2) => {
306+
const map_url = (tag_name == 'script') ? (match1 || match2) : match1;
307+
const map_data = (map_url.match(/data:(?:application|text)\/json;(?:charset[:=]\S+?;)?base64,(\S*)/) || [])[1];
308+
if (map_data) {
309+
// sourceMappingURL is data URL
310+
if (processed.map) {
311+
log_warning('Not implemented. ' +
312+
'Found sourcemap in both processed.code and processed.map. ' +
313+
'Please update your preprocessor to return only one sourcemap.');
314+
// ignore attached sourcemap
315+
return '';
316+
}
317+
processed.map = b64dec(map_data); // use attached sourcemap
318+
return ''; // remove from processed.code
319+
}
320+
// sourceMappingURL is path or URL
321+
if (!processed.map) {
322+
log_warning(`Found sourcemap path ${JSON.stringify(map_url)} in processed.code, but no sourcemap data. ` +
323+
'Please update your preprocessor to return sourcemap data directly.');
324+
}
325+
// ignore sourcemap path
326+
return ''; // remove from processed.code
327+
});
328+
}

test/sourcemaps/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ describe('sourcemaps', () => {
3131
const inputCode = fs.readFileSync(inputFile, 'utf-8');
3232
const input = {
3333
code: inputCode,
34-
locate: getLocator(inputCode)
34+
locate: getLocator(inputCode),
35+
locate_1: getLocator(inputCode, { offsetLine: 1 })
3536
};
3637

3738
const preprocessed = await svelte.preprocess(
@@ -41,7 +42,7 @@ describe('sourcemaps', () => {
4142
filename: 'input.svelte'
4243
}
4344
);
44-
45+
4546
const { js, css } = svelte.compile(
4647
preprocessed.code, {
4748
filename: 'input.svelte',
@@ -86,12 +87,14 @@ describe('sourcemaps', () => {
8687

8788
assert.deepEqual(
8889
js.map.sources.slice().sort(),
89-
(config.js_map_sources || ['input.svelte']).sort()
90+
(config.js_map_sources || ['input.svelte']).sort(),
91+
'js.map.sources is wrong'
9092
);
9193
if (css.map) {
9294
assert.deepEqual(
9395
css.map.sources.slice().sort(),
94-
(config.css_map_sources || ['input.svelte']).sort()
96+
(config.css_map_sources || ['input.svelte']).sort(),
97+
'css.map.sources is wrong'
9598
);
9699
}
97100

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import MagicString from 'magic-string';
2+
3+
let indent_size = 4;
4+
let comment_multi = true;
5+
function get_processor(tag_name, search, replace) {
6+
return {
7+
[tag_name]: ({ content, filename }) => {
8+
let code = content.slice();
9+
const ms = new MagicString(code);
10+
11+
const idx = ms.original.indexOf(search);
12+
if (idx == -1) throw new Error('search not found in src');
13+
ms.overwrite(idx, idx + search.length, replace, { storeName: true });
14+
15+
// change line + column
16+
const indent = Array.from({ length: indent_size }).join(' ');
17+
ms.prependLeft(idx, '\n'+indent);
18+
19+
const map_opts = { source: filename, hires: true, includeContent: false };
20+
const map = ms.generateMap(map_opts);
21+
const attach_line = (tag_name == 'style' || comment_multi)
22+
? `\n/*# sourceMappingURL=${map.toUrl()} */`
23+
: `\n//# sourceMappingURL=${map.toUrl()}` // only in script
24+
;
25+
code = ms.toString() + attach_line;
26+
27+
indent_size += 2;
28+
if (tag_name == 'script') comment_multi = !comment_multi;
29+
return { code };
30+
}
31+
};
32+
}
33+
34+
export default {
35+
preprocess: [
36+
37+
get_processor('script', 'replace_me_script', 'done_replace_script_1'),
38+
get_processor('script', 'done_replace_script_1', 'done_replace_script_2'),
39+
40+
get_processor('style', '.replace_me_style', '.done_replace_style_1'),
41+
get_processor('style', '.done_replace_style_1', '.done_replace_style_2')
42+
43+
]
44+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<style>
2+
.replace_me_style {
3+
color: red;
4+
}
5+
</style>
6+
<script>
7+
let
8+
replace_me_script = 'hello'
9+
;
10+
</script>
11+
<h1 class="done_replace_style_2">{done_replace_script_2}</h1>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as assert from 'assert';
2+
3+
const get_line_column = obj => ({ line: obj.line, column: obj.column });
4+
5+
export function test({ input, css, js }) {
6+
7+
let out_obj, loc_output, actual, loc_input, expected;
8+
9+
out_obj = js;
10+
// we need the second occurence of 'done_replace_script_2' in output.js
11+
// the first occurence is mapped back to markup '{done_replace_script_2}'
12+
loc_output = out_obj.locate_1('done_replace_script_2');
13+
loc_output = out_obj.locate_1('done_replace_script_2', loc_output.character + 1);
14+
actual = out_obj.mapConsumer.originalPositionFor(loc_output);
15+
loc_input = input.locate_1('replace_me_script');
16+
expected = {
17+
source: 'input.svelte',
18+
name: 'replace_me_script',
19+
...get_line_column(loc_input)
20+
};
21+
assert.deepEqual(actual, expected);
22+
23+
out_obj = css;
24+
loc_output = out_obj.locate_1('.done_replace_style_2');
25+
actual = out_obj.mapConsumer.originalPositionFor(loc_output);
26+
loc_input = input.locate_1('.replace_me_style');
27+
expected = {
28+
source: 'input.svelte',
29+
name: '.replace_me_style',
30+
...get_line_column(loc_input)
31+
};
32+
assert.deepEqual(actual, expected);
33+
34+
assert.equal(
35+
js.code.indexOf('\n/*# sourceMappingURL=data:application/json;base64,'),
36+
-1,
37+
'magic-comment attachments were NOT removed'
38+
);
39+
40+
assert.equal(
41+
css.code.indexOf('\n/*# sourceMappingURL=data:application/json;base64,'),
42+
-1,
43+
'magic-comment attachments were NOT removed'
44+
);
45+
}

0 commit comments

Comments
 (0)