Skip to content

Commit

Permalink
refactor(compiler): replace copied codes from ts source with public API
Browse files Browse the repository at this point in the history
We use directly API `transpileModule` together with additional AST transformers from Angular.

This can be achieved by creating a minimal `Program` (actually copied from ts source) and fetch this `Program` to AST transformers
  • Loading branch information
ahnpnl committed Jun 30, 2024
1 parent 15d4c8c commit ad7a297
Show file tree
Hide file tree
Showing 2 changed files with 28 additions and 100 deletions.
122 changes: 26 additions & 96 deletions src/compiler/ng-jest-compiler.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import os from 'os';
import path from 'path';

import { type TsJestAstTransformer, TsCompiler, type ConfigSet } from 'ts-jest';
import type * as ts from 'typescript';

Expand All @@ -20,118 +23,45 @@ export class NgJestCompiler extends TsCompiler {
* and we need `Program` to be able to use Angular `replace-resources` transformer.
*/
protected _transpileOutput(fileContent: string, filePath: string): ts.TranspileOutput {
const diagnostics: ts.Diagnostic[] = [];
const compilerOptions = { ...this._compilerOptions };
const options: ts.CompilerOptions = compilerOptions
? // @ts-expect-error internal TypeScript API
this._ts.fixupCompilerOptions(compilerOptions, diagnostics)
: {};

// mix in default options
const defaultOptions = this._ts.getDefaultCompilerOptions();
for (const key in defaultOptions) {
// @ts-expect-error internal TypeScript API
if (this._ts.hasProperty(defaultOptions, key) && options[key] === undefined) {
options[key] = defaultOptions[key];
}
}

// @ts-expect-error internal TypeScript API
for (const option of this._ts.transpileOptionValueCompilerOptions) {
options[option.name] = option.transpileOptionValue;
}

/**
* transpileModule does not write anything to disk so there is no need to verify that there are no conflicts between
* input and output paths.
*/
options.suppressOutputPathCheck = true;

// Filename can be non-ts file.
options.allowNonTsExtensions = true;

// if jsx is specified then treat file as .tsx
const inputFileName = filePath || (compilerOptions && compilerOptions.jsx ? 'module.tsx' : 'module.ts');
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const sourceFile = this._ts.createSourceFile(inputFileName, fileContent, options.target!); // TODO: GH#18217

// @ts-expect-error internal TypeScript API
const newLine = this._ts.getNewLineCharacter(options);

// Output
let outputText: string | undefined;
let sourceMapText: string | undefined;

// Create a compilerHost object to allow the compiler to read and write files
const sourceFile = this._ts.createSourceFile(filePath, fileContent, this._compilerOptions.target!);
const compilerHost: ts.CompilerHost = {
getSourceFile: (fileName) =>
// @ts-expect-error internal TypeScript API
fileName === this._ts.normalizePath(inputFileName) ? sourceFile : undefined,
writeFile: (name, text) => {
// @ts-expect-error internal TypeScript API
if (this._ts.fileExtensionIs(name, '.map')) {
// @ts-expect-error internal TypeScript API
this._ts.Debug.assertEqual(
sourceMapText,
undefined,
'Unexpected multiple source map outputs, file:',
name,
);
sourceMapText = text;
} else {
// @ts-expect-error internal TypeScript API
this._ts.Debug.assertEqual(outputText, undefined, 'Unexpected multiple outputs, file:', name);
outputText = text;
}
},
getSourceFile: (fileName) => (fileName === path.normalize(filePath) ? sourceFile : undefined),
// eslint-disable-next-line @typescript-eslint/no-empty-function
writeFile: () => {},
getDefaultLibFileName: () => 'lib.d.ts',
useCaseSensitiveFileNames: () => false,
getCanonicalFileName: (fileName) => fileName,
getCurrentDirectory: () => '',
getNewLine: () => newLine,
fileExists: (fileName): boolean => fileName === inputFileName,
getNewLine: () => os.EOL,
fileExists: (fileName): boolean => fileName === filePath,
readFile: () => '',
directoryExists: () => true,
getDirectories: () => [],
};

this.program = this._ts.createProgram([inputFileName], options, compilerHost);
if (this.configSet.shouldReportDiagnostics(inputFileName)) {
// @ts-expect-error internal TypeScript API
this._ts.addRange(/*to*/ diagnostics, /*from*/ this.program.getSyntacticDiagnostics(sourceFile));
// @ts-expect-error internal TypeScript API
this._ts.addRange(/*to*/ diagnostics, /*from*/ this.program.getOptionsDiagnostics());
}
// Emit
this.program.emit(
/*targetSourceFile*/ undefined,
/*writeFile*/ undefined,
/*cancellationToken*/ undefined,
/*emitOnlyDtsFiles*/ undefined,
this._makeTransformers(this.configSet.resolvedTransformers),
);
if (outputText === undefined) {
// @ts-expect-error internal TypeScript API
return this._ts.Debug.fail('Output generation failed');
}

return { outputText, diagnostics, sourceMapText };
this.program = this._ts.createProgram([filePath], this._compilerOptions, compilerHost);

return this._ts.transpileModule(fileContent, {
fileName: filePath,
transformers: this._makeTransformers(this.configSet.resolvedTransformers),
compilerOptions: this._compilerOptions,
reportDiagnostics: this.configSet.shouldReportDiagnostics(filePath),
});
}

protected _makeTransformers(customTransformers: TsJestAstTransformer): ts.CustomTransformers {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const program = this.program!;
const allTransformers = super._makeTransformers(customTransformers);

return {
...super._makeTransformers(customTransformers).after,
...super._makeTransformers(customTransformers).afterDeclarations,
...allTransformers.after,
...allTransformers.afterDeclarations,
before: [
...customTransformers.before.map((beforeTransformer) =>
beforeTransformer.factory(this, beforeTransformer.options),
),
replaceResources(this),
angularJitApplicationTransform(program),
] as Array<ts.TransformerFactory<ts.SourceFile> | ts.CustomTransformerFactory>,
...(allTransformers.before ?? []),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
replaceResources(this.program!),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
angularJitApplicationTransform(this.program!),
],
};
}
}
6 changes: 2 additions & 4 deletions src/transformers/replace-resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import type { TsCompilerInstance } from 'ts-jest';
import ts from 'typescript';

import { STYLES, STYLE_URLS, TEMPLATE_URL, TEMPLATE, REQUIRE, COMPONENT, STYLE_URL } from '../constants';
Expand Down Expand Up @@ -59,10 +58,9 @@ const shouldTransform = (fileName: string) => !fileName.endsWith('.ngfactory.ts'
* templateUrl: __NG_CLI_RESOURCE__0,
* })
*/
export function replaceResources({ program }: TsCompilerInstance): ts.TransformerFactory<ts.SourceFile> {
export function replaceResources(program: ts.Program): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const typeChecker = program!.getTypeChecker();
const typeChecker = program.getTypeChecker();
const resourceImportDeclarations: ts.ImportDeclaration[] = [];
const moduleKind = context.getCompilerOptions().module;
const nodeFactory = context.factory;
Expand Down

0 comments on commit ad7a297

Please sign in to comment.