feat(testing): use createNodesV2 for cypress and playwright (#26301)

This commit is contained in:
Jason Jean 2024-05-31 17:53:31 -04:00 committed by GitHub
parent 92718fd52b
commit 1e7cd7e9e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 591 additions and 469 deletions

View File

@ -1 +1 @@
export { createNodes, createDependencies } from './src/plugins/plugin'; export { createNodesV2, createNodes } from './src/plugins/plugin';

View File

@ -1,13 +1,11 @@
import { import {
CreateNodesContext,
createProjectGraphAsync, createProjectGraphAsync,
formatFiles, formatFiles,
joinPathFragments,
type TargetConfiguration, type TargetConfiguration,
type Tree, type Tree,
} from '@nx/devkit'; } from '@nx/devkit';
import { migrateExecutorToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; import { migrateExecutorToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator';
import { createNodes } from '../../plugins/plugin'; import { createNodesV2 } from '../../plugins/plugin';
import { targetOptionsToCliMap } from './lib/target-options-map'; import { targetOptionsToCliMap } from './lib/target-options-map';
import { upsertBaseUrl } from './lib/upsert-baseUrl'; import { upsertBaseUrl } from './lib/upsert-baseUrl';
import { addDevServerTargetToConfig } from './lib/add-dev-server-target-to-config'; import { addDevServerTargetToConfig } from './lib/add-dev-server-target-to-config';
@ -31,7 +29,7 @@ export async function convertToInferred(tree: Tree, options: Schema) {
ciTargetName: 'e2e-ci', ciTargetName: 'e2e-ci',
}), }),
postTargetTransformer, postTargetTransformer,
createNodes, createNodesV2,
options.project options.project
); );
@ -45,7 +43,7 @@ export async function convertToInferred(tree: Tree, options: Schema) {
ciTargetName: 'e2e-ci', ciTargetName: 'e2e-ci',
}), }),
postTargetTransformer, postTargetTransformer,
createNodes, createNodesV2,
options.project options.project
); );

View File

