Skip to content

Commit

Permalink
feat(compiler): support ESM for isolatedModules: false (#721)
Browse files Browse the repository at this point in the history
- 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`
  • Loading branch information
ahnpnl committed Jan 8, 2021
1 parent 5888706 commit a2166f8
Show file tree
Hide file tree
Showing 11 changed files with 160 additions and 31 deletions.
20 changes: 20 additions & 0 deletions 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: '<rootDir>/tsconfig-esm.spec.json',
stringifyContentPathRegex: '\\.html$',
useESM: true,
}
},
moduleNameMapper: {
...baseConfig.moduleNameMapper,
'tslib': '<rootDir>/node_modules/tslib/tslib.es6.js',
},
transformIgnorePatterns: ['node_modules/(?!tslib)'],
};
1 change: 1 addition & 0 deletions e2e/test-app-v10/package.json
Expand Up @@ -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,
Expand Down
20 changes: 20 additions & 0 deletions 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: '<rootDir>/tsconfig-esm.spec.json',
stringifyContentPathRegex: '\\.html$',
useESM: true,
}
},
moduleNameMapper: {
...baseConfig.moduleNameMapper,
'tslib': '<rootDir>/node_modules/tslib/tslib.es6.js',
},
transformIgnorePatterns: ['node_modules/(?!tslib)'],
};
1 change: 1 addition & 0 deletions e2e/test-app-v11/package.json
Expand Up @@ -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,
Expand Down
20 changes: 20 additions & 0 deletions 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: '<rootDir>/tsconfig-esm.spec.json',
stringifyContentPathRegex: '\\.html$',
useESM: true,
}
},
moduleNameMapper: {
...baseConfig.moduleNameMapper,
'tslib': '<rootDir>/node_modules/tslib/tslib.es6.js',
},
transformIgnorePatterns: ['node_modules/(?!tslib)'],
};
1 change: 1 addition & 0 deletions e2e/test-app-v9/package.json
Expand Up @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions scripts/e2e.js
Expand Up @@ -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');
Expand All @@ -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();
Expand Down
20 changes: 20 additions & 0 deletions 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'."`;
16 changes: 7 additions & 9 deletions src/__tests__/__snapshots__/replace-resources.spec.ts.snap
Expand Up @@ -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 };
//# "
`;

Expand Down
38 changes: 33 additions & 5 deletions src/__tests__/ng-jest-compiler.spec.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}
});
});

Expand Down Expand Up @@ -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');
Expand All @@ -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] : [],
Expand All @@ -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] : [],
Expand All @@ -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],
Expand Down
41 changes: 24 additions & 17 deletions src/compiler/ng-jest-compiler.ts
Expand Up @@ -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');

Expand Down Expand Up @@ -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,
);
Expand Down

0 comments on commit a2166f8

Please sign in to comment.