diff --git a/e2e/nx-plugin-e2e/tests/plugin-derive-config.e2e.test.ts b/e2e/nx-plugin-e2e/tests/plugin-derive-config.e2e.test.ts new file mode 100644 index 000000000..0535964c0 --- /dev/null +++ b/e2e/nx-plugin-e2e/tests/plugin-derive-config.e2e.test.ts @@ -0,0 +1,171 @@ +import { type Tree, writeJson } from '@nx/devkit'; +import path from 'node:path'; +import { readProjectConfiguration } from 'nx/src/generators/utils/project-configuration'; +import { afterEach, expect } from 'vitest'; +import { generateCodePushupConfig } from '@code-pushup/nx-plugin'; +import { + generateProject, + generateWorkspaceAndProject, + materializeTree, + nxShowProjectJson, + nxTargetProject, + registerPluginInWorkspace, +} from '@code-pushup/test-nx-utils'; +import { + E2E_ENVIRONMENTS_DIR, + TEST_OUTPUT_DIR, + teardownTestFolder, +} from '@code-pushup/test-utils'; + +describe('nx-plugin-derived-config', () => { + let root: string; + let tree: Tree; + const projectName = 'pkg'; + const testFileDir = path.join( + E2E_ENVIRONMENTS_DIR, + nxTargetProject(), + TEST_OUTPUT_DIR, + 'plugin-create-nodes', + ); + + beforeEach(async () => { + tree = await generateWorkspaceAndProject(); + registerPluginInWorkspace(tree, '@code-pushup/nx-plugin'); + await generateProject(tree, projectName); + root = readProjectConfiguration(tree, projectName).root; + generateCodePushupConfig(tree, root); + }); + + afterEach(async () => { + await teardownTestFolder(testFileDir); + }); + + it('should derive config from project.json', async () => { + const cwd = path.join(testFileDir, 'project-config'); + const projectJsonPath = path.join('libs', projectName, 'project.json'); + const packageJsonPath = path.join('libs', projectName, 'package.json'); + tree.delete(projectJsonPath); + tree.delete(packageJsonPath); + writeJson(tree, projectJsonPath, { + root, + name: projectName, + targets: { + 'code-pushup': { + executor: `@code-pushup/nx-plugin:cli`, + options: { + 'persist.filename': 'my-report', + }, + }, + }, + }); + await materializeTree(tree, cwd); + + const { code, projectJson } = await nxShowProjectJson(cwd, projectName); + expect(code).toBe(0); + + expect(projectJson.targets).toStrictEqual( + expect.objectContaining({ + 'code-pushup': { + configurations: {}, + executor: `@code-pushup/nx-plugin:cli`, + options: { + 'persist.filename': 'my-report', + }, + parallelism: true, + }, + }), + ); + }); + + it('should derive config from package.json', async () => { + const cwd = path.join(testFileDir, 'package-config'); + const projectJsonPath = path.join('libs', projectName, 'project.json'); + const packageJsonPath = path.join('libs', projectName, 'package.json'); + tree.delete(projectJsonPath); + tree.delete(packageJsonPath); + writeJson(tree, packageJsonPath, { + name: `@code-pushup/${projectName}`, + nx: { + root, + name: projectName, + targets: { + 'code-pushup': { + executor: `@code-pushup/nx-plugin:cli`, + options: { + 'persist.filename': 'my-report', + }, + }, + }, + }, + }); + await materializeTree(tree, cwd); + + const { code, projectJson } = await nxShowProjectJson(cwd, projectName); + expect(code).toBe(0); + + expect(projectJson.targets).toStrictEqual( + expect.objectContaining({ + 'code-pushup': { + configurations: {}, + executor: `@code-pushup/nx-plugin:cli`, + options: { + 'persist.filename': 'my-report', + }, + parallelism: true, + }, + }), + ); + }); + + it('should derive config from mixed', async () => { + const cwd = path.join(testFileDir, 'mixed-config'); + const projectJsonPath = path.join('libs', projectName, 'project.json'); + const packageJsonPath = path.join('libs', projectName, 'package.json'); + + writeJson(tree, projectJsonPath, { + root, + name: projectName, + targets: { + 'code-pushup': { + executor: `@code-pushup/nx-plugin:cli`, + options: { + 'persist.filename': 'my-report', + }, + }, + }, + }); + writeJson(tree, packageJsonPath, { + name: `@code-pushup/${projectName}`, + nx: { + root, + name: projectName, + targets: { + 'code-pushup': { + executor: `@code-pushup/nx-plugin:cli`, + options: { + 'persist.outputPath': 'my-dir', + }, + }, + }, + }, + }); + await materializeTree(tree, cwd); + + const { code, projectJson } = await nxShowProjectJson(cwd, projectName); + expect(code).toBe(0); + + expect(projectJson.targets).toStrictEqual( + expect.objectContaining({ + 'code-pushup': { + configurations: {}, + executor: `@code-pushup/nx-plugin:cli`, + options: { + 'persist.filename': 'my-report', + 'persist.outputPath': 'my-dir', + }, + parallelism: true, + }, + }), + ); + }); +}); diff --git a/package-lock.json b/package-lock.json index 57caf40ff..362d35d3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,7 @@ "@nx/react": "19.8.13", "@nx/vite": "19.8.13", "@nx/workspace": "19.8.13", - "@push-based/nx-verdaccio": "0.0.0-alpha.30", + "@push-based/nx-verdaccio": "^0.0.0-alpha.31", "@swc-node/register": "1.9.2", "@swc/cli": "0.3.14", "@swc/core": "1.5.7", @@ -5761,9 +5761,9 @@ } }, "node_modules/@push-based/nx-verdaccio": { - "version": "0.0.0-alpha.30", - "resolved": "https://registry.npmjs.org/@push-based/nx-verdaccio/-/nx-verdaccio-0.0.0-alpha.30.tgz", - "integrity": "sha512-PB/WpfcqmyypyXkWKJBVinIgvOgYJV3ckXdQXKBHJuEKyUnfiUObmgnGqmlQqCgFlv5tvPkNGEFXDPTGL9JyGw==", + "version": "0.0.0-alpha.31", + "resolved": "https://registry.npmjs.org/@push-based/nx-verdaccio/-/nx-verdaccio-0.0.0-alpha.31.tgz", + "integrity": "sha512-+wWLvwTVwjsS/9CsuJY68us6+vcD06sKQm7/OtWNnxmGIZ3bpTqv5qor/BSixvMRde9AaGAwwROvTb4Z4Dsdyg==", "dev": true, "dependencies": { "@nx/devkit": "19.8.0", diff --git a/package.json b/package.json index 735a5c8a1..ffd44ee72 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@nx/react": "19.8.13", "@nx/vite": "19.8.13", "@nx/workspace": "19.8.13", - "@push-based/nx-verdaccio": "0.0.0-alpha.30", + "@push-based/nx-verdaccio": "^0.0.0-alpha.31", "@swc-node/register": "1.9.2", "@swc/cli": "0.3.14", "@swc/core": "1.5.7", diff --git a/packages/nx-plugin/README.md b/packages/nx-plugin/README.md index a1fc57551..e347e466e 100644 --- a/packages/nx-plugin/README.md +++ b/packages/nx-plugin/README.md @@ -66,3 +66,5 @@ Examples: - `nx run :code-pushup` - `nx run :code-pushup print-config --persist.filename=custom-report` + +> ℹ️ This plugin supports both V1 and V2 plugin strategies. When installed in an Nx workspace using Nx 18 or later, the executors will be automatically inferred. Learn more about inferred tasks [here](https://nx.dev/concepts/inferred-tasks). diff --git a/packages/nx-plugin/src/plugin/plugin.ts b/packages/nx-plugin/src/plugin/plugin.ts index 1f125f5a8..0afdf1f25 100644 --- a/packages/nx-plugin/src/plugin/plugin.ts +++ b/packages/nx-plugin/src/plugin/plugin.ts @@ -1,14 +1,16 @@ -import type { - CreateNodes, - CreateNodesContext, - CreateNodesResult, +import { + type CreateNodes, + type CreateNodesContext, + type CreateNodesResult, + type CreateNodesV2, + createNodesFromFiles, } from '@nx/devkit'; import { PROJECT_JSON_FILE_NAME } from '../internal/constants.js'; import { createTargets } from './target/targets.js'; import type { CreateNodesOptions } from './types.js'; import { normalizedCreateNodesContext } from './utils.js'; -// name has to be "createNodes" to get picked up by Nx + createNodesFromFiles( + async (globMatchingFile, internalOptions) => { + const parsedCreateNodesOptions = internalOptions as CreateNodesOptions; + + const normalizedContext = await normalizedCreateNodesContext( + context, + globMatchingFile, + parsedCreateNodesOptions, + ); + + return { + projects: { + [normalizedContext.projectRoot]: { + targets: await createTargets(normalizedContext), + }, + }, + }; + }, + configFiles, + options, + context, + ), +]; diff --git a/packages/nx-plugin/src/plugin/plugin.unit.test.ts b/packages/nx-plugin/src/plugin/plugin.unit.test.ts index 62b3c0b2f..cc8c77d0e 100644 --- a/packages/nx-plugin/src/plugin/plugin.unit.test.ts +++ b/packages/nx-plugin/src/plugin/plugin.unit.test.ts @@ -1,139 +1,160 @@ -import type { CreateNodesContext } from '@nx/devkit'; +import type { CreateNodesContext, CreateNodesContextV2 } from '@nx/devkit'; import { vol } from 'memfs'; import { describe, expect } from 'vitest'; -import { invokeCreateNodesOnVirtualFiles } from '@code-pushup/test-nx-utils'; +import { + createNodesContextV1, + createNodesContextV2, + invokeCreateNodesOnVirtualFilesV1, + invokeCreateNodesOnVirtualFilesV2, +} from '@code-pushup/test-nx-utils'; import { PACKAGE_NAME, PROJECT_JSON_FILE_NAME } from '../internal/constants.js'; import { CP_TARGET_NAME } from './constants.js'; -import { createNodes } from './plugin.js'; +import { createNodes, createNodesV2 } from './plugin.js'; describe('@code-pushup/nx-plugin/plugin', () => { - let context: CreateNodesContext; + describe('V1', () => { + let context: CreateNodesContext; - beforeEach(() => { - context = { - nxJsonConfiguration: {}, - workspaceRoot: '', - configFiles: [], - }; - }); + beforeEach(() => { + context = createNodesContextV1({ + nxJsonConfiguration: {}, + workspaceRoot: '', + }); + }); - afterEach(() => { - vol.reset(); - }); + afterEach(() => { + vol.reset(); + }); - it('should normalize context and use it to create the configuration target on ROOT project', async () => { - const projectRoot = '.'; - const matchingFilesData = { - [`${projectRoot}/${PROJECT_JSON_FILE_NAME}`]: `${JSON.stringify({ - name: '@org/empty-root', - })}`, - }; + it('should normalize context and use it to create the configuration target on ROOT project', async () => { + const projectRoot = '.'; + const matchingFilesData = { + [`${projectRoot}/${PROJECT_JSON_FILE_NAME}`]: `${JSON.stringify({ + name: '@org/empty-root', + })}`, + }; - await expect( - invokeCreateNodesOnVirtualFiles( - createNodes, - context, - {}, - { matchingFilesData }, - ), - ).resolves.toStrictEqual({ - [projectRoot]: { - targets: { - [`${CP_TARGET_NAME}--configuration`]: { - command: `nx g ${PACKAGE_NAME}:configuration --skipTarget --targetName="code-pushup" --project="@org/empty-root"`, + await expect( + invokeCreateNodesOnVirtualFilesV1( + createNodes, + context, + {}, + { matchingFilesData }, + ), + ).resolves.toStrictEqual({ + [projectRoot]: { + targets: { + [`${CP_TARGET_NAME}--configuration`]: { + command: `nx g ${PACKAGE_NAME}:configuration --skipTarget --targetName="code-pushup" --project="@org/empty-root"`, + }, }, }, - }, + }); }); - }); - it('should normalize context and use it to create the configuration target on PACKAGE project', async () => { - const projectRoot = 'apps/my-app'; - const matchingFilesData = { - [`${projectRoot}/${PROJECT_JSON_FILE_NAME}`]: `${JSON.stringify({ - name: '@org/empty-root', - })}`, - }; + it('should create the executor target on PACKAGE project if configured', async () => { + const projectRoot = 'apps/my-app'; + const matchingFilesData = { + [`${projectRoot}/${PROJECT_JSON_FILE_NAME}`]: `${JSON.stringify({ + name: '@org/empty-root', + })}`, + [`${projectRoot}/code-pushup.config.ts`]: '{}', + }; - await expect( - invokeCreateNodesOnVirtualFiles( - createNodes, - context, - {}, - { matchingFilesData }, - ), - ).resolves.toStrictEqual({ - [projectRoot]: { - targets: { - [`${CP_TARGET_NAME}--configuration`]: { - command: `nx g ${PACKAGE_NAME}:configuration --skipTarget --targetName="code-pushup" --project="@org/empty-root"`, + await expect( + invokeCreateNodesOnVirtualFilesV1( + createNodes, + context, + { + projectPrefix: 'cli', + }, + { matchingFilesData }, + ), + ).resolves.toStrictEqual({ + [projectRoot]: { + targets: { + [CP_TARGET_NAME]: { + executor: `${PACKAGE_NAME}:cli`, + options: { + projectPrefix: 'cli', + }, + }, }, }, - }, + }); }); }); - it('should create the executor target on ROOT project if configured', async () => { - const projectRoot = '.'; - const matchingFilesData = { - [`${projectRoot}/${PROJECT_JSON_FILE_NAME}`]: `${JSON.stringify({ - name: '@org/empty-root', - })}`, - [`${projectRoot}/code-pushup.config.ts`]: '{}', - }; + describe('V2', () => { + let context: CreateNodesContextV2; - await expect( - invokeCreateNodesOnVirtualFiles( - createNodes, - context, - { - projectPrefix: 'cli', - }, - { matchingFilesData }, - ), - ).resolves.toStrictEqual({ - [projectRoot]: { - targets: { - [CP_TARGET_NAME]: { - executor: `${PACKAGE_NAME}:cli`, - options: { - projectPrefix: 'cli', + beforeEach(() => { + context = createNodesContextV2({ + nxJsonConfiguration: {}, + workspaceRoot: '', + }); + }); + + afterEach(() => { + vol.reset(); + }); + + it('should normalize context and use it to create the configuration target on ROOT project', async () => { + const projectRoot = '.'; + const matchingFilesData = { + [`${projectRoot}/${PROJECT_JSON_FILE_NAME}`]: `${JSON.stringify({ + name: '@org/empty-root', + })}`, + }; + + await expect( + invokeCreateNodesOnVirtualFilesV2( + createNodesV2, + context, + {}, + { matchingFilesData }, + ), + ).resolves.toStrictEqual({ + [projectRoot]: { + targets: { + [`${CP_TARGET_NAME}--configuration`]: { + command: `nx g ${PACKAGE_NAME}:configuration --skipTarget --targetName="code-pushup" --project="@org/empty-root"`, }, }, }, - }, + }); }); - }); - it('should create the executor target on PACKAGE project if configured', async () => { - const projectRoot = 'apps/my-app'; - const matchingFilesData = { - [`${projectRoot}/${PROJECT_JSON_FILE_NAME}`]: `${JSON.stringify({ - name: '@org/empty-root', - })}`, - [`${projectRoot}/code-pushup.config.ts`]: '{}', - }; + it('should create the executor target on PACKAGE project if configured', async () => { + const projectRoot = 'apps/my-app'; + const matchingFilesData = { + [`${projectRoot}/${PROJECT_JSON_FILE_NAME}`]: `${JSON.stringify({ + name: '@org/empty-root', + })}`, + [`${projectRoot}/code-pushup.config.ts`]: '{}', + }; - await expect( - invokeCreateNodesOnVirtualFiles( - createNodes, - context, - { - projectPrefix: 'cli', - }, - { matchingFilesData }, - ), - ).resolves.toStrictEqual({ - [projectRoot]: { - targets: { - [CP_TARGET_NAME]: { - executor: `${PACKAGE_NAME}:cli`, - options: { - projectPrefix: 'cli', + await expect( + invokeCreateNodesOnVirtualFilesV2( + createNodesV2, + context, + { + projectPrefix: 'cli', + }, + { matchingFilesData }, + ), + ).resolves.toStrictEqual({ + [projectRoot]: { + targets: { + [CP_TARGET_NAME]: { + executor: `${PACKAGE_NAME}:cli`, + options: { + projectPrefix: 'cli', + }, }, }, }, - }, + }); }); }); }); diff --git a/packages/nx-plugin/src/plugin/target/configuration-target.ts b/packages/nx-plugin/src/plugin/target/configuration-target.ts index d19b9325b..dce38ff54 100644 --- a/packages/nx-plugin/src/plugin/target/configuration-target.ts +++ b/packages/nx-plugin/src/plugin/target/configuration-target.ts @@ -1,5 +1,5 @@ import type { TargetConfiguration } from '@nx/devkit'; -import type { RunCommandsOptions } from 'nx/src/executors/run-commands/run-commands.impl'; +import type { RunCommandsOptions } from 'nx/src/executors/run-commands/run-commands.impl.js'; import { objectToCliArgs } from '../../executors/internal/cli.js'; import { PACKAGE_NAME } from '../../internal/constants.js'; import { CP_TARGET_NAME } from '../constants.js'; diff --git a/packages/nx-plugin/src/plugin/types.ts b/packages/nx-plugin/src/plugin/types.ts index 4fd57ed95..8182c8697 100644 --- a/packages/nx-plugin/src/plugin/types.ts +++ b/packages/nx-plugin/src/plugin/types.ts @@ -18,8 +18,14 @@ export type ProjectConfigurationWithName = WithRequired< 'name' >; -export type NormalizedCreateNodesContext = CreateNodesContext & - CreateTargetsOptions; +export type NormalizedCreateNodesContext = ( + | CreateNodesContext + | CreateNodesContextV2 +) & { + projectJson: ProjectConfigurationWithName; + projectRoot: string; + createOptions: CreateNodesOptions; +}; -export type NormalizedCreateNodesV2Context = CreateNodesContextV2 & +export type NormalizedCreateNodesContextV2 = CreateNodesContextV2 & CreateTargetsOptions; diff --git a/packages/nx-plugin/src/plugin/utils.ts b/packages/nx-plugin/src/plugin/utils.ts index 8d551f682..3a473c587 100644 --- a/packages/nx-plugin/src/plugin/utils.ts +++ b/packages/nx-plugin/src/plugin/utils.ts @@ -5,12 +5,19 @@ import { CP_TARGET_NAME } from './constants.js'; import type { CreateNodesOptions, NormalizedCreateNodesContext, - NormalizedCreateNodesV2Context, + NormalizedCreateNodesContextV2, ProjectConfigurationWithName, } from './types.js'; +/** + * Normalize the context for a V1 or V2 Plugin. + * @param context - The context for a V1 or V2 Plugin. + * @param projectConfigurationFile - The project configuration file. + * @param createOptions - The create options. + * @returns The normalized context. + */ export async function normalizedCreateNodesContext( - context: CreateNodesContext, + context: CreateNodesContext | CreateNodesContextV2, projectConfigurationFile: string, createOptions: CreateNodesOptions = {}, ): Promise { @@ -42,7 +49,7 @@ export async function normalizedCreateNodesV2Context( context: CreateNodesContextV2, projectConfigurationFile: string, createOptions: CreateNodesOptions = {}, -): Promise { +): Promise { const projectRoot = path.dirname(projectConfigurationFile); try { diff --git a/packages/nx-plugin/src/plugin/utils.unit.test.ts b/packages/nx-plugin/src/plugin/utils.unit.test.ts index edf2bf1cb..439b72445 100644 --- a/packages/nx-plugin/src/plugin/utils.unit.test.ts +++ b/packages/nx-plugin/src/plugin/utils.unit.test.ts @@ -1,6 +1,6 @@ import { vol } from 'memfs'; import { describe, expect } from 'vitest'; -import { createNodesContext } from '@code-pushup/test-nx-utils'; +import { createNodesContextV2 } from '@code-pushup/test-nx-utils'; import { MEMFS_VOLUME } from '@code-pushup/test-utils'; import { normalizedCreateNodesContext } from './utils.js'; @@ -15,7 +15,7 @@ describe('normalizedCreateNodesContext', () => { await expect( normalizedCreateNodesContext( - createNodesContext({ workspaceRoot: MEMFS_VOLUME }), + createNodesContextV2({ workspaceRoot: MEMFS_VOLUME }), 'project.json', ), ).resolves.toStrictEqual( @@ -37,7 +37,7 @@ describe('normalizedCreateNodesContext', () => { await expect( normalizedCreateNodesContext( - createNodesContext(), + createNodesContextV2(), 'packages/utils/project.json', ), ).resolves.toStrictEqual( @@ -59,7 +59,7 @@ describe('normalizedCreateNodesContext', () => { await expect( normalizedCreateNodesContext( - createNodesContext({ + createNodesContextV2({ nxJsonConfiguration: { workspaceLayout: { libsDir: 'libs', @@ -90,7 +90,7 @@ describe('normalizedCreateNodesContext', () => { ); await expect( - normalizedCreateNodesContext(createNodesContext(), 'project.json'), + normalizedCreateNodesContext(createNodesContextV2(), 'project.json'), ).resolves.toStrictEqual( expect.objectContaining({ projectJson: { @@ -109,7 +109,7 @@ describe('normalizedCreateNodesContext', () => { ); await expect( - normalizedCreateNodesContext(createNodesContext(), 'project.json'), + normalizedCreateNodesContext(createNodesContextV2(), 'project.json'), ).rejects.toThrow('Error parsing project.json file project.json.'); }); @@ -124,7 +124,7 @@ describe('normalizedCreateNodesContext', () => { ); await expect( - normalizedCreateNodesContext(createNodesContext(), 'project.json'), + normalizedCreateNodesContext(createNodesContextV2(), 'project.json'), ).resolves.toStrictEqual( expect.objectContaining({ createOptions: { @@ -145,7 +145,7 @@ describe('normalizedCreateNodesContext', () => { ); await expect( - normalizedCreateNodesContext(createNodesContext(), 'project.json', { + normalizedCreateNodesContext(createNodesContextV2(), 'project.json', { projectPrefix: 'cli', }), ).resolves.toStrictEqual( diff --git a/testing/test-nx-utils/src/lib/utils/nx-plugin.ts b/testing/test-nx-utils/src/lib/utils/nx-plugin.ts index 65676bd9a..5a4b29506 100644 --- a/testing/test-nx-utils/src/lib/utils/nx-plugin.ts +++ b/testing/test-nx-utils/src/lib/utils/nx-plugin.ts @@ -3,6 +3,7 @@ import type { CreateNodesContext, CreateNodesContextV2, CreateNodesResult, + CreateNodesV2, } from '@nx/devkit'; import { vol } from 'memfs'; import { MEMFS_VOLUME } from '@code-pushup/test-utils'; @@ -28,10 +29,9 @@ import { MEMFS_VOLUME } from '@code-pushup/test-utils'; * @param createNodeOptions * @param mockData */ -export async function invokeCreateNodesOnVirtualFiles< +export async function invokeCreateNodesOnVirtualFilesV1< T extends Record | undefined, >( - // FIXME: refactor this to use the V2 api & remove the eslint disable on the whole file createNodes: CreateNodes, context: CreateNodesContext, createNodeOptions: T, @@ -55,22 +55,43 @@ export async function invokeCreateNodesOnVirtualFiles< ); } -export function createNodesContext( +export async function invokeCreateNodesOnVirtualFilesV2< + T extends Record | undefined, +>( + createNodes: CreateNodesV2, + context: CreateNodesContextV2, + createNodeOptions: T, + mockData: { + matchingFilesData: Record; + }, +) { + const { matchingFilesData } = mockData; + vol.fromJSON(matchingFilesData, MEMFS_VOLUME); + + const files = Object.keys(matchingFilesData); + + const results = await createNodes[1](files, createNodeOptions, context); + + const result: NonNullable = {}; + return results.reduce( + (acc, [_, { projects }]) => ({ ...acc, ...projects }), + result, + ); +} + +export function createNodesContextV1( options?: Partial, ): CreateNodesContext { - const { - workspaceRoot = process.cwd(), - nxJsonConfiguration = {}, - configFiles = [], - } = options ?? {}; + const { workspaceRoot = process.cwd(), nxJsonConfiguration = {} } = + options ?? {}; return { workspaceRoot, nxJsonConfiguration, - configFiles, + configFiles: [], }; } -export function createNodesV2Context( +export function createNodesContextV2( options?: Partial, ): CreateNodesContextV2 { const { workspaceRoot = process.cwd(), nxJsonConfiguration = {} } = diff --git a/testing/test-nx-utils/src/lib/utils/nx-plugin.unit.test.ts b/testing/test-nx-utils/src/lib/utils/nx-plugin.unit.test.ts index 7710cfccf..ea672ac11 100644 --- a/testing/test-nx-utils/src/lib/utils/nx-plugin.unit.test.ts +++ b/testing/test-nx-utils/src/lib/utils/nx-plugin.unit.test.ts @@ -1,65 +1,143 @@ -import * as process from 'node:process'; +import process from 'node:process'; import { describe, expect } from 'vitest'; import { - createNodesContext, - invokeCreateNodesOnVirtualFiles, + createNodesContextV1, + createNodesContextV2, + invokeCreateNodesOnVirtualFilesV1, + invokeCreateNodesOnVirtualFilesV2, } from './nx-plugin.js'; -describe('createNodesContext', () => { - it('should return a context with the provided options', () => { - const context = createNodesContext({ - workspaceRoot: 'root', - nxJsonConfiguration: { plugins: [] }, - }); - expect(context).toStrictEqual( - expect.objectContaining({ +describe('V1', () => { + describe('createNodesContextV1', () => { + it('should return a context with the provided options', () => { + const context = createNodesContextV1({ workspaceRoot: 'root', nxJsonConfiguration: { plugins: [] }, - }), - ); - }); + }); + expect(context).toEqual({ + workspaceRoot: 'root', + nxJsonConfiguration: { plugins: [] }, + configFiles: [], + }); + }); - it('should return a context with defaults', () => { - const context = createNodesContext(); - expect(context).toStrictEqual( - expect.objectContaining({ + it('should return a context with defaults', () => { + const context = createNodesContextV1(); + expect(context).toEqual({ workspaceRoot: process.cwd(), nxJsonConfiguration: {}, - }), - ); + configFiles: [], + }); + }); }); -}); -describe('invokeCreateNodesOnVirtualFiles', () => { - it('should invoke passed function if matching file is given', async () => { - const createNodesFnSpy = vi.fn().mockResolvedValue({}); - await expect( - invokeCreateNodesOnVirtualFiles( - [`**/project.json`, createNodesFnSpy], - createNodesContext(), - {}, - { - matchingFilesData: { - '**/project.json': JSON.stringify({ - name: 'my-lib', - }), + describe('invokeCreateNodesOnVirtualFilesV1', () => { + it('should invoke passed function if matching file is given', async () => { + const createNodesFnSpy = vi + .fn() + .mockResolvedValue({ projects: { 'my-lib': {} } }); + await expect( + invokeCreateNodesOnVirtualFilesV1( + [`**/project.json`, createNodesFnSpy], + createNodesContextV1(), + {}, + { + matchingFilesData: { + '**/project.json': JSON.stringify({ + name: 'my-lib', + }), + }, }, - }, - ), - ).resolves.toStrictEqual({}); - expect(createNodesFnSpy).toHaveBeenCalledTimes(1); + ), + ).resolves.toStrictEqual({ 'my-lib': {} }); + expect(createNodesFnSpy).toHaveBeenCalledTimes(1); + expect(createNodesFnSpy).toHaveBeenCalledWith( + '**/project.json', + {}, + expect.any(Object), + ); + }); + + it('should NOT invoke passed function if matching file is NOT given', async () => { + const createNodesFnSpy = vi.fn().mockResolvedValue({}); + await expect( + invokeCreateNodesOnVirtualFilesV1( + [`**/project.json`, createNodesFnSpy], + createNodesContextV1(), + {}, + { matchingFilesData: {} }, + ), + ).resolves.toStrictEqual({}); + expect(createNodesFnSpy).not.toHaveBeenCalled(); + }); + }); +}); + +describe('V2', () => { + describe('createNodesContext', () => { + it('should return a context with the provided options', () => { + const context = createNodesContextV2({ + workspaceRoot: 'root', + nxJsonConfiguration: { plugins: [] }, + }); + expect(context).toEqual({ + workspaceRoot: 'root', + nxJsonConfiguration: { plugins: [] }, + }); + }); + + it('should return a context with defaults', () => { + const context = createNodesContextV2(); + expect(context).toEqual({ + workspaceRoot: process.cwd(), + nxJsonConfiguration: {}, + }); + }); }); - it('should NOT invoke passed function if matching file is NOT given', async () => { - const createNodesFnSpy = vi.fn().mockResolvedValue({}); - await expect( - invokeCreateNodesOnVirtualFiles( - [`**/project.json`, createNodesFnSpy], - createNodesContext(), + describe('invokeCreateNodesOnVirtualFilesV2', () => { + it('should invoke passed function if matching file is given', async () => { + const createNodesFnSpy = vi + .fn() + .mockResolvedValue([ + ['**/project.json', { projects: { 'my-lib': {} } }], + ]); + + await expect( + invokeCreateNodesOnVirtualFilesV2( + [`**/project.json`, createNodesFnSpy], + createNodesContextV2(), + {}, + { + matchingFilesData: { + '**/project.json': JSON.stringify({ + name: 'my-lib', + }), + }, + }, + ), + ).resolves.toStrictEqual({ 'my-lib': {} }); + + expect(createNodesFnSpy).toHaveBeenCalledTimes(1); + expect(createNodesFnSpy).toHaveBeenCalledWith( + ['**/project.json'], {}, - { matchingFilesData: {} }, - ), - ).resolves.toStrictEqual({}); - expect(createNodesFnSpy).not.toHaveBeenCalled(); + expect.any(Object), + ); + }); + + it('should NOT invoke passed function if matching file is NOT given', async () => { + const createNodesFnSpy = vi.fn().mockResolvedValue([]); + await expect( + invokeCreateNodesOnVirtualFilesV2( + [`**/project.json`, createNodesFnSpy], + createNodesContextV2(), + {}, + { matchingFilesData: {} }, + ), + ).resolves.toStrictEqual({}); + expect(createNodesFnSpy).toHaveBeenCalledTimes(1); + expect(createNodesFnSpy).toHaveBeenCalledWith([], {}, expect.any(Object)); + }); }); }); diff --git a/testing/test-nx-utils/src/lib/utils/nx.ts b/testing/test-nx-utils/src/lib/utils/nx.ts index e6e700068..252df5913 100644 --- a/testing/test-nx-utils/src/lib/utils/nx.ts +++ b/testing/test-nx-utils/src/lib/utils/nx.ts @@ -34,16 +34,16 @@ export function executorContext< }; } -export async function generateWorkspaceAndProject( +export async function generateProject( + tree: Tree, options: | string | (Omit, 'name'> & { name: string; }), ) { - const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); const { name, ...normalizedOptions } = - typeof options === 'string' ? { name: options } : options; + typeof options === 'string' ? { name: options } : (options ?? {}); await libraryGenerator(tree, { name, directory: path.join('libs', name), @@ -56,6 +56,20 @@ export async function generateWorkspaceAndProject( projectNameAndRootFormat: 'as-provided', ...normalizedOptions, }); +} +export async function generateWorkspaceAndProject( + options?: + | string + | (Omit, 'name'> & { + name: string; + }), +) { + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + const { name, ...opts } = + typeof options === 'string' ? { name: options } : (options ?? {}); + if (name) { + await generateProject(tree, { ...opts, name }); + } return tree; }