diff --git a/e2e/__tests__/calc/calc.component.ts b/e2e/__tests__/calc/calc.component.ts index 3ea636f839..c2370997e1 100644 --- a/e2e/__tests__/calc/calc.component.ts +++ b/e2e/__tests__/calc/calc.component.ts @@ -1,7 +1,5 @@ import { Component, OnInit, Input } from '@angular/core'; -import { Observable } from 'rxjs'; - -const image = require('e2e/test-app-v10/src/assets/its_something.png'); +import * as image from 'e2e/test-app-v10/src/assets/its_something.png'; @Component({ selector: 'app-calc', @@ -23,7 +21,6 @@ export class CalcComponent implements OnInit { @Input() hasAClass = false; prop1: number; image: string; - observable$: Observable; constructor() { this.init(); diff --git a/e2e/__tests__/forward-ref/forward-ref.spec.ts b/e2e/__tests__/forward-ref/forward-ref.spec.ts index 66615dd350..5a29ea7c04 100644 --- a/e2e/__tests__/forward-ref/forward-ref.spec.ts +++ b/e2e/__tests__/forward-ref/forward-ref.spec.ts @@ -1,6 +1,6 @@ import { forwardRef, Inject, Injector } from '@angular/core'; -const shouldSkipTest = process.env.NG_VERSION === 'v9' || process.env.SKIP_TEST === 'true'; +const shouldSkipTest = process.env.NG_VERSION === 'v9' || process.env.ISOLATED_MODULES === 'true'; const skipTest = shouldSkipTest ? test.skip : test if (shouldSkipTest) { diff --git a/e2e/__tests__/simple-with-styles/simple-with-styles.component.ts b/e2e/__tests__/simple-with-styles/simple-with-styles.component.ts index 32d4b6fc95..eb3c81c874 100644 --- a/e2e/__tests__/simple-with-styles/simple-with-styles.component.ts +++ b/e2e/__tests__/simple-with-styles/simple-with-styles.component.ts @@ -3,6 +3,7 @@ import { Component } from '@angular/core'; @Component({ selector: 'app-simple-with-styles', templateUrl: './simple-with-styles.component.html', + styleUrls: ['./simple-with-styles.scss'], // we have to setup styles this way, since simple styles/styleUrs properties will be removed (jest does not unit test styles) styles: [` .some-class { color: red } diff --git a/e2e/__tests__/simple-with-styles/simple-with-styles.scss b/e2e/__tests__/simple-with-styles/simple-with-styles.scss new file mode 100644 index 0000000000..2b0f777bd6 --- /dev/null +++ b/e2e/__tests__/simple-with-styles/simple-with-styles.scss @@ -0,0 +1,3 @@ +h1 { + font-size: 1.6rem; +} diff --git a/e2e/test-app-v10/src/typings.d.ts b/e2e/test-app-v10/src/typings.d.ts index ef5c7bd620..f8fa832c0f 100644 --- a/e2e/test-app-v10/src/typings.d.ts +++ b/e2e/test-app-v10/src/typings.d.ts @@ -3,3 +3,4 @@ declare var module: NodeModule; interface NodeModule { id: string; } +declare module '*.png'; diff --git a/e2e/test-app-v11/src/typings.d.ts b/e2e/test-app-v11/src/typings.d.ts index ef5c7bd620..f8fa832c0f 100644 --- a/e2e/test-app-v11/src/typings.d.ts +++ b/e2e/test-app-v11/src/typings.d.ts @@ -3,3 +3,4 @@ declare var module: NodeModule; interface NodeModule { id: string; } +declare module '*.png'; diff --git a/e2e/test-app-v9/src/typings.d.ts b/e2e/test-app-v9/src/typings.d.ts index ef5c7bd620..f8fa832c0f 100644 --- a/e2e/test-app-v9/src/typings.d.ts +++ b/e2e/test-app-v9/src/typings.d.ts @@ -3,3 +3,4 @@ declare var module: NodeModule; interface NodeModule { id: string; } +declare module '*.png'; diff --git a/scripts/e2e.js b/scripts/e2e.js index 1c63433758..579f5ca10a 100755 --- a/scripts/e2e.js +++ b/scripts/e2e.js @@ -25,15 +25,18 @@ const executeTest = (projectRealPath) => { logger.log(); logger.log('setting NG_VERSION environment variable'); + logger.log(); const projectName = projectRealPath.match(/([^\\/]*)\/*$/)[1]; process.env.NG_VERSION = projectName.substring(projectName.lastIndexOf('-') + 1); // then we install it in the repo logger.log('ensuring all dependencies of target project are installed'); + logger.log(); execa.sync('yarn', ['install'], { cwd: projectRealPath }); logger.log('cleaning old assets in target project'); + logger.log(); const testCasesDest = join(projectRealPath, 'src', '__tests__'); const presetDir = join(projectRealPath, 'node_modules', 'jest-preset-angular'); @@ -42,6 +45,7 @@ const executeTest = (projectRealPath) => { mkdirSync(presetDir); logger.log('copying distributed assets to target project'); + logger.log(); copySync(join(cwd, 'jest-preset.js'), `${presetDir}/jest-preset.js`); copySync(join(cwd, 'ngcc-jest-processor.js'), `${presetDir}/ngcc-jest-processor.js`); @@ -50,6 +54,7 @@ const executeTest = (projectRealPath) => { copySync(join(cwd, 'build'), `${presetDir}/build`); logger.log('copying test cases to target project'); + logger.log(); copySync(join(cwd, 'e2e', '__tests__'), testCasesDest); @@ -66,7 +71,7 @@ const executeTest = (projectRealPath) => { // cmdESMIso.push(...jestArgs); } - logger.log('starting non isolatedModules tests'); + logger.log('STARTING NONE ISOLATED MODULES TESTS'); logger.log(); logger.log('starting the CJS tests using:', ...cmdCjsUnIso); logger.log(); @@ -77,11 +82,13 @@ const executeTest = (projectRealPath) => { env: process.env, }); - logger.log('starting isolatedModules tests'); logger.log(); - logger.log('setting SKIP_TEST environment variable for isolatedModules true'); - process.env.SKIP_TEST = 'true'; + logger.log('STARTING ISOLATED MODULES TESTS'); + logger.log(); + logger.log('setting ISOLATED_MODULES environment variable for isolatedModules true'); + process.env.ISOLATED_MODULES = 'true'; + logger.log(); logger.log('starting the CommonJS tests using:', ...cmdCjsIso); logger.log(); @@ -104,7 +111,7 @@ const executeTest = (projectRealPath) => { execa.sync('rimraf', [testCasesDest]); delete process.env.NG_VERSION; - delete process.env.SKIP_TEST; + delete process.env.ISOLATED_MODULES; }; const cwd = process.cwd(); diff --git a/src/__tests__/__helpers__/test-helpers.ts b/src/__tests__/__helpers__/test-helpers.ts new file mode 100644 index 0000000000..c33ce7e7b2 --- /dev/null +++ b/src/__tests__/__helpers__/test-helpers.ts @@ -0,0 +1,12 @@ +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +export const jestCfgStub = { + testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'], + testRegex: ['(/__tests__/.*|(\\\\.|/)(test|spec))\\\\.[jt]sx?$'], + globals: { + 'ts-jest': { + diagnostics: { + pretty: false, + }, + }, + }, +} as any; // eslint-disable-line @typescript-eslint/no-explicit-any diff --git a/src/__tests__/__mocks__/app.component.html b/src/__tests__/__mocks__/app.component.html new file mode 100644 index 0000000000..be8061d4d5 --- /dev/null +++ b/src/__tests__/__mocks__/app.component.html @@ -0,0 +1 @@ +

App works

diff --git a/src/__tests__/__mocks__/app.component.scss b/src/__tests__/__mocks__/app.component.scss new file mode 100644 index 0000000000..2b0f777bd6 --- /dev/null +++ b/src/__tests__/__mocks__/app.component.scss @@ -0,0 +1,3 @@ +h1 { + font-size: 1.6rem; +} diff --git a/src/__tests__/__mocks__/app.component.ts b/src/__tests__/__mocks__/app.component.ts index db49a17c2a..c40c47db55 100644 --- a/src/__tests__/__mocks__/app.component.ts +++ b/src/__tests__/__mocks__/app.component.ts @@ -3,7 +3,14 @@ import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'], + styleUrls: ['./app.component.scss', './foo.component.css'], + styles: [ + ` + h1 { + font-size: 1.6rem; + } + `, + ], }) export class AppComponent { title = 'test-app-v10'; diff --git a/src/__tests__/__snapshots__/replace-resources.spec.ts.snap b/src/__tests__/__snapshots__/replace-resources.spec.ts.snap new file mode 100644 index 0000000000..29610bde70 --- /dev/null +++ b/src/__tests__/__snapshots__/replace-resources.spec.ts.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Replace resources transformer should use inline-files + strip-styles for isolatedModules true 1`] = ` +"\\"use strict\\"; +Object.defineProperty(exports, \\"__esModule\\", { value: true }); +exports.AppComponent = void 0; +const tslib_1 = require(\\"tslib\\"); +const core_1 = require(\\"@angular/core\\"); +let AppComponent = class AppComponent { + constructor() { + this.title = 'test-app-v10'; + } +}; +AppComponent = tslib_1.__decorate([ + core_1.Component({ + selector: 'app-root', + template: require('./app.component.html'), + styleUrls: [], + styles: [], + }) +], AppComponent); +exports.AppComponent = AppComponent; +//# " +`; + +exports[`Replace resources transformer should use replaceResources transformer from @angular/compiler-cli for isolatedModules false 1`] = ` +"\\"use strict\\"; +Object.defineProperty(exports, \\"__esModule\\", { value: true }); +exports.AppComponent = void 0; +const tslib_1 = require(\\"tslib\\"); +const core_1 = require(\\"@angular/core\\"); +let AppComponent = class AppComponent { + constructor() { + this.title = 'test-app-v10'; + } +}; +AppComponent = tslib_1.__decorate([ + core_1.Component({ + selector: 'app-root', + template: require(\\"./app.component.html\\"), + styles: [] + }) +], AppComponent); +exports.AppComponent = AppComponent; +//# " +`; diff --git a/src/__tests__/replace-resources.spec.ts b/src/__tests__/replace-resources.spec.ts new file mode 100644 index 0000000000..5fd79ba353 --- /dev/null +++ b/src/__tests__/replace-resources.spec.ts @@ -0,0 +1,52 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { SOURCE_MAPPING_PREFIX } from 'ts-jest/dist/compiler/compiler-utils'; + +import { NgJestConfig } from '../config/ng-jest-config'; +import { jestCfgStub } from './__helpers__/test-helpers'; +import { NgJestCompiler } from '../compiler/ng-jest-compiler'; + +describe('Replace resources transformer', () => { + const fileName = join(__dirname, '__mocks__', 'app.component.ts'); + const fileContent = readFileSync(fileName, 'utf-8'); + + test('should use replaceResources transformer from @angular/compiler-cli for isolatedModules false', () => { + const ngJestConfig = new NgJestConfig({ + ...jestCfgStub, + globals: { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + 'ts-jest': { + ...jestCfgStub.globals['ts-jest'], + isolatedModules: false, + }, + }, + }); + const compiler = new NgJestCompiler(ngJestConfig, new Map()); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const emittedResult = compiler.getCompiledOutput(fileName, fileContent, false)!; + + // Source map is different based on file location which can fail on CI, so we only compare snapshot for js + expect(emittedResult.substring(0, emittedResult.indexOf(SOURCE_MAPPING_PREFIX))).toMatchSnapshot(); + }); + + test('should use inline-files + strip-styles for isolatedModules true', () => { + const ngJestConfig = new NgJestConfig({ + ...jestCfgStub, + globals: { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + 'ts-jest': { + ...jestCfgStub.globals['ts-jest'], + isolatedModules: true, + }, + }, + }); + const compiler = new NgJestCompiler(ngJestConfig, new Map()); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const emittedResult = compiler.getCompiledOutput(fileName, fileContent, false)!; + + // Source map is different based on file location which can fail on CI, so we only compare snapshot for js + expect(emittedResult.substring(0, emittedResult.indexOf(SOURCE_MAPPING_PREFIX))).toMatchSnapshot(); + }); +}); diff --git a/src/compiler/ng-jest-compiler.ts b/src/compiler/ng-jest-compiler.ts index beb3e618f8..7ae3d304d8 100644 --- a/src/compiler/ng-jest-compiler.ts +++ b/src/compiler/ng-jest-compiler.ts @@ -9,6 +9,7 @@ import { factory as downlevelCtor } from '../transformers/downlevel-ctor'; import { factory as inlineFiles } from '../transformers/inline-files'; import { factory as stripStyles } from '../transformers/strip-styles'; import { NgJestCompilerHost } from './compiler-host'; +import { replaceResources } from '../transformers/replace-resources'; export class NgJestCompiler implements CompilerInstance { private _compilerOptions!: CompilerOptions; @@ -36,15 +37,9 @@ export class NgJestCompiler implements CompilerInstance { getCompiledOutput(fileName: string, fileContent: string, supportsStaticESM: boolean): string { const customTransformers = this.ngJestConfig.customTransformers; - const transformers = { - ...customTransformers, - before: [ - // hoisting from `ts-jest` or other before transformers - ...(customTransformers.before as ts.TransformerFactory[]), - inlineFiles(this.ngJestConfig), - stripStyles(this.ngJestConfig), - ], - }; + const isAppPath = (fileName: string) => !fileName.endsWith('.ngfactory.ts') && !fileName.endsWith('.ngstyle.ts'); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const getTypeChecker = () => this._program!.getTypeChecker(); if (this._program) { const allDiagnostics = []; if (!this._rootNames.includes(fileName)) { @@ -60,10 +55,9 @@ export class NgJestCompiler implements CompilerInstance { const sourceFile = this._program.getSourceFile(fileName); const emitResult = this._program.emit(sourceFile, undefined, undefined, undefined, { - ...transformers, + ...customTransformers, before: [ - // hoisting from `ts-jest` or other before transformers - ...transformers.before, + ...(customTransformers.before as ts.TransformerFactory[]), /** * Downlevel constructor parameters for DI support. This is required to support forwardRef in ES2015 due to * TDZ issues. This wrapper is needed here due to the program not being available until after @@ -71,6 +65,7 @@ export class NgJestCompiler implements CompilerInstance { * _createCompilerHost */ downlevelCtor(this._program), + replaceResources(isAppPath, getTypeChecker), ], }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -110,7 +105,15 @@ export class NgJestCompiler implements CompilerInstance { const result: ts.TranspileOutput = this._ts.transpileModule(fileContent, { fileName, - transformers, + transformers: { + ...customTransformers, + before: [ + // hoisting from `ts-jest` or other before transformers + ...(customTransformers.before as ts.TransformerFactory[]), + inlineFiles(this.ngJestConfig), + stripStyles(this.ngJestConfig), + ], + }, compilerOptions: { ...this._compilerOptions, module: moduleKind, @@ -127,7 +130,7 @@ export class NgJestCompiler implements CompilerInstance { } } - private _setupOptions({ parsedTsConfig }: NgJestConfig) { + private _setupOptions({ parsedTsConfig }: NgJestConfig): void { this._logger.debug({ parsedTsConfig }, '_setupOptions: initializing compiler config'); this._compilerOptions = { ...parsedTsConfig.options }; diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000000..bf4036663f --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,12 @@ +/** Angular component decorator templateUrl property name */ +export const TEMPLATE_URL = 'templateUrl'; +/** Angular component decorator styleUrls property name */ +export const STYLE_URLS = 'styleUrls'; +/** Angular component decorator styles property name */ +export const STYLES = 'styles'; +/** Angular component decorator template property name */ +export const TEMPLATE = 'template'; +/** Node require function name */ +export const REQUIRE = 'require'; +/** Angular component decorator name */ +export const COMPONENT = 'Component'; diff --git a/src/transformers/inline-files.ts b/src/transformers/inline-files.ts index c205b126e3..f50829e5fa 100644 --- a/src/transformers/inline-files.ts +++ b/src/transformers/inline-files.ts @@ -31,22 +31,16 @@ import type { Visitor, PropertyAssignment, LiteralLikeNode, + StringLiteral, } from 'typescript'; -import { getCreateStringLiteral, ConfigSet } from './transform-utils'; +import type { ConfigSet } from 'ts-jest/dist/config/config-set'; + +import { TEMPLATE_URL, STYLE_URLS, REQUIRE, TEMPLATE } from '../constants'; // replace original ts-jest ConfigSet with this simple interface, as it would require // jest-preset-angular to add several babel devDependencies to get the other types // inside the ConfigSet right -/** Angular component decorator TemplateUrl property name */ -const TEMPLATE_URL = 'templateUrl'; -/** Angular component decorator StyleUrls property name */ -const STYLE_URLS = 'styleUrls'; -/** Angular component decorator Template property name */ -const TEMPLATE = 'template'; -/** Node require function name */ -const REQUIRE = 'require'; - /** * Property names anywhere in an angular project to transform */ @@ -74,8 +68,20 @@ export function factory(cs: ConfigSet): (ctx: TransformationContext) => Transfor * Our compiler (typescript, or a module with typescript-like interface) */ const ts = cs.compilerModule; + function getCreateStringLiteral(): typeof ts.createStringLiteral { + if (ts.createStringLiteral && typeof ts.createStringLiteral === 'function') { + return ts.createStringLiteral; + } - const createStringLiteral = getCreateStringLiteral(ts); + return function createStringLiteral(text: string) { + const node = ts.createNode(ts.SyntaxKind.StringLiteral, -1, -1); + node.text = text; + node.flags |= ts.NodeFlags.Synthesized; + + return node; + }; + } + const createStringLiteral = getCreateStringLiteral(); /** * Traverses the AST down to the relevant assignments anywhere in the file diff --git a/src/transformers/replace-resources.ts b/src/transformers/replace-resources.ts new file mode 100644 index 0000000000..422005780d --- /dev/null +++ b/src/transformers/replace-resources.ts @@ -0,0 +1,261 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * 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 * as ts from 'typescript'; + +import { STYLES, STYLE_URLS, TEMPLATE_URL, TEMPLATE, REQUIRE, COMPONENT } from '../constants'; + +/** + * Source https://github.com/angular/angular-cli/blob/master/packages/ngtools/webpack/src/transformers/replace_resources.ts + * + * Check `@Component` to do following things: + * - Replace `templateUrl` path with `require` for `CommonJS` or a constant with `import` for `ESM` + * - Combine `styles` and `styleUrls` to become `styles` with empty array as value because we don't test css + * + * @example + * + * Given the input + * @Component({ + * selector: 'foo', + * templateUrl: './foo.component.html`, + * styleUrls: ['./foo.component.scss'], + * styles: [`h1 { font-size: 16px }`], + * }) + * + * Produced the output for `CommonJS` + * @Component({ + * selector: 'foo', + * templateUrl: require('./foo.component.html'), + * styles: [], + * }) + * + * or for `ESM` + * import __NG_CLI_RESOURCE__0 from './foo.component.html'; + * + * @Component({ + * selector: 'foo', + * templateUrl: __NG_CLI_RESOURCE__0, + * styles: [], + * }) + */ +export function replaceResources( + shouldTransform: (fileName: string) => boolean, + getTypeChecker: () => ts.TypeChecker, +): ts.TransformerFactory { + return (context: ts.TransformationContext) => { + const typeChecker = getTypeChecker(); + const resourceImportDeclarations: ts.ImportDeclaration[] = []; + const moduleKind = context.getCompilerOptions().module; + const visitNode: ts.Visitor = (node: ts.Node) => { + if (ts.isClassDeclaration(node)) { + const decorators = ts.visitNodes(node.decorators, (node) => + ts.isDecorator(node) ? visitDecorator(node, typeChecker, resourceImportDeclarations, moduleKind) : node, + ); + + return ts.updateClassDeclaration( + node, + decorators, + node.modifiers, + node.name, + node.typeParameters, + node.heritageClauses, + node.members, + ); + } + + return ts.visitEachChild(node, visitNode, context); + }; + + return (sourceFile: ts.SourceFile) => { + if (!shouldTransform(sourceFile.fileName)) { + return sourceFile; + } + + const updatedSourceFile = ts.visitNode(sourceFile, visitNode); + if (resourceImportDeclarations.length) { + // Add resource imports + return ts.updateSourceFileNode( + updatedSourceFile, + ts.setTextRange( + ts.createNodeArray([...resourceImportDeclarations, ...updatedSourceFile.statements]), + updatedSourceFile.statements, + ), + ); + } + + return updatedSourceFile; + }; + }; +} + +function visitDecorator( + node: ts.Decorator, + typeChecker: ts.TypeChecker, + resourceImportDeclarations: ts.ImportDeclaration[], + moduleKind?: ts.ModuleKind, +): ts.Decorator { + if (!isComponentDecorator(node, typeChecker)) { + return node; + } + + if (!ts.isCallExpression(node.expression)) { + return node; + } + + const decoratorFactory = node.expression; + const args = decoratorFactory.arguments; + if (args.length !== 1 || !ts.isObjectLiteralExpression(args[0])) { + // Unsupported component metadata + return node; + } + + const objectExpression = args[0] as ts.ObjectLiteralExpression; + + // visit all properties + let properties = ts.visitNodes(objectExpression.properties, (node) => + ts.isObjectLiteralElementLike(node) ? visitComponentMetadata(node, resourceImportDeclarations, moduleKind) : node, + ); + + // replace properties with updated properties + const styleProperty = ts.createPropertyAssignment(ts.createIdentifier(STYLES), ts.createArrayLiteral([])); + + properties = ts.createNodeArray([...properties, styleProperty]); + + return ts.updateDecorator( + node, + ts.updateCall(decoratorFactory, decoratorFactory.expression, decoratorFactory.typeArguments, [ + ts.updateObjectLiteral(objectExpression, properties), + ]), + ); +} + +function visitComponentMetadata( + node: ts.ObjectLiteralElementLike, + resourceImportDeclarations: ts.ImportDeclaration[], + moduleKind?: ts.ModuleKind, +): ts.ObjectLiteralElementLike | undefined { + if (!ts.isPropertyAssignment(node) || ts.isComputedPropertyName(node.name)) { + return node; + } + + const name = node.name.text; + switch (name) { + case 'moduleId': + return undefined; + + case TEMPLATE_URL: + // eslint-disable-next-line no-case-declarations + const importName = createResourceImport(node.initializer, resourceImportDeclarations, moduleKind); + if (!importName) { + return node; + } + + return ts.updatePropertyAssignment(node, ts.createIdentifier(TEMPLATE), importName); + case STYLES: + case STYLE_URLS: + if (!ts.isArrayLiteralExpression(node.initializer)) { + return node; + } + + return undefined; + default: + return node; + } +} + +export function getResourceUrl(node: ts.Node): string | null { + // only analyze strings + if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) { + return null; + } + + return `${/^\.?\.\//.test(node.text) ? '' : './'}${node.text}`; +} + +function isComponentDecorator(node: ts.Node, typeChecker: ts.TypeChecker): node is ts.Decorator { + if (!ts.isDecorator(node)) { + return false; + } + + const origin = getDecoratorOrigin(node, typeChecker); + + return !!(origin && origin.module === '@angular/core' && origin.name === COMPONENT); +} + +function createResourceImport( + node: ts.Node, + resourceImportDeclarations: ts.ImportDeclaration[], + moduleKind = ts.ModuleKind.ES2015, +): ts.Identifier | ts.Expression | null { + const url = getResourceUrl(node); + if (!url) { + return null; + } + + const urlLiteral = ts.createLiteral(url); + if (moduleKind < ts.ModuleKind.ES2015) { + return ts.createCall( + /* expression */ ts.createIdentifier(REQUIRE), + /* type arguments */ undefined, + /* arguments array */ [urlLiteral], + ); + } else { + const importName = ts.createIdentifier(`__NG_CLI_RESOURCE__${resourceImportDeclarations.length}`); + resourceImportDeclarations.push( + ts.createImportDeclaration(undefined, undefined, ts.createImportClause(importName, undefined), urlLiteral), + ); + + return importName; + } +} + +interface DecoratorOrigin { + name: string; + module: string; +} + +function getDecoratorOrigin(decorator: ts.Decorator, typeChecker: ts.TypeChecker): DecoratorOrigin | null { + if (!ts.isCallExpression(decorator.expression)) { + return null; + } + + let identifier: ts.Node; + let name = ''; + + if (ts.isPropertyAccessExpression(decorator.expression.expression)) { + identifier = decorator.expression.expression.expression; + name = decorator.expression.expression.name.text; + } else if (ts.isIdentifier(decorator.expression.expression)) { + identifier = decorator.expression.expression; + } else { + return null; + } + + // NOTE: resolver.getReferencedImportDeclaration would work as well but is internal + const symbol = typeChecker.getSymbolAtLocation(identifier); + if (symbol && symbol.declarations && symbol.declarations.length > 0) { + const declaration = symbol.declarations[0]; + let module: string; + + if (ts.isImportSpecifier(declaration)) { + name = (declaration.propertyName || declaration.name).text; + module = (declaration.parent.parent.parent.moduleSpecifier as ts.Identifier).text; + } else if (ts.isNamespaceImport(declaration)) { + // Use the name from the decorator namespace property access + module = (declaration.parent.parent.moduleSpecifier as ts.Identifier).text; + } else if (ts.isImportClause(declaration)) { + name = (declaration.name as ts.Identifier).text; + module = (declaration.parent.moduleSpecifier as ts.Identifier).text; + } else { + return null; + } + + return { name, module }; + } + + return null; +} diff --git a/src/transformers/strip-styles.ts b/src/transformers/strip-styles.ts index 638aaa7b71..63b7ac9c10 100644 --- a/src/transformers/strip-styles.ts +++ b/src/transformers/strip-styles.ts @@ -34,12 +34,10 @@ import type { CallExpression, ObjectLiteralExpression, } from 'typescript'; -import type { ConfigSet } from './transform-utils'; +import type { ConfigSet } from 'ts-jest/dist/config/config-set'; + +import { STYLES, COMPONENT } from '../constants'; -/** Angular component decorator Styles property name */ -const STYLES = 'styles'; -/** Angular component decorator name */ -const COMPONENT = 'Component'; /** All props to be transformed inside a decorator */ const TRANSFORM_IN_DECORATOR_PROPS = [STYLES]; diff --git a/src/transformers/transform-utils.ts b/src/transformers/transform-utils.ts deleted file mode 100644 index c98162437b..0000000000 --- a/src/transformers/transform-utils.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type * as TS from 'typescript'; - -// replace original ts-jest ConfigSet with this simple interface, as it would require -// jest-preset-angular to add several babel devDependencies to get the other types -// inside the ConfigSet right -export interface ConfigSet { - compilerModule: typeof TS; -} - -/** - * returns the compiler function to create a string literal. If an old version - * of the TypeScript module is used, it will create a function that replaces the - * behavior of the `createStringLiteral` function. - * @param ts TypeScript compiler module - */ -export function getCreateStringLiteral(ts: typeof TS): typeof TS.createStringLiteral { - if (ts.createStringLiteral && typeof ts.createStringLiteral === 'function') { - return ts.createStringLiteral; - } - - return function createStringLiteral(text: string) { - const node = ts.createNode(ts.SyntaxKind.StringLiteral, -1, -1); - node.text = text; - node.flags |= ts.NodeFlags.Synthesized; - - return node; - }; -}