fix(nextjs): Add missing e2e-ci target for cypress (#21805)

This commit is contained in:
Nicholas Cunningham 2024-02-16 14:36:01 -07:00 committed by GitHub
parent 11c849afab
commit 4c8c24b97a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 206 additions and 5 deletions

1
.gitignore vendored
View File

@ -31,6 +31,7 @@ CHANGELOG.md
# Next.js # Next.js
.next .next
out
# Angular Cache # Angular Cache
.angular .angular

View File

@ -60,7 +60,8 @@ The `@nx/next/plugin` is configured in the `plugins` array in `nx.json`.
"options": { "options": {
"buildTargetName": "build", "buildTargetName": "build",
"devTargetName": "dev", "devTargetName": "dev",
"startTargetName": "start" "startTargetName": "start",
"serveStaticTargetName": "serve-static"
} }
} }
] ]
@ -70,6 +71,10 @@ The `@nx/next/plugin` is configured in the `plugins` array in `nx.json`.
- The `buildTargetName` option controls the name of Next.js' compilation task which compiles the application for production deployment. The default name is `build`. - The `buildTargetName` option controls the name of Next.js' compilation task which compiles the application for production deployment. The default name is `build`.
- The `devTargetName` option controls the name of Next.js' development serve task which starts the application in development mode. The default name is `dev`. - The `devTargetName` option controls the name of Next.js' development serve task which starts the application in development mode. The default name is `dev`.
- The `startTargetName` option controls the name of Next.js' production serve task which starts the application in production mode. The default name is `start`. - The `startTargetName` option controls the name of Next.js' production serve task which starts the application in production mode. The default name is `start`.
- The `serveStaticTargetName` option controls the name of Next.js' static export task which exports the application to static HTML files. The default name is `serve-static`.
{% /tab %}
{% tab label="Nx < 18" %}
{% /tab %} {% /tab %}
{% tab label="Nx < 18" %} {% tab label="Nx < 18" %}
@ -246,9 +251,50 @@ const nextConfig = {
nx: { nx: {
svgr: false, svgr: false,
}, },
output: 'export',
}; };
``` ```
After setting the output to `export`, you can run the `build` command to generate the static HTML files.
```shell
nx build my-next-app
```
You can then check your project folder for the `out` folder which contains the static HTML files.
```shell
├── index.d.ts
├── jest.config.ts
├── next-env.d.ts
├── next.config.js
├── out
├── project.json
├── public
├── specs
├── src
├── tsconfig.json
└── tsconfig.spec.json
```
#### E2E testing
You can perform end-to-end (E2E) testing on static HTML files using a test runner like Cypress. When you create a Next.js application, Nx automatically creates a `serve-static` target. This target is designed to serve the static HTML files produced by the build command.
This feature is particularly useful for testing in continuous integration (CI) pipelines, where resources may be constrained. Unlike the `dev` and `start` targets, `serve-static` does not require a Next.js server to operate, making it more efficient and faster by eliminating background processes, such as file change monitoring.
To utilize the `serve-static` target for testing, run the following command:
```shell
nx serve-static my-next-app-e2e
```
This command performs several actions:
1. It will build the Next.js application and generate the static HTML files.
2. It will serve the static HTML files using a simple HTTP server.
3. It will run the Cypress tests against the served static HTML files.
### Deploying Next.js Applications ### Deploying Next.js Applications
Once you are ready to deploy your Next.js application, you have absolute freedom to choose any hosting provider that fits your needs. Once you are ready to deploy your Next.js application, you have absolute freedom to choose any hosting provider that fits your needs.

View File

