From 9ff004df4e9499622f17279a192151a1f6f88edd Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 1 Aug 2024 15:29:26 +0100 Subject: [PATCH 01/22] fix(vite): add migration to add dependsOn for preview --- .../add-depends-on-for-preview.spec.ts | 74 +++++++++++++++++++ .../add-depends-on-for-preview.ts | 26 +++++++ 2 files changed, 100 insertions(+) create mode 100644 packages/vite/src/migrations/update-19-5-5/add-depends-on-for-preview.spec.ts create mode 100644 packages/vite/src/migrations/update-19-5-5/add-depends-on-for-preview.ts diff --git a/packages/vite/src/migrations/update-19-5-5/add-depends-on-for-preview.spec.ts b/packages/vite/src/migrations/update-19-5-5/add-depends-on-for-preview.spec.ts new file mode 100644 index 0000000000000..e3882e44b4bdd --- /dev/null +++ b/packages/vite/src/migrations/update-19-5-5/add-depends-on-for-preview.spec.ts @@ -0,0 +1,74 @@ +import addDependsOnForPreview from './add-depends-on-for-preview'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { readJson } from '@nx/devkit'; + +describe('addDependsOnForPreview', () => { + it('should update when preview target exists in project.json', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + tree.write( + 'apps/app/project.json', + JSON.stringify({ + name: 'app', + root: 'apps/app', + projectType: 'application', + targets: { + preview: { + executor: '@nx/vite:preview-server', + }, + }, + }) + ); + + // ACT + await addDependsOnForPreview(tree); + + // ASSERT + expect(readJson(tree, 'apps/app/project.json').targets) + .toMatchInlineSnapshot(` + { + "preview": { + "dependsOn": [ + "build", + ], + "executor": "@nx/vite:preview-server", + }, + } + `); + }); + + it('should not update when preview target exists in project.json and has a dependsOn already', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + tree.write( + 'apps/app/project.json', + JSON.stringify({ + name: 'app', + root: 'apps/app', + projectType: 'application', + targets: { + preview: { + dependsOn: ['build'], + executor: '@nx/vite:preview-server', + }, + }, + }) + ); + + // ACT + await addDependsOnForPreview(tree); + + // ASSERT + expect(readJson(tree, 'apps/app/project.json').targets) + .toMatchInlineSnapshot(` + { + "preview": { + "dependsOn": [ + "build", + ], + "executor": "@nx/vite:preview-server", + }, + } + `); + }); +}); diff --git a/packages/vite/src/migrations/update-19-5-5/add-depends-on-for-preview.ts b/packages/vite/src/migrations/update-19-5-5/add-depends-on-for-preview.ts new file mode 100644 index 0000000000000..f78f0a360bc51 --- /dev/null +++ b/packages/vite/src/migrations/update-19-5-5/add-depends-on-for-preview.ts @@ -0,0 +1,26 @@ +import { + type Tree, + formatFiles, + readProjectConfiguration, + updateProjectConfiguration, +} from '@nx/devkit'; +import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils'; +import { VitePreviewServerExecutorOptions } from '../../executors/preview-server/schema'; + +export default async function (tree: Tree) { + forEachExecutorOptions( + tree, + '@nx/vite:preview-server', + (_, projectName, targetName) => { + const project = readProjectConfiguration(tree, projectName); + project.targets[targetName].dependsOn ??= []; + if (project.targets[targetName].dependsOn.includes('build')) { + return; + } + project.targets[targetName].dependsOn.push('build'); + updateProjectConfiguration(tree, projectName, project); + } + ); + + await formatFiles(tree); +} From 08c45a3eb35befefbf520b54ac50f39e9a91c0d4 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 1 Aug 2024 16:06:13 +0100 Subject: [PATCH 02/22] fix(testing): add migration for cypress should use preview for vite applications --- .../update-ci-webserver-for-vite.spec.ts | 262 ++++++++++++++++++ .../update-ci-webserver-for-vite.ts | 165 +++++++++++ 2 files changed, 427 insertions(+) create mode 100644 packages/cypress/src/migrations/update-19-5-5/update-ci-webserver-for-vite.spec.ts create mode 100644 packages/cypress/src/migrations/update-19-5-5/update-ci-webserver-for-vite.ts diff --git a/packages/cypress/src/migrations/update-19-5-5/update-ci-webserver-for-vite.spec.ts b/packages/cypress/src/migrations/update-19-5-5/update-ci-webserver-for-vite.spec.ts new file mode 100644 index 0000000000000..05dd4aaa0d817 --- /dev/null +++ b/packages/cypress/src/migrations/update-19-5-5/update-ci-webserver-for-vite.spec.ts @@ -0,0 +1,262 @@ +import updateCiWebserverForVite from './update-ci-webserver-for-vite'; +import { + type Tree, + type ProjectGraph, + readNxJson, + updateNxJson, +} from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; + +let projectGraph: ProjectGraph; +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: jest.fn().mockImplementation(async () => { + return projectGraph; + }), +})); + +describe('updateCiWebserverForVite', () => { + let tree: Tree; + let tempFs: TempFs; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + tempFs = new TempFs('add-e2e-ci'); + tree.root = tempFs.tempDir; + projectGraph = { + nodes: {}, + dependencies: {}, + externalNodes: {}, + }; + }); + + afterEach(() => { + tempFs.reset(); + }); + + it('should do nothing if vite is not found for application', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs, { + buildTargetName: 'build', + ciTargetName: 'e2e-ci', + appName: 'app', + noVite: true, + }); + + // ACT + await updateCiWebserverForVite(tree); + + // ASSERT + expect(tree.read('app-e2e/cypress.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + import { defineConfig } from 'cypress'; + export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { + cypressDir: 'src', + bundler: 'vite', + webServerCommands: { + default: 'nx run app:serve', + production: 'nx run app:preview', + }, + ciWebServerCommand: 'nx run app:serve-static', + }), + baseUrl: 'http://localhost:4200', + }, + }); + " + `); + }); + + it('should update ciWebServerCommand to preview for vite app', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }, + { + plugin: '@nx/vite/plugin', + options: { + buildTargetName: 'build', + previewTargetName: 'preview', + }, + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs); + + // ACT + await updateCiWebserverForVite(tree); + + // ASSERT + expect(tree.read('app-e2e/cypress.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + import { defineConfig } from 'cypress'; + export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { + cypressDir: 'src', + bundler: 'vite', + webServerCommands: { + default: 'nx run app:serve', + production: 'nx run app:preview', + }, + ciWebServerCommand: 'nx run app:preview', + ciBaseUrl: 'http://localhost:4300', + }), + baseUrl: 'http://localhost:4200', + }, + }); + " + `); + }); +}); + +function addProject( + tree: Tree, + tempFs: TempFs, + overrides: { + ciTargetName: string; + buildTargetName: string; + appName: string; + noCi?: boolean; + noVite?: boolean; + } = { ciTargetName: 'e2e-ci', buildTargetName: 'build', appName: 'app' } +) { + const appProjectConfig = { + name: overrides.appName, + root: overrides.appName, + sourceRoot: `${overrides.appName}/src`, + projectType: 'application', + }; + const viteConfig = `/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/${overrides.appName}', + server: { + port: 4200, + host: 'localhost', + }, + preview: { + port: 4300, + host: 'localhost', + }, + plugins: [react(), nxViteTsPaths()], + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + build: { + outDir: '../../dist/${overrides.appName}', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, +});`; + + const e2eProjectConfig = { + name: `${overrides.appName}-e2e`, + root: `${overrides.appName}-e2e`, + sourceRoot: `${overrides.appName}-e2e/src`, + projectType: 'application', + }; + + const cypressConfig = `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; +import { defineConfig } from 'cypress'; +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { + cypressDir: 'src', + bundler: 'vite', + webServerCommands: { + default: 'nx run ${overrides.appName}:serve', + production: 'nx run ${overrides.appName}:preview', + }, + ${ + !overrides.noCi + ? `ciWebServerCommand: 'nx run ${overrides.appName}:serve-static',` + : '' + } + }), + baseUrl: 'http://localhost:4200', + }, +}); +`; + + if (!overrides.noVite) { + tree.write(`${overrides.appName}/vite.config.ts`, viteConfig); + } + tree.write( + `${overrides.appName}/project.json`, + JSON.stringify(appProjectConfig) + ); + tree.write(`${overrides.appName}-e2e/cypress.config.ts`, cypressConfig); + tree.write( + `${overrides.appName}-e2e/project.json`, + JSON.stringify(e2eProjectConfig) + ); + if (!overrides.noVite) { + tempFs.createFile(`${overrides.appName}/vite.config.ts`, viteConfig); + } + tempFs.createFilesSync({ + [`${overrides.appName}/project.json`]: JSON.stringify(appProjectConfig), + [`${overrides.appName}-e2e/cypress.config.ts`]: cypressConfig, + [`${overrides.appName}-e2e/project.json`]: JSON.stringify(e2eProjectConfig), + }); + + projectGraph.nodes[overrides.appName] = { + name: overrides.appName, + type: 'app', + data: { + projectType: 'application', + root: overrides.appName, + targets: { + [overrides.buildTargetName]: {}, + 'serve-static': { + options: { + buildTarget: overrides.buildTargetName, + }, + }, + }, + }, + }; + + projectGraph.nodes[`${overrides.appName}-e2e`] = { + name: `${overrides.appName}-e2e`, + type: 'app', + data: { + projectType: 'application', + root: `${overrides.appName}-e2e`, + targets: { + e2e: {}, + [overrides.ciTargetName]: {}, + }, + }, + }; +} diff --git a/packages/cypress/src/migrations/update-19-5-5/update-ci-webserver-for-vite.ts b/packages/cypress/src/migrations/update-19-5-5/update-ci-webserver-for-vite.ts new file mode 100644 index 0000000000000..86ace3649e6f9 --- /dev/null +++ b/packages/cypress/src/migrations/update-19-5-5/update-ci-webserver-for-vite.ts @@ -0,0 +1,165 @@ +import { + type Tree, + CreateNodesV2, + createProjectGraphAsync, + readNxJson, + parseTargetString, + joinPathFragments, + PluginConfiguration, + CreateNodes, + formatFiles, +} from '@nx/devkit'; +import { + retrieveProjectConfigurations, + LoadedNxPlugin, + ProjectConfigurationsError, + findMatchingConfigFiles, +} from 'nx/src/devkit-internals'; +import { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import { CypressPluginOptions } from '../../plugins/plugin'; + +export default async function (tree: Tree) { + const pluginName = '@nx/cypress/plugin'; + const graph = await createProjectGraphAsync(); + const nxJson = readNxJson(tree); + const matchingPluginRegistrations = nxJson.plugins?.filter((p) => + typeof p === 'string' ? p === pluginName : p.plugin === pluginName + ); + + const { + createNodesV2, + }: { createNodesV2: CreateNodesV2 } = await import( + pluginName + ); + + for (const plugin of matchingPluginRegistrations) { + let projectConfigs: ConfigurationResult; + try { + const loadedPlugin = new LoadedNxPlugin( + { createNodesV2, name: pluginName }, + plugin + ); + projectConfigs = await retrieveProjectConfigurations( + [loadedPlugin], + tree.root, + nxJson + ); + } catch (e) { + if (e instanceof ProjectConfigurationsError) { + projectConfigs = e.partialProjectConfigurationsResult; + } else { + throw e; + } + } + + for (const configFile of projectConfigs.matchingProjectFiles) { + const configFileContents = tree.read(configFile, 'utf-8'); + if (!configFileContents.includes('ciWebServerCommand')) { + continue; + } + + const ast = tsquery.ast(configFileContents); + const CI_WEBSERVER_COMMAND_SELECTOR = + 'ObjectLiteralExpression PropertyAssignment:has(Identifier[name=ciWebServerCommand]) > StringLiteral'; + const nodes = tsquery(ast, CI_WEBSERVER_COMMAND_SELECTOR, { + visitAllChildren: true, + }); + if (!nodes.length) { + continue; + } + const ciWebServerCommand = nodes[0].getText(); + const NX_TARGET_REGEX = "(?<=nx run )[^']+"; + const matches = ciWebServerCommand.match(NX_TARGET_REGEX); + if (!matches) { + continue; + } + const targetString = matches[0]; + const { project, target, configuration } = parseTargetString( + targetString, + graph + ); + + const pathToViteConfig = [ + joinPathFragments(graph.nodes[project].data.root, 'vite.config.ts'), + joinPathFragments(graph.nodes[project].data.root, 'vite.config.js'), + ].find((p) => tree.exists(p)); + + if (!pathToViteConfig) { + continue; + } + + const viteConfigContents = tree.read(pathToViteConfig, 'utf-8'); + if (!viteConfigContents.includes('preview:')) { + continue; + } + + const matchingVitePlugin = await findPluginForConfigFile( + tree, + '@nx/vite/plugin', + pathToViteConfig + ); + const previewTargetName = matchingVitePlugin + ? typeof matchingVitePlugin === 'string' + ? 'preview' + : (matchingVitePlugin.options as any)?.previewTargetName ?? 'preview' + : 'preview'; + + tree.write( + configFile, + `${configFileContents.slice( + 0, + nodes[0].getStart() + )}'nx run ${project}:${previewTargetName}', + ciBaseUrl: "http://localhost:4300"${configFileContents.slice( + nodes[0].getEnd() + )}` + ); + } + } + + await formatFiles(tree); +} + +async function findPluginForConfigFile( + tree: Tree, + pluginName: string, + pathToConfigFile: string +): Promise { + const nxJson = readNxJson(tree); + if (!nxJson.plugins) { + return; + } + + const pluginRegistrations: PluginConfiguration[] = nxJson.plugins.filter( + (p) => (typeof p === 'string' ? p === pluginName : p.plugin === pluginName) + ); + + for (const plugin of pluginRegistrations) { + if (typeof plugin === 'string') { + return plugin; + } + + if (!plugin.include && !plugin.exclude) { + return plugin; + } + + if (plugin.include || plugin.exclude) { + const resolvedPlugin: { + createNodes?: CreateNodes; + createNodesV2?: CreateNodesV2; + } = await import(pluginName); + const pluginGlob = + resolvedPlugin.createNodesV2?.[0] ?? resolvedPlugin.createNodes?.[0]; + const matchingConfigFile = findMatchingConfigFiles( + [pathToConfigFile], + pluginGlob, + plugin.include, + plugin.exclude + ); + if (matchingConfigFile.length) { + return plugin; + } + } + } +} From 69546338cf0dfe40366acd90826ed1e94b2d1777 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 1 Aug 2024 17:01:27 +0100 Subject: [PATCH 03/22] fix(testing): add migration for playwright to use serve-static or preview for command --- ...e-serve-static-preview-for-command.spec.ts | 234 ++++++++++++++++++ .../use-serve-static-preview-for-command.ts | 142 +++++++++++ 2 files changed, 376 insertions(+) create mode 100644 packages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.spec.ts create mode 100644 packages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.ts diff --git a/packages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.spec.ts b/packages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.spec.ts new file mode 100644 index 0000000000000..0122a55e20c47 --- /dev/null +++ b/packages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.spec.ts @@ -0,0 +1,234 @@ +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { ProjectGraph, type Tree } from '@nx/devkit'; +import useServeStaticPreviewForCommand from './use-serve-static-preview-for-command'; +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; + +let projectGraph: ProjectGraph; +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: jest.fn().mockImplementation(async () => { + return projectGraph; + }), +})); + +describe('useServeStaticPreviewForCommand', () => { + let tree: Tree; + let tempFs: TempFs; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + tempFs = new TempFs('add-e2e-ci'); + tree.root = tempFs.tempDir; + projectGraph = { + nodes: {}, + dependencies: {}, + externalNodes: {}, + }; + }); + + afterEach(() => { + tempFs.reset(); + }); + + it('should update when it does not use serve-static for non-vite', async () => { + // ARRANGE + addProject(tree, tempFs, { noVite: true }); + + // ACT + await useServeStaticPreviewForCommand(tree); + + // ASSERT + expect(tree.read('app-e2e/playwright.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "import { defineConfig, devices } from '@playwright/test'; + import { nxE2EPreset } from '@nx/playwright/preset'; + + import { workspaceRoot } from '@nx/devkit'; + + const baseURL = process.env['BASE_URL'] || 'http://localhost:4200'; + + export default defineConfig({ + ...nxE2EPreset(__filename, { testDir: './src' }), + use: { + baseURL, + trace: 'on-first-retry', + }, + webServer: { + command: 'npx nx run app:serve-static', + url: 'http://localhost:4200', + reuseExistingServer: !process.env.CI, + cwd: workspaceRoot, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + }); + " + `); + }); + it('should update when it does not use preview for vite', async () => { + // ARRANGE + addProject(tree, tempFs); + + // ACT + await useServeStaticPreviewForCommand(tree); + + // ASSERT + expect(tree.read('app-e2e/playwright.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "import { defineConfig, devices } from '@playwright/test'; + import { nxE2EPreset } from '@nx/playwright/preset'; + + import { workspaceRoot } from '@nx/devkit'; + + const baseURL = process.env['BASE_URL'] || 'http://localhost:4300'; + + export default defineConfig({ + ...nxE2EPreset(__filename, { testDir: './src' }), + use: { + baseURL, + trace: 'on-first-retry', + }, + webServer: { + command: 'npx nx run app:preview', + url: 'http://localhost:4300', + reuseExistingServer: !process.env.CI, + cwd: workspaceRoot, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + }); + " + `); + }); +}); + +const basePlaywrightConfig = ( + appName: string +) => `import { defineConfig, devices } from '@playwright/test'; +import { nxE2EPreset } from '@nx/playwright/preset'; + +import { workspaceRoot } from '@nx/devkit'; + +const baseURL = process.env['BASE_URL'] || 'http://localhost:4200'; + +export default defineConfig({ + ...nxE2EPreset(__filename, { testDir: './src' }), + use: { + baseURL, + trace: 'on-first-retry', + }, + webServer: { + command: 'npx nx run ${appName}:serve', + url: 'http://localhost:4200', + reuseExistingServer: !process.env.CI, + cwd: workspaceRoot, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +});`; + +const viteConfig = `/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/app', + server: { + port: 4200, + host: 'localhost', + }, + preview: { + port: 4300, + host: 'localhost', + }, + plugins: [react(), nxViteTsPaths()], + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + build: { + outDir: '../../dist/app', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, +});`; + +function addProject( + tree: Tree, + tempFs: TempFs, + overrides: { + noVite?: boolean; + } = {} +) { + const appProjectConfig = { + name: 'app', + root: 'app', + sourceRoot: `${'app'}/src`, + projectType: 'application', + }; + + const e2eProjectConfig = { + name: `app-e2e`, + root: `app-e2e`, + sourceRoot: `app-e2e/src`, + projectType: 'application', + }; + + if (!overrides.noVite) { + tree.write(`app/vite.config.ts`, viteConfig); + } else { + tree.write(`app/webpack.config.ts`, ``); + } + + tree.write(`app/project.json`, JSON.stringify(appProjectConfig)); + tree.write(`app-e2e/playwright.config.ts`, basePlaywrightConfig('app')); + tree.write(`app-e2e/project.json`, JSON.stringify(e2eProjectConfig)); + if (!overrides.noVite) { + tempFs.createFile(`app/vite.config.ts`, viteConfig); + } else { + tempFs.createFile(`app/webpack.config.ts`, ``); + } + tempFs.createFilesSync({ + [`app/project.json`]: JSON.stringify(appProjectConfig), + [`app-e2e/playwright.config.ts`]: basePlaywrightConfig('app'), + [`app-e2e/project.json`]: JSON.stringify(e2eProjectConfig), + }); + + projectGraph.nodes['app'] = { + name: 'app', + type: 'app', + data: { + projectType: 'application', + root: 'app', + targets: {}, + }, + }; + + projectGraph.nodes[`app-e2e`] = { + name: `app-e2e`, + type: 'app', + data: { + projectType: 'application', + root: `app-e2e`, + targets: { + e2e: {}, + }, + }, + }; +} diff --git a/packages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.ts b/packages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.ts new file mode 100644 index 0000000000000..c98bfff41a031 --- /dev/null +++ b/packages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.ts @@ -0,0 +1,142 @@ +import { + createProjectGraphAsync, + formatFiles, + getPackageManagerCommand, + joinPathFragments, + parseTargetString, + type Tree, + visitNotIgnoredFiles, +} from '@nx/devkit'; +import { tsquery } from '@phenomnomnominal/tsquery'; + +export default async function (tree: Tree) { + const graph = await createProjectGraphAsync(); + visitNotIgnoredFiles(tree, '', (path) => { + if (!path.endsWith('playwright.config.ts')) { + return; + } + + let playwrightConfigFileContents = tree.read(path, 'utf-8'); + + const WEBSERVER_COMMAND_SELECTOR = + 'PropertyAssignment:has(Identifier[name=webServer]) PropertyAssignment:has(Identifier[name=command]) > StringLiteral'; + let ast = tsquery.ast(playwrightConfigFileContents); + const nodes = tsquery(ast, WEBSERVER_COMMAND_SELECTOR, { + visitAllChildren: true, + }); + if (!nodes.length) { + return; + } + + const commandValueNode = nodes[0]; + const command = commandValueNode.getText(); + let project: string; + if (command.includes('nx run')) { + const NX_TARGET_REGEX = "(?<=nx run )[^']+"; + const matches = command.match(NX_TARGET_REGEX); + if (!matches) { + return; + } + const targetString = matches[0]; + const parsedTargetString = parseTargetString(targetString, graph); + + if ( + parsedTargetString.target === 'serve-static' || + parsedTargetString.target === 'preview' + ) { + return; + } + + project = parsedTargetString.project; + } else { + const NX_PROJECT_REGEX = "(?<=nx [^ ]+ )[^']+"; + const matches = command.match(NX_PROJECT_REGEX); + if (!matches) { + return; + } + project = matches[0]; + } + + const pathToViteConfig = [ + joinPathFragments(graph.nodes[project].data.root, 'vite.config.ts'), + joinPathFragments(graph.nodes[project].data.root, 'vite.config.js'), + ].find((p) => tree.exists(p)); + + if (!pathToViteConfig) { + const newCommand = `${ + getPackageManagerCommand().exec + } nx run ${project}:serve-static`; + tree.write( + path, + `${playwrightConfigFileContents.slice( + 0, + commandValueNode.getStart() + )}"${newCommand}"${playwrightConfigFileContents.slice( + commandValueNode.getEnd() + )}` + ); + } else { + const newCommand = `${ + getPackageManagerCommand().exec + } nx run ${project}:preview`; + tree.write( + path, + `${playwrightConfigFileContents.slice( + 0, + commandValueNode.getStart() + )}"${newCommand}"${playwrightConfigFileContents.slice( + commandValueNode.getEnd() + )}` + ); + playwrightConfigFileContents = tree.read(path, 'utf-8'); + ast = tsquery.ast(playwrightConfigFileContents); + + const BASE_URL_SELECTOR = + 'VariableDeclaration:has(Identifier[name=baseURL])'; + const baseUrlNodes = tsquery(ast, BASE_URL_SELECTOR, { + visitAllChildren: true, + }); + if (!baseUrlNodes.length) { + return; + } + + const baseUrlNode = baseUrlNodes[0]; + const newBaseUrlVariableDeclaration = + "baseURL = process.env['BASE_URL'] || 'http://localhost:4300';"; + tree.write( + path, + `${playwrightConfigFileContents.slice( + 0, + baseUrlNode.getStart() + )}${newBaseUrlVariableDeclaration}${playwrightConfigFileContents.slice( + baseUrlNode.getEnd() + )}` + ); + + playwrightConfigFileContents = tree.read(path, 'utf-8'); + ast = tsquery.ast(playwrightConfigFileContents); + const WEB_SERVER_URL_SELECTOR = + 'PropertyAssignment:has(Identifier[name=webServer]) PropertyAssignment:has(Identifier[name=url]) > StringLiteral'; + const webServerUrlNodes = tsquery(ast, WEB_SERVER_URL_SELECTOR, { + visitAllChildren: true, + }); + if (!webServerUrlNodes.length) { + return; + } + + const webServerUrlNode = webServerUrlNodes[0]; + const newWebServerUrl = "'http://localhost:4300'"; + tree.write( + path, + `${playwrightConfigFileContents.slice( + 0, + webServerUrlNode.getStart() + )}${newWebServerUrl}${playwrightConfigFileContents.slice( + webServerUrlNode.getEnd() + )}` + ); + } + }); + + await formatFiles(tree); +} From b14898151d984e8dbca95fd676326d6f13459c06 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 2 Aug 2024 08:54:47 +0100 Subject: [PATCH 04/22] chore(testing): point migrations to 19.6 release --- .../update-ci-webserver-for-vite.spec.ts | 262 ------------------ .../update-ci-webserver-for-vite.ts | 165 ----------- ...e-serve-static-preview-for-command.spec.ts | 234 ---------------- .../use-serve-static-preview-for-command.ts | 142 ---------- .../add-depends-on-for-preview.spec.ts | 74 ----- .../add-depends-on-for-preview.ts | 26 -- 6 files changed, 903 deletions(-) delete mode 100644 packages/cypress/src/migrations/update-19-5-5/update-ci-webserver-for-vite.spec.ts delete mode 100644 packages/cypress/src/migrations/update-19-5-5/update-ci-webserver-for-vite.ts delete mode 100644 packages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.spec.ts delete mode 100644 packages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.ts delete mode 100644 packages/vite/src/migrations/update-19-5-5/add-depends-on-for-preview.spec.ts delete mode 100644 packages/vite/src/migrations/update-19-5-5/add-depends-on-for-preview.ts diff --git a/packages/cypress/src/migrations/update-19-5-5/update-ci-webserver-for-vite.spec.ts b/packages/cypress/src/migrations/update-19-5-5/update-ci-webserver-for-vite.spec.ts deleted file mode 100644 index 05dd4aaa0d817..0000000000000 --- a/packages/cypress/src/migrations/update-19-5-5/update-ci-webserver-for-vite.spec.ts +++ /dev/null @@ -1,262 +0,0 @@ -import updateCiWebserverForVite from './update-ci-webserver-for-vite'; -import { - type Tree, - type ProjectGraph, - readNxJson, - updateNxJson, -} from '@nx/devkit'; -import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; -import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; - -let projectGraph: ProjectGraph; -jest.mock('@nx/devkit', () => ({ - ...jest.requireActual('@nx/devkit'), - createProjectGraphAsync: jest.fn().mockImplementation(async () => { - return projectGraph; - }), -})); - -describe('updateCiWebserverForVite', () => { - let tree: Tree; - let tempFs: TempFs; - - beforeEach(() => { - tree = createTreeWithEmptyWorkspace(); - tempFs = new TempFs('add-e2e-ci'); - tree.root = tempFs.tempDir; - projectGraph = { - nodes: {}, - dependencies: {}, - externalNodes: {}, - }; - }); - - afterEach(() => { - tempFs.reset(); - }); - - it('should do nothing if vite is not found for application', async () => { - // ARRANGE - const nxJson = readNxJson(tree); - nxJson.plugins = [ - { - plugin: '@nx/cypress/plugin', - options: { - targetName: 'e2e', - ciTargetName: 'e2e-ci', - }, - }, - ]; - updateNxJson(tree, nxJson); - - addProject(tree, tempFs, { - buildTargetName: 'build', - ciTargetName: 'e2e-ci', - appName: 'app', - noVite: true, - }); - - // ACT - await updateCiWebserverForVite(tree); - - // ASSERT - expect(tree.read('app-e2e/cypress.config.ts', 'utf-8')) - .toMatchInlineSnapshot(` - "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; - import { defineConfig } from 'cypress'; - export default defineConfig({ - e2e: { - ...nxE2EPreset(__filename, { - cypressDir: 'src', - bundler: 'vite', - webServerCommands: { - default: 'nx run app:serve', - production: 'nx run app:preview', - }, - ciWebServerCommand: 'nx run app:serve-static', - }), - baseUrl: 'http://localhost:4200', - }, - }); - " - `); - }); - - it('should update ciWebServerCommand to preview for vite app', async () => { - // ARRANGE - const nxJson = readNxJson(tree); - nxJson.plugins = [ - { - plugin: '@nx/cypress/plugin', - options: { - targetName: 'e2e', - ciTargetName: 'e2e-ci', - }, - }, - { - plugin: '@nx/vite/plugin', - options: { - buildTargetName: 'build', - previewTargetName: 'preview', - }, - }, - ]; - updateNxJson(tree, nxJson); - - addProject(tree, tempFs); - - // ACT - await updateCiWebserverForVite(tree); - - // ASSERT - expect(tree.read('app-e2e/cypress.config.ts', 'utf-8')) - .toMatchInlineSnapshot(` - "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; - import { defineConfig } from 'cypress'; - export default defineConfig({ - e2e: { - ...nxE2EPreset(__filename, { - cypressDir: 'src', - bundler: 'vite', - webServerCommands: { - default: 'nx run app:serve', - production: 'nx run app:preview', - }, - ciWebServerCommand: 'nx run app:preview', - ciBaseUrl: 'http://localhost:4300', - }), - baseUrl: 'http://localhost:4200', - }, - }); - " - `); - }); -}); - -function addProject( - tree: Tree, - tempFs: TempFs, - overrides: { - ciTargetName: string; - buildTargetName: string; - appName: string; - noCi?: boolean; - noVite?: boolean; - } = { ciTargetName: 'e2e-ci', buildTargetName: 'build', appName: 'app' } -) { - const appProjectConfig = { - name: overrides.appName, - root: overrides.appName, - sourceRoot: `${overrides.appName}/src`, - projectType: 'application', - }; - const viteConfig = `/// -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; -export default defineConfig({ - root: __dirname, - cacheDir: '../../node_modules/.vite/${overrides.appName}', - server: { - port: 4200, - host: 'localhost', - }, - preview: { - port: 4300, - host: 'localhost', - }, - plugins: [react(), nxViteTsPaths()], - // Uncomment this if you are using workers. - // worker: { - // plugins: [ nxViteTsPaths() ], - // }, - build: { - outDir: '../../dist/${overrides.appName}', - emptyOutDir: true, - reportCompressedSize: true, - commonjsOptions: { - transformMixedEsModules: true, - }, - }, -});`; - - const e2eProjectConfig = { - name: `${overrides.appName}-e2e`, - root: `${overrides.appName}-e2e`, - sourceRoot: `${overrides.appName}-e2e/src`, - projectType: 'application', - }; - - const cypressConfig = `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; -import { defineConfig } from 'cypress'; -export default defineConfig({ - e2e: { - ...nxE2EPreset(__filename, { - cypressDir: 'src', - bundler: 'vite', - webServerCommands: { - default: 'nx run ${overrides.appName}:serve', - production: 'nx run ${overrides.appName}:preview', - }, - ${ - !overrides.noCi - ? `ciWebServerCommand: 'nx run ${overrides.appName}:serve-static',` - : '' - } - }), - baseUrl: 'http://localhost:4200', - }, -}); -`; - - if (!overrides.noVite) { - tree.write(`${overrides.appName}/vite.config.ts`, viteConfig); - } - tree.write( - `${overrides.appName}/project.json`, - JSON.stringify(appProjectConfig) - ); - tree.write(`${overrides.appName}-e2e/cypress.config.ts`, cypressConfig); - tree.write( - `${overrides.appName}-e2e/project.json`, - JSON.stringify(e2eProjectConfig) - ); - if (!overrides.noVite) { - tempFs.createFile(`${overrides.appName}/vite.config.ts`, viteConfig); - } - tempFs.createFilesSync({ - [`${overrides.appName}/project.json`]: JSON.stringify(appProjectConfig), - [`${overrides.appName}-e2e/cypress.config.ts`]: cypressConfig, - [`${overrides.appName}-e2e/project.json`]: JSON.stringify(e2eProjectConfig), - }); - - projectGraph.nodes[overrides.appName] = { - name: overrides.appName, - type: 'app', - data: { - projectType: 'application', - root: overrides.appName, - targets: { - [overrides.buildTargetName]: {}, - 'serve-static': { - options: { - buildTarget: overrides.buildTargetName, - }, - }, - }, - }, - }; - - projectGraph.nodes[`${overrides.appName}-e2e`] = { - name: `${overrides.appName}-e2e`, - type: 'app', - data: { - projectType: 'application', - root: `${overrides.appName}-e2e`, - targets: { - e2e: {}, - [overrides.ciTargetName]: {}, - }, - }, - }; -} diff --git a/packages/cypress/src/migrations/update-19-5-5/update-ci-webserver-for-vite.ts b/packages/cypress/src/migrations/update-19-5-5/update-ci-webserver-for-vite.ts deleted file mode 100644 index 86ace3649e6f9..0000000000000 --- a/packages/cypress/src/migrations/update-19-5-5/update-ci-webserver-for-vite.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { - type Tree, - CreateNodesV2, - createProjectGraphAsync, - readNxJson, - parseTargetString, - joinPathFragments, - PluginConfiguration, - CreateNodes, - formatFiles, -} from '@nx/devkit'; -import { - retrieveProjectConfigurations, - LoadedNxPlugin, - ProjectConfigurationsError, - findMatchingConfigFiles, -} from 'nx/src/devkit-internals'; -import { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils'; -import { tsquery } from '@phenomnomnominal/tsquery'; -import { CypressPluginOptions } from '../../plugins/plugin'; - -export default async function (tree: Tree) { - const pluginName = '@nx/cypress/plugin'; - const graph = await createProjectGraphAsync(); - const nxJson = readNxJson(tree); - const matchingPluginRegistrations = nxJson.plugins?.filter((p) => - typeof p === 'string' ? p === pluginName : p.plugin === pluginName - ); - - const { - createNodesV2, - }: { createNodesV2: CreateNodesV2 } = await import( - pluginName - ); - - for (const plugin of matchingPluginRegistrations) { - let projectConfigs: ConfigurationResult; - try { - const loadedPlugin = new LoadedNxPlugin( - { createNodesV2, name: pluginName }, - plugin - ); - projectConfigs = await retrieveProjectConfigurations( - [loadedPlugin], - tree.root, - nxJson - ); - } catch (e) { - if (e instanceof ProjectConfigurationsError) { - projectConfigs = e.partialProjectConfigurationsResult; - } else { - throw e; - } - } - - for (const configFile of projectConfigs.matchingProjectFiles) { - const configFileContents = tree.read(configFile, 'utf-8'); - if (!configFileContents.includes('ciWebServerCommand')) { - continue; - } - - const ast = tsquery.ast(configFileContents); - const CI_WEBSERVER_COMMAND_SELECTOR = - 'ObjectLiteralExpression PropertyAssignment:has(Identifier[name=ciWebServerCommand]) > StringLiteral'; - const nodes = tsquery(ast, CI_WEBSERVER_COMMAND_SELECTOR, { - visitAllChildren: true, - }); - if (!nodes.length) { - continue; - } - const ciWebServerCommand = nodes[0].getText(); - const NX_TARGET_REGEX = "(?<=nx run )[^']+"; - const matches = ciWebServerCommand.match(NX_TARGET_REGEX); - if (!matches) { - continue; - } - const targetString = matches[0]; - const { project, target, configuration } = parseTargetString( - targetString, - graph - ); - - const pathToViteConfig = [ - joinPathFragments(graph.nodes[project].data.root, 'vite.config.ts'), - joinPathFragments(graph.nodes[project].data.root, 'vite.config.js'), - ].find((p) => tree.exists(p)); - - if (!pathToViteConfig) { - continue; - } - - const viteConfigContents = tree.read(pathToViteConfig, 'utf-8'); - if (!viteConfigContents.includes('preview:')) { - continue; - } - - const matchingVitePlugin = await findPluginForConfigFile( - tree, - '@nx/vite/plugin', - pathToViteConfig - ); - const previewTargetName = matchingVitePlugin - ? typeof matchingVitePlugin === 'string' - ? 'preview' - : (matchingVitePlugin.options as any)?.previewTargetName ?? 'preview' - : 'preview'; - - tree.write( - configFile, - `${configFileContents.slice( - 0, - nodes[0].getStart() - )}'nx run ${project}:${previewTargetName}', - ciBaseUrl: "http://localhost:4300"${configFileContents.slice( - nodes[0].getEnd() - )}` - ); - } - } - - await formatFiles(tree); -} - -async function findPluginForConfigFile( - tree: Tree, - pluginName: string, - pathToConfigFile: string -): Promise { - const nxJson = readNxJson(tree); - if (!nxJson.plugins) { - return; - } - - const pluginRegistrations: PluginConfiguration[] = nxJson.plugins.filter( - (p) => (typeof p === 'string' ? p === pluginName : p.plugin === pluginName) - ); - - for (const plugin of pluginRegistrations) { - if (typeof plugin === 'string') { - return plugin; - } - - if (!plugin.include && !plugin.exclude) { - return plugin; - } - - if (plugin.include || plugin.exclude) { - const resolvedPlugin: { - createNodes?: CreateNodes; - createNodesV2?: CreateNodesV2; - } = await import(pluginName); - const pluginGlob = - resolvedPlugin.createNodesV2?.[0] ?? resolvedPlugin.createNodes?.[0]; - const matchingConfigFile = findMatchingConfigFiles( - [pathToConfigFile], - pluginGlob, - plugin.include, - plugin.exclude - ); - if (matchingConfigFile.length) { - return plugin; - } - } - } -} diff --git a/packages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.spec.ts b/packages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.spec.ts deleted file mode 100644 index 0122a55e20c47..0000000000000 --- a/packages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.spec.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; -import { ProjectGraph, type Tree } from '@nx/devkit'; -import useServeStaticPreviewForCommand from './use-serve-static-preview-for-command'; -import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; - -let projectGraph: ProjectGraph; -jest.mock('@nx/devkit', () => ({ - ...jest.requireActual('@nx/devkit'), - createProjectGraphAsync: jest.fn().mockImplementation(async () => { - return projectGraph; - }), -})); - -describe('useServeStaticPreviewForCommand', () => { - let tree: Tree; - let tempFs: TempFs; - - beforeEach(() => { - tree = createTreeWithEmptyWorkspace(); - tempFs = new TempFs('add-e2e-ci'); - tree.root = tempFs.tempDir; - projectGraph = { - nodes: {}, - dependencies: {}, - externalNodes: {}, - }; - }); - - afterEach(() => { - tempFs.reset(); - }); - - it('should update when it does not use serve-static for non-vite', async () => { - // ARRANGE - addProject(tree, tempFs, { noVite: true }); - - // ACT - await useServeStaticPreviewForCommand(tree); - - // ASSERT - expect(tree.read('app-e2e/playwright.config.ts', 'utf-8')) - .toMatchInlineSnapshot(` - "import { defineConfig, devices } from '@playwright/test'; - import { nxE2EPreset } from '@nx/playwright/preset'; - - import { workspaceRoot } from '@nx/devkit'; - - const baseURL = process.env['BASE_URL'] || 'http://localhost:4200'; - - export default defineConfig({ - ...nxE2EPreset(__filename, { testDir: './src' }), - use: { - baseURL, - trace: 'on-first-retry', - }, - webServer: { - command: 'npx nx run app:serve-static', - url: 'http://localhost:4200', - reuseExistingServer: !process.env.CI, - cwd: workspaceRoot, - }, - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - ], - }); - " - `); - }); - it('should update when it does not use preview for vite', async () => { - // ARRANGE - addProject(tree, tempFs); - - // ACT - await useServeStaticPreviewForCommand(tree); - - // ASSERT - expect(tree.read('app-e2e/playwright.config.ts', 'utf-8')) - .toMatchInlineSnapshot(` - "import { defineConfig, devices } from '@playwright/test'; - import { nxE2EPreset } from '@nx/playwright/preset'; - - import { workspaceRoot } from '@nx/devkit'; - - const baseURL = process.env['BASE_URL'] || 'http://localhost:4300'; - - export default defineConfig({ - ...nxE2EPreset(__filename, { testDir: './src' }), - use: { - baseURL, - trace: 'on-first-retry', - }, - webServer: { - command: 'npx nx run app:preview', - url: 'http://localhost:4300', - reuseExistingServer: !process.env.CI, - cwd: workspaceRoot, - }, - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - ], - }); - " - `); - }); -}); - -const basePlaywrightConfig = ( - appName: string -) => `import { defineConfig, devices } from '@playwright/test'; -import { nxE2EPreset } from '@nx/playwright/preset'; - -import { workspaceRoot } from '@nx/devkit'; - -const baseURL = process.env['BASE_URL'] || 'http://localhost:4200'; - -export default defineConfig({ - ...nxE2EPreset(__filename, { testDir: './src' }), - use: { - baseURL, - trace: 'on-first-retry', - }, - webServer: { - command: 'npx nx run ${appName}:serve', - url: 'http://localhost:4200', - reuseExistingServer: !process.env.CI, - cwd: workspaceRoot, - }, - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - ], -});`; - -const viteConfig = `/// -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; -export default defineConfig({ - root: __dirname, - cacheDir: '../../node_modules/.vite/app', - server: { - port: 4200, - host: 'localhost', - }, - preview: { - port: 4300, - host: 'localhost', - }, - plugins: [react(), nxViteTsPaths()], - // Uncomment this if you are using workers. - // worker: { - // plugins: [ nxViteTsPaths() ], - // }, - build: { - outDir: '../../dist/app', - emptyOutDir: true, - reportCompressedSize: true, - commonjsOptions: { - transformMixedEsModules: true, - }, - }, -});`; - -function addProject( - tree: Tree, - tempFs: TempFs, - overrides: { - noVite?: boolean; - } = {} -) { - const appProjectConfig = { - name: 'app', - root: 'app', - sourceRoot: `${'app'}/src`, - projectType: 'application', - }; - - const e2eProjectConfig = { - name: `app-e2e`, - root: `app-e2e`, - sourceRoot: `app-e2e/src`, - projectType: 'application', - }; - - if (!overrides.noVite) { - tree.write(`app/vite.config.ts`, viteConfig); - } else { - tree.write(`app/webpack.config.ts`, ``); - } - - tree.write(`app/project.json`, JSON.stringify(appProjectConfig)); - tree.write(`app-e2e/playwright.config.ts`, basePlaywrightConfig('app')); - tree.write(`app-e2e/project.json`, JSON.stringify(e2eProjectConfig)); - if (!overrides.noVite) { - tempFs.createFile(`app/vite.config.ts`, viteConfig); - } else { - tempFs.createFile(`app/webpack.config.ts`, ``); - } - tempFs.createFilesSync({ - [`app/project.json`]: JSON.stringify(appProjectConfig), - [`app-e2e/playwright.config.ts`]: basePlaywrightConfig('app'), - [`app-e2e/project.json`]: JSON.stringify(e2eProjectConfig), - }); - - projectGraph.nodes['app'] = { - name: 'app', - type: 'app', - data: { - projectType: 'application', - root: 'app', - targets: {}, - }, - }; - - projectGraph.nodes[`app-e2e`] = { - name: `app-e2e`, - type: 'app', - data: { - projectType: 'application', - root: `app-e2e`, - targets: { - e2e: {}, - }, - }, - }; -} diff --git a/packages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.ts b/packages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.ts deleted file mode 100644 index c98bfff41a031..0000000000000 --- a/packages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { - createProjectGraphAsync, - formatFiles, - getPackageManagerCommand, - joinPathFragments, - parseTargetString, - type Tree, - visitNotIgnoredFiles, -} from '@nx/devkit'; -import { tsquery } from '@phenomnomnominal/tsquery'; - -export default async function (tree: Tree) { - const graph = await createProjectGraphAsync(); - visitNotIgnoredFiles(tree, '', (path) => { - if (!path.endsWith('playwright.config.ts')) { - return; - } - - let playwrightConfigFileContents = tree.read(path, 'utf-8'); - - const WEBSERVER_COMMAND_SELECTOR = - 'PropertyAssignment:has(Identifier[name=webServer]) PropertyAssignment:has(Identifier[name=command]) > StringLiteral'; - let ast = tsquery.ast(playwrightConfigFileContents); - const nodes = tsquery(ast, WEBSERVER_COMMAND_SELECTOR, { - visitAllChildren: true, - }); - if (!nodes.length) { - return; - } - - const commandValueNode = nodes[0]; - const command = commandValueNode.getText(); - let project: string; - if (command.includes('nx run')) { - const NX_TARGET_REGEX = "(?<=nx run )[^']+"; - const matches = command.match(NX_TARGET_REGEX); - if (!matches) { - return; - } - const targetString = matches[0]; - const parsedTargetString = parseTargetString(targetString, graph); - - if ( - parsedTargetString.target === 'serve-static' || - parsedTargetString.target === 'preview' - ) { - return; - } - - project = parsedTargetString.project; - } else { - const NX_PROJECT_REGEX = "(?<=nx [^ ]+ )[^']+"; - const matches = command.match(NX_PROJECT_REGEX); - if (!matches) { - return; - } - project = matches[0]; - } - - const pathToViteConfig = [ - joinPathFragments(graph.nodes[project].data.root, 'vite.config.ts'), - joinPathFragments(graph.nodes[project].data.root, 'vite.config.js'), - ].find((p) => tree.exists(p)); - - if (!pathToViteConfig) { - const newCommand = `${ - getPackageManagerCommand().exec - } nx run ${project}:serve-static`; - tree.write( - path, - `${playwrightConfigFileContents.slice( - 0, - commandValueNode.getStart() - )}"${newCommand}"${playwrightConfigFileContents.slice( - commandValueNode.getEnd() - )}` - ); - } else { - const newCommand = `${ - getPackageManagerCommand().exec - } nx run ${project}:preview`; - tree.write( - path, - `${playwrightConfigFileContents.slice( - 0, - commandValueNode.getStart() - )}"${newCommand}"${playwrightConfigFileContents.slice( - commandValueNode.getEnd() - )}` - ); - playwrightConfigFileContents = tree.read(path, 'utf-8'); - ast = tsquery.ast(playwrightConfigFileContents); - - const BASE_URL_SELECTOR = - 'VariableDeclaration:has(Identifier[name=baseURL])'; - const baseUrlNodes = tsquery(ast, BASE_URL_SELECTOR, { - visitAllChildren: true, - }); - if (!baseUrlNodes.length) { - return; - } - - const baseUrlNode = baseUrlNodes[0]; - const newBaseUrlVariableDeclaration = - "baseURL = process.env['BASE_URL'] || 'http://localhost:4300';"; - tree.write( - path, - `${playwrightConfigFileContents.slice( - 0, - baseUrlNode.getStart() - )}${newBaseUrlVariableDeclaration}${playwrightConfigFileContents.slice( - baseUrlNode.getEnd() - )}` - ); - - playwrightConfigFileContents = tree.read(path, 'utf-8'); - ast = tsquery.ast(playwrightConfigFileContents); - const WEB_SERVER_URL_SELECTOR = - 'PropertyAssignment:has(Identifier[name=webServer]) PropertyAssignment:has(Identifier[name=url]) > StringLiteral'; - const webServerUrlNodes = tsquery(ast, WEB_SERVER_URL_SELECTOR, { - visitAllChildren: true, - }); - if (!webServerUrlNodes.length) { - return; - } - - const webServerUrlNode = webServerUrlNodes[0]; - const newWebServerUrl = "'http://localhost:4300'"; - tree.write( - path, - `${playwrightConfigFileContents.slice( - 0, - webServerUrlNode.getStart() - )}${newWebServerUrl}${playwrightConfigFileContents.slice( - webServerUrlNode.getEnd() - )}` - ); - } - }); - - await formatFiles(tree); -} diff --git a/packages/vite/src/migrations/update-19-5-5/add-depends-on-for-preview.spec.ts b/packages/vite/src/migrations/update-19-5-5/add-depends-on-for-preview.spec.ts deleted file mode 100644 index e3882e44b4bdd..0000000000000 --- a/packages/vite/src/migrations/update-19-5-5/add-depends-on-for-preview.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import addDependsOnForPreview from './add-depends-on-for-preview'; -import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; -import { readJson } from '@nx/devkit'; - -describe('addDependsOnForPreview', () => { - it('should update when preview target exists in project.json', async () => { - // ARRANGE - const tree = createTreeWithEmptyWorkspace(); - tree.write( - 'apps/app/project.json', - JSON.stringify({ - name: 'app', - root: 'apps/app', - projectType: 'application', - targets: { - preview: { - executor: '@nx/vite:preview-server', - }, - }, - }) - ); - - // ACT - await addDependsOnForPreview(tree); - - // ASSERT - expect(readJson(tree, 'apps/app/project.json').targets) - .toMatchInlineSnapshot(` - { - "preview": { - "dependsOn": [ - "build", - ], - "executor": "@nx/vite:preview-server", - }, - } - `); - }); - - it('should not update when preview target exists in project.json and has a dependsOn already', async () => { - // ARRANGE - const tree = createTreeWithEmptyWorkspace(); - tree.write( - 'apps/app/project.json', - JSON.stringify({ - name: 'app', - root: 'apps/app', - projectType: 'application', - targets: { - preview: { - dependsOn: ['build'], - executor: '@nx/vite:preview-server', - }, - }, - }) - ); - - // ACT - await addDependsOnForPreview(tree); - - // ASSERT - expect(readJson(tree, 'apps/app/project.json').targets) - .toMatchInlineSnapshot(` - { - "preview": { - "dependsOn": [ - "build", - ], - "executor": "@nx/vite:preview-server", - }, - } - `); - }); -}); diff --git a/packages/vite/src/migrations/update-19-5-5/add-depends-on-for-preview.ts b/packages/vite/src/migrations/update-19-5-5/add-depends-on-for-preview.ts deleted file mode 100644 index f78f0a360bc51..0000000000000 --- a/packages/vite/src/migrations/update-19-5-5/add-depends-on-for-preview.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { - type Tree, - formatFiles, - readProjectConfiguration, - updateProjectConfiguration, -} from '@nx/devkit'; -import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils'; -import { VitePreviewServerExecutorOptions } from '../../executors/preview-server/schema'; - -export default async function (tree: Tree) { - forEachExecutorOptions( - tree, - '@nx/vite:preview-server', - (_, projectName, targetName) => { - const project = readProjectConfiguration(tree, projectName); - project.targets[targetName].dependsOn ??= []; - if (project.targets[targetName].dependsOn.includes('build')) { - return; - } - project.targets[targetName].dependsOn.push('build'); - updateProjectConfiguration(tree, projectName, project); - } - ); - - await formatFiles(tree); -} From 15e63893eb3f165f8bd55545c7f303a7483eef88 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Mon, 29 Jul 2024 13:41:11 +0100 Subject: [PATCH 05/22] feat(devkit): add util for adding e2e-ci target defaults --- .../application/lib/create-project.ts | 2 +- .../src/generators/library/lib/add-project.ts | 2 +- .../generators/add-build-target-defaults.ts | 19 - .../generators/target-defaults-utils.spec.ts | 377 ++++++++++++++++++ .../src/generators/target-defaults-utils.ts | 98 +++++ .../generators/configuration/configuration.ts | 2 +- .../generators/application/lib/add-project.ts | 2 +- .../expo/src/generators/library/library.ts | 2 +- packages/js/src/generators/library/library.ts | 2 +- .../src/generators/setup-build/generator.ts | 2 +- .../generators/application/lib/add-project.ts | 2 +- .../src/generators/application/application.ts | 2 +- .../node/src/generators/library/library.ts | 2 +- packages/nx/src/devkit-internals.ts | 4 +- .../application/application.impl.ts | 2 +- .../generators/configuration/configuration.ts | 2 +- packages/vite/src/utils/generator-utils.ts | 2 +- .../src/generators/application/application.ts | 2 +- .../generators/configuration/configuration.ts | 2 +- 19 files changed, 492 insertions(+), 36 deletions(-) delete mode 100644 packages/devkit/src/generators/add-build-target-defaults.ts create mode 100644 packages/devkit/src/generators/target-defaults-utils.spec.ts create mode 100644 packages/devkit/src/generators/target-defaults-utils.ts diff --git a/packages/angular/src/generators/application/lib/create-project.ts b/packages/angular/src/generators/application/lib/create-project.ts index 5451263d0a9cc..b1597ad2e4831 100644 --- a/packages/angular/src/generators/application/lib/create-project.ts +++ b/packages/angular/src/generators/application/lib/create-project.ts @@ -2,7 +2,7 @@ import { addProjectConfiguration, joinPathFragments, Tree } from '@nx/devkit'; import type { AngularProjectConfiguration } from '../../../utils/types'; import { getInstalledAngularVersionInfo } from '../../utils/version-utils'; import type { NormalizedSchema } from './normalized-schema'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export function createProject(tree: Tree, options: NormalizedSchema) { const { major: angularMajorVersion } = getInstalledAngularVersionInfo(tree); diff --git a/packages/angular/src/generators/library/lib/add-project.ts b/packages/angular/src/generators/library/lib/add-project.ts index 9ac3e015a9037..466ed991d8b74 100644 --- a/packages/angular/src/generators/library/lib/add-project.ts +++ b/packages/angular/src/generators/library/lib/add-project.ts @@ -2,7 +2,7 @@ import type { Tree } from '@nx/devkit'; import { addProjectConfiguration, joinPathFragments } from '@nx/devkit'; import type { AngularProjectConfiguration } from '../../../utils/types'; import type { NormalizedSchema } from './normalized-schema'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export function addProject( tree: Tree, diff --git a/packages/devkit/src/generators/add-build-target-defaults.ts b/packages/devkit/src/generators/add-build-target-defaults.ts deleted file mode 100644 index 6158f6e3b8cd1..0000000000000 --- a/packages/devkit/src/generators/add-build-target-defaults.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { readNxJson, Tree, updateNxJson } from 'nx/src/devkit-exports'; - -export function addBuildTargetDefaults( - tree: Tree, - executorName: string, - buildTargetName = 'build' -): void { - const nxJson = readNxJson(tree); - nxJson.targetDefaults ??= {}; - nxJson.targetDefaults[executorName] ??= { - cache: true, - dependsOn: [`^${buildTargetName}`], - inputs: - nxJson.namedInputs && 'production' in nxJson.namedInputs - ? ['production', '^production'] - : ['default', '^default'], - }; - updateNxJson(tree, nxJson); -} diff --git a/packages/devkit/src/generators/target-defaults-utils.spec.ts b/packages/devkit/src/generators/target-defaults-utils.spec.ts new file mode 100644 index 0000000000000..45769cc4645d7 --- /dev/null +++ b/packages/devkit/src/generators/target-defaults-utils.spec.ts @@ -0,0 +1,377 @@ +import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports'; +import { readNxJson, updateNxJson, type Tree } from 'nx/src/devkit-exports'; +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; +import { addE2eCiTargetDefaults } from './target-defaults-utils'; +describe('target-defaults-utils', () => { + describe('addE2eCiTargetDefaults', () => { + let tree: Tree; + let tempFs: TempFs; + beforeEach(() => { + tempFs = new TempFs('target-defaults-utils'); + tree = createTreeWithEmptyWorkspace(); + tree.root = tempFs.tempDir; + }); + + afterEach(() => { + tempFs.cleanup(); + jest.resetModules(); + }); + + it('should add e2e-ci--**/* target default for e2e plugin for specified build target when it does not exist', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); + }); + + it('should update existing e2e-ci--**/* target default for e2e plugin for specified build target when it does not exist in dependsOn', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }); + nxJson.targetDefaults ??= {}; + nxJson.targetDefaults['e2e-ci--**/*'] = { + dependsOn: ['^build'], + }; + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build-base', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + "^build-base", + ], + } + `); + }); + + it('should read the ciTargetName and add a new entry when it does not exist', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'cypress:e2e-ci', + }, + }); + nxJson.targetDefaults ??= {}; + nxJson.targetDefaults['e2e-ci--**/*'] = { + dependsOn: ['^build'], + }; + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build-base', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); + expect(newNxJson.targetDefaults['cypress:e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build-base", + ], + } + `); + }); + + it('should not add additional e2e-ci--**/* target default for e2e plugin when it already exists with build target', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }); + nxJson.targetDefaults ??= {}; + nxJson.targetDefaults['e2e-ci--**/*'] = { + dependsOn: ['^build'], + }; + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should do nothing when there are no nxJson.plugins does not exist', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = undefined; + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should do nothing when there are nxJson.plugins but e2e plugin is not registered', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/playwright/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should choose the correct plugin when there are includes', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + include: ['libs/**'], + }); + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'cypress:e2e-ci', + }, + include: ['apps/**'], + }); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults['cypress:e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); + }); + + it('should choose the correct plugin when there are excludes', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + exclude: ['apps/**'], + }); + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'cypress:e2e-ci', + }, + exclude: ['libs/**'], + }); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults['cypress:e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); + }); + + it('should use the default name when the plugin registration is a string', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push('@nx/cypress/plugin'); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); + }); + }); +}); diff --git a/packages/devkit/src/generators/target-defaults-utils.ts b/packages/devkit/src/generators/target-defaults-utils.ts new file mode 100644 index 0000000000000..d2fd347ccafdd --- /dev/null +++ b/packages/devkit/src/generators/target-defaults-utils.ts @@ -0,0 +1,98 @@ +import { + type CreateNodes, + type CreateNodesV2, + type PluginConfiguration, + type Tree, + readNxJson, + updateNxJson, +} from 'nx/src/devkit-exports'; +import { findMatchingConfigFiles } from 'nx/src/devkit-internals'; + +export function addBuildTargetDefaults( + tree: Tree, + executorName: string, + buildTargetName = 'build' +): void { + const nxJson = readNxJson(tree); + nxJson.targetDefaults ??= {}; + nxJson.targetDefaults[executorName] ??= { + cache: true, + dependsOn: [`^${buildTargetName}`], + inputs: + nxJson.namedInputs && 'production' in nxJson.namedInputs + ? ['production', '^production'] + : ['default', '^default'], + }; + updateNxJson(tree, nxJson); +} + +export async function addE2eCiTargetDefaults( + tree: Tree, + e2ePlugin: string, + buildTarget: string, + pathToE2EConfigFile: string +): Promise { + const nxJson = readNxJson(tree); + if (!nxJson.plugins) { + return; + } + + const e2ePluginRegistrations = nxJson.plugins.filter((p) => + typeof p === 'string' ? p === e2ePlugin : p.plugin === e2ePlugin + ); + if (!e2ePluginRegistrations.length) { + return; + } + + const resolvedE2ePlugin: { + createNodes?: CreateNodes; + createNodesV2?: CreateNodesV2; + } = await import(e2ePlugin); + const e2ePluginGlob = + resolvedE2ePlugin.createNodesV2?.[0] ?? resolvedE2ePlugin.createNodes?.[0]; + + let foundPluginForApplication: PluginConfiguration; + for (let i = 0; i < e2ePluginRegistrations.length; i++) { + let candidatePluginForApplication = e2ePluginRegistrations[i]; + if (typeof candidatePluginForApplication === 'string') { + foundPluginForApplication = candidatePluginForApplication; + break; + } + + const matchingConfigFiles = findMatchingConfigFiles( + [pathToE2EConfigFile], + e2ePluginGlob, + candidatePluginForApplication.include, + candidatePluginForApplication.exclude + ); + + if (matchingConfigFiles.length) { + foundPluginForApplication = candidatePluginForApplication; + break; + } + } + + if (!foundPluginForApplication) { + return; + } + + const ciTargetName = + typeof foundPluginForApplication === 'string' + ? 'e2e-ci' + : (foundPluginForApplication.options as any)?.ciTargetName ?? 'e2e-ci'; + + const ciTargetNameGlob = `${ciTargetName}--**/*`; + nxJson.targetDefaults ??= {}; + const e2eCiTargetDefaults = nxJson.targetDefaults[ciTargetNameGlob]; + if (!e2eCiTargetDefaults) { + nxJson.targetDefaults[ciTargetNameGlob] = { + dependsOn: [buildTarget], + }; + } else { + e2eCiTargetDefaults.dependsOn ??= []; + if (!e2eCiTargetDefaults.dependsOn.includes(buildTarget)) { + e2eCiTargetDefaults.dependsOn.push(buildTarget); + } + } + updateNxJson(tree, nxJson); +} diff --git a/packages/esbuild/src/generators/configuration/configuration.ts b/packages/esbuild/src/generators/configuration/configuration.ts index 14d3aa2a5abd3..f838f143ded4a 100644 --- a/packages/esbuild/src/generators/configuration/configuration.ts +++ b/packages/esbuild/src/generators/configuration/configuration.ts @@ -12,7 +12,7 @@ import { getImportPath } from '@nx/js/src/utils/get-import-path'; import { esbuildInitGenerator } from '../init/init'; import { EsBuildExecutorOptions } from '../../executors/esbuild/schema'; import { EsBuildProjectSchema } from './schema'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export async function configurationGenerator( tree: Tree, diff --git a/packages/expo/src/generators/application/lib/add-project.ts b/packages/expo/src/generators/application/lib/add-project.ts index 1f5e725474464..1f79ea08c95c0 100644 --- a/packages/expo/src/generators/application/lib/add-project.ts +++ b/packages/expo/src/generators/application/lib/add-project.ts @@ -8,7 +8,7 @@ import { import { hasExpoPlugin } from '../../../utils/has-expo-plugin'; import { NormalizedSchema } from './normalize-options'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export function addProject(host: Tree, options: NormalizedSchema) { const nxJson = readNxJson(host); diff --git a/packages/expo/src/generators/library/library.ts b/packages/expo/src/generators/library/library.ts index 61c3eb58b9696..7751b2e641235 100644 --- a/packages/expo/src/generators/library/library.ts +++ b/packages/expo/src/generators/library/library.ts @@ -32,7 +32,7 @@ import { NormalizedSchema, normalizeOptions } from './lib/normalize-options'; import { Schema } from './schema'; import { ensureDependencies } from '../../utils/ensure-dependencies'; import { initRootBabelConfig } from '../../utils/init-root-babel-config'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; export async function expoLibraryGenerator( diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index 6ec990008bcfb..4efc7a7e3d9ab 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -25,7 +25,7 @@ import { type ProjectNameAndRootOptions, } from '@nx/devkit/src/generators/project-name-and-root-utils'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; import { findMatchingProjects } from 'nx/src/utils/find-matching-projects'; import { type PackageJson } from 'nx/src/utils/package-json'; diff --git a/packages/js/src/generators/setup-build/generator.ts b/packages/js/src/generators/setup-build/generator.ts index b83e251c1b77e..c39416c3e31c0 100644 --- a/packages/js/src/generators/setup-build/generator.ts +++ b/packages/js/src/generators/setup-build/generator.ts @@ -12,7 +12,7 @@ import { addSwcConfig } from '../../utils/swc/add-swc-config'; import { addSwcDependencies } from '../../utils/swc/add-swc-dependencies'; import { nxVersion } from '../../utils/versions'; import { SetupBuildGeneratorSchema } from './schema'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export async function setupBuildGenerator( tree: Tree, diff --git a/packages/next/src/generators/application/lib/add-project.ts b/packages/next/src/generators/application/lib/add-project.ts index 84ad72664cb2f..947d9ce6fbe23 100644 --- a/packages/next/src/generators/application/lib/add-project.ts +++ b/packages/next/src/generators/application/lib/add-project.ts @@ -5,7 +5,7 @@ import { readNxJson, Tree, } from '@nx/devkit'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export function addProject(host: Tree, options: NormalizedSchema) { const targets: Record = {}; diff --git a/packages/node/src/generators/application/application.ts b/packages/node/src/generators/application/application.ts index 9ad70f32eafe0..a6ec2ce256ea3 100644 --- a/packages/node/src/generators/application/application.ts +++ b/packages/node/src/generators/application/application.ts @@ -48,7 +48,7 @@ import { initGenerator } from '../init/init'; import { setupDockerGenerator } from '../setup-docker/setup-docker'; import { Schema } from './schema'; import { hasWebpackPlugin } from '../../utils/has-webpack-plugin'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; export interface NormalizedSchema extends Schema { diff --git a/packages/node/src/generators/library/library.ts b/packages/node/src/generators/library/library.ts index 9499236ced506..2ecec34cf3203 100644 --- a/packages/node/src/generators/library/library.ts +++ b/packages/node/src/generators/library/library.ts @@ -22,7 +22,7 @@ import { join } from 'path'; import { tslibVersion, typesNodeVersion } from '../../utils/versions'; import { initGenerator } from '../init/init'; import { Schema } from './schema'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export interface NormalizedSchema extends Schema { fileName: string; diff --git a/packages/nx/src/devkit-internals.ts b/packages/nx/src/devkit-internals.ts index 72be433b4c674..d380df1298833 100644 --- a/packages/nx/src/devkit-internals.ts +++ b/packages/nx/src/devkit-internals.ts @@ -8,11 +8,11 @@ export { getExecutorInformation } from './command-line/run/executor-utils'; export { readNxJson as readNxJsonFromDisk } from './config/nx-json'; export { calculateDefaultProjectName } from './config/calculate-default-project-name'; export { retrieveProjectConfigurationsWithAngularProjects } from './project-graph/utils/retrieve-workspace-files'; +export { mergeTargetConfigurations } from './project-graph/utils/project-configuration-utils'; export { - mergeTargetConfigurations, + readProjectConfigurationsFromRootMap, findMatchingConfigFiles, } from './project-graph/utils/project-configuration-utils'; -export { readProjectConfigurationsFromRootMap } from './project-graph/utils/project-configuration-utils'; export { splitTarget } from './utils/split-target'; export { combineOptionsForExecutor } from './utils/params'; export { sortObjectByKeys } from './utils/object-sort'; diff --git a/packages/remix/src/generators/application/application.impl.ts b/packages/remix/src/generators/application/application.impl.ts index fc99f0f8653b8..e86017360f294 100644 --- a/packages/remix/src/generators/application/application.impl.ts +++ b/packages/remix/src/generators/application/application.impl.ts @@ -33,7 +33,7 @@ import { NxRemixGeneratorSchema } from './schema'; import { updateDependencies } from '../utils/update-dependencies'; import initGenerator from '../init/init'; import { initGenerator as jsInitGenerator } from '@nx/js'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; import { updateJestTestMatch } from '../../utils/testing-config-utils'; diff --git a/packages/rollup/src/generators/configuration/configuration.ts b/packages/rollup/src/generators/configuration/configuration.ts index e28a07b3be0e0..f12e0c584b43d 100644 --- a/packages/rollup/src/generators/configuration/configuration.ts +++ b/packages/rollup/src/generators/configuration/configuration.ts @@ -16,7 +16,7 @@ import { getImportPath } from '@nx/js/src/utils/get-import-path'; import { rollupInitGenerator } from '../init/init'; import { RollupExecutorOptions } from '../../executors/rollup/schema'; import { RollupProjectSchema } from './schema'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { ensureDependencies } from '../../utils/ensure-dependencies'; import { hasPlugin } from '../../utils/has-plugin'; import { RollupWithNxPluginOptions } from '../../plugins/with-nx/with-nx-options'; diff --git a/packages/vite/src/utils/generator-utils.ts b/packages/vite/src/utils/generator-utils.ts index 0b7c2790f155c..a1a997c02a493 100644 --- a/packages/vite/src/utils/generator-utils.ts +++ b/packages/vite/src/utils/generator-utils.ts @@ -14,7 +14,7 @@ import { VitePreviewServerExecutorOptions } from '../executors/preview-server/sc import { VitestExecutorOptions } from '../executors/test/schema'; import { ViteConfigurationGeneratorSchema } from '../generators/configuration/schema'; import { ensureViteConfigIsCorrect } from './vite-config-edit-utils'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export type Target = 'build' | 'serve' | 'test' | 'preview'; export type TargetFlags = Partial>; diff --git a/packages/web/src/generators/application/application.ts b/packages/web/src/generators/application/application.ts index 5b0df944b12fc..823d771bbbd2d 100644 --- a/packages/web/src/generators/application/application.ts +++ b/packages/web/src/generators/application/application.ts @@ -37,7 +37,7 @@ import { webInitGenerator } from '../init/init'; import { Schema } from './schema'; import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope'; import { hasWebpackPlugin } from '../../utils/has-webpack-plugin'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; import { VitePluginOptions } from '@nx/vite/src/plugins/plugin'; import { WebpackPluginOptions } from '@nx/webpack/src/plugins/plugin'; diff --git a/packages/webpack/src/generators/configuration/configuration.ts b/packages/webpack/src/generators/configuration/configuration.ts index d655004455241..f4cd67321e320 100644 --- a/packages/webpack/src/generators/configuration/configuration.ts +++ b/packages/webpack/src/generators/configuration/configuration.ts @@ -15,7 +15,7 @@ import { webpackInitGenerator } from '../init/init'; import { ConfigurationGeneratorSchema } from './schema'; import { WebpackExecutorOptions } from '../../executors/webpack/schema'; import { hasPlugin } from '../../utils/has-plugin'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { ensureDependencies } from '../../utils/ensure-dependencies'; export function configurationGenerator( From 2edfb1a512f3d0b6822123887a59e56390153de6 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Mon, 29 Jul 2024 15:48:06 +0100 Subject: [PATCH 06/22] fix(angular): add e2e-ci target defaults when e2e project added --- .../__snapshots__/application.spec.ts.snap | 323 ------------------ .../application/application.spec.ts | 10 +- .../src/generators/application/lib/add-e2e.ts | 17 + 3 files changed, 26 insertions(+), 324 deletions(-) diff --git a/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap index f0aba9c3e5ba3..2a0f9f246fbc8 100644 --- a/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap +++ b/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap @@ -322,92 +322,6 @@ exports[`app --project-name-and-root-format=derived should generate correctly wh } `; -exports[`app --project-name-and-root-format=derived should generate correctly when directory is provided: e2e tsconfig.json 1`] = ` -{ - "compilerOptions": { - "allowJs": true, - "forceConsistentCasingInFileNames": true, - "module": "commonjs", - "noFallthroughCasesInSwitch": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noPropertyAccessFromIndexSignature": true, - "outDir": "../../../dist/out-tsc", - "sourceMap": false, - "strict": true, - "types": [ - "cypress", - "node", - ], - }, - "extends": "../../../tsconfig.base.json", - "include": [ - "**/*.ts", - "**/*.js", - "cypress.config.ts", - "**/*.cy.ts", - "**/*.cy.js", - "**/*.d.ts", - ], -} -`; - -exports[`app --project-name-and-root-format=derived should generate correctly when directory is provided: tsconfig.app.json 1`] = ` -{ - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "types": [], - }, - "exclude": [ - "jest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - ], - "extends": "./tsconfig.json", - "files": [ - "src/main.ts", - ], - "include": [ - "src/**/*.d.ts", - ], -} -`; - -exports[`app --project-name-and-root-format=derived should generate correctly when directory is provided: tsconfig.json 1`] = ` -{ - "angularCompilerOptions": { - "enableI18nLegacyMessageIdFormat": false, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "strictTemplates": true, - }, - "compilerOptions": { - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noPropertyAccessFromIndexSignature": true, - "strict": true, - "target": "es2022", - }, - "extends": "../../../tsconfig.base.json", - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.editor.json", - }, - { - "path": "./tsconfig.app.json", - }, - { - "path": "./tsconfig.spec.json", - }, - ], -} -`; - exports[`app --project-name-and-root-format=derived should generate correctly when no directory is provided 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", @@ -524,92 +438,6 @@ exports[`app --project-name-and-root-format=derived should generate correctly wh } `; -exports[`app --project-name-and-root-format=derived should generate correctly when no directory is provided: e2e tsconfig.json 1`] = ` -{ - "compilerOptions": { - "allowJs": true, - "forceConsistentCasingInFileNames": true, - "module": "commonjs", - "noFallthroughCasesInSwitch": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noPropertyAccessFromIndexSignature": true, - "outDir": "../../dist/out-tsc", - "sourceMap": false, - "strict": true, - "types": [ - "cypress", - "node", - ], - }, - "extends": "../../tsconfig.base.json", - "include": [ - "**/*.ts", - "**/*.js", - "cypress.config.ts", - "**/*.cy.ts", - "**/*.cy.js", - "**/*.d.ts", - ], -} -`; - -exports[`app --project-name-and-root-format=derived should generate correctly when no directory is provided: tsconfig.app.json 1`] = ` -{ - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "types": [], - }, - "exclude": [ - "jest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - ], - "extends": "./tsconfig.json", - "files": [ - "src/main.ts", - ], - "include": [ - "src/**/*.d.ts", - ], -} -`; - -exports[`app --project-name-and-root-format=derived should generate correctly when no directory is provided: tsconfig.json 1`] = ` -{ - "angularCompilerOptions": { - "enableI18nLegacyMessageIdFormat": false, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "strictTemplates": true, - }, - "compilerOptions": { - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noPropertyAccessFromIndexSignature": true, - "strict": true, - "target": "es2022", - }, - "extends": "../../tsconfig.base.json", - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.editor.json", - }, - { - "path": "./tsconfig.app.json", - }, - { - "path": "./tsconfig.spec.json", - }, - ], -} -`; - exports[`app --standalone should generate a standalone app correctly with routing 1`] = ` "import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; @@ -766,71 +594,6 @@ export const appConfig: ApplicationConfig = { " `; -exports[`app --strict should enable strict type checking: app tsconfig.json 1`] = ` -{ - "angularCompilerOptions": { - "enableI18nLegacyMessageIdFormat": false, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "strictTemplates": true, - }, - "compilerOptions": { - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noPropertyAccessFromIndexSignature": true, - "strict": true, - "target": "es2022", - }, - "extends": "../tsconfig.base.json", - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.editor.json", - }, - { - "path": "./tsconfig.app.json", - }, - { - "path": "./tsconfig.spec.json", - }, - ], -} -`; - -exports[`app --strict should enable strict type checking: e2e tsconfig.json 1`] = ` -{ - "compilerOptions": { - "allowJs": true, - "forceConsistentCasingInFileNames": true, - "module": "commonjs", - "noFallthroughCasesInSwitch": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noPropertyAccessFromIndexSignature": true, - "outDir": "../dist/out-tsc", - "sourceMap": false, - "strict": true, - "types": [ - "cypress", - "node", - ], - }, - "extends": "../tsconfig.base.json", - "include": [ - "**/*.ts", - "**/*.js", - "cypress.config.ts", - "**/*.cy.ts", - "**/*.cy.js", - "**/*.d.ts", - ], -} -`; - exports[`app angular compat support should import "ApplicationConfig" from "@angular/platform-browser" 1`] = ` "import { ApplicationConfig } from '@angular/core'; import { provideRouter } from '@angular/router'; @@ -1147,92 +910,6 @@ exports[`app not nested should create project configs 2`] = ` } `; -exports[`app not nested should generate files: e2e tsconfig.json 1`] = ` -{ - "compilerOptions": { - "allowJs": true, - "forceConsistentCasingInFileNames": true, - "module": "commonjs", - "noFallthroughCasesInSwitch": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noPropertyAccessFromIndexSignature": true, - "outDir": "../dist/out-tsc", - "sourceMap": false, - "strict": true, - "types": [ - "cypress", - "node", - ], - }, - "extends": "../tsconfig.base.json", - "include": [ - "**/*.ts", - "**/*.js", - "cypress.config.ts", - "**/*.cy.ts", - "**/*.cy.js", - "**/*.d.ts", - ], -} -`; - -exports[`app not nested should generate files: tsconfig.app.json 1`] = ` -{ - "compilerOptions": { - "outDir": "../dist/out-tsc", - "types": [], - }, - "exclude": [ - "jest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - ], - "extends": "./tsconfig.json", - "files": [ - "src/main.ts", - ], - "include": [ - "src/**/*.d.ts", - ], -} -`; - -exports[`app not nested should generate files: tsconfig.json 1`] = ` -{ - "angularCompilerOptions": { - "enableI18nLegacyMessageIdFormat": false, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "strictTemplates": true, - }, - "compilerOptions": { - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noPropertyAccessFromIndexSignature": true, - "strict": true, - "target": "es2022", - }, - "extends": "../tsconfig.base.json", - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.editor.json", - }, - { - "path": "./tsconfig.app.json", - }, - { - "path": "./tsconfig.spec.json", - }, - ], -} -`; - exports[`app should generate correct tsconfig.editor.json 1`] = ` { "compilerOptions": {}, diff --git a/packages/angular/src/generators/application/application.spec.ts b/packages/angular/src/generators/application/application.spec.ts index e936d428ab4f7..6ab04c4b5c7e1 100644 --- a/packages/angular/src/generators/application/application.spec.ts +++ b/packages/angular/src/generators/application/application.spec.ts @@ -569,7 +569,8 @@ describe('app', () => { it('should add eslint plugin and no lint target to e2e project', async () => { await generateApp(appTree, 'my-app', { linter: Linter.EsLint }); - expect(readNxJson(appTree).plugins).toMatchInlineSnapshot(` + const nxJson = readNxJson(appTree); + expect(nxJson.plugins).toMatchInlineSnapshot(` [ { "options": { @@ -588,6 +589,13 @@ describe('app', () => { }, ] `); + expect(nxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); expect( readProjectConfiguration(appTree, 'my-app-e2e').targets.lint ).toBeUndefined(); diff --git a/packages/angular/src/generators/application/lib/add-e2e.ts b/packages/angular/src/generators/application/lib/add-e2e.ts index 08fa9b2863df0..7c3de1a055e27 100644 --- a/packages/angular/src/generators/application/lib/add-e2e.ts +++ b/packages/angular/src/generators/application/lib/add-e2e.ts @@ -12,6 +12,7 @@ import { import { nxVersion } from '../../../utils/versions'; import { getInstalledAngularVersionInfo } from '../../utils/version-utils'; import type { NormalizedSchema } from './normalized-schema'; +import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export async function addE2e(tree: Tree, options: NormalizedSchema) { // since e2e are separate projects, default to adding plugins @@ -45,6 +46,14 @@ export async function addE2e(tree: Tree, options: NormalizedSchema) { rootProject: options.rootProject, addPlugin, }); + if (addPlugin) { + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build', + joinPathFragments(options.e2eProjectRoot, 'cypress.config.ts') + ); + } } else if (options.e2eTestRunner === 'playwright') { const { configurationGenerator } = ensurePackage< typeof import('@nx/playwright') @@ -71,6 +80,14 @@ export async function addE2e(tree: Tree, options: NormalizedSchema) { rootProject: options.rootProject, addPlugin, }); + if (addPlugin) { + await addE2eCiTargetDefaults( + tree, + '@nx/playwright/plugin', + '^build', + joinPathFragments(options.e2eProjectRoot, 'playwright.config.ts') + ); + } } } From d7a05b3eaedde0c8b017f9cbfdcc5da8ac610d73 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Mon, 29 Jul 2024 16:13:21 +0100 Subject: [PATCH 07/22] feat(devkit): add util for finding the plugin registration matching a config file --- .../utils/find-plugin-for-config-file.spec.ts | 173 ++++++++++++++++++ .../src/utils/find-plugin-for-config-file.ts | 50 +++++ 2 files changed, 223 insertions(+) create mode 100644 packages/devkit/src/utils/find-plugin-for-config-file.spec.ts create mode 100644 packages/devkit/src/utils/find-plugin-for-config-file.ts diff --git a/packages/devkit/src/utils/find-plugin-for-config-file.spec.ts b/packages/devkit/src/utils/find-plugin-for-config-file.spec.ts new file mode 100644 index 0000000000000..941fb356dbcf9 --- /dev/null +++ b/packages/devkit/src/utils/find-plugin-for-config-file.spec.ts @@ -0,0 +1,173 @@ +import { type Tree, readNxJson, updateNxJson } from 'nx/src/devkit-exports'; +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; +import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports'; +import { findPluginForConfigFile } from './find-plugin-for-config-file'; + +describe('find-plugin-for-config-file', () => { + let tree: Tree; + let tempFs: TempFs; + beforeEach(() => { + tempFs = new TempFs('target-defaults-utils'); + tree = createTreeWithEmptyWorkspace(); + tree.root = tempFs.tempDir; + }); + + afterEach(() => { + tempFs.cleanup(); + jest.resetModules(); + }); + + it('should return the plugin when its registered as just a string', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push('@nx/cypress/plugin'); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + const plugin = await findPluginForConfigFile( + tree, + '@nx/cypress/plugin', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + expect(plugin).toBeTruthy(); + expect(plugin).toEqual('@nx/cypress/plugin'); + }); + + it('should return the plugin when it does not have an include or exclude', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + const plugin = await findPluginForConfigFile( + tree, + '@nx/cypress/plugin', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + expect(plugin).toBeTruthy(); + expect(plugin).toMatchInlineSnapshot(` + { + "options": { + "ciTargetName": "e2e-ci", + "targetName": "e2e", + }, + "plugin": "@nx/cypress/plugin", + } + `); + }); + + it('should return the plugin when it the includes finds the config file', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + include: ['libs/**'], + }); + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'cypress:e2e-ci', + }, + include: ['apps/**'], + }); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + const plugin = await findPluginForConfigFile( + tree, + '@nx/cypress/plugin', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + expect(plugin).toBeTruthy(); + expect(plugin).toMatchInlineSnapshot(` + { + "include": [ + "apps/**", + ], + "options": { + "ciTargetName": "cypress:e2e-ci", + "targetName": "e2e", + }, + "plugin": "@nx/cypress/plugin", + } + `); + }); + + it('should return a valid plugin when it the excludes does not include the config file', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'cypress:e2e-ci', + }, + exclude: ['apps/**'], + }); + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + exclude: ['libs/**'], + }); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + const plugin = await findPluginForConfigFile( + tree, + '@nx/cypress/plugin', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + expect(plugin).toBeTruthy(); + expect(plugin).toMatchInlineSnapshot(` + { + "exclude": [ + "libs/**", + ], + "options": { + "ciTargetName": "e2e-ci", + "targetName": "e2e", + }, + "plugin": "@nx/cypress/plugin", + } + `); + }); +}); diff --git a/packages/devkit/src/utils/find-plugin-for-config-file.ts b/packages/devkit/src/utils/find-plugin-for-config-file.ts new file mode 100644 index 0000000000000..7098f07bab8b1 --- /dev/null +++ b/packages/devkit/src/utils/find-plugin-for-config-file.ts @@ -0,0 +1,50 @@ +import { + type Tree, + type PluginConfiguration, + readNxJson, + CreateNodes, + CreateNodesV2, +} from 'nx/src/devkit-exports'; +import { findMatchingConfigFiles } from 'nx/src/devkit-internals'; +export async function findPluginForConfigFile( + tree: Tree, + pluginName: string, + pathToConfigFile: string +): Promise { + const nxJson = readNxJson(tree); + if (!nxJson.plugins) { + return; + } + + const pluginRegistrations: PluginConfiguration[] = nxJson.plugins.filter( + (p) => (typeof p === 'string' ? p === pluginName : p.plugin === pluginName) + ); + + for (const plugin of pluginRegistrations) { + if (typeof plugin === 'string') { + return plugin; + } + + if (!plugin.include && !plugin.exclude) { + return plugin; + } + + if (plugin.include || plugin.exclude) { + const resolvedPlugin: { + createNodes?: CreateNodes; + createNodesV2?: CreateNodesV2; + } = await import(pluginName); + const pluginGlob = + resolvedPlugin.createNodesV2?.[0] ?? resolvedPlugin.createNodes?.[0]; + const matchingConfigFile = findMatchingConfigFiles( + [pathToConfigFile], + pluginGlob, + plugin.include, + plugin.exclude + ); + if (matchingConfigFile.length) { + return plugin; + } + } + } +} From a52950fb3f40f277b0a6e95c2b1a5b329dc7c745 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Mon, 29 Jul 2024 16:38:52 +0100 Subject: [PATCH 08/22] fix(expo): application generator should add e2e-ci target defaults --- .../application/application.spec.ts | 55 +++++++++++++ .../src/generators/application/lib/add-e2e.ts | 79 +++++++++++++++++-- 2 files changed, 129 insertions(+), 5 deletions(-) diff --git a/packages/expo/src/generators/application/application.spec.ts b/packages/expo/src/generators/application/application.spec.ts index 3b3b574eb10d2..3b8331011c28c 100644 --- a/packages/expo/src/generators/application/application.spec.ts +++ b/packages/expo/src/generators/application/application.spec.ts @@ -3,6 +3,7 @@ import 'nx/src/internal-testing-utils/mock-project-graph'; import { getProjects, readJson, + readNxJson, readProjectConfiguration, Tree, } from '@nx/devkit'; @@ -282,4 +283,58 @@ describe('app', () => { }); }); }); + + describe('cypress', () => { + it('should create e2e app with e2e-ci targetDefaults', async () => { + await expoApplicationGenerator(appTree, { + name: 'my-app', + directory: 'my-dir', + linter: Linter.EsLint, + e2eTestRunner: 'cypress', + js: false, + skipFormat: false, + unitTestRunner: 'none', + projectNameAndRootFormat: 'as-provided', + addPlugin: true, + }); + + // ASSERT + const nxJson = readNxJson(appTree); + expect(nxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(` + { + "dependsOn": [ + "^export", + ], + } + `); + }); + }); + + describe('playwright', () => { + // TODO(colum): importing the `@nx/playwright/plugin` results in Jest having issues with ESM + // That causes this test to fail. Need to investigate this more + xit('should create e2e app with e2e-ci targetDefaults', async () => { + await expoApplicationGenerator(appTree, { + name: 'my-app', + directory: 'my-dir', + linter: Linter.EsLint, + e2eTestRunner: 'playwright', + js: false, + skipFormat: false, + unitTestRunner: 'none', + projectNameAndRootFormat: 'as-provided', + addPlugin: true, + }); + + // ASSERT + const nxJson = readNxJson(appTree); + expect(nxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(` + { + "dependsOn": [ + "^export", + ], + } + `); + }); + }); }); diff --git a/packages/expo/src/generators/application/lib/add-e2e.ts b/packages/expo/src/generators/application/lib/add-e2e.ts index 45671e2fb90f3..9b059213f80c2 100644 --- a/packages/expo/src/generators/application/lib/add-e2e.ts +++ b/packages/expo/src/generators/application/lib/add-e2e.ts @@ -4,12 +4,15 @@ import { ensurePackage, getPackageManagerCommand, joinPathFragments, + readNxJson, } from '@nx/devkit'; import { webStaticServeGenerator } from '@nx/web'; import { nxVersion } from '../../../utils/versions'; import { hasExpoPlugin } from '../../../utils/has-expo-plugin'; import { NormalizedSchema } from './normalize-options'; +import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; +import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file'; export async function addE2e( tree: Tree, @@ -18,8 +21,7 @@ export async function addE2e( const hasPlugin = hasExpoPlugin(tree); switch (options.e2eTestRunner) { case 'cypress': { - const hasNxExpoPlugin = hasExpoPlugin(tree); - if (!hasNxExpoPlugin) { + if (!hasPlugin) { await webStaticServeGenerator(tree, { buildTarget: `${options.projectName}:export`, targetName: 'serve-static', @@ -39,7 +41,7 @@ export async function addE2e( tags: [], }); - return await configurationGenerator(tree, { + const e2eTask = await configurationGenerator(tree, { ...options, project: options.e2eProjectName, directory: 'src', @@ -49,12 +51,46 @@ export async function addE2e( devServerTarget: `${options.projectName}:${options.e2eWebServerTarget}`, port: options.e2ePort, baseUrl: options.e2eWebServerAddress, - ciWebServerCommand: hasNxExpoPlugin + ciWebServerCommand: hasPlugin ? `nx run ${options.projectName}:serve-static` : undefined, jsx: true, rootProject: options.rootProject, }); + + if ( + options.addPlugin || + readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/cypress/plugin' + : p.plugin === '@nx/cypress/plugin' + ) + ) { + let buildTarget = '^export'; + if (hasPlugin) { + const matchingExpoPlugin = await findPluginForConfigFile( + tree, + '@nx/expo/plugin', + joinPathFragments(options.appProjectRoot, 'app.json') + ); + if (matchingExpoPlugin && typeof matchingExpoPlugin !== 'string') { + buildTarget = `^${ + (matchingExpoPlugin.options as any)?.exportTargetName ?? 'export' + }`; + } + } + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + buildTarget, + joinPathFragments( + options.e2eProjectRoot, + `cypress.config.${options.js ? 'js' : 'ts'}` + ) + ); + } + + return e2eTask; } case 'playwright': { const { configurationGenerator } = ensurePackage< @@ -67,7 +103,8 @@ export async function addE2e( targets: {}, implicitDependencies: [options.projectName], }); - return configurationGenerator(tree, { + + const e2eTask = await configurationGenerator(tree, { project: options.e2eProjectName, skipFormat: true, skipPackageJson: options.skipPackageJson, @@ -80,7 +117,39 @@ export async function addE2e( } ${options.name}`, webServerAddress: options.e2eWebServerAddress, rootProject: options.rootProject, + addPlugin: options.addPlugin, }); + + if ( + options.addPlugin || + readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/playwright/plugin' + : p.plugin === '@nx/playwright/plugin' + ) + ) { + let buildTarget = '^export'; + if (hasPlugin) { + const matchingExpoPlugin = await findPluginForConfigFile( + tree, + '@nx/expo/plugin', + joinPathFragments(options.appProjectRoot, 'app.json') + ); + if (matchingExpoPlugin && typeof matchingExpoPlugin !== 'string') { + buildTarget = `^${ + (matchingExpoPlugin.options as any)?.exportTargetName ?? 'export' + }`; + } + } + await addE2eCiTargetDefaults( + tree, + '@nx/playwright/plugin', + buildTarget, + joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`) + ); + } + + return e2eTask; } case 'detox': const { detoxApplicationGenerator } = ensurePackage< From 49a70911fe17286eadb97ecfefa6f3cea779182d Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Mon, 29 Jul 2024 16:48:05 +0100 Subject: [PATCH 09/22] fix(nextjs): application generator should add e2e-ci target defaults --- .../application/application.spec.ts | 133 +++++++----------- .../src/generators/application/lib/add-e2e.ts | 71 +++++++++- 2 files changed, 119 insertions(+), 85 deletions(-) diff --git a/packages/next/src/generators/application/application.spec.ts b/packages/next/src/generators/application/application.spec.ts index 2fb1f976f479f..818b3f836cdfd 100644 --- a/packages/next/src/generators/application/application.spec.ts +++ b/packages/next/src/generators/application/application.spec.ts @@ -2,6 +2,7 @@ import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { getProjects, readJson, + readNxJson, readProjectConfiguration, Tree, } from '@nx/devkit'; @@ -199,23 +200,7 @@ describe('app', () => { "import './global.css'; export const metadata = { - title: 'Welcome to ${name}', - description: 'Generated by create-nx-workspace', - }; - - export default function RootLayout({ - children, - }: { - children: React.ReactNode; - }) { - return ( - - {children} - - ); - } - " - `); + title: 'Welcome to `); }); }); @@ -238,23 +223,7 @@ describe('app', () => { "import './global.less'; export const metadata = { - title: 'Welcome to ${name}', - description: 'Generated by create-nx-workspace', - }; - - export default function RootLayout({ - children, - }: { - children: React.ReactNode; - }) { - return ( - - {children} - - ); - } - " - `); + title: 'Welcome to `); }); }); @@ -364,23 +333,7 @@ describe('app', () => { "import './global.css'; export const metadata = { - title: 'Welcome to ${name}', - description: 'Generated by create-nx-workspace', - }; - - export default function RootLayout({ - children, - }: { - children: React.ReactNode; - }) { - return ( - - {children} - - ); - } - " - `); + title: 'Welcome to `); }); it('should add jsxImportSource in tsconfig.json', async () => { @@ -559,6 +512,51 @@ describe('app', () => { }); }); + describe('--e2e-test-runner cypress', () => { + it('should generate e2e-ci targetDefaults', async () => { + const name = uniq(); + + await applicationGenerator(tree, { + name, + style: 'css', + e2eTestRunner: 'cypress', + projectNameAndRootFormat: 'as-provided', + addPlugin: true, + }); + expect(readNxJson(tree).targetDefaults['e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); + }); + }); + + describe('--e2e-test-runner playwright', () => { + // TODO(colum): Investigate why import('@nx/playwright/plugin') causes ESM issues for Jest + xit('should generate e2e-ci targetDefaults', async () => { + const name = uniq(); + + await applicationGenerator(tree, { + name, + style: 'css', + e2eTestRunner: 'playwright', + projectNameAndRootFormat: 'as-provided', + addPlugin: true, + }); + expect(readNxJson(tree).targetDefaults['e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); + }); + }); + it('should generate functional components by default', async () => { const name = uniq(); @@ -616,38 +614,7 @@ describe('app', () => { "rules": { "@next/next/no-html-link-for-pages": [ "error", - "${name}/pages", - ], - }, - }, - { - "files": [ - "*.ts", - "*.tsx", - ], - "rules": {}, - }, - { - "files": [ - "*.js", - "*.jsx", - ], - "rules": {}, - }, - { - "env": { - "jest": true, - }, - "files": [ - "*.spec.ts", - "*.spec.tsx", - "*.spec.js", - "*.spec.jsx", - ], - }, - ], - } - `); + "`); }); }); diff --git a/packages/next/src/generators/application/lib/add-e2e.ts b/packages/next/src/generators/application/lib/add-e2e.ts index 50290052424ca..967d06502229a 100644 --- a/packages/next/src/generators/application/lib/add-e2e.ts +++ b/packages/next/src/generators/application/lib/add-e2e.ts @@ -11,6 +11,8 @@ import { Linter } from '@nx/eslint'; import { nxVersion } from '../../../utils/versions'; import { NormalizedSchema } from './normalize-options'; import { webStaticServeGenerator } from '@nx/web'; +import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file'; +import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export async function addE2e(host: Tree, options: NormalizedSchema) { const nxJson = readNxJson(host); @@ -42,7 +44,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { implicitDependencies: [options.projectName], }); - return configurationGenerator(host, { + const e2eTask = await configurationGenerator(host, { ...options, linter: Linter.EsLint, project: options.e2eProjectName, @@ -60,6 +62,40 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { ? `nx run ${options.projectName}:serve-static` : undefined, }); + + if ( + options.addPlugin || + readNxJson(host).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/cypress/plugin' + : p.plugin === '@nx/cypress/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasPlugin) { + const matchingPlugin = await findPluginForConfigFile( + host, + '@nx/next/plugin', + joinPathFragments(options.appProjectRoot, 'app.json') + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + host, + '@nx/cypress/plugin', + buildTarget, + joinPathFragments( + options.e2eProjectRoot, + `cypress.config.${options.js ? 'js' : 'ts'}` + ) + ); + } + + return e2eTask; } else if (options.e2eTestRunner === 'playwright') { const { configurationGenerator } = ensurePackage< typeof import('@nx/playwright') @@ -71,7 +107,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { tags: [], implicitDependencies: [options.projectName], }); - return configurationGenerator(host, { + const e2eTask = await configurationGenerator(host, { rootProject: options.rootProject, project: options.e2eProjectName, skipFormat: true, @@ -86,6 +122,37 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { } ${options.projectName}`, addPlugin: options.addPlugin, }); + + if ( + options.addPlugin || + readNxJson(host).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/playwright/plugin' + : p.plugin === '@nx/playwright/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasPlugin) { + const matchingPlugin = await findPluginForConfigFile( + host, + '@nx/next/plugin', + joinPathFragments(options.appProjectRoot, 'app.json') + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + host, + '@nx/playwright/plugin', + buildTarget, + joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`) + ); + } + + return e2eTask; } return () => {}; } From 93bc523f1b6d988e73f849df93130ffd0859d2cf Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Mon, 29 Jul 2024 17:25:54 +0100 Subject: [PATCH 10/22] chore(repo): add tsconfig path mapping for @nx/playwright/* --- packages/expo/src/generators/application/application.spec.ts | 4 +--- packages/next/src/generators/application/application.spec.ts | 3 +-- tsconfig.base.json | 1 + 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/expo/src/generators/application/application.spec.ts b/packages/expo/src/generators/application/application.spec.ts index 3b8331011c28c..087b6ee6c8cca 100644 --- a/packages/expo/src/generators/application/application.spec.ts +++ b/packages/expo/src/generators/application/application.spec.ts @@ -311,9 +311,7 @@ describe('app', () => { }); describe('playwright', () => { - // TODO(colum): importing the `@nx/playwright/plugin` results in Jest having issues with ESM - // That causes this test to fail. Need to investigate this more - xit('should create e2e app with e2e-ci targetDefaults', async () => { + it('should create e2e app with e2e-ci targetDefaults', async () => { await expoApplicationGenerator(appTree, { name: 'my-app', directory: 'my-dir', diff --git a/packages/next/src/generators/application/application.spec.ts b/packages/next/src/generators/application/application.spec.ts index 818b3f836cdfd..f67e1b753db88 100644 --- a/packages/next/src/generators/application/application.spec.ts +++ b/packages/next/src/generators/application/application.spec.ts @@ -535,8 +535,7 @@ describe('app', () => { }); describe('--e2e-test-runner playwright', () => { - // TODO(colum): Investigate why import('@nx/playwright/plugin') causes ESM issues for Jest - xit('should generate e2e-ci targetDefaults', async () => { + it('should generate e2e-ci targetDefaults', async () => { const name = uniq(); await applicationGenerator(tree, { diff --git a/tsconfig.base.json b/tsconfig.base.json index 4f74457191501..e93b67d7681e8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -115,6 +115,7 @@ "@nx/nx-dev/ui-theme": ["nx-dev/ui-theme/src/index.ts"], "@nx/nx-dev/util-ai": ["nx-dev/util-ai/src/index.ts"], "@nx/playwright": ["packages/playwright/index.ts"], + "@nx/playwright/*": ["packages/playwright/*"], "@nx/plugin": ["packages/plugin"], "@nx/plugin/*": ["packages/plugin/*"], "@nx/react": ["packages/react"], From f8fbe522556797f5bc9d00c09cdf6b0ca99d0c9c Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Mon, 29 Jul 2024 17:27:51 +0100 Subject: [PATCH 11/22] fix(nuxt): add e2e-ci target defaults --- .../__snapshots__/application.spec.ts.snap | 16 ++++++ .../application/application.spec.ts | 1 + .../src/generators/application/lib/add-e2e.ts | 51 ++++++++++++++++++- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap index d27094dbc898b..2b212d22ab51b 100644 --- a/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap +++ b/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap @@ -10,6 +10,14 @@ exports[`app generated files content - as-provided - my-app general application .cache" `; +exports[`app generated files content - as-provided - my-app general application should add the nuxt and vitest plugins 1`] = ` +{ + "dependsOn": [ + "^build-static", + ], +} +`; + exports[`app generated files content - as-provided - my-app general application should configure eslint correctly 1`] = ` "{ "extends": ["@nuxt/eslint-config", "../.eslintrc.json"], @@ -341,6 +349,14 @@ exports[`app generated files content - as-provided - myApp general application s .cache" `; +exports[`app generated files content - as-provided - myApp general application should add the nuxt and vitest plugins 1`] = ` +{ + "dependsOn": [ + "^build-static", + ], +} +`; + exports[`app generated files content - as-provided - myApp general application should configure eslint correctly 1`] = ` "{ "extends": ["@nuxt/eslint-config", "../.eslintrc.json"], diff --git a/packages/nuxt/src/generators/application/application.spec.ts b/packages/nuxt/src/generators/application/application.spec.ts index d03bdbe32f7a5..cc7602937ab97 100644 --- a/packages/nuxt/src/generators/application/application.spec.ts +++ b/packages/nuxt/src/generators/application/application.spec.ts @@ -96,6 +96,7 @@ describe('app', () => { nxJson.plugins.find((p) => p.plugin === '@nx/vite/plugin') ) ); + expect(nxJson.targetDefaults['e2e-ci--**/*']).toMatchSnapshot(); }); }); diff --git a/packages/nuxt/src/generators/application/lib/add-e2e.ts b/packages/nuxt/src/generators/application/lib/add-e2e.ts index 35fb27fc83484..029b1d13e6e43 100644 --- a/packages/nuxt/src/generators/application/lib/add-e2e.ts +++ b/packages/nuxt/src/generators/application/lib/add-e2e.ts @@ -7,6 +7,8 @@ import { } from '@nx/devkit'; import { nxVersion } from '../../../utils/versions'; import { NormalizedSchema } from '../schema'; +import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file'; +import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export async function addE2e(host: Tree, options: NormalizedSchema) { if (options.e2eTestRunner === 'cypress') { @@ -21,7 +23,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { tags: [], implicitDependencies: [options.projectName], }); - return await configurationGenerator(host, { + const e2eTask = await configurationGenerator(host, { ...options, project: options.e2eProjectName, directory: 'src', @@ -38,6 +40,30 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { jsx: true, addPlugin: true, }); + + let buildTarget = '^build-static'; + const matchingPlugin = await findPluginForConfigFile( + host, + '@nx/nuxt/plugin', + joinPathFragments(options.appProjectRoot, 'app.json') + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildStaticTargetName ?? 'build-static' + }`; + } + + await addE2eCiTargetDefaults( + host, + '@nx/cypress/plugin', + buildTarget, + joinPathFragments( + options.e2eProjectRoot, + `cypress.config.${options.js ? 'js' : 'ts'}` + ) + ); + + return e2eTask; } else if (options.e2eTestRunner === 'playwright') { const { configurationGenerator } = ensurePackage< typeof import('@nx/playwright') @@ -48,7 +74,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { targets: {}, implicitDependencies: [options.projectName], }); - return configurationGenerator(host, { + const e2eTask = await configurationGenerator(host, { project: options.e2eProjectName, skipFormat: true, skipPackageJson: options.skipPackageJson, @@ -62,6 +88,27 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { } ${options.projectName}`, addPlugin: true, }); + + let buildTarget = '^build-static'; + const matchingPlugin = await findPluginForConfigFile( + host, + '@nx/nuxt/plugin', + joinPathFragments(options.appProjectRoot, 'app.json') + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildStaticTargetName ?? 'build-static' + }`; + } + + await addE2eCiTargetDefaults( + host, + '@nx/playwright/plugin', + buildTarget, + joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`) + ); + + return e2eTask; } return () => {}; } From b309ea094ae6cd10c1d914a51e6112465ba4f50c Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 30 Jul 2024 10:49:04 +0100 Subject: [PATCH 12/22] fix(nuxt): find correct config file for matching plugin --- .../next/src/generators/application/lib/add-e2e.ts | 4 ++-- .../nuxt/src/generators/application/lib/add-e2e.ts | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/next/src/generators/application/lib/add-e2e.ts b/packages/next/src/generators/application/lib/add-e2e.ts index 967d06502229a..11cb0eb3f98b6 100644 --- a/packages/next/src/generators/application/lib/add-e2e.ts +++ b/packages/next/src/generators/application/lib/add-e2e.ts @@ -76,7 +76,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { const matchingPlugin = await findPluginForConfigFile( host, '@nx/next/plugin', - joinPathFragments(options.appProjectRoot, 'app.json') + joinPathFragments(options.appProjectRoot, 'next.config.js') ); if (matchingPlugin && typeof matchingPlugin !== 'string') { buildTarget = `^${ @@ -136,7 +136,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { const matchingPlugin = await findPluginForConfigFile( host, '@nx/next/plugin', - joinPathFragments(options.appProjectRoot, 'app.json') + joinPathFragments(options.appProjectRoot, 'next.config.js') ); if (matchingPlugin && typeof matchingPlugin !== 'string') { buildTarget = `^${ diff --git a/packages/nuxt/src/generators/application/lib/add-e2e.ts b/packages/nuxt/src/generators/application/lib/add-e2e.ts index 029b1d13e6e43..3c7bc78fe1082 100644 --- a/packages/nuxt/src/generators/application/lib/add-e2e.ts +++ b/packages/nuxt/src/generators/application/lib/add-e2e.ts @@ -45,7 +45,10 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { const matchingPlugin = await findPluginForConfigFile( host, '@nx/nuxt/plugin', - joinPathFragments(options.appProjectRoot, 'app.json') + joinPathFragments( + options.appProjectRoot, + `nuxt.config.${options.js ? 'js' : 'ts'}` + ) ); if (matchingPlugin && typeof matchingPlugin !== 'string') { buildTarget = `^${ @@ -93,7 +96,10 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { const matchingPlugin = await findPluginForConfigFile( host, '@nx/nuxt/plugin', - joinPathFragments(options.appProjectRoot, 'app.json') + joinPathFragments( + options.appProjectRoot, + `nuxt.config.${options.js ? 'js' : 'ts'}` + ) ); if (matchingPlugin && typeof matchingPlugin !== 'string') { buildTarget = `^${ From 3a0c06db55c42463d6009cc20b48282e62f6c115 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 30 Jul 2024 10:58:55 +0100 Subject: [PATCH 13/22] fix(react): application generator should add e2e-ci target defaults --- .../application/application.spec.ts | 95 +++++++++++++++++++ .../src/generators/application/lib/add-e2e.ts | 84 +++++++++++++++- 2 files changed, 177 insertions(+), 2 deletions(-) diff --git a/packages/react/src/generators/application/application.spec.ts b/packages/react/src/generators/application/application.spec.ts index e223343c079f6..d1d84e6a767bb 100644 --- a/packages/react/src/generators/application/application.spec.ts +++ b/packages/react/src/generators/application/application.spec.ts @@ -1086,4 +1086,99 @@ describe('app', () => { } `); }); + + it('should add e2e-ci targetDefaults to nxJson when addPlugin=true with playwright', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + let nxJson = readNxJson(tree); + delete nxJson.targetDefaults; + updateNxJson(tree, nxJson); + + // ACT + await applicationGenerator(tree, { + name: 'myapp', + addPlugin: true, + linter: Linter.None, + style: 'none', + e2eTestRunner: 'playwright', + }); + + // ASSERT + nxJson = readNxJson(tree); + expect(nxJson.targetDefaults).toMatchInlineSnapshot(` + { + "e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + } + `); + }); + + it('should add e2e-ci targetDefaults to nxJson when addPlugin=true with cypress', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + let nxJson = readNxJson(tree); + delete nxJson.targetDefaults; + updateNxJson(tree, nxJson); + + // ACT + await applicationGenerator(tree, { + name: 'myapp', + addPlugin: true, + linter: Linter.None, + style: 'none', + e2eTestRunner: 'cypress', + }); + + // ASSERT + nxJson = readNxJson(tree); + expect(nxJson.targetDefaults).toMatchInlineSnapshot(` + { + "e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + } + `); + }); + + it('should add e2e-ci targetDefaults to nxJson when addPlugin=true with cypress and use the defined webpack buildTargetName', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + let nxJson = readNxJson(tree); + delete nxJson.targetDefaults; + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/webpack/plugin', + options: { + buildTargetName: 'build-base', + }, + }); + updateNxJson(tree, nxJson); + + // ACT + await applicationGenerator(tree, { + name: 'myapp', + addPlugin: true, + linter: Linter.None, + style: 'none', + bundler: 'webpack', + e2eTestRunner: 'cypress', + }); + + // ASSERT + nxJson = readNxJson(tree); + expect(nxJson.targetDefaults).toMatchInlineSnapshot(` + { + "e2e-ci--**/*": { + "dependsOn": [ + "^build-base", + ], + }, + } + `); + }); }); diff --git a/packages/react/src/generators/application/lib/add-e2e.ts b/packages/react/src/generators/application/lib/add-e2e.ts index 5da77e12ce49a..2f09da931de21 100644 --- a/packages/react/src/generators/application/lib/add-e2e.ts +++ b/packages/react/src/generators/application/lib/add-e2e.ts @@ -4,6 +4,7 @@ import { ensurePackage, getPackageManagerCommand, joinPathFragments, + readNxJson, } from '@nx/devkit'; import { webStaticServeGenerator } from '@nx/web'; @@ -11,6 +12,8 @@ import { nxVersion } from '../../../utils/versions'; import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin'; import { hasVitePlugin } from '../../../utils/has-vite-plugin'; import { NormalizedSchema } from '../schema'; +import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file'; +import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export async function addE2e( tree: Tree, @@ -41,7 +44,7 @@ export async function addE2e( tags: [], }); - return await configurationGenerator(tree, { + const e2eTask = await configurationGenerator(tree, { ...options, project: options.e2eProjectName, directory: 'src', @@ -64,6 +67,46 @@ export async function addE2e( ciBaseUrl: options.bundler === 'vite' ? options.e2eCiBaseUrl : undefined, }); + + if ( + options.addPlugin || + readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/cypress/plugin' + : p.plugin === '@nx/cypress/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasNxBuildPlugin) { + const configFile = + options.bundler === 'webpack' + ? 'webpack.config.js' + : options.bundler === 'vite' + ? `vite.config.${options.js ? 'js' : 'ts'}` + : 'webpack.config.js'; + const matchingPlugin = await findPluginForConfigFile( + tree, + `@nx/${options.bundler}/plugin`, + joinPathFragments(options.appProjectRoot, configFile) + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + buildTarget, + joinPathFragments( + options.e2eProjectRoot, + `cypress.config.${options.js ? 'js' : 'ts'}` + ) + ); + } + + return e2eTask; } case 'playwright': { const { configurationGenerator } = ensurePackage< @@ -76,7 +119,7 @@ export async function addE2e( targets: {}, implicitDependencies: [options.projectName], }); - return configurationGenerator(tree, { + const e2eTask = await configurationGenerator(tree, { project: options.e2eProjectName, skipFormat: true, skipPackageJson: options.skipPackageJson, @@ -91,6 +134,43 @@ export async function addE2e( rootProject: options.rootProject, addPlugin: options.addPlugin, }); + + if ( + options.addPlugin || + readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/playwright/plugin' + : p.plugin === '@nx/playwright/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasNxBuildPlugin) { + const configFile = + options.bundler === 'webpack' + ? 'webpack.config.js' + : options.bundler === 'vite' + ? `vite.config.${options.js ? 'js' : 'ts'}` + : 'webpack.config.js'; + const matchingPlugin = await findPluginForConfigFile( + tree, + `@nx/${options.bundler}/plugin`, + joinPathFragments(options.appProjectRoot, configFile) + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + tree, + '@nx/playwright/plugin', + buildTarget, + joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`) + ); + } + + return e2eTask; } case 'none': default: From a3b876179b5cf2dd8cc29447a4be26c45760eea6 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 30 Jul 2024 11:08:08 +0100 Subject: [PATCH 14/22] fix(remix): application generator should add e2e-ci target defaults --- .../application/application.impl.spec.ts | 18 ++++- .../src/generators/application/lib/add-e2e.ts | 77 ++++++++++++++++++- 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/packages/remix/src/generators/application/application.impl.spec.ts b/packages/remix/src/generators/application/application.impl.spec.ts index 75d8ed5cad92d..f9b0d2576758a 100644 --- a/packages/remix/src/generators/application/application.impl.spec.ts +++ b/packages/remix/src/generators/application/application.impl.spec.ts @@ -1,6 +1,6 @@ import 'nx/src/internal-testing-utils/mock-project-graph'; -import { joinPathFragments, readJson, type Tree } from '@nx/devkit'; +import { joinPathFragments, readJson, readNxJson, type Tree } from '@nx/devkit'; import * as devkit from '@nx/devkit'; import { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; @@ -129,6 +129,14 @@ describe('Remix Application', () => { expectTargetsToBeCorrect(tree, '.'); expect(tree.read('e2e/cypress.config.ts', 'utf-8')).toMatchSnapshot(); + expect(readNxJson(tree).targetDefaults['e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); }); }); @@ -148,6 +156,14 @@ describe('Remix Application', () => { expectTargetsToBeCorrect(tree, '.'); expect(tree.read('e2e/playwright.config.ts', 'utf-8')).toMatchSnapshot(); + expect(readNxJson(tree).targetDefaults['e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); }); }); diff --git a/packages/remix/src/generators/application/lib/add-e2e.ts b/packages/remix/src/generators/application/lib/add-e2e.ts index 6f5fec99e607c..43283705b7b02 100644 --- a/packages/remix/src/generators/application/lib/add-e2e.ts +++ b/packages/remix/src/generators/application/lib/add-e2e.ts @@ -6,11 +6,19 @@ import { updateProjectConfiguration, ensurePackage, getPackageManagerCommand, + readNxJson, } from '@nx/devkit'; import { type NormalizedSchema } from './normalize-options'; import { getPackageVersion } from '../../../utils/versions'; +import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file'; +import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export async function addE2E(tree: Tree, options: NormalizedSchema) { + const hasRemixPlugin = readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/remix/plugin' + : p.plugin === '@nx/remix/plugin' + ); if (options.e2eTestRunner === 'cypress') { const { configurationGenerator } = ensurePackage< typeof import('@nx/cypress') @@ -25,7 +33,7 @@ export async function addE2E(tree: Tree, options: NormalizedSchema) { implicitDependencies: [options.projectName], }); - return await configurationGenerator(tree, { + const e2eTask = await configurationGenerator(tree, { project: options.e2eProjectName, directory: 'src', skipFormat: true, @@ -33,6 +41,40 @@ export async function addE2E(tree: Tree, options: NormalizedSchema) { baseUrl: options.e2eWebServerAddress, addPlugin: options.addPlugin, }); + + if ( + options.addPlugin || + readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/cypress/plugin' + : p.plugin === '@nx/cypress/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasRemixPlugin) { + const matchingPlugin = await findPluginForConfigFile( + tree, + `@nx/remix/plugin`, + joinPathFragments(options.projectRoot, 'remix.config.js') + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + buildTarget, + joinPathFragments( + options.e2eProjectRoot, + `cypress.config.${options.js ? 'js' : 'ts'}` + ) + ); + } + + return e2eTask; } else if (options.e2eTestRunner === 'playwright') { const { configurationGenerator } = ensurePackage< typeof import('@nx/playwright') @@ -47,7 +89,7 @@ export async function addE2E(tree: Tree, options: NormalizedSchema) { implicitDependencies: [options.projectName], }); - return configurationGenerator(tree, { + const e2eTask = await configurationGenerator(tree, { project: options.e2eProjectName, skipFormat: true, skipPackageJson: false, @@ -62,6 +104,37 @@ export async function addE2E(tree: Tree, options: NormalizedSchema) { rootProject: options.rootProject, addPlugin: options.addPlugin, }); + + if ( + options.addPlugin || + readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/playwright/plugin' + : p.plugin === '@nx/playwright/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasRemixPlugin) { + const matchingPlugin = await findPluginForConfigFile( + tree, + `@nx/remix/plugin`, + joinPathFragments(options.projectRoot, 'remix.config.js') + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + tree, + '@nx/playwright/plugin', + buildTarget, + joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`) + ); + } + + return e2eTask; } else { return () => {}; } From 5336749b048174c05a33f26b4d80e4a04130f2dc Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 30 Jul 2024 11:13:45 +0100 Subject: [PATCH 15/22] fix(vue): application generator should add e2e-ci target defaults --- .../application/application.spec.ts | 9 +++ .../src/generators/application/lib/add-e2e.ts | 77 ++++++++++++++++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/packages/vue/src/generators/application/application.spec.ts b/packages/vue/src/generators/application/application.spec.ts index aab1a17a2300c..d52eb53c323ed 100644 --- a/packages/vue/src/generators/application/application.spec.ts +++ b/packages/vue/src/generators/application/application.spec.ts @@ -40,6 +40,7 @@ describe('application generator', () => { ...options, unitTestRunner: 'vitest', e2eTestRunner: 'playwright', + addPlugin: true, }); expect(tree.read('.eslintrc.json', 'utf-8')).toMatchSnapshot(); expect(tree.read('test/vite.config.ts', 'utf-8')).toMatchSnapshot(); @@ -49,6 +50,14 @@ describe('application generator', () => { tree.read('test-e2e/playwright.config.ts', 'utf-8') ).toMatchSnapshot(); expect(listFiles(tree)).toMatchSnapshot(); + expect(readNxJson(tree).targetDefaults['e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); }); it('should set up project correctly for cypress', async () => { diff --git a/packages/vue/src/generators/application/lib/add-e2e.ts b/packages/vue/src/generators/application/lib/add-e2e.ts index 9268da0e2bd16..ae2578e334e9c 100644 --- a/packages/vue/src/generators/application/lib/add-e2e.ts +++ b/packages/vue/src/generators/application/lib/add-e2e.ts @@ -10,6 +10,8 @@ import { webStaticServeGenerator } from '@nx/web'; import { nxVersion } from '../../../utils/versions'; import { NormalizedSchema } from '../schema'; +import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file'; +import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export async function addE2e( tree: Tree, @@ -52,7 +54,7 @@ export async function addE2e( tags: [], implicitDependencies: [options.projectName], }); - return await configurationGenerator(tree, { + const e2eTask = await configurationGenerator(tree, { ...options, project: options.e2eProjectName, directory: 'src', @@ -70,6 +72,43 @@ export async function addE2e( ciWebServerCommand: `nx run ${options.projectName}:${e2eCiWebServerTarget}`, ciBaseUrl: 'http://localhost:4300', }); + + if ( + options.addPlugin || + readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/cypress/plugin' + : p.plugin === '@nx/cypress/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasPlugin) { + const matchingPlugin = await findPluginForConfigFile( + tree, + `@nx/vite/plugin`, + joinPathFragments( + options.appProjectRoot, + `vite.config.${options.js ? 'js' : 'ts'}` + ) + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + buildTarget, + joinPathFragments( + options.e2eProjectRoot, + `cypress.config.${options.js ? 'js' : 'ts'}` + ) + ); + } + + return e2eTask; } case 'playwright': { const { configurationGenerator } = ensurePackage< @@ -82,7 +121,7 @@ export async function addE2e( targets: {}, implicitDependencies: [options.projectName], }); - return configurationGenerator(tree, { + const e2eTask = await configurationGenerator(tree, { ...options, project: options.e2eProjectName, skipFormat: true, @@ -96,6 +135,40 @@ export async function addE2e( }:${e2eCiWebServerTarget}`, webServerAddress: 'http://localhost:4300', }); + + if ( + options.addPlugin || + readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/playwright/plugin' + : p.plugin === '@nx/playwright/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasPlugin) { + const matchingPlugin = await findPluginForConfigFile( + tree, + `@nx/vite/plugin`, + joinPathFragments( + options.appProjectRoot, + `vite.config.${options.js ? 'js' : 'ts'}` + ) + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + tree, + '@nx/playwright/plugin', + buildTarget, + joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`) + ); + } + + return e2eTask; } case 'none': default: From 0d90aad43c258028916c89c66b59a24a6372edda Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 30 Jul 2024 11:23:35 +0100 Subject: [PATCH 16/22] fix(web): application generator should add e2e-ci target defaults --- .../application/application.spec.ts | 8 ++ .../src/generators/application/application.ts | 90 +++++++++++++++++-- 2 files changed, 90 insertions(+), 8 deletions(-) diff --git a/packages/web/src/generators/application/application.spec.ts b/packages/web/src/generators/application/application.spec.ts index 6aec78f324b93..cab0c67e7d4c2 100644 --- a/packages/web/src/generators/application/application.spec.ts +++ b/packages/web/src/generators/application/application.spec.ts @@ -43,6 +43,14 @@ describe('app', () => { expect(readProjectConfiguration(tree, 'my-app-e2e').root).toEqual( 'my-app-e2e' ); + expect(readNxJson(tree).targetDefaults['e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); }, 60_000); it('should update tags and implicit dependencies', async () => { diff --git a/packages/web/src/generators/application/application.ts b/packages/web/src/generators/application/application.ts index 823d771bbbd2d..9dbed0511df91 100644 --- a/packages/web/src/generators/application/application.ts +++ b/packages/web/src/generators/application/application.ts @@ -9,6 +9,7 @@ import { joinPathFragments, names, offsetFromRoot, + type PluginConfiguration, readNxJson, readProjectConfiguration, runTasksInSerial, @@ -37,12 +38,15 @@ import { webInitGenerator } from '../init/init'; import { Schema } from './schema'; import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope'; import { hasWebpackPlugin } from '../../utils/has-webpack-plugin'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; +import { + addBuildTargetDefaults, + addE2eCiTargetDefaults, +} from '@nx/devkit/src/generators/target-defaults-utils'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; import { VitePluginOptions } from '@nx/vite/src/plugins/plugin'; import { WebpackPluginOptions } from '@nx/webpack/src/plugins/plugin'; -import { hasVitePlugin } from '../../utils/has-vite-plugin'; import staticServeConfiguration from '../static-serve/static-serve-configuration'; +import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file'; interface NormalizedSchema extends Schema { projectName: string; @@ -368,10 +372,20 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) { tasks.push(lintTask); } - const hasNxBuildPlugin = - (options.bundler === 'webpack' && hasWebpackPlugin(host)) || - (options.bundler === 'vite' && hasVitePlugin(host)); - if (!hasNxBuildPlugin) { + const nxJson = readNxJson(host); + let hasPlugin: PluginConfiguration | undefined; + let buildPlugin: string; + let buildConfigFile: string; + if (options.bundler === 'webpack' || options.bundler === 'vite') { + buildPlugin = `@nx/${options.bundler}/plugin`; + buildConfigFile = + options.bundler === 'webpack' ? 'webpack.config.js' : `vite.config.ts`; + hasPlugin = nxJson.plugins?.find((p) => + typeof p === 'string' ? p === buildPlugin : p.plugin === buildPlugin + ); + } + + if (!hasPlugin) { await staticServeConfiguration(host, { buildTarget: `${options.projectName}:build`, spa: true, @@ -396,17 +410,47 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) { baseUrl: options.e2eWebServerAddress, directory: 'src', skipFormat: true, - webServerCommands: hasNxBuildPlugin + webServerCommands: hasPlugin ? { default: `nx run ${options.projectName}:${options.e2eWebServerTarget}`, production: `nx run ${options.projectName}:preview`, } : undefined, - ciWebServerCommand: hasNxBuildPlugin + ciWebServerCommand: hasPlugin ? `nx run ${options.projectName}:${options.e2eCiWebServerTarget}` : undefined, ciBaseUrl: options.bundler === 'vite' ? options.e2eCiBaseUrl : undefined, }); + + if ( + options.addPlugin || + readNxJson(host).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/cypress/plugin' + : p.plugin === '@nx/cypress/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasPlugin) { + const matchingPlugin = await findPluginForConfigFile( + host, + buildPlugin, + joinPathFragments(options.appProjectRoot, buildConfigFile) + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + host, + '@nx/cypress/plugin', + buildTarget, + joinPathFragments(options.e2eProjectRoot, `cypress.config.ts`) + ); + } + tasks.push(cypressTask); } else if (options.e2eTestRunner === 'playwright') { const { configurationGenerator: playwrightConfigGenerator } = ensurePackage< @@ -434,6 +478,36 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) { webServerAddress: options.e2eCiBaseUrl, addPlugin: options.addPlugin, }); + + if ( + options.addPlugin || + readNxJson(host).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/playwright/plugin' + : p.plugin === '@nx/playwright/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasPlugin) { + const matchingPlugin = await findPluginForConfigFile( + host, + buildPlugin, + joinPathFragments(options.appProjectRoot, buildConfigFile) + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + host, + '@nx/playwright/plugin', + buildTarget, + joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`) + ); + } + tasks.push(playwrightTask); } if (options.unitTestRunner === 'jest') { From b0dbf5c43b09cf760d31d5feeb680c1066d96852 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 30 Jul 2024 11:25:58 +0100 Subject: [PATCH 17/22] fix(repo): add missing snapshots --- .../__snapshots__/application.spec.ts.snap | 323 ++++++++++++++++++ .../application/application.spec.ts | 75 +++- 2 files changed, 392 insertions(+), 6 deletions(-) diff --git a/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap index 2a0f9f246fbc8..f0aba9c3e5ba3 100644 --- a/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap +++ b/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap @@ -322,6 +322,92 @@ exports[`app --project-name-and-root-format=derived should generate correctly wh } `; +exports[`app --project-name-and-root-format=derived should generate correctly when directory is provided: e2e tsconfig.json 1`] = ` +{ + "compilerOptions": { + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "module": "commonjs", + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "outDir": "../../../dist/out-tsc", + "sourceMap": false, + "strict": true, + "types": [ + "cypress", + "node", + ], + }, + "extends": "../../../tsconfig.base.json", + "include": [ + "**/*.ts", + "**/*.js", + "cypress.config.ts", + "**/*.cy.ts", + "**/*.cy.js", + "**/*.d.ts", + ], +} +`; + +exports[`app --project-name-and-root-format=derived should generate correctly when directory is provided: tsconfig.app.json 1`] = ` +{ + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [], + }, + "exclude": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + ], + "extends": "./tsconfig.json", + "files": [ + "src/main.ts", + ], + "include": [ + "src/**/*.d.ts", + ], +} +`; + +exports[`app --project-name-and-root-format=derived should generate correctly when directory is provided: tsconfig.json 1`] = ` +{ + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true, + }, + "compilerOptions": { + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "strict": true, + "target": "es2022", + }, + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.editor.json", + }, + { + "path": "./tsconfig.app.json", + }, + { + "path": "./tsconfig.spec.json", + }, + ], +} +`; + exports[`app --project-name-and-root-format=derived should generate correctly when no directory is provided 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", @@ -438,6 +524,92 @@ exports[`app --project-name-and-root-format=derived should generate correctly wh } `; +exports[`app --project-name-and-root-format=derived should generate correctly when no directory is provided: e2e tsconfig.json 1`] = ` +{ + "compilerOptions": { + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "module": "commonjs", + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "outDir": "../../dist/out-tsc", + "sourceMap": false, + "strict": true, + "types": [ + "cypress", + "node", + ], + }, + "extends": "../../tsconfig.base.json", + "include": [ + "**/*.ts", + "**/*.js", + "cypress.config.ts", + "**/*.cy.ts", + "**/*.cy.js", + "**/*.d.ts", + ], +} +`; + +exports[`app --project-name-and-root-format=derived should generate correctly when no directory is provided: tsconfig.app.json 1`] = ` +{ + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [], + }, + "exclude": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + ], + "extends": "./tsconfig.json", + "files": [ + "src/main.ts", + ], + "include": [ + "src/**/*.d.ts", + ], +} +`; + +exports[`app --project-name-and-root-format=derived should generate correctly when no directory is provided: tsconfig.json 1`] = ` +{ + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true, + }, + "compilerOptions": { + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "strict": true, + "target": "es2022", + }, + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.editor.json", + }, + { + "path": "./tsconfig.app.json", + }, + { + "path": "./tsconfig.spec.json", + }, + ], +} +`; + exports[`app --standalone should generate a standalone app correctly with routing 1`] = ` "import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; @@ -594,6 +766,71 @@ export const appConfig: ApplicationConfig = { " `; +exports[`app --strict should enable strict type checking: app tsconfig.json 1`] = ` +{ + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true, + }, + "compilerOptions": { + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "strict": true, + "target": "es2022", + }, + "extends": "../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.editor.json", + }, + { + "path": "./tsconfig.app.json", + }, + { + "path": "./tsconfig.spec.json", + }, + ], +} +`; + +exports[`app --strict should enable strict type checking: e2e tsconfig.json 1`] = ` +{ + "compilerOptions": { + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "module": "commonjs", + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "outDir": "../dist/out-tsc", + "sourceMap": false, + "strict": true, + "types": [ + "cypress", + "node", + ], + }, + "extends": "../tsconfig.base.json", + "include": [ + "**/*.ts", + "**/*.js", + "cypress.config.ts", + "**/*.cy.ts", + "**/*.cy.js", + "**/*.d.ts", + ], +} +`; + exports[`app angular compat support should import "ApplicationConfig" from "@angular/platform-browser" 1`] = ` "import { ApplicationConfig } from '@angular/core'; import { provideRouter } from '@angular/router'; @@ -910,6 +1147,92 @@ exports[`app not nested should create project configs 2`] = ` } `; +exports[`app not nested should generate files: e2e tsconfig.json 1`] = ` +{ + "compilerOptions": { + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "module": "commonjs", + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "outDir": "../dist/out-tsc", + "sourceMap": false, + "strict": true, + "types": [ + "cypress", + "node", + ], + }, + "extends": "../tsconfig.base.json", + "include": [ + "**/*.ts", + "**/*.js", + "cypress.config.ts", + "**/*.cy.ts", + "**/*.cy.js", + "**/*.d.ts", + ], +} +`; + +exports[`app not nested should generate files: tsconfig.app.json 1`] = ` +{ + "compilerOptions": { + "outDir": "../dist/out-tsc", + "types": [], + }, + "exclude": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + ], + "extends": "./tsconfig.json", + "files": [ + "src/main.ts", + ], + "include": [ + "src/**/*.d.ts", + ], +} +`; + +exports[`app not nested should generate files: tsconfig.json 1`] = ` +{ + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true, + }, + "compilerOptions": { + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "strict": true, + "target": "es2022", + }, + "extends": "../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.editor.json", + }, + { + "path": "./tsconfig.app.json", + }, + { + "path": "./tsconfig.spec.json", + }, + ], +} +`; + exports[`app should generate correct tsconfig.editor.json 1`] = ` { "compilerOptions": {}, diff --git a/packages/next/src/generators/application/application.spec.ts b/packages/next/src/generators/application/application.spec.ts index f67e1b753db88..8f0a1b2380d98 100644 --- a/packages/next/src/generators/application/application.spec.ts +++ b/packages/next/src/generators/application/application.spec.ts @@ -183,7 +183,7 @@ describe('app', () => { describe('--style scss', () => { it('should generate scss styles', async () => { - const name = uniq(); + const name = 'myapp'; await applicationGenerator(tree, { name, style: 'scss', @@ -200,13 +200,29 @@ describe('app', () => { "import './global.css'; export const metadata = { - title: 'Welcome to `); + title: 'Welcome to myapp', + description: 'Generated by create-nx-workspace', + }; + + export default function RootLayout({ + children, + }: { + children: React.ReactNode; + }) { + return ( + + {children} + + ); + } + " + `); }); }); describe('--style less', () => { it('should generate less styles', async () => { - const name = uniq(); + const name = 'myapp2'; await applicationGenerator(tree, { name, style: 'less', @@ -223,7 +239,23 @@ describe('app', () => { "import './global.less'; export const metadata = { - title: 'Welcome to `); + title: 'Welcome to myapp2', + description: 'Generated by create-nx-workspace', + }; + + export default function RootLayout({ + children, + }: { + children: React.ReactNode; + }) { + return ( + + {children} + + ); + } + " + `); }); }); @@ -573,7 +605,7 @@ describe('app', () => { describe('--linter', () => { describe('default (eslint)', () => { it('should add .eslintrc.json and dependencies', async () => { - const name = uniq(); + const name = 'myapp3'; await applicationGenerator(tree, { name, @@ -613,7 +645,38 @@ describe('app', () => { "rules": { "@next/next/no-html-link-for-pages": [ "error", - "`); + "myapp3/pages", + ], + }, + }, + { + "files": [ + "*.ts", + "*.tsx", + ], + "rules": {}, + }, + { + "files": [ + "*.js", + "*.jsx", + ], + "rules": {}, + }, + { + "env": { + "jest": true, + }, + "files": [ + "*.spec.ts", + "*.spec.tsx", + "*.spec.js", + "*.spec.jsx", + ], + }, + ], + } + `); }); }); From 42f1ed32643ede39479d6acf0c9ff83ed14aa453 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Wed, 31 Jul 2024 11:34:23 +0100 Subject: [PATCH 18/22] feat(testing): add cypress migration to add e2e-ci targetDefaults --- packages/cypress/migrations.json | 6 + .../add-e2e-ci-target-defaults.spec.ts | 366 ++++++++++++++++++ .../add-e2e-ci-target-defaults.ts | 104 +++++ 3 files changed, 476 insertions(+) create mode 100644 packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts create mode 100644 packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts diff --git a/packages/cypress/migrations.json b/packages/cypress/migrations.json index b1df577eb07dc..2a7993064badd 100644 --- a/packages/cypress/migrations.json +++ b/packages/cypress/migrations.json @@ -35,6 +35,12 @@ "version": "19.6.0-beta.0", "description": "Update ciWebServerCommand to use previewTargetName if Vite is detected for the application.", "implementation": "./src/migrations/update-19-6-0/update-ci-webserver-for-vite" + }, + "update-19-6-0-add-e2e-ci-target-defaults": { + "cli": "nx", + "version": "19.6.0-beta.0", + "description": "Add inferred ciTargetNames to targetDefaults with dependsOn to ensure dependent application builds are scheduled before atomized tasks.", + "implementation": "./src/migrations/update-19-6-0/add-e2e-ci-target-defaults" } }, "packageJsonUpdates": { diff --git a/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts b/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts new file mode 100644 index 0000000000000..e82fea4f7de3e --- /dev/null +++ b/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts @@ -0,0 +1,366 @@ +import { ProjectGraph, readNxJson, type Tree, updateNxJson } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; +import addE2eCiTargetDefaults from './add-e2e-ci-target-defaults'; + +let projectGraph: ProjectGraph; +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: jest.fn().mockImplementation(async () => { + return projectGraph; + }), +})); + +describe('add-e2e-ci-target-defaults', () => { + let tree: Tree; + let tempFs: TempFs; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + tempFs = new TempFs('add-e2e-ci'); + tree.root = tempFs.tempDir; + projectGraph = { + nodes: {}, + dependencies: {}, + externalNodes: {}, + }; + }); + + afterEach(() => { + tempFs.reset(); + }); + + it('should do nothing when the plugin is not registered', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = []; + updateNxJson(tree, nxJson); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should add the targetDefaults with the correct ciTargetName and buildTarget when there is one plugin', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should add the targetDefaults with the correct ciTargetNames and buildTargets when there is more than one plugin', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + include: ['app-e2e/**'], + }, + { + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'cypress:e2e-ci', + }, + include: ['shop-e2e/**'], + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs); + addProject(tree, tempFs, { + buildTargetName: 'build', + ciTargetName: 'cypress:e2e-ci', + appName: 'shop', + }); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "cypress:e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + "e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should only add the targetDefaults with the correct ciTargetName and buildTargets when there is more than one plugin with only one matching multiple projects', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + include: ['cart-e2e/**'], + }, + { + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'cypress:e2e-ci', + }, + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs); + addProject(tree, tempFs, { + buildTargetName: 'bundle', + ciTargetName: 'cypress:e2e-ci', + appName: 'shop', + }); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "cypress:e2e-ci--**/*": { + "dependsOn": [ + "^build", + "^bundle", + ], + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should not add the targetDefaults when the ciWebServerCommand is not present', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'cypress:e2e-ci', + }, + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs, { + appName: 'app', + buildTargetName: 'build', + ciTargetName: 'cypress:e2e-ci', + noCi: true, + }); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "lint": { + "cache": true, + }, + } + `); + }); +}); + +function addProject( + tree: Tree, + tempFs: TempFs, + overrides: { + ciTargetName: string; + buildTargetName: string; + appName: string; + noCi?: boolean; + } = { ciTargetName: 'e2e-ci', buildTargetName: 'build', appName: 'app' } +) { + const appProjectConfig = { + name: overrides.appName, + root: overrides.appName, + sourceRoot: `${overrides.appName}/src`, + projectType: 'application', + }; + const viteConfig = `/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/${overrides.appName}', + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + outDir: '../../dist/${overrides.appName}', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, +});`; + + const e2eProjectConfig = { + name: `${overrides.appName}-e2e`, + root: `${overrides.appName}-e2e`, + sourceRoot: `${overrides.appName}-e2e/src`, + projectType: 'application', + }; + + const cypressConfig = `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { + cypressDir: 'src', + bundler: 'vite', + webServerCommands: { + default: 'nx run ${overrides.appName}:serve', + production: 'nx run ${overrides.appName}:preview', + }, + ${ + !overrides.noCi + ? `ciWebServerCommand: 'nx run ${overrides.appName}:serve-static',` + : '' + } + }), + baseUrl: 'http://localhost:4200', + }, +}); +`; + + tree.write(`${overrides.appName}/vite.config.ts`, viteConfig); + tree.write( + `${overrides.appName}/project.json`, + JSON.stringify(appProjectConfig) + ); + tree.write(`${overrides.appName}-e2e/cypress.config.ts`, cypressConfig); + tree.write( + `${overrides.appName}-e2e/project.json`, + JSON.stringify(e2eProjectConfig) + ); + tempFs.createFilesSync({ + [`${overrides.appName}/vite.config.ts`]: viteConfig, + [`${overrides.appName}/project.json`]: JSON.stringify(appProjectConfig), + [`${overrides.appName}-e2e/cypress.config.ts`]: cypressConfig, + [`${overrides.appName}-e2e/project.json`]: JSON.stringify(e2eProjectConfig), + }); + + projectGraph.nodes[overrides.appName] = { + name: overrides.appName, + type: 'app', + data: { + projectType: 'application', + root: overrides.appName, + targets: { + [overrides.buildTargetName]: {}, + 'serve-static': { + options: { + buildTarget: overrides.buildTargetName, + }, + }, + }, + }, + }; + + projectGraph.nodes[`${overrides.appName}-e2e`] = { + name: `${overrides.appName}-e2e`, + type: 'app', + data: { + projectType: 'application', + root: `${overrides.appName}-e2e`, + targets: { + e2e: {}, + [overrides.ciTargetName]: {}, + }, + }, + }; +} diff --git a/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts b/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts new file mode 100644 index 0000000000000..2d53c1506f2f1 --- /dev/null +++ b/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts @@ -0,0 +1,104 @@ +import { + type Tree, + type CreateNodesV2, + formatFiles, + readNxJson, + createProjectGraphAsync, + parseTargetString, +} from '@nx/devkit'; +import { addE2eCiTargetDefaults as _addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; +import { LoadedNxPlugin } from 'nx/src/project-graph/plugins/internal-api'; +import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils'; +import { + ProjectConfigurationsError, + retrieveProjectConfigurations, +} from 'nx/src/devkit-internals'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import { type CypressPluginOptions } from '../../plugins/plugin'; + +export default async function addE2eCiTargetDefaults(tree: Tree) { + const pluginName = '@nx/cypress/plugin'; + const graph = await createProjectGraphAsync(); + const nxJson = readNxJson(tree); + const matchingPluginRegistrations = nxJson.plugins?.filter((p) => + typeof p === 'string' ? p === pluginName : p.plugin === pluginName + ); + + const { + createNodesV2, + }: { createNodesV2: CreateNodesV2 } = await import( + pluginName + ); + + for (const plugin of matchingPluginRegistrations) { + let projectConfigs: ConfigurationResult; + try { + const loadedPlugin = new LoadedNxPlugin( + { createNodesV2, name: pluginName }, + plugin + ); + projectConfigs = await retrieveProjectConfigurations( + [loadedPlugin], + tree.root, + nxJson + ); + } catch (e) { + if (e instanceof ProjectConfigurationsError) { + projectConfigs = e.partialProjectConfigurationsResult; + } else { + throw e; + } + } + + for (const configFile of projectConfigs.matchingProjectFiles) { + const configFileContents = tree.read(configFile, 'utf-8'); + if (!configFileContents.includes('ciWebServerCommand')) { + continue; + } + + const ast = tsquery.ast(configFileContents); + const CI_WEBSERVER_COMMAND_SELECTOR = + 'ObjectLiteralExpression PropertyAssignment:has(Identifier[name=ciWebServerCommand]) > StringLiteral'; + const nodes = tsquery(ast, CI_WEBSERVER_COMMAND_SELECTOR, { + visitAllChildren: true, + }); + if (!nodes.length) { + continue; + } + const ciWebServerCommand = nodes[0].getText(); + const NX_TARGET_REGEX = "(?<=nx run )[^']+"; + const matches = ciWebServerCommand.match(NX_TARGET_REGEX); + if (!matches) { + continue; + } + const targetString = matches[0]; + const { project, target, configuration } = parseTargetString( + targetString, + graph + ); + + const serveStaticTarget = graph.nodes[project].data.targets[target]; + if ( + !serveStaticTarget.options.buildTarget && + configuration && + !serveStaticTarget.configurations?.[configuration]?.buildTarget + ) { + continue; + } + + const serveStaticBuildTarget = configuration + ? serveStaticTarget.configurations[configuration].buildTarget + : serveStaticTarget.options.buildTarget; + + const buildTarget = + '^' + + (serveStaticBuildTarget.includes(':') + ? parseTargetString(serveStaticBuildTarget, graph).target + : serveStaticBuildTarget); + + await _addE2eCiTargetDefaults(tree, pluginName, buildTarget, configFile); + } + } + + await formatFiles(tree); +} From c9c6731ce04b3bb21fcd3a44d27d4b4cf38bfbc0 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 2 Aug 2024 16:47:14 +0100 Subject: [PATCH 19/22] fix(testing): add migration for playwright e2e-ci --- .../add-e2e-ci-target-defaults.ts | 24 +- .../add-e2e-ci-target-defaults.spec.ts | 333 ++++++++++++++++++ .../add-e2e-ci-target-defaults.ts | 112 ++++++ .../use-serve-static-preview-for-command.ts | 2 + 4 files changed, 456 insertions(+), 15 deletions(-) create mode 100644 packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts create mode 100644 packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts diff --git a/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts b/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts index 2d53c1506f2f1..ae01f7061be12 100644 --- a/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts +++ b/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts @@ -78,23 +78,17 @@ export default async function addE2eCiTargetDefaults(tree: Tree) { ); const serveStaticTarget = graph.nodes[project].data.targets[target]; - if ( - !serveStaticTarget.options.buildTarget && - configuration && - !serveStaticTarget.configurations?.[configuration]?.buildTarget - ) { - continue; + let resolvedBuildTarget: string; + if (serveStaticTarget.dependsOn) { + resolvedBuildTarget = serveStaticTarget.dependsOn.join(','); + } else { + resolvedBuildTarget = + (configuration + ? serveStaticTarget.configurations[configuration].buildTarget + : serveStaticTarget.options.buildTarget) ?? 'build'; } - const serveStaticBuildTarget = configuration - ? serveStaticTarget.configurations[configuration].buildTarget - : serveStaticTarget.options.buildTarget; - - const buildTarget = - '^' + - (serveStaticBuildTarget.includes(':') - ? parseTargetString(serveStaticBuildTarget, graph).target - : serveStaticBuildTarget); + const buildTarget = `^${resolvedBuildTarget}`; await _addE2eCiTargetDefaults(tree, pluginName, buildTarget, configFile); } diff --git a/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts b/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts new file mode 100644 index 0000000000000..ba65c8f32bf68 --- /dev/null +++ b/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts @@ -0,0 +1,333 @@ +import { ProjectGraph, readNxJson, type Tree, updateNxJson } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; +import addE2eCiTargetDefaults from './add-e2e-ci-target-defaults'; + +let projectGraph: ProjectGraph; +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: jest.fn().mockImplementation(async () => { + return projectGraph; + }), +})); + +describe('add-e2e-ci-target-defaults', () => { + let tree: Tree; + let tempFs: TempFs; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + tempFs = new TempFs('add-e2e-ci'); + tree.root = tempFs.tempDir; + projectGraph = { + nodes: {}, + dependencies: {}, + externalNodes: {}, + }; + }); + + afterEach(() => { + tempFs.reset(); + }); + + it('should do nothing when the plugin is not registered', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = []; + updateNxJson(tree, nxJson); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should add the targetDefaults with the correct ciTargetName and buildTarget when there is one plugin', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/playwright/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should add the targetDefaults with the correct ciTargetNames and buildTargets when there is more than one plugin', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/playwright/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + include: ['app-e2e/**'], + }, + { + plugin: '@nx/playwright/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'playwright:e2e-ci', + }, + include: ['shop-e2e/**'], + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs); + addProject(tree, tempFs, { + buildTargetName: 'build', + ciTargetName: 'playwright:e2e-ci', + appName: 'shop', + }); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + "lint": { + "cache": true, + }, + "playwright:e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + } + `); + }); + + it('should only add the targetDefaults with the correct ciTargetName and buildTargets when there is more than one plugin with only one matching multiple projects', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/playwright/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + include: ['cart-e2e/**'], + }, + { + plugin: '@nx/playwright/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'playwright:e2e-ci', + }, + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs); + addProject(tree, tempFs, { + buildTargetName: 'bundle', + ciTargetName: 'playwright:e2e-ci', + appName: 'shop', + }); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "lint": { + "cache": true, + }, + "playwright:e2e-ci--**/*": { + "dependsOn": [ + "^build", + "^bundle", + ], + }, + } + `); + }); +}); + +function addProject( + tree: Tree, + tempFs: TempFs, + overrides: { + ciTargetName: string; + buildTargetName: string; + appName: string; + noCi?: boolean; + } = { ciTargetName: 'e2e-ci', buildTargetName: 'build', appName: 'app' } +) { + const appProjectConfig = { + name: overrides.appName, + root: overrides.appName, + sourceRoot: `${overrides.appName}/src`, + projectType: 'application', + }; + const viteConfig = `/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/${overrides.appName}', + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + outDir: '../../dist/${overrides.appName}', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, +});`; + + const e2eProjectConfig = { + name: `${overrides.appName}-e2e`, + root: `${overrides.appName}-e2e`, + sourceRoot: `${overrides.appName}-e2e/src`, + projectType: 'application', + }; + + const playwrightConfig = `import { defineConfig, devices } from '@playwright/test'; +import { nxE2EPreset } from '@nx/playwright/preset'; + +import { workspaceRoot } from '@nx/devkit'; + +const baseURL = process.env['BASE_URL'] || 'http://localhost:4200'; + +export default defineConfig({ + ...nxE2EPreset(__filename, { testDir: './src' }), + use: { + baseURL, + trace: 'on-first-retry', + }, + webServer: { + command: 'npx nx run ${overrides.appName}:serve-static', + url: 'http://localhost:4200', + reuseExistingServer: !process.env.CI, + cwd: workspaceRoot, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +});`; + + tree.write(`${overrides.appName}/vite.config.ts`, viteConfig); + tree.write( + `${overrides.appName}/project.json`, + JSON.stringify(appProjectConfig) + ); + tree.write(`${overrides.appName}-e2e/playwright.config.ts`, playwrightConfig); + tree.write( + `${overrides.appName}-e2e/project.json`, + JSON.stringify(e2eProjectConfig) + ); + tempFs.createFilesSync({ + [`${overrides.appName}/vite.config.ts`]: viteConfig, + [`${overrides.appName}/project.json`]: JSON.stringify(appProjectConfig), + [`${overrides.appName}-e2e/playwright.config.ts`]: playwrightConfig, + [`${overrides.appName}-e2e/project.json`]: JSON.stringify(e2eProjectConfig), + }); + + projectGraph.nodes[overrides.appName] = { + name: overrides.appName, + type: 'app', + data: { + projectType: 'application', + root: overrides.appName, + targets: { + [overrides.buildTargetName]: {}, + 'serve-static': { + dependsOn: [overrides.buildTargetName], + options: { + buildTarget: overrides.buildTargetName, + }, + }, + }, + }, + }; + + projectGraph.nodes[`${overrides.appName}-e2e`] = { + name: `${overrides.appName}-e2e`, + type: 'app', + data: { + projectType: 'application', + root: `${overrides.appName}-e2e`, + targets: { + e2e: {}, + [overrides.ciTargetName]: {}, + }, + }, + }; +} diff --git a/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts b/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts new file mode 100644 index 0000000000000..d46a5c57955b6 --- /dev/null +++ b/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts @@ -0,0 +1,112 @@ +import { + type Tree, + type CreateNodesV2, + formatFiles, + readNxJson, + createProjectGraphAsync, + parseTargetString, +} from '@nx/devkit'; +import { addE2eCiTargetDefaults as _addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; +import { LoadedNxPlugin } from 'nx/src/project-graph/plugins/internal-api'; +import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils'; +import { + ProjectConfigurationsError, + retrieveProjectConfigurations, +} from 'nx/src/devkit-internals'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import { type PlaywrightPluginOptions } from '../../plugins/plugin'; + +export default async function addE2eCiTargetDefaults(tree: Tree) { + const pluginName = '@nx/playwright/plugin'; + const graph = await createProjectGraphAsync(); + const nxJson = readNxJson(tree); + const matchingPluginRegistrations = nxJson.plugins?.filter((p) => + typeof p === 'string' ? p === pluginName : p.plugin === pluginName + ); + + const { + createNodesV2, + }: { createNodesV2: CreateNodesV2 } = await import( + pluginName + ); + + for (const plugin of matchingPluginRegistrations) { + let projectConfigs: ConfigurationResult; + try { + const loadedPlugin = new LoadedNxPlugin( + { createNodesV2, name: pluginName }, + plugin + ); + projectConfigs = await retrieveProjectConfigurations( + [loadedPlugin], + tree.root, + nxJson + ); + } catch (e) { + if (e instanceof ProjectConfigurationsError) { + projectConfigs = e.partialProjectConfigurationsResult; + } else { + throw e; + } + } + + for (const configFile of projectConfigs.matchingProjectFiles) { + const configFileContents = tree.read(configFile, 'utf-8'); + + const ast = tsquery.ast(configFileContents); + const CI_WEBSERVER_COMMAND_SELECTOR = + 'PropertyAssignment:has(Identifier[name=webServer]) PropertyAssignment:has(Identifier[name=command]) > StringLiteral'; + const nodes = tsquery(ast, CI_WEBSERVER_COMMAND_SELECTOR, { + visitAllChildren: true, + }); + if (!nodes.length) { + continue; + } + const ciWebServerCommand = nodes[0].getText(); + let serveStaticProject: string; + let serveStaticTarget: string; + let serveStaticConfiguration: string; + if (ciWebServerCommand.includes('nx run')) { + const NX_TARGET_REGEX = "(?<=nx run )[^']+"; + const matches = ciWebServerCommand.match(NX_TARGET_REGEX); + if (!matches) { + continue; + } + const targetString = matches[0]; + const { project, target, configuration } = parseTargetString( + targetString, + graph + ); + serveStaticProject = project; + serveStaticTarget = target; + serveStaticConfiguration = configuration; + } else { + const NX_PROJECT_REGEX = 'nx\\s+([^ ]+)\\s+([^ ]+)'; + const matches = ciWebServerCommand.match(NX_PROJECT_REGEX); + if (!matches) { + return; + } + serveStaticTarget = matches[1]; + serveStaticProject = matches[2]; + } + + const resolvedServeStaticTarget = + graph.nodes[serveStaticProject].data.targets[serveStaticTarget]; + + let resolvedBuildTarget: string; + if (resolvedServeStaticTarget.dependsOn) { + resolvedBuildTarget = resolvedServeStaticTarget.dependsOn.join(','); + } else { + resolvedBuildTarget = + (serveStaticConfiguration + ? resolvedServeStaticTarget.configurations[serveStaticConfiguration] + .buildTarget + : resolvedServeStaticTarget.options.buildTarget) ?? 'build'; + } + + const buildTarget = `^${resolvedBuildTarget}`; + + await _addE2eCiTargetDefaults(tree, pluginName, buildTarget, configFile); + } + } +} diff --git a/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.ts b/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.ts index c98bfff41a031..88b58a6e6ddfc 100644 --- a/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.ts +++ b/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.ts @@ -8,6 +8,7 @@ import { visitNotIgnoredFiles, } from '@nx/devkit'; import { tsquery } from '@phenomnomnominal/tsquery'; +import addE2eCiTargetDefaults from './add-e2e-ci-target-defaults'; export default async function (tree: Tree) { const graph = await createProjectGraphAsync(); @@ -138,5 +139,6 @@ export default async function (tree: Tree) { } }); + await addE2eCiTargetDefaults(tree); await formatFiles(tree); } From 4dcc4f24be2ece34c47eed6d36149641c159e072 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Mon, 5 Aug 2024 15:00:49 +0100 Subject: [PATCH 20/22] fix(testing): playwright add e2ecidefault should handle no matching plugins --- .../migrations/update-19-6-0/add-e2e-ci-target-defaults.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts b/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts index d46a5c57955b6..d108c5a03731e 100644 --- a/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts +++ b/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts @@ -24,6 +24,10 @@ export default async function addE2eCiTargetDefaults(tree: Tree) { typeof p === 'string' ? p === pluginName : p.plugin === pluginName ); + if (!matchingPluginRegistrations) { + return; + } + const { createNodesV2, }: { createNodesV2: CreateNodesV2 } = await import( From 05b89709fffcb5af52c5bb999f5a2eeba12f8314 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Mon, 5 Aug 2024 15:01:13 +0100 Subject: [PATCH 21/22] fix(testing): cypress add e2ecidefault should handle no matching plugins --- .../migrations/update-19-6-0/add-e2e-ci-target-defaults.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts b/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts index ae01f7061be12..3749d5c43434d 100644 --- a/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts +++ b/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts @@ -24,6 +24,10 @@ export default async function addE2eCiTargetDefaults(tree: Tree) { typeof p === 'string' ? p === pluginName : p.plugin === pluginName ); + if (!matchingPluginRegistrations) { + return; + } + const { createNodesV2, }: { createNodesV2: CreateNodesV2 } = await import( From 01b753666ddca3888740357cf5c0b229708641c1 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Wed, 7 Aug 2024 16:35:20 +0100 Subject: [PATCH 22/22] chore(nextjs): use uniq for test --- .../application/application.spec.ts | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/next/src/generators/application/application.spec.ts b/packages/next/src/generators/application/application.spec.ts index 8f0a1b2380d98..fa54ed0b95d6d 100644 --- a/packages/next/src/generators/application/application.spec.ts +++ b/packages/next/src/generators/application/application.spec.ts @@ -183,7 +183,7 @@ describe('app', () => { describe('--style scss', () => { it('should generate scss styles', async () => { - const name = 'myapp'; + const name = uniq(); await applicationGenerator(tree, { name, style: 'scss', @@ -200,7 +200,7 @@ describe('app', () => { "import './global.css'; export const metadata = { - title: 'Welcome to myapp', + title: 'Welcome to ${name}', description: 'Generated by create-nx-workspace', }; @@ -222,7 +222,7 @@ describe('app', () => { describe('--style less', () => { it('should generate less styles', async () => { - const name = 'myapp2'; + const name = uniq(); await applicationGenerator(tree, { name, style: 'less', @@ -239,7 +239,7 @@ describe('app', () => { "import './global.less'; export const metadata = { - title: 'Welcome to myapp2', + title: 'Welcome to ${name}', description: 'Generated by create-nx-workspace', }; @@ -360,12 +360,9 @@ describe('app', () => { const indexContent = tree.read(`${name}/src/app/page.tsx`, 'utf-8'); expect(indexContent).not.toContain(`import styles from './page.module`); expect(indexContent).toContain(`import styled from '@emotion/styled'`); - expect(tree.read(`${name}/src/app/layout.tsx`, 'utf-8')) - .toMatchInlineSnapshot(` - "import './global.css'; - - export const metadata = { - title: 'Welcome to `); + expect( + tree.read(`${name}/src/app/layout.tsx`, 'utf-8') + ).toMatchInlineSnapshot(``); }); it('should add jsxImportSource in tsconfig.json', async () => { @@ -605,7 +602,7 @@ describe('app', () => { describe('--linter', () => { describe('default (eslint)', () => { it('should add .eslintrc.json and dependencies', async () => { - const name = 'myapp3'; + const name = uniq(); await applicationGenerator(tree, { name, @@ -645,7 +642,7 @@ describe('app', () => { "rules": { "@next/next/no-html-link-for-pages": [ "error", - "myapp3/pages", + "${name}/pages", ], }, },