From 23f82f517519eedcf28bd917118d66118b20df35 Mon Sep 17 00:00:00 2001 From: FrozenPandaz Date: Fri, 31 May 2024 16:27:20 -0400 Subject: [PATCH] feat(testing): use createNodesV2 for cypress and playwright --- packages/cypress/plugin.ts | 2 +- .../convert-to-inferred.ts | 8 +- packages/cypress/src/generators/init/init.ts | 10 +- packages/cypress/src/plugins/plugin.spec.ts | 411 +++++++++--------- packages/cypress/src/plugins/plugin.ts | 132 +++--- .../executor-to-plugin-migrator.ts | 39 +- .../convert-to-inferred.ts | 6 +- packages/playwright/plugin.ts | 2 +- .../convert-to-inferred.ts | 4 +- .../playwright/src/generators/init/init.ts | 8 +- .../playwright/src/plugins/plugin.spec.ts | 279 ++++++------ packages/playwright/src/plugins/plugin.ts | 135 +++--- 12 files changed, 579 insertions(+), 457 deletions(-) diff --git a/packages/cypress/plugin.ts b/packages/cypress/plugin.ts index c2bb22ffb070e..f1e8ea5c23a25 100644 --- a/packages/cypress/plugin.ts +++ b/packages/cypress/plugin.ts @@ -1 +1 @@ -export { createNodes, createDependencies } from './src/plugins/plugin'; +export { createNodesV2, createNodes } from './src/plugins/plugin'; diff --git a/packages/cypress/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/cypress/src/generators/convert-to-inferred/convert-to-inferred.ts index 1ded42517c65d..736a01aafd2ee 100644 --- a/packages/cypress/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/cypress/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -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 ); diff --git a/packages/cypress/src/generators/init/init.ts b/packages/cypress/src/generators/init/init.ts index 9836d72fd5e77..b8deed06a0569 100644 --- a/packages/cypress/src/generators/init/init.ts +++ b/packages/cypress/src/generators/init/init.ts @@ -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'], diff --git a/packages/cypress/src/plugins/plugin.spec.ts b/packages/cypress/src/plugins/plugin.spec.ts index fbb4bd3254822..fc811af05478b 100644 --- a/packages/cypress/src/plugins/plugin.spec.ts +++ b/packages/cypress/src/plugins/plugin.spec.ts @@ -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", + "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", + ], }, - ], - "executor": "nx:noop", - "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 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--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", ], }, - ], - "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": ".", + "open-cypress": { + "command": "cypress open", + "metadata": { + "description": "Opens Cypress", + "technologies": [ + "cypress", + ], + }, + "options": { + "cwd": ".", + }, + }, }, }, }, }, - }, - } + ], + ] `); }); diff --git a/packages/cypress/src/plugins/plugin.ts b/packages/cypress/src/plugins/plugin.ts index 1151907965abf..630231b791dac 100644 --- a/packages/cypress/src/plugins/plugin.ts +++ b/packages/cypress/src/plugins/plugin.ts @@ -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 { +function readTargetsCache(cachePath: string): Record { 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 = [ - '**/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 {}; +export const createNodesV2: CreateNodesV2 = [ + cypressConfigGlob, + async (configFiles, options, context) => { + const optionsHash = hashObject(options); + const cachePath = join( + projectGraphCacheDirectory, + `cypress-${optionsHash}.hash` + ); + const targetsCache = readTargetsCache(cachePath); + try { + return await createNodesFromFiles( + (configFile, options, context) => + createNodesInternal(configFile, options, context, targetsCache), + configFiles, + options, + context + ); + } finally { + writeTargetsToCache(cachePath, targetsCache); } + }, +]; - const hash = calculateHashForCreateNodes(projectRoot, options, context, [ - getLockFileName(detectPackageManager(context.workspaceRoot)), - ]); - - targetsCache[hash] ??= await buildCypressTargets( - configFilePath, - projectRoot, - options, - context +/** + * @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 = [ + 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.' ); - const { targets, metadata } = targetsCache[hash]; - - const project: Omit = { - projectType: 'application', - targets, - metadata, - }; - - return { - projects: { - [projectRoot]: project, - }, - }; + 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 = { + projectType: 'application', + targets, + metadata, + }; + + return { + projects: { + [projectRoot]: project, + }, + }; +} + function getOutputs( projectRoot: string, cypressConfig: any, diff --git a/packages/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator.ts b/packages/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator.ts index 0f71cd97e36e0..773a042c83798 100644 --- a/packages/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator.ts +++ b/packages/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator.ts @@ -17,6 +17,7 @@ import { TargetConfiguration, Tree, CreateNodes, + CreateNodesV2, } from 'nx/src/devkit-exports'; import { @@ -52,8 +53,8 @@ class ExecutorToPluginMigrator { #targetDefaultsForExecutor: Partial; #targetAndProjectsToMigrate: Map>; #pluginToAddForTarget: Map>; - #createNodes: CreateNodes; - #configFiles: string[]; + #createNodes?: CreateNodes; + #createNodesV2?: CreateNodesV2; #createNodesResultsForTargets: Map; constructor( @@ -63,7 +64,8 @@ class ExecutorToPluginMigrator { pluginPath: string, pluginOptionsBuilder: PluginOptionsBuilder, postTargetTransformer: PostTargetTransformer, - createNodes: CreateNodes, + createNodes?: CreateNodes, + createNodesV2?: CreateNodesV2, specificProjectToMigrate?: string, skipTargetFilter?: SkipTargetFilter ) { @@ -74,6 +76,7 @@ class ExecutorToPluginMigrator { 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 { ) { const loadedPlugin = new LoadedNxPlugin( { + createNodesV2: this.#createNodesV2, createNodes: this.#createNodes, name: this.#pluginPath, }, @@ -373,6 +377,7 @@ class ExecutorToPluginMigrator { 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 { } } - this.#configFiles = Array.from(projectConfigs.matchingProjectFiles); this.#createNodesResultsForTargets.set(targetName, projectConfigs); } } } export async function migrateExecutorToPlugin( + tree: Tree, + projectGraph: ProjectGraph, + executor: string, + pluginPath: string, + pluginOptionsBuilder: PluginOptionsBuilder, + postTargetTransformer: PostTargetTransformer, + createNodes: CreateNodesV2, + specificProjectToMigrate?: string, + skipTargetFilter?: SkipTargetFilter +): Promise>> { + const migrator = new ExecutorToPluginMigrator( + tree, + projectGraph, + executor, + pluginPath, + pluginOptionsBuilder, + postTargetTransformer, + undefined, + createNodes, + specificProjectToMigrate, + skipTargetFilter + ); + return await migrator.run(); +} + +export async function migrateExecutorToPluginV1( tree: Tree, projectGraph: ProjectGraph, executor: string, @@ -421,6 +451,7 @@ export async function migrateExecutorToPlugin( pluginOptionsBuilder, postTargetTransformer, createNodes, + undefined, specificProjectToMigrate, skipTargetFilter ); diff --git a/packages/eslint/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/eslint/src/generators/convert-to-inferred/convert-to-inferred.ts index 5666981194857..2fef8a342582d 100644 --- a/packages/eslint/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/eslint/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -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( + await migrateExecutorToPluginV1( tree, projectGraph, '@nx/eslint:lint', @@ -31,7 +31,7 @@ export async function convertToInferred(tree: Tree, options: Schema) { ); const migratedProjectsLegacy = - await migrateExecutorToPlugin( + await migrateExecutorToPluginV1( tree, projectGraph, '@nrwl/linter:eslint', diff --git a/packages/playwright/plugin.ts b/packages/playwright/plugin.ts index 6a6eb207ac047..f22ed34c32ef3 100644 --- a/packages/playwright/plugin.ts +++ b/packages/playwright/plugin.ts @@ -1,5 +1,5 @@ export { createNodes, + createNodesV2, PlaywrightPluginOptions, - createDependencies, } from './src/plugins/plugin'; diff --git a/packages/playwright/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/playwright/src/generators/convert-to-inferred/convert-to-inferred.ts index 856e3ca1f13fd..48f64569c2a76 100644 --- a/packages/playwright/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/playwright/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -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 ); diff --git a/packages/playwright/src/generators/init/init.ts b/packages/playwright/src/generators/init/init.ts index c4475a14d4fc2..493b4295e2ba7 100644 --- a/packages/playwright/src/generators/init/init.ts +++ b/packages/playwright/src/generators/init/init.ts @@ -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 ); diff --git a/packages/playwright/src/plugins/plugin.spec.ts b/packages/playwright/src/plugins/plugin.spec.ts index 1e9f75c95c670..a431927a7b79a 100644 --- a/packages/playwright/src/plugins/plugin.spec.ts +++ b/packages/playwright/src/plugins/plugin.spec.ts @@ -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", - ], - }, - }, - "root": ".", - "targets": { - "e2e": { - "cache": true, - "command": "playwright test", - "inputs": [ - "default", - "^production", - { - "externalDependencies": [ - "@playwright/test", - ], + expect(results).toMatchInlineSnapshot(` + [ + [ + "playwright.config.js", + { + "projects": { + ".": { + "metadata": { + "targetGroups": { + "E2E (CI)": [ + "e2e-ci", + ], + }, }, - ], - "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", - ], + "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", + ], + }, }, - ], - "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", - ], - }, - }, - "root": ".", - "targets": { - "e2e": { - "cache": true, - "command": "playwright test", - "inputs": [ - "default", - "^production", - { - "externalDependencies": [ - "@playwright/test", - ], + expect(results).toMatchInlineSnapshot(` + [ + [ + "playwright.config.js", + { + "projects": { + ".": { + "metadata": { + "targetGroups": { + "E2E (CI)": [ + "e2e-ci", + ], + }, }, - ], - "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", - ], + "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", + ], + }, }, - ], - "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", diff --git a/packages/playwright/src/plugins/plugin.ts b/packages/playwright/src/plugins/plugin.ts index c8057e54d7222..a1596b63cb612 100644 --- a/packages/playwright/src/plugins/plugin.ts +++ b/packages/playwright/src/plugins/plugin.ts @@ -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,68 +36,100 @@ interface NormalizedOptions { ciTargetName?: string; } -const cachePath = join(projectGraphCacheDirectory, 'playwright.hash'); - -const targetsCache = readTargetsCache(); - type PlaywrightTargets = Pick; -function readTargetsCache(): Record { +function readTargetsCache( + cachePath: string +): Record { return existsSync(cachePath) ? readJsonFile(cachePath) : {}; } -function writeTargetsToCache() { - const oldCache = readTargetsCache(); - writeJsonFile(cachePath, { - ...readTargetsCache, - targetsCache, - }); +function writeTargetsToCache( + cachePath: string, + results: Record +) { + writeJsonFile(cachePath, results); } -export const createDependencies: CreateDependencies = () => { - writeTargetsToCache(); - return []; -}; +const playwrightConfigGlob = '**/playwright.config.{js,ts,cjs,cts,mjs,mts}'; +export const createNodesV2: CreateNodesV2 = [ + playwrightConfigGlob, + async (configFilePaths, options, context) => { + const optionsHash = hashObject(options); + const cachePath = join( + projectGraphCacheDirectory, + `playwright-${optionsHash}.hash` + ); + 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 = [ - '**/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 {}; - } + 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, {}); + }, +]; - const normalizedOptions = normalizeOptions(options); +async function createNodesInternal( + configFilePath: string, + options: PlaywrightPluginOptions, + context: CreateNodesContext, + targetsCache: Record +) { + 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)), - ]); + const normalizedOptions = normalizeOptions(options); - targetsCache[hash] ??= await buildPlaywrightTargets( - configFilePath, - projectRoot, - normalizedOptions, - context - ); - const { targets, metadata } = targetsCache[hash]; - - return { - projects: { - [projectRoot]: { - root: projectRoot, - targets, - metadata, - }, + 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,