@ -60,7 +60,8 @@ The `@nx/next/plugin` is configured in the `plugins` array in `nx.json`.
"options": { "options": {
"buildTargetName": "build", "buildTargetName": "build",
"devTargetName": "dev", "devTargetName": "dev",
"startTargetName": "start" "startTargetName": "start",
"serveStaticTargetName": "serve-static"
} }
} }
] ]
@ -70,6 +71,10 @@ The `@nx/next/plugin` is configured in the `plugins` array in `nx.json`.
- The `buildTargetName` option controls the name of Next.js' compilation task which compiles the application for production deployment. The default name is `build`. - The `buildTargetName` option controls the name of Next.js' compilation task which compiles the application for production deployment. The default name is `build`.
- The `devTargetName` option controls the name of Next.js' development serve task which starts the application in development mode. The default name is `dev`. - The `devTargetName` option controls the name of Next.js' development serve task which starts the application in development mode. The default name is `dev`.
- The `startTargetName` option controls the name of Next.js' production serve task which starts the application in production mode. The default name is `start`. - The `startTargetName` option controls the name of Next.js' production serve task which starts the application in production mode. The default name is `start`.
- The `serveStaticTargetName` option controls the name of Next.js' static export task which exports the application to static HTML files. The default name is `serve-static`.
{% /tab %}
{% tab label="Nx < 18" %}
{% /tab %} {% /tab %}
{% tab label="Nx < 18" %} {% tab label="Nx < 18" %}
@ -246,9 +251,50 @@ const nextConfig = {
nx: { nx: {
svgr: false, svgr: false,
}, },
output: 'export',
}; };
``` ```
After setting the output to `export`, you can run the `build` command to generate the static HTML files.
```shell
nx build my-next-app
```
You can then check your project folder for the `out` folder which contains the static HTML files.
```shell
├── index.d.ts
├── jest.config.ts
├── next-env.d.ts
├── next.config.js
├── out
├── project.json
├── public
├── specs
├── src
├── tsconfig.json
└── tsconfig.spec.json
```
#### E2E testing
You can perform end-to-end (E2E) testing on static HTML files using a test runner like Cypress. When you create a Next.js application, Nx automatically creates a `serve-static` target. This target is designed to serve the static HTML files produced by the build command.
This feature is particularly useful for testing in continuous integration (CI) pipelines, where resources may be constrained. Unlike the `dev` and `start` targets, `serve-static` does not require a Next.js server to operate, making it more efficient and faster by eliminating background processes, such as file change monitoring.
To utilize the `serve-static` target for testing, run the following command:
```shell
nx serve-static my-next-app-e2e
```
This command performs several actions:
1. It will build the Next.js application and generate the static HTML files.
2. It will serve the static HTML files using a simple HTTP server.
3. It will run the Cypress tests against the served static HTML files.
### Deploying Next.js Applications ### Deploying Next.js Applications
Once you are ready to deploy your Next.js application, you have absolute freedom to choose any hosting provider that fits your needs. Once you are ready to deploy your Next.js application, you have absolute freedom to choose any hosting provider that fits your needs.

View File

