Skip to content

Commit 4c8c24b

Browse files
authored
fix(nextjs): Add missing e2e-ci target for cypress (#21805)
1 parent 11c849a commit 4c8c24b

File tree

13 files changed

+206
-5
lines changed

13 files changed

+206
-5
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ CHANGELOG.md
3131

3232
# Next.js
3333
.next
34+
out
3435

3536
# Angular Cache
3637
.angular

docs/generated/packages/next/documents/overview.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ The `@nx/next/plugin` is configured in the `plugins` array in `nx.json`.
6060
"options": {
6161
"buildTargetName": "build",
6262
"devTargetName": "dev",
63-
"startTargetName": "start"
63+
"startTargetName": "start",
64+
"serveStaticTargetName": "serve-static"
6465
}
6566
}
6667
]
@@ -70,6 +71,10 @@ The `@nx/next/plugin` is configured in the `plugins` array in `nx.json`.
7071
- The `buildTargetName` option controls the name of Next.js' compilation task which compiles the application for production deployment. The default name is `build`.
7172
- The `devTargetName` option controls the name of Next.js' development serve task which starts the application in development mode. The default name is `dev`.
7273
- The `startTargetName` option controls the name of Next.js' production serve task which starts the application in production mode. The default name is `start`.
74+
- 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`.
75+
76+
{% /tab %}
77+
{% tab label="Nx < 18" %}
7378

7479
{% /tab %}
7580
{% tab label="Nx < 18" %}
@@ -246,9 +251,50 @@ const nextConfig = {
246251
nx: {
247252
svgr: false,
248253
},
254+
output: 'export',
249255
};
250256
```
251257

258+
After setting the output to `export`, you can run the `build` command to generate the static HTML files.
259+
260+
```shell
261+
nx build my-next-app
262+
```
263+
264+
You can then check your project folder for the `out` folder which contains the static HTML files.
265+
266+
```shell
267+
├── index.d.ts
268+
├── jest.config.ts
269+
├── next-env.d.ts
270+
├── next.config.js
271+
├── out
272+
├── project.json
273+
├── public
274+
├── specs
275+
├── src
276+
├── tsconfig.json
277+
└── tsconfig.spec.json
278+
```
279+
280+
#### E2E testing
281+
282+
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.
283+
284+
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.
285+
286+
To utilize the `serve-static` target for testing, run the following command:
287+
288+
```shell
289+
nx serve-static my-next-app-e2e
290+
```
291+
292+
This command performs several actions:
293+
294+
1. It will build the Next.js application and generate the static HTML files.
295+
2. It will serve the static HTML files using a simple HTTP server.
296+
3. It will run the Cypress tests against the served static HTML files.
297+
252298
### Deploying Next.js Applications
253299

254300
Once you are ready to deploy your Next.js application, you have absolute freedom to choose any hosting provider that fits your needs.

docs/shared/packages/next/plugin-overview.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ The `@nx/next/plugin` is configured in the `plugins` array in `nx.json`.
6060
"options": {
6161
"buildTargetName": "build",
6262
"devTargetName": "dev",
63-
"startTargetName": "start"
63+
"startTargetName": "start",
64+
"serveStaticTargetName": "serve-static"
6465
}
6566
}
6667
]
@@ -70,6 +71,10 @@ The `@nx/next/plugin` is configured in the `plugins` array in `nx.json`.
7071
- The `buildTargetName` option controls the name of Next.js' compilation task which compiles the application for production deployment. The default name is `build`.
7172
- The `devTargetName` option controls the name of Next.js' development serve task which starts the application in development mode. The default name is `dev`.
7273
- The `startTargetName` option controls the name of Next.js' production serve task which starts the application in production mode. The default name is `start`.
74+
- 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`.
75+
76+
{% /tab %}
77+
{% tab label="Nx < 18" %}
7378

7479
{% /tab %}
7580
{% tab label="Nx < 18" %}
@@ -246,9 +251,50 @@ const nextConfig = {
246251
nx: {
247252
svgr: false,
248253
},
254+
output: 'export',
249255
};
250256
```
251257

258+
After setting the output to `export`, you can run the `build` command to generate the static HTML files.
259+
260+
```shell
261+
nx build my-next-app
262+
```
263+
264+
You can then check your project folder for the `out` folder which contains the static HTML files.
265+
266+
```shell
267+
├── index.d.ts
268+
├── jest.config.ts
269+
├── next-env.d.ts
270+
├── next.config.js
271+
├── out
272+
├── project.json
273+
├── public
274+
├── specs
275+
├── src
276+
├── tsconfig.json
277+
└── tsconfig.spec.json
278+
```
279+
280+
#### E2E testing
281+
282+
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.
283+
284+
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.
285+
286+
To utilize the `serve-static` target for testing, run the following command:
287+
288+
```shell
289+
nx serve-static my-next-app-e2e
290+
```
291+
292+
This command performs several actions:
293+
294+
1. It will build the Next.js application and generate the static HTML files.
295+
2. It will serve the static HTML files using a simple HTTP server.
296+
3. It will run the Cypress tests against the served static HTML files.
297+
252298
### Deploying Next.js Applications
253299

