Skip to content

Commit b1124af

Browse files
halfnelsonmilahu
andcommitted
Add sourcemap support to preprocessors
Co-authored-by: Milan Hauth <[email protected]>
1 parent a13ab60 commit b1124af

File tree

33 files changed

+996
-22
lines changed

33 files changed

+996
-22
lines changed

package-lock.json

Lines changed: 19 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
},
5757
"homepage": "https://github.com/sveltejs/svelte#README",
5858
"devDependencies": {
59+
"@ampproject/remapping": "^0.3.0",
5960
"@rollup/plugin-commonjs": "^11.0.0",
6061
"@rollup/plugin-json": "^4.0.1",
6162
"@rollup/plugin-node-resolve": "^6.0.0",
@@ -89,6 +90,7 @@
8990
"rollup": "^1.27.14",
9091
"source-map": "^0.7.3",
9192
"source-map-support": "^0.5.13",
93+
"sourcemap-codec": "^1.4.8",
9294
"tiny-glob": "^0.2.6",
9395
"tslib": "^1.10.0",
9496
"typescript": "^3.5.3"

src/compiler/compile/Component.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import add_to_set from './utils/add_to_set';
2929
import check_graph_for_cycles from './utils/check_graph_for_cycles';
3030
import { print, x, b } from 'code-red';
3131
import { is_reserved_keyword } from './utils/reserved_keywords';
32+
import { combine_sourcemaps, sourcemap_define_tostring_tourl } from '../utils/string_with_sourcemap';
3233
import Element from './nodes/Element';
3334

3435
interface ComponentOptions {
@@ -330,6 +331,29 @@ export default class Component {
330331
js.map.sourcesContent = [
331332
this.source
332333
];
334+
335+
if (compile_options.sourcemap) {
336+
if (js.map) {
337+
js.map = combine_sourcemaps(
338+
this.file,
339+
[
340+
js.map, // idx 1: internal
341+
compile_options.sourcemap // idx 0: external: svelte.preprocess, etc
342+
]
343+
);
344+
sourcemap_define_tostring_tourl(js.map);
345+
}
346+
if (css.map) {
347+
css.map = combine_sourcemaps(
348+
this.file,
349+
[
350+
css.map, // idx 1: internal
351+
compile_options.sourcemap // idx 0: external: svelte.preprocess, etc
352+
]
353+
);
354+
sourcemap_define_tostring_tourl(css.map);
355+
}
356+
}
333357
}
334358

335359
return {

src/compiler/compile/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const valid_options = [
1111
'format',
1212
'name',
1313
'filename',
14+
'sourcemap',
1415
'generate',
1516
'outputFilename',
1617
'cssOutputFilename',

src/compiler/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export interface CompileOptions {
110110
filename?: string;
111111
generate?: 'dom' | 'ssr' | false;
112112

113+
sourcemap?: object | string;
113114
outputFilename?: string;
114115
cssOutputFilename?: string;
115116
sveltePath?: string;

src/compiler/preprocess/index.ts

Lines changed: 112 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types';
2+
import { decode as decode_mappings } from 'sourcemap-codec';
3+
import { getLocator } from 'locate-character';
4+
import { StringWithSourcemap, sourcemap_add_offset, combine_sourcemaps } from '../utils/string_with_sourcemap';
5+
16
export interface Processed {
27
code: string;
3-
map?: object | string;
8+
map?: string | object; // we be opaque with the type here to avoid dependency on the remapping module for our public types.
49
dependencies?: string[];
510
}
611

@@ -37,12 +42,18 @@ function parse_attributes(str: string) {
3742
interface Replacement {
3843
offset: number;
3944
length: number;
40-
replacement: string;
45+
replacement: StringWithSourcemap;
4146
}
4247

43-
async function replace_async(str: string, re: RegExp, func: (...any) => Promise<string>) {
48+
async function replace_async(
49+
filename: string,
50+
source: string,
51+
get_location: ReturnType<typeof getLocator>,
52+
re: RegExp,
53+
func: (...any) => Promise<StringWithSourcemap>
54+
): Promise<StringWithSourcemap> {
4455
const replacements: Array<Promise<Replacement>> = [];
45-
str.replace(re, (...args) => {
56+
source.replace(re, (...args) => {
4657
replacements.push(
4758
func(...args).then(
4859
res =>
@@ -55,16 +66,52 @@ async function replace_async(str: string, re: RegExp, func: (...any) => Promise<
5566
);
5667
return '';
5768
});
58-
let out = '';
69+
const out = new StringWithSourcemap();
5970
let last_end = 0;
6071
for (const { offset, length, replacement } of await Promise.all(
6172
replacements
6273
)) {
63-
out += str.slice(last_end, offset) + replacement;
74+
// content = unchanged source characters before the replaced segment
75+
const content = StringWithSourcemap.from_source(
76+
filename, source.slice(last_end, offset), get_location(last_end));
77+
out.concat(content).concat(replacement);
6478
last_end = offset + length;
6579
}
66-
out += str.slice(last_end);
67-
return out;
80+
// final_content = unchanged source characters after last replaced segment
81+
const final_content = StringWithSourcemap.from_source(
82+
filename, source.slice(last_end), get_location(last_end));
83+
return out.concat(final_content);
84+
}
85+
86+
// Convert a preprocessor output and its leading prefix and trailing suffix into StringWithSourceMap
87+
function get_replacement(
88+
filename: string,
89+
offset: number,
90+
get_location: ReturnType<typeof getLocator>,
91+
original: string,
92+
processed: Processed,
93+
prefix: string,
94+
suffix: string
95+
): StringWithSourcemap {
96+
97+
// Convert the unchanged prefix and suffix to StringWithSourcemap
98+
const prefix_with_map = StringWithSourcemap.from_source(
99+
filename, prefix, get_location(offset));
100+
const suffix_with_map = StringWithSourcemap.from_source(
101+
filename, suffix, get_location(offset + prefix.length + original.length));
102+
103+
// Convert the preprocessed code and its sourcemap to a StringWithSourcemap
104+
let decoded_map: DecodedSourceMap;
105+
if (processed.map) {
106+
decoded_map = typeof processed.map === 'string' ? JSON.parse(processed.map) : processed.map;
107+
if (typeof(decoded_map.mappings) === 'string')
108+
decoded_map.mappings = decode_mappings(decoded_map.mappings);
109+
sourcemap_add_offset(decoded_map, get_location(offset + prefix.length));
110+
}
111+
const processed_with_map = StringWithSourcemap.from_processed(processed.code, decoded_map);
112+
113+
// Surround the processed code with the prefix and suffix, retaining valid sourcemappings
114+
return prefix_with_map.concat(processed_with_map).concat(suffix_with_map);
68115
}
69116

70117
export default async function preprocess(
@@ -76,60 +123,107 @@ export default async function preprocess(
76123
const filename = (options && options.filename) || preprocessor.filename; // legacy
77124
const dependencies = [];
78125

79-
const preprocessors = Array.isArray(preprocessor) ? preprocessor : [preprocessor];
126+
const preprocessors = Array.isArray(preprocessor) ? preprocessor : [preprocessor || {}];
80127

81128
const markup = preprocessors.map(p => p.markup).filter(Boolean);
82129
const script = preprocessors.map(p => p.script).filter(Boolean);
83130
const style = preprocessors.map(p => p.style).filter(Boolean);
84131

132+
// sourcemap_list is sorted in reverse order from last map (index 0) to first map (index -1)
133+
// so we use sourcemap_list.unshift() to add new maps
134+
// https://github.com/ampproject/remapping#multiple-transformations-of-a-file
135+
const sourcemap_list: Array<DecodedSourceMap | RawSourceMap> = [];
136+
137+
// TODO keep track: what preprocessor generated what sourcemap? to make debugging easier = detect low-resolution sourcemaps in fn combine_mappings
138+
85139
for (const fn of markup) {
140+
141+
// run markup preprocessor
86142
const processed = await fn({
87143
content: source,
88144
filename
89145
});
146+
90147
if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
91148
source = processed ? processed.code : source;
149+
if (processed && processed.map)
150+
sourcemap_list.unshift(
151+
typeof(processed.map) === 'string'
152+
? JSON.parse(processed.map)
153+
: processed.map
154+
);
92155
}
93156

94157
for (const fn of script) {
95-
source = await replace_async(
158+
const get_location = getLocator(source);
159+
const res = await replace_async(
160+
filename,
96161
source,
162+
get_location,
97163
/<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi,
98-
async (match, attributes = '', content = '') => {
164+
async (match, attributes = '', content = '', offset) => {
165+
const no_change = () => StringWithSourcemap.from_source(
166+
filename, match, get_location(offset));
99167
if (!attributes && !content) {
100-
return match;
168+
return no_change();
101169
}
102170
attributes = attributes || '';
171+
content = content || '';
172+
173+
// run script preprocessor
103174
const processed = await fn({
104175
content,
105176
attributes: parse_attributes(attributes),
106177
filename
107178
});
108179
if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
109-
return processed ? `<script${attributes}>${processed.code}</script>` : match;
180+
return processed
181+
? get_replacement(filename, offset, get_location, content, processed, `<script${attributes}>`, '</script>')
182+
: no_change();
110183
}
111184
);
185+
source = res.string;
186+
sourcemap_list.unshift(res.map);
112187
}
113188

114189
for (const fn of style) {
115-
source = await replace_async(
190+
const get_location = getLocator(source);
191+
const res = await replace_async(
192+
filename,
116193
source,
194+
get_location,
117195
/<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi,
118-
async (match, attributes = '', content = '') => {
196+
async (match, attributes = '', content = '', offset) => {
197+
const no_change = () => StringWithSourcemap.from_source(
198+
filename, match, get_location(offset));
119199
if (!attributes && !content) {
120-
return match;
200+
return no_change();
121201
}
202+
attributes = attributes || '';
203+
content = content || '';
204+
205+
// run style preprocessor
122206
const processed: Processed = await fn({
123207
content,
124208
attributes: parse_attributes(attributes),
125209
filename
126210
});
127211
if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
128-
return processed ? `<style${attributes}>${processed.code}</style>` : match;
212+
return processed
213+
? get_replacement(filename, offset, get_location, content, processed, `<style${attributes}>`, '</style>')
214+
: no_change();
129215
}
130216
);
217+
source = res.string;
218+
sourcemap_list.unshift(res.map);
131219
}
132220

221+
// Combine all the source maps for each preprocessor function into one
222+
const map: RawSourceMap = combine_sourcemaps(
223+
filename,
224+
sourcemap_list
225+
);
226+
133227
return {
134228
// TODO return separated output, in future version where svelte.compile supports it:
135229
// style: { code: styleCode, map: styleMap },
@@ -138,7 +232,7 @@ export default async function preprocess(
138232

139233
code: source,
140234
dependencies: [...new Set(dependencies)],
141-
235+
map: (map as object),
142236
toString() {
143237
return source;
144238
}

0 commit comments

Comments
 (0)