From d6015613f5296eec650be280969d3c6861857252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Thu, 23 Jan 2025 17:59:16 +0100 Subject: [PATCH] feat(misc): use `@swc/jest` instead of `ts-jest` for the ts solution setup (#29718) ## Current Behavior When using the TS solution setup and `jest` is used, `ts-jest` is used as the transformer in most cases (except when the build compiler is `swc`). The `ts-jest` transformer doesn't support modern module resolutions like `nodenext` and it doesn't support TS project references either. ## Expected Behavior When using the TS solution setup and `jest` is used, `@swc/jest` should be used as the transformer in cases where previously `ts-jest` was being used and regardless of using `swc` as the build compiler. ## Related Issue(s) Fixes # --- .../packages/jest/documents/overview.md | 31 +++ .../packages/node/generators/application.json | 3 +- docs/shared/packages/jest/jest-plugin.md | 31 +++ .../workspace-rules-project.spec.ts | 92 +++++++- .../workspace-rules-project.ts | 3 +- .../configuration/configuration.spec.ts | 170 ++++++++++++++ .../generators/configuration/configuration.ts | 13 ++ .../{ => common}/src/test-setup.ts__tmpl__ | 0 .../{ => common}/tsconfig.spec.json__tmpl__ | 0 .../jest.config.ts__tmpl__ | 0 .../jest.config.ts__tmpl__ | 28 +++ .../configuration/lib/create-files.ts | 70 ++++-- .../ts-solution/tsconfig.lib.json__tmpl__ | 2 +- .../js/src/generators/library/library.spec.ts | 209 ++++++++++++++++-- packages/js/src/generators/library/library.ts | 61 ++--- .../js/src/generators/library/schema.d.ts | 1 + packages/js/src/utils/swc/add-swc-config.ts | 43 +++- .../src/generators/library/library.spec.ts | 6 +- .../application/application.spec.ts | 59 +++++ .../src/generators/application/application.ts | 6 +- .../src/generators/application/schema.json | 3 +- .../src/generators/library/library.spec.ts | 6 +- .../src/generators/e2e-project/e2e.spec.ts | 121 ++++++++++ .../plugin/src/generators/e2e-project/e2e.ts | 1 + .../src/generators/plugin/plugin.spec.ts | 113 +++++++++- .../application/lib/normalize-options.ts | 4 +- .../src/generators/vitest/vitest-generator.ts | 21 +- .../vite/src/generators/vitest/vitest.spec.ts | 91 ++++++++ 28 files changed, 1101 insertions(+), 87 deletions(-) rename packages/jest/src/generators/configuration/files/{ => common}/src/test-setup.ts__tmpl__ (100%) rename packages/jest/src/generators/configuration/files/{ => common}/tsconfig.spec.json__tmpl__ (100%) rename packages/jest/src/generators/configuration/files/{ => jest-config-non-ts-solution}/jest.config.ts__tmpl__ (100%) create mode 100644 packages/jest/src/generators/configuration/files/jest-config-ts-solution/jest.config.ts__tmpl__ diff --git a/docs/generated/packages/jest/documents/overview.md b/docs/generated/packages/jest/documents/overview.md index 93bf998c4438b..60349085f9e0b 100644 --- a/docs/generated/packages/jest/documents/overview.md +++ b/docs/generated/packages/jest/documents/overview.md @@ -316,6 +316,9 @@ export default async function () { If you're using `@swc/jest` and a global setup/teardown file, you have to set the `noInterop: false` and use dynamic imports within the setup function: +{% tabs %} +{% tab label="Using the config from .swcrc" %} + ```typescript {% fileName="apps//jest.config.ts" %} /* eslint-disable */ import { readFileSync } from 'fs'; @@ -344,6 +347,34 @@ export default { }; ``` +{% /tab %} + +{% tab label="Using the config from .spec.swcrc" %} + +```typescript {% fileName="apps//jest.config.ts" %} +/* eslint-disable */ +import { readFileSync } from 'fs'; + +// Reading the SWC compilation config for the spec files +const swcJestConfig = JSON.parse( + readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8') +); + +// Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves +swcJestConfig.swcrc = false; + +export default { + globalSetup: '/src/global-setup-swc.ts', + transform: { + '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], + }, + // other settings +}; +``` + +{% /tab %} +{% /tabs %} + ```typescript {% fileName="global-setup-swc.ts" %} import { registerTsProject } from '@nx/js/src/internal'; const cleanupRegisteredPaths = registerTsProject('./tsconfig.base.json'); diff --git a/docs/generated/packages/node/generators/application.json b/docs/generated/packages/node/generators/application.json index 6aa298ea4903d..8adcfa844b274 100644 --- a/docs/generated/packages/node/generators/application.json +++ b/docs/generated/packages/node/generators/application.json @@ -69,8 +69,7 @@ }, "swcJest": { "type": "boolean", - "description": "Use `@swc/jest` instead `ts-jest` for faster test compilation.", - "default": false + "description": "Use `@swc/jest` instead `ts-jest` for faster test compilation." }, "babelJest": { "type": "boolean", diff --git a/docs/shared/packages/jest/jest-plugin.md b/docs/shared/packages/jest/jest-plugin.md index 93bf998c4438b..60349085f9e0b 100644 --- a/docs/shared/packages/jest/jest-plugin.md +++ b/docs/shared/packages/jest/jest-plugin.md @@ -316,6 +316,9 @@ export default async function () { If you're using `@swc/jest` and a global setup/teardown file, you have to set the `noInterop: false` and use dynamic imports within the setup function: +{% tabs %} +{% tab label="Using the config from .swcrc" %} + ```typescript {% fileName="apps//jest.config.ts" %} /* eslint-disable */ import { readFileSync } from 'fs'; @@ -344,6 +347,34 @@ export default { }; ``` +{% /tab %} + +{% tab label="Using the config from .spec.swcrc" %} + +```typescript {% fileName="apps//jest.config.ts" %} +/* eslint-disable */ +import { readFileSync } from 'fs'; + +// Reading the SWC compilation config for the spec files +const swcJestConfig = JSON.parse( + readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8') +); + +// Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves +swcJestConfig.swcrc = false; + +export default { + globalSetup: '/src/global-setup-swc.ts', + transform: { + '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], + }, + // other settings +}; +``` + +{% /tab %} +{% /tabs %} + ```typescript {% fileName="global-setup-swc.ts" %} import { registerTsProject } from '@nx/js/src/internal'; const cleanupRegisteredPaths = registerTsProject('./tsconfig.base.json'); diff --git a/packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.spec.ts b/packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.spec.ts index 0a0482e781e15..a1d7675b28634 100644 --- a/packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.spec.ts +++ b/packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.spec.ts @@ -6,6 +6,7 @@ import { readJson, Tree, updateJson, + writeJson, } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { @@ -70,10 +71,24 @@ describe('@nx/eslint:workspace-rules-project', () => { expect(tsConfig.extends).toBe('../../tsconfig.json'); }); - it('should create a project with a test target', async () => { + it('should create the jest config using ts-jest', async () => { await lintWorkspaceRulesProjectGenerator(tree); expect(tree.exists('tools/eslint-rules/jest.config.ts')).toBeTruthy(); + expect(tree.read('tools/eslint-rules/jest.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "export default { + displayName: 'eslint-rules', + preset: '../../jest.preset.js', + transform: { + '^.+\\\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/tools/eslint-rules', + }; + " + `); + expect(tree.exists('tools/eslint-rules/.spec.swcrc')).toBeFalsy(); }); it('should not update the required files if the project already exists', async () => { @@ -104,4 +119,79 @@ describe('@nx/eslint:workspace-rules-project', () => { customTsconfigContents ); }); + + describe('TS solution setup', () => { + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + updateJson(tree, 'package.json', (json) => { + json.workspaces = ['packages/*']; + return json; + }); + writeJson(tree, 'tsconfig.base.json', { + compilerOptions: { composite: true }, + }); + writeJson(tree, 'tsconfig.json', { + extends: './tsconfig.base.json', + files: [], + references: [], + }); + }); + + it('should create the jest config using @swc/jest', async () => { + await lintWorkspaceRulesProjectGenerator(tree); + + expect(tree.exists('tools/eslint-rules/jest.config.ts')).toBeTruthy(); + expect(tree.read('tools/eslint-rules/jest.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "/* eslint-disable */ + import { readFileSync } from 'fs'; + + // Reading the SWC compilation config for the spec files + const swcJestConfig = JSON.parse( + readFileSync(\`\${__dirname}/.spec.swcrc\`, 'utf-8') + ); + + // Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves + swcJestConfig.swcrc = false; + + export default { + displayName: 'eslint-rules', + preset: '../../jest.preset.js', + transform: { + '^.+\\\\.[tj]s$': ['@swc/jest', swcJestConfig], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: 'test-output/jest/coverage', + }; + " + `); + expect(tree.exists('tools/eslint-rules/.spec.swcrc')).toBeTruthy(); + expect(tree.read('tools/eslint-rules/.spec.swcrc', 'utf-8')) + .toMatchInlineSnapshot(` + "{ + "jsc": { + "target": "es2017", + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "decoratorMetadata": true, + "legacyDecorator": true + }, + "keepClassNames": true, + "externalHelpers": true, + "loose": true + }, + "module": { + "type": "es6" + }, + "sourceMaps": true, + "exclude": [] + } + " + `); + }); + }); }); diff --git a/packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.ts b/packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.ts index c1f85c66d3e1a..bf0a18510f48a 100644 --- a/packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.ts +++ b/packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.ts @@ -15,6 +15,7 @@ import { } from '@nx/devkit'; import { getRelativePathToRootTsConfig } from '@nx/js'; import { addSwcRegisterDependencies } from '@nx/js/src/utils/swc/add-swc-dependencies'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { join } from 'path'; import { nxVersion, typescriptESLintVersion } from '../../utils/versions'; import { workspaceLintPluginDir } from '../../utils/workspace-lint-rules'; @@ -80,7 +81,7 @@ export async function lintWorkspaceRulesProjectGenerator( supportTsx: false, skipSerializers: true, setupFile: 'none', - compiler: 'tsc', + compiler: isUsingTsSolutionSetup(tree) ? 'swc' : 'tsc', skipFormat: true, }) ); diff --git a/packages/jest/src/generators/configuration/configuration.spec.ts b/packages/jest/src/generators/configuration/configuration.spec.ts index 449cbc7d42dd7..d32f09ee14141 100644 --- a/packages/jest/src/generators/configuration/configuration.spec.ts +++ b/packages/jest/src/generators/configuration/configuration.spec.ts @@ -8,6 +8,7 @@ import { updateProjectConfiguration, writeJson, updateJson, + readNxJson, } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { jestConfigObject } from '../../utils/config/functions'; @@ -337,7 +338,10 @@ describe('jestProject', () => { compiler: 'swc', supportTsx: true, } as JestProjectSchema); + expect(tree.read('libs/lib1/jest.config.ts', 'utf-8')).toMatchSnapshot(); + // assert the TS solution setup doesn't leak into the old/integrated setup + expect(tree.exists('libs/lib1/.spec.swcrc')).toBeFalsy(); }); }); @@ -459,4 +463,170 @@ describe('jestProject', () => { `); }); }); + + describe('TS solution setup', () => { + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + updateJson(tree, 'package.json', (json) => { + json.workspaces = ['packages/*']; + return json; + }); + writeJson(tree, 'tsconfig.base.json', { + compilerOptions: { composite: true }, + }); + writeJson(tree, 'tsconfig.json', { + extends: './tsconfig.base.json', + files: [], + references: [], + }); + + addProjectConfiguration(tree, 'pkg1', { + root: 'packages/pkg1', + sourceRoot: 'packages/pkg1/src', + targets: { + lint: { + executor: '@nx/eslint:lint', + options: {}, + }, + }, + }); + writeJson(tree, 'packages/pkg1/tsconfig.json', { + files: [], + include: [], + references: [], + }); + }); + + it('should generate files', async () => { + await configurationGenerator(tree, { + ...defaultOptions, + project: 'pkg1', + }); + + expect(tree.exists('packages/pkg1/tsconfig.spec.json')).toBeTruthy(); + expect(tree.exists('packages/pkg1/jest.config.ts')).toBeTruthy(); + expect(tree.read('packages/pkg1/jest.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "export default { + displayName: 'pkg1', + preset: '../../jest.preset.js', + coverageDirectory: 'test-output/jest/coverage', + }; + " + `); + expect(tree.exists('packages/pkg1/.spec.swcrc')).toBeFalsy(); + }); + + it(`should setup a task pipeline for the test target to depend on the deps' build target`, async () => { + await configurationGenerator(tree, { + ...defaultOptions, + project: 'pkg1', + }); + + const nxJson = readNxJson(tree); + expect(nxJson.targetDefaults.test.dependsOn).toStrictEqual(['^build']); + }); + + it('should generate files with swc compiler', async () => { + await configurationGenerator(tree, { + ...defaultOptions, + project: 'pkg1', + compiler: 'swc', + }); + + expect(tree.exists('packages/pkg1/tsconfig.spec.json')).toBeTruthy(); + expect(tree.exists('packages/pkg1/jest.config.ts')).toBeTruthy(); + expect(tree.read('packages/pkg1/jest.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "/* eslint-disable */ + import { readFileSync } from 'fs'; + + // Reading the SWC compilation config for the spec files + const swcJestConfig = JSON.parse( + readFileSync(\`\${__dirname}/.spec.swcrc\`, 'utf-8') + ); + + // Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves + swcJestConfig.swcrc = false; + + export default { + displayName: 'pkg1', + preset: '../../jest.preset.js', + transform: { + '^.+\\\\.[tj]s$': ['@swc/jest', swcJestConfig], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: 'test-output/jest/coverage', + }; + " + `); + expect(tree.exists('packages/pkg1/.spec.swcrc')).toBeTruthy(); + expect(tree.read('packages/pkg1/.spec.swcrc', 'utf-8')) + .toMatchInlineSnapshot(` + "{ + "jsc": { + "target": "es2017", + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "decoratorMetadata": true, + "legacyDecorator": true + }, + "keepClassNames": true, + "externalHelpers": true, + "loose": true + }, + "module": { + "type": "es6" + }, + "sourceMaps": true, + "exclude": [] + } + " + `); + }); + + it('should generate the correct options for swc when "supportTsx: true"', async () => { + await configurationGenerator(tree, { + ...defaultOptions, + project: 'pkg1', + compiler: 'swc', + supportTsx: true, + }); + + expect(tree.read('packages/pkg1/.spec.swcrc', 'utf-8')) + .toMatchInlineSnapshot(` + "{ + "jsc": { + "target": "es2017", + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true, + "tsx": true + }, + "transform": { + "decoratorMetadata": true, + "legacyDecorator": true, + "react": { + "runtime": "automatic" + } + }, + "keepClassNames": true, + "externalHelpers": true, + "loose": true + }, + "module": { + "type": "es6" + }, + "sourceMaps": true, + "exclude": [] + } + " + `); + }); + }); }); diff --git a/packages/jest/src/generators/configuration/configuration.ts b/packages/jest/src/generators/configuration/configuration.ts index a294ec79c90d5..30a4968ff1bc5 100644 --- a/packages/jest/src/generators/configuration/configuration.ts +++ b/packages/jest/src/generators/configuration/configuration.ts @@ -6,6 +6,7 @@ import { readProjectConfiguration, runTasksInSerial, Tree, + updateNxJson, } from '@nx/devkit'; import { initGenerator as jsInitGenerator } from '@nx/js'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; @@ -120,6 +121,18 @@ export async function configurationGeneratorInternal( if (options.isTsSolutionSetup) { ignoreTestOutput(tree); + + // in the TS solution setup, the test target depends on the build outputs + // so we need to setup the task pipeline accordingly + const nxJson = readNxJson(tree); + nxJson.targetDefaults ??= {}; + nxJson.targetDefaults[options.targetName] ??= {}; + nxJson.targetDefaults[options.targetName].dependsOn ??= []; + nxJson.targetDefaults[options.targetName].dependsOn.push('^build'); + nxJson.targetDefaults[options.targetName].dependsOn = Array.from( + new Set(nxJson.targetDefaults[options.targetName].dependsOn) + ); + updateNxJson(tree, nxJson); } if (!schema.skipFormat) { diff --git a/packages/jest/src/generators/configuration/files/src/test-setup.ts__tmpl__ b/packages/jest/src/generators/configuration/files/common/src/test-setup.ts__tmpl__ similarity index 100% rename from packages/jest/src/generators/configuration/files/src/test-setup.ts__tmpl__ rename to packages/jest/src/generators/configuration/files/common/src/test-setup.ts__tmpl__ diff --git a/packages/jest/src/generators/configuration/files/tsconfig.spec.json__tmpl__ b/packages/jest/src/generators/configuration/files/common/tsconfig.spec.json__tmpl__ similarity index 100% rename from packages/jest/src/generators/configuration/files/tsconfig.spec.json__tmpl__ rename to packages/jest/src/generators/configuration/files/common/tsconfig.spec.json__tmpl__ diff --git a/packages/jest/src/generators/configuration/files/jest.config.ts__tmpl__ b/packages/jest/src/generators/configuration/files/jest-config-non-ts-solution/jest.config.ts__tmpl__ similarity index 100% rename from packages/jest/src/generators/configuration/files/jest.config.ts__tmpl__ rename to packages/jest/src/generators/configuration/files/jest-config-non-ts-solution/jest.config.ts__tmpl__ diff --git a/packages/jest/src/generators/configuration/files/jest-config-ts-solution/jest.config.ts__tmpl__ b/packages/jest/src/generators/configuration/files/jest-config-ts-solution/jest.config.ts__tmpl__ new file mode 100644 index 0000000000000..3b45a1fa34737 --- /dev/null +++ b/packages/jest/src/generators/configuration/files/jest-config-ts-solution/jest.config.ts__tmpl__ @@ -0,0 +1,28 @@ +<%_ if (transformer === '@swc/jest') { _%> +/* eslint-disable */ +<% if(js) {%>const { readFileSync } = require('fs')<% } else { %>import { readFileSync } from 'fs';<% } %> + +// Reading the SWC compilation config for the spec files +const swcJestConfig = JSON.parse( + readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8') +); + +// Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves +swcJestConfig.swcrc = false; + +<%_ } _%> +<% if(js){ %>module.exports =<% } else{ %>export default<% } %> { + displayName: '<%= project %>', + preset: '<%= offsetFromRoot %>jest.preset.<%= presetExt %>',<% if(setupFile !== 'none') { %> + setupFilesAfterEnv: ['/src/test-setup.ts'],<% } %><% if(testEnvironment) { %> + testEnvironment: '<%= testEnvironment %>',<% } %><% if(skipSerializers){ %> + transform: { + <% if (supportTsx){ %>'^.+\\.[tj]sx?$'<% } else { %>'^.+\\.[tj]s$'<% } %>: <% if (transformerOptions) { %>['<%= transformer %>', <%- transformerOptions %>]<% } else { %>'<%= transformer %>'<% } %> + }, + <% if (supportTsx) { %>moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],<% } else { %>moduleFileExtensions: ['ts', 'js', 'html'],<% } %><% } %> + coverageDirectory: '<%= coverageDirectory %>'<% if(rootProject){ %>, + testMatch: [ + '/src/**/__tests__/**/*.[jt]s?(x)', + '/src/**/*(*.)@(spec|test).[jt]s?(x)', + ],<% } %> +}; diff --git a/packages/jest/src/generators/configuration/lib/create-files.ts b/packages/jest/src/generators/configuration/lib/create-files.ts index 1bdf076da8e2c..6f79044810e58 100644 --- a/packages/jest/src/generators/configuration/lib/create-files.ts +++ b/packages/jest/src/generators/configuration/lib/create-files.ts @@ -4,10 +4,10 @@ import { readProjectConfiguration, Tree, } from '@nx/devkit'; +import { addSwcTestConfig } from '@nx/js/src/utils/swc/add-swc-config'; import { join } from 'path'; import type { JestPresetExtension } from '../../../utils/config/config-file'; import { NormalizedJestProjectSchema } from '../schema'; -import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; export function createFiles( tree: Tree, @@ -16,8 +16,8 @@ export function createFiles( ) { const projectConfig = readProjectConfiguration(tree, options.project); - const filesFolder = - options.setupFile === 'angular' ? '../files-angular' : '../files'; + const commonFilesFolder = + options.setupFile === 'angular' ? '../files-angular' : '../files/common'; let transformer: string; let transformerOptions: string | null = null; @@ -25,7 +25,9 @@ export function createFiles( transformer = 'babel-jest'; } else if (options.compiler === 'swc') { transformer = '@swc/jest'; - if (options.supportTsx) { + if (options.isTsSolutionSetup) { + transformerOptions = 'swcJestConfig'; + } else if (options.supportTsx) { transformerOptions = "{ jsc: { parser: { syntax: 'typescript', tsx: true }, transform: { react: { runtime: 'automatic' } } } }"; } @@ -34,20 +36,27 @@ export function createFiles( transformerOptions = "{ tsconfig: '/tsconfig.spec.json' }"; } - const isTsSolutionSetup = isUsingTsSolutionSetup(tree); + if (options.compiler === 'swc' && options.isTsSolutionSetup) { + addSwcTestConfig(tree, projectConfig.root, 'es6', options.supportTsx); + } const projectRoot = options.rootProject ? options.project : projectConfig.root; const rootOffset = offsetFromRoot(projectConfig.root); - generateFiles(tree, join(__dirname, filesFolder), projectConfig.root, { + // jsdom is the default in the nx preset + const testEnvironment = + options.testEnvironment === 'none' || options.testEnvironment === 'jsdom' + ? '' + : options.testEnvironment; + const coverageDirectory = options.isTsSolutionSetup + ? `test-output/jest/coverage` + : `${rootOffset}coverage/${projectRoot}`; + + generateFiles(tree, join(__dirname, commonFilesFolder), projectConfig.root, { tmpl: '', ...options, - // jsdom is the default - testEnvironment: - options.testEnvironment === 'none' || options.testEnvironment === 'jsdom' - ? '' - : options.testEnvironment, + testEnvironment, transformer, transformerOptions, js: !!options.js, @@ -55,17 +64,44 @@ export function createFiles( projectRoot, offsetFromRoot: rootOffset, presetExt, - coverageDirectory: isTsSolutionSetup - ? `test-output/jest/coverage` - : `${rootOffset}coverage/${projectRoot}`, - extendedConfig: isTsSolutionSetup + coverageDirectory, + extendedConfig: options.isTsSolutionSetup ? `${rootOffset}tsconfig.base.json` : './tsconfig.json', - outDir: isTsSolutionSetup ? `./out-tsc/jest` : `${rootOffset}dist/out-tsc`, + outDir: options.isTsSolutionSetup + ? `./out-tsc/jest` + : `${rootOffset}dist/out-tsc`, module: - !isTsSolutionSetup || transformer === 'ts-jest' ? 'commonjs' : undefined, + !options.isTsSolutionSetup || transformer === 'ts-jest' + ? 'commonjs' + : undefined, }); + if (options.setupFile !== 'angular') { + generateFiles( + tree, + join( + __dirname, + options.isTsSolutionSetup + ? '../files/jest-config-ts-solution' + : '../files/jest-config-non-ts-solution' + ), + projectConfig.root, + { + tmpl: '', + ...options, + testEnvironment, + transformer, + transformerOptions, + js: !!options.js, + rootProject: options.rootProject, + offsetFromRoot: rootOffset, + presetExt, + coverageDirectory, + } + ); + } + if (options.setupFile === 'none') { tree.delete(join(projectConfig.root, './src/test-setup.ts')); } diff --git a/packages/js/src/generators/library/files/tsconfig-lib/ts-solution/tsconfig.lib.json__tmpl__ b/packages/js/src/generators/library/files/tsconfig-lib/ts-solution/tsconfig.lib.json__tmpl__ index 453d7c021d931..18f1667695467 100644 --- a/packages/js/src/generators/library/files/tsconfig-lib/ts-solution/tsconfig.lib.json__tmpl__ +++ b/packages/js/src/generators/library/files/tsconfig-lib/ts-solution/tsconfig.lib.json__tmpl__ @@ -5,7 +5,7 @@ "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", - "emitDeclarationOnly": false,<% if (compilerOptions.length) { %> + "emitDeclarationOnly": <%= emitDeclarationOnly %>,<% if (compilerOptions.length) { %> <%- compilerOptions %>,<% } %> "types": ["node"] }, diff --git a/packages/js/src/generators/library/library.spec.ts b/packages/js/src/generators/library/library.spec.ts index 54e1d72ffd774..a24937455ac15 100644 --- a/packages/js/src/generators/library/library.spec.ts +++ b/packages/js/src/generators/library/library.spec.ts @@ -34,6 +34,39 @@ describe('lib', () => { tree.write('/.gitignore', ''); }); + it.each` + bundler + ${'esbuild'} + ${'none'} + ${'rollup'} + ${'swc'} + ${'tsc'} + ${'vite'} + `( + 'should generate tsconfig.lib.json with integrated setup when bundler=$bundler', + async ({ bundler }) => { + await libraryGenerator(tree, { + ...defaultOptions, + directory: 'my-lib', + bundler, + unitTestRunner: 'none', + linter: 'none', + }); + + const { compilerOptions } = readJson(tree, 'my-lib/tsconfig.lib.json'); + expect(compilerOptions).toEqual( + expect.objectContaining({ + outDir: '../dist/out-tsc', + declaration: true, + }) + ); + // check a few options set in the TS solution setup are not set + expect(compilerOptions.rootDir).not.toBeDefined(); + expect(compilerOptions.tsBuildInfoFile).not.toBeDefined(); + expect(compilerOptions.emitDeclarationOnly).not.toBeDefined(); + } + ); + describe('configs', () => { it('should generate an empty ts lib using --config=npm-scripts', async () => { await libraryGenerator(tree, { @@ -782,34 +815,100 @@ describe('lib', () => { }); describe('--unit-test-runner jest', () => { - it('should generate test configuration', async () => { - await libraryGenerator(tree, { - ...defaultOptions, - directory: 'my-lib', - unitTestRunner: 'jest', - }); + it.each` + bundler + ${'esbuild'} + ${'none'} + ${'tsc'} + ${'vite'} + `( + 'should generate test config with ts-jest when bundler=$bundler', + async ({ bundler }) => { + await libraryGenerator(tree, { + ...defaultOptions, + directory: 'my-lib', + unitTestRunner: 'jest', + bundler, + }); - expect(tree.exists('my-lib/tsconfig.spec.json')).toBeTruthy(); - expect(tree.exists('my-lib/jest.config.ts')).toBeTruthy(); - expect(tree.exists('my-lib/src/lib/my-lib.spec.ts')).toBeTruthy(); + expect(tree.exists('my-lib/tsconfig.spec.json')).toBeTruthy(); + expect(tree.exists('my-lib/src/lib/my-lib.spec.ts')).toBeTruthy(); + expect(tree.exists('my-lib/jest.config.ts')).toBeTruthy(); + expect(tree.read('my-lib/jest.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "export default { + displayName: 'my-lib', + preset: '../jest.preset.js', + transform: { + '^.+\\\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../coverage/my-lib', + }; + " + `); + const readme = tree.read('my-lib/README.md', 'utf-8'); + expect(readme).toContain('nx test my-lib'); + } + ); - expect(tree.exists(`my-lib/jest.config.ts`)).toBeTruthy(); - expect(tree.read(`my-lib/jest.config.ts`, 'utf-8')) - .toMatchInlineSnapshot(` - "export default { + it.each` + bundler + ${'swc'} + ${'rollup'} + `( + 'should generate test config with @swc/jest when bundler=$bundler', + async ({ bundler }) => { + await libraryGenerator(tree, { + ...defaultOptions, + directory: 'my-lib', + unitTestRunner: 'jest', + bundler, + }); + + expect(tree.exists('my-lib/tsconfig.spec.json')).toBeTruthy(); + expect(tree.exists('my-lib/src/lib/my-lib.spec.ts')).toBeTruthy(); + expect(tree.exists('my-lib/jest.config.ts')).toBeTruthy(); + expect(tree.read('my-lib/jest.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "/* eslint-disable */ + import { readFileSync } from 'fs'; + + // Reading the SWC compilation config and remove the "exclude" + // for the test files to be compiled by SWC + const { exclude: _, ...swcJestConfig } = JSON.parse( + readFileSync(\`\${__dirname}/.swcrc\`, 'utf-8') + ); + + // disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves. + // If we do not disable this, SWC Core will read .swcrc and won't transform our test files due to "exclude" + if (swcJestConfig.swcrc === undefined) { + swcJestConfig.swcrc = false; + } + + // Uncomment if using global setup/teardown files being transformed via swc + // https://nx.dev/nx-api/jest/documents/overview#global-setupteardown-with-nx-libraries + // jest needs EsModule Interop to find the default exported setup/teardown functions + // swcJestConfig.module.noInterop = false; + + export default { displayName: 'my-lib', preset: '../jest.preset.js', transform: { - '^.+\\\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + '^.+\\\\.[tj]s$': ['@swc/jest', swcJestConfig], }, moduleFileExtensions: ['ts', 'js', 'html'], + testEnvironment: 'jsdom', coverageDirectory: '../coverage/my-lib', }; " `); - const readme = tree.read('my-lib/README.md', 'utf-8'); - expect(readme).toContain('nx test my-lib'); - }); + const readme = tree.read('my-lib/README.md', 'utf-8'); + expect(readme).toContain('nx test my-lib'); + // assert the TS solution setup doesn't leak into the old/integrated setup + expect(tree.exists('my-lib/.spec.swcrc')).toBeFalsy(); + } + ); it('should generate test configuration with swc and js', async () => { await libraryGenerator(tree, { @@ -2011,5 +2110,81 @@ describe('lib', () => { } `); }); + + it.each` + bundler + ${'esbuild'} + ${'none'} + ${'rollup'} + ${'swc'} + ${'tsc'} + ${'vite'} + `( + 'should generate js test config with @swc/jest when bundler=$bundler', + async ({ bundler }) => { + await libraryGenerator(tree, { + ...defaultOptions, + directory: 'my-lib', + unitTestRunner: 'jest', + bundler, + }); + + expect(tree.exists('my-lib/tsconfig.spec.json')).toBeTruthy(); + expect(tree.exists('my-lib/src/lib/my-lib.spec.ts')).toBeTruthy(); + expect(tree.exists('my-lib/jest.config.ts')).toBeTruthy(); + expect(tree.read('my-lib/jest.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "/* eslint-disable */ + import { readFileSync } from 'fs'; + + // Reading the SWC compilation config for the spec files + const swcJestConfig = JSON.parse( + readFileSync(\`\${__dirname}/.spec.swcrc\`, 'utf-8') + ); + + // Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves + swcJestConfig.swcrc = false; + + export default { + displayName: 'my-lib', + preset: '../jest.preset.js', + transform: { + '^.+\\\\.[tj]s$': ['@swc/jest', swcJestConfig], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: 'test-output/jest/coverage', + }; + " + `); + expect(tree.read('my-lib/.spec.swcrc', 'utf-8')).toMatchInlineSnapshot(` + "{ + "jsc": { + "target": "es2017", + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "decoratorMetadata": true, + "legacyDecorator": true + }, + "keepClassNames": true, + "externalHelpers": true, + "loose": true + }, + "module": { + "type": "es6" + }, + "sourceMaps": true, + "exclude": [] + } + " + `); + expect(tree.read('my-lib/README.md', 'utf-8')).toContain( + 'nx test my-lib' + ); + } + ); }); }); diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index 406314d179c5f..7b33890e64a35 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -154,7 +154,11 @@ export async function libraryGeneratorInternal( if (options.unitTestRunner === 'jest') { const jestCallback = await addJest(tree, options); tasks.push(jestCallback); - if (options.bundler === 'swc' || options.bundler === 'rollup') { + + if ( + !options.isUsingTsSolutionConfig && + (options.bundler === 'swc' || options.bundler === 'rollup') + ) { replaceJestConfig(tree, options); } } else if ( @@ -626,9 +630,10 @@ function createFiles(tree: Tree, options: NormalizedLibraryGeneratorOptions) { ...determineEntryFields(options), }; - if (options.bundler === 'none') { - updatedPackageJson.type = 'module'; - } else if (options.bundler !== 'vite' && options.bundler !== 'rollup') { + if ( + options.isUsingTsSolutionConfig && + !['none', 'rollup', 'vite'].includes(options.bundler) + ) { return getUpdatedPackageJsonContent(updatedPackageJson, { main: join(options.projectRoot, 'src/index.ts'), outputPath: joinPathFragments(options.projectRoot, 'dist'), @@ -658,20 +663,19 @@ function createFiles(tree: Tree, options: NormalizedLibraryGeneratorOptions) { packageJson.files = ['dist', '!**/*.tsbuildinfo']; } - if (options.isUsingTsSolutionConfig) { - if (options.bundler === 'none') { - packageJson.type = 'module'; - } else if (options.bundler !== 'vite' && options.bundler !== 'rollup') { - packageJson = getUpdatedPackageJsonContent(packageJson, { - main: join(options.projectRoot, 'src/index.ts'), - outputPath: joinPathFragments(options.projectRoot, 'dist'), - projectRoot: options.projectRoot, - rootDir: join(options.projectRoot, 'src'), - generateExportsField: true, - packageJsonPath, - format: ['esm'], - }); - } + if ( + options.isUsingTsSolutionConfig && + !['none', 'rollup', 'vite'].includes(options.bundler) + ) { + packageJson = getUpdatedPackageJsonContent(packageJson, { + main: join(options.projectRoot, 'src/index.ts'), + outputPath: joinPathFragments(options.projectRoot, 'dist'), + projectRoot: options.projectRoot, + rootDir: join(options.projectRoot, 'src'), + generateExportsField: true, + packageJsonPath, + format: ['esm'], + }); } writeJson(tree, packageJsonPath, packageJson); @@ -710,14 +714,13 @@ async function addJest( setupFile: 'none', supportTsx: false, skipSerializers: true, - testEnvironment: options.testEnvironment, + testEnvironment: options.testEnvironment ?? 'node', skipFormat: true, - compiler: - options.bundler === 'swc' || options.bundler === 'tsc' - ? options.bundler - : options.bundler === 'rollup' - ? 'swc' - : undefined, + compiler: options.shouldUseSwcJest + ? 'swc' + : options.bundler === 'tsc' + ? 'tsc' + : undefined, runtimeTsconfigFileName: 'tsconfig.lib.json', }); } @@ -886,6 +889,11 @@ async function normalizeOptions( // We default to generate a project.json file if the new setup is not being used options.useProjectJson ??= !isUsingTsSolutionConfig; + const shouldUseSwcJest = + options.bundler === 'swc' || + options.bundler === 'rollup' || + isUsingTsSolutionConfig; + return { ...options, fileName, @@ -896,6 +904,7 @@ async function normalizeOptions( importPath, hasPlugin, isUsingTsSolutionConfig, + shouldUseSwcJest, }; } @@ -1020,6 +1029,7 @@ function createProjectTsConfigs( options.bundler === 'tsc' ? 'dist' : `out-tsc/${options.projectRoot.split('/').pop()}`, + emitDeclarationOnly: options.bundler === 'tsc' ? false : true, } ); @@ -1201,6 +1211,7 @@ function determineEntryFields( case 'none': { if (options.isUsingTsSolutionConfig) { return { + type: 'module', main: options.js ? './src/index.js' : './src/index.ts', types: options.js ? './src/index.js' : './src/index.ts', exports: { diff --git a/packages/js/src/generators/library/schema.d.ts b/packages/js/src/generators/library/schema.d.ts index d6ffe011b6fde..3a8967ff46c37 100644 --- a/packages/js/src/generators/library/schema.d.ts +++ b/packages/js/src/generators/library/schema.d.ts @@ -46,4 +46,5 @@ export interface NormalizedLibraryGeneratorOptions importPath?: string; hasPlugin: boolean; isUsingTsSolutionConfig: boolean; + shouldUseSwcJest: boolean; } diff --git a/packages/js/src/utils/swc/add-swc-config.ts b/packages/js/src/utils/swc/add-swc-config.ts index 004ccf8fc748e..f6783e7bffcf8 100644 --- a/packages/js/src/utils/swc/add-swc-config.ts +++ b/packages/js/src/utils/swc/add-swc-config.ts @@ -10,17 +10,33 @@ export const defaultExclude = [ '.*.js$', ]; -const swcOptionsString = (type: 'commonjs' | 'es6' = 'commonjs') => `{ +const swcOptionsString = ( + type: 'commonjs' | 'es6' = 'commonjs', + exclude: string[], + supportTsx: boolean +) => `{ "jsc": { "target": "es2017", "parser": { "syntax": "typescript", "decorators": true, - "dynamicImport": true + "dynamicImport": true${ + supportTsx + ? `, + "tsx": true` + : '' + } }, "transform": { "decoratorMetadata": true, - "legacyDecorator": true + "legacyDecorator": true${ + supportTsx + ? `, + "react": { + "runtime": "automatic" + }` + : '' + } }, "keepClassNames": true, "externalHelpers": true, @@ -30,15 +46,28 @@ const swcOptionsString = (type: 'commonjs' | 'es6' = 'commonjs') => `{ "type": "${type}" }, "sourceMaps": true, - "exclude": ${JSON.stringify(defaultExclude)} -}`; + "exclude": ${JSON.stringify(exclude)} +} +`; export function addSwcConfig( tree: Tree, projectDir: string, - type: 'commonjs' | 'es6' = 'commonjs' + type: 'commonjs' | 'es6' = 'commonjs', + supportTsx: boolean = false ) { const swcrcPath = join(projectDir, '.swcrc'); if (tree.exists(swcrcPath)) return; - tree.write(swcrcPath, swcOptionsString(type)); + tree.write(swcrcPath, swcOptionsString(type, defaultExclude, supportTsx)); +} + +export function addSwcTestConfig( + tree: Tree, + projectDir: string, + type: 'commonjs' | 'es6' = 'commonjs', + supportTsx: boolean = false +) { + const swcrcPath = join(projectDir, '.spec.swcrc'); + if (tree.exists(swcrcPath)) return; + tree.write(swcrcPath, swcOptionsString(type, [], supportTsx)); } diff --git a/packages/nest/src/generators/library/library.spec.ts b/packages/nest/src/generators/library/library.spec.ts index 4c23fa2727dc1..a67dc159f8ebb 100644 --- a/packages/nest/src/generators/library/library.spec.ts +++ b/packages/nest/src/generators/library/library.spec.ts @@ -398,7 +398,7 @@ describe('lib', () => { { "compilerOptions": { "baseUrl": ".", - "emitDeclarationOnly": false, + "emitDeclarationOnly": true, "forceConsistentCasingInFileNames": true, "importHelpers": true, "module": "nodenext", @@ -435,8 +435,8 @@ describe('lib', () => { "compilerOptions": { "forceConsistentCasingInFileNames": true, "importHelpers": true, - "module": "commonjs", - "moduleResolution": "node10", + "module": "nodenext", + "moduleResolution": "nodenext", "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "noImplicitReturns": true, diff --git a/packages/node/src/generators/application/application.spec.ts b/packages/node/src/generators/application/application.spec.ts index d564c3859f756..185920fa4827a 100644 --- a/packages/node/src/generators/application/application.spec.ts +++ b/packages/node/src/generators/application/application.spec.ts @@ -716,5 +716,64 @@ describe('app', () => { } `); }); + + it('should use @swc/jest for jest', async () => { + await applicationGenerator(tree, { + directory: 'apps/my-app', + swcJest: true, + } as Schema); + + expect(tree.read('apps/my-app/jest.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "/* eslint-disable */ + import { readFileSync } from 'fs'; + + // Reading the SWC compilation config for the spec files + const swcJestConfig = JSON.parse( + readFileSync(\`\${__dirname}/.spec.swcrc\`, 'utf-8') + ); + + // Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves + swcJestConfig.swcrc = false; + + export default { + displayName: 'my-app', + preset: '../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\\\.[tj]s$': ['@swc/jest', swcJestConfig], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: 'test-output/jest/coverage', + }; + " + `); + expect(tree.read('apps/my-app/.spec.swcrc', 'utf-8')) + .toMatchInlineSnapshot(` + "{ + "jsc": { + "target": "es2017", + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "decoratorMetadata": true, + "legacyDecorator": true + }, + "keepClassNames": true, + "externalHelpers": true, + "loose": true + }, + "module": { + "type": "es6" + }, + "sourceMaps": true, + "exclude": [] + } + " + `); + }); }); }); diff --git a/packages/node/src/generators/application/application.ts b/packages/node/src/generators/application/application.ts index 5a25fd3528278..bf96a69dcbc35 100644 --- a/packages/node/src/generators/application/application.ts +++ b/packages/node/src/generators/application/application.ts @@ -634,6 +634,9 @@ async function normalizeOptions( process.env.NX_ADD_PLUGINS !== 'false' && nxJson.useInferencePlugins !== false; + const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host); + const swcJest = options.swcJest ?? isUsingTsSolutionConfig; + return { addPlugin, ...options, @@ -651,7 +654,8 @@ async function normalizeOptions( 'dist', options.rootProject ? options.name : appProjectRoot ), - isUsingTsSolutionConfig: isUsingTsSolutionSetup(host), + isUsingTsSolutionConfig, + swcJest, }; } diff --git a/packages/node/src/generators/application/schema.json b/packages/node/src/generators/application/schema.json index 114ca3d0f0982..0717df09cc72f 100644 --- a/packages/node/src/generators/application/schema.json +++ b/packages/node/src/generators/application/schema.json @@ -69,8 +69,7 @@ }, "swcJest": { "type": "boolean", - "description": "Use `@swc/jest` instead `ts-jest` for faster test compilation.", - "default": false + "description": "Use `@swc/jest` instead `ts-jest` for faster test compilation." }, "babelJest": { "type": "boolean", diff --git a/packages/node/src/generators/library/library.spec.ts b/packages/node/src/generators/library/library.spec.ts index f000e9cac18d0..bfb45305807e5 100644 --- a/packages/node/src/generators/library/library.spec.ts +++ b/packages/node/src/generators/library/library.spec.ts @@ -609,7 +609,7 @@ describe('lib', () => { { "compilerOptions": { "baseUrl": ".", - "emitDeclarationOnly": false, + "emitDeclarationOnly": true, "module": "nodenext", "moduleResolution": "nodenext", "outDir": "dist", @@ -634,8 +634,8 @@ describe('lib', () => { expect(readJson(tree, 'mylib/tsconfig.spec.json')).toMatchInlineSnapshot(` { "compilerOptions": { - "module": "commonjs", - "moduleResolution": "node10", + "module": "nodenext", + "moduleResolution": "nodenext", "outDir": "./out-tsc/jest", "types": [ "jest", diff --git a/packages/plugin/src/generators/e2e-project/e2e.spec.ts b/packages/plugin/src/generators/e2e-project/e2e.spec.ts index 976416d6092c4..63b9053c68afe 100644 --- a/packages/plugin/src/generators/e2e-project/e2e.spec.ts +++ b/packages/plugin/src/generators/e2e-project/e2e.spec.ts @@ -7,6 +7,7 @@ import { readJson, getProjects, writeJson, + updateJson, } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { e2eProjectGenerator } from './e2e'; @@ -160,6 +161,22 @@ describe('NxPlugin e2e-project Generator', () => { expect(tree.exists('my-plugin-e2e/tsconfig.spec.json')).toBeTruthy(); expect(tree.exists('my-plugin-e2e/jest.config.ts')).toBeTruthy(); + expect(tree.read('my-plugin-e2e/jest.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "export default { + displayName: 'my-plugin-e2e', + preset: '../jest.preset.js', + transform: { + '^.+\\\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../coverage/my-plugin-e2e', + globalSetup: '../tools/scripts/start-local-registry.ts', + globalTeardown: '../tools/scripts/stop-local-registry.ts', + }; + " + `); + expect(tree.exists('my-plugin-e2e/.spec.swcrc')).toBeFalsy(); }); it('should setup the eslint builder', async () => { @@ -174,4 +191,108 @@ describe('NxPlugin e2e-project Generator', () => { tree.read('my-plugin-e2e/.eslintrc.json', 'utf-8') ).toMatchSnapshot(); }); + + describe('TS solution setup', () => { + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + updateJson(tree, 'package.json', (json) => { + json.workspaces = ['packages/*']; + return json; + }); + writeJson(tree, 'tsconfig.base.json', { + compilerOptions: { + composite: true, + declaration: true, + }, + }); + writeJson(tree, 'tsconfig.json', { + extends: './tsconfig.base.json', + files: [], + references: [], + }); + + // add a plugin project to the workspace for validations + addProjectConfiguration(tree, 'my-plugin', { + root: 'packages/my-plugin', + }); + writeJson(tree, 'packages/my-plugin/package.json', { + name: 'my-plugin', + }); + }); + + it('should add jest support', async () => { + await e2eProjectGenerator(tree, { + pluginName: 'my-plugin', + npmPackageName: '@proj/my-plugin', + projectDirectory: 'packages/my-plugin', + pluginOutputPath: `dist/packages/my-plugin`, + }); + + const project = readProjectConfiguration(tree, 'my-plugin-e2e'); + + expect(project.targets.e2e).toMatchObject({ + options: expect.objectContaining({ + jestConfig: 'packages/my-plugin-e2e/jest.config.ts', + }), + }); + + expect( + tree.exists('packages/my-plugin-e2e/tsconfig.spec.json') + ).toBeTruthy(); + expect(tree.exists('packages/my-plugin-e2e/jest.config.ts')).toBeTruthy(); + expect(tree.read('packages/my-plugin-e2e/jest.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "/* eslint-disable */ + import { readFileSync } from 'fs'; + + // Reading the SWC compilation config for the spec files + const swcJestConfig = JSON.parse( + readFileSync(\`\${__dirname}/.spec.swcrc\`, 'utf-8') + ); + + // Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves + swcJestConfig.swcrc = false; + + export default { + displayName: 'my-plugin-e2e', + preset: '../../jest.preset.js', + transform: { + '^.+\\\\.[tj]s$': ['@swc/jest', swcJestConfig], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: 'test-output/jest/coverage', + globalSetup: '../../tools/scripts/start-local-registry.ts', + globalTeardown: '../../tools/scripts/stop-local-registry.ts', + }; + " + `); + expect(tree.exists('packages/my-plugin-e2e/.spec.swcrc')).toBeTruthy(); + expect(tree.read('packages/my-plugin-e2e/.spec.swcrc', 'utf-8')) + .toMatchInlineSnapshot(` + "{ + "jsc": { + "target": "es2017", + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "decoratorMetadata": true, + "legacyDecorator": true + }, + "keepClassNames": true, + "externalHelpers": true, + "loose": true + }, + "module": { + "type": "es6" + }, + "sourceMaps": true, + "exclude": [] + } + " + `); + }); + }); }); diff --git a/packages/plugin/src/generators/e2e-project/e2e.ts b/packages/plugin/src/generators/e2e-project/e2e.ts index 78dc75ae381d0..90c0609ca4d7f 100644 --- a/packages/plugin/src/generators/e2e-project/e2e.ts +++ b/packages/plugin/src/generators/e2e-project/e2e.ts @@ -148,6 +148,7 @@ async function addJest(host: Tree, options: NormalizedSchema) { skipSerializers: true, skipFormat: true, addPlugin: options.addPlugin, + compiler: options.isTsSolutionSetup ? 'swc' : undefined, }); const { startLocalRegistryPath, stopLocalRegistryPath } = diff --git a/packages/plugin/src/generators/plugin/plugin.spec.ts b/packages/plugin/src/generators/plugin/plugin.spec.ts index 6b36c8b1831eb..89972b46c6e71 100644 --- a/packages/plugin/src/generators/plugin/plugin.spec.ts +++ b/packages/plugin/src/generators/plugin/plugin.spec.ts @@ -7,6 +7,7 @@ import { readProjectConfiguration, Tree, updateJson, + writeJson, } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { Linter } from '@nx/eslint'; @@ -192,9 +193,22 @@ describe('NxPlugin Plugin Generator', () => { }) ); - ['my-plugin/jest.config.ts'].forEach((path) => - expect(tree.exists(path)).toBeTruthy() - ); + expect(tree.exists('my-plugin/jest.config.ts')).toBeTruthy(); + expect(tree.read('my-plugin/jest.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "export default { + displayName: 'my-plugin', + preset: '../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../coverage/my-plugin', + }; + " + `); + expect(tree.exists('my-plugin/.spec.swcrc')).toBeFalsy(); const projectTargets = readProjectConfiguration( tree, @@ -312,4 +326,97 @@ describe('NxPlugin Plugin Generator', () => { expect(projects.has('my-plugin-e2e')).toBe(false); }); }); + + describe('TS solution setup', () => { + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + updateJson(tree, 'package.json', (json) => { + json.workspaces = ['packages/*']; + return json; + }); + writeJson(tree, 'tsconfig.base.json', { + compilerOptions: { + composite: true, + declaration: true, + }, + }); + writeJson(tree, 'tsconfig.json', { + extends: './tsconfig.base.json', + files: [], + references: [], + }); + }); + + it('should generate test files with jest.config.ts', async () => { + await pluginGenerator( + tree, + getSchema({ + directory: 'my-plugin', + unitTestRunner: 'jest', + }) + ); + + expect(tree.exists('my-plugin/jest.config.ts')).toBeTruthy(); + expect(tree.read('my-plugin/jest.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "/* eslint-disable */ + import { readFileSync } from 'fs'; + + // Reading the SWC compilation config for the spec files + const swcJestConfig = JSON.parse( + readFileSync(\`\${__dirname}/.spec.swcrc\`, 'utf-8') + ); + + // Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves + swcJestConfig.swcrc = false; + + export default { + displayName: 'my-plugin', + preset: '../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\\\.[tj]s$': ['@swc/jest', swcJestConfig], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: 'test-output/jest/coverage', + }; + " + `); + expect(tree.exists('my-plugin/.spec.swcrc')).toBeTruthy(); + expect(tree.read('my-plugin/.spec.swcrc', 'utf-8')) + .toMatchInlineSnapshot(` + "{ + "jsc": { + "target": "es2017", + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "decoratorMetadata": true, + "legacyDecorator": true + }, + "keepClassNames": true, + "externalHelpers": true, + "loose": true + }, + "module": { + "type": "es6" + }, + "sourceMaps": true, + "exclude": [] + } + " + `); + + const projectTargets = readProjectConfiguration( + tree, + 'my-plugin' + ).targets; + + expect(projectTargets.test).toBeDefined(); + expect(projectTargets.test?.executor).toEqual('@nx/jest:jest'); + }); + }); }); diff --git a/packages/remix/src/generators/application/lib/normalize-options.ts b/packages/remix/src/generators/application/lib/normalize-options.ts index afc8f4cead7cb..d1ae13cf4e0ab 100644 --- a/packages/remix/src/generators/application/lib/normalize-options.ts +++ b/packages/remix/src/generators/application/lib/normalize-options.ts @@ -3,8 +3,9 @@ import { determineProjectNameAndRootOptions, ensureProjectName, } from '@nx/devkit/src/generators/project-name-and-root-utils'; -import { type NxRemixGeneratorSchema } from '../schema'; import { Linter } from '@nx/eslint'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { type NxRemixGeneratorSchema } from '../schema'; export interface NormalizedSchema extends NxRemixGeneratorSchema { projectName: string; @@ -50,5 +51,6 @@ export async function normalizeOptions( e2eProjectName, e2eProjectRoot, parsedTags, + useTsSolution: options.useTsSolution ?? isUsingTsSolutionSetup(tree), }; } diff --git a/packages/vite/src/generators/vitest/vitest-generator.ts b/packages/vite/src/generators/vitest/vitest-generator.ts index 25c8b7563da1c..11909e964d7e6 100644 --- a/packages/vite/src/generators/vitest/vitest-generator.ts +++ b/packages/vite/src/generators/vitest/vitest-generator.ts @@ -12,6 +12,7 @@ import { runTasksInSerial, Tree, updateJson, + updateNxJson, } from '@nx/devkit'; import { initGenerator as jsInitGenerator } from '@nx/js'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; @@ -148,9 +149,23 @@ getTestBed().initTestEnvironment( } } - createFiles(tree, schema, root); + const isTsSolutionSetup = isUsingTsSolutionSetup(tree); + + createFiles(tree, schema, root, isTsSolutionSetup); updateTsConfig(tree, schema, root, projectType); + if (isTsSolutionSetup) { + // in the TS solution setup, the test target depends on the build outputs + // so we need to setup the task pipeline accordingly + const nxJson = readNxJson(tree); + const testTarget = schema.testTarget ?? 'test'; + nxJson.targetDefaults ??= {}; + nxJson.targetDefaults[testTarget] ??= {}; + nxJson.targetDefaults[testTarget].dependsOn ??= []; + nxJson.targetDefaults[testTarget].dependsOn.push('^build'); + updateNxJson(tree, nxJson); + } + const coverageProviderDependency = getCoverageProviderDependency( schema.coverageProvider ); @@ -304,9 +319,9 @@ function updateTsConfig( function createFiles( tree: Tree, options: VitestGeneratorSchema, - projectRoot: string + projectRoot: string, + isTsSolutionSetup: boolean ) { - const isTsSolutionSetup = isUsingTsSolutionSetup(tree); const rootOffset = offsetFromRoot(projectRoot); generateFiles(tree, join(__dirname, 'files'), projectRoot, { diff --git a/packages/vite/src/generators/vitest/vitest.spec.ts b/packages/vite/src/generators/vitest/vitest.spec.ts index 6f5453baf5ab2..1a75ac651be4e 100644 --- a/packages/vite/src/generators/vitest/vitest.spec.ts +++ b/packages/vite/src/generators/vitest/vitest.spec.ts @@ -1,10 +1,13 @@ import 'nx/src/internal-testing-utils/mock-project-graph'; import { + addProjectConfiguration, createProjectGraphAsync, readJson, + readNxJson, Tree, updateJson, + writeJson, } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; @@ -241,6 +244,94 @@ describe('vitest generator', () => { expect(devDependencies['@analogjs/vitest-angular']).toBeDefined(); }); }); + + describe('TS solution setup', () => { + beforeEach(() => { + appTree = createTreeWithEmptyWorkspace(); + updateJson(appTree, 'package.json', (json) => { + json.workspaces = ['packages/*']; + return json; + }); + writeJson(appTree, 'tsconfig.base.json', { + compilerOptions: { composite: true }, + }); + writeJson(appTree, 'tsconfig.json', { + extends: './tsconfig.base.json', + files: [], + references: [], + }); + + addProjectConfiguration(appTree, 'pkg1', { + root: 'packages/pkg1', + sourceRoot: 'packages/pkg1/src', + targets: { + lint: { + executor: '@nx/eslint:lint', + options: {}, + }, + }, + }); + writeJson(appTree, 'packages/pkg1/tsconfig.json', { + files: [], + include: [], + references: [], + }); + }); + + it('should add a tsconfig.spec.json file', async () => { + await generator(appTree, { + project: 'pkg1', + coverageProvider: 'v8', + }); + + const tsconfig = readJson(appTree, 'packages/pkg1/tsconfig.json'); + expect(tsconfig.references).toEqual( + expect.arrayContaining([{ path: './tsconfig.spec.json' }]) + ); + expect(appTree.read('packages/pkg1/tsconfig.spec.json', 'utf-8')) + .toMatchInlineSnapshot(` + "{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out-tsc/vitest", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] + } + " + `); + }); + + it(`should setup a task pipeline for the test target to depend on the deps' build target`, async () => { + await generator(appTree, { + project: 'pkg1', + coverageProvider: 'v8', + }); + + const nxJson = readNxJson(appTree); + expect(nxJson.targetDefaults.test.dependsOn).toStrictEqual(['^build']); + }); + }); }); function setUpAngularWorkspace() {