@ -10,14 +10,10 @@ import {
Tree, Tree,
updateNxJson, updateNxJson,
} from '@nx/devkit'; } from '@nx/devkit';
import { import { addPlugin as _addPlugin } from '@nx/devkit/src/utils/add-plugin';
addPluginV1 as _addPlugin, import { createNodesV2 } from '../../plugins/plugin';
generateCombinations,
} from '@nx/devkit/src/utils/add-plugin';
import { createNodes } from '../../plugins/plugin';
import { cypressVersion, nxVersion } from '../../utils/versions'; import { cypressVersion, nxVersion } from '../../utils/versions';
import { Schema } from './schema'; import { Schema } from './schema';
import { CypressPluginOptions } from '../../plugins/plugin';
function setupE2ETargetDefaults(tree: Tree) { function setupE2ETargetDefaults(tree: Tree) {
const nxJson = readNxJson(tree); const nxJson = readNxJson(tree);
@ -69,7 +65,7 @@ export function addPlugin(
tree, tree,
graph, graph,
'@nx/cypress/plugin', '@nx/cypress/plugin',
createNodes, createNodesV2,
{ {
targetName: ['e2e', 'cypress:e2e', 'cypress-e2e'], targetName: ['e2e', 'cypress:e2e', 'cypress-e2e'],
openTargetName: ['open-cypress', 'cypress-open'], openTargetName: ['open-cypress', 'cypress-open'],

View File

@ -1,14 +1,14 @@
import { CreateNodesContext } from '@nx/devkit'; import { CreateNodesContext } from '@nx/devkit';
import { defineConfig } from 'cypress'; import { defineConfig } from 'cypress';
import { createNodes } from './plugin'; import { createNodesV2 } from './plugin';
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
import { resetWorkspaceContext } from 'nx/src/utils/workspace-context'; import { resetWorkspaceContext } from 'nx/src/utils/workspace-context';
import { join } from 'path'; import { join } from 'path';
import { nxE2EPreset } from '../../plugins/cypress-preset'; import { nxE2EPreset } from '../../plugins/cypress-preset';
describe('@nx/cypress/plugin', () => { describe('@nx/cypress/plugin', () => {
let createNodesFunction = createNodes[1]; let createNodesFunction = createNodesV2[1];
let context: CreateNodesContext; let context: CreateNodesContext;
let tempFs: TempFs; let tempFs: TempFs;
@ -65,7 +65,7 @@ describe('@nx/cypress/plugin', () => {
}) })
); );
const nodes = await createNodesFunction( const nodes = await createNodesFunction(
'cypress.config.js', ['cypress.config.js'],
{ {
targetName: 'e2e', targetName: 'e2e',
}, },
@ -73,59 +73,64 @@ describe('@nx/cypress/plugin', () => {
); );
expect(nodes).toMatchInlineSnapshot(` expect(nodes).toMatchInlineSnapshot(`
{ [
"projects": { [
".": { "cypress.config.js",
"metadata": undefined, {
"projectType": "application", "projects": {
"targets": { ".": {
"e2e": { "metadata": undefined,
"cache": true, "projectType": "application",
"command": "cypress run", "targets": {
"configurations": { "e2e": {
"production": { "cache": true,
"command": "cypress run --env webServerCommand="nx run my-app:serve:production"", "command": "cypress run",
}, "configurations": {
}, "production": {
"inputs": [ "command": "cypress run --env webServerCommand="nx run my-app:serve:production"",
"default", },
"^production", },
{ "inputs": [
"externalDependencies": [ "default",
"cypress", "^production",
{
"externalDependencies": [
"cypress",
],
},
],
"metadata": {
"description": "Runs Cypress Tests",
"technologies": [
"cypress",
],
},
"options": {
"cwd": ".",
},
"outputs": [
"{projectRoot}/dist/videos",
"{projectRoot}/dist/screenshots",
], ],
}, },
], "open-cypress": {
"metadata": { "command": "cypress open",
"description": "Runs Cypress Tests", "metadata": {
"technologies": [ "description": "Opens Cypress",
"cypress", "technologies": [
], "cypress",
}, ],
"options": { },
"cwd": ".", "options": {
}, "cwd": ".",
"outputs": [ },
"{projectRoot}/dist/videos", },
"{projectRoot}/dist/screenshots",
],
},
"open-cypress": {
"command": "cypress open",
"metadata": {
"description": "Opens Cypress",
"technologies": [
"cypress",
],
},
"options": {
"cwd": ".",
}, },
}, },
}, },
}, },
}, ],
} ]
`); `);
}); });
@ -143,7 +148,7 @@ describe('@nx/cypress/plugin', () => {
}) })
); );
const nodes = await createNodesFunction( const nodes = await createNodesFunction(
'cypress.config.js', ['cypress.config.js'],
{ {
componentTestingTargetName: 'component-test', componentTestingTargetName: 'component-test',
}, },
@ -151,54 +156,59 @@ describe('@nx/cypress/plugin', () => {
); );
expect(nodes).toMatchInlineSnapshot(` expect(nodes).toMatchInlineSnapshot(`
{ [
"projects": { [
".": { "cypress.config.js",
"metadata": undefined, {
"projectType": "application", "projects": {
"targets": { ".": {
"component-test": { "metadata": undefined,
"cache": true, "projectType": "application",
"command": "cypress run --component", "targets": {
"inputs": [ "component-test": {
"default", "cache": true,
"^production", "command": "cypress run --component",
{ "inputs": [
"externalDependencies": [ "default",
"cypress", "^production",
{
"externalDependencies": [
"cypress",
],
},
],
"metadata": {
"description": "Runs Cypress Component Tests",
"technologies": [
"cypress",
],
},
"options": {
"cwd": ".",
},
"outputs": [
"{projectRoot}/dist/videos",
"{projectRoot}/dist/screenshots",
], ],
}, },
], "open-cypress": {
"metadata": { "command": "cypress open",
"description": "Runs Cypress Component Tests", "metadata": {
"technologies": [ "description": "Opens Cypress",
"cypress", "technologies": [
], "cypress",
}, ],
"options": { },
"cwd": ".", "options": {
}, "cwd": ".",
"outputs": [ },
"{projectRoot}/dist/videos", },
"{projectRoot}/dist/screenshots",
],
},
"open-cypress": {
"command": "cypress open",
"metadata": {
"description": "Opens Cypress",
"technologies": [
"cypress",
],
},
"options": {
"cwd": ".",
}, },
}, },
}, },
}, },
}, ],
} ]
`); `);
}); });
@ -220,7 +230,7 @@ describe('@nx/cypress/plugin', () => {
}) })
); );
const nodes = await createNodesFunction( const nodes = await createNodesFunction(
'cypress.config.js', ['cypress.config.js'],
{ {
componentTestingTargetName: 'component-test', componentTestingTargetName: 'component-test',
}, },
@ -228,122 +238,127 @@ describe('@nx/cypress/plugin', () => {
); );
expect(nodes).toMatchInlineSnapshot(` expect(nodes).toMatchInlineSnapshot(`
{ [
"projects": { [
".": { "cypress.config.js",
"metadata": { {
"targetGroups": { "projects": {
"E2E (CI)": [ ".": {
"e2e-ci--src/test.cy.ts", "metadata": {
"e2e-ci", "targetGroups": {
], "E2E (CI)": [
}, "e2e-ci--src/test.cy.ts",
}, "e2e-ci",
"projectType": "application",
"targets": {
"e2e": {
"cache": true,
"command": "cypress run",
"configurations": {
"production": {
"command": "cypress run --env webServerCommand="my-app:serve:production"",
},
},
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"cypress",
], ],
}, },
],
"metadata": {
"description": "Runs Cypress Tests",
"technologies": [
"cypress",
],
}, },
"options": { "projectType": "application",
"cwd": ".", "targets": {
}, "e2e": {
"outputs": [ "cache": true,
"{projectRoot}/dist/videos", "command": "cypress run",
"{projectRoot}/dist/screenshots", "configurations": {
], "production": {
}, "command": "cypress run --env webServerCommand="my-app:serve:production"",
"e2e-ci": { },
"cache": true, },
"dependsOn": [ "inputs": [
{ "default",
"params": "forward", "^production",
"projects": "self", {
"target": "e2e-ci--src/test.cy.ts", "externalDependencies": [
}, "cypress",
], ],
"executor": "nx:noop", },
"inputs": [ ],
"default", "metadata": {
"^production", "description": "Runs Cypress Tests",
{ "technologies": [
"externalDependencies": [ "cypress",
"cypress", ],
},
"options": {
"cwd": ".",
},
"outputs": [
"{projectRoot}/dist/videos",
"{projectRoot}/dist/screenshots",
], ],
}, },
], "e2e-ci": {
"metadata": { "cache": true,
"description": "Runs Cypress Tests in CI", "dependsOn": [
"technologies": [ {
"cypress", "params": "forward",
], "projects": "self",
}, "target": "e2e-ci--src/test.cy.ts",
"outputs": [ },
"{projectRoot}/dist/videos", ],
"{projectRoot}/dist/screenshots", "executor": "nx:noop",
], "inputs": [
}, "default",
"e2e-ci--src/test.cy.ts": { "^production",
"cache": true, {
"command": "cypress run --env webServerCommand="my-app:serve-static" --spec src/test.cy.ts", "externalDependencies": [
"inputs": [ "cypress",
"default", ],
"^production", },
{ ],
"externalDependencies": [ "metadata": {
"cypress", "description": "Runs Cypress Tests in CI",
"technologies": [
"cypress",
],
},
"outputs": [
"{projectRoot}/dist/videos",
"{projectRoot}/dist/screenshots",
], ],
}, },
], "e2e-ci--src/test.cy.ts": {
"metadata": { "cache": true,
"description": "Runs Cypress Tests in src/test.cy.ts in CI", "command": "cypress run --env webServerCommand="my-app:serve-static" --spec src/test.cy.ts",
"technologies": [ "inputs": [
"cypress", "default",
], "^production",
}, {
"options": { "externalDependencies": [
"cwd": ".", "cypress",
}, ],
"outputs": [ },
"{projectRoot}/dist/videos", ],
"{projectRoot}/dist/screenshots", "metadata": {
], "description": "Runs Cypress Tests in src/test.cy.ts in CI",
}, "technologies": [
"open-cypress": { "cypress",
"command": "cypress open", ],
"metadata": { },
"description": "Opens Cypress", "options": {
"technologies": [ "cwd": ".",
"cypress", },
], "outputs": [
}, "{projectRoot}/dist/videos",
"options": { "{projectRoot}/dist/screenshots",
"cwd": ".", ],
},
"open-cypress": {
"command": "cypress open",
"metadata": {
"description": "Opens Cypress",
"technologies": [
"cypress",
],
},
"options": {
"cwd": ".",
},
},
}, },
}, },
}, },
}, },
}, ],
} ]
`); `);
}); });

