Skip to content

Commit

Permalink
fix(nextjs): Add missing e2e-ci target for cypress
Browse files Browse the repository at this point in the history
  • Loading branch information
ndcunningham committed Feb 14, 2024
1 parent 343c0f6 commit 8905e6f
Show file tree
Hide file tree
Showing 14 changed files with 200 additions and 3 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
41 changes: 41 additions & 0 deletions docs/generated/packages/next/documents/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,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
41 changes: 41 additions & 0 deletions docs/shared/packages/next/plugin-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,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
40 changes: 40 additions & 0 deletions e2e/next-core/src/next.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import {
checkFilesDoNotExist,
checkFilesExist,
cleanupProject,
killPorts,
newProject,
readFile,
runCLI,
runE2ETests,
uniq,
updateFile,
} from '@nx/e2e/utils';
Expand Down Expand Up @@ -183,6 +185,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
8 changes: 8 additions & 0 deletions packages/next/src/generators/application/lib/add-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ export function addProject(host: Tree, options: NormalizedSchema) {
buildTarget: `${options.projectName}:build:production`,
},
};
} else {
// We need to add the serve-static target so that the port can be configured.
targets['serve-static'] = {
executor: '@nx/web:file-server',
options: {
port: 3000,
},
};
}

const project: ProjectConfiguration = {
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
14 changes: 14 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,13 @@ 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",
"staticFilePath": "{projectRoot}/out",
},
},
"my-start": {
"command": "next start",
"dependsOn": [
Expand Down Expand Up @@ -85,6 +92,13 @@ exports[`@nx/next/plugin root projects should create nodes 1`] = `
"cwd": ".",
},
},
"serve-static": {
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "build",
"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
18 changes: 17 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,18 @@ 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',
},
};

return targetConfig;
}

async function getOutputs(projectRoot, nextConfig) {
let dir = '.next';
const { PHASE_PRODUCTION_BUILD } = require('next/constants');
Expand Down Expand Up @@ -196,6 +211,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
7 changes: 7 additions & 0 deletions packages/web/src/executors/file-server/file-server.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -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;
}
}
Expand Down

0 comments on commit 8905e6f

Please sign in to comment.