Skip to content

Commit 8c97341

Browse files
committed
fix: handle sourcemaps in typescript transformer
- introduce two deps: magic-string, sorcery - produce a source map on each step of the process - store all source maps in a chain - refactor transformer to increase readability
1 parent e5a73db commit 8c97341

File tree

4 files changed

+294
-69
lines changed

4 files changed

+294
-69
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@
9393
"@types/pug": "^2.0.4",
9494
"@types/sass": "^1.16.0",
9595
"detect-indent": "^6.0.0",
96+
"magic-string": "^0.25.7",
97+
"sorcery": "^0.10.0",
9698
"strip-indent": "^3.0.0"
9799
},
98100
"peerDependencies": {

src/transformers/typescript.ts

Lines changed: 231 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,24 @@ import { dirname, isAbsolute, join } from 'path';
22

33
import ts from 'typescript';
44
import { compile } from 'svelte/compiler';
5+
import MagicString from 'magic-string';
6+
import sorcery from 'sorcery';
57

68
import { throwTypescriptError } from '../modules/errors';
79
import { createTagRegex, parseAttributes, stripTags } from '../modules/markup';
810
import type { Transformer, Options } from '../types';
911

10-
type CompilerOptions = Options.Typescript['compilerOptions'];
12+
type CompilerOptions = ts.CompilerOptions;
13+
14+
type SourceMapChain = {
15+
content: Record<string, string>;
16+
sourcemaps: Record<string, object>;
17+
};
1118

1219
function createFormatDiagnosticsHost(cwd: string): ts.FormatDiagnosticsHost {
1320
return {
14-
getCanonicalFileName: (fileName: string) => fileName,
21+
getCanonicalFileName: (fileName: string) =>
22+
fileName.replace('.injected.ts', ''),
1523
getCurrentDirectory: () => cwd,
1624
getNewLine: () => ts.sys.newLine,
1725
};
@@ -70,16 +78,37 @@ function getComponentScriptContent(markup: string): string {
7078
return '';
7179
}
7280

81+
function createSourceMapChain({
82+
filename,
83+
content,
84+
compilerOptions,
85+
}: {
86+
filename: string;
87+
content: string;
88+
compilerOptions: CompilerOptions;
89+
}): SourceMapChain | undefined {
90+
if (compilerOptions.sourceMap) {
91+
return {
92+
content: {
93+
[filename]: content,
94+
},
95+
sourcemaps: {},
96+
};
97+
}
98+
}
99+
73100
function injectVarsToCode({
74101
content,
75102
markup,
76103
filename,
77104
attributes,
105+
sourceMapChain,
78106
}: {
79107
content: string;
80108
markup?: string;
81-
filename?: string;
109+
filename: string;
82110
attributes?: Record<string, any>;
111+
sourceMapChain?: SourceMapChain;
83112
}): string {
84113
if (!markup) return content;
85114

@@ -93,90 +122,103 @@ function injectVarsToCode({
93122
const sep = '\nconst $$$$$$$$ = null;\n';
94123
const varsValues = vars.map((v) => v.name).join(',');
95124
const injectedVars = `const $$vars$$ = [${varsValues}];`;
125+
const injectedCode =
126+
attributes?.context === 'module'
127+
? `${sep}${getComponentScriptContent(markup)}\n${injectedVars}`
128+
: `${sep}${injectedVars}`;
129+
130+
if (sourceMapChain) {
131+
const s = new MagicString(content);
96132

97-
if (attributes?.context === 'module') {
98-
const componentScript = getComponentScriptContent(markup);
133+
s.append(injectedCode);
99134

100-
return `${content}${sep}${componentScript}\n${injectedVars}`;
135+
const fname = `${filename}.injected.ts`;
136+
const code = s.toString();
137+
const map = s.generateMap({
138+
source: filename,
139+
file: fname,
140+
});
141+
142+
sourceMapChain.content[fname] = code;
143+
sourceMapChain.sourcemaps[fname] = map;
144+
145+
return code;
101146
}
102147

103-
return `${content}${sep}${injectedVars}`;
148+
return `${content}${injectedCode}`;
104149
}
105150

106151
function stripInjectedCode({
107-
compiledCode,
152+
transpiledCode,
108153
markup,
154+
filename,
155+
sourceMapChain,
109156
}: {
110-
compiledCode: string;
157+
transpiledCode: string;
111158
markup?: string;
159+
filename: string;
160+
sourceMapChain?: SourceMapChain;
112161
}): string {
113-
return markup
114-
? compiledCode.slice(0, compiledCode.indexOf('const $$$$$$$$ = null;'))
115-
: compiledCode;
116-
}
162+
if (!markup) return transpiledCode;
117163

118-
export function loadTsconfig(
119-
compilerOptionsJSON: any,
120-
filename: string,
121-
tsOptions: Options.Typescript,
122-
) {
123-
if (typeof tsOptions.tsconfigFile === 'boolean') {
124-
return { errors: [], options: compilerOptionsJSON };
125-
}
164+
const injectedCodeStart = transpiledCode.indexOf('const $$$$$$$$ = null;');
126165

127-
let basePath = process.cwd();
166+
if (sourceMapChain) {
167+
const s = new MagicString(transpiledCode);
168+
const st = s.snip(0, injectedCodeStart);
128169

129-
const fileDirectory = (tsOptions.tsconfigDirectory ||
130-
dirname(filename)) as string;
170+
const source = `${filename}.transpiled.js`;
171+
const file = `${filename}.js`;
172+
const code = st.toString();
173+
const map = st.generateMap({
174+
source,
175+
file,
176+
});
131177

132-
let tsconfigFile =
133-
tsOptions.tsconfigFile ||
134-
ts.findConfigFile(fileDirectory, ts.sys.fileExists);
178+
sourceMapChain.content[file] = code;
179+
sourceMapChain.sourcemaps[file] = map;
135180

136-
tsconfigFile = isAbsolute(tsconfigFile)
137-
? tsconfigFile
138-
: join(basePath, tsconfigFile);
181+
return code;
182+
}
139183

140-
basePath = dirname(tsconfigFile);
184+
return transpiledCode.slice(0, injectedCodeStart);
185+
}
141186

142-
const { error, config } = ts.readConfigFile(tsconfigFile, ts.sys.readFile);
187+
async function concatSourceMaps({
188+
filename,
189+
markup,
190+
sourceMapChain,
191+
}: {
192+
filename: string;
193+
markup?: string;
194+
sourceMapChain?: SourceMapChain;
195+
}): Promise<string | object | undefined> {
196+
if (!sourceMapChain) return;
143197

144-
if (error) {
145-
throw new Error(formatDiagnostics(error, basePath));
198+
if (!markup) {
199+
return sourceMapChain.sourcemaps[`${filename}.js`];
146200
}
147201

148-
// Do this so TS will not search for initial files which might take a while
149-
config.include = [];
150-
151-
let { errors, options } = ts.parseJsonConfigFileContent(
152-
config,
153-
ts.sys,
154-
basePath,
155-
compilerOptionsJSON,
156-
tsconfigFile,
157-
);
158-
159-
// Filter out "no files found error"
160-
errors = errors.filter((d) => d.code !== 18003);
202+
const chain = await sorcery.load(`${filename}.js`, sourceMapChain);
161203

162-
return { errors, options };
204+
return chain.apply();
163205
}
164206

165-
const transformer: Transformer<Options.Typescript> = ({
166-
content,
207+
function getCompilerOptions({
167208
filename,
168-
markup,
169-
options = {},
170-
attributes,
171-
}) => {
209+
options,
210+
basePath,
211+
}: {
212+
filename: string;
213+
options: Options.Typescript;
214+
basePath: string;
215+
}): CompilerOptions {
172216
// default options
173217
const compilerOptionsJSON = {
174218
moduleResolution: 'node',
175219
target: 'es6',
176220
};
177221

178-
const basePath = process.cwd();
179-
180222
Object.assign(compilerOptionsJSON, options.compilerOptions);
181223

182224
const { errors, options: convertedCompilerOptions } =
@@ -203,19 +245,38 @@ const transformer: Transformer<Options.Typescript> = ({
203245
);
204246
}
205247

248+
return compilerOptions;
249+
}
250+
251+
function transpileTs({
252+
code,
253+
markup,
254+
filename,
255+
basePath,
256+
options,
257+
compilerOptions,
258+
sourceMapChain,
259+
}: {
260+
code: string;
261+
markup: string;
262+
filename: string;
263+
basePath: string;
264+
options: Options.Typescript;
265+
compilerOptions: CompilerOptions;
266+
sourceMapChain: SourceMapChain;
267+
}): { transpiledCode: string; diagnostics: ts.Diagnostic[] } {
268+
const fileName = markup ? `${filename}.injected.ts` : filename;
269+
206270
const {
207-
outputText: compiledCode,
208-
sourceMapText: map,
271+
outputText: transpiledCode,
272+
sourceMapText,
209273
diagnostics,
210-
} = ts.transpileModule(
211-
injectVarsToCode({ content, markup, filename, attributes }),
212-
{
213-
fileName: filename,
214-
compilerOptions,
215-
reportDiagnostics: options.reportDiagnostics !== false,
216-
transformers: markup ? {} : { before: [importTransformer] },
217-
},
218-
);
274+
} = ts.transpileModule(code, {
275+
fileName,
276+
compilerOptions,
277+
reportDiagnostics: options.reportDiagnostics !== false,
278+
transformers: markup ? {} : { before: [importTransformer] },
279+
});
219280

220281
if (diagnostics.length > 0) {
221282
// could this be handled elsewhere?
@@ -232,7 +293,109 @@ const transformer: Transformer<Options.Typescript> = ({
232293
}
233294
}
234295

235-
const code = stripInjectedCode({ compiledCode, markup });
296+
if (sourceMapChain) {
297+
const fname = markup ? `${filename}.transpiled.js` : `${filename}.js`;
298+
299+
sourceMapChain.content[fname] = transpiledCode;
300+
sourceMapChain.sourcemaps[fname] = JSON.parse(sourceMapText);
301+
}
302+
303+
return { transpiledCode, diagnostics };
304+
}
305+
306+
export function loadTsconfig(
307+
compilerOptionsJSON: any,
308+
filename: string,
309+
tsOptions: Options.Typescript,
310+
) {
311+
if (typeof tsOptions.tsconfigFile === 'boolean') {
312+
return { errors: [], options: compilerOptionsJSON };
313+
}
314+
315+
let basePath = process.cwd();
316+
317+
const fileDirectory = (tsOptions.tsconfigDirectory ||
318+
dirname(filename)) as string;
319+
320+
let tsconfigFile =
321+
tsOptions.tsconfigFile ||
322+
ts.findConfigFile(fileDirectory, ts.sys.fileExists);
323+
324+
tsconfigFile = isAbsolute(tsconfigFile)
325+
? tsconfigFile
326+
: join(basePath, tsconfigFile);
327+
328+
basePath = dirname(tsconfigFile);
329+
330+
const { error, config } = ts.readConfigFile(tsconfigFile, ts.sys.readFile);
331+
332+
if (error) {
333+
throw new Error(formatDiagnostics(error, basePath));
334+
}
335+
336+
// Do this so TS will not search for initial files which might take a while
337+
config.include = [];
338+
339+
let { errors, options } = ts.parseJsonConfigFileContent(
340+
config,
341+
ts.sys,
342+
basePath,
343+
compilerOptionsJSON,
344+
tsconfigFile,
345+
);
346+
347+
// Filter out "no files found error"
348+
errors = errors.filter((d) => d.code !== 18003);
349+
350+
return { errors, options };
351+
}
352+
353+
const transformer: Transformer<Options.Typescript> = async ({
354+
content,
355+
filename = 'source.svelte',
356+
markup,
357+
options = {},
358+
attributes,
359+
}) => {
360+
const basePath = process.cwd();
361+
const compilerOptions = getCompilerOptions({ filename, options, basePath });
362+
363+
const sourceMapChain = createSourceMapChain({
364+
filename,
365+
content,
366+
compilerOptions,
367+
});
368+
369+
const injectedCode = injectVarsToCode({
370+
content,
371+
markup,
372+
filename,
373+
attributes,
374+
sourceMapChain,
375+
});
376+
377+
const { transpiledCode, diagnostics } = transpileTs({
378+
code: injectedCode,
379+
markup,
380+
filename,
381+
basePath,
382+
options,
383+
compilerOptions,
384+
sourceMapChain,
385+
});
386+
387+
const code = stripInjectedCode({
388+
transpiledCode,
389+
markup,
390+
filename,
391+
sourceMapChain,
392+
});
393+
394+
const map = await concatSourceMaps({
395+
filename,
396+
markup,
397+
sourceMapChain,
398+
});
236399

237400
return {
238401
code,

0 commit comments

Comments
 (0)