254300
Once you are ready to deploy your Next.js application, you have absolute freedom to choose any hosting provider that fits your needs.

e2e/next-core/src/next.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
newProject,
66
readFile,
77
runCLI,
8+
runE2ETests,
89
uniq,
910
updateFile,
1011
} from '@nx/e2e/utils';
@@ -183,6 +184,44 @@ describe('Next.js Applications', () => {
183184
`Successfully ran target build for project ${appName}`
184185
);
185186
}, 300_000);
187+
188+
it('should run e2e-ci test', async () => {
189+
const appName = uniq('app');
190+
191+
runCLI(
192+
`generate @nx/next:app ${appName} --no-interactive --style=css --project-name-and-root-format=as-provided`
193+
);
194+
195+
// Update the cypress timeout to 25 seconds since we need to build and wait for the server to start
196+
updateFile(`${appName}-e2e/cypress.config.ts`, (_) => {
197+
return `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
198+
199+
import { defineConfig } from 'cypress';
200+
201+
export default defineConfig({
202+
e2e: {
203+
...nxE2EPreset(__filename, {
204+
cypressDir: 'src',
205+
webServerCommands: { default: 'nx run ${appName}:start' },
206+
webServerConfig: { timeout: 25_000 },
207+
ciWebServerCommand: 'nx run ${appName}:serve-static',
208+
}),
209+
baseUrl: 'http://localhost:3000',
210+
},
211+
});
212+
213+
`;
214+
});
215+
216+
if (runE2ETests()) {
217+
const e2eResults = runCLI(`e2e-ci ${appName}-e2e --verbose`, {
218+
verbose: true,
219+
});
220+
expect(e2eResults).toContain(
221+
'Successfully ran target e2e-ci for project'
222+
);
223+
}
224+
}, 600_000);
186225
});
187226

188227
function getData(port, path = ''): Promise<any> {

packages/next/plugins/with-nx.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,12 @@ function withNx(
197197
: joinPathFragments(outputDir, '.next');
198198
}
199199

200+
// 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'.
201+
if (process.env.NX_SERVE_STATIC_BUILD_RUNNING === 'true') {
202+
nextConfig.output = 'export';
203+
nextConfig.distDir = 'out';
204+
}
205+
200206
const userWebpackConfig = nextConfig.webpack;
201207

202208
const { createWebpackConfig } = require('@nx/next/src/utils/config');

packages/next/src/generators/application/lib/add-e2e.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Linter } from '@nx/eslint';
1010

1111
import { nxVersion } from '../../../utils/versions';
1212
import { NormalizedSchema } from './normalize-options';
13+
import { webStaticServeGenerator } from '@nx/web';
1314