View File

@ -1,9 +1,11 @@
import { import {
CreateDependencies,
CreateNodes, CreateNodes,
CreateNodesContext, CreateNodesContext,
createNodesFromFiles,
CreateNodesV2,
detectPackageManager, detectPackageManager,
joinPathFragments, joinPathFragments,
logger,
normalizePath, normalizePath,
NxJsonConfiguration, NxJsonConfiguration,
ProjectConfiguration, ProjectConfiguration,
@ -22,6 +24,7 @@ import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory'; import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';
import { NX_PLUGIN_OPTIONS } from '../utils/constants'; import { NX_PLUGIN_OPTIONS } from '../utils/constants';
import { loadConfigFile } from '@nx/devkit/src/utils/config-utils'; import { loadConfigFile } from '@nx/devkit/src/utils/config-utils';
import { hashObject } from 'nx/src/devkit-internals';
export interface CypressPluginOptions { export interface CypressPluginOptions {
ciTargetName?: string; ciTargetName?: string;
@ -30,67 +33,96 @@ export interface CypressPluginOptions {
componentTestingTargetName?: string; componentTestingTargetName?: string;
} }
const cachePath = join(projectGraphCacheDirectory, 'cypress.hash'); function readTargetsCache(cachePath: string): Record<string, CypressTargets> {
const targetsCache = readTargetsCache();
function readTargetsCache(): Record<string, CypressTargets> {
return existsSync(cachePath) ? readJsonFile(cachePath) : {}; return existsSync(cachePath) ? readJsonFile(cachePath) : {};
} }
function writeTargetsToCache() { function writeTargetsToCache(cachePath: string, results: CypressTargets) {
const oldCache = readTargetsCache(); writeJsonFile(cachePath, results);
writeJsonFile(cachePath, {
...oldCache,
targetsCache,
});
} }
export const createDependencies: CreateDependencies = () => { const cypressConfigGlob = '**/cypress.config.{js,ts,mjs,cjs}';
writeTargetsToCache();
return [];
};
export const createNodes: CreateNodes<CypressPluginOptions> = [ export const createNodesV2: CreateNodesV2<CypressPluginOptions> = [
'**/cypress.config.{js,ts,mjs,cjs}', cypressConfigGlob,
async (configFilePath, options, context) => { async (configFiles, options, context) => {
options = normalizeOptions(options); const optionsHash = hashObject(options);
const projectRoot = dirname(configFilePath); const cachePath = join(
projectGraphCacheDirectory,
// Do not create a project if package.json and project.json isn't there. `cypress-${optionsHash}.hash`
const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot));
if (
!siblingFiles.includes('package.json') &&
!siblingFiles.includes('project.json')
) {
return {};
}
const hash = calculateHashForCreateNodes(projectRoot, options, context, [
getLockFileName(detectPackageManager(context.workspaceRoot)),
]);
targetsCache[hash] ??= await buildCypressTargets(
configFilePath,
projectRoot,
options,
context
); );
const { targets, metadata } = targetsCache[hash]; const targetsCache = readTargetsCache(cachePath);
try {
const project: Omit<ProjectConfiguration, 'root'> = { return await createNodesFromFiles(
projectType: 'application', (configFile, options, context) =>
targets, createNodesInternal(configFile, options, context, targetsCache),
metadata, configFiles,
}; options,
context
return { );
projects: { } finally {
[projectRoot]: project, writeTargetsToCache(cachePath, targetsCache);
}, }
};
}, },
]; ];
/**
* @deprecated This is replaced with {@link createNodesV2}. Update your plugin to export its own `createNodesV2` function that wraps this one instead.
* This function will change to the v2 function in Nx 20.
*/
export const createNodes: CreateNodes<CypressPluginOptions> = [
cypressConfigGlob,
(configFile, options, context) => {
logger.warn(
'`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.'
);
return createNodesInternal(configFile, options, context, {});
},
];
async function createNodesInternal(
configFilePath: string,
options: CypressPluginOptions,
context: CreateNodesContext,
targetsCache: CypressTargets
) {
options = normalizeOptions(options);
const projectRoot = dirname(configFilePath);
// Do not create a project if package.json and project.json isn't there.
const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot));
if (
!siblingFiles.includes('package.json') &&
!siblingFiles.includes('project.json')
) {
return {};
}
const hash = calculateHashForCreateNodes(projectRoot, options, context, [
getLockFileName(detectPackageManager(context.workspaceRoot)),
]);
targetsCache[hash] ??= await buildCypressTargets(
configFilePath,
projectRoot,
options,
context
);
const { targets, metadata } = targetsCache[hash];
const project: Omit<ProjectConfiguration, 'root'> = {
projectType: 'application',
targets,
metadata,
};
return {
projects: {
[projectRoot]: project,
},
};
}
function getOutputs( function getOutputs(
projectRoot: string, projectRoot: string,
cypressConfig: any, cypressConfig: any,

View File

@ -17,6 +17,7 @@ import {
TargetConfiguration, TargetConfiguration,
Tree, Tree,
CreateNodes, CreateNodes,
CreateNodesV2,
} from 'nx/src/devkit-exports'; } from 'nx/src/devkit-exports';
import { import {
@ -52,8 +53,8 @@ class ExecutorToPluginMigrator<T> {
#targetDefaultsForExecutor: Partial<TargetConfiguration>; #targetDefaultsForExecutor: Partial<TargetConfiguration>;
#targetAndProjectsToMigrate: Map<string, Set<string>>; #targetAndProjectsToMigrate: Map<string, Set<string>>;
#pluginToAddForTarget: Map<string, ExpandedPluginConfiguration<T>>; #pluginToAddForTarget: Map<string, ExpandedPluginConfiguration<T>>;
#createNodes: CreateNodes<T>; #createNodes?: CreateNodes<T>;
#configFiles: string[]; #createNodesV2?: CreateNodesV2<T>;
#createNodesResultsForTargets: Map<string, ConfigurationResult>; #createNodesResultsForTargets: Map<string, ConfigurationResult>;
constructor( constructor(
@ -63,7 +64,8 @@ class ExecutorToPluginMigrator<T> {
pluginPath: string, pluginPath: string,
pluginOptionsBuilder: PluginOptionsBuilder<T>, pluginOptionsBuilder: PluginOptionsBuilder<T>,
postTargetTransformer: PostTargetTransformer, postTargetTransformer: PostTargetTransformer,
createNodes: CreateNodes<T>, createNodes?: CreateNodes<T>,
createNodesV2?: CreateNodesV2<T>,
specificProjectToMigrate?: string, specificProjectToMigrate?: string,
skipTargetFilter?: SkipTargetFilter skipTargetFilter?: SkipTargetFilter
) { ) {
@ -74,6 +76,7 @@ class ExecutorToPluginMigrator<T> {
this.#pluginOptionsBuilder = pluginOptionsBuilder; this.#pluginOptionsBuilder = pluginOptionsBuilder;
this.#postTargetTransformer = postTargetTransformer; this.#postTargetTransformer = postTargetTransformer;
this.#createNodes = createNodes; this.#createNodes = createNodes;
this.#createNodesV2 = createNodesV2;
this.#specificProjectToMigrate = specificProjectToMigrate; this.#specificProjectToMigrate = specificProjectToMigrate;
this.#skipTargetFilter = skipTargetFilter ?? ((...args) => [false, '']); this.#skipTargetFilter = skipTargetFilter ?? ((...args) => [false, '']);
} }
@ -224,6 +227,7 @@ class ExecutorToPluginMigrator<T> {
) { ) {
const loadedPlugin = new LoadedNxPlugin( const loadedPlugin = new LoadedNxPlugin(
{ {
createNodesV2: this.#createNodesV2,
createNodes: this.#createNodes, createNodes: this.#createNodes,
name: this.#pluginPath, name: this.#pluginPath,
}, },
@ -373,6 +377,7 @@ class ExecutorToPluginMigrator<T> {
for (const targetName of this.#targetAndProjectsToMigrate.keys()) { for (const targetName of this.#targetAndProjectsToMigrate.keys()) {
const loadedPlugin = new LoadedNxPlugin( const loadedPlugin = new LoadedNxPlugin(
{ {
createNodesV2: this.#createNodesV2,
createNodes: this.#createNodes, createNodes: this.#createNodes,
name: this.#pluginPath, name: this.#pluginPath,
}, },
@ -396,13 +401,38 @@ class ExecutorToPluginMigrator<T> {
} }
} }
this.#configFiles = Array.from(projectConfigs.matchingProjectFiles);
this.#createNodesResultsForTargets.set(targetName, projectConfigs); this.#createNodesResultsForTargets.set(targetName, projectConfigs);
} }
} }
} }
export async function migrateExecutorToPlugin<T>( export async function migrateExecutorToPlugin<T>(
tree: Tree,
projectGraph: ProjectGraph,
executor: string,
pluginPath: string,
pluginOptionsBuilder: PluginOptionsBuilder<T>,
postTargetTransformer: PostTargetTransformer,
createNodes: CreateNodesV2<T>,
specificProjectToMigrate?: string,
skipTargetFilter?: SkipTargetFilter
): Promise<Map<string, Set<string>>> {
const migrator = new ExecutorToPluginMigrator<T>(
tree,
projectGraph,
executor,
pluginPath,
pluginOptionsBuilder,
postTargetTransformer,
undefined,
createNodes,
specificProjectToMigrate,
skipTargetFilter
);
return await migrator.run();
}
export async function migrateExecutorToPluginV1<T>(
tree: Tree, tree: Tree,
projectGraph: ProjectGraph, projectGraph: ProjectGraph,
executor: string, executor: string,
@ -421,6 +451,7 @@ export async function migrateExecutorToPlugin<T>(
pluginOptionsBuilder, pluginOptionsBuilder,
postTargetTransformer, postTargetTransformer,
createNodes, createNodes,
undefined,
specificProjectToMigrate, specificProjectToMigrate,
skipTargetFilter skipTargetFilter
); );

View File

@ -6,7 +6,7 @@ import {
type Tree, type Tree,
} from '@nx/devkit'; } from '@nx/devkit';
import { createNodes, EslintPluginOptions } from '../../plugins/plugin'; import { createNodes, EslintPluginOptions } from '../../plugins/plugin';
import { migrateExecutorToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; import { migrateExecutorToPluginV1 } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator';
import { targetOptionsToCliMap } from './lib/target-options-map'; import { targetOptionsToCliMap } from './lib/target-options-map';
import { interpolate } from 'nx/src/tasks-runner/utils'; import { interpolate } from 'nx/src/tasks-runner/utils';
@ -19,7 +19,7 @@ export async function convertToInferred(tree: Tree, options: Schema) {
const projectGraph = await createProjectGraphAsync(); const projectGraph = await createProjectGraphAsync();
const migratedProjectsModern = const migratedProjectsModern =
await migrateExecutorToPlugin<EslintPluginOptions>( await migrateExecutorToPluginV1<EslintPluginOptions>(
tree, tree,
projectGraph, projectGraph,
'@nx/eslint:lint', '@nx/eslint:lint',
@ -31,7 +31,7 @@ export async function convertToInferred(tree: Tree, options: Schema) {
); );
const migratedProjectsLegacy = const migratedProjectsLegacy =
await migrateExecutorToPlugin<EslintPluginOptions>( await migrateExecutorToPluginV1<EslintPluginOptions>(
tree, tree,
projectGraph, projectGraph,
'@nrwl/linter:eslint', '@nrwl/linter:eslint',

View File

@ -1,5 +1,5 @@
export { export {
createNodes, createNodes,
createNodesV2,
PlaywrightPluginOptions, PlaywrightPluginOptions,
createDependencies,
} from './src/plugins/plugin'; } from './src/plugins/plugin';

View File

@ -5,7 +5,7 @@ import {
type TargetConfiguration, type TargetConfiguration,
type Tree, type Tree,
} from '@nx/devkit'; } from '@nx/devkit';
import { createNodes, PlaywrightPluginOptions } from '../../plugins/plugin'; import { createNodesV2, PlaywrightPluginOptions } from '../../plugins/plugin';
import { migrateExecutorToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; import { migrateExecutorToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator';
interface Schema { interface Schema {
@ -24,7 +24,7 @@ export async function convertToInferred(tree: Tree, options: Schema) {
'@nx/playwright/plugin', '@nx/playwright/plugin',
(targetName) => ({ targetName, ciTargetName: 'e2e-ci' }), (targetName) => ({ targetName, ciTargetName: 'e2e-ci' }),
postTargetTransformer, postTargetTransformer,
createNodes, createNodesV2,
options.project options.project
); );

View File

@ -7,8 +7,8 @@ import {
runTasksInSerial, runTasksInSerial,
Tree, Tree,
} from '@nx/devkit'; } from '@nx/devkit';
import { addPluginV1 } from '@nx/devkit/src/utils/add-plugin'; import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
import { createNodes } from '../../plugins/plugin'; import { createNodesV2 } from '../../plugins/plugin';
import { nxVersion, playwrightVersion } from '../../utils/versions'; import { nxVersion, playwrightVersion } from '../../utils/versions';
import { InitGeneratorSchema } from './schema'; import { InitGeneratorSchema } from './schema';
@ -45,11 +45,11 @@ export async function initGeneratorInternal(
} }
if (options.addPlugin) { if (options.addPlugin) {
await addPluginV1( await addPlugin(
tree, tree,
await createProjectGraphAsync(), await createProjectGraphAsync(),
'@nx/playwright/plugin', '@nx/playwright/plugin',
createNodes, createNodesV2,
{ targetName: ['e2e', 'playwright:e2e', 'playwright-e2e'] }, { targetName: ['e2e', 'playwright:e2e', 'playwright-e2e'] },
options.updatePackageScripts options.updatePackageScripts
); );

View File

@ -1,11 +1,11 @@
import { CreateNodesContext } from '@nx/devkit'; import { CreateNodesContext } from '@nx/devkit';
import { TempFs } from '@nx/devkit/internal-testing-utils'; import { TempFs } from '@nx/devkit/internal-testing-utils';
import { createNodes } from './plugin'; import { createNodesV2 } from './plugin';
import { PlaywrightTestConfig } from '@playwright/test'; import { PlaywrightTestConfig } from '@playwright/test';
describe('@nx/playwright/plugin', () => { describe('@nx/playwright/plugin', () => {
let createNodesFunction = createNodes[1]; let createNodesFunction = createNodesV2[1];
let context: CreateNodesContext; let context: CreateNodesContext;
let tempFs: TempFs; let tempFs: TempFs;
@ -35,77 +35,84 @@ describe('@nx/playwright/plugin', () => {
it('should create nodes with default playwright configuration', async () => { it('should create nodes with default playwright configuration', async () => {
await mockPlaywrightConfig(tempFs, {}); await mockPlaywrightConfig(tempFs, {});
const { projects } = await createNodesFunction( const results = await createNodesFunction(
'playwright.config.js', ['playwright.config.js'],
{ {
targetName: 'e2e', targetName: 'e2e',
}, },
context context
); );
expect(projects).toMatchInlineSnapshot(` expect(results).toMatchInlineSnapshot(`
{ [
".": { [
"metadata": { "playwright.config.js",
"targetGroups": { {
"E2E (CI)": [ "projects": {
"e2e-ci", ".": {
], "metadata": {
"targetGroups": {
"E2E (CI)": [
"e2e-ci",
],
},
},
"root": ".",
"targets": {
"e2e": {
"cache": true,
"command": "playwright test",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"@playwright/test",
],
},
],
"metadata": {
"description": "Runs Playwright Tests",
"technologies": [
"playwright",
],
},
"options": {
"cwd": "{projectRoot}",
},
"outputs": [
"{projectRoot}/test-results",
],
},
"e2e-ci": {
"cache": true,
"dependsOn": [],
"executor": "nx:noop",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"@playwright/test",
],
},
],
"metadata": {
"description": "Runs Playwright Tests in CI",
"technologies": [
"playwright",
],
},
"outputs": [
"{projectRoot}/test-results",
],
},
},
},
}, },
}, },
"root": ".", ],
"targets": { ]
"e2e": {
"cache": true,
"command": "playwright test",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"@playwright/test",
],
},
],
"metadata": {
"description": "Runs Playwright Tests",
"technologies": [
"playwright",
],
},
"options": {
"cwd": "{projectRoot}",
},
"outputs": [
"{projectRoot}/test-results",
],
},
"e2e-ci": {
"cache": true,
"dependsOn": [],
"executor": "nx:noop",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"@playwright/test",
],
},
],
"metadata": {
"description": "Runs Playwright Tests in CI",
"technologies": [
"playwright",
],
},
"outputs": [
"{projectRoot}/test-results",
],
},
},
},
}
`); `);
}); });
@ -117,83 +124,90 @@ describe('@nx/playwright/plugin', () => {
['html', { outputFolder: 'test-results/html' }], ['html', { outputFolder: 'test-results/html' }],
], ],
}); });
const { projects } = await createNodesFunction( const results = await createNodesFunction(
'playwright.config.js', ['playwright.config.js'],
{ {
targetName: 'e2e', targetName: 'e2e',
}, },
context context
); );
expect(projects).toMatchInlineSnapshot(` expect(results).toMatchInlineSnapshot(`
{ [
".": { [
"metadata": { "playwright.config.js",
"targetGroups": { {
"E2E (CI)": [ "projects": {
"e2e-ci", ".": {
], "metadata": {
"targetGroups": {
"E2E (CI)": [
"e2e-ci",
],
},
},
"root": ".",
"targets": {
"e2e": {
"cache": true,
"command": "playwright test",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"@playwright/test",
],
},
],
"metadata": {
"description": "Runs Playwright Tests",
"technologies": [
"playwright",
],
},
"options": {
"cwd": "{projectRoot}",
},
"outputs": [
"{projectRoot}/playwright-report",
"{projectRoot}/test-results/report.json",
"{projectRoot}/test-results/html",
"{projectRoot}/test-results",
],
},
"e2e-ci": {
"cache": true,
"dependsOn": [],
"executor": "nx:noop",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"@playwright/test",
],
},
],
"metadata": {
"description": "Runs Playwright Tests in CI",
"technologies": [
"playwright",
],
},
"outputs": [
"{projectRoot}/playwright-report",
"{projectRoot}/test-results/report.json",
"{projectRoot}/test-results/html",
"{projectRoot}/test-results",
],
},
},
},
}, },
}, },
"root": ".", ],
"targets": { ]
"e2e": {
"cache": true,
"command": "playwright test",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"@playwright/test",
],
},
],
"metadata": {
"description": "Runs Playwright Tests",
"technologies": [
"playwright",
],
},
"options": {
"cwd": "{projectRoot}",
},
"outputs": [
"{projectRoot}/playwright-report",
"{projectRoot}/test-results/report.json",
"{projectRoot}/test-results/html",
"{projectRoot}/test-results",
],
},
"e2e-ci": {
"cache": true,
"dependsOn": [],
"executor": "nx:noop",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"@playwright/test",
],
},
],
"metadata": {
"description": "Runs Playwright Tests in CI",
"technologies": [
"playwright",
],
},
"outputs": [
"{projectRoot}/playwright-report",
"{projectRoot}/test-results/report.json",
"{projectRoot}/test-results/html",
"{projectRoot}/test-results",
],
},
},
},
}
`); `);
}); });
@ -213,16 +227,17 @@ describe('@nx/playwright/plugin', () => {
'not-tests/run-me.spec.ts': '', 'not-tests/run-me.spec.ts': '',
}); });
const { projects } = await createNodesFunction( const results = await createNodesFunction(
'playwright.config.js', ['playwright.config.js'],
{ {
targetName: 'e2e', targetName: 'e2e',
ciTargetName: 'e2e-ci', ciTargetName: 'e2e-ci',
}, },
context context
); );
const { targets } = projects['.']; const project = results[0][1].projects['.'];
expect(projects['.'].metadata.targetGroups).toMatchInlineSnapshot(` const { targets } = project;
expect(project.metadata.targetGroups).toMatchInlineSnapshot(`
{ {
"E2E (CI)": [ "E2E (CI)": [
"e2e-ci--tests/run-me-2.spec.ts", "e2e-ci--tests/run-me-2.spec.ts",

View File

@ -2,11 +2,13 @@ import { existsSync, readdirSync } from 'fs';
import { dirname, join, relative } from 'path'; import { dirname, join, relative } from 'path';
import { import {
CreateDependencies,
CreateNodes, CreateNodes,
CreateNodesContext, CreateNodesContext,
createNodesFromFiles,
CreateNodesV2,
detectPackageManager, detectPackageManager,
joinPathFragments, joinPathFragments,
logger,
normalizePath, normalizePath,
ProjectConfiguration, ProjectConfiguration,
readJsonFile, readJsonFile,
@ -22,6 +24,7 @@ import { minimatch } from 'minimatch';
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory'; import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';
import { getLockFileName } from '@nx/js'; import { getLockFileName } from '@nx/js';
import { loadConfigFile } from '@nx/devkit/src/utils/config-utils'; import { loadConfigFile } from '@nx/devkit/src/utils/config-utils';
import { hashObject } from 'nx/src/hasher/file-hasher';
export interface PlaywrightPluginOptions { export interface PlaywrightPluginOptions {
targetName?: string; targetName?: string;
@ -33,69 +36,101 @@ interface NormalizedOptions {
ciTargetName?: string; ciTargetName?: string;
} }
const cachePath = join(projectGraphCacheDirectory, 'playwright.hash');
const targetsCache = readTargetsCache();
type PlaywrightTargets = Pick<ProjectConfiguration, 'targets' | 'metadata'>; type PlaywrightTargets = Pick<ProjectConfiguration, 'targets' | 'metadata'>;
function readTargetsCache(): Record<string, PlaywrightTargets> { function readTargetsCache(
cachePath: string
): Record<string, PlaywrightTargets> {
return existsSync(cachePath) ? readJsonFile(cachePath) : {}; return existsSync(cachePath) ? readJsonFile(cachePath) : {};
} }
function writeTargetsToCache() { function writeTargetsToCache(
const oldCache = readTargetsCache(); cachePath: string,
writeJsonFile(cachePath, { results: Record<string, PlaywrightTargets>
...readTargetsCache, ) {
targetsCache, writeJsonFile(cachePath, results);
});
} }
export const createDependencies: CreateDependencies = () => { const playwrightConfigGlob = '**/playwright.config.{js,ts,cjs,cts,mjs,mts}';
writeTargetsToCache(); export const createNodesV2: CreateNodesV2<PlaywrightPluginOptions> = [
return []; playwrightConfigGlob,
}; async (configFilePaths, options, context) => {
const optionsHash = hashObject(options);
export const createNodes: CreateNodes<PlaywrightPluginOptions> = [ const cachePath = join(
'**/playwright.config.{js,ts,cjs,cts,mjs,mts}', projectGraphCacheDirectory,
async (configFilePath, options, context) => { `playwright-${optionsHash}.hash`
const projectRoot = dirname(configFilePath);
// Do not create a project if package.json and project.json isn't there.
const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot));
if (
!siblingFiles.includes('package.json') &&
!siblingFiles.includes('project.json')
) {
return {};
}
const normalizedOptions = normalizeOptions(options);
const hash = calculateHashForCreateNodes(projectRoot, options, context, [
getLockFileName(detectPackageManager(context.workspaceRoot)),
]);
targetsCache[hash] ??= await buildPlaywrightTargets(
configFilePath,
projectRoot,
normalizedOptions,
context
); );
const { targets, metadata } = targetsCache[hash]; const targetsCache = readTargetsCache(cachePath);
try {
return { return await createNodesFromFiles(
projects: { (configFile, options, context) =>
[projectRoot]: { createNodesInternal(configFile, options, context, targetsCache),
root: projectRoot, configFilePaths,
targets, options,
metadata, context
}, );
}, } finally {
}; writeTargetsToCache(cachePath, targetsCache);
}
}, },
]; ];
/**
* @deprecated This is replaced with {@link createNodesV2}. Update your plugin to export its own `createNodesV2` function that wraps this one instead.
* This function will change to the v2 function in Nx 20.
*/
export const createNodes: CreateNodes<PlaywrightPluginOptions> = [
playwrightConfigGlob,
async (configFile, options, context) => {
logger.warn(
'`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.'
);
return createNodesInternal(configFile, options, context, {});
},
];
async function createNodesInternal(
configFilePath: string,
options: PlaywrightPluginOptions,
context: CreateNodesContext,
targetsCache: Record<string, PlaywrightTargets>
) {
const projectRoot = dirname(configFilePath);
// Do not create a project if package.json and project.json isn't there.
const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot));
if (
!siblingFiles.includes('package.json') &&
!siblingFiles.includes('project.json')
) {
return {};
}
const normalizedOptions = normalizeOptions(options);
const hash = calculateHashForCreateNodes(projectRoot, options, context, [
getLockFileName(detectPackageManager(context.workspaceRoot)),
]);
targetsCache[hash] ??= await buildPlaywrightTargets(
configFilePath,
projectRoot,
normalizedOptions,
context
);
const { targets, metadata } = targetsCache[hash];
return {
projects: {
[projectRoot]: {
root: projectRoot,
targets,
metadata,
},
},
};
}
async function buildPlaywrightTargets( async function buildPlaywrightTargets(
configFilePath: string, configFilePath: string,
projectRoot: string, projectRoot: string,