Skip to content

feat: nx-plugin with v2 format #971

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions e2e/nx-plugin-e2e/tests/plugin-derive-config.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -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,
},
}),
);
});
});
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
2 changes: 2 additions & 0 deletions packages/nx-plugin/README.md
Original file line number Diff line number Diff line change
@@ -66,3 +66,5 @@ Examples:

- `nx run <project-name>:code-pushup`
- `nx run <project-name>: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).
40 changes: 35 additions & 5 deletions packages/nx-plugin/src/plugin/plugin.ts
Original file line number Diff line number Diff line change
@@ -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 <v20
/** 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,
),
];
233 changes: 127 additions & 106 deletions packages/nx-plugin/src/plugin/plugin.unit.test.ts
Original file line number Diff line number Diff line change
@@ -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',
},
},
},
},
},
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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';
12 changes: 9 additions & 3 deletions packages/nx-plugin/src/plugin/types.ts
Original file line number Diff line number Diff line change
@@ -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;
13 changes: 10 additions & 3 deletions packages/nx-plugin/src/plugin/utils.ts
Original file line number Diff line number Diff line change
@@ -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<NormalizedCreateNodesContext> {
@@ -42,7 +49,7 @@ export async function normalizedCreateNodesV2Context(
context: CreateNodesContextV2,
projectConfigurationFile: string,
createOptions: CreateNodesOptions = {},
): Promise<NormalizedCreateNodesV2Context> {
): Promise<NormalizedCreateNodesContextV2> {
const projectRoot = path.dirname(projectConfigurationFile);

try {
16 changes: 8 additions & 8 deletions packages/nx-plugin/src/plugin/utils.unit.test.ts
Original file line number Diff line number Diff line change
@@ -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(
41 changes: 31 additions & 10 deletions testing/test-nx-utils/src/lib/utils/nx-plugin.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> | 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<string, unknown> | undefined,
>(
createNodes: CreateNodesV2,
context: CreateNodesContextV2,
createNodeOptions: T,
mockData: {
matchingFilesData: Record<string, string>;
},
) {
const { matchingFilesData } = mockData;
vol.fromJSON(matchingFilesData, MEMFS_VOLUME);

const files = Object.keys(matchingFilesData);

const results = await createNodes[1](files, createNodeOptions, context);

const result: NonNullable<CreateNodesResult['projects']> = {};
return results.reduce(
(acc, [_, { projects }]) => ({ ...acc, ...projects }),
result,
);
}

export function createNodesContextV1(
options?: Partial<CreateNodesContext>,
): 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>,
): CreateNodesContextV2 {
const { workspaceRoot = process.cwd(), nxJsonConfiguration = {} } =
174 changes: 126 additions & 48 deletions testing/test-nx-utils/src/lib/utils/nx-plugin.unit.test.ts
Original file line number Diff line number Diff line change
@@ -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));
});
});
});
20 changes: 17 additions & 3 deletions testing/test-nx-utils/src/lib/utils/nx.ts
Original file line number Diff line number Diff line change
@@ -34,16 +34,16 @@ export function executorContext<
};
}

export async function generateWorkspaceAndProject(
export async function generateProject(
tree: Tree,
options:
| string
| (Omit<Partial<LibraryGeneratorSchema>, '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<Partial<LibraryGeneratorSchema>, '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;
}