fix(nextjs): Add missing e2e-ci target for cypress (#21805)
This commit is contained in:
parent
11c849afab
commit
4c8c24b97a
1
.gitignore
vendored
1
.gitignore
vendored
@ -31,6 +31,7 @@ CHANGELOG.md
|
||||
|
||||
# Next.js
|
||||
.next
|
||||
out
|
||||
|
||||
# Angular Cache
|
||||
.angular
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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<
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -20,6 +20,7 @@ export function addPlugin(tree: Tree) {
|
||||
buildTargetName: 'build',
|
||||
devTargetName: 'dev',
|
||||
startTargetName: 'start',
|
||||
serveStaticTargetName: 'serve-static',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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": [
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user