Skip to content

Commit 435a6d5

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 435a6d5

File tree

4 files changed

+288
-70
lines changed

4 files changed

+288
-70
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: 225 additions & 69 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,97 @@ 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}`;
96129

97-
if (attributes?.context === 'module') {
98-
const componentScript = getComponentScriptContent(markup);
130+
if (sourceMapChain) {
131+
const s = new MagicString(content);
99132

100-
return `${content}${sep}${componentScript}\n${injectedVars}`;
133+
s.append(injectedCode);
134+
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-
}
117-
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-
}
126-
127-
let basePath = process.cwd();
128-
129-
const fileDirectory = (tsOptions.tsconfigDirectory ||
130-
dirname(filename)) as string;
162+
if (!markup) return transpiledCode;
131163

132-
let tsconfigFile =
133-
tsOptions.tsconfigFile ||
134-
ts.findConfigFile(fileDirectory, ts.sys.fileExists);
164+
const injectedCodeStart = transpiledCode.indexOf('const $$$$$$$$ = null;');
135165

136-
tsconfigFile = isAbsolute(tsconfigFile)
137-
? tsconfigFile
138-
: join(basePath, tsconfigFile);
166+
if (sourceMapChain) {
167+
const s = new MagicString(transpiledCode);
168+
const st = s.snip(0, injectedCodeStart);
139169

140-
basePath = dirname(tsconfigFile);
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+
});
141177

142-
const { error, config } = ts.readConfigFile(tsconfigFile, ts.sys.readFile);
178+
sourceMapChain.content[file] = code;
179+
sourceMapChain.sourcemaps[file] = map;
143180

144-
if (error) {
145-
throw new Error(formatDiagnostics(error, basePath));
181+
return code;
146182
}
147183

148-
// Do this so TS will not search for initial files which might take a while
149-
config.include = [];
184+
return transpiledCode.slice(0, injectedCodeStart);
185+
}
150186

151-
let { errors, options } = ts.parseJsonConfigFileContent(
152-
config,
153-
ts.sys,
154-
basePath,
155-
compilerOptionsJSON,
156-
tsconfigFile,
157-
);
187+
async function concatSourceMaps({
188+
filename,
189+
sourceMapChain,
190+
}: {
191+
filename: string;
192+
sourceMapChain?: SourceMapChain;
193+
}): Promise<string | object | undefined> {
194+
if (!sourceMapChain) return;
158195

159-
// Filter out "no files found error"
160-
errors = errors.filter((d) => d.code !== 18003);
196+
const chain = await sorcery.load(`${filename}.js`, sourceMapChain);
161197

162-
return { errors, options };
198+
return chain.apply();
163199
}
164200

165-
const transformer: Transformer<Options.Typescript> = ({
166-
content,
201+
function getCompilerOptions({
167202
filename,
168-
markup,
169-
options = {},
170-
attributes,
171-
}) => {
203+
options,
204+
basePath,
205+
}: {
206+
filename: string;
207+
options: Options.Typescript;
208+
basePath: string;
209+
}): CompilerOptions {
172210
// default options
173211
const compilerOptionsJSON = {
174212
moduleResolution: 'node',
175213
target: 'es6',
176214
};
177215

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

182218
const { errors, options: convertedCompilerOptions } =
@@ -203,19 +239,38 @@ const transformer: Transformer<Options.Typescript> = ({
203239
);
204240
}
205241

242+
return compilerOptions;
243+
}
244+
245+
function transpileTs({
246+
code,
247+
markup,
248+
filename,
249+
basePath,
250+
options,
251+
compilerOptions,
252+
sourceMapChain,
253+
}: {
254+
code: string;
255+
markup: string;
256+
filename: string;
257+
basePath: string;
258+
options: Options.Typescript;
259+
compilerOptions: CompilerOptions;
260+
sourceMapChain: SourceMapChain;
261+
}): { transpiledCode: string; diagnostics: ts.Diagnostic[] } {
262+
const fileName = markup ? `${filename}.injected.ts` : filename;
263+
206264
const {
207-
outputText: compiledCode,
208-
sourceMapText: map,
265+
outputText: transpiledCode,
266+
sourceMapText,
209267
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-
);
268+
} = ts.transpileModule(code, {
269+
fileName,
270+
compilerOptions,
271+
reportDiagnostics: options.reportDiagnostics !== false,
272+
transformers: markup ? {} : { before: [importTransformer] },
273+
});
219274

220275
if (diagnostics.length > 0) {
221276
// could this be handled elsewhere?
@@ -232,7 +287,108 @@ const transformer: Transformer<Options.Typescript> = ({
232287
}
233288
}
234289

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

237393
return {
238394
code,

0 commit comments

Comments
 (0)