diff --git a/e2e/jest/src/jest-root.test.ts b/e2e/jest/src/jest-root.test.ts index 22ed385cfb..86c274bcd8 100644 --- a/e2e/jest/src/jest-root.test.ts +++ b/e2e/jest/src/jest-root.test.ts @@ -6,13 +6,16 @@ describe('Jest root projects', () => { describe('angular', () => { beforeAll(() => { - newProject({ packages: ['@nx/angular', '@nx/react'] }); + newProject({ + packages: ['@nx/angular'], + unsetProjectNameAndRootFormat: false, + }); + runCLI( + `generate @nx/angular:app ${myapp} --directory . --rootProject --projectNameAndRootFormat as-provided --no-interactive` + ); }); it('should test root level app projects', async () => { - runCLI( - `generate @nx/angular:app ${myapp} --rootProject=true --no-interactive` - ); const rootProjectTestResults = await runCLIAsync(`test ${myapp}`); expect(rootProjectTestResults.combinedOutput).toContain( 'Test Suites: 1 passed, 1 total' @@ -20,9 +23,8 @@ describe('Jest root projects', () => { }, 300_000); it('should add lib project and tests should still work', async () => { - runCLI(`generate @nx/angular:lib ${mylib} --no-interactive`); runCLI( - `generate @nx/angular:component ${mylib} --export --standalone --project=${mylib} --no-interactive` + `generate @nx/angular:lib ${mylib} --projectNameAndRootFormat as-provided --no-interactive` ); const libProjectTestResults = await runCLIAsync(`test ${mylib}`); @@ -41,12 +43,16 @@ describe('Jest root projects', () => { describe('react', () => { beforeAll(() => { - newProject(); + newProject({ + packages: ['@nx/react'], + unsetProjectNameAndRootFormat: false, + }); + runCLI( + `generate @nx/react:app ${myapp} --directory . --rootProject --projectNameAndRootFormat as-provided` + ); }); it('should test root level app projects', async () => { - runCLI(`generate @nx/react:app ${myapp} --rootProject=true`); - const rootProjectTestResults = await runCLIAsync(`test ${myapp}`); expect(rootProjectTestResults.combinedOutput).toContain( @@ -55,7 +61,9 @@ describe('Jest root projects', () => { }, 300_000); it('should add lib project and tests should still work', async () => { - runCLI(`generate @nx/react:lib ${mylib} --unitTestRunner=jest`); + runCLI( + `generate @nx/react:lib ${mylib} --unitTestRunner=jest --projectNameAndRootFormat as-provided` + ); const libProjectTestResults = await runCLIAsync(`test ${mylib}`); diff --git a/e2e/next-core/src/next-pcv3.test.ts b/e2e/next-core/src/next-pcv3.test.ts index 1550573c95..0ee191908e 100644 --- a/e2e/next-core/src/next-pcv3.test.ts +++ b/e2e/next-core/src/next-pcv3.test.ts @@ -17,7 +17,9 @@ describe('@nx/next/plugin', () => { let appName: string; beforeAll(() => { - project = newProject(); + project = newProject({ + packages: ['@nx/next'], + }); appName = uniq('app'); runCLI( `generate @nx/next:app ${appName} --project-name-and-root-format=as-provided --no-interactive`, diff --git a/e2e/nx-misc/src/workspace.test.ts b/e2e/nx-misc/src/workspace.test.ts index 5c0aecc4e2..aeab6f3c57 100644 --- a/e2e/nx-misc/src/workspace.test.ts +++ b/e2e/nx-misc/src/workspace.test.ts @@ -24,7 +24,7 @@ describe('@nx/workspace:convert-to-monorepo', () => { afterEach(() => cleanupProject()); - it('should convert a standalone project to a monorepo', async () => { + it('should convert a standalone webpack and jest react project to a monorepo', async () => { const reactApp = uniq('reactapp'); runCLI( `generate @nx/react:app ${reactApp} --rootProject=true --bundler=webpack --unitTestRunner=jest --e2eTestRunner=cypress --no-interactive` @@ -43,6 +43,26 @@ describe('@nx/workspace:convert-to-monorepo', () => { expect(() => runCLI(`lint e2e`)).not.toThrow(); expect(() => runCLI(`e2e e2e`)).not.toThrow(); }); + + it('should be convert a standalone vite and playwright react project to a monorepo', async () => { + const reactApp = uniq('reactapp'); + runCLI( + `generate @nx/react:app ${reactApp} --rootProject=true --bundler=vite --unitTestRunner vitest --e2eTestRunner=playwright --no-interactive` + ); + + runCLI('generate @nx/workspace:convert-to-monorepo --no-interactive'); + + checkFilesExist( + `apps/${reactApp}/src/main.tsx`, + `apps/e2e/playwright.config.ts` + ); + + expect(() => runCLI(`build ${reactApp}`)).not.toThrow(); + expect(() => runCLI(`test ${reactApp}`)).not.toThrow(); + expect(() => runCLI(`lint ${reactApp}`)).not.toThrow(); + expect(() => runCLI(`lint e2e`)).not.toThrow(); + expect(() => runCLI(`e2e e2e`)).not.toThrow(); + }); }); describe('Workspace Tests', () => { diff --git a/e2e/webpack/src/webpack.pcv3.test.ts b/e2e/webpack/src/webpack.pcv3.test.ts index f2925fd706..5cab7732ed 100644 --- a/e2e/webpack/src/webpack.pcv3.test.ts +++ b/e2e/webpack/src/webpack.pcv3.test.ts @@ -11,7 +11,10 @@ describe('Webpack Plugin (PCv3)', () => { beforeAll(() => { originalPcv3 = process.env.NX_PCV3; process.env.NX_PCV3 = 'true'; - newProject(); + newProject({ + packages: ['@nx/react'], + unsetProjectNameAndRootFormat: false, + }); }); afterAll(() => { @@ -19,13 +22,21 @@ describe('Webpack Plugin (PCv3)', () => { cleanupProject(); }); - it('should generate, build, and serve React applications', () => { + it('should generate, build, and serve React applications and libraries', () => { const appName = uniq('app'); + const libName = uniq('lib'); runCLI( - `generate @nx/react:app ${appName} --bundler webpack --e2eTestRunner=cypress --no-interactive` + `generate @nx/react:app ${appName} --bundler webpack --e2eTestRunner=cypress --rootProject --no-interactive` ); - expect(true).toBe(true); + expect(() => runCLI(`test ${appName}`)).not.toThrow(); + + runCLI( + `generate @nx/react:lib ${libName} --unitTestRunner jest --no-interactive` + ); + + expect(() => runCLI(`test ${appName}`)).not.toThrow(); + expect(() => runCLI(`test ${libName}`)).not.toThrow(); // TODO: figure out why this test hangs in CI (maybe down to sudo prompt?) // expect(() => runCLI(`build ${appName}`)).not.toThrow(); diff --git a/packages/angular/src/generators/application/lib/add-unit-test-runner.ts b/packages/angular/src/generators/application/lib/add-unit-test-runner.ts index 6d99336d8a..81948ee2e4 100644 --- a/packages/angular/src/generators/application/lib/add-unit-test-runner.ts +++ b/packages/angular/src/generators/application/lib/add-unit-test-runner.ts @@ -6,6 +6,7 @@ import type { NormalizedSchema } from './normalized-schema'; export async function addUnitTestRunner(host: Tree, options: NormalizedSchema) { if (options.unitTestRunner === UnitTestRunner.Jest) { await configurationGenerator(host, { + ...options, project: options.name, setupFile: 'angular', supportTsx: false, diff --git a/packages/angular/src/generators/cypress-component-configuration/cypress-component-configuration.spec.ts b/packages/angular/src/generators/cypress-component-configuration/cypress-component-configuration.spec.ts index 99891ff047..47e51e1dc2 100644 --- a/packages/angular/src/generators/cypress-component-configuration/cypress-component-configuration.spec.ts +++ b/packages/angular/src/generators/cypress-component-configuration/cypress-component-configuration.spec.ts @@ -37,6 +37,11 @@ describe('Cypress Component Testing Configuration', () => { tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); tree.write('.gitignore', ''); mockedInstalledCypressVersion.mockReturnValue(10); + + projectGraph = { + dependencies: {}, + nodes: {}, + }; }); afterEach(() => { @@ -191,6 +196,9 @@ describe('Cypress Component Testing Configuration', () => { export: true, skipFormat: true, }); + + jest.clearAllMocks(); + const appConfig = readProjectConfiguration(tree, 'fancy-app'); appConfig.targets['build'].executor = 'something/else'; updateProjectConfiguration(tree, 'fancy-app', appConfig); diff --git a/packages/angular/src/generators/library/library.spec.ts b/packages/angular/src/generators/library/library.spec.ts index d0ecbd5372..cbbdb55f86 100644 --- a/packages/angular/src/generators/library/library.spec.ts +++ b/packages/angular/src/generators/library/library.spec.ts @@ -48,6 +48,11 @@ describe('lib', () => { beforeEach(() => { tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + + projectGraph = { + dependencies: {}, + nodes: {}, + }; }); it('should run the library generator without erroring if the directory has a trailing slash', async () => { diff --git a/packages/angular/src/migrations/update-15-9-0/update-testing-tsconfig.spec.ts b/packages/angular/src/migrations/update-15-9-0/update-testing-tsconfig.spec.ts index 4d504cc1a8..5da3ccf68b 100644 --- a/packages/angular/src/migrations/update-15-9-0/update-testing-tsconfig.spec.ts +++ b/packages/angular/src/migrations/update-15-9-0/update-testing-tsconfig.spec.ts @@ -26,6 +26,11 @@ describe('Jest+Ng - 15.9.0 - tsconfig updates', () => { beforeEach(() => { tree = createTreeWithEmptyWorkspace(); tree.write('.gitignore', ''); + + projectGraph = { + dependencies: {}, + nodes: {}, + }; }); it('should update tsconfig.spec.json with target es2016', async () => { diff --git a/packages/jest/package.json b/packages/jest/package.json index 25c461a51f..fcda8fb738 100644 --- a/packages/jest/package.json +++ b/packages/jest/package.json @@ -42,6 +42,7 @@ "jest-config": "^29.4.1", "jest-resolve": "^29.4.1", "jest-util": "^29.4.1", + "minimatch": "3.0.5", "resolve.exports": "1.1.0", "tslib": "^2.3.0", "@nx/devkit": "file:../devkit", diff --git a/packages/jest/plugin.ts b/packages/jest/plugin.ts new file mode 100644 index 0000000000..c03221112c --- /dev/null +++ b/packages/jest/plugin.ts @@ -0,0 +1,5 @@ +export { + createNodes, + createDependencies, + JestPluginOptions, +} from './src/plugins/plugin'; diff --git a/packages/jest/src/generators/configuration/configuration.ts b/packages/jest/src/generators/configuration/configuration.ts index 4f062facd3..58fedf03f4 100644 --- a/packages/jest/src/generators/configuration/configuration.ts +++ b/packages/jest/src/generators/configuration/configuration.ts @@ -9,6 +9,7 @@ import { Tree, GeneratorCallback, readProjectConfiguration, + readNxJson, } from '@nx/devkit'; const schemaDefaults = { @@ -65,7 +66,16 @@ export async function configurationGenerator( checkForTestTarget(tree, options); createFiles(tree, options); updateTsConfig(tree, options); - updateWorkspace(tree, options); + + const nxJson = readNxJson(tree); + const hasPlugin = nxJson.plugins?.some((p) => + typeof p === 'string' + ? p === '@nx/jest/plugin' + : p.plugin === '@nx/jest/plugin' + ); + if (!hasPlugin) { + updateWorkspace(tree, options); + } if (!schema.skipFormat) { await formatFiles(tree); diff --git a/packages/jest/src/generators/init/init.spec.ts b/packages/jest/src/generators/init/init.spec.ts index e2eb9f96e0..ec69dde8ce 100644 --- a/packages/jest/src/generators/init/init.spec.ts +++ b/packages/jest/src/generators/init/init.spec.ts @@ -1,6 +1,15 @@ +let projectGraph: ProjectGraph; +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: jest.fn().mockImplementation(async () => { + return projectGraph; + }), +})); + import { - addProjectConfiguration, + addProjectConfiguration as _addProjectConfiguration, NxJsonConfiguration, + ProjectGraph, readJson, readProjectConfiguration, stripIndents, @@ -11,11 +20,29 @@ import { import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { jestInitGenerator } from './init'; +function addProjectConfiguration(tree, name, project) { + _addProjectConfiguration(tree, name, project); + projectGraph.nodes[name] = { + name: name, + type: 'lib', + data: { + root: project.root, + targets: project.targets, + }, + }; +} + describe('jest', () => { let tree: Tree; beforeEach(() => { tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + + projectGraph = { + nodes: {}, + dependencies: {}, + externalNodes: {}, + }; }); it('should generate files with --js flag', async () => { @@ -250,6 +277,21 @@ export default { ); await jestInitGenerator(tree, { rootProject: false }); expect(tree.exists('jest.config.app.ts')).toBeTruthy(); + expect(tree.read('jest.config.app.ts', 'utf-8')).toMatchInlineSnapshot(` + " + /* eslint-disable */ + export default { + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], + globals: { 'ts-jest': { tsconfig: '/tsconfig.spec.json' } }, + displayName: 'my-project', + testEnvironment: 'node', + preset: './jest.preset.js', + }; + " + `); expect(tree.read('jest.config.ts', 'utf-8')) .toEqual(`import { getJestProjects } from '@nx/jest'; diff --git a/packages/jest/src/generators/init/init.ts b/packages/jest/src/generators/init/init.ts index 1757985cfb..767d9fdec5 100644 --- a/packages/jest/src/generators/init/init.ts +++ b/packages/jest/src/generators/init/init.ts @@ -1,11 +1,13 @@ import { addDependenciesToPackageJson, + createProjectGraphAsync, GeneratorCallback, - getProjects, readNxJson, + readProjectConfiguration, removeDependenciesFromPackageJson, runTasksInSerial, stripIndents, + TargetConfiguration, Tree, updateJson, updateNxJson, @@ -26,6 +28,7 @@ import { typesNodeVersion, } from '../../utils/versions'; import { JestInitSchema } from './schema'; +import { readTargetDefaultsForTarget } from 'nx/src/project-graph/utils/project-configuration-utils'; interface NormalizedSchema extends ReturnType {} @@ -53,7 +56,28 @@ function generateGlobalConfig(tree: Tree, isJS: boolean) { tree.write(`jest.config.${isJS ? 'js' : 'ts'}`, contents); } -function createJestConfig(tree: Tree, options: NormalizedSchema) { +function addPlugin(tree: Tree) { + const nxJson = readNxJson(tree); + + nxJson.plugins ??= []; + if ( + !nxJson.plugins.some((p) => + typeof p === 'string' + ? p === '@nx/jest/plugin' + : p.plugin === '@nx/jest/plugin' + ) + ) { + nxJson.plugins.push({ + plugin: '@nx/jest/plugin', + options: { + targetName: 'test', + }, + }); + } + updateNxJson(tree, nxJson); +} + +async function createJestConfig(tree: Tree, options: NormalizedSchema) { if (!tree.exists('jest.preset.js')) { // preset is always js file. tree.write( @@ -64,8 +88,15 @@ function createJestConfig(tree: Tree, options: NormalizedSchema) { module.exports = { ...nxPreset }` ); + const shouldAddPlugin = process.env.NX_PCV3 === 'true'; + if (shouldAddPlugin) { + addPlugin(tree); + } + updateProductionFileSet(tree); - addJestTargetDefaults(tree); + if (!shouldAddPlugin) { + addJestTargetDefaults(tree, shouldAddPlugin); + } } if (options.rootProject) { // we don't want any config to be made because the `configurationGenerator` will do it. @@ -84,30 +115,83 @@ function createJestConfig(tree: Tree, options: NormalizedSchema) { if (tree.exists(rootJestPath)) { // moving from root project config to monorepo-style config - const projects = getProjects(tree); - const projectNames = Array.from(projects.keys()); - const rootProject = projectNames.find( - (projectName) => projects.get(projectName)?.root === '.' + const { nodes: projects } = await createProjectGraphAsync(); + const projectConfigurations = Object.values(projects); + const rootProject = projectConfigurations.find( + (projectNode) => projectNode.data?.root === '.' ); // root project might have been removed, // if it's missing there's nothing to migrate if (rootProject) { - const rootProjectConfig = projects.get(rootProject); - const jestTarget = Object.values(rootProjectConfig.targets || {}).find( - (t) => - t?.executor === '@nx/jest:jest' || t?.executor === '@nrwl/jest:jest' + const jestTarget = Object.entries(rootProject.data?.targets ?? {}).find( + ([_, t]) => + ((t?.executor === '@nx/jest:jest' || + t?.executor === '@nrwl/jest:jest') && + t?.options?.jestConfig === rootJestPath) || + (t?.executor === 'nx:run-commands' && t?.options?.command === 'jest') ); - const isProjectConfig = jestTarget?.options?.jestConfig === rootJestPath; - // if root project doesn't have jest target, there's nothing to migrate - if (isProjectConfig) { - const jestProjectConfig = `jest.config.${ - rootProjectConfig.projectType === 'application' ? 'app' : 'lib' - }.${options.js ? 'js' : 'ts'}`; - - tree.rename(rootJestPath, jestProjectConfig); - jestTarget.options.jestConfig = jestProjectConfig; - updateProjectConfiguration(tree, rootProject, rootProjectConfig); + if (!jestTarget) { + return; } + + const [jestTargetName, jestTargetConfigInGraph] = jestTarget; + // if root project doesn't have jest target, there's nothing to migrate + const rootProjectConfig = readProjectConfiguration( + tree, + rootProject.name + ); + + if ( + rootProjectConfig.targets['test']?.executor === 'nx:run-commands' + ? rootProjectConfig.targets['test']?.command !== 'jest' + : rootProjectConfig.targets['test']?.options?.jestConfig !== + rootJestPath + ) { + // Jest target has already been updated + return; + } + + const jestProjectConfig = `jest.config.${ + rootProjectConfig.projectType === 'application' ? 'app' : 'lib' + }.${options.js ? 'js' : 'ts'}`; + + tree.rename(rootJestPath, jestProjectConfig); + + const nxJson = readNxJson(tree); + const targetDefaults = readTargetDefaultsForTarget( + jestTargetName, + nxJson.targetDefaults, + jestTargetConfigInGraph.executor + ); + + const target: TargetConfiguration = (rootProjectConfig.targets[ + jestTargetName + ] ??= + jestTargetConfigInGraph.executor === 'nx:run-commands' + ? { command: `jest --config ${jestProjectConfig}` } + : { + executor: jestTargetConfigInGraph.executor, + options: {}, + }); + + if (target.executor === '@nx/jest:jest') { + target.options.jestConfig = jestProjectConfig; + } + + if (targetDefaults?.cache === undefined) { + target.cache = jestTargetConfigInGraph.cache; + } + if (targetDefaults?.inputs === undefined) { + target.inputs = jestTargetConfigInGraph.inputs; + } + if (targetDefaults?.outputs === undefined) { + target.outputs = jestTargetConfigInGraph.outputs; + } + if (targetDefaults?.dependsOn === undefined) { + target.dependsOn = jestTargetConfigInGraph.dependsOn; + } + + updateProjectConfiguration(tree, rootProject.name, rootProjectConfig); // generate new global config as it was move to project config or is missing generateGlobalConfig(tree, options.js); } @@ -139,20 +223,23 @@ function updateProductionFileSet(tree: Tree) { updateNxJson(tree, nxJson); } -function addJestTargetDefaults(tree: Tree) { +function addJestTargetDefaults(tree: Tree, hasPlugin: boolean) { const nxJson = readNxJson(tree); - const productionFileSet = nxJson.namedInputs?.production; nxJson.targetDefaults ??= {}; nxJson.targetDefaults['@nx/jest:jest'] ??= {}; - nxJson.targetDefaults['@nx/jest:jest'].cache ??= true; - // Test targets depend on all their project's sources + production sources of dependencies - nxJson.targetDefaults['@nx/jest:jest'].inputs ??= [ - 'default', - productionFileSet ? '^production' : '^default', - '{workspaceRoot}/jest.preset.js', - ]; + if (!hasPlugin) { + const productionFileSet = nxJson.namedInputs?.production; + + nxJson.targetDefaults['@nx/jest:jest'].cache ??= true; + // Test targets depend on all their project's sources + production sources of dependencies + nxJson.targetDefaults['@nx/jest:jest'].inputs ??= [ + 'default', + productionFileSet ? '^production' : '^default', + '{workspaceRoot}/jest.preset.js', + ]; + } nxJson.targetDefaults['@nx/jest:jest'].options ??= { passWithNoTests: true, @@ -231,7 +318,7 @@ export async function jestInitGenerator( }) ); - createJestConfig(tree, options); + await createJestConfig(tree, options); if (!options.skipPackageJson) { removeDependenciesFromPackageJson(tree, ['@nx/jest'], []); diff --git a/packages/jest/src/plugins/plugin.spec.ts b/packages/jest/src/plugins/plugin.spec.ts new file mode 100644 index 0000000000..2eb0db233d --- /dev/null +++ b/packages/jest/src/plugins/plugin.spec.ts @@ -0,0 +1,82 @@ +import { CreateNodesContext } from '@nx/devkit'; +import { join } from 'path'; + +import { createNodes } from './plugin'; +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; + +describe('@nx/jest/plugin', () => { + let createNodesFunction = createNodes[1]; + let context: CreateNodesContext; + let tempFs: TempFs; + + beforeEach(async () => { + tempFs = new TempFs('test'); + context = { + nxJsonConfiguration: { + namedInputs: { + default: ['{projectRoot}/**/*'], + production: ['!{projectRoot}/**/*.spec.ts'], + }, + }, + workspaceRoot: tempFs.tempDir, + }; + + await tempFs.createFiles({ + 'proj/jest.config.js': '', + 'proj/project.json': '{}', + }); + }); + + afterEach(() => { + jest.resetModules(); + }); + + it('should create nodes based on jest.config.ts', async () => { + mockJestConfig( + { + coverageDirectory: '../coverage', + }, + context + ); + const nodes = await createNodesFunction( + 'proj/jest.config.js', + { + targetName: 'test', + }, + context + ); + + expect(nodes.projects.proj).toMatchInlineSnapshot(` + { + "root": "proj", + "targets": { + "test": { + "cache": true, + "command": "jest", + "inputs": [ + "default", + "^production", + { + "externalDependencies": [ + "jest", + ], + }, + ], + "options": { + "cwd": "proj", + }, + "outputs": [ + "{workspaceRoot}/coverage", + ], + }, + }, + } + `); + }); +}); + +function mockJestConfig(config: any, context: CreateNodesContext) { + jest.mock(join(context.workspaceRoot, 'proj/jest.config.js'), () => config, { + virtual: true, + }); +} diff --git a/packages/jest/src/plugins/plugin.ts b/packages/jest/src/plugins/plugin.ts new file mode 100644 index 0000000000..e7d2c3aae1 --- /dev/null +++ b/packages/jest/src/plugins/plugin.ts @@ -0,0 +1,200 @@ +import { + CreateDependencies, + CreateNodes, + CreateNodesContext, + joinPathFragments, + NxJsonConfiguration, + readJsonFile, + TargetConfiguration, + writeJsonFile, +} from '@nx/devkit'; +import { dirname, join, relative, resolve } from 'path'; + +import { readTargetDefaultsForTarget } from 'nx/src/project-graph/utils/project-configuration-utils'; +import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs'; +import { existsSync, readdirSync } from 'fs'; +import { readConfig } from 'jest-config'; +import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory'; +import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; +import { getGlobPatternsFromPackageManagerWorkspaces } from 'nx/plugins/package-json-workspaces'; +import { combineGlobPatterns } from 'nx/src/utils/globs'; +import * as minimatch from 'minimatch'; + +export interface JestPluginOptions { + targetName?: string; +} + +const cachePath = join(projectGraphCacheDirectory, 'jest.hash'); +const targetsCache = existsSync(cachePath) ? readTargetsCache() : {}; + +const calculatedTargets: Record< + string, + Record +> = {}; + +function readTargetsCache(): Record< + string, + Record +> { + return readJsonFile(cachePath); +} + +function writeTargetsToCache( + targets: Record> +) { + writeJsonFile(cachePath, targets); +} + +export const createDependencies: CreateDependencies = () => { + writeTargetsToCache(calculatedTargets); + return []; +}; + +export const createNodes: CreateNodes = [ + '**/jest.config.{cjs,mjs,js,cts,mts,ts}', + async (configFilePath, options, context) => { + const projectRoot = dirname(configFilePath); + + const packageManagerWorkspacesGlob = combineGlobPatterns( + getGlobPatternsFromPackageManagerWorkspaces(context.workspaceRoot) + ); + + // Do not create a project if package.json and project.json isn't there. + const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot)); + if ( + !siblingFiles.includes('package.json') && + !siblingFiles.includes('project.json') + ) { + return {}; + } else if ( + !siblingFiles.includes('project.json') && + siblingFiles.includes('package.json') + ) { + const path = joinPathFragments(projectRoot, 'package.json'); + + const isPackageJsonProject = minimatch( + path, + packageManagerWorkspacesGlob + ); + + if (!isPackageJsonProject) { + return {}; + } + } + + options = normalizeOptions(options); + + const hash = calculateHashForCreateNodes(projectRoot, options, context); + const targets = + targetsCache[hash] ?? + (await buildJestTargets(configFilePath, projectRoot, options, context)); + + calculatedTargets[hash] = targets; + + return { + projects: { + [projectRoot]: { + root: projectRoot, + targets: targets, + }, + }, + }; + }, +]; + +async function buildJestTargets( + configFilePath: string, + projectRoot: string, + options: JestPluginOptions, + context: CreateNodesContext +) { + const config = await readConfig( + { + _: [], + $0: undefined, + }, + resolve(context.workspaceRoot, configFilePath), + true, + null, + undefined, + true + ); + + const targetDefaults = readTargetDefaultsForTarget( + options.targetName, + context.nxJsonConfiguration.targetDefaults, + 'nx:run-commands' + ); + + const namedInputs = getNamedInputs(projectRoot, context); + + const targets: Record = {}; + + const target: TargetConfiguration = (targets[options.targetName] = { + command: 'jest', + options: { + cwd: projectRoot, + }, + }); + + if (!targetDefaults?.cache) { + target.cache = true; + } + if (!targetDefaults?.inputs) { + target.inputs = getInputs(namedInputs); + } + if (!targetDefaults?.outputs) { + target.outputs = getOutputs(projectRoot, config, context); + } + + return targets; +} + +function getInputs( + namedInputs: NxJsonConfiguration['namedInputs'] +): TargetConfiguration['inputs'] { + return [ + ...('production' in namedInputs + ? ['default', '^production'] + : ['default', '^default']), + { + externalDependencies: ['jest'], + }, + ]; +} + +function getOutputs( + projectRoot: string, + { globalConfig }: Awaited>, + context: CreateNodesContext +): string[] { + function getOutput(path: string): string { + const relativePath = relative( + join(context.workspaceRoot, projectRoot), + path + ); + if (relativePath.startsWith('..')) { + return join('{workspaceRoot}', join(projectRoot, relativePath)); + } else { + return join('{projectRoot}', relativePath); + } + } + + const outputs = []; + + for (const outputOption of [ + globalConfig.coverageDirectory, + globalConfig.outputFile, + ]) { + if (outputOption) { + outputs.push(getOutput(outputOption)); + } + } + + return outputs; +} +function normalizeOptions(options: JestPluginOptions): JestPluginOptions { + options ??= {}; + options.targetName ??= 'test'; + return options; +} diff --git a/packages/workspace/src/generators/convert-to-monorepo/convert-to-monorepo.spec.ts b/packages/workspace/src/generators/convert-to-monorepo/convert-to-monorepo.spec.ts index 358f2455d7..78653e9a83 100644 --- a/packages/workspace/src/generators/convert-to-monorepo/convert-to-monorepo.spec.ts +++ b/packages/workspace/src/generators/convert-to-monorepo/convert-to-monorepo.spec.ts @@ -104,44 +104,6 @@ describe('monorepo generator', () => { expect(tree.exists('libs/inner/my-lib/src/index.ts')).toBeTruthy(); }); - it('should convert root React app (Webpack, Jest)', async () => { - await reactAppGenerator(tree, { - name: 'demo', - style: 'css', - bundler: 'webpack', - unitTestRunner: 'jest', - e2eTestRunner: 'none', - linter: 'eslint', - rootProject: true, - }); - - await monorepoGenerator(tree, {}); - - expect(readProjectConfiguration(tree, 'demo')).toMatchObject({ - sourceRoot: 'apps/demo/src', - targets: { - build: { - executor: '@nx/webpack:webpack', - options: { - main: 'apps/demo/src/main.tsx', - tsConfig: 'apps/demo/tsconfig.app.json', - webpackConfig: 'apps/demo/webpack.config.js', - }, - }, - test: { - executor: '@nx/jest:jest', - options: { - jestConfig: 'apps/demo/jest.config.app.ts', - }, - }, - }, - }); - - // Extracted base config files - expect(tree.exists('tsconfig.base.json')).toBeTruthy(); - expect(tree.exists('jest.config.ts')).toBeTruthy(); - }); - it('should convert root Next.js app with existing libraries', async () => { await nextAppGenerator(tree, { name: 'demo', diff --git a/packages/workspace/src/generators/move/move.spec.ts b/packages/workspace/src/generators/move/move.spec.ts index 77a3d6eadf..ef0a907463 100644 --- a/packages/workspace/src/generators/move/move.spec.ts +++ b/packages/workspace/src/generators/move/move.spec.ts @@ -136,279 +136,6 @@ describe('move', () => { ); }); - it('should support moving root projects', async () => { - // Test that these are not moved - tree.write('.gitignore', ''); - tree.write('README.md', ''); - - await libraryGenerator(tree, { - name: 'my-lib', - rootProject: true, - bundler: 'tsc', - buildable: true, - unitTestRunner: 'jest', - linter: 'eslint', - projectNameAndRootFormat: 'as-provided', - }); - - updateJson(tree, 'tsconfig.json', (json) => { - json.extends = './tsconfig.base.json'; - json.files = ['./node_modules/@foo/bar/index.d.ts']; - return json; - }); - - let projectJson = readJson(tree, 'project.json'); - expect(projectJson['$schema']).toEqual( - 'node_modules/nx/schemas/project-schema.json' - ); - // Test that this does not get moved - tree.write('other-lib/index.ts', ''); - - await moveGenerator(tree, { - projectName: 'my-lib', - importPath: '@proj/my-lib', - updateImportPath: true, - destination: 'my-lib', - projectNameAndRootFormat: 'as-provided', - }); - - expect(readJson(tree, 'my-lib/project.json')).toMatchObject({ - name: 'my-lib', - $schema: '../node_modules/nx/schemas/project-schema.json', - sourceRoot: 'my-lib/src', - projectType: 'library', - targets: { - build: { - executor: '@nx/js:tsc', - outputs: ['{options.outputPath}'], - options: { - outputPath: 'dist/my-lib', - main: 'my-lib/src/index.ts', - tsConfig: 'my-lib/tsconfig.lib.json', - }, - }, - lint: { - executor: '@nx/eslint:lint', - }, - test: { - executor: '@nx/jest:jest', - outputs: ['{workspaceRoot}/coverage/{projectName}'], - }, - }, - }); - - expect(readJson(tree, 'my-lib/tsconfig.json')).toMatchObject({ - extends: '../tsconfig.base.json', - files: ['../node_modules/@foo/bar/index.d.ts'], - references: [ - { path: './tsconfig.lib.json' }, - { path: './tsconfig.spec.json' }, - ], - }); - - const jestConfig = tree.read('my-lib/jest.config.lib.ts', 'utf-8'); - expect(jestConfig).toContain(`preset: '../jest.preset.js'`); - - expect(tree.exists('my-lib/tsconfig.lib.json')).toBeTruthy(); - expect(tree.exists('my-lib/tsconfig.spec.json')).toBeTruthy(); - expect(tree.exists('my-lib/.eslintrc.json')).toBeTruthy(); - expect(tree.exists('my-lib/src/index.ts')).toBeTruthy(); - - // Test that other libs and workspace files are not moved. - expect(tree.exists('package.json')).toBeTruthy(); - expect(tree.exists('README.md')).toBeTruthy(); - expect(tree.exists('.gitignore')).toBeTruthy(); - expect(tree.exists('other-lib/index.ts')).toBeTruthy(); - - // Test that root configs are extracted - expect(tree.exists('tsconfig.base.json')).toBeTruthy(); - expect(tree.exists('jest.config.ts')).toBeTruthy(); - expect(tree.exists('.eslintrc.base.json')).not.toBeTruthy(); - expect(tree.exists('.eslintrc.json')).toBeTruthy(); - - // Test that eslint migration was done - expect(readJson(tree, 'my-lib/.eslintrc.json').extends) - .toMatchInlineSnapshot(` - [ - "../.eslintrc.json", - ] - `); - expect(readJson(tree, 'my-lib/.eslintrc.json').plugins).not.toBeDefined(); - expect(readJson(tree, '.eslintrc.json').plugins).toEqual(['@nx']); - }); - - it('should support moving standalone repos', async () => { - // Test that these are not moved - tree.write('.gitignore', ''); - tree.write('README.md', ''); - - await applicationGenerator(tree, { - name: 'react-app', - rootProject: true, - unitTestRunner: 'jest', - e2eTestRunner: 'cypress', - linter: 'eslint', - style: 'css', - projectNameAndRootFormat: 'as-provided', - }); - expect(readJson(tree, '.eslintrc.json').plugins).toEqual(['@nx']); - expect(readJson(tree, 'e2e/.eslintrc.json').plugins).toEqual(['@nx']); - - // Test that this does not get moved - tree.write('other-lib/index.ts', ''); - - await moveGenerator(tree, { - projectName: 'react-app', - updateImportPath: false, - destination: 'apps/react-app', - projectNameAndRootFormat: 'as-provided', - }); - - // expect both eslint configs to have been changed - expect(tree.exists('.eslintrc.json')).toBeDefined(); - expect( - readJson(tree, 'apps/react-app/.eslintrc.json').plugins - ).toBeUndefined(); - expect(readJson(tree, 'e2e/.eslintrc.json').plugins).toBeUndefined(); - - await moveGenerator(tree, { - projectName: 'e2e', - updateImportPath: false, - destination: 'apps/react-app-e2e', - projectNameAndRootFormat: 'as-provided', - }); - - expect(tree.read('apps/react-app-e2e/cypress.config.ts').toString()) - .toMatchInlineSnapshot(` - "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; - - import { defineConfig } from 'cypress'; - - export default defineConfig({ - e2e: { - ...nxE2EPreset(__filename, { cypressDir: 'src' }), - baseUrl: 'http://localhost:4200', - }, - }); - " - `); - }); - - it('should correctly move standalone repos that have migrated eslint config', async () => { - // Test that these are not moved - tree.write('.gitignore', ''); - tree.write('README.md', ''); - - await applicationGenerator(tree, { - name: 'react-app', - rootProject: true, - unitTestRunner: 'jest', - e2eTestRunner: 'cypress', - linter: 'eslint', - style: 'css', - projectNameAndRootFormat: 'as-provided', - }); - await libraryGenerator(tree, { - name: 'my-lib', - bundler: 'tsc', - buildable: true, - unitTestRunner: 'jest', - linter: 'eslint', - directory: 'my-lib', - projectNameAndRootFormat: 'as-provided', - }); - // assess the correct starting position - expect(tree.exists('.eslintrc.base.json')).toBeTruthy(); - expect(readJson(tree, '.eslintrc.json').plugins).not.toBeDefined(); - expect(readJson(tree, '.eslintrc.json').extends).toEqual([ - 'plugin:@nx/react', - './.eslintrc.base.json', - ]); - expect(readJson(tree, 'e2e/.eslintrc.json').plugins).not.toBeDefined(); - expect(readJson(tree, 'e2e/.eslintrc.json').extends).toEqual([ - 'plugin:cypress/recommended', - '../.eslintrc.base.json', - ]); - - await moveGenerator(tree, { - projectName: 'react-app', - updateImportPath: false, - destination: 'apps/react-app', - projectNameAndRootFormat: 'as-provided', - }); - - // expect both eslint configs to have been changed - expect(tree.exists('.eslintrc.json')).toBeTruthy(); - expect(tree.exists('.eslintrc.base.json')).toBeFalsy(); - - expect(readJson(tree, 'apps/react-app/.eslintrc.json').extends).toEqual([ - 'plugin:@nx/react', - '../../.eslintrc.json', - ]); - expect(readJson(tree, 'e2e/.eslintrc.json').extends).toEqual([ - 'plugin:cypress/recommended', - '../.eslintrc.json', - ]); - }); - - it('should support scoped new project name for libraries', async () => { - await libraryGenerator(tree, { - name: 'my-lib', - projectNameAndRootFormat: 'as-provided', - }); - - await moveGenerator(tree, { - projectName: 'my-lib', - newProjectName: '@proj/shared-my-lib', - updateImportPath: true, - destination: 'shared/my-lib', - projectNameAndRootFormat: 'as-provided', - }); - - expect(tree.exists('shared/my-lib/package.json')).toBeTruthy(); - expect(tree.exists('shared/my-lib/tsconfig.lib.json')).toBeTruthy(); - expect(tree.exists('shared/my-lib/src/index.ts')).toBeTruthy(); - expect(readProjectConfiguration(tree, '@proj/shared-my-lib')) - .toMatchInlineSnapshot(` - { - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "name": "@proj/shared-my-lib", - "projectType": "library", - "root": "shared/my-lib", - "sourceRoot": "shared/my-lib/src", - "tags": [], - "targets": { - "build": { - "executor": "@nx/js:tsc", - "options": { - "assets": [ - "shared/my-lib/*.md", - ], - "main": "shared/my-lib/src/index.ts", - "outputPath": "dist/shared/my-lib", - "tsConfig": "shared/my-lib/tsconfig.lib.json", - }, - "outputs": [ - "{options.outputPath}", - ], - }, - "lint": { - "executor": "@nx/eslint:lint", - }, - "test": { - "executor": "@nx/jest:jest", - "options": { - "jestConfig": "shared/my-lib/jest.config.ts", - }, - "outputs": [ - "{workspaceRoot}/coverage/{projectRoot}", - ], - }, - }, - } - `); - }); - it('should move project correctly when --project-name-and-root-format=derived', async () => { await libraryGenerator(tree, { name: 'my-lib',