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
out
# Angular Cache
.angular

View File

@ -60,7 +60,8 @@ The `@nx/next/plugin` is configured in the `plugins` array in `nx.json`.
"options": {
"buildTargetName": "build",
"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 `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 `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 label="Nx < 18" %}
@ -246,9 +251,50 @@ const nextConfig = {
nx: {
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
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": {
"buildTargetName": "build",
"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 `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 `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 label="Nx < 18" %}
@ -246,9 +251,50 @@ const nextConfig = {
nx: {
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
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,
readFile,
runCLI,
runE2ETests,
uniq,
updateFile,
} from '@nx/e2e/utils';
@ -183,6 +184,44 @@ describe('Next.js Applications', () => {
`Successfully ran target build for project ${appName}`
);
}, 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> {

View File

@ -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');

View File

@ -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<

View File

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

View File

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

View File

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

View File

@ -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
);

View File

@ -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<NextPluginOptions> = [
) {
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,19 @@ 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',
port: 3000,
},
};
return targetConfig;
}
async function getOutputs(projectRoot, nextConfig) {
let dir = '.next';
const { PHASE_PRODUCTION_BUILD } = require('next/constants');
@ -196,6 +212,7 @@ function normalizeOptions(options: NextPluginOptions): NextPluginOptions {
options.buildTargetName ??= 'build';
options.devTargetName ??= 'dev';
options.startTargetName ??= 'start';
options.serveStaticTargetName ??= 'serve-static';
return options;
}

View File

@ -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);

View File

@ -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;
}
}