1415
export async function addE2e(host: Tree, options: NormalizedSchema) {
1516
const nxJson = readNxJson(host);
@@ -18,17 +19,28 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
1819
? p === '@nx/next/plugin'
1920
: p.plugin === '@nx/next/plugin'
2021
);
22+
2123
if (options.e2eTestRunner === 'cypress') {
2224
const { configurationGenerator } = ensurePackage<
2325
typeof import('@nx/cypress')
2426
>('@nx/cypress', nxVersion);
27+
28+
if (!hasPlugin) {
29+
webStaticServeGenerator(host, {
30+
buildTarget: `${options.projectName}:build`,
31+
outputPath: `${options.outputPath}/out`,
32+
targetName: 'serve-static',
33+
});
34+
}
35+
2536
addProjectConfiguration(host, options.e2eProjectName, {
2637
root: options.e2eProjectRoot,
2738
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
2839
targets: {},
2940
tags: [],
3041
implicitDependencies: [options.projectName],
3142
});
43+
3244
return configurationGenerator(host, {
3345
...options,
3446
linter: Linter.EsLint,
@@ -40,6 +52,14 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
4052
}`,
4153
baseUrl: `http://localhost:${hasPlugin ? '3000' : '4200'}`,
4254
jsx: true,
55+
webServerCommands: hasPlugin
56+
? {
57+
default: `nx run ${options.projectName}:start`,
58+
}
59+
: undefined,
60+
ciWebServerCommand: hasPlugin
61+
? `nx run ${options.projectName}:serve-static`
62+
: undefined,
4363
});
4464
} else if (options.e2eTestRunner === 'playwright') {
4565
const { configurationGenerator } = ensurePackage<

packages/next/src/generators/custom-server/custom-server.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('app', () => {
2828
});
2929

3030
it('should create a custom server with swc', async () => {
31-
const name = uniq('custom-server');
31+
const name = uniq('custom-server-swc');
3232

3333
await applicationGenerator(tree, {
3434
name,

packages/next/src/generators/init/lib/add-plugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export function addPlugin(tree: Tree) {
2020
buildTargetName: 'build',
2121
devTargetName: 'dev',
2222
startTargetName: 'start',
23+
serveStaticTargetName: 'serve-static',
2324
},
2425
});
2526

packages/next/src/plugins/__snapshots__/plugin.spec.ts.snap

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ exports[`@nx/next/plugin integrated projects should create nodes 1`] = `
3535
"cwd": "my-app",
3636
},
3737
},
38+
"my-serve-static": {
39+
"executor": "@nx/web:file-server",
40+
"options": {
41+
"buildTarget": "my-build",
42+
"port": 3000,
43+
"staticFilePath": "{projectRoot}/out",
44+
},
45+
},
3846
"my-start": {
3947
"command": "next start",
4048
"dependsOn": [
@@ -85,6 +93,14 @@ exports[`@nx/next/plugin root projects should create nodes 1`] = `
8593
"cwd": ".",
8694
},
8795
},
96+
"serve-static": {
97+
"executor": "@nx/web:file-server",
98+
"options": {
99+
"buildTarget": "build",
100+
"port": 3000,
101+
"staticFilePath": "{projectRoot}/out",
102+
},
103+
},
88104
"start": {
89105
"command": "next start",
90106
"dependsOn": [

packages/next/src/plugins/plugin.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ describe('@nx/next/plugin', () => {
3434
buildTargetName: 'build',
3535
devTargetName: 'dev',
3636
startTargetName: 'start',
37+
serveStaticTargetName: 'serve-static',
3738
},
3839
context
3940
);
@@ -73,6 +74,7 @@ describe('@nx/next/plugin', () => {
7374
buildTargetName: 'my-build',
7475
devTargetName: 'my-serve',
7576
startTargetName: 'my-start',
77+
serveStaticTargetName: 'my-serve-static',
7678
},
7779
context
7880
);

packages/next/src/plugins/plugin.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface NextPluginOptions {
2121
buildTargetName?: string;
2222
devTargetName?: string;
2323
startTargetName?: string;
24+
serveStaticTargetName?: string;
2425
}
2526

2627
const cachePath = join(projectGraphCacheDirectory, 'next.hash');
@@ -62,7 +63,6 @@ export const createNodes: CreateNodes<NextPluginOptions> = [
6263
) {
6364
return {};
6465
}
65-
6666
options = normalizeOptions(options);
6767

6868
const hash = calculateHashForCreateNodes(projectRoot, options, context, [
@@ -106,6 +106,9 @@ async function buildNextTargets(
106106
targets[options.devTargetName] = getDevTargetConfig(projectRoot);
107107

108108
targets[options.startTargetName] = getStartTargetConfig(options, projectRoot);
109+
110+
targets[options.serveStaticTargetName] = getStaticServeTargetConfig(options);
111+
109112
return targets;
110113
}
111114

@@ -152,6 +155,19 @@ function getStartTargetConfig(options: NextPluginOptions, projectRoot: string) {
152155
return targetConfig;
153156
}
154157

158+
function getStaticServeTargetConfig(options: NextPluginOptions) {
159+
const targetConfig: TargetConfiguration = {
160+
executor: '@nx/web:file-server',
161+
options: {
162+
buildTarget: options.buildTargetName,
163+
staticFilePath: '{projectRoot}/out',
164+
port: 3000,
165+
},
166+
};
167+
168+
return targetConfig;
169+
}
170+
155171
async function getOutputs(projectRoot, nextConfig) {
156172
let dir = '.next';
157173
const { PHASE_PRODUCTION_BUILD } = require('next/constants');
@@ -196,6 +212,7 @@ function normalizeOptions(options: NextPluginOptions): NextPluginOptions {
196212
options.buildTargetName ??= 'build';
197213
options.devTargetName ??= 'dev';
198214
options.startTargetName ??= 'start';
215+
options.serveStaticTargetName ??= 'serve-static';
199216
return options;
200217
}
201218

packages/next/src/utils/add-gitignore-entry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function addGitIgnoreEntry(host: Tree) {
1212
ig.add(host.read('.gitignore', 'utf-8'));
1313

1414
if (!ig.ignores('apps/example/.next')) {
15-
content = `${content}\n\n# Next.js\n.next\n`;
15+
content = `${content}\n\n# Next.js\n.next\nout\n`;
1616
}
1717

1818
host.write('.gitignore', content);

0 commit comments

Comments
 (0)