Skip to content

Commit 927ac71

Browse files
authored
feat: generate preprocessor sourcemaps (#101)
* feat: generate preprocessor sourcemaps * fix: lint error after format * fix: set filename as source instead of output name for sourcemap
1 parent bc74e46 commit 927ac71

File tree

10 files changed

+198
-25
lines changed

10 files changed

+198
-25
lines changed

.changeset/plenty-pumpkins-buy.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/vite-plugin-svelte': patch
3+
---
4+
5+
reduce log output with log.once function to filter repetetive messages

.changeset/proud-dragons-shout.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@sveltejs/vite-plugin-svelte': minor
3+
---
4+
5+
Experimental: Generate sourcemaps for preprocessors that lack them
6+
7+
enable option `experimental.generateMissingPreprocessorSourcemaps` to use it

packages/vite-plugin-svelte/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"dependencies": {
4747
"@rollup/pluginutils": "^4.1.0",
4848
"debug": "^4.3.2",
49+
"diff-match-patch": "^1.0.5",
4950
"kleur": "^4.1.4",
5051
"magic-string": "^0.25.7",
5152
"require-relative": "^0.8.7",
@@ -57,6 +58,7 @@
5758
},
5859
"devDependencies": {
5960
"@types/debug": "^4.1.6",
61+
"@types/diff-match-patch": "^1.0.32",
6062
"esbuild": "^0.12.15",
6163
"rollup": "^2.53.1",
6264
"svelte": "^3.38.3",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { buildMagicString, buildSourceMap } from '../sourcemap';
2+
3+
describe('sourcemap', () => {
4+
describe('buildMagicString', () => {
5+
it('should return a valid magic string', () => {
6+
const from = 'h1{color: blue}\nh2{color: green}\nh3{color: red}\n';
7+
const to = 'h1{color: blue}\ndiv{color: white}\nh3{color: red}\nh2{color: green}\n';
8+
const m = buildMagicString(from, to);
9+
expect(m).toBeDefined();
10+
expect(m.original).toBe(from);
11+
expect(m.toString()).toBe(to);
12+
});
13+
});
14+
describe('buildSourceMap', () => {
15+
it('should return a map with mappings and filename', () => {
16+
const map = buildSourceMap('foo', 'bar', 'foo.txt');
17+
expect(map).toBeDefined();
18+
expect(map.mappings).toBeDefined();
19+
expect(map.mappings[0]).toBeDefined();
20+
expect(map.mappings[0][0]).toBeDefined();
21+
expect(map.sources[0]).toBe('foo.txt');
22+
});
23+
});
24+
});

packages/vite-plugin-svelte/src/utils/log.ts

+14
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,30 @@ function _log(logger: any, message: string, payload?: any) {
6464
export interface LogFn {
6565
(message: string, payload?: any): void;
6666
enabled: boolean;
67+
once: (message: string, payload?: any) => void;
6768
}
6869

6970
function createLogger(level: string): LogFn {
7071
const logger = loggers[level];
7172
const logFn: LogFn = _log.bind(null, logger) as LogFn;
73+
const logged = new Set<String>();
74+
const once = function (message: string, payload?: any) {
75+
if (logged.has(message)) {
76+
return;
77+
}
78+
logged.add(message);
79+
logFn.apply(null, [message, payload]);
80+
};
7281
Object.defineProperty(logFn, 'enabled', {
7382
get() {
7483
return logger.enabled;
7584
}
7685
});
86+
Object.defineProperty(logFn, 'once', {
87+
get() {
88+
return once;
89+
}
90+
});
7791
return logFn;
7892
}
7993

packages/vite-plugin-svelte/src/utils/options.ts

+5
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,11 @@ export interface ExperimentalOptions {
405405
* @default false
406406
*/
407407
useVitePreprocess?: boolean;
408+
409+
/**
410+
* wrap all preprocessors in with a function that adds a sourcemap to the output if missing
411+
*/
412+
generateMissingPreprocessorSourcemaps?: boolean;
408413
}
409414

410415
export interface ResolvedOptions extends Options {

packages/vite-plugin-svelte/src/utils/preprocess.ts

+63-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { ResolvedConfig, TransformResult, Plugin } from 'vite';
22
import MagicString from 'magic-string';
3-
import { Preprocessor, PreprocessorGroup, ResolvedOptions } from './options';
3+
import { Preprocessor, PreprocessorGroup, Processed, ResolvedOptions } from './options';
44
import { TransformPluginContext } from 'rollup';
55
import { log } from './log';
6+
import { buildSourceMap } from './sourcemap';
67

78
const supportedStyleLangs = ['css', 'less', 'sass', 'scss', 'styl', 'stylus', 'postcss'];
89

@@ -38,13 +39,13 @@ function createPreprocessorFromVitePlugin(
3839
moduleId
3940
)) as TransformResult;
4041
// TODO vite:css transform currently returns an empty mapping that would kill svelte compiler.
41-
const hasMap = !!transformResult.map?.mappings;
42+
const hasMap = transformResult.map && transformResult.map?.mappings !== '';
4243
if (transformResult.map?.sources?.[0] === moduleId) {
4344
transformResult.map.sources[0] = filename as string;
4445
}
4546
return {
4647
code: transformResult.code,
47-
map: hasMap ? (transformResult.map as object) : null,
48+
map: hasMap ? (transformResult.map as object) : undefined,
4849
dependencies: transformResult.deps
4950
};
5051
};
@@ -73,7 +74,7 @@ function createInjectScopeEverythingRulePreprocessorGroup(): PreprocessorGroup {
7374
s.append(' *{}');
7475
return {
7576
code: s.toString(),
76-
map: s.generateDecodedMap({ file: filename })
77+
map: s.generateDecodedMap({ source: filename, hires: true })
7778
};
7879
}
7980
};
@@ -158,4 +159,62 @@ export function addExtraPreprocessors(options: ResolvedOptions, config: Resolved
158159
options.preprocess = [options.preprocess, ...extra];
159160
}
160161
}
162+
const generateMissingSourceMaps = !!options.experimental?.generateMissingPreprocessorSourcemaps;
163+
if (options.preprocess && generateMissingSourceMaps) {
164+
options.preprocess = Array.isArray(options.preprocess)
165+
? options.preprocess.map((p, i) => validateSourceMapOutputWrapper(p, i))
166+
: validateSourceMapOutputWrapper(options.preprocess, 0);
167+
}
168+
}
169+
170+
function validateSourceMapOutputWrapper(group: PreprocessorGroup, i: number): PreprocessorGroup {
171+
const wrapper: PreprocessorGroup = {};
172+
173+
for (const [processorType, processorFn] of Object.entries(group) as Array<
174+
// eslint-disable-next-line no-unused-vars
175+
[keyof PreprocessorGroup, (options: { filename?: string; content: string }) => Processed]
176+
>) {
177+
wrapper[processorType] = async (options) => {
178+
const result = await processorFn(options);
179+
180+
if (result && result.code !== options.content) {
181+
let invalidMap = false;
182+
if (!result.map) {
183+
invalidMap = true;
184+
log.warn.enabled &&
185+
log.warn.once(
186+
`preprocessor at index ${i} did not return a sourcemap for ${processorType} transform`,
187+
{
188+
filename: options.filename,
189+
type: processorType,
190+
processor: processorFn.toString()
191+
}
192+
);
193+
} else if ((result.map as any)?.mappings === '') {
194+
invalidMap = true;
195+
log.warn.enabled &&
196+
log.warn.once(
197+
`preprocessor at index ${i} returned an invalid empty sourcemap for ${processorType} transform`,
198+
{
199+
filename: options.filename,
200+
type: processorType,
201+
processor: processorFn.toString()
202+
}
203+
);
204+
}
205+
if (invalidMap) {
206+
try {
207+
const map = buildSourceMap(options.content, result.code, options.filename);
208+
log.warn.once('adding generated sourcemap to preprocesor result');
209+
result.map = map;
210+
} catch (e) {
211+
log.error(`failed to build sourcemap`, e);
212+
}
213+
}
214+
}
215+
return result;
216+
};
217+
}
218+
219+
return wrapper;
161220
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import MagicString, { MagicStringOptions } from 'magic-string';
2+
import { diff_match_patch, DIFF_DELETE, DIFF_INSERT } from 'diff-match-patch';
3+
4+
export function buildMagicString(
5+
from: string,
6+
to: string,
7+
options?: MagicStringOptions
8+
): MagicString {
9+
const dmp = new diff_match_patch();
10+
const diffs = dmp.diff_main(from, to);
11+
dmp.diff_cleanupSemantic(diffs);
12+
const m = new MagicString(from, options);
13+
let pos = 0;
14+
for (let i = 0; i < diffs.length; i++) {
15+
const diff = diffs[i];
16+
const nextDiff = diffs[i + 1];
17+
if (diff[0] === DIFF_DELETE) {
18+
if (nextDiff?.[0] === DIFF_INSERT) {
19+
// delete followed by insert, use overwrite and skip ahead
20+
m.overwrite(pos, pos + diff[1].length, nextDiff[1]);
21+
i++;
22+
} else {
23+
m.remove(pos, pos + diff[1].length);
24+
}
25+
pos += diff[1].length;
26+
} else if (diff[0] === DIFF_INSERT) {
27+
if (nextDiff) {
28+
m.appendRight(pos, diff[1]);
29+
} else {
30+
m.append(diff[1]);
31+
}
32+
} else {
33+
// unchanged block, advance pos
34+
pos += diff[1].length;
35+
}
36+
}
37+
// at this point m.toString() === to
38+
return m;
39+
}
40+
41+
export function buildSourceMap(from: string, to: string, filename?: string) {
42+
// @ts-ignore
43+
const m = buildMagicString(from, to, { filename });
44+
return m.generateDecodedMap({ source: filename, hires: true, includeContent: false });
45+
}

pnpm-lock.yaml

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

scripts/jestPerTestSetup.ts

+19-18
Original file line numberDiff line numberDiff line change
@@ -156,25 +156,26 @@ afterAll(async () => {
156156
err = e;
157157
}
158158
}
159-
160-
// unlink node modules to prevent removal of linked modules on cleanup
161-
const temp_node_modules = path.join(tempDir, 'node_modules');
162-
try {
163-
await fs.unlink(temp_node_modules);
164-
} catch (e) {
165-
console.error(`failed to unlink ${temp_node_modules}`);
166-
if (!err) {
167-
err = e;
159+
if (tempDir) {
160+
// unlink node modules to prevent removal of linked modules on cleanup
161+
const temp_node_modules = path.join(tempDir, 'node_modules');
162+
try {
163+
await fs.unlink(temp_node_modules);
164+
} catch (e) {
165+
console.error(`failed to unlink ${temp_node_modules}`);
166+
if (!err) {
167+
err = e;
168+
}
168169
}
169-
}
170-
const logDir = path.join(testDir(), 'logs');
171-
const logFile = path.join(logDir, 'browser.log');
172-
try {
173-
await fs.writeFile(logFile, logs.join('\n'));
174-
} catch (e) {
175-
console.error(`failed to write browserlogs in ${logFile}`, e);
176-
if (!err) {
177-
err = e;
170+
const logDir = path.join(testDir(), 'logs');
171+
const logFile = path.join(logDir, 'browser.log');
172+
try {
173+
await fs.writeFile(logFile, logs.join('\n'));
174+
} catch (e) {
175+
console.error(`failed to write browserlogs in ${logFile}`, e);
176+
if (!err) {
177+
err = e;
178+
}
178179
}
179180
}
180181

0 commit comments

Comments
 (0)