diff --git a/.eslintignore b/.eslintignore index b7c5a9d5f6..477fb2495a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,7 +1,8 @@ -build/ -node_modules/ +build +dist +node_modules src/config/setup-jest.ts -coverage/ +coverage website src/transformers/downlevel_decorators_transform src/ngtsc diff --git a/.gitignore b/.gitignore index 3805aaf259..7cab6d1bfa 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,5 @@ website/.yarn/* !.yarn/versions !/e2e/full-ivy-lib/node_modules !/e2e/partial-ivy-lib/node_modules -!/e2e/process-js-packages/node_modules src/transformers/downlevel_decorators_transform src/ngtsc diff --git a/e2e/__tests__/process-js-packages.test.ts b/e2e/__tests__/process-js-packages.test.ts index d0900770f9..c8a0ca03d8 100644 --- a/e2e/__tests__/process-js-packages.test.ts +++ b/e2e/__tests__/process-js-packages.test.ts @@ -1,27 +1,27 @@ -import fs from 'fs'; import path from 'path'; -import { jsonNoCache as runWithJsonNoCache } from '../run-jest'; +import { onNodeVersions, jsonNoCache as runWithJsonNoCache } from '../run-jest'; +import { runYarnInstall } from '../utils'; -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); +const DIR = 'process-js-packages'; -test(`successfully runs the tests inside ${TEST_DIR_NAME}`, () => { - process.env.NG_JEST_LOG = LOG_FILE_NAME; +beforeAll(() => { + runYarnInstall(path.join(__dirname, '..', DIR)); +}); - const { json } = runWithJsonNoCache(TEST_DIR_NAME); +test(`successfully run the tests inside ${DIR} with CommonJS mode`, () => { + const { json } = runWithJsonNoCache(DIR); 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'); +// The versions where vm.Module exists and commonjs with "exports" is not broken +onNodeVersions('>=12.16.0', () => { + test(`successfully run the tests inside ${DIR} with ESM mode`, () => { + const { json } = runWithJsonNoCache(DIR, ['-c=jest-esm.config.mjs'], { + nodeOptions: '--experimental-vm-modules --no-warnings', + }); - delete process.env.NG_JEST_LOG; - fs.unlinkSync(LOG_FILE_PATH); + expect(json.success).toBe(true); + }); }); diff --git a/e2e/process-js-packages/__tests__/process-js-packages.spec.ts b/e2e/process-js-packages/__tests__/process-js-packages.spec.ts index 0926f4edce..68d031c64a 100644 --- a/e2e/process-js-packages/__tests__/process-js-packages.spec.ts +++ b/e2e/process-js-packages/__tests__/process-js-packages.spec.ts @@ -1,5 +1,9 @@ -import { foo } from 'my-lib'; +import { MarkerClusterer } from '@googlemaps/markerclusterer'; +import camelCase from 'lodash-es/camelCase'; +import { __assign } from 'tslib'; test('should pass', () => { - expect(foo).toBe(1); + expect(typeof MarkerClusterer).toBe('function'); + expect(camelCase('foo-bar')).toBe('fooBar'); + expect(typeof __assign).toBe('function'); }); diff --git a/e2e/process-js-packages/jest-esm.config.mjs b/e2e/process-js-packages/jest-esm.config.mjs new file mode 100644 index 0000000000..1168bdaddf --- /dev/null +++ b/e2e/process-js-packages/jest-esm.config.mjs @@ -0,0 +1,17 @@ +export default { + extensionsToTreatAsEsm: ['.ts'], + globals: { + 'ts-jest': { + tsconfig: 'tsconfig-esm.spec.json', + useESM: true, + isolatedModules: true, + }, + }, + transform: { + '^.+\\.(ts|js)$': '/../../build/index.js', + }, + moduleNameMapper: { + '@googlemaps/markerclusterer': '@googlemaps/markerclusterer/dist/index.esm.js', + }, + transformIgnorePatterns: ['node_modules/(?!@googlemaps/markerclusterer)'], +}; diff --git a/e2e/process-js-packages/jest.config.js b/e2e/process-js-packages/jest.config.js new file mode 100644 index 0000000000..24d7ce695e --- /dev/null +++ b/e2e/process-js-packages/jest.config.js @@ -0,0 +1,15 @@ +module.exports = { + globals: { + 'ts-jest': { + tsconfig: 'tsconfig.spec.json', + isolatedModules: true, + }, + ngJest: { + processWithEsbuild: ['**/node_modules/lodash-es/*.js'], + }, + }, + transform: { + '^.+\\.(ts|js|mjs)$': '/../../build/index.js', + }, + transformIgnorePatterns: ['node_modules/(?!lodash-es)'], +}; 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 deleted file mode 100644 index 959296a1f7..0000000000 --- a/e2e/process-js-packages/node_modules/my-lib/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 95c5d282cb..0000000000 --- a/e2e/process-js-packages/node_modules/my-lib/index.js +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 0f733a9ef2..0000000000 --- a/e2e/process-js-packages/node_modules/my-lib/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "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 index 5991425e7e..404728a610 100644 --- a/e2e/process-js-packages/package.json +++ b/e2e/process-js-packages/package.json @@ -1,15 +1,10 @@ { "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)"] + "private": true, + "devDependencies": { + "lodash-es": "^4.17.21", + "@types/lodash-es": "^4.17.6", + "tslib": "^2.4.0", + "@googlemaps/markerclusterer": "^2.0.6" } } diff --git a/e2e/process-js-packages/tsconfig-esm.spec.json b/e2e/process-js-packages/tsconfig-esm.spec.json new file mode 100644 index 0000000000..e037a97ca5 --- /dev/null +++ b/e2e/process-js-packages/tsconfig-esm.spec.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "allowJs": true, + "esModuleInterop": true, + "module": "ESNext", + "skipLibCheck": true + }, + "includes": ["__tests__/*.spec.ts"] +} diff --git a/e2e/process-js-packages/tsconfig.spec.json b/e2e/process-js-packages/tsconfig.spec.json new file mode 100644 index 0000000000..eb7ee89b02 --- /dev/null +++ b/e2e/process-js-packages/tsconfig.spec.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "allowJs": true, + "module": "CommonJS", + "skipLibCheck": true + }, + "includes": ["__tests__/*.spec.ts"] +} diff --git a/e2e/process-js-packages/yarn.lock b/e2e/process-js-packages/yarn.lock new file mode 100644 index 0000000000..c20bb5027d --- /dev/null +++ b/e2e/process-js-packages/yarn.lock @@ -0,0 +1,50 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@googlemaps/markerclusterer@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@googlemaps/markerclusterer/-/markerclusterer-2.0.6.tgz#f3c157e24f5c95ab1748710c5a584ae1fae51d61" + integrity sha512-kO8Q77V3aqR2tVZ3SDXs9ycCiWYpd+FadxIJVtDKlO9LlMs415GS686+XvDLMLorR/RvwQHkquHZM8RbZ3bCrg== + dependencies: + fast-deep-equal "^3.1.3" + supercluster "^7.1.3" + +"@types/lodash-es@^4.17.6": + version "4.17.6" + resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.6.tgz#c2ed4c8320ffa6f11b43eb89e9eaeec65966a0a0" + integrity sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.182" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" + integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== + +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +kdbush@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-3.0.0.tgz#f8484794d47004cc2d85ed3a79353dbe0abc2bf0" + integrity sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew== + +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + +supercluster@^7.1.3: + version "7.1.5" + resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-7.1.5.tgz#65a6ce4a037a972767740614c19051b64b8be5a3" + integrity sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg== + dependencies: + kdbush "^3.0.0" + +tslib@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== diff --git a/e2e/run-jest.ts b/e2e/run-jest.ts index b21e3f632e..585769b22d 100644 --- a/e2e/run-jest.ts +++ b/e2e/run-jest.ts @@ -118,8 +118,8 @@ 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 jsonNoCache = (dir: string, jestArgs?: string[], options: RunJestOptions = {}): RunJestJsonResult => { + return json(dir, jestArgs ? [...jestArgs, '--no-cache'] : ['--no-cache'], options); }; export const onNodeVersions = (versionRange: string, testBody: () => void): void => { diff --git a/src/__snapshots__/ng-jest-transformer.spec.ts.snap b/src/__snapshots__/ng-jest-transformer.spec.ts.snap index 7a6faae06a..14880ea162 100644 --- a/src/__snapshots__/ng-jest-transformer.spec.ts.snap +++ b/src/__snapshots__/ng-jest-transformer.spec.ts.snap @@ -30,7 +30,7 @@ Array [ "format": "cjs", "loader": "js", "sourceRoot": undefined, - "sourcefile": "node_modules\\\\foo.js", + "sourcefile": "node_modules/foo.js", "sourcemap": false, "sourcesContent": true, "target": "es2015", @@ -68,7 +68,7 @@ Array [ "format": "cjs", "loader": "js", "sourceRoot": undefined, - "sourcefile": "node_modules\\\\foo.js", + "sourcefile": "node_modules/foo.js", "sourcemap": true, "sourcesContent": true, "target": "es2016", @@ -106,7 +106,7 @@ Array [ "format": "cjs", "loader": "js", "sourceRoot": undefined, - "sourcefile": "node_modules\\\\foo.js", + "sourcefile": "node_modules/foo.js", "sourcemap": true, "sourcesContent": true, "target": "es2015", @@ -144,7 +144,7 @@ Array [ "format": "esm", "loader": "js", "sourceRoot": undefined, - "sourcefile": "node_modules\\\\foo.js", + "sourcefile": "node_modules/foo.js", "sourcemap": false, "sourcesContent": true, "target": "es2015", @@ -182,7 +182,7 @@ Array [ "format": "esm", "loader": "js", "sourceRoot": undefined, - "sourcefile": "node_modules\\\\foo.js", + "sourcefile": "node_modules/foo.js", "sourcemap": true, "sourcesContent": true, "target": "es2016", @@ -220,7 +220,7 @@ Array [ "format": "esm", "loader": "js", "sourceRoot": undefined, - "sourcefile": "node_modules\\\\foo.js", + "sourcefile": "node_modules/foo.js", "sourcemap": true, "sourcesContent": true, "target": "es2015", diff --git a/src/config/ng-jest-config.ts b/src/config/ng-jest-config.ts index 6c048e29fd..7e76dfef9f 100644 --- a/src/config/ng-jest-config.ts +++ b/src/config/ng-jest-config.ts @@ -1,8 +1,26 @@ +import type { Logger } from 'bs-logger'; +import { globsToMatcher } from 'jest-util'; +import type { ProjectConfigTsJest, RawCompilerOptions } from 'ts-jest'; import { ConfigSet } from 'ts-jest/dist/legacy/config/config-set'; -import type { RawCompilerOptions } from 'ts-jest/dist/raw-compiler-options'; import type { ParsedCommandLine } from 'typescript'; +/** + * Some NPM packages like `tslib` is distributed in such a way that `esbuild` cannot process it, so we fall back to use TypeScript API + */ +const defaultProcessWithEsbuildPatterns = ['**/*.mjs']; + export class NgJestConfig extends ConfigSet { + readonly processWithEsbuild: ReturnType; + + constructor(jestConfig: ProjectConfigTsJest | undefined, parentLogger?: Logger | undefined) { + super(jestConfig, parentLogger); + const jestGlobalsConfig = jestConfig?.globals?.ngJest ?? Object.create(null); + this.processWithEsbuild = globsToMatcher([ + ...(jestGlobalsConfig.processWithEsbuild ?? []), + ...defaultProcessWithEsbuildPatterns, + ]); + } + /** * Override `ts-jest` behavior because we use `readConfiguration` which will read and resolve tsconfig. */ diff --git a/src/ng-jest-transformer.spec.ts b/src/ng-jest-transformer.spec.ts index 0c089a8da4..676595210b 100644 --- a/src/ng-jest-transformer.spec.ts +++ b/src/ng-jest-transformer.spec.ts @@ -115,6 +115,9 @@ describe('NgJestTransformer', () => { 'ts-jest': { tsconfig, }, + ngJest: { + processWithEsbuild: ['node_modules/foo.js'], + }, }, }, } as any; // eslint-disable-line @typescript-eslint/no-explicit-any @@ -133,7 +136,7 @@ describe('NgJestTransformer', () => { export { pi }; `, - 'node_modules\\foo.js', + 'node_modules/foo.js', transformCfg, ); @@ -173,6 +176,9 @@ describe('NgJestTransformer', () => { tsconfig, useESM: true, }, + ngJest: { + processWithEsbuild: ['node_modules/foo.js'], + }, }, }, supportsStaticESM: true, @@ -192,7 +198,7 @@ describe('NgJestTransformer', () => { export { pi }; `, - 'node_modules\\foo.js', + 'node_modules/foo.js', transformCfg, ); diff --git a/src/ng-jest-transformer.ts b/src/ng-jest-transformer.ts index 5391a413bf..96ed5155c8 100644 --- a/src/ng-jest-transformer.ts +++ b/src/ng-jest-transformer.ts @@ -1,5 +1,4 @@ import { spawnSync } from 'child_process'; -import path from 'path'; import type { TransformedSource } from '@jest/transform'; import { LogContexts, LogLevels, type Logger, createLogger } from 'bs-logger'; @@ -55,16 +54,7 @@ export class NgJestTransformer extends TsJestTransformer { process(fileContent: string, filePath: string, transformOptions: TransformOptionsTsJest): TransformedSource { // @ts-expect-error we are accessing the private cache to avoid creating new objects all the time const configSet = super._configsFor(transformOptions); - /** - * 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' || - (/node_modules\/(.*.js$)/.test(filePath.replace(/\\/g, '/')) && !filePath.includes('tslib')) - ) { + if (configSet.processWithEsbuild(filePath)) { this.#ngJestLogger.debug({ filePath }, 'process with esbuild'); const compilerOpts = configSet.parsedTsConfig.options;