From 4302fb6bd6679c622db0da8df5afd8019df671c8 Mon Sep 17 00:00:00 2001 From: Nicholas Cunningham Date: Tue, 13 Feb 2024 15:20:07 -0700 Subject: [PATCH] fix(nextjs): Add missing e2e-ci target for cypress --- .gitignore | 1 + e2e/next-core/src/next.test.ts | 26 +++++++++++++++++++ packages/next/plugins/with-nx.ts | 6 +++++ .../src/generators/application/lib/add-e2e.ts | 20 ++++++++++++++ .../generators/application/lib/add-project.ts | 8 ++++++ .../src/generators/init/lib/add-plugin.ts | 1 + .../plugins/__snapshots__/plugin.spec.ts.snap | 14 ++++++++++ packages/next/src/plugins/plugin.spec.ts | 2 ++ packages/next/src/plugins/plugin.ts | 18 ++++++++++++- .../next/src/utils/add-gitignore-entry.ts | 2 +- .../executors/file-server/file-server.impl.ts | 7 +++++ 11 files changed, 103 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 5f42518c9978f6..cb9667809701e6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ CHANGELOG.md # Next.js .next +out # Angular Cache .angular diff --git a/e2e/next-core/src/next.test.ts b/e2e/next-core/src/next.test.ts index eb933a6808d0d3..ec7c4b2237383e 100644 --- a/e2e/next-core/src/next.test.ts +++ b/e2e/next-core/src/next.test.ts @@ -2,9 +2,11 @@ import { checkFilesDoNotExist, checkFilesExist, cleanupProject, + killPorts, newProject, readFile, runCLI, + runE2ETests, uniq, updateFile, } from '@nx/e2e/utils'; @@ -183,6 +185,30 @@ describe('Next.js Applications', () => { `Successfully ran target build for project ${appName}` ); }, 300_000); + + it('should run e2e-ci test', async () => { + const appName = uniq('app'); + + // Build first so it uses cache for e2e + runCLI( + `generate @nx/next:app ${appName} --no-interactive --style=css --project-name-and-root-format=as-provided` + ); + + if (runE2ETests()) { + const buildResult = runCLI(`build ${appName}`); + + expect(buildResult).toContain( + `Successfully ran target build for project ${appName}` + ); + + const e2eResults = runCLI(`e2e-ci ${appName}-e2e --verbose`, { + verbose: true, + }); + expect(e2eResults).toContain( + 'Successfully ran target e2e-ci for project' + ); + } + }, 1_000_000); }); function getData(port, path = ''): Promise { diff --git a/packages/next/plugins/with-nx.ts b/packages/next/plugins/with-nx.ts index 87d29498587722..555c57be61d02e 100644 --- a/packages/next/plugins/with-nx.ts +++ b/packages/next/plugins/with-nx.ts @@ -197,6 +197,12 @@ function withNx( : joinPathFragments(outputDir, '.next'); } + // If we are running a static serve of the Next.js app, we need to change the output to 'export' and the distDir to 'out'. + if (process.env.NX_SERVE_STATIC_BUILD_RUNNING === 'true') { + nextConfig.output = 'export'; + nextConfig.distDir = 'out'; + } + const userWebpackConfig = nextConfig.webpack; const { createWebpackConfig } = require('@nx/next/src/utils/config'); diff --git a/packages/next/src/generators/application/lib/add-e2e.ts b/packages/next/src/generators/application/lib/add-e2e.ts index 99e8891101ccfc..95c7f74fefcde3 100644 --- a/packages/next/src/generators/application/lib/add-e2e.ts +++ b/packages/next/src/generators/application/lib/add-e2e.ts @@ -10,6 +10,7 @@ import { Linter } from '@nx/eslint'; import { nxVersion } from '../../../utils/versions'; import { NormalizedSchema } from './normalize-options'; +import { webStaticServeGenerator } from '@nx/web'; export async function addE2e(host: Tree, options: NormalizedSchema) { const nxJson = readNxJson(host); @@ -18,10 +19,20 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { ? p === '@nx/next/plugin' : p.plugin === '@nx/next/plugin' ); + if (options.e2eTestRunner === 'cypress') { const { configurationGenerator } = ensurePackage< typeof import('@nx/cypress') >('@nx/cypress', nxVersion); + + if (!hasPlugin) { + webStaticServeGenerator(host, { + buildTarget: `${options.projectName}:build`, + outputPath: `${options.outputPath}/out`, + targetName: 'serve-static', + }); + } + addProjectConfiguration(host, options.e2eProjectName, { root: options.e2eProjectRoot, sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), @@ -29,6 +40,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { tags: [], implicitDependencies: [options.projectName], }); + return configurationGenerator(host, { ...options, linter: Linter.EsLint, @@ -40,6 +52,14 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { }`, baseUrl: `http://localhost:${hasPlugin ? '3000' : '4200'}`, jsx: true, + webServerCommands: hasPlugin + ? { + default: `nx run ${options.projectName}:start`, + } + : undefined, + ciWebServerCommand: hasPlugin + ? `nx run ${options.projectName}:serve-static` + : undefined, }); } else if (options.e2eTestRunner === 'playwright') { const { configurationGenerator } = ensurePackage< diff --git a/packages/next/src/generators/application/lib/add-project.ts b/packages/next/src/generators/application/lib/add-project.ts index 84ad72664cb2f5..847033faaa14d0 100644 --- a/packages/next/src/generators/application/lib/add-project.ts +++ b/packages/next/src/generators/application/lib/add-project.ts @@ -63,6 +63,14 @@ export function addProject(host: Tree, options: NormalizedSchema) { buildTarget: `${options.projectName}:build:production`, }, }; + } else { + // We need to add the serve-static target so that the port can be configured. + targets['serve-static'] = { + executor: '@nx/web:file-server', + options: { + port: 3000, + }, + }; } const project: ProjectConfiguration = { diff --git a/packages/next/src/generators/init/lib/add-plugin.ts b/packages/next/src/generators/init/lib/add-plugin.ts index 2835c14968f21a..216d065ab4a91b 100644 --- a/packages/next/src/generators/init/lib/add-plugin.ts +++ b/packages/next/src/generators/init/lib/add-plugin.ts @@ -20,6 +20,7 @@ export function addPlugin(tree: Tree) { buildTargetName: 'build', devTargetName: 'dev', startTargetName: 'start', + serveStaticTargetName: 'serve-static', }, }); diff --git a/packages/next/src/plugins/__snapshots__/plugin.spec.ts.snap b/packages/next/src/plugins/__snapshots__/plugin.spec.ts.snap index 64fa1296359fc8..e9faef0e5fe968 100644 --- a/packages/next/src/plugins/__snapshots__/plugin.spec.ts.snap +++ b/packages/next/src/plugins/__snapshots__/plugin.spec.ts.snap @@ -35,6 +35,13 @@ exports[`@nx/next/plugin integrated projects should create nodes 1`] = ` "cwd": "my-app", }, }, + "my-serve-static": { + "executor": "@nx/web:file-server", + "options": { + "buildTarget": "my-build", + "staticFilePath": "{projectRoot}/out", + }, + }, "my-start": { "command": "next start", "dependsOn": [ @@ -85,6 +92,13 @@ exports[`@nx/next/plugin root projects should create nodes 1`] = ` "cwd": ".", }, }, + "serve-static": { + "executor": "@nx/web:file-server", + "options": { + "buildTarget": "build", + "staticFilePath": "{projectRoot}/out", + }, + }, "start": { "command": "next start", "dependsOn": [ diff --git a/packages/next/src/plugins/plugin.spec.ts b/packages/next/src/plugins/plugin.spec.ts index b0006368c5d203..0b1f5e6e662aa1 100644 --- a/packages/next/src/plugins/plugin.spec.ts +++ b/packages/next/src/plugins/plugin.spec.ts @@ -34,6 +34,7 @@ describe('@nx/next/plugin', () => { buildTargetName: 'build', devTargetName: 'dev', startTargetName: 'start', + serveStaticTargetName: 'serve-static', }, context ); @@ -73,6 +74,7 @@ describe('@nx/next/plugin', () => { buildTargetName: 'my-build', devTargetName: 'my-serve', startTargetName: 'my-start', + serveStaticTargetName: 'my-serve-static', }, context ); diff --git a/packages/next/src/plugins/plugin.ts b/packages/next/src/plugins/plugin.ts index dcc2d5c1e140cd..18416f77ba329b 100644 --- a/packages/next/src/plugins/plugin.ts +++ b/packages/next/src/plugins/plugin.ts @@ -21,6 +21,7 @@ export interface NextPluginOptions { buildTargetName?: string; devTargetName?: string; startTargetName?: string; + serveStaticTargetName?: string; } const cachePath = join(projectGraphCacheDirectory, 'next.hash'); @@ -62,7 +63,6 @@ export const createNodes: CreateNodes = [ ) { return {}; } - options = normalizeOptions(options); const hash = calculateHashForCreateNodes(projectRoot, options, context, [ @@ -106,6 +106,9 @@ async function buildNextTargets( targets[options.devTargetName] = getDevTargetConfig(projectRoot); targets[options.startTargetName] = getStartTargetConfig(options, projectRoot); + + targets[options.serveStaticTargetName] = getStaticServeTargetConfig(options); + return targets; } @@ -152,6 +155,18 @@ function getStartTargetConfig(options: NextPluginOptions, projectRoot: string) { return targetConfig; } +function getStaticServeTargetConfig(options: NextPluginOptions) { + const targetConfig: TargetConfiguration = { + executor: '@nx/web:file-server', + options: { + buildTarget: options.buildTargetName, + staticFilePath: '{projectRoot}/out', + }, + }; + + return targetConfig; +} + async function getOutputs(projectRoot, nextConfig) { let dir = '.next'; const { PHASE_PRODUCTION_BUILD } = require('next/constants'); @@ -196,6 +211,7 @@ function normalizeOptions(options: NextPluginOptions): NextPluginOptions { options.buildTargetName ??= 'build'; options.devTargetName ??= 'dev'; options.startTargetName ??= 'start'; + options.serveStaticTargetName ??= 'serve-static'; return options; } diff --git a/packages/next/src/utils/add-gitignore-entry.ts b/packages/next/src/utils/add-gitignore-entry.ts index 561d6201d2b5d6..6f66cfdc0552a5 100644 --- a/packages/next/src/utils/add-gitignore-entry.ts +++ b/packages/next/src/utils/add-gitignore-entry.ts @@ -12,7 +12,7 @@ export function addGitIgnoreEntry(host: Tree) { ig.add(host.read('.gitignore', 'utf-8')); if (!ig.ignores('apps/example/.next')) { - content = `${content}\n\n# Next.js\n.next\n`; + content = `${content}\n\n# Next.js\n.next\nout\n`; } host.write('.gitignore', content); diff --git a/packages/web/src/executors/file-server/file-server.impl.ts b/packages/web/src/executors/file-server/file-server.impl.ts index 12a432338eb44a..2b042bee81821b 100644 --- a/packages/web/src/executors/file-server/file-server.impl.ts +++ b/packages/web/src/executors/file-server/file-server.impl.ts @@ -149,6 +149,12 @@ export default async function* fileServerExecutor( const run = () => { if (!running) { running = true; + /** + * Expose a variable to the build target to know if it's being run by the serve-static executor + * This is useful because a config might need to change if it's being run by serve-static without the user's input + * or if being ran by another executor (eg. E2E tests) + * */ + process.env.NX_SERVE_STATIC_BUILD_RUNNING = 'true'; try { const args = getBuildTargetCommand(options, context); execFileSync(pmCmd, args, { @@ -159,6 +165,7 @@ export default async function* fileServerExecutor( `Build target failed: ${chalk.bold(options.buildTarget)}` ); } finally { + process.env.NX_SERVE_STATIC_BUILD_RUNNING = undefined; running = false; } }