From ad7a297e4ddc3ab53ed2ed55a865cfb10f42dfda Mon Sep 17 00:00:00 2001 From: ahnpnl Date: Mon, 1 Jul 2024 00:42:49 +0200 Subject: [PATCH] refactor(compiler): replace copied codes from ts source with public API 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 --- src/compiler/ng-jest-compiler.ts | 122 ++++++-------------------- src/transformers/replace-resources.ts | 6 +- 2 files changed, 28 insertions(+), 100 deletions(-) diff --git a/src/compiler/ng-jest-compiler.ts b/src/compiler/ng-jest-compiler.ts index 901bb5d9d6..5952234653 100644 --- a/src/compiler/ng-jest-compiler.ts +++ b/src/compiler/ng-jest-compiler.ts @@ -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'; @@ -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.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!), + ], }; } } diff --git a/src/transformers/replace-resources.ts b/src/transformers/replace-resources.ts index 34b4ea9625..fda9a6fd3c 100644 --- a/src/transformers/replace-resources.ts +++ b/src/transformers/replace-resources.ts @@ -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'; @@ -59,10 +58,9 @@ const shouldTransform = (fileName: string) => !fileName.endsWith('.ngfactory.ts' * templateUrl: __NG_CLI_RESOURCE__0, * }) */ -export function replaceResources({ program }: TsCompilerInstance): ts.TransformerFactory { +export function replaceResources(program: ts.Program): ts.TransformerFactory { 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;