From a2166f859b1c89340ee889520595d05fa3cf65dc Mon Sep 17 00:00:00 2001 From: Ahn Date: Fri, 8 Jan 2021 16:56:20 +0100 Subject: [PATCH] feat(compiler): support ESM for `isolatedModules: false` (#721) - Make sure `esModuleInterop` and `allowSyntheticDefaultImports` true to support import CJS into ESM - Adjust `module` value correctly according to Jest transform option `supportsStaticESM` and `ts-jest` config `useESM` --- e2e/test-app-v10/jest-esm-uniso.config.js | 20 +++++++++ e2e/test-app-v10/package.json | 1 + e2e/test-app-v11/jest-esm-uniso.config.js | 20 +++++++++ e2e/test-app-v11/package.json | 1 + e2e/test-app-v9/jest-esm-uniso.config.js | 20 +++++++++ e2e/test-app-v9/package.json | 1 + scripts/e2e.js | 13 ++++++ .../ng-jest-compiler.spec.ts.snap | 20 +++++++++ .../replace-resources.spec.ts.snap | 16 ++++---- src/__tests__/ng-jest-compiler.spec.ts | 38 ++++++++++++++--- src/compiler/ng-jest-compiler.ts | 41 +++++++++++-------- 11 files changed, 160 insertions(+), 31 deletions(-) create mode 100644 e2e/test-app-v10/jest-esm-uniso.config.js create mode 100644 e2e/test-app-v11/jest-esm-uniso.config.js create mode 100644 e2e/test-app-v9/jest-esm-uniso.config.js diff --git a/e2e/test-app-v10/jest-esm-uniso.config.js b/e2e/test-app-v10/jest-esm-uniso.config.js new file mode 100644 index 0000000000..9997a613e9 --- /dev/null +++ b/e2e/test-app-v10/jest-esm-uniso.config.js @@ -0,0 +1,20 @@ +require('jest-preset-angular/ngcc-jest-processor'); +const baseConfig = require('./jest.config'); + +/** @type {import('ts-jest/dist/types').ProjectConfigTsJest} */ +module.exports = { + ...baseConfig, + extensionsToTreatAsEsm: ['.ts'], + globals: { + 'ts-jest': { + tsconfig: '/tsconfig-esm.spec.json', + stringifyContentPathRegex: '\\.html$', + useESM: true, + } + }, + moduleNameMapper: { + ...baseConfig.moduleNameMapper, + 'tslib': '/node_modules/tslib/tslib.es6.js', + }, + transformIgnorePatterns: ['node_modules/(?!tslib)'], +}; diff --git a/e2e/test-app-v10/package.json b/e2e/test-app-v10/package.json index a4f19d5ab6..a44725e8d1 100644 --- a/e2e/test-app-v10/package.json +++ b/e2e/test-app-v10/package.json @@ -8,6 +8,7 @@ "test-cjs-uniso": "jest --clearCache && jest -c=jest-cjs-uniso.config.js", "test-cjs-iso": "jest --clearCache && jest -c=jest-cjs-iso.config.js", "test-esm-iso": "jest --clearCache && node --experimental-vm-modules ./node_modules/jest/bin/jest.js -c=jest-esm-iso.config.js", + "test-esm-uniso": "jest --clearCache && node --experimental-vm-modules ./node_modules/jest/bin/jest.js -c=jest-esm-uniso.config.js", "lint": "ng lint" }, "private": true, diff --git a/e2e/test-app-v11/jest-esm-uniso.config.js b/e2e/test-app-v11/jest-esm-uniso.config.js new file mode 100644 index 0000000000..9997a613e9 --- /dev/null +++ b/e2e/test-app-v11/jest-esm-uniso.config.js @@ -0,0 +1,20 @@ +require('jest-preset-angular/ngcc-jest-processor'); +const baseConfig = require('./jest.config'); + +/** @type {import('ts-jest/dist/types').ProjectConfigTsJest} */ +module.exports = { + ...baseConfig, + extensionsToTreatAsEsm: ['.ts'], + globals: { + 'ts-jest': { + tsconfig: '/tsconfig-esm.spec.json', + stringifyContentPathRegex: '\\.html$', + useESM: true, + } + }, + moduleNameMapper: { + ...baseConfig.moduleNameMapper, + 'tslib': '/node_modules/tslib/tslib.es6.js', + }, + transformIgnorePatterns: ['node_modules/(?!tslib)'], +}; diff --git a/e2e/test-app-v11/package.json b/e2e/test-app-v11/package.json index d198c88577..fe7031d2a6 100644 --- a/e2e/test-app-v11/package.json +++ b/e2e/test-app-v11/package.json @@ -8,6 +8,7 @@ "test-cjs-uniso": "jest --clearCache && jest -c=jest-cjs-uniso.config.js", "test-cjs-iso": "jest --clearCache && jest -c=jest-cjs-iso.config.js", "test-esm-iso": "jest --clearCache && node --experimental-vm-modules ./node_modules/jest/bin/jest.js -c=jest-esm-iso.config.js", + "test-esm-uniso": "jest --clearCache && node --experimental-vm-modules ./node_modules/jest/bin/jest.js -c=jest-esm-uniso.config.js", "lint": "ng lint" }, "private": true, diff --git a/e2e/test-app-v9/jest-esm-uniso.config.js b/e2e/test-app-v9/jest-esm-uniso.config.js new file mode 100644 index 0000000000..9997a613e9 --- /dev/null +++ b/e2e/test-app-v9/jest-esm-uniso.config.js @@ -0,0 +1,20 @@ +require('jest-preset-angular/ngcc-jest-processor'); +const baseConfig = require('./jest.config'); + +/** @type {import('ts-jest/dist/types').ProjectConfigTsJest} */ +module.exports = { + ...baseConfig, + extensionsToTreatAsEsm: ['.ts'], + globals: { + 'ts-jest': { + tsconfig: '/tsconfig-esm.spec.json', + stringifyContentPathRegex: '\\.html$', + useESM: true, + } + }, + moduleNameMapper: { + ...baseConfig.moduleNameMapper, + 'tslib': '/node_modules/tslib/tslib.es6.js', + }, + transformIgnorePatterns: ['node_modules/(?!tslib)'], +}; diff --git a/e2e/test-app-v9/package.json b/e2e/test-app-v9/package.json index a68d09d47c..024d3f9528 100644 --- a/e2e/test-app-v9/package.json +++ b/e2e/test-app-v9/package.json @@ -8,6 +8,7 @@ "test-cjs-uniso": "jest --clearCache && jest -c=jest-cjs-uniso.config.js", "test-cjs-iso": "jest --clearCache && jest -c=jest-cjs-iso.config.js", "test-esm-iso": "jest --clearCache && node --experimental-vm-modules ./node_modules/jest/bin/jest.js -c=jest-esm-iso.config.js", + "test-esm-uniso": "jest --clearCache && node --experimental-vm-modules ./node_modules/jest/bin/jest.js -c=jest-esm-uniso.config.js", "lint": "ng lint" }, "private": true, diff --git a/scripts/e2e.js b/scripts/e2e.js index 46a0abe0cc..028a7ce85e 100755 --- a/scripts/e2e.js +++ b/scripts/e2e.js @@ -57,13 +57,16 @@ const executeTest = (projectRealPath) => { const cmdCjsUnIso = ['yarn', 'test-cjs-uniso']; const cmdCjsIso = ['yarn', 'test-cjs-iso']; const cmdESMIso = ['yarn', 'test-esm-iso']; + const cmdESMUnIso = ['yarn', 'test-esm-uniso']; if (jestArgs.length) { cmdCjsUnIso.push('--'); cmdCjsIso.push('--'); cmdESMIso.push('--'); + cmdESMUnIso.push('--'); cmdCjsUnIso.push(...jestArgs); cmdCjsIso.push(...jestArgs); cmdESMIso.push(...jestArgs); + cmdESMUnIso.push(...jestArgs); } logger.log('STARTING NONE ISOLATED MODULES TESTS'); @@ -77,6 +80,16 @@ const executeTest = (projectRealPath) => { env: process.env, }); + logger.log(); + logger.log('starting the ESM tests using:', ...cmdESMUnIso); + logger.log(); + + execa.sync(cmdESMUnIso.shift(), cmdESMUnIso, { + cwd: projectRealPath, + stdio: 'inherit', + env: process.env, + }); + logger.log(); logger.log('STARTING ISOLATED MODULES TESTS'); logger.log(); diff --git a/src/__tests__/__snapshots__/ng-jest-compiler.spec.ts.snap b/src/__tests__/__snapshots__/ng-jest-compiler.spec.ts.snap index c7cd22ec37..a1678d03b5 100644 --- a/src/__tests__/__snapshots__/ng-jest-compiler.spec.ts.snap +++ b/src/__tests__/__snapshots__/ng-jest-compiler.spec.ts.snap @@ -1,5 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`NgJestCompiler with isolatedModule false should compile codes with useESM true 1`] = ` +"import { __decorate } from \\"tslib\\"; +import __NG_CLI_RESOURCE__0 from \\"./app.component.html\\"; +import { Component } from '@angular/core'; +let AppComponent = class AppComponent { + constructor() { + this.title = 'test-app-v10'; + } +}; +AppComponent = __decorate([ + Component({ + selector: 'app-root', + template: __NG_CLI_RESOURCE__0, + styles: [] + }) +], AppComponent); +export { AppComponent }; +//# " +`; + exports[`NgJestCompiler with isolatedModule false should throw diagnostics error for new file which is: known by Program 1`] = `"src/__tests__/__mocks__/foo.component.ts(8,3): error TS2322: Type '\\"test-app-v10\\"' is not assignable to type 'number'."`; exports[`NgJestCompiler with isolatedModule false should throw diagnostics error for new file which is: not known by Program 1`] = `"src/__tests__/__mocks__/foo.component.ts(8,3): error TS2322: Type '\\"test-app-v10\\"' is not assignable to type 'number'."`; diff --git a/src/__tests__/__snapshots__/replace-resources.spec.ts.snap b/src/__tests__/__snapshots__/replace-resources.spec.ts.snap index 240ab2ce5a..853eaf6e61 100644 --- a/src/__tests__/__snapshots__/replace-resources.spec.ts.snap +++ b/src/__tests__/__snapshots__/replace-resources.spec.ts.snap @@ -23,24 +23,22 @@ exports.AppComponent = AppComponent; `; exports[`Replace resources transformer with isolatedModules false should use replaceResources transformer from @angular/compiler-cli with useESM 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\\"); +"import { __decorate } from \\"tslib\\"; +import __NG_CLI_RESOURCE__0 from \\"./app.component.html\\"; +import { Component } from '@angular/core'; let AppComponent = class AppComponent { constructor() { this.title = 'test-app-v10'; } }; -AppComponent = tslib_1.__decorate([ - core_1.Component({ +AppComponent = __decorate([ + Component({ selector: 'app-root', - template: require(\\"./app.component.html\\"), + template: __NG_CLI_RESOURCE__0, styles: [] }) ], AppComponent); -exports.AppComponent = AppComponent; +export { AppComponent }; //# " `; diff --git a/src/__tests__/ng-jest-compiler.spec.ts b/src/__tests__/ng-jest-compiler.spec.ts index e452f819e1..c687626bab 100644 --- a/src/__tests__/ng-jest-compiler.spec.ts +++ b/src/__tests__/ng-jest-compiler.spec.ts @@ -2,6 +2,7 @@ import { readFileSync } from 'fs'; import { join } from 'path'; import { jest } from '@jest/globals'; +import { SOURCE_MAPPING_PREFIX } from 'ts-jest/dist/compiler/compiler-utils'; import ts from 'typescript'; import { NgJestCompiler } from '../compiler/ng-jest-compiler'; @@ -50,10 +51,15 @@ describe('NgJestCompiler', () => { // @ts-expect-error testing purpose expect(compiler._transpileModule).toHaveBeenCalled(); // @ts-expect-error testing purpose - const moduleKind = compiler._transpileModule.mock.calls[0][1].compilerOptions.module; - useESM - ? expect(moduleKind).not.toEqual(ts.ModuleKind.CommonJS) - : expect(moduleKind).toEqual(ts.ModuleKind.CommonJS); + const { module, esModuleInterop, allowSyntheticDefaultImports } = compiler._transpileModule.mock.calls[0][1] + .compilerOptions as ts.CompilerOptions; + if (useESM) { + expect(module).not.toEqual(ts.ModuleKind.CommonJS); + expect(allowSyntheticDefaultImports).toEqual(true); + expect(esModuleInterop).toEqual(true); + } else { + expect(module).toEqual(ts.ModuleKind.CommonJS); + } }); }); @@ -81,7 +87,6 @@ describe('NgJestCompiler', () => { 'exports.AppComponent = AppComponent;\n' + '//# sourceMappingURL=app.component.js.map\n'; - const ngJestConfig = new NgJestConfig(jestCfgStub); const noErrorFileName = join(mockFolder, 'app.component.ts'); const noErrorFileContent = readFileSync(noErrorFileName, 'utf-8'); const hasErrorFileName = join(mockFolder, 'foo.component.ts'); @@ -90,6 +95,7 @@ describe('NgJestCompiler', () => { test.each([noErrorFileName, undefined])( 'should return compiled result for new file which is not known or known by Program', (fileName) => { + const ngJestConfig = new NgJestConfig(jestCfgStub); ngJestConfig.parsedTsConfig = { ...ngJestConfig.parsedTsConfig, rootNames: fileName ? [fileName] : [], @@ -107,7 +113,28 @@ describe('NgJestCompiler', () => { }, ); + test('should compile codes with useESM true', () => { + const ngJestConfig = new NgJestConfig({ + ...jestCfgStub, + globals: { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + 'ts-jest': { + ...jestCfgStub.globals['ts-jest'], + useESM: true, + }, + }, + }); + const compiler = new NgJestCompiler(ngJestConfig, new Map()); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const emittedResult = compiler.getCompiledOutput(noErrorFileName, noErrorFileContent, true)!; + + // 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.each([hasErrorFileName, undefined])('should throw diagnostics error for new file which is', (fileName) => { + const ngJestConfig = new NgJestConfig(jestCfgStub); ngJestConfig.parsedTsConfig = { ...ngJestConfig.parsedTsConfig, rootNames: fileName ? [fileName] : [], @@ -120,6 +147,7 @@ describe('NgJestCompiler', () => { }); test('should not throw diagnostics error when shouldReportDiagnostics return false', () => { + const ngJestConfig = new NgJestConfig(jestCfgStub); ngJestConfig.parsedTsConfig = { ...ngJestConfig.parsedTsConfig, rootNames: [hasErrorFileName], diff --git a/src/compiler/ng-jest-compiler.ts b/src/compiler/ng-jest-compiler.ts index 576d193c97..e7bf145f2f 100644 --- a/src/compiler/ng-jest-compiler.ts +++ b/src/compiler/ng-jest-compiler.ts @@ -46,8 +46,30 @@ export class NgJestCompiler implements CompilerInstance { getCompiledOutput(fileName: string, fileContent: string, supportsStaticESM: boolean): string { const customTransformers = this.ngJestConfig.customTransformers; + let moduleKind = this._compilerOptions.module; + let esModuleInterop = this._compilerOptions.esModuleInterop; + let allowSyntheticDefaultImports = this._compilerOptions.allowSyntheticDefaultImports; + if (supportsStaticESM && this.ngJestConfig.useESM) { + moduleKind = + !moduleKind || + (moduleKind && + ![this._ts.ModuleKind.ES2015, this._ts.ModuleKind.ES2020, this._ts.ModuleKind.ESNext].includes(moduleKind)) + ? this._ts.ModuleKind.ESNext + : moduleKind; + // Make sure `esModuleInterop` and `allowSyntheticDefaultImports` true to support import CJS into ESM + esModuleInterop = true; + allowSyntheticDefaultImports = true; + } else { + moduleKind = this._ts.ModuleKind.CommonJS; + } + this._compilerOptions = { + ...this._compilerOptions, + allowSyntheticDefaultImports, + esModuleInterop, + module: moduleKind, + }; if (this._program) { - const allDiagnostics = []; + const allDiagnostics: ts.Diagnostic[] = []; if (!this._rootNames.includes(fileName)) { this._logger.debug({ fileName }, 'getCompiledOutput: update memory host, rootFiles and Program'); @@ -95,28 +117,13 @@ export class NgJestCompiler implements CompilerInstance { return ''; } } else { - let moduleKind = this._compilerOptions.module; - if (supportsStaticESM && this.ngJestConfig.useESM) { - moduleKind = - !moduleKind || - (moduleKind && - ![this._ts.ModuleKind.ES2015, this._ts.ModuleKind.ES2020, this._ts.ModuleKind.ESNext].includes(moduleKind)) - ? this._ts.ModuleKind.ESNext - : moduleKind; - } else { - moduleKind = this._ts.ModuleKind.CommonJS; - } - this._logger.debug({ fileName }, 'getCompiledOutput: compiling as isolated module'); const result: ts.TranspileOutput = this._transpileModule( fileContent, { fileName, - compilerOptions: { - ...this._compilerOptions, - module: moduleKind, - }, + compilerOptions: this._compilerOptions, }, customTransformers, );