From 1de3bf04886fbc91ab821e965ec94a2d8dc741be Mon Sep 17 00:00:00 2001 From: Ahn <27772165+ahnpnl@users.noreply.github.com> Date: Mon, 15 Nov 2021 16:01:54 +0100 Subject: [PATCH] perf: process `js` files in `node_modules` with `esbuild` (#1169) BREAKING CHANGE `js` files from `node_modules` are now compiled with `esbuild` to improve performance --- .gitignore | 1 + e2e/__tests__/ast-transformers.test.ts | 10 +- e2e/__tests__/async.test.ts | 6 +- e2e/__tests__/babel-support.test.ts | 6 +- e2e/__tests__/custom-typings.test.ts | 6 +- e2e/__tests__/jest-globals.test.ts | 6 +- e2e/__tests__/ng-deep-import.test.ts | 6 +- e2e/__tests__/ng-lib-import.test.ts | 6 +- e2e/__tests__/path-mapping.test.ts | 6 +- e2e/__tests__/process-js-packages.test.ts | 27 +++ e2e/__tests__/snapshot-serializers.test.ts | 6 +- .../__tests__/foo-js-packages.spec.ts | 5 + .../node_modules/my-lib/index.d.ts | 1 + .../node_modules/my-lib/index.js | 3 + .../node_modules/my-lib/package.json | 5 + e2e/process-js-packages/package.json | 15 ++ e2e/run-jest.ts | 4 + ...est.ts.snap => foo.component.spec.ts.snap} | 0 ...omponent.test.ts => foo.component.spec.ts} | 0 scripts/test-examples.js | 9 + .../ng-jest-transformer.spec.ts.snap | 186 +++++++++++++++++- src/__tests__/ng-jest-transformer.spec.ts | 178 ++++++++++++++--- src/ng-jest-transformer.ts | 39 +++- 23 files changed, 471 insertions(+), 60 deletions(-) create mode 100644 e2e/__tests__/process-js-packages.test.ts create mode 100644 e2e/process-js-packages/__tests__/foo-js-packages.spec.ts create mode 100644 e2e/process-js-packages/node_modules/my-lib/index.d.ts create mode 100644 e2e/process-js-packages/node_modules/my-lib/index.js create mode 100644 e2e/process-js-packages/node_modules/my-lib/package.json create mode 100644 e2e/process-js-packages/package.json rename e2e/snapshot-serializers/__tests__/__snapshots__/{foo.component.test.ts.snap => foo.component.spec.ts.snap} (100%) rename e2e/snapshot-serializers/__tests__/{foo.component.test.ts => foo.component.spec.ts} (100%) diff --git a/.gitignore b/.gitignore index 0fe167cd37..ccd27a6ddf 100644 --- a/.gitignore +++ b/.gitignore @@ -18,5 +18,6 @@ e2e/**/.yarn/* !e2e/**/.yarn/sdks !e2e/**/.yarn/versions !/e2e/ng-lib-import/node_modules +!/e2e/process-js-packages/node_modules src/transformers/downlevel_decorators_transform src/ngtsc diff --git a/e2e/__tests__/ast-transformers.test.ts b/e2e/__tests__/ast-transformers.test.ts index 661cd8e9df..8fbacf94e7 100644 --- a/e2e/__tests__/ast-transformers.test.ts +++ b/e2e/__tests__/ast-transformers.test.ts @@ -1,16 +1,16 @@ -import { json as runWithJson } from '../run-jest'; +import { jsonNoCache as runWithJsonNoCache } from '../run-jest'; describe('hoisting', () => { const DIR = 'ast-transformers/hoisting'; test(`successfully runs the tests inside ${DIR} with isolatedModules: false`, () => { - const { json } = runWithJson(DIR); + const { json } = runWithJsonNoCache(DIR); expect(json.success).toBe(true); }); test(`successfully runs the tests inside ${DIR} with isolatedModules: true`, () => { - const { json } = runWithJson(DIR, ['-c=jest-isolated.config.js']); + const { json } = runWithJsonNoCache(DIR, ['-c=jest-isolated.config.js']); expect(json.success).toBe(true); }); @@ -20,13 +20,13 @@ describe('ng-jit-transformers', () => { const DIR = 'ast-transformers/ng-jit-transformers'; test(`successfully runs the tests inside ${DIR} with isolatedModules: false`, () => { - const { json } = runWithJson(DIR); + const { json } = runWithJsonNoCache(DIR); expect(json.success).toBe(true); }); test(`successfully runs the tests inside ${DIR} with isolatedModules: true`, () => { - const { json } = runWithJson(DIR, ['-c=jest-isolated.config.js']); + const { json } = runWithJsonNoCache(DIR, ['-c=jest-isolated.config.js']); expect(json.success).toBe(true); }); diff --git a/e2e/__tests__/async.test.ts b/e2e/__tests__/async.test.ts index 6329a21654..e2081ae8b7 100644 --- a/e2e/__tests__/async.test.ts +++ b/e2e/__tests__/async.test.ts @@ -1,6 +1,6 @@ import path from 'path'; -import { json as runWithJson } from '../run-jest'; +import { jsonNoCache as runWithJsonNoCache } from '../run-jest'; import { runYarnInstall } from '../utils'; const DIR = path.join(__dirname, '..', 'async'); @@ -10,13 +10,13 @@ beforeAll(() => { }); test(`successfully runs the tests inside ${DIR} with isolatedModules: false`, () => { - const { json } = runWithJson(DIR); + const { json } = runWithJsonNoCache(DIR); expect(json.success).toBe(true); }); test(`successfully runs the tests inside ${DIR} with isolatedModules: true`, () => { - const { json } = runWithJson(DIR, ['-c=jest-isolated.config.js']); + const { json } = runWithJsonNoCache(DIR, ['-c=jest-isolated.config.js']); expect(json.success).toBe(true); }); diff --git a/e2e/__tests__/babel-support.test.ts b/e2e/__tests__/babel-support.test.ts index 22d4670810..2dc94f6b1b 100644 --- a/e2e/__tests__/babel-support.test.ts +++ b/e2e/__tests__/babel-support.test.ts @@ -1,6 +1,6 @@ import path from 'path'; -import { json as runWithJson } from '../run-jest'; +import { jsonNoCache as runWithJsonNoCache } from '../run-jest'; import { runYarnInstall } from '../utils'; const DIR = path.join(__dirname, '..', 'babel-support'); @@ -10,13 +10,13 @@ beforeAll(() => { }); test(`successfully runs the tests inside ${DIR} with isolatedModules: false`, () => { - const { json } = runWithJson(DIR); + const { json } = runWithJsonNoCache(DIR); expect(json.success).toBe(true); }); test(`successfully runs the tests inside ${DIR} with isolatedModules: true`, () => { - const { json } = runWithJson(DIR, ['-c=jest-isolated.config.js']); + const { json } = runWithJsonNoCache(DIR, ['-c=jest-isolated.config.js']); expect(json.success).toBe(true); }); diff --git a/e2e/__tests__/custom-typings.test.ts b/e2e/__tests__/custom-typings.test.ts index b359cf1a49..f9fa5e580e 100644 --- a/e2e/__tests__/custom-typings.test.ts +++ b/e2e/__tests__/custom-typings.test.ts @@ -1,6 +1,6 @@ import path from 'path'; -import { json as runWithJson } from '../run-jest'; +import { jsonNoCache as runWithJsonNoCache } from '../run-jest'; import { runYarnInstall } from '../utils'; const DIR = path.join(__dirname, '..', 'custom-typings'); @@ -10,13 +10,13 @@ beforeAll(() => { }); test(`successfully runs the tests inside ${DIR} with isolatedModules: false`, () => { - const { json } = runWithJson(DIR); + const { json } = runWithJsonNoCache(DIR); expect(json.success).toBe(true); }); test(`successfully runs the tests inside ${DIR} with isolatedModules: true`, () => { - const { json } = runWithJson(DIR, ['-c=jest-isolated.config.js']); + const { json } = runWithJsonNoCache(DIR, ['-c=jest-isolated.config.js']); expect(json.success).toBe(true); }); diff --git a/e2e/__tests__/jest-globals.test.ts b/e2e/__tests__/jest-globals.test.ts index 9b5ec2d9e4..8df302999f 100644 --- a/e2e/__tests__/jest-globals.test.ts +++ b/e2e/__tests__/jest-globals.test.ts @@ -1,15 +1,15 @@ -import { json as runWithJson } from '../run-jest'; +import { jsonNoCache as runWithJsonNoCache } from '../run-jest'; const DIR = 'jest-globals'; test(`successfully runs the tests inside ${DIR} with isolatedModules: false`, () => { - const { json } = runWithJson(DIR); + const { json } = runWithJsonNoCache(DIR); expect(json.success).toBe(true); }); test(`successfully runs the tests inside ${DIR} with isolatedModules: true`, () => { - const { json } = runWithJson(DIR, ['-c=jest-isolated.config.js']); + const { json } = runWithJsonNoCache(DIR, ['-c=jest-isolated.config.js']); expect(json.success).toBe(true); }); diff --git a/e2e/__tests__/ng-deep-import.test.ts b/e2e/__tests__/ng-deep-import.test.ts index 99f70b2526..98dba07ed5 100644 --- a/e2e/__tests__/ng-deep-import.test.ts +++ b/e2e/__tests__/ng-deep-import.test.ts @@ -1,15 +1,15 @@ -import { json as runWithJson } from '../run-jest'; +import { jsonNoCache as runWithJsonNoCache } from '../run-jest'; const DIR = 'ng-deep-import'; test(`successfully runs the tests inside ${DIR} with isolatedModules: false`, () => { - const { json } = runWithJson(DIR); + const { json } = runWithJsonNoCache(DIR); expect(json.success).toBe(true); }); test(`successfully runs the tests inside ${DIR} with isolatedModules: true`, () => { - const { json } = runWithJson(DIR, ['-c=jest-isolated.config.js']); + const { json } = runWithJsonNoCache(DIR, ['-c=jest-isolated.config.js']); expect(json.success).toBe(true); }); diff --git a/e2e/__tests__/ng-lib-import.test.ts b/e2e/__tests__/ng-lib-import.test.ts index 10807a1d69..bd212028a7 100644 --- a/e2e/__tests__/ng-lib-import.test.ts +++ b/e2e/__tests__/ng-lib-import.test.ts @@ -1,15 +1,15 @@ -import { json as runWithJson } from '../run-jest'; +import { jsonNoCache as runWithJsonNoCache } from '../run-jest'; const DIR = 'ng-lib-import'; test(`successfully runs the tests inside ${DIR} with isolatedModules: false`, () => { - const { json } = runWithJson(DIR); + const { json } = runWithJsonNoCache(DIR); expect(json.success).toBe(true); }); test(`successfully runs the tests inside ${DIR} with isolatedModules: true`, () => { - const { json } = runWithJson(DIR, ['-c=jest-isolated.config.js']); + const { json } = runWithJsonNoCache(DIR, ['-c=jest-isolated.config.js']); expect(json.success).toBe(true); }); diff --git a/e2e/__tests__/path-mapping.test.ts b/e2e/__tests__/path-mapping.test.ts index 7cece0dfbf..2de5e65014 100644 --- a/e2e/__tests__/path-mapping.test.ts +++ b/e2e/__tests__/path-mapping.test.ts @@ -1,15 +1,15 @@ -import { json as runWithJson } from '../run-jest'; +import { jsonNoCache as runWithJsonNoCache } from '../run-jest'; const DIR = 'path-mapping'; test(`successfully runs the tests inside ${DIR} with isolatedModules: false`, () => { - const { json } = runWithJson(DIR); + const { json } = runWithJsonNoCache(DIR); expect(json.success).toBe(true); }); test(`successfully runs the tests inside ${DIR} with isolatedModules: true`, () => { - const { json } = runWithJson(DIR, ['-c=jest-isolated.config.js']); + const { json } = runWithJsonNoCache(DIR, ['-c=jest-isolated.config.js']); expect(json.success).toBe(true); }); diff --git a/e2e/__tests__/process-js-packages.test.ts b/e2e/__tests__/process-js-packages.test.ts new file mode 100644 index 0000000000..d0900770f9 --- /dev/null +++ b/e2e/__tests__/process-js-packages.test.ts @@ -0,0 +1,27 @@ +import fs from 'fs'; +import path from 'path'; + +import { jsonNoCache as runWithJsonNoCache } from '../run-jest'; + +const TEST_DIR_NAME = 'process-js-packages'; +const TEST_DIR_PATH = path.join(__dirname, '..', TEST_DIR_NAME); +const LOG_FILE_NAME = 'ng-jest.log'; +const LOG_FILE_PATH = path.join(TEST_DIR_PATH, LOG_FILE_NAME); + +test(`successfully runs the tests inside ${TEST_DIR_NAME}`, () => { + process.env.NG_JEST_LOG = LOG_FILE_NAME; + + const { json } = runWithJsonNoCache(TEST_DIR_NAME); + + expect(json.success).toBe(true); + expect(fs.existsSync(LOG_FILE_PATH)).toBe(true); + + const logFileContent = fs.readFileSync(LOG_FILE_PATH, 'utf-8'); + const logFileContentAsJson = JSON.parse(logFileContent); + + expect(/node_modules\/(.*.m?js$)/.test(logFileContentAsJson.context.filePath.replace(/\\/g, '/'))).toBe(true); + expect(logFileContentAsJson.message).toBe('process with esbuild'); + + delete process.env.NG_JEST_LOG; + fs.unlinkSync(LOG_FILE_PATH); +}); diff --git a/e2e/__tests__/snapshot-serializers.test.ts b/e2e/__tests__/snapshot-serializers.test.ts index b029cd29e0..f986143ba3 100644 --- a/e2e/__tests__/snapshot-serializers.test.ts +++ b/e2e/__tests__/snapshot-serializers.test.ts @@ -1,15 +1,15 @@ -import { json as runWithJson } from '../run-jest'; +import { jsonNoCache as runWithJsonNoCache } from '../run-jest'; const DIR = 'snapshot-serializers'; test(`successfully runs the tests inside ${DIR} with isolatedModules: false`, () => { - const { json } = runWithJson(DIR); + const { json } = runWithJsonNoCache(DIR); expect(json.success).toBe(true); }); test(`successfully runs the tests inside ${DIR} with isolatedModules: true`, () => { - const { json } = runWithJson(DIR, ['-c=jest-isolated.config.js']); + const { json } = runWithJsonNoCache(DIR, ['-c=jest-isolated.config.js']); expect(json.success).toBe(true); }); diff --git a/e2e/process-js-packages/__tests__/foo-js-packages.spec.ts b/e2e/process-js-packages/__tests__/foo-js-packages.spec.ts new file mode 100644 index 0000000000..0926f4edce --- /dev/null +++ b/e2e/process-js-packages/__tests__/foo-js-packages.spec.ts @@ -0,0 +1,5 @@ +import { foo } from 'my-lib'; + +test('should pass', () => { + expect(foo).toBe(1); +}); diff --git a/e2e/process-js-packages/node_modules/my-lib/index.d.ts b/e2e/process-js-packages/node_modules/my-lib/index.d.ts new file mode 100644 index 0000000000..959296a1f7 --- /dev/null +++ b/e2e/process-js-packages/node_modules/my-lib/index.d.ts @@ -0,0 +1 @@ +export declare const foo: number; \ No newline at end of file diff --git a/e2e/process-js-packages/node_modules/my-lib/index.js b/e2e/process-js-packages/node_modules/my-lib/index.js new file mode 100644 index 0000000000..95c5d282cb --- /dev/null +++ b/e2e/process-js-packages/node_modules/my-lib/index.js @@ -0,0 +1,3 @@ +const foo = 1; + +export { foo }; \ No newline at end of file diff --git a/e2e/process-js-packages/node_modules/my-lib/package.json b/e2e/process-js-packages/node_modules/my-lib/package.json new file mode 100644 index 0000000000..0f733a9ef2 --- /dev/null +++ b/e2e/process-js-packages/node_modules/my-lib/package.json @@ -0,0 +1,5 @@ +{ + "name": "my-lib", + "version": "1.0.0", + "main": "index.js" +} diff --git a/e2e/process-js-packages/package.json b/e2e/process-js-packages/package.json new file mode 100644 index 0000000000..5991425e7e --- /dev/null +++ b/e2e/process-js-packages/package.json @@ -0,0 +1,15 @@ +{ + "name": "process-js-packages", + "jest": { + "moduleFileExtensions": ["ts", "html", "js", "json", "mjs"], + "globals": { + "ts-jest": { + "isolatedModules": true + } + }, + "transform": { + "^.+\\.(ts|js|mjs|html)$": "/../../build/index.js" + }, + "transformIgnorePatterns": ["node_modules/(?!my-lib)"] + } +} diff --git a/e2e/run-jest.ts b/e2e/run-jest.ts index 7f3199d1bc..b21e3f632e 100644 --- a/e2e/run-jest.ts +++ b/e2e/run-jest.ts @@ -118,6 +118,10 @@ export const json = function (dir: string, args?: string[], options: RunJestOpti } }; +export const jsonNoCache = (dir: string, args?: string[], options: RunJestOptions = {}): RunJestJsonResult => { + return json(dir, args ? [...args, '--no-cache'] : ['--no-cache'], options); +}; + export const onNodeVersions = (versionRange: string, testBody: () => void): void => { const description = `on node ${versionRange}`; if (semver.satisfies(process.versions.node, versionRange)) { diff --git a/e2e/snapshot-serializers/__tests__/__snapshots__/foo.component.test.ts.snap b/e2e/snapshot-serializers/__tests__/__snapshots__/foo.component.spec.ts.snap similarity index 100% rename from e2e/snapshot-serializers/__tests__/__snapshots__/foo.component.test.ts.snap rename to e2e/snapshot-serializers/__tests__/__snapshots__/foo.component.spec.ts.snap diff --git a/e2e/snapshot-serializers/__tests__/foo.component.test.ts b/e2e/snapshot-serializers/__tests__/foo.component.spec.ts similarity index 100% rename from e2e/snapshot-serializers/__tests__/foo.component.test.ts rename to e2e/snapshot-serializers/__tests__/foo.component.spec.ts diff --git a/scripts/test-examples.js b/scripts/test-examples.js index 7e130b36e3..4a2762fa1b 100755 --- a/scripts/test-examples.js +++ b/scripts/test-examples.js @@ -9,6 +9,7 @@ const { exampleAppsToRun, rootDir } = require('./paths'); const executeTest = (projectPath) => { // we change current directory process.chdir(projectPath); + // reading package.json const projectPkg = require(join(projectPath, 'package.json')); if (!projectPkg.name) projectPkg.name = 'unknown'; @@ -19,6 +20,11 @@ const executeTest = (projectPath) => { logger.log('='.repeat(20), `${projectPkg.name}@${projectPkg.version}`, 'in', projectPath, '='.repeat(20)); logger.log(); + logger.log('cleaning up'); + logger.log(); + + execa.sync('rimraf', [join(projectPath, 'node_modules')]); + // then we install it in the repo logger.log('ensuring all dependencies of target project are installed'); logger.log(); @@ -50,6 +56,7 @@ const executeTest = (projectPath) => { env: process.env, }); + logger.log(); logger.log('starting the CommonJS tests with isolatedModules: true using:', ...cmdIsolatedLine); logger.log(); @@ -61,6 +68,7 @@ const executeTest = (projectPath) => { // TODO: Enable when fully support ESM with Angular 13 if (!projectPkg.version.startsWith('13')) { + logger.log(); logger.log('starting the ESM tests with isolatedModules: false using:', ...cmdESMLine); logger.log(); @@ -70,6 +78,7 @@ const executeTest = (projectPath) => { env: process.env, }); + logger.log(); logger.log('starting the ESM tests with isolatedModules: true using:', ...cmdESMIsolatedLine); logger.log(); diff --git a/src/__tests__/__snapshots__/ng-jest-transformer.spec.ts.snap b/src/__tests__/__snapshots__/ng-jest-transformer.spec.ts.snap index 93ff237e26..7a6faae06a 100644 --- a/src/__tests__/__snapshots__/ng-jest-transformer.spec.ts.snap +++ b/src/__tests__/__snapshots__/ng-jest-transformer.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`NgJestTransformer should process successfully a mjs file to CommonJS codes 1`] = ` +exports[`NgJestTransformer should use esbuild to process mjs or \`node_modules\` js files to CJS codes 1`] = ` Array [ " const pi = parseFloat(3.124); @@ -10,13 +10,16 @@ Array [ Object { "format": "cjs", "loader": "js", + "sourceRoot": undefined, + "sourcefile": "foo.mjs", "sourcemap": false, + "sourcesContent": true, "target": "es2015", }, ] `; -exports[`NgJestTransformer should process successfully a mjs file to CommonJS codes 2`] = ` +exports[`NgJestTransformer should use esbuild to process mjs or \`node_modules\` js files to CJS codes 2`] = ` Array [ " const pi = parseFloat(3.124); @@ -26,13 +29,35 @@ Array [ Object { "format": "cjs", "loader": "js", + "sourceRoot": undefined, + "sourcefile": "node_modules\\\\foo.js", + "sourcemap": false, + "sourcesContent": true, + "target": "es2015", + }, +] +`; + +exports[`NgJestTransformer should use esbuild to process mjs or \`node_modules\` js files to CJS codes 3`] = ` +Array [ + " + const pi = parseFloat(3.124); + + export { pi }; + ", + Object { + "format": "cjs", + "loader": "js", + "sourceRoot": undefined, + "sourcefile": "foo.mjs", "sourcemap": true, + "sourcesContent": true, "target": "es2016", }, ] `; -exports[`NgJestTransformer should process successfully a mjs file to CommonJS codes 3`] = ` +exports[`NgJestTransformer should use esbuild to process mjs or \`node_modules\` js files to CJS codes 4`] = ` Array [ " const pi = parseFloat(3.124); @@ -42,7 +67,162 @@ Array [ Object { "format": "cjs", "loader": "js", + "sourceRoot": undefined, + "sourcefile": "node_modules\\\\foo.js", + "sourcemap": true, + "sourcesContent": true, + "target": "es2016", + }, +] +`; + +exports[`NgJestTransformer should use esbuild to process mjs or \`node_modules\` js files to CJS codes 5`] = ` +Array [ + " + const pi = parseFloat(3.124); + + export { pi }; + ", + Object { + "format": "cjs", + "loader": "js", + "sourceRoot": undefined, + "sourcefile": "foo.mjs", + "sourcemap": true, + "sourcesContent": true, + "target": "es2015", + }, +] +`; + +exports[`NgJestTransformer should use esbuild to process mjs or \`node_modules\` js files to CJS codes 6`] = ` +Array [ + " + const pi = parseFloat(3.124); + + export { pi }; + ", + Object { + "format": "cjs", + "loader": "js", + "sourceRoot": undefined, + "sourcefile": "node_modules\\\\foo.js", + "sourcemap": true, + "sourcesContent": true, + "target": "es2015", + }, +] +`; + +exports[`NgJestTransformer should use esbuild to process mjs or \`node_modules\` js files to ESM codes 1`] = ` +Array [ + " + const pi = parseFloat(3.124); + + export { pi }; + ", + Object { + "format": "esm", + "loader": "js", + "sourceRoot": undefined, + "sourcefile": "foo.mjs", + "sourcemap": false, + "sourcesContent": true, + "target": "es2015", + }, +] +`; + +exports[`NgJestTransformer should use esbuild to process mjs or \`node_modules\` js files to ESM codes 2`] = ` +Array [ + " + const pi = parseFloat(3.124); + + export { pi }; + ", + Object { + "format": "esm", + "loader": "js", + "sourceRoot": undefined, + "sourcefile": "node_modules\\\\foo.js", + "sourcemap": false, + "sourcesContent": true, + "target": "es2015", + }, +] +`; + +exports[`NgJestTransformer should use esbuild to process mjs or \`node_modules\` js files to ESM codes 3`] = ` +Array [ + " + const pi = parseFloat(3.124); + + export { pi }; + ", + Object { + "format": "esm", + "loader": "js", + "sourceRoot": undefined, + "sourcefile": "foo.mjs", + "sourcemap": true, + "sourcesContent": true, + "target": "es2016", + }, +] +`; + +exports[`NgJestTransformer should use esbuild to process mjs or \`node_modules\` js files to ESM codes 4`] = ` +Array [ + " + const pi = parseFloat(3.124); + + export { pi }; + ", + Object { + "format": "esm", + "loader": "js", + "sourceRoot": undefined, + "sourcefile": "node_modules\\\\foo.js", + "sourcemap": true, + "sourcesContent": true, + "target": "es2016", + }, +] +`; + +exports[`NgJestTransformer should use esbuild to process mjs or \`node_modules\` js files to ESM codes 5`] = ` +Array [ + " + const pi = parseFloat(3.124); + + export { pi }; + ", + Object { + "format": "esm", + "loader": "js", + "sourceRoot": undefined, + "sourcefile": "foo.mjs", + "sourcemap": true, + "sourcesContent": true, + "target": "es2015", + }, +] +`; + +exports[`NgJestTransformer should use esbuild to process mjs or \`node_modules\` js files to ESM codes 6`] = ` +Array [ + " + const pi = parseFloat(3.124); + + export { pi }; + ", + Object { + "format": "esm", + "loader": "js", + "sourceRoot": undefined, + "sourcefile": "node_modules\\\\foo.js", "sourcemap": true, + "sourcesContent": true, "target": "es2015", }, ] diff --git a/src/__tests__/ng-jest-transformer.spec.ts b/src/__tests__/ng-jest-transformer.spec.ts index d71217f471..83ccf9094a 100644 --- a/src/__tests__/ng-jest-transformer.spec.ts +++ b/src/__tests__/ng-jest-transformer.spec.ts @@ -8,10 +8,7 @@ const tr = new NgJestTransformer(); jest.mock('esbuild', () => { return { - transformSync: jest.fn().mockReturnValue({ - code: 'bla bla', - map: JSON.stringify({ version: 1, sourceContent: 'foo foo' }), - }), + transformSync: jest.fn().mockImplementation(jest.requireActual('esbuild').transformSync), }; }); @@ -33,6 +30,60 @@ describe('NgJestTransformer', () => { expect(cs).toBeInstanceOf(NgJestConfig); }); + test('should not use esbuild to process js files which are not from `node_modules`', () => { + tr.process( + ` + const pi = parseFloat(3.124); + + export { pi }; + `, + 'foo.js', + { + config: { + cwd: process.cwd(), + extensionsToTreatAsEsm: [], + testMatch: [], + testRegex: [], + globals: { + 'ts-jest': { + isolatedModules: true, + }, + }, + }, + } as any, // eslint-disable-line @typescript-eslint/no-explicit-any, + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any, + expect(transformSync as unknown as jest.MockInstance).not.toHaveBeenCalled(); + }); + + test('should not use esbuild to process tslib file', () => { + tr.process( + ` + const pi = parseFloat(3.124); + + export { pi }; + `, + 'node_modules/tslib.es6.js', + { + config: { + cwd: process.cwd(), + extensionsToTreatAsEsm: [], + testMatch: [], + testRegex: [], + globals: { + 'ts-jest': { + isolatedModules: true, + }, + }, + }, + } as any, // eslint-disable-line @typescript-eslint/no-explicit-any, + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any, + expect(transformSync as unknown as jest.MockInstance).not.toHaveBeenCalled(); + }); + test.each([ { tsconfig: { @@ -47,36 +98,119 @@ describe('NgJestTransformer', () => { { tsconfig: {}, }, - ])('should process successfully a mjs file to CommonJS codes', ({ tsconfig }) => { - const result = tr.process( + ])('should use esbuild to process mjs or `node_modules` js files to CJS codes', ({ tsconfig }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const transformSyncMock = transformSync as unknown as jest.MockInstance; + const transformCfg = { + config: { + cwd: process.cwd(), + extensionsToTreatAsEsm: [], + testMatch: [], + testRegex: [], + globals: { + 'ts-jest': { + tsconfig, + }, + }, + }, + } as any; // eslint-disable-line @typescript-eslint/no-explicit-any + const mjsOutput = tr.process( ` const pi = parseFloat(3.124); export { pi }; `, 'foo.mjs', - { - config: { - cwd: process.cwd(), - extensionsToTreatAsEsm: [], - testMatch: [], - testRegex: [], - globals: { - 'ts-jest': { - tsconfig, - }, + transformCfg, + ); + const cjsOutput = tr.process( + ` + const pi = parseFloat(3.124); + + export { pi }; + `, + 'node_modules\\foo.js', + transformCfg, + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(transformSyncMock.mock.calls[0]).toMatchSnapshot(); + expect(transformSyncMock.mock.calls[1]).toMatchSnapshot(); + // @ts-expect-error `code` is a property of `TransformSource` + expect(mjsOutput.code).toBeDefined(); + // @ts-expect-error `code` is a property of `TransformSource` + expect(cjsOutput.code).toBeDefined(); + // @ts-expect-error `code` is a property of `TransformSource` + expect(mjsOutput.map).toEqual(expect.any(String)); + // @ts-expect-error `code` is a property of `TransformSource` + expect(cjsOutput.map).toEqual(expect.any(String)); + + transformSyncMock.mockClear(); + }); + + test.each([ + { + tsconfig: { + sourceMap: false, + }, + }, + { + tsconfig: { + target: 'es2016', + }, + }, + { + tsconfig: {}, + }, + ])('should use esbuild to process mjs or `node_modules` js files to ESM codes', ({ tsconfig }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const transformSyncMock = transformSync as unknown as jest.MockInstance; + const transformCfg = { + config: { + cwd: process.cwd(), + extensionsToTreatAsEsm: [], + testMatch: [], + testRegex: [], + globals: { + 'ts-jest': { + tsconfig, + useESM: true, }, }, - } as any, // eslint-disable-line @typescript-eslint/no-explicit-any + }, + supportsStaticESM: true, + } as any; // eslint-disable-line @typescript-eslint/no-explicit-any + const mjsOutput = tr.process( + ` + const pi = parseFloat(3.124); + + export { pi }; + `, + 'foo.mjs', + transformCfg, + ); + const cjsOutput = tr.process( + ` + const pi = parseFloat(3.124); + + export { pi }; + `, + 'node_modules\\foo.js', + transformCfg, ); // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((transformSync as unknown as jest.MockInstance).mock.calls[0]).toMatchSnapshot(); + expect(transformSyncMock.mock.calls[0]).toMatchSnapshot(); + expect(transformSyncMock.mock.calls[1]).toMatchSnapshot(); // @ts-expect-error `code` is a property of `TransformSource` - expect(result.code).toBeDefined(); + expect(mjsOutput.code).toBeDefined(); // @ts-expect-error `code` is a property of `TransformSource` - expect(result.map).toBeDefined(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (transformSync as unknown as jest.MockInstance).mockClear(); + expect(cjsOutput.code).toBeDefined(); + // @ts-expect-error `code` is a property of `TransformSource` + expect(mjsOutput.map).toEqual(expect.any(String)); + // @ts-expect-error `code` is a property of `TransformSource` + expect(cjsOutput.map).toEqual(expect.any(String)); + + transformSyncMock.mockClear(); }); }); diff --git a/src/ng-jest-transformer.ts b/src/ng-jest-transformer.ts index 60925e45e5..6da5917ee7 100644 --- a/src/ng-jest-transformer.ts +++ b/src/ng-jest-transformer.ts @@ -2,6 +2,7 @@ import path from 'path'; import type { TransformedSource } from '@jest/transform'; import type { Config } from '@jest/types'; +import { LogContexts, LogLevels, Logger, createLogger } from 'bs-logger'; import { transformSync } from 'esbuild'; import { ConfigSet } from 'ts-jest/dist/config/config-set'; import { TsJestTransformer } from 'ts-jest/dist/ts-jest-transformer'; @@ -11,6 +12,21 @@ import { NgJestCompiler } from './compiler/ng-jest-compiler'; import { NgJestConfig } from './config/ng-jest-config'; export class NgJestTransformer extends TsJestTransformer { + #ngJestLogger: Logger; + + constructor() { + super(); + this.#ngJestLogger = createLogger({ + context: { + [LogContexts.package]: 'ts-jest', + [LogContexts.logLevel]: LogLevels.trace, + // eslint-disable-next-line @typescript-eslint/no-var-requires + version: require('../package.json').version, + }, + targets: process.env.NG_JEST_LOG ?? undefined, + }); + } + protected _createConfigSet(config: ProjectConfigTsJest | undefined): ConfigSet { return new NgJestConfig(config); } @@ -26,15 +42,26 @@ export class NgJestTransformer extends TsJestTransformer { ): TransformedSource | string { const configSet = this._createConfigSet(transformOptions.config); /** - * TypeScript < 4.5 doesn't support compiling `.mjs` file by default when running `tsc` which throws error + * TypeScript < 4.5 doesn't support compiling `.mjs` file by default when running `tsc` which throws error. Also we + * transform `js` files from `node_modules` assuming that `node_modules` contains compiled files to speed up compilation. + * IMPORTANT: we exclude `tslib` from compilation because it has issue with compilation. The original `tslib.js` or + * `tslib.es6.js` works well with Jest without extra compilation */ - if (path.extname(filePath) === '.mjs') { - const { target, sourceMap } = configSet.parsedTsConfig.options; + if ( + path.extname(filePath) === '.mjs' || + (/node_modules\/(.*.js$)/.test(filePath.replace(/\\/g, '/')) && !filePath.includes('tslib')) + ) { + this.#ngJestLogger.debug({ filePath }, 'process with esbuild'); + + const compilerOpts = configSet.parsedTsConfig.options; const { code, map } = transformSync(fileContent, { loader: 'js', - format: 'cjs', - target: target === configSet.compilerModule.ScriptTarget.ES2015 ? 'es2015' : 'es2016', - sourcemap: sourceMap, + format: transformOptions.supportsStaticESM && configSet.useESM ? 'esm' : 'cjs', + target: compilerOpts.target === configSet.compilerModule.ScriptTarget.ES2015 ? 'es2015' : 'es2016', + sourcemap: compilerOpts.sourceMap, + sourcefile: filePath, + sourcesContent: true, + sourceRoot: compilerOpts.sourceRoot, }); return {