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 {
CreateNodesContext,
createProjectGraphAsync,
formatFiles,
joinPathFragments,
type TargetConfiguration,
type Tree,
} from '@nx/devkit';
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 { upsertBaseUrl } from './lib/upsert-baseUrl';
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',
}),
postTargetTransformer,
createNodes,
createNodesV2,
options.project
);
@ -45,7 +43,7 @@ export async function convertToInferred(tree: Tree, options: Schema) {
ciTargetName: 'e2e-ci',
}),
postTargetTransformer,
createNodes,
createNodesV2,
options.project
);

View File

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

View File

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

View File

@ -1,9 +1,11 @@
import {
CreateDependencies,
CreateNodes,
CreateNodesContext,
createNodesFromFiles,
CreateNodesV2,
detectPackageManager,
joinPathFragments,
logger,
normalizePath,
NxJsonConfiguration,
ProjectConfiguration,
@ -22,6 +24,7 @@ import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';
import { NX_PLUGIN_OPTIONS } from '../utils/constants';
import { loadConfigFile } from '@nx/devkit/src/utils/config-utils';
import { hashObject } from 'nx/src/devkit-internals';
export interface CypressPluginOptions {
ciTargetName?: string;
@ -30,67 +33,96 @@ export interface CypressPluginOptions {
componentTestingTargetName?: string;
}
const cachePath = join(projectGraphCacheDirectory, 'cypress.hash');
const targetsCache = readTargetsCache();
function readTargetsCache(): Record<string, CypressTargets> {
function readTargetsCache(cachePath: string): Record<string, CypressTargets> {
return existsSync(cachePath) ? readJsonFile(cachePath) : {};
}
function writeTargetsToCache() {
const oldCache = readTargetsCache();
writeJsonFile(cachePath, {
...oldCache,
targetsCache,
});
function writeTargetsToCache(cachePath: string, results: CypressTargets) {
writeJsonFile(cachePath, results);
}
export const createDependencies: CreateDependencies = () => {
writeTargetsToCache();
return [];
};
const cypressConfigGlob = '**/cypress.config.{js,ts,mjs,cjs}';
export const createNodes: CreateNodes<CypressPluginOptions> = [
'**/cypress.config.{js,ts,mjs,cjs}',
async (configFilePath, options, context) => {
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
export const createNodesV2: CreateNodesV2<CypressPluginOptions> = [
cypressConfigGlob,
async (configFiles, options, context) => {
const optionsHash = hashObject(options);
const cachePath = join(
projectGraphCacheDirectory,
`cypress-${optionsHash}.hash`
);
const { targets, metadata } = targetsCache[hash];
const project: Omit<ProjectConfiguration, 'root'> = {
projectType: 'application',
targets,
metadata,
};
return {
projects: {
[projectRoot]: project,
},
};
const targetsCache = readTargetsCache(cachePath);
try {
return await createNodesFromFiles(
(configFile, options, context) =>
createNodesInternal(configFile, options, context, targetsCache),
configFiles,
options,
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<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(
projectRoot: string,
cypressConfig: any,

View File

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

View File

@ -6,7 +6,7 @@ import {
type Tree,
} from '@nx/devkit';
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 { interpolate } from 'nx/src/tasks-runner/utils';
@ -19,7 +19,7 @@ export async function convertToInferred(tree: Tree, options: Schema) {
const projectGraph = await createProjectGraphAsync();
const migratedProjectsModern =
await migrateExecutorToPlugin<EslintPluginOptions>(
await migrateExecutorToPluginV1<EslintPluginOptions>(
tree,
projectGraph,
'@nx/eslint:lint',
@ -31,7 +31,7 @@ export async function convertToInferred(tree: Tree, options: Schema) {
);
const migratedProjectsLegacy =
await migrateExecutorToPlugin<EslintPluginOptions>(
await migrateExecutorToPluginV1<EslintPluginOptions>(
tree,
projectGraph,
'@nrwl/linter:eslint',

View File

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

View File

@ -5,7 +5,7 @@ import {
type TargetConfiguration,
type Tree,
} 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';
interface Schema {
@ -24,7 +24,7 @@ export async function convertToInferred(tree: Tree, options: Schema) {
'@nx/playwright/plugin',
(targetName) => ({ targetName, ciTargetName: 'e2e-ci' }),
postTargetTransformer,
createNodes,
createNodesV2,
options.project
);

View File

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

View File

@ -1,11 +1,11 @@
import { CreateNodesContext } from '@nx/devkit';
import { TempFs } from '@nx/devkit/internal-testing-utils';
import { createNodes } from './plugin';
import { createNodesV2 } from './plugin';
import { PlaywrightTestConfig } from '@playwright/test';
describe('@nx/playwright/plugin', () => {
let createNodesFunction = createNodes[1];
let createNodesFunction = createNodesV2[1];
let context: CreateNodesContext;
let tempFs: TempFs;
@ -35,77 +35,84 @@ describe('@nx/playwright/plugin', () => {
it('should create nodes with default playwright configuration', async () => {
await mockPlaywrightConfig(tempFs, {});
const { projects } = await createNodesFunction(
'playwright.config.js',
const results = await createNodesFunction(
['playwright.config.js'],
{
targetName: 'e2e',
},
context
);
expect(projects).toMatchInlineSnapshot(`
{
".": {
"metadata": {
"targetGroups": {
"E2E (CI)": [
"e2e-ci",
],
expect(results).toMatchInlineSnapshot(`
[
[
"playwright.config.js",
{
"projects": {
".": {
"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' }],
],
});
const { projects } = await createNodesFunction(
'playwright.config.js',
const results = await createNodesFunction(
['playwright.config.js'],
{
targetName: 'e2e',
},
context
);
expect(projects).toMatchInlineSnapshot(`
{
".": {
"metadata": {
"targetGroups": {
"E2E (CI)": [
"e2e-ci",
],
expect(results).toMatchInlineSnapshot(`
[
[
"playwright.config.js",
{
"projects": {
".": {
"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': '',
});
const { projects } = await createNodesFunction(
'playwright.config.js',
const results = await createNodesFunction(
['playwright.config.js'],
{
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
context
);
const { targets } = projects['.'];
expect(projects['.'].metadata.targetGroups).toMatchInlineSnapshot(`
const project = results[0][1].projects['.'];
const { targets } = project;
expect(project.metadata.targetGroups).toMatchInlineSnapshot(`
{
"E2E (CI)": [
"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 {
CreateDependencies,
CreateNodes,
CreateNodesContext,
createNodesFromFiles,
CreateNodesV2,
detectPackageManager,
joinPathFragments,
logger,
normalizePath,
ProjectConfiguration,
readJsonFile,
@ -22,6 +24,7 @@ import { minimatch } from 'minimatch';
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';
import { getLockFileName } from '@nx/js';
import { loadConfigFile } from '@nx/devkit/src/utils/config-utils';
import { hashObject } from 'nx/src/hasher/file-hasher';
export interface PlaywrightPluginOptions {
targetName?: string;
@ -33,69 +36,101 @@ interface NormalizedOptions {
ciTargetName?: string;
}
const cachePath = join(projectGraphCacheDirectory, 'playwright.hash');
const targetsCache = readTargetsCache();
type PlaywrightTargets = Pick<ProjectConfiguration, 'targets' | 'metadata'>;
function readTargetsCache(): Record<string, PlaywrightTargets> {
function readTargetsCache(
cachePath: string
): Record<string, PlaywrightTargets> {
return existsSync(cachePath) ? readJsonFile(cachePath) : {};
}
function writeTargetsToCache() {
const oldCache = readTargetsCache();
writeJsonFile(cachePath, {
...readTargetsCache,
targetsCache,
});
function writeTargetsToCache(
cachePath: string,
results: Record<string, PlaywrightTargets>
) {
writeJsonFile(cachePath, results);
}
export const createDependencies: CreateDependencies = () => {
writeTargetsToCache();
return [];
};
export const createNodes: CreateNodes<PlaywrightPluginOptions> = [
'**/playwright.config.{js,ts,cjs,cts,mjs,mts}',
async (configFilePath, options, context) => {
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 playwrightConfigGlob = '**/playwright.config.{js,ts,cjs,cts,mjs,mts}';
export const createNodesV2: CreateNodesV2<PlaywrightPluginOptions> = [
playwrightConfigGlob,
async (configFilePaths, options, context) => {
const optionsHash = hashObject(options);
const cachePath = join(
projectGraphCacheDirectory,
`playwright-${optionsHash}.hash`
);
const { targets, metadata } = targetsCache[hash];
return {
projects: {
[projectRoot]: {
root: projectRoot,
targets,
metadata,
},
},
};
const targetsCache = readTargetsCache(cachePath);
try {
return await createNodesFromFiles(
(configFile, options, context) =>
createNodesInternal(configFile, options, context, targetsCache),
configFilePaths,
options,
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(
configFilePath: string,
projectRoot: string,