Skip to content

Commit f0b9a29

Browse files
halfnelsonmilahu
andcommitted
Performance improvements, mild refactoring, and better css map support
Co-authored-by: Milan Hauth <[email protected]>
1 parent a2bef2f commit f0b9a29

File tree

8 files changed

+205
-89
lines changed

8 files changed

+205
-89
lines changed

src/compiler/compile/Component.ts

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +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 { combine_sourcemaps, sourcemap_define_tostring_tourl } from '../utils/string_with_sourcemap';
32+
import { apply_preprocessor_sourcemap } from '../utils/string_with_sourcemap';
3333
import Element from './nodes/Element';
34+
import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping/dist/types/types';
3435

3536
interface ComponentOptions {
3637
namespace?: string;
@@ -332,28 +333,7 @@ export default class Component {
332333
this.source
333334
];
334335

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-
}
336+
js.map = apply_preprocessor_sourcemap(this.file, js.map, compile_options.sourcemap as (string | RawSourceMap | DecodedSourceMap));
357337
}
358338

359339
return {

src/compiler/compile/render_dom/index.ts

Lines changed: 5 additions & 0 deletions
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/utils/string_with_sourcemap.ts

Lines changed: 97 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { DecodedSourceMap, RawSourceMap, SourceMapSegment, SourceMapLoader } from '@ampproject/remapping/dist/types/types';
1+
import { DecodedSourceMap, RawSourceMap, SourceMapLoader } from '@ampproject/remapping/dist/types/types';
22
import remapping from '@ampproject/remapping';
3+
import { SourceMap } from 'magic-string';
34

45
type SourceLocation = {
56
line: number;
@@ -14,17 +15,21 @@ function last_line_length(s: string) {
1415
export function sourcemap_add_offset(
1516
map: DecodedSourceMap, offset: SourceLocation
1617
) {
18+
if (map.mappings.length == 0) return map;
1719
// shift columns in first line
18-
const m = map.mappings;
19-
m[0].forEach(seg => {
20+
const segment_list = map.mappings[0];
21+
for (let segment = 0; segment < segment_list.length; segment++) {
22+
const seg = segment_list[segment];
2023
if (seg[3]) seg[3] += offset.column;
21-
});
24+
}
2225
// shift lines
23-
m.forEach(line => {
24-
line.forEach(seg => {
26+
for (let line = 0; line < map.mappings.length; line++) {
27+
const segment_list = map.mappings[line];
28+
for (let segment = 0; segment < segment_list.length; segment++) {
29+
const seg = segment_list[segment];
2530
if (seg[2]) seg[2] += offset.line;
26-
});
27-
});
31+
}
32+
}
2833
}
2934

3035
function merge_tables<T>(this_table: T[], other_table): [T[], number[], boolean, boolean] {
@@ -91,6 +96,8 @@ export class StringWithSourcemap {
9196
const m1 = this.map;
9297
const m2 = other.map;
9398

99+
if (m2.mappings.length == 0) return this;
100+
94101
// combine sources and names
95102
const [sources, new_source_idx, sources_changed, sources_idx_changed] = merge_tables(m1.sources, m2.sources);
96103
const [names, new_name_idx, names_changed, names_idx_changed] = merge_tables(m1.names, m2.names);
@@ -100,24 +107,30 @@ export class StringWithSourcemap {
100107

101108
// unswitched loops are faster
102109
if (sources_idx_changed && names_idx_changed) {
103-
m2.mappings.forEach(line => {
104-
line.forEach(seg => {
110+
for (let line = 0; line < m2.mappings.length; line++) {
111+
const segment_list = m2.mappings[line];
112+
for (let segment = 0; segment < segment_list.length; segment++) {
113+
const seg = segment_list[segment];
105114
if (seg[1]) seg[1] = new_source_idx[seg[1]];
106115
if (seg[4]) seg[4] = new_name_idx[seg[4]];
107-
});
108-
});
116+
}
117+
}
109118
} else if (sources_idx_changed) {
110-
m2.mappings.forEach(line => {
111-
line.forEach(seg => {
119+
for (let line = 0; line < m2.mappings.length; line++) {
120+
const segment_list = m2.mappings[line];
121+
for (let segment = 0; segment < segment_list.length; segment++) {
122+
const seg = segment_list[segment];
112123
if (seg[1]) seg[1] = new_source_idx[seg[1]];
113-
});
114-
});
124+
}
125+
}
115126
} else if (names_idx_changed) {
116-
m2.mappings.forEach(line => {
117-
line.forEach(seg => {
127+
for (let line = 0; line < m2.mappings.length; line++) {
128+
const segment_list = m2.mappings[line];
129+
for (let segment = 0; segment < segment_list.length; segment++) {
130+
const seg = segment_list[segment];
118131
if (seg[4]) seg[4] = new_name_idx[seg[4]];
119-
});
120-
});
132+
}
133+
}
121134
}
122135

123136
// combine the mappings
@@ -129,10 +142,10 @@ export class StringWithSourcemap {
129142

130143
const column_offset = last_line_length(this.string);
131144
if (m2.mappings.length > 0 && column_offset > 0) {
132-
// shift columns in first line
133-
m2.mappings[0].forEach(seg => {
134-
seg[0] += column_offset;
135-
});
145+
const first_line = m2.mappings[0];
146+
for (let i = 0; i < first_line.length; i++) {
147+
first_line[i][0] += column_offset;
148+
}
136149
}
137150

138151
// combine last line + first line
@@ -146,38 +159,40 @@ export class StringWithSourcemap {
146159

147160
static from_processed(string: string, map?: DecodedSourceMap): StringWithSourcemap {
148161
if (map) return new StringWithSourcemap(string, map);
162+
if (string == '') return new StringWithSourcemap();
149163
map = { version: 3, names: [], sources: [], mappings: [] };
150-
if (string == '') return new StringWithSourcemap(string, map);
164+
151165
// add empty SourceMapSegment[] for every line
152-
const lineCount = string.split('\n').length;
153-
map.mappings = Array.from({length: lineCount}).map(_ => []);
166+
const line_count = (string.match(/\n/g) || '').length;
167+
for (let i = 0; i < line_count; i++) map.mappings.push([]);
154168
return new StringWithSourcemap(string, map);
155169
}
156170

157171
static from_source(
158-
source_file: string, source: string, offset_in_source?: SourceLocation
172+
source_file: string, source: string, offset?: SourceLocation
159173
): StringWithSourcemap {
160-
const offset = offset_in_source || { line: 0, column: 0 };
174+
if (!offset) offset = { line: 0, column: 0 };
161175
const map: DecodedSourceMap = { version: 3, names: [], sources: [source_file], mappings: [] };
162-
if (source.length == 0) return new StringWithSourcemap(source, map);
176+
if (source == '') return new StringWithSourcemap(source, map);
163177

164178
// we create a high resolution identity map here,
165179
// we know that it will eventually be merged with svelte's map,
166180
// at which stage the resolution will decrease.
167-
map.mappings = source.split('\n').map((line, line_idx) => {
168-
let pos = 0;
169-
const segs = line.split(/([^\d\w\s]|\s+)/g)
170-
.filter(s => s !== '').map(s => {
171-
const seg: SourceMapSegment = [
172-
pos, 0,
173-
line_idx + offset.line,
174-
pos + (line_idx == 0 ? offset.column : 0) // shift first line
175-
];
176-
pos = pos + s.length;
177-
return seg;
178-
});
179-
return segs;
180-
});
181+
const line_list = source.split('\n');
182+
for (let line = 0; line < line_list.length; line++) {
183+
map.mappings.push([]);
184+
const token_list = line_list[line].split(/([^\d\w\s]|\s+)/g);
185+
for (let token = 0, column = 0; token < token_list.length; token++) {
186+
if (token_list[token] == '') continue;
187+
map.mappings[line].push([column, 0, offset.line + line, column]);
188+
column += token_list[token].length;
189+
}
190+
}
191+
// shift columns in first line
192+
const segment_list = map.mappings[0];
193+
for (let segment = 0; segment < segment_list.length; segment++) {
194+
segment_list[segment][3] += offset.column;
195+
}
181196
return new StringWithSourcemap(source, map);
182197
}
183198
}
@@ -191,34 +206,51 @@ export function combine_sourcemaps(
191206
let map_idx = 1;
192207
const map: RawSourceMap =
193208
sourcemap_list.slice(0, -1)
194-
.find(m => m.sources.length !== 1) === undefined
209+
.find(m => m.sources.length !== 1) === undefined
195210

196211
? remapping( // use array interface
197-
// only the oldest sourcemap can have multiple sources
198-
sourcemap_list,
199-
() => null,
200-
true // skip optional field `sourcesContent`
201-
)
212+
// only the oldest sourcemap can have multiple sources
213+
sourcemap_list,
214+
() => null,
215+
true // skip optional field `sourcesContent`
216+
)
202217

203218
: remapping( // use loader interface
204-
sourcemap_list[0], // last map
205-
function loader(sourcefile) {
206-
if (sourcefile === filename && sourcemap_list[map_idx]) {
207-
return sourcemap_list[map_idx++]; // idx 1, 2, ...
208-
// bundle file = branch node
209-
}
210-
else return null; // source file = leaf node
211-
} as SourceMapLoader,
212-
true
213-
);
219+
sourcemap_list[0], // last map
220+
function loader(sourcefile) {
221+
if (sourcefile === filename && sourcemap_list[map_idx]) {
222+
return sourcemap_list[map_idx++]; // idx 1, 2, ...
223+
// bundle file = branch node
224+
}
225+
else return null; // source file = leaf node
226+
} as SourceMapLoader,
227+
true
228+
);
214229

215230
if (!map.file) delete map.file; // skip optional field `file`
216231

217232
return map;
218233
}
219234

220-
export function sourcemap_define_tostring_tourl(map) {
221-
Object.defineProperties(map, {
235+
// browser vs node.js
236+
const b64enc = typeof btoa == 'function' ? btoa : b => Buffer.from(b).toString('base64');
237+
238+
export function apply_preprocessor_sourcemap(filename: string, svelte_map: SourceMap, preprocessor_map_input: string | DecodedSourceMap | RawSourceMap): SourceMap {
239+
if (!svelte_map || !preprocessor_map_input) return svelte_map;
240+
241+
const preprocessor_map = typeof preprocessor_map_input === 'string' ? JSON.parse(preprocessor_map_input) : preprocessor_map_input;
242+
243+
const result_map = combine_sourcemaps(
244+
filename,
245+
[
246+
svelte_map as RawSourceMap,
247+
preprocessor_map
248+
]
249+
) as RawSourceMap;
250+
251+
//Svelte expects a SourceMap which includes toUrl and toString. Instead of using the magic-string constructor that takes a decoded map
252+
//we just tack on the extra properties.
253+
Object.defineProperties(result_map, {
222254
toString: {
223255
enumerable: false,
224256
value: function toString() {
@@ -228,8 +260,10 @@ export function sourcemap_define_tostring_tourl(map) {
228260
toUrl: {
229261
enumerable: false,
230262
value: function toUrl() {
231-
return 'data:application/json;charset=utf-8;base64,' + btoa(this.toString());
263+
return 'data:application/json;charset=utf-8;base64,' + b64enc(this.toString());
232264
}
233265
}
234266
});
267+
268+
return result_map as SourceMap;
235269
}

test/setup.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require.extensions['.js'] = function(module, filename) {
1212
.replace(/^import (\w+) from ['"]([^'"]+)['"];?/gm, 'var {default: $1} = require("$2");')
1313
.replace(/^import {([^}]+)} from ['"](.+)['"];?/gm, 'var {$1} = require("$2");')
1414
.replace(/^export default /gm, 'exports.default = ')
15-
.replace(/^export (const|let|var|class|function) (\w+)/gm, (match, type, name) => {
15+
.replace(/^export (const|let|var|class|function|async\s+function) (\w+)/gm, (match, type, name) => {
1616
exports.push(name);
1717
return `${type} ${name}`;
1818
})

test/sourcemaps/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ describe('sourcemaps', () => {
4848
// filenames for sourcemaps
4949
sourcemap: preprocessed.map,
5050
outputFilename: `${outputName}.js`,
51-
cssOutputFilename: `${outputName}.css`
51+
cssOutputFilename: `${outputName}.css`,
52+
...(config.compile_options || {})
5253
});
5354

5455
js.code = js.code.replace(
@@ -108,7 +109,7 @@ describe('sourcemaps', () => {
108109
css.mapConsumer = css.map && await new SourceMapConsumer(css.map);
109110
css.locate = getLocator(css.code || '');
110111
css.locate_1 = getLocator(css.code || '', { offsetLine: 1 });
111-
test({ assert, input, preprocessed, js, css });
112+
await test({ assert, input, preprocessed, js, css });
112113
});
113114
});
114115
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import MagicString from 'magic-string';
2+
3+
// TODO move util fns to test index.js
4+
5+
function result(filename, src) {
6+
return {
7+
code: src.toString(),
8+
map: src.generateMap({
9+
source: filename,
10+
hires: true,
11+
includeContent: false
12+
})
13+
};
14+
}
15+
16+
function replace_all(src, search, replace) {
17+
let idx = src.original.indexOf(search);
18+
if (idx == -1) throw new Error('search not found in src');
19+
do {
20+
src.overwrite(idx, idx + search.length, replace);
21+
} while ((idx = src.original.indexOf(search, idx + 1)) != -1);
22+
}
23+
24+
export default {
25+
compile_options: {
26+
dev: true
27+
},
28+
preprocess: [
29+
{ style: ({ content, filename }) => {
30+
const src = new MagicString(content);
31+
replace_all(src, '--replace-me-once', '\n --done-replace-once');
32+
replace_all(src, '--replace-me-twice', '\n--almost-done-replace-twice');
33+
return result(filename, src);
34+
} },
35+
{ style: ({ content, filename }) => {
36+
const src = new MagicString(content);
37+
replace_all(src, '--almost-done-replace-twice', '\n --done-replace-twice');
38+
return result(filename, src);
39+
} }
40+
]
41+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<h1>Testing Styles</h1>
2+
<h2>Testing Styles 2</h2>
3+
<div>Testing Styles 3</div>
4+
<script>export const b = 2;</script>
5+
<style>
6+
h1 {
7+
--replace-me-once: red;
8+
}
9+
h2 {
10+
--replace-me-twice: green;
11+
}
12+
div {
13+
--keep-me: blue;
14+
}
15+
</style>

0 commit comments

Comments
 (0)