Skip to content

Commit

Permalink
fix(nextjs): Add missing e2e-ci target for cypress (#21805)
Browse files Browse the repository at this point in the history
  • Loading branch information
ndcunningham authored Feb 16, 2024
1 parent 11c849a commit 4c8c24b
Show file tree
Hide file tree
Showing 13 changed files with 206 additions and 5 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ CHANGELOG.md

# Next.js
.next
out

# Angular Cache
.angular
Expand Down
48 changes: 47 additions & 1 deletion docs/generated/packages/next/documents/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
]
Expand All @@ -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" %}
Expand Down Expand Up @@ -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.
Expand Down
48 changes: 47 additions & 1 deletion docs/shared/packages/next/plugin-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
]
Expand All @@ -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" %}
Expand Down Expand Up @@ -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.
Expand Down
39 changes: 39 additions & 0 deletions e2e/next-core/src/next.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
newProject,
readFile,
runCLI,
runE2ETests,
uniq,
updateFile,
} from '@nx/e2e/utils';
Expand Down Expand Up @@ -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> {
Expand Down
6 changes: 6 additions & 0 deletions packages/next/plugins/with-nx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
20 changes: 20 additions & 0 deletions packages/next/src/generators/application/lib/add-e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -18,17 +19,28 @@ 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'),
targets: {},
tags: [],
implicitDependencies: [options.projectName],
});

return configurationGenerator(host, {
...options,
linter: Linter.EsLint,
Expand All @@ -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<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/generators/init/lib/add-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function addPlugin(tree: Tree) {
buildTargetName: 'build',
devTargetName: 'dev',
startTargetName: 'start',
serveStaticTargetName: 'serve-static',
},
});

Expand Down
16 changes: 16 additions & 0 deletions packages/next/src/plugins/__snapshots__/plugin.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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": [
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/plugins/plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe('@nx/next/plugin', () => {
buildTargetName: 'build',
devTargetName: 'dev',
startTargetName: 'start',
serveStaticTargetName: 'serve-static',
},
context
);
Expand Down Expand Up @@ -73,6 +74,7 @@ describe('@nx/next/plugin', () => {
buildTargetName: 'my-build',
devTargetName: 'my-serve',
startTargetName: 'my-start',
serveStaticTargetName: 'my-serve-static',
},
context
);
Expand Down
19 changes: 18 additions & 1 deletion packages/next/src/plugins/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface NextPluginOptions {
buildTargetName?: string;
devTargetName?: string;
startTargetName?: string;
serveStaticTargetName?: string;
}

const cachePath = join(projectGraphCacheDirectory, 'next.hash');
Expand Down Expand Up @@ -62,7 +63,6 @@ export const createNodes: CreateNodes<NextPluginOptions> = [
) {
return {};
}

options = normalizeOptions(options);

const hash = calculateHashForCreateNodes(projectRoot, options, context, [
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -196,6 +212,7 @@ function normalizeOptions(options: NextPluginOptions): NextPluginOptions {
options.buildTargetName ??= 'build';
options.devTargetName ??= 'dev';
options.startTargetName ??= 'start';
options.serveStaticTargetName ??= 'serve-static';
return options;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/utils/add-gitignore-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 4c8c24b

Please sign in to comment.