Skip to content

Commit

Permalink
fix: handle sourcemaps in typescript transformer
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
SomaticIT committed Apr 15, 2021
1 parent e5a73db commit 435a6d5
Show file tree
Hide file tree
Showing 4 changed files with 288 additions and 70 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@
"@types/pug": "^2.0.4",
"@types/sass": "^1.16.0",
"detect-indent": "^6.0.0",
"magic-string": "^0.25.7",
"sorcery": "^0.10.0",
"strip-indent": "^3.0.0"
},
"peerDependencies": {
Expand Down
294 changes: 225 additions & 69 deletions src/transformers/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@ import { dirname, isAbsolute, join } from 'path';

import ts from 'typescript';
import { compile } from 'svelte/compiler';
import MagicString from 'magic-string';
import sorcery from 'sorcery';

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

type CompilerOptions = Options.Typescript['compilerOptions'];
type CompilerOptions = ts.CompilerOptions;

type SourceMapChain = {
content: Record<string, string>;
sourcemaps: Record<string, object>;
};

function createFormatDiagnosticsHost(cwd: string): ts.FormatDiagnosticsHost {
return {
getCanonicalFileName: (fileName: string) => fileName,
getCanonicalFileName: (fileName: string) =>
fileName.replace('.injected.ts', ''),
getCurrentDirectory: () => cwd,
getNewLine: () => ts.sys.newLine,
};
Expand Down Expand Up @@ -70,16 +78,37 @@ function getComponentScriptContent(markup: string): string {
return '';
}

function createSourceMapChain({
filename,
content,
compilerOptions,
}: {
filename: string;
content: string;
compilerOptions: CompilerOptions;
}): SourceMapChain | undefined {
if (compilerOptions.sourceMap) {
return {
content: {
[filename]: content,
},
sourcemaps: {},
};
}
}

function injectVarsToCode({
content,
markup,
filename,
attributes,
sourceMapChain,
}: {
content: string;
markup?: string;
filename?: string;
filename: string;
attributes?: Record<string, any>;
sourceMapChain?: SourceMapChain;
}): string {
if (!markup) return content;

Expand All @@ -93,90 +122,97 @@ function injectVarsToCode({
const sep = '\nconst $$$$$$$$ = null;\n';
const varsValues = vars.map((v) => v.name).join(',');
const injectedVars = `const $$vars$$ = [${varsValues}];`;
const injectedCode =
attributes?.context === 'module'
? `${sep}${getComponentScriptContent(markup)}\n${injectedVars}`
: `${sep}${injectedVars}`;

if (attributes?.context === 'module') {
const componentScript = getComponentScriptContent(markup);
if (sourceMapChain) {
const s = new MagicString(content);

return `${content}${sep}${componentScript}\n${injectedVars}`;
s.append(injectedCode);

const fname = `${filename}.injected.ts`;
const code = s.toString();
const map = s.generateMap({
source: filename,
file: fname,
});

sourceMapChain.content[fname] = code;
sourceMapChain.sourcemaps[fname] = map;

return code;
}

return `${content}${sep}${injectedVars}`;
return `${content}${injectedCode}`;
}

function stripInjectedCode({
compiledCode,
transpiledCode,
markup,
filename,
sourceMapChain,
}: {
compiledCode: string;
transpiledCode: string;
markup?: string;
filename: string;
sourceMapChain?: SourceMapChain;
}): string {
return markup
? compiledCode.slice(0, compiledCode.indexOf('const $$$$$$$$ = null;'))
: compiledCode;
}

export function loadTsconfig(
compilerOptionsJSON: any,
filename: string,
tsOptions: Options.Typescript,
) {
if (typeof tsOptions.tsconfigFile === 'boolean') {
return { errors: [], options: compilerOptionsJSON };
}

let basePath = process.cwd();

const fileDirectory = (tsOptions.tsconfigDirectory ||
dirname(filename)) as string;
if (!markup) return transpiledCode;

let tsconfigFile =
tsOptions.tsconfigFile ||
ts.findConfigFile(fileDirectory, ts.sys.fileExists);
const injectedCodeStart = transpiledCode.indexOf('const $$$$$$$$ = null;');

tsconfigFile = isAbsolute(tsconfigFile)
? tsconfigFile
: join(basePath, tsconfigFile);
if (sourceMapChain) {
const s = new MagicString(transpiledCode);
const st = s.snip(0, injectedCodeStart);

basePath = dirname(tsconfigFile);
const source = `${filename}.transpiled.js`;
const file = `${filename}.js`;
const code = st.toString();
const map = st.generateMap({
source,
file,
});

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

if (error) {
throw new Error(formatDiagnostics(error, basePath));
return code;
}

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

let { errors, options } = ts.parseJsonConfigFileContent(
config,
ts.sys,
basePath,
compilerOptionsJSON,
tsconfigFile,
);
async function concatSourceMaps({
filename,
sourceMapChain,
}: {
filename: string;
sourceMapChain?: SourceMapChain;
}): Promise<string | object | undefined> {
if (!sourceMapChain) return;

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

return { errors, options };
return chain.apply();
}

const transformer: Transformer<Options.Typescript> = ({
content,
function getCompilerOptions({
filename,
markup,
options = {},
attributes,
}) => {
options,
basePath,
}: {
filename: string;
options: Options.Typescript;
basePath: string;
}): CompilerOptions {
// default options
const compilerOptionsJSON = {
moduleResolution: 'node',
target: 'es6',
};

const basePath = process.cwd();

Object.assign(compilerOptionsJSON, options.compilerOptions);

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

return compilerOptions;
}

function transpileTs({
code,
markup,
filename,
basePath,
options,
compilerOptions,
sourceMapChain,
}: {
code: string;
markup: string;
filename: string;
basePath: string;
options: Options.Typescript;
compilerOptions: CompilerOptions;
sourceMapChain: SourceMapChain;
}): { transpiledCode: string; diagnostics: ts.Diagnostic[] } {
const fileName = markup ? `${filename}.injected.ts` : filename;

const {
outputText: compiledCode,
sourceMapText: map,
outputText: transpiledCode,
sourceMapText,
diagnostics,
} = ts.transpileModule(
injectVarsToCode({ content, markup, filename, attributes }),
{
fileName: filename,
compilerOptions,
reportDiagnostics: options.reportDiagnostics !== false,
transformers: markup ? {} : { before: [importTransformer] },
},
);
} = ts.transpileModule(code, {
fileName,
compilerOptions,
reportDiagnostics: options.reportDiagnostics !== false,
transformers: markup ? {} : { before: [importTransformer] },
});

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

const code = stripInjectedCode({ compiledCode, markup });
if (sourceMapChain) {
const fname = markup ? `${filename}.transpiled.js` : `${filename}.js`;

sourceMapChain.content[fname] = transpiledCode;
sourceMapChain.sourcemaps[fname] = JSON.parse(sourceMapText);
}

return { transpiledCode, diagnostics };
}

export function loadTsconfig(
compilerOptionsJSON: any,
filename: string,
tsOptions: Options.Typescript,
) {
if (typeof tsOptions.tsconfigFile === 'boolean') {
return { errors: [], options: compilerOptionsJSON };
}

let basePath = process.cwd();

const fileDirectory = (tsOptions.tsconfigDirectory ||
dirname(filename)) as string;

let tsconfigFile =
tsOptions.tsconfigFile ||
ts.findConfigFile(fileDirectory, ts.sys.fileExists);

tsconfigFile = isAbsolute(tsconfigFile)
? tsconfigFile
: join(basePath, tsconfigFile);

basePath = dirname(tsconfigFile);

const { error, config } = ts.readConfigFile(tsconfigFile, ts.sys.readFile);

if (error) {
throw new Error(formatDiagnostics(error, basePath));
}

// Do this so TS will not search for initial files which might take a while
config.include = [];

let { errors, options } = ts.parseJsonConfigFileContent(
config,
ts.sys,
basePath,
compilerOptionsJSON,
tsconfigFile,
);

// Filter out "no files found error"
errors = errors.filter((d) => d.code !== 18003);

return { errors, options };
}

const transformer: Transformer<Options.Typescript> = async ({
content,
filename = 'source.svelte',
markup,
options = {},
attributes,
}) => {
const basePath = process.cwd();
const compilerOptions = getCompilerOptions({ filename, options, basePath });

const sourceMapChain = createSourceMapChain({
filename,
content,
compilerOptions,
});

const injectedCode = injectVarsToCode({
content,
markup,
filename,
attributes,
sourceMapChain,
});

const { transpiledCode, diagnostics } = transpileTs({
code: injectedCode,
markup,
filename,
basePath,
options,
compilerOptions,
sourceMapChain,
});

const code = stripInjectedCode({
transpiledCode,
markup,
filename,
sourceMapChain,
});

const map = await concatSourceMaps({
filename,
sourceMapChain,
});

return {
code,
Expand Down
Loading

0 comments on commit 435a6d5

Please sign in to comment.