Skip to content

Commit 4c9b108

Browse files
halfnelsonmilahubenmccann
authored andcommitted
Preprocessor sourcemap support (sveltejs#5584)
Co-authored-by: Milan Hauth <[email protected]> Co-authored-by: Ben McCann <[email protected]>
1 parent c9ebe95 commit 4c9b108

File tree

36 files changed

+970
-43
lines changed

36 files changed

+970
-43
lines changed

package-lock.json

+19-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
},
9595
"homepage": "https://github.com/sveltejs/svelte#README",
9696
"devDependencies": {
97+
"@ampproject/remapping": "^0.3.0",
9798
"@rollup/plugin-commonjs": "^11.0.0",
9899
"@rollup/plugin-json": "^4.0.1",
99100
"@rollup/plugin-node-resolve": "^6.0.0",
@@ -127,6 +128,7 @@
127128
"rollup": "^1.27.14",
128129
"source-map": "^0.7.3",
129130
"source-map-support": "^0.5.13",
131+
"sourcemap-codec": "^1.4.8",
130132
"tiny-glob": "^0.2.6",
131133
"tslib": "^1.10.0",
132134
"typescript": "^3.5.3"

src/compiler/compile/Component.ts

+4
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ 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 { apply_preprocessor_sourcemap } from '../utils/string_with_sourcemap';
3233
import Element from './nodes/Element';
34+
import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping/dist/types/types';
3335

3436
interface ComponentOptions {
3537
namespace?: string;
@@ -326,6 +328,8 @@ export default class Component {
326328
js.map.sourcesContent = [
327329
this.source
328330
];
331+
332+
js.map = apply_preprocessor_sourcemap(this.file, js.map, compile_options.sourcemap as (string | RawSourceMap | DecodedSourceMap));
329333
}
330334

331335
return {

src/compiler/compile/index.ts

+1
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/compile/render_dom/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { extract_names, Scope } from '../utils/scope';
77
import { invalidate } from './invalidate';
88
import Block from './Block';
99
import { ClassDeclaration, FunctionExpression, Node, Statement, ObjectExpression, Expression } from 'estree';
10+
import { apply_preprocessor_sourcemap } from '../../utils/string_with_sourcemap';
11+
import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types';
1012

1113
export default function dom(
1214
component: Component,
@@ -30,6 +32,9 @@ export default function dom(
3032
}
3133

3234
const css = component.stylesheet.render(options.filename, !options.customElement);
35+
36+
css.map = apply_preprocessor_sourcemap(options.filename, css.map, options.sourcemap as string | RawSourceMap | DecodedSourceMap);
37+
3338
const styles = component.stylesheet.has_styles && options.dev
3439
? `${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */`
3540
: css.code;

src/compiler/interfaces.ts

+1
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

+118-36
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 are 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,55 @@ 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+
/**
87+
* Convert a preprocessor output and its leading prefix and trailing suffix into StringWithSourceMap
88+
*/
89+
function get_replacement(
90+
filename: string,
91+
offset: number,
92+
get_location: ReturnType<typeof getLocator>,
93+
original: string,
94+
processed: Processed,
95+
prefix: string,
96+
suffix: string
97+
): StringWithSourcemap {
98+
99+
// Convert the unchanged prefix and suffix to StringWithSourcemap
100+
const prefix_with_map = StringWithSourcemap.from_source(
101+
filename, prefix, get_location(offset));
102+
const suffix_with_map = StringWithSourcemap.from_source(
103+
filename, suffix, get_location(offset + prefix.length + original.length));
104+
105+
// Convert the preprocessed code and its sourcemap to a StringWithSourcemap
106+
let decoded_map: DecodedSourceMap;
107+
if (processed.map) {
108+
decoded_map = typeof processed.map === 'string' ? JSON.parse(processed.map) : processed.map;
109+
if (typeof(decoded_map.mappings) === 'string') {
110+
decoded_map.mappings = decode_mappings(decoded_map.mappings);
111+
}
112+
sourcemap_add_offset(decoded_map, get_location(offset + prefix.length));
113+
}
114+
const processed_with_map = StringWithSourcemap.from_processed(processed.code, decoded_map);
115+
116+
// Surround the processed code with the prefix and suffix, retaining valid sourcemappings
117+
return prefix_with_map.concat(processed_with_map).concat(suffix_with_map);
68118
}
69119

70120
export default async function preprocess(
@@ -76,60 +126,92 @@ export default async function preprocess(
76126
const filename = (options && options.filename) || preprocessor.filename; // legacy
77127
const dependencies = [];
78128

79-
const preprocessors = Array.isArray(preprocessor) ? preprocessor : [preprocessor];
129+
const preprocessors = preprocessor
130+
? Array.isArray(preprocessor) ? preprocessor : [preprocessor]
131+
: [];
80132

81133
const markup = preprocessors.map(p => p.markup).filter(Boolean);
82134
const script = preprocessors.map(p => p.script).filter(Boolean);
83135
const style = preprocessors.map(p => p.style).filter(Boolean);
84136

137+
// sourcemap_list is sorted in reverse order from last map (index 0) to first map (index -1)
138+
// so we use sourcemap_list.unshift() to add new maps
139+
// https://github.com/ampproject/remapping#multiple-transformations-of-a-file
140+
const sourcemap_list: Array<DecodedSourceMap | RawSourceMap> = [];
141+
142+
// TODO keep track: what preprocessor generated what sourcemap? to make debugging easier = detect low-resolution sourcemaps in fn combine_mappings
143+
85144
for (const fn of markup) {
145+
146+
// run markup preprocessor
86147
const processed = await fn({
87148
content: source,
88149
filename
89150
});
90-
if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
91-
source = processed ? processed.code : source;
151+
152+
if (!processed) continue;
153+
154+
if (processed.dependencies) dependencies.push(...processed.dependencies);
155+
source = processed.code;
156+
if (processed.map) {
157+
sourcemap_list.unshift(
158+
typeof(processed.map) === 'string'
159+
? JSON.parse(processed.map)
160+
: processed.map
161+
);
162+
}
92163
}
93164

94-
for (const fn of script) {
95-
source = await replace_async(
165+
async function preprocess_tag_content(tag_name: 'style' | 'script', preprocessor: Preprocessor) {
166+
const get_location = getLocator(source);
167+
const tag_regex = tag_name == 'style'
168+
? /<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi
169+
: /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi;
170+
171+
const res = await replace_async(
172+
filename,
96173
source,
97-
/<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi,
98-
async (match, attributes = '', content = '') => {
174+
get_location,
175+
tag_regex,
176+
async (match, attributes = '', content = '', offset) => {
177+
const no_change = () => StringWithSourcemap.from_source(
178+
filename, match, get_location(offset));
99179
if (!attributes && !content) {
100-
return match;
180+
return no_change();
101181
}
102182
attributes = attributes || '';
103-
const processed = await fn({
183+
content = content || '';
184+
185+
// run script preprocessor
186+
const processed = await preprocessor({
104187
content,
105188
attributes: parse_attributes(attributes),
106189
filename
107190
});
108-
if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
109-
return processed ? `<script${attributes}>${processed.code}</script>` : match;
191+
192+
if (!processed) return no_change();
193+
if (processed.dependencies) dependencies.push(...processed.dependencies);
194+
return get_replacement(filename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, `</${tag_name}>`);
110195
}
111196
);
197+
source = res.string;
198+
sourcemap_list.unshift(res.map);
199+
}
200+
201+
for (const fn of script) {
202+
await preprocess_tag_content('script', fn);
112203
}
113204

114205
for (const fn of style) {
115-
source = await replace_async(
116-
source,
117-
/<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi,
118-
async (match, attributes = '', content = '') => {
119-
if (!attributes && !content) {
120-
return match;
121-
}
122-
const processed: Processed = await fn({
123-
content,
124-
attributes: parse_attributes(attributes),
125-
filename
126-
});
127-
if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
128-
return processed ? `<style${attributes}>${processed.code}</style>` : match;
129-
}
130-
);
206+
await preprocess_tag_content('style', fn);
131207
}
132208

209+
// Combine all the source maps for each preprocessor function into one
210+
const map: RawSourceMap = combine_sourcemaps(
211+
filename,
212+
sourcemap_list
213+
);
214+
133215
return {
134216
// TODO return separated output, in future version where svelte.compile supports it:
135217
// style: { code: styleCode, map: styleMap },
@@ -138,7 +220,7 @@ export default async function preprocess(
138220

139221
code: source,
140222
dependencies: [...new Set(dependencies)],
141-
223+
map: (map as object),
142224
toString() {
143225
return source;
144226
}

0 commit comments

Comments
 (0)