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..f554fa1bc --- /dev/null +++ b/e2e/nx-plugin-e2e/tests/plugin-derive-config.e2e.test.ts @@ -0,0 +1,167 @@ +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 } 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/packages/nx-plugin/src/plugin/plugin.ts b/packages/nx-plugin/src/plugin/plugin.ts index 9129f1bd7..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 +/** Create the nodes for a V1 Plugin. The name `createNodes` is required by Nx in order to be picked up as a plugin. */ export const createNodes: CreateNodes = [ `**/${PROJECT_JSON_FILE_NAME}`, async ( @@ -32,3 +34,31 @@ export const createNodes: CreateNodes = [ }; }, ]; + +/** Create the nodes for a V2 Plugin. The name `createNodesV2` is required by Nx in order to be picked up as a plugin. */ +export const createNodesV2: CreateNodesV2 = [ + `**/${PROJECT_JSON_FILE_NAME}`, + async (configFiles, options, context) => + 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 c51ebf570..cc8c77d0e 100644 --- a/packages/nx-plugin/src/plugin/plugin.unit.test.ts +++ b/packages/nx-plugin/src/plugin/plugin.unit.test.ts @@ -1,138 +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: '', - }; - }); + 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 5e8d59db7..623537fbe 100644 --- a/packages/nx-plugin/src/plugin/types.ts +++ b/packages/nx-plugin/src/plugin/types.ts @@ -1,4 +1,8 @@ -import type { CreateNodesContext, ProjectConfiguration } from '@nx/devkit'; +import type { + CreateNodesContext, + CreateNodesContextV2, + ProjectConfiguration, +} from '@nx/devkit'; import type { WithRequired } from '@code-pushup/utils'; import type { DynamicTargetOptions } from '../internal/types.js'; @@ -13,7 +17,16 @@ export type ProjectConfigurationWithName = WithRequired< 'name' >; -export type NormalizedCreateNodesContext = CreateNodesContext & { +export type NormalizedCreateNodesContext = ( + | CreateNodesContext + | CreateNodesContextV2 +) & { + projectJson: ProjectConfigurationWithName; + projectRoot: string; + createOptions: CreateNodesOptions; +}; + +export type NormalizedCreateNodesContextV2 = CreateNodesContextV2 & { projectJson: ProjectConfigurationWithName; projectRoot: string; createOptions: CreateNodesOptions; diff --git a/packages/nx-plugin/src/plugin/utils.ts b/packages/nx-plugin/src/plugin/utils.ts index e7a819f8d..17376cec8 100644 --- a/packages/nx-plugin/src/plugin/utils.ts +++ b/packages/nx-plugin/src/plugin/utils.ts @@ -1,4 +1,4 @@ -import type { CreateNodesContext } from '@nx/devkit'; +import type { CreateNodesContext, CreateNodesContextV2 } from '@nx/devkit'; import { readFile } from 'node:fs/promises'; import * as path from 'node:path'; import { CP_TARGET_NAME } from './constants.js'; @@ -8,8 +8,15 @@ import type { 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 { 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 30d9706ba..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,7 +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 = {} } = + options ?? {}; + return { + workspaceRoot, + nxJsonConfiguration, + configFiles: [], + }; +} + +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 68a9d5daa..5c92eae0d 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,61 +1,143 @@ import * as 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: [] }, +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: [], + }); }); - expect(context).toEqual({ - workspaceRoot: 'root', - nxJsonConfiguration: { plugins: [] }, + + it('should return a context with defaults', () => { + const context = createNodesContextV1(); + expect(context).toEqual({ + workspaceRoot: process.cwd(), + nxJsonConfiguration: {}, + configFiles: [], + }); }); }); - it('should return a context with defaults', () => { - const context = createNodesContext(); - expect(context).toEqual({ - workspaceRoot: process.cwd(), - nxJsonConfiguration: {}, + 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({ '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('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', - }), - }, - }, - ), - ).resolves.toStrictEqual({}); - expect(createNodesFnSpy).toHaveBeenCalledTimes(1); +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; }