diff --git a/.gitignore b/.gitignore
index 73a946634..9f8744370 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,4 +47,5 @@ Thumbs.db
**/.code-pushup
# Nx workspace cache
-.nx
+.nx/cache
+.nx/workspace-data
diff --git a/e2e/nx-plugin-nx18-e2e/.eslintrc.json b/e2e/nx-plugin-nx18-e2e/.eslintrc.json
new file mode 100644
index 000000000..fef34b026
--- /dev/null
+++ b/e2e/nx-plugin-nx18-e2e/.eslintrc.json
@@ -0,0 +1,12 @@
+{
+ "extends": ["../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*", "code-pushup.config*.ts"],
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx"],
+ "parserOptions": {
+ "project": ["e2e/nx-plugin-nx18-e2e/tsconfig.*?.json"]
+ }
+ }
+ ]
+}
diff --git a/e2e/nx-plugin-nx18-e2e/project.json b/e2e/nx-plugin-nx18-e2e/project.json
new file mode 100644
index 000000000..d89a68475
--- /dev/null
+++ b/e2e/nx-plugin-nx18-e2e/project.json
@@ -0,0 +1,23 @@
+{
+ "name": "nx-plugin-nx18-e2e",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "e2e/nx-plugin-nx18-e2e/src",
+ "projectType": "application",
+ "targets": {
+ "lint": {
+ "executor": "@nx/linter:eslint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": ["e2e/nx-plugin-nx18-e2e/**/*.ts"]
+ }
+ },
+ "e2e": {
+ "executor": "@nx/vite:test",
+ "options": {
+ "configFile": "e2e/nx-plugin-nx18-e2e/vite.config.e2e.ts"
+ }
+ }
+ },
+ "implicitDependencies": ["test-utils", "nx-plugin"],
+ "tags": ["scope:tooling", "type:e2e"]
+}
diff --git a/e2e/nx-plugin-nx18-e2e/tests/plugin-create-nodes.e2e.test.ts b/e2e/nx-plugin-nx18-e2e/tests/plugin-create-nodes.e2e.test.ts
new file mode 100644
index 000000000..c5c74629f
--- /dev/null
+++ b/e2e/nx-plugin-nx18-e2e/tests/plugin-create-nodes.e2e.test.ts
@@ -0,0 +1,57 @@
+import { join } from 'node:path';
+import { afterEach, expect } from 'vitest';
+import {
+ nxShowProjectJson,
+ registerPluginInNxJson,
+} from '@code-pushup/test-nx-utils';
+import { teardownTestFolder } from '@code-pushup/test-setup';
+import { executeProcess } from '@code-pushup/utils';
+
+describe('nx-plugin-nx18', () => {
+ const project = 'my-lib';
+ const envRoot = 'tmp/e2e/nx-plugin-nx18-e2e';
+ const baseDir = join(envRoot, '__test__/plugin/create-nodes');
+
+ beforeEach(async () => {
+ await executeProcess({
+ command: 'npx',
+ args: [
+ '--yes',
+ 'create-nx-workspace@18.3.5',
+ '__test__/plugin/create-nodes/nxv18',
+ '--preset=apps',
+ '--appName=my-app',
+ '--style=none',
+ '--packageManager=npm',
+ '--interactive=false',
+ '--ci=skip',
+ ],
+ cwd: envRoot,
+ });
+ await registerPluginInNxJson(
+ join(envRoot, 'code-pushup.config.ts'),
+ '@code-pushup/nx-plugin',
+ );
+ });
+
+ afterEach(async () => {
+ await teardownTestFolder(baseDir);
+ });
+
+ it('should add configuration target dynamically in nx18', async () => {
+ const { code, projectJson } = await nxShowProjectJson(envRoot, project);
+ expect(code).toBe(0);
+
+ expect(projectJson.targets).toStrictEqual({
+ ['code-pushup--configuration']: {
+ configurations: {},
+ executor: 'nx:run-commands',
+ options: {
+ command: `nx g @code-pushup/nx-plugin:configuration --skipTarget --targetName="code-pushup" --project="${project}"`,
+ },
+ },
+ });
+
+ expect(projectJson.targets).toMatchSnapshot();
+ });
+}, 300000);
diff --git a/e2e/nx-plugin-nx18-e2e/tsconfig.json b/e2e/nx-plugin-nx18-e2e/tsconfig.json
new file mode 100644
index 000000000..f5a2f890a
--- /dev/null
+++ b/e2e/nx-plugin-nx18-e2e/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "module": "ESNext",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "types": ["vitest"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.test.json"
+ }
+ ]
+}
diff --git a/e2e/nx-plugin-nx18-e2e/tsconfig.test.json b/e2e/nx-plugin-nx18-e2e/tsconfig.test.json
new file mode 100644
index 000000000..cc6383cf6
--- /dev/null
+++ b/e2e/nx-plugin-nx18-e2e/tsconfig.test.json
@@ -0,0 +1,13 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"]
+ },
+ "include": [
+ "vite.config.e2e.ts",
+ "tests/**/*.e2e.test.ts",
+ "tests/**/*.d.ts",
+ "mocks/**/*.ts"
+ ]
+}
diff --git a/e2e/nx-plugin-nx18-e2e/vite.config.e2e.ts b/e2e/nx-plugin-nx18-e2e/vite.config.e2e.ts
new file mode 100644
index 000000000..368115ffd
--- /dev/null
+++ b/e2e/nx-plugin-nx18-e2e/vite.config.e2e.ts
@@ -0,0 +1,21 @@
+///
+import { defineConfig } from 'vite';
+import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases';
+
+export default defineConfig({
+ cacheDir: '../../node_modules/.vite/nx-plugin-nx18-e2e',
+ test: {
+ reporters: ['basic'],
+ testTimeout: 160_000,
+ globals: true,
+ alias: tsconfigPathAliases(),
+ pool: 'threads',
+ poolOptions: { threads: { singleThread: true } },
+ cache: {
+ dir: '../../node_modules/.vitest',
+ },
+ environment: 'node',
+ include: ['tests/**/*.e2e.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+ setupFiles: ['../../testing/test-setup/src/lib/reset.mocks.ts'],
+ },
+});
diff --git a/packages/nx-plugin/src/executors/cli/utils.unit.test.ts b/packages/nx-plugin/src/executors/cli/utils.unit.test.ts
index 19da42b17..84ed39aad 100644
--- a/packages/nx-plugin/src/executors/cli/utils.unit.test.ts
+++ b/packages/nx-plugin/src/executors/cli/utils.unit.test.ts
@@ -92,7 +92,7 @@ describe('parseAutorunExecutorOptions', () => {
}),
);
- expect(osAgnosticPath(executorOptions.persist?.outputDir)).toBe(
+ expect(osAgnosticPath(executorOptions?.persist?.outputDir)).toBe(
osAgnosticPath('workspaceRoot/.code-pushup/my-app'),
);
});
diff --git a/packages/nx-plugin/src/plugin/caching.ts b/packages/nx-plugin/src/plugin/caching.ts
new file mode 100644
index 000000000..a1b33fbfc
--- /dev/null
+++ b/packages/nx-plugin/src/plugin/caching.ts
@@ -0,0 +1,50 @@
+import {
+ type ProjectConfiguration,
+ readJsonFile,
+ writeJsonFile,
+} from '@nx/devkit';
+import { existsSync } from 'node:fs';
+import { hashObject } from 'nx/src/hasher/file-hasher';
+
+export function cacheKey(prefix: string, hashData: Record) {
+ return `${prefix}-${hashObject(hashData)}`;
+}
+
+export function getCacheRecord(
+ targetsCache: Record,
+ prefix: string,
+ hashData: Record,
+) {
+ const targetCacheKey = cacheKey(prefix, hashData);
+
+ if (targetsCache[targetCacheKey]) {
+ return targetsCache[targetCacheKey];
+ }
+ return undefined;
+}
+
+export function setCacheRecord(
+ targetsCache: Record,
+ prefix: string,
+ hashData: Record,
+ cacheData: T,
+) {
+ const targetCacheKey = cacheKey(prefix, hashData);
+
+ return (targetsCache[targetCacheKey] = cacheData);
+}
+
+export function readTargetsCache(
+ cachePath: string,
+): Record> {
+ return process.env.NX_CACHE_PROJECT_GRAPH !== 'false' && existsSync(cachePath)
+ ? readJsonFile(cachePath)
+ : {};
+}
+
+export function writeTargetsToCache(
+ cachePath: string,
+ results: Record>,
+) {
+ writeJsonFile(cachePath, results);
+}
diff --git a/packages/nx-plugin/src/plugin/caching.unit.test.ts b/packages/nx-plugin/src/plugin/caching.unit.test.ts
new file mode 100644
index 000000000..4ef120a02
--- /dev/null
+++ b/packages/nx-plugin/src/plugin/caching.unit.test.ts
@@ -0,0 +1,119 @@
+import * as hasher from 'nx/src/hasher/file-hasher';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { cacheKey, getCacheRecord, setCacheRecord } from './caching';
+
+describe('cacheKey', () => {
+ let hashObjectSpy;
+
+ beforeEach(() => {
+ hashObjectSpy = vi
+ .spyOn(hasher, 'hashObject')
+ .mockImplementation(() => '42');
+ });
+
+ afterEach(() => {
+ hashObjectSpy.mockClear();
+ });
+
+ it('should start with provided prefix', () => {
+ expect(cacheKey('verdaccio', {} as Record)).toMatch(
+ /^verdaccio-/,
+ );
+ });
+
+ it('should use hashObject to generate hash', () => {
+ expect(cacheKey('x', { prop: 42 } as Record)).toMatch(
+ /[0-9]*$/,
+ );
+ expect(hashObjectSpy).toHaveBeenCalledTimes(1);
+ expect(hashObjectSpy).toHaveBeenCalledWith({ prop: 42 });
+ });
+
+ it('should generate correct hash for empty object', () => {
+ expect(cacheKey('x', { prop: 42 } as Record)).toBe('x-42');
+ expect(hashObjectSpy).toHaveBeenCalledTimes(1);
+ expect(hashObjectSpy).toHaveBeenCalledWith({ prop: 42 });
+ });
+});
+
+describe('getCacheRecord', () => {
+ let hashObjectSpy;
+
+ beforeEach(() => {
+ hashObjectSpy = vi
+ .spyOn(hasher, 'hashObject')
+ .mockImplementation(() => '42');
+ });
+
+ afterEach(() => {
+ hashObjectSpy.mockClear();
+ });
+
+ it('should get cached data if given', () => {
+ const prefix = 'verdaccio';
+ const targetsCache = {
+ 'verdaccio-42': 'cacheData',
+ };
+ const hashData = { prop: 42 };
+
+ expect(getCacheRecord(targetsCache, prefix, hashData)).toBe('cacheData');
+ });
+
+ it('should return undefined if no cache hit', () => {
+ const targetsCache = {};
+ const prefix = 'verdaccio';
+ const hashData = { prop: 43 };
+ expect(getCacheRecord(targetsCache, prefix, hashData)).toBe(undefined);
+ });
+
+ it('should call cacheKey and hashObject', () => {
+ const targetsCache = {
+ 'verdaccio-42': 'cacheData',
+ };
+ const prefix = 'verdaccio';
+ const hashData = { prop: 42 };
+ const hashObjectSpy = vi.spyOn(hasher, 'hashObject');
+ getCacheRecord(targetsCache, prefix, hashData);
+ expect(hashObjectSpy).toHaveBeenCalledTimes(1);
+ expect(hashObjectSpy).toHaveBeenCalledWith({ prop: 42 });
+ });
+});
+
+describe('setCacheRecord', () => {
+ let hashObjectSpy;
+
+ beforeEach(() => {
+ hashObjectSpy = vi
+ .spyOn(hasher, 'hashObject')
+ .mockImplementation(() => '42');
+ });
+
+ afterEach(() => {
+ hashObjectSpy.mockClear();
+ });
+
+ it('should set cached data if given', () => {
+ const prefix = 'verdaccio';
+ const targetsCache = {};
+ const hashData = { prop: 42 };
+ expect(getCacheRecord(targetsCache, prefix, hashData)).toStrictEqual(
+ undefined,
+ );
+ expect(
+ setCacheRecord(targetsCache, prefix, hashData, { test: 41 }),
+ ).not.toThrowError();
+ expect(getCacheRecord(targetsCache, prefix, hashData)).toStrictEqual({
+ test: 41,
+ });
+ });
+
+ it('should return cached data after setting', () => {
+ const prefix = 'verdaccio';
+ const targetsCache = {};
+ const hashData = { prop: 42 };
+
+ expect(
+ setCacheRecord(targetsCache, prefix, hashData, { test: 41 }),
+ ).toStrictEqual({ test: 41 });
+ });
+});
diff --git a/packages/nx-plugin/src/plugin/constants.ts b/packages/nx-plugin/src/plugin/constants.ts
index bf2e81d9f..2b117219b 100644
--- a/packages/nx-plugin/src/plugin/constants.ts
+++ b/packages/nx-plugin/src/plugin/constants.ts
@@ -1 +1,2 @@
+export const PLUGIN_NAME = '@code-pushup/nx-plugin';
export const CP_TARGET_NAME = 'code-pushup';
diff --git a/packages/nx-plugin/src/plugin/plugin.ts b/packages/nx-plugin/src/plugin/plugin.ts
index 9129f1bd7..bb0f09846 100644
--- a/packages/nx-plugin/src/plugin/plugin.ts
+++ b/packages/nx-plugin/src/plugin/plugin.ts
@@ -3,32 +3,36 @@ import type {
CreateNodesContext,
CreateNodesResult,
} 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';
+import { normalizeCreateNodesOptions } from '@push-based/nx-verdaccio/src/plugin/normalize-create-nodes-options';
+import { createProjectConfiguration } from '@push-based/nx-verdaccio/src/plugin/targets/create-targets';
+import { PROJECT_JSON_FILE_NAME } from '../internal/constants';
+
+type FileMatcher = `${string}${typeof PROJECT_JSON_FILE_NAME}`;
+const PROJECT_JSON_FILE_GLOB = `**/${PROJECT_JSON_FILE_NAME}` as FileMatcher;
// name has to be "createNodes" to get picked up by Nx
-export const createNodes: CreateNodes = [
- `**/${PROJECT_JSON_FILE_NAME}`,
- async (
- projectConfigurationFile: string,
- createNodesOptions: unknown,
- context: CreateNodesContext,
- ): Promise => {
- const parsedCreateNodesOptions = createNodesOptions as CreateNodesOptions;
- const normalizedContext = await normalizedCreateNodesContext(
- context,
- projectConfigurationFile,
- parsedCreateNodesOptions,
- );
+export const createNodes = [
+ PROJECT_JSON_FILE_GLOB,
+ createNodesV1Fn,
+] satisfies CreateNodes;
+
+export async function createNodesV1Fn(
+ projectConfigurationFile: string,
+ createNodesOptions: unknown,
+ _: CreateNodesContext,
+): Promise {
+ const projectJson = await loadProjectConfiguration(projectConfigurationFile);
+ const createOptions = normalizeCreateNodesOptions(createNodesOptions);
- return {
- projects: {
- [normalizedContext.projectRoot]: {
- targets: await createTargets(normalizedContext),
- },
+ const { targets } = await createProjectConfiguration(
+ projectJson,
+ createOptions,
+ );
+ return {
+ projects: {
+ [projectJson.root]: {
+ targets,
},
- };
- },
-];
+ },
+ };
+}
diff --git a/packages/nx-plugin/src/plugin/plugin.unit.test.ts b/packages/nx-plugin/src/plugin/plugin.unit.test.ts
index c51ebf570..db5f14a3c 100644
--- a/packages/nx-plugin/src/plugin/plugin.unit.test.ts
+++ b/packages/nx-plugin/src/plugin/plugin.unit.test.ts
@@ -1,45 +1,62 @@
import type { CreateNodesContext } from '@nx/devkit';
import { vol } from 'memfs';
+import { join } from 'node:path';
import { describe, expect } from 'vitest';
-import { invokeCreateNodesOnVirtualFiles } from '@code-pushup/test-nx-utils';
+import { MEMFS_VOLUME } from '@code-pushup/test-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 { createNodesV1Fn } from './plugin';
-describe('@code-pushup/nx-plugin/plugin', () => {
- let context: CreateNodesContext;
+describe('createNodesV1Fn', () => {
+ const context: CreateNodesContext = {
+ nxJsonConfiguration: {},
+ workspaceRoot: '',
+ };
- beforeEach(() => {
- context = {
- nxJsonConfiguration: {},
- workspaceRoot: '',
- };
- });
+ it('should normalize context of project.json with missing root property', async () => {
+ vol.fromJSON(
+ {
+ [PROJECT_JSON_FILE_NAME]: `${JSON.stringify({
+ name: '@org/empty-root',
+ })}`,
+ },
+ MEMFS_VOLUME,
+ );
- afterEach(() => {
- vol.reset();
+ await expect(
+ createNodesV1Fn(PROJECT_JSON_FILE_NAME, {}, context),
+ ).resolves.toStrictEqual({
+ projects: {
+ ['.']: {
+ 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 ROOT project', async () => {
- const projectRoot = '.';
- const matchingFilesData = {
- [`${projectRoot}/${PROJECT_JSON_FILE_NAME}`]: `${JSON.stringify({
- name: '@org/empty-root',
- })}`,
- };
+ vol.fromJSON(
+ {
+ [PROJECT_JSON_FILE_NAME]: `${JSON.stringify({
+ name: '@org/empty-root',
+ })}`,
+ },
+ MEMFS_VOLUME,
+ );
await expect(
- invokeCreateNodesOnVirtualFiles(
- createNodes,
- context,
- {},
- { matchingFilesData },
- ),
+ createNodesV1Fn(PROJECT_JSON_FILE_NAME, {}, context),
).resolves.toStrictEqual({
- [projectRoot]: {
- targets: {
- [`${CP_TARGET_NAME}--configuration`]: {
- command: `nx g ${PACKAGE_NAME}:configuration --skipTarget --targetName="code-pushup" --project="@org/empty-root"`,
+ projects: {
+ ['.']: {
+ targets: {
+ [`${CP_TARGET_NAME}--configuration`]: {
+ command: `nx g ${PACKAGE_NAME}:configuration --skipTarget --targetName="code-pushup" --project="@org/empty-root"`,
+ },
},
},
},
@@ -48,24 +65,25 @@ describe('@code-pushup/nx-plugin/plugin', () => {
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',
- })}`,
- };
+ vol.fromJSON(
+ {
+ [join(projectRoot, PROJECT_JSON_FILE_NAME)]: `${JSON.stringify({
+ root: projectRoot,
+ name: '@org/empty-root',
+ })}`,
+ },
+ MEMFS_VOLUME,
+ );
await expect(
- invokeCreateNodesOnVirtualFiles(
- createNodes,
- context,
- {},
- { matchingFilesData },
- ),
+ createNodesV1Fn(join(projectRoot, PROJECT_JSON_FILE_NAME), {}, context),
).resolves.toStrictEqual({
- [projectRoot]: {
- targets: {
- [`${CP_TARGET_NAME}--configuration`]: {
- command: `nx g ${PACKAGE_NAME}:configuration --skipTarget --targetName="code-pushup" --project="@org/empty-root"`,
+ projects: {
+ [projectRoot]: {
+ targets: {
+ [`${CP_TARGET_NAME}--configuration`]: {
+ command: `nx g ${PACKAGE_NAME}:configuration --skipTarget --targetName="code-pushup" --project="@org/empty-root"`,
+ },
},
},
},
@@ -73,30 +91,34 @@ describe('@code-pushup/nx-plugin/plugin', () => {
});
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`]: '{}',
- };
+ vol.fromJSON(
+ {
+ [PROJECT_JSON_FILE_NAME]: `${JSON.stringify({
+ root: '.',
+ name: '@org/empty-root',
+ })}`,
+ ['code-pushup.config.ts']: '{}',
+ },
+ MEMFS_VOLUME,
+ );
await expect(
- invokeCreateNodesOnVirtualFiles(
- createNodes,
- context,
+ createNodesV1Fn(
+ PROJECT_JSON_FILE_NAME,
{
projectPrefix: 'cli',
},
- { matchingFilesData },
+ context,
),
).resolves.toStrictEqual({
- [projectRoot]: {
- targets: {
- [CP_TARGET_NAME]: {
- executor: `${PACKAGE_NAME}:cli`,
- options: {
- projectPrefix: 'cli',
+ projects: {
+ ['.']: {
+ targets: {
+ [CP_TARGET_NAME]: {
+ executor: `${PACKAGE_NAME}:cli`,
+ options: {
+ projectPrefix: 'cli',
+ },
},
},
},
@@ -106,29 +128,34 @@ describe('@code-pushup/nx-plugin/plugin', () => {
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`]: '{}',
- };
+ vol.fromJSON(
+ {
+ [join(projectRoot, PROJECT_JSON_FILE_NAME)]: `${JSON.stringify({
+ root: projectRoot,
+ name: '@org/empty-root',
+ })}`,
+ [join(projectRoot, 'code-pushup.config.ts')]: '{}',
+ },
+ MEMFS_VOLUME,
+ );
await expect(
- invokeCreateNodesOnVirtualFiles(
- createNodes,
- context,
+ createNodesV1Fn(
+ join(projectRoot, PROJECT_JSON_FILE_NAME),
{
projectPrefix: 'cli',
},
- { matchingFilesData },
+ context,
),
).resolves.toStrictEqual({
- [projectRoot]: {
- targets: {
- [CP_TARGET_NAME]: {
- executor: `${PACKAGE_NAME}:cli`,
- options: {
- projectPrefix: 'cli',
+ projects: {
+ [projectRoot]: {
+ targets: {
+ [CP_TARGET_NAME]: {
+ executor: `${PACKAGE_NAME}:cli`,
+ options: {
+ projectPrefix: 'cli',
+ },
},
},
},
diff --git a/packages/nx-plugin/src/plugin/target/targets.ts b/packages/nx-plugin/src/plugin/target/targets.ts
index 3e659688b..a0d882965 100644
--- a/packages/nx-plugin/src/plugin/target/targets.ts
+++ b/packages/nx-plugin/src/plugin/target/targets.ts
@@ -1,19 +1,17 @@
+import type { ProjectConfiguration } from '@nx/devkit';
import { readdir } from 'node:fs/promises';
-import { CP_TARGET_NAME } from '../constants.js';
-import type { NormalizedCreateNodesContext } from '../types.js';
-import { createConfigurationTarget } from './configuration-target.js';
-import { CODE_PUSHUP_CONFIG_REGEX } from './constants.js';
-import { createExecutorTarget } from './executor-target.js';
+import { CP_TARGET_NAME } from '../constants';
+import type { NormalizedCreateNodesOptions } from '../types';
+import { createConfigurationTarget } from './configuration-target';
+import { CODE_PUSHUP_CONFIG_REGEX } from './constants';
+import { createExecutorTarget } from './executor-target';
export async function createTargets(
- normalizedContext: NormalizedCreateNodesContext,
+ projectConfig: ProjectConfiguration,
+ options: NormalizedCreateNodesOptions,
) {
- const {
- targetName = CP_TARGET_NAME,
- bin,
- projectPrefix,
- } = normalizedContext.createOptions;
- const rootFiles = await readdir(normalizedContext.projectRoot);
+ const { targetName = CP_TARGET_NAME, bin, projectPrefix } = options;
+ const rootFiles = await readdir(projectConfig.root);
return rootFiles.some(filename => filename.match(CODE_PUSHUP_CONFIG_REGEX))
? {
[targetName]: createExecutorTarget({ bin, projectPrefix }),
@@ -22,7 +20,7 @@ export async function createTargets(
{
[`${targetName}--configuration`]: createConfigurationTarget({
targetName,
- projectName: normalizedContext.projectJson.name,
+ projectName: projectConfig.name,
bin,
}),
};
diff --git a/packages/nx-plugin/src/plugin/target/targets.unit.test.ts b/packages/nx-plugin/src/plugin/target/targets.unit.test.ts
index 9b730f726..781d90b0c 100644
--- a/packages/nx-plugin/src/plugin/target/targets.unit.test.ts
+++ b/packages/nx-plugin/src/plugin/target/targets.unit.test.ts
@@ -1,13 +1,19 @@
+import type { ProjectConfiguration } from '@nx/devkit';
import { vol } from 'memfs';
import { rm } from 'node:fs/promises';
import { afterEach, beforeEach, expect } from 'vitest';
import { MEMFS_VOLUME } from '@code-pushup/test-utils';
import { DEFAULT_TARGET_NAME, PACKAGE_NAME } from '../../internal/constants.js';
import { CP_TARGET_NAME } from '../constants.js';
-import type { NormalizedCreateNodesContext } from '../types.js';
+import type { NormalizedCreateNodesOptions } from '../types.js';
import { createTargets } from './targets.js';
describe('createTargets', () => {
+ const projectName = 'plugin-my-plugin';
+ const projectConfig = {
+ root: '.',
+ name: projectName,
+ } as ProjectConfiguration;
beforeEach(async () => {
// needed to have the folder present. readdir otherwise it fails
vol.fromJSON(
@@ -24,15 +30,8 @@ describe('createTargets', () => {
});
it('should return configuration targets for project without code-pushup config', async () => {
- const projectName = 'plugin-my-plugin';
await expect(
- createTargets({
- projectRoot: '.',
- projectJson: {
- name: projectName,
- },
- createOptions: {},
- } as NormalizedCreateNodesContext),
+ createTargets(projectConfig, {} as NormalizedCreateNodesOptions),
).resolves.toStrictEqual({
[`${CP_TARGET_NAME}--configuration`]: {
command: `nx g ${PACKAGE_NAME}:configuration --skipTarget --targetName="code-pushup" --project="${projectName}"`,
@@ -41,27 +40,17 @@ describe('createTargets', () => {
});
it('should return configuration targets for empty project without code-pushup config and consider targetName', async () => {
- const projectName = 'plugin-my-plugin';
const targetName = 'cp';
await expect(
- createTargets({
- projectRoot: '.',
- projectJson: {
- name: projectName,
- },
- createOptions: {
- targetName,
- },
- } as NormalizedCreateNodesContext),
+ createTargets(projectConfig, { targetName }),
).resolves.toStrictEqual({
[`${targetName}--configuration`]: {
- command: `nx g ${PACKAGE_NAME}:configuration --skipTarget --targetName="cp" --project="${projectName}"`,
+ command: `nx g ${PACKAGE_NAME}:configuration --skipTarget --targetName="${targetName}" --project="${projectName}"`,
},
});
});
it('should NOT return configuration target if code-pushup config is given', async () => {
- const projectName = 'plugin-my-plugin';
vol.fromJSON(
{
[`code-pushup.config.ts`]: `{}`,
@@ -70,15 +59,7 @@ describe('createTargets', () => {
);
const targetName = 'cp';
await expect(
- createTargets({
- projectRoot: '.',
- projectJson: {
- name: projectName,
- },
- createOptions: {
- targetName,
- },
- } as NormalizedCreateNodesContext),
+ createTargets(projectConfig, { targetName }),
).resolves.toStrictEqual(
expect.not.objectContaining({
[`${targetName}--configuration`]: expect.any(Object),
@@ -87,7 +68,6 @@ describe('createTargets', () => {
});
it('should return executor target if code-pushup config is given', async () => {
- const projectName = 'plugin-my-plugin';
vol.fromJSON(
{
[`code-pushup.config.ts`]: `{}`,
@@ -96,15 +76,7 @@ describe('createTargets', () => {
);
const targetName = 'cp';
await expect(
- createTargets({
- projectRoot: '.',
- projectJson: {
- name: projectName,
- },
- createOptions: {
- targetName,
- },
- } as NormalizedCreateNodesContext),
+ createTargets(projectConfig, { targetName }),
).resolves.toStrictEqual(
expect.objectContaining({
[targetName]: {
@@ -115,7 +87,6 @@ describe('createTargets', () => {
});
it('should return executor targets for project if configured', async () => {
- const projectName = 'plugin-my-plugin';
vol.fromJSON(
{
[`code-pushup.config.ts`]: `{}`,
@@ -123,13 +94,7 @@ describe('createTargets', () => {
MEMFS_VOLUME,
);
await expect(
- createTargets({
- projectRoot: '.',
- projectJson: {
- name: projectName,
- },
- createOptions: {},
- } as NormalizedCreateNodesContext),
+ createTargets(projectConfig, {} as NormalizedCreateNodesOptions),
).resolves.toStrictEqual({
[DEFAULT_TARGET_NAME]: {
executor: '@code-pushup/nx-plugin:cli',
@@ -138,7 +103,6 @@ describe('createTargets', () => {
});
it('should return executor targets for configured project and use given targetName', async () => {
- const projectName = 'plugin-my-plugin';
vol.fromJSON(
{
[`code-pushup.config.ts`]: `{}`,
@@ -146,15 +110,7 @@ describe('createTargets', () => {
MEMFS_VOLUME,
);
await expect(
- createTargets({
- projectRoot: '.',
- projectJson: {
- name: projectName,
- },
- createOptions: {
- targetName: 'cp',
- },
- } as NormalizedCreateNodesContext),
+ createTargets(projectConfig, { targetName: 'cp' }),
).resolves.toStrictEqual({
cp: {
executor: '@code-pushup/nx-plugin:cli',
@@ -163,7 +119,6 @@ describe('createTargets', () => {
});
it('should include projectPrefix options in executor targets if given', async () => {
- const projectName = 'plugin-my-plugin';
vol.fromJSON(
{
[`code-pushup.config.ts`]: `{}`,
@@ -171,15 +126,9 @@ describe('createTargets', () => {
MEMFS_VOLUME,
);
await expect(
- createTargets({
- projectRoot: '.',
- projectJson: {
- name: projectName,
- },
- createOptions: {
- projectPrefix: 'cli',
- },
- } as NormalizedCreateNodesContext),
+ createTargets(projectConfig, {
+ projectPrefix: 'cli',
+ } as NormalizedCreateNodesOptions),
).resolves.toStrictEqual({
[DEFAULT_TARGET_NAME]: expect.objectContaining({
options: { projectPrefix: 'cli' },
diff --git a/packages/nx-plugin/src/plugin/types.ts b/packages/nx-plugin/src/plugin/types.ts
index 5e8d59db7..9d75dea8c 100644
--- a/packages/nx-plugin/src/plugin/types.ts
+++ b/packages/nx-plugin/src/plugin/types.ts
@@ -13,8 +13,13 @@ export type ProjectConfigurationWithName = WithRequired<
'name'
>;
+export type NormalizedCreateNodesOptions = Omit<
+ CreateNodesOptions,
+ 'targetName'
+> &
+ Required>;
export type NormalizedCreateNodesContext = CreateNodesContext & {
projectJson: ProjectConfigurationWithName;
projectRoot: string;
- createOptions: CreateNodesOptions;
+ createOptions: NormalizedCreateNodesOptions;
};
diff --git a/packages/nx-plugin/src/plugin/utils.ts b/packages/nx-plugin/src/plugin/utils.ts
index e7a819f8d..845ed3ad4 100644
--- a/packages/nx-plugin/src/plugin/utils.ts
+++ b/packages/nx-plugin/src/plugin/utils.ts
@@ -1,12 +1,11 @@
-import type { CreateNodesContext } from '@nx/devkit';
-import { readFile } from 'node:fs/promises';
+import type {ProjectConfiguration} from '@nx/devkit';
+import {readFile} from 'node:fs/promises';
import * as path from 'node:path';
-import { CP_TARGET_NAME } from './constants.js';
-import type {
- CreateNodesOptions,
- NormalizedCreateNodesContext,
- ProjectConfigurationWithName,
-} from './types.js';
+import {dirname, join} from 'node:path';
+import {createTargets} from './target/targets';
+import type {CreateNodesOptions, NormalizedCreateNodesOptions} from './types';
+import {CP_TARGET_NAME} from './constants.js';
+import type {CreateNodesOptions, NormalizedCreateNodesContext,} from './types.js';
export async function normalizedCreateNodesContext(
context: CreateNodesContext,
@@ -15,24 +14,45 @@ export async function normalizedCreateNodesContext(
): Promise {
const projectRoot = path.dirname(projectConfigurationFile);
- try {
- const projectJson = JSON.parse(
- (await readFile(projectConfigurationFile)).toString(),
- ) as ProjectConfigurationWithName;
+export function normalizeCreateNodesOptions(
+ options: unknown = {},
+): NormalizedCreateNodesOptions {
+ const { targetName = CP_TARGET_NAME } = options as CreateNodesOptions;
+ return {
+ ...(options as CreateNodesOptions),
+ targetName,
+ };
+}
- const { targetName = CP_TARGET_NAME } = createOptions;
- return {
- ...context,
- projectJson,
- projectRoot,
- createOptions: {
- ...createOptions,
- targetName,
- },
- };
- } catch {
- throw new Error(
- `Error parsing project.json file ${projectConfigurationFile}.`,
- );
+export async function loadProjectConfiguration(
+ projectConfigurationFile: string,
+): Promise {
+ const projectConfiguration = (await readFile(
+ join(process.cwd(), projectConfigurationFile),
+ 'utf8',
+ ).then(JSON.parse)) as Omit & { root?: string };
+ if (
+ !('name' in projectConfiguration) ||
+ typeof projectConfiguration.name !== 'string'
+ ) {
+ throw new Error('Project name is required');
}
+ return {
+ ...projectConfiguration,
+ root: projectConfiguration.root ?? dirname(projectConfigurationFile),
+ };
+}
+
+export async function createProjectConfiguration(
+ projectConfiguration: ProjectConfiguration,
+ options: CreateNodesOptions,
+): Promise<
+ Pick &
+ Partial>
+> {
+ const normalizeOptions = normalizeCreateNodesOptions(options);
+ return {
+ namedInputs: {},
+ targets: await createTargets(projectConfiguration, normalizeOptions),
+ };
}
diff --git a/packages/nx-plugin/src/plugin/utils.unit.test.ts b/packages/nx-plugin/src/plugin/utils.unit.test.ts
index edf2bf1cb..36c2a74de 100644
--- a/packages/nx-plugin/src/plugin/utils.unit.test.ts
+++ b/packages/nx-plugin/src/plugin/utils.unit.test.ts
@@ -1,160 +1,121 @@
import { vol } from 'memfs';
import { describe, expect } from 'vitest';
-import { createNodesContext } from '@code-pushup/test-nx-utils';
import { MEMFS_VOLUME } from '@code-pushup/test-utils';
-import { normalizedCreateNodesContext } from './utils.js';
+import * as targets from './target/targets';
+import {
+ createProjectConfiguration,
+ loadProjectConfiguration,
+ normalizeCreateNodesOptions,
+} from './utils';
-describe('normalizedCreateNodesContext', () => {
- it('should provide workspaceRoot', async () => {
- vol.fromJSON(
- {
- 'project.json': JSON.stringify({ name: 'my-project' }),
- },
- MEMFS_VOLUME,
- );
-
- await expect(
- normalizedCreateNodesContext(
- createNodesContext({ workspaceRoot: MEMFS_VOLUME }),
- 'project.json',
- ),
- ).resolves.toStrictEqual(
- expect.objectContaining({
- workspaceRoot: MEMFS_VOLUME,
- }),
- );
+describe('normalizeCreateNodesOptions', () => {
+ it('should provide default targetName in options', () => {
+ expect(normalizeCreateNodesOptions({})).toStrictEqual({
+ targetName: 'code-pushup',
+ });
});
- it('should provide projectRoot', async () => {
- vol.fromJSON(
- {
- 'packages/utils/project.json': JSON.stringify({
- name: 'my-project',
- }),
- },
- MEMFS_VOLUME,
- );
-
- await expect(
- normalizedCreateNodesContext(
- createNodesContext(),
- 'packages/utils/project.json',
- ),
- ).resolves.toStrictEqual(
- expect.objectContaining({
- projectRoot: 'packages/utils',
+ it('should use provided options', () => {
+ expect(
+ normalizeCreateNodesOptions({
+ targetName: 'cp',
+ projectPrefix: 'cli',
}),
- );
+ ).toStrictEqual({
+ targetName: 'cp',
+ projectPrefix: 'cli',
+ });
});
+});
- it('should provide nxJsonConfiguration', async () => {
+describe('loadProjectConfiguration', () => {
+ it('should load project configuration', async () => {
vol.fromJSON(
{
- 'project.json': JSON.stringify({
- name: 'my-project',
- }),
- },
- MEMFS_VOLUME,
- );
-
- await expect(
- normalizedCreateNodesContext(
- createNodesContext({
- nxJsonConfiguration: {
- workspaceLayout: {
- libsDir: 'libs',
- },
+ ['project.json']: JSON.stringify(
+ {
+ root: '.',
+ name: 'my-lib',
},
- }),
- 'project.json',
- ),
- ).resolves.toStrictEqual(
- expect.objectContaining({
- nxJsonConfiguration: {
- workspaceLayout: {
- libsDir: 'libs',
- },
- },
- }),
- );
- });
-
- it('should provide projectJson', async () => {
- vol.fromJSON(
- {
- 'project.json': JSON.stringify({
- name: 'my-project',
- }),
+ null,
+ 2,
+ ),
},
MEMFS_VOLUME,
);
-
await expect(
- normalizedCreateNodesContext(createNodesContext(), 'project.json'),
- ).resolves.toStrictEqual(
- expect.objectContaining({
- projectJson: {
- name: 'my-project',
- },
- }),
- );
+ loadProjectConfiguration('./project.json'),
+ ).resolves.toStrictEqual({
+ root: '.',
+ name: 'my-lib',
+ });
});
- it('should throw for empty project.json', async () => {
+ it('should load project configuration and provide fallback for root if not given', async () => {
vol.fromJSON(
{
- 'project.json': '',
+ ['packages/project.json']: JSON.stringify(
+ {
+ name: 'my-lib',
+ },
+ null,
+ 2,
+ ),
},
MEMFS_VOLUME,
);
-
await expect(
- normalizedCreateNodesContext(createNodesContext(), 'project.json'),
- ).rejects.toThrow('Error parsing project.json file project.json.');
+ loadProjectConfiguration('./packages/project.json'),
+ ).resolves.toStrictEqual({
+ root: './packages',
+ name: 'my-lib',
+ });
});
+});
- it('should provide default targetName in createOptions', async () => {
+describe('createProjectConfiguration', () => {
+ it('should create project configuration', async () => {
+ const root = '.';
vol.fromJSON(
{
- 'project.json': JSON.stringify({
- name: 'my-project',
- }),
+ ['project.json']: JSON.stringify(
+ {
+ root,
+ name: 'my-lib',
+ },
+ null,
+ 2,
+ ),
},
MEMFS_VOLUME,
);
await expect(
- normalizedCreateNodesContext(createNodesContext(), 'project.json'),
- ).resolves.toStrictEqual(
- expect.objectContaining({
- createOptions: {
- targetName: 'code-pushup',
+ createProjectConfiguration(
+ {
+ root,
+ name: 'my-lib',
},
- }),
- );
+ {},
+ ),
+ ).resolves.toStrictEqual({
+ namedInputs: {},
+ targets: expect.any(Object),
+ });
});
- it('should provide createOptions', async () => {
- vol.fromJSON(
- {
- 'project.json': JSON.stringify({
- name: 'my-project',
- }),
- },
- MEMFS_VOLUME,
- );
-
- await expect(
- normalizedCreateNodesContext(createNodesContext(), 'project.json', {
- projectPrefix: 'cli',
- }),
- ).resolves.toStrictEqual(
- expect.objectContaining({
- createOptions: {
- targetName: 'code-pushup',
- projectPrefix: 'cli',
- },
- }),
- );
+ it('should normalize options and pass project configuration and options to createTargets', async () => {
+ const createTargetsSpy = vi
+ .spyOn(targets, 'createTargets')
+ .mockResolvedValue({ proj: {} });
+ const projectCfg = {
+ root: '.',
+ name: 'my-lib',
+ };
+ await createProjectConfiguration(projectCfg, {});
+ expect(createTargetsSpy).toHaveBeenCalledTimes(1);
+ expect(createTargetsSpy).toHaveBeenCalledWith(projectCfg, {
+ targetName: 'code-pushup',
+ });
});
});
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..000cdecc9 100644
--- a/testing/test-nx-utils/src/lib/utils/nx-plugin.ts
+++ b/testing/test-nx-utils/src/lib/utils/nx-plugin.ts
@@ -1,59 +1,4 @@
-import type {
- CreateNodes,
- CreateNodesContext,
- CreateNodesContextV2,
- CreateNodesResult,
-} from '@nx/devkit';
-import { vol } from 'memfs';
-import { MEMFS_VOLUME } from '@code-pushup/test-utils';
-
-/**
- * Unit Testing helper for the createNodes function of a Nx plugin.
- * This function will create files over `memfs` from testCfg.matchingFilesData
- * and invoke the createNodes function on each of the files provided including potential createNodeOptions.
- * It will aggregate the results of each invocation and return the projects from CreateNodesResult.
- *
- * @example
- * ```ts
- * const projects = await createFilesAndInvokeCreateNodesOnThem(createNodes, context, undefined, { matchingFilesData});
- * // project should have one target created
- * const targets = projects[projectRoot]?.targets ?? {};
- * expect(Object.keys(targets)).toHaveLength(1);
- * // target should be the init target
- * expect(targets[`${CP_TARGET_NAME}--init`]).toBeDefined();
- * ```
- *
- * @param createNodes
- * @param context
- * @param createNodeOptions
- * @param mockData
- */
-export async function invokeCreateNodesOnVirtualFiles<
- 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,
- mockData: {
- matchingFilesData: Record;
- },
-) {
- const { matchingFilesData } = mockData;
- vol.fromJSON(matchingFilesData, MEMFS_VOLUME);
-
- const results = await Promise.all(
- Object.keys(matchingFilesData).map(file =>
- createNodes[1](file, createNodeOptions, context),
- ),
- );
-
- const result: NonNullable = {};
- return results.reduce(
- (acc, { projects }) => ({ ...acc, ...projects }),
- result,
- );
-}
+import type { CreateNodesContext } from '@nx/devkit';
export function createNodesContext(
options?: Partial,
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..d1f533304 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,5 +1,6 @@
import * as process from 'node:process';
import { describe, expect } from 'vitest';
+import { createNodesContext } from './nx-plugin';
import {
createNodesContext,
invokeCreateNodesOnVirtualFiles,
@@ -25,37 +26,3 @@ describe('createNodesContext', () => {
});
});
});
-
-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);
- });
-
- 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(),
- {},
- { matchingFilesData: {} },
- ),
- ).resolves.toStrictEqual({});
- expect(createNodesFnSpy).not.toHaveBeenCalled();
- });
-});
diff --git a/testing/test-nx-utils/src/lib/utils/nx.ts b/testing/test-nx-utils/src/lib/utils/nx.ts
index e6e700068..b38229f7f 100644
--- a/testing/test-nx-utils/src/lib/utils/nx.ts
+++ b/testing/test-nx-utils/src/lib/utils/nx.ts
@@ -4,10 +4,12 @@ import {
type PluginConfiguration,
type ProjectConfiguration,
type Tree,
+ readJson,
updateJson,
} from '@nx/devkit';
import { libraryGenerator } from '@nx/js';
import type { LibraryGeneratorSchema } from '@nx/js/src/utils/schema';
+import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { createTreeWithEmptyWorkspace } from 'nx/src/generators/testing-utils/create-tree-with-empty-workspace';
import { executeProcess } from '@code-pushup/utils';
@@ -76,6 +78,28 @@ export function registerPluginInWorkspace(
}));
}
+export async function registerPluginInNxJson(
+ nxJsonPath: string,
+ configuration: PluginConfiguration,
+) {
+ const normalizedPluginConfiguration =
+ typeof configuration === 'string'
+ ? {
+ plugin: configuration,
+ }
+ : configuration;
+ const json = JSON.parse(
+ (await readFile(nxJsonPath)).toString(),
+ ) as NxJsonConfiguration;
+ await writeFile(
+ nxJsonPath,
+ JSON.stringify({
+ ...json,
+ plugins: [...(json.plugins ?? []), normalizedPluginConfiguration],
+ }),
+ );
+}
+
export async function nxShowProjectJson(
cwd: string,
project: string,