@ -5,6 +5,7 @@ import {
newProject, newProject,
readFile, readFile,
runCLI, runCLI,
runE2ETests,
uniq, uniq,
updateFile, updateFile,
} from '@nx/e2e/utils'; } from '@nx/e2e/utils';
@ -183,6 +184,44 @@ describe('Next.js Applications', () => {
`Successfully ran target build for project ${appName}` `Successfully ran target build for project ${appName}`
); );
}, 300_000); }, 300_000);
it('should run e2e-ci test', async () => {
const appName = uniq('app');
runCLI(
`generate @nx/next:app ${appName} --no-interactive --style=css --project-name-and-root-format=as-provided`
);
// Update the cypress timeout to 25 seconds since we need to build and wait for the server to start
updateFile(`${appName}-e2e/cypress.config.ts`, (_) => {
return `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename, {
cypressDir: 'src',
webServerCommands: { default: 'nx run ${appName}:start' },
webServerConfig: { timeout: 25_000 },
ciWebServerCommand: 'nx run ${appName}:serve-static',
}),
baseUrl: 'http://localhost:3000',
},
});
`;
});
if (runE2ETests()) {
const e2eResults = runCLI(`e2e-ci ${appName}-e2e --verbose`, {
verbose: true,
});
expect(e2eResults).toContain(
'Successfully ran target e2e-ci for project'
);
}
}, 600_000);
}); });
function getData(port, path = ''): Promise<any> { function getData(port, path = ''): Promise<any> {

View File

@ -197,6 +197,12 @@ function withNx(
: joinPathFragments(outputDir, '.next'); : 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 userWebpackConfig = nextConfig.webpack;
const { createWebpackConfig } = require('@nx/next/src/utils/config'); const { createWebpackConfig } = require('@nx/next/src/utils/config');

View File

@ -10,6 +10,7 @@ import { Linter } from '@nx/eslint';
import { nxVersion } from '../../../utils/versions'; import { nxVersion } from '../../../utils/versions';
import { NormalizedSchema } from './normalize-options'; import { NormalizedSchema } from './normalize-options';
import { webStaticServeGenerator } from '@nx/web';
export async function addE2e(host: Tree, options: NormalizedSchema) { export async function addE2e(host: Tree, options: NormalizedSchema) {
const nxJson = readNxJson(host); const nxJson = readNxJson(host);
@ -18,10 +19,20 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
? p === '@nx/next/plugin' ? p === '@nx/next/plugin'
: p.plugin === '@nx/next/plugin' : p.plugin === '@nx/next/plugin'
); );
if (options.e2eTestRunner === 'cypress') { if (options.e2eTestRunner === 'cypress') {
const { configurationGenerator } = ensurePackage< const { configurationGenerator } = ensurePackage<
typeof import('@nx/cypress') typeof import('@nx/cypress')
>('@nx/cypress', nxVersion); >('@nx/cypress', nxVersion);
if (!hasPlugin) {
webStaticServeGenerator(host, {
buildTarget: `${options.projectName}:build`,
outputPath: `${options.outputPath}/out`,
targetName: 'serve-static',
});
}
addProjectConfiguration(host, options.e2eProjectName, { addProjectConfiguration(host, options.e2eProjectName, {
root: options.e2eProjectRoot, root: options.e2eProjectRoot,
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
@ -29,6 +40,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
tags: [], tags: [],
implicitDependencies: [options.projectName], implicitDependencies: [options.projectName],
}); });
return configurationGenerator(host, { return configurationGenerator(host, {
...options, ...options,
linter: Linter.EsLint, linter: Linter.EsLint,
@ -40,6 +52,14 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
}`, }`,
baseUrl: `http://localhost:${hasPlugin ? '3000' : '4200'}`, baseUrl: `http://localhost:${hasPlugin ? '3000' : '4200'}`,
jsx: true, 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') { } else if (options.e2eTestRunner === 'playwright') {
const { configurationGenerator } = ensurePackage< const { configurationGenerator } = ensurePackage<

View File

@ -28,7 +28,7 @@ describe('app', () => {
}); });
it('should create a custom server with swc', async () => { it('should create a custom server with swc', async () => {
const name = uniq('custom-server'); const name = uniq('custom-server-swc');
await applicationGenerator(tree, { await applicationGenerator(tree, {
name, name,

View File

@ -20,6 +20,7 @@ export function addPlugin(tree: Tree) {
buildTargetName: 'build', buildTargetName: 'build',
devTargetName: 'dev', devTargetName: 'dev',
startTargetName: 'start', startTargetName: 'start',
serveStaticTargetName: 'serve-static',
}, },
}); });

View File

@ -35,6 +35,14 @@ exports[`@nx/next/plugin integrated projects should create nodes 1`] = `
"cwd": "my-app", "cwd": "my-app",
}, },
}, },
"my-serve-static": {
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "my-build",
"port": 3000,
"staticFilePath": "{projectRoot}/out",
},
},
"my-start": { "my-start": {
"command": "next start", "command": "next start",
"dependsOn": [ "dependsOn": [
@ -85,6 +93,14 @@ exports[`@nx/next/plugin root projects should create nodes 1`] = `
"cwd": ".", "cwd": ".",
}, },
}, },
"serve-static": {
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "build",
"port": 3000,
"staticFilePath": "{projectRoot}/out",
},
},
"start": { "start": {
"command": "next start", "command": "next start",
"dependsOn": [ "dependsOn": [

View File

@ -34,6 +34,7 @@ describe('@nx/next/plugin', () => {
buildTargetName: 'build', buildTargetName: 'build',
devTargetName: 'dev', devTargetName: 'dev',
startTargetName: 'start', startTargetName: 'start',
serveStaticTargetName: 'serve-static',
}, },
context context
); );
@ -73,6 +74,7 @@ describe('@nx/next/plugin', () => {
buildTargetName: 'my-build', buildTargetName: 'my-build',
devTargetName: 'my-serve', devTargetName: 'my-serve',
startTargetName: 'my-start', startTargetName: 'my-start',
serveStaticTargetName: 'my-serve-static',
}, },
context context
); );

View File

@ -21,6 +21,7 @@ export interface NextPluginOptions {
buildTargetName?: string; buildTargetName?: string;
devTargetName?: string; devTargetName?: string;
startTargetName?: string; startTargetName?: string;
serveStaticTargetName?: string;
} }
const cachePath = join(projectGraphCacheDirectory, 'next.hash'); const cachePath = join(projectGraphCacheDirectory, 'next.hash');
@ -62,7 +63,6 @@ export const createNodes: CreateNodes<NextPluginOptions> = [
) { ) {
return {}; return {};
} }
options = normalizeOptions(options); options = normalizeOptions(options);
const hash = calculateHashForCreateNodes(projectRoot, options, context, [ const hash = calculateHashForCreateNodes(projectRoot, options, context, [
@ -106,6 +106,9 @@ async function buildNextTargets(
targets[options.devTargetName] = getDevTargetConfig(projectRoot); targets[options.devTargetName] = getDevTargetConfig(projectRoot);
targets[options.startTargetName] = getStartTargetConfig(options, projectRoot); targets[options.startTargetName] = getStartTargetConfig(options, projectRoot);
targets[options.serveStaticTargetName] = getStaticServeTargetConfig(options);
return targets; return targets;
} }
@ -152,6 +155,19 @@ function getStartTargetConfig(options: NextPluginOptions, projectRoot: string) {
return targetConfig; return targetConfig;
} }
function getStaticServeTargetConfig(options: NextPluginOptions) {
const targetConfig: TargetConfiguration = {
executor: '@nx/web:file-server',
options: {
buildTarget: options.buildTargetName,
staticFilePath: '{projectRoot}/out',
port: 3000,
},
};
return targetConfig;
}
async function getOutputs(projectRoot, nextConfig) { async function getOutputs(projectRoot, nextConfig) {
let dir = '.next'; let dir = '.next';
const { PHASE_PRODUCTION_BUILD } = require('next/constants'); const { PHASE_PRODUCTION_BUILD } = require('next/constants');
@ -196,6 +212,7 @@ function normalizeOptions(options: NextPluginOptions): NextPluginOptions {
options.buildTargetName ??= 'build'; options.buildTargetName ??= 'build';
options.devTargetName ??= 'dev'; options.devTargetName ??= 'dev';
options.startTargetName ??= 'start'; options.startTargetName ??= 'start';
options.serveStaticTargetName ??= 'serve-static';
return options; return options;
} }

View File

@ -12,7 +12,7 @@ export function addGitIgnoreEntry(host: Tree) {
ig.add(host.read('.gitignore', 'utf-8')); ig.add(host.read('.gitignore', 'utf-8'));
if (!ig.ignores('apps/example/.next')) { 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); host.write('.gitignore', content);

View File

@ -149,6 +149,12 @@ export default async function* fileServerExecutor(
const run = () => { const run = () => {
if (!running) { if (!running) {
running = true; 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 { try {
const args = getBuildTargetCommand(options, context); const args = getBuildTargetCommand(options, context);
execFileSync(pmCmd, args, { execFileSync(pmCmd, args, {
@ -159,6 +165,7 @@ export default async function* fileServerExecutor(
`Build target failed: ${chalk.bold(options.buildTarget)}` `Build target failed: ${chalk.bold(options.buildTarget)}`
); );
} finally { } finally {
process.env.NX_SERVE_STATIC_BUILD_RUNNING = undefined;
running = false; running = false;
} }
} }