From f0b9931dc286d7c4e344a6f96b9c14c897724b00 Mon Sep 17 00:00:00 2001 From: Nicholas Cunningham Date: Thu, 24 Aug 2023 17:50:02 -0600 Subject: [PATCH] fix(nextjs): improve e2e test by reusing the project --- CODEOWNERS | 3 +- e2e/{next => next-core}/jest.config.ts | 2 +- e2e/{next => next-core}/project.json | 4 +- .../src/next-appdir.test.ts | 8 +- e2e/next-core/src/next-lock-file.test.ts | 43 +++ e2e/next-core/src/next-structure.test.ts | 205 +++++++++++++ e2e/next-core/src/next-webpack.test.ts | 98 ++++++ e2e/{next => next-core}/src/next.test.ts | 279 +----------------- e2e/{next => next-core}/src/utils.ts | 0 e2e/{next => next-core}/tsconfig.json | 0 e2e/{next => next-core}/tsconfig.spec.json | 0 e2e/next-extensions/jest.config.ts | 13 + e2e/next-extensions/project.json | 10 + .../src/next-component-tests.test.ts | 0 .../src/next-experimental.test.ts | 8 +- .../src/next-storybook.test.ts | 11 +- .../src/next-styles.test.ts | 2 +- e2e/next-extensions/src/utils.ts | 55 ++++ e2e/next-extensions/tsconfig.json | 13 + e2e/next-extensions/tsconfig.spec.json | 20 ++ 20 files changed, 476 insertions(+), 298 deletions(-) rename e2e/{next => next-core}/jest.config.ts (91%) rename e2e/{next => next-core}/project.json (72%) rename e2e/{next => next-core}/src/next-appdir.test.ts (93%) create mode 100644 e2e/next-core/src/next-lock-file.test.ts create mode 100644 e2e/next-core/src/next-structure.test.ts create mode 100644 e2e/next-core/src/next-webpack.test.ts rename e2e/{next => next-core}/src/next.test.ts (50%) rename e2e/{next => next-core}/src/utils.ts (100%) rename e2e/{next => next-core}/tsconfig.json (100%) rename e2e/{next => next-core}/tsconfig.spec.json (100%) create mode 100644 e2e/next-extensions/jest.config.ts create mode 100644 e2e/next-extensions/project.json rename e2e/{next => next-extensions}/src/next-component-tests.test.ts (100%) rename e2e/{next => next-extensions}/src/next-experimental.test.ts (94%) rename e2e/{next => next-extensions}/src/next-storybook.test.ts (82%) rename e2e/{next => next-extensions}/src/next-styles.test.ts (98%) create mode 100644 e2e/next-extensions/src/utils.ts create mode 100644 e2e/next-extensions/tsconfig.json create mode 100644 e2e/next-extensions/tsconfig.spec.json diff --git a/CODEOWNERS b/CODEOWNERS index c864973e8d5a69..f5ddf0e3bba7a3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -36,7 +36,8 @@ pnpm-lock.yaml @nrwl/nx-core-reviewers /e2e/react-core/** @nrwl/nx-react-reviewers /e2e/react-extensions/** @nrwl/nx-react-reviewers /packages/next/** @nrwl/nx-react-reviewers -/e2e/next/** @nrwl/nx-react-reviewers +/e2e/next-core/** @nrwl/nx-react-reviewers +/e2e/next-extensions/** @nrwl/nx-react-reviewers /packages/react/plugins/component-testing/** @nrwl/nx-react-reviewers @nrwl/nx-testing-tools-reviewers /packages/react/src/generators/cypress-component-configuration/** @nrwl/nx-react-reviewers @nrwl/nx-testing-tools-reviewers /packages/react/src/generators/component-test/** @nrwl/nx-react-reviewers @nrwl/nx-testing-tools-reviewers diff --git a/e2e/next/jest.config.ts b/e2e/next-core/jest.config.ts similarity index 91% rename from e2e/next/jest.config.ts rename to e2e/next-core/jest.config.ts index 50c96a9e5ab47a..ac14e93c131256 100644 --- a/e2e/next/jest.config.ts +++ b/e2e/next-core/jest.config.ts @@ -8,6 +8,6 @@ export default { globals: {}, globalSetup: '../utils/global-setup.ts', globalTeardown: '../utils/global-teardown.ts', - displayName: 'e2e-next', + displayName: 'e2e-next-core', preset: '../../jest.preset.js', }; diff --git a/e2e/next/project.json b/e2e/next-core/project.json similarity index 72% rename from e2e/next/project.json rename to e2e/next-core/project.json index 71081e6cd49f33..8fd9a8748c6ee6 100644 --- a/e2e/next/project.json +++ b/e2e/next-core/project.json @@ -1,7 +1,7 @@ { - "name": "e2e-next", + "name": "e2e-next-core", "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "e2e/next", + "sourceRoot": "e2e/next-core/src", "projectType": "application", "targets": { "e2e": {} diff --git a/e2e/next/src/next-appdir.test.ts b/e2e/next-core/src/next-appdir.test.ts similarity index 93% rename from e2e/next/src/next-appdir.test.ts rename to e2e/next-core/src/next-appdir.test.ts index 3b00e2a56a087a..be0054f1369b67 100644 --- a/e2e/next/src/next-appdir.test.ts +++ b/e2e/next-core/src/next-appdir.test.ts @@ -11,13 +11,9 @@ import { checkApp } from './utils'; describe('Next.js App Router', () => { let proj: string; - beforeEach(() => { - proj = newProject(); - }); + beforeAll(() => (proj = newProject())); - afterEach(() => { - cleanupProject(); - }); + afterAll(() => cleanupProject()); it('should be able to generate and build app with default App Router', async () => { const appName = uniq('app'); diff --git a/e2e/next-core/src/next-lock-file.test.ts b/e2e/next-core/src/next-lock-file.test.ts new file mode 100644 index 00000000000000..ae190babdb68db --- /dev/null +++ b/e2e/next-core/src/next-lock-file.test.ts @@ -0,0 +1,43 @@ +import { detectPackageManager, joinPathFragments } from '@nx/devkit'; +import { + checkFilesExist, + cleanupProject, + getPackageManagerCommand, + newProject, + packageManagerLockFile, + runCLI, + runCommand, + tmpProjPath, + uniq, +} from '@nx/e2e/utils'; + +describe('Next.js Lock File', () => { + let proj: string; + let originalEnv: string; + let packageManager; + + beforeEach(() => { + proj = newProject(); + packageManager = detectPackageManager(tmpProjPath()); + originalEnv = process.env.NODE_ENV; + }); + + afterEach(() => { + process.env.NODE_ENV = originalEnv; + cleanupProject(); + }); + + it('should build and install pruned lock file', () => { + const appName = uniq('app'); + runCLI(`generate @nx/next:app ${appName} --no-interactive --style=css`); + + const result = runCLI(`build ${appName} --generateLockfile=true`); + expect(result).not.toMatch(/Graph is not consistent/); + checkFilesExist( + `dist/apps/${appName}/${packageManagerLockFile[packageManager]}` + ); + runCommand(`${getPackageManagerCommand().ciInstall}`, { + cwd: joinPathFragments(tmpProjPath(), 'dist/apps', appName), + }); + }, 1_000_000); +}); diff --git a/e2e/next-core/src/next-structure.test.ts b/e2e/next-core/src/next-structure.test.ts new file mode 100644 index 00000000000000..272648d818d7c2 --- /dev/null +++ b/e2e/next-core/src/next-structure.test.ts @@ -0,0 +1,205 @@ +import { removeSync, mkdirSync } from 'fs-extra'; +import { capitalize } from '@nx/devkit/src/utils/string-utils'; +import { checkApp } from './utils'; +import { + checkFilesExist, + cleanupProject, + isNotWindows, + killPort, + newProject, + readFile, + runCLI, + runCommandUntil, + tmpProjPath, + uniq, + updateFile, + updateProjectConfig, +} from '@nx/e2e/utils'; + +describe('Next.js Apps Libs', () => { + let proj: string; + let originalEnv: string; + + beforeEach(() => { + proj = newProject(); + originalEnv = process.env.NODE_ENV; + }); + + afterEach(() => { + process.env.NODE_ENV = originalEnv; + cleanupProject(); + }); + + it('should generate app + libs', async () => { + // Remove apps/libs folder and use packages. + // Allows us to test other integrated monorepo setup that had a regression. + // See: https://github.com/nrwl/nx/issues/16658 + removeSync(`${tmpProjPath()}/libs`); + removeSync(`${tmpProjPath()}/apps`); + mkdirSync(`${tmpProjPath()}/packages`); + + const appName = uniq('app'); + const nextLib = uniq('nextlib'); + const jsLib = uniq('tslib'); + const buildableLib = uniq('buildablelib'); + + runCLI( + `generate @nx/next:app ${appName} --no-interactive --style=css --appDir=false` + ); + runCLI(`generate @nx/next:lib ${nextLib} --no-interactive`); + runCLI(`generate @nx/js:lib ${jsLib} --no-interactive`); + runCLI( + `generate @nx/js:lib ${buildableLib} --no-interactive --bundler=vite` + ); + + // Create file in public that should be copied to dist + updateFile(`packages/${appName}/public/a/b.txt`, `Hello World!`); + + // Additional assets that should be copied to dist + const sharedLib = uniq('sharedLib'); + await updateProjectConfig(appName, (json) => { + json.targets.build.options.assets = [ + { + glob: '**/*', + input: `packages/${sharedLib}/src/assets`, + output: 'shared/ui', + }, + ]; + return json; + }); + updateFile(`packages/${sharedLib}/src/assets/hello.txt`, 'Hello World!'); + + // create a css file in node_modules so that it can be imported in a lib + // to test that it works as expected + updateFile( + 'node_modules/@nx/next/test-styles.css', + 'h1 { background-color: red; }' + ); + + updateFile( + `packages/${jsLib}/src/lib/${jsLib}.ts`, + ` + export function jsLib(): string { + return 'Hello Nx'; + }; + + // testing whether async-await code in Node / Next.js api routes works as expected + export async function jsLibAsync() { + return await Promise.resolve('hell0'); + } + ` + ); + + updateFile( + `packages/${buildableLib}/src/lib/${buildableLib}.ts`, + ` + export function buildableLib(): string { + return 'Hello Buildable'; + }; + ` + ); + + const mainPath = `packages/${appName}/pages/index.tsx`; + const content = readFile(mainPath); + + updateFile( + `packages/${appName}/pages/api/hello.ts`, + ` + import { jsLibAsync } from '@${proj}/${jsLib}'; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + export default async function handler(_: any, res: any) { + const value = await jsLibAsync(); + res.send(value); + } + ` + ); + + updateFile( + mainPath, + ` + import { jsLib } from '@${proj}/${jsLib}'; + import { buildableLib } from '@${proj}/${buildableLib}'; + /* eslint-disable */ + import dynamic from 'next/dynamic'; + + const TestComponent = dynamic( + () => import('@${proj}/${nextLib}').then(d => d.${capitalize( + nextLib + )}) + ); + ${content.replace( + ``, + ` +
+ {jsLib()} + {buildableLib()} + +
+ ` + )}` + ); + + const e2eTestPath = `packages/${appName}-e2e/src/e2e/app.cy.ts`; + const e2eContent = readFile(e2eTestPath); + updateFile( + e2eTestPath, + ` + ${ + e2eContent + + ` + it('should successfully call async API route', () => { + cy.request('/api/hello').its('body').should('include', 'hell0'); + }); + ` + } + ` + ); + + await checkApp(appName, { + checkUnitTest: true, + checkLint: true, + checkE2E: isNotWindows(), + checkExport: false, + appsDir: 'packages', + }); + + // public and shared assets should both be copied to dist + checkFilesExist( + `dist/packages/${appName}/public/a/b.txt`, + `dist/packages/${appName}/public/shared/ui/hello.txt` + ); + + // Check that compiled next config does not contain bad imports + const nextConfigPath = `dist/packages/${appName}/next.config.js`; + expect(nextConfigPath).not.toContain(`require("../`); // missing relative paths + expect(nextConfigPath).not.toContain(`require("nx/`); // dev-only packages + expect(nextConfigPath).not.toContain(`require("@nx/`); // dev-only packages + + // Check that `nx serve --prod` works with previous production build (e.g. `nx build `). + const prodServePort = 4000; + const prodServeProcess = await runCommandUntil( + `run ${appName}:serve --prod --port=${prodServePort}`, + (output) => { + return output.includes(`localhost:${prodServePort}`); + } + ); + + // Check that the output is self-contained (i.e. can run with its own package.json + node_modules) + const selfContainedPort = 3000; + runCLI( + `generate @nx/workspace:run-commands serve-prod --project ${appName} --cwd=dist/packages/${appName} --command="npx next start --port=${selfContainedPort}"` + ); + const selfContainedProcess = await runCommandUntil( + `run ${appName}:serve-prod`, + (output) => { + return output.includes(`localhost:${selfContainedPort}`); + } + ); + + prodServeProcess.kill(); + selfContainedProcess.kill(); + await killPort(prodServePort); + await killPort(selfContainedPort); + }, 600_000); +}); diff --git a/e2e/next-core/src/next-webpack.test.ts b/e2e/next-core/src/next-webpack.test.ts new file mode 100644 index 00000000000000..b1c956d64e622c --- /dev/null +++ b/e2e/next-core/src/next-webpack.test.ts @@ -0,0 +1,98 @@ +import { + checkFilesExist, + cleanupProject, + newProject, + rmDist, + runCLI, + uniq, + updateFile, + updateProjectConfig, +} from '@nx/e2e/utils'; + +describe('Next.js Webpack', () => { + let proj: string; + let originalEnv: string; + + beforeEach(() => { + proj = newProject(); + originalEnv = process.env.NODE_ENV; + }); + + afterEach(() => { + process.env.NODE_ENV = originalEnv; + cleanupProject(); + }); + + it('should support custom webpack and run-commands using withNx', async () => { + const appName = uniq('app'); + + runCLI( + `generate @nx/next:app ${appName} --no-interactive --style=css --appDir=false` + ); + + updateFile( + `apps/${appName}/next.config.js`, + ` + const { withNx } = require('@nx/next'); + const nextConfig = { + nx: { + svgr: false, + }, + webpack: (config, context) => { + // Make sure SVGR plugin is disabled if nx.svgr === false (see above) + const found = config.module.rules.find(r => { + if (!r.test || !r.test.test('test.svg')) return false; + if (!r.oneOf || !r.oneOf.use) return false; + return r.oneOf.use.some(rr => /svgr/.test(rr.loader)); + }); + if (found) throw new Error('Found SVGR plugin'); + + console.log('NODE_ENV is', process.env.NODE_ENV); + + return config; + } + }; + + module.exports = withNx(nextConfig); + ` + ); + // deleting `NODE_ENV` value, so that it's `undefined`, and not `"test"` + // by the time it reaches the build executor. + // this simulates existing behaviour of running a next.js build executor via Nx + delete process.env.NODE_ENV; + const result = runCLI(`build ${appName}`); + + checkFilesExist(`dist/apps/${appName}/next.config.js`); + expect(result).toContain('NODE_ENV is production'); + + updateFile( + `apps/${appName}/next.config.js`, + ` + const { withNx } = require('@nx/next'); + // Not including "nx" entry should still work. + const nextConfig = {}; + + module.exports = withNx(nextConfig); + ` + ); + rmDist(); + runCLI(`build ${appName}`); + checkFilesExist(`dist/apps/${appName}/next.config.js`); + + // Make sure withNx works with run-commands. + await updateProjectConfig(appName, (json) => { + json.targets.build = { + command: 'npx next build', + outputs: [`apps/${appName}/.next`], + options: { + cwd: `apps/${appName}`, + }, + }; + return json; + }); + expect(() => { + runCLI(`build ${appName}`); + }).not.toThrow(); + checkFilesExist(`apps/${appName}/.next/build-manifest.json`); + }, 300_000); +}); diff --git a/e2e/next/src/next.test.ts b/e2e/next-core/src/next.test.ts similarity index 50% rename from e2e/next/src/next.test.ts rename to e2e/next-core/src/next.test.ts index 710054d1875b16..f347914944c869 100644 --- a/e2e/next/src/next.test.ts +++ b/e2e/next-core/src/next.test.ts @@ -1,217 +1,35 @@ -import { detectPackageManager, joinPathFragments } from '@nx/devkit'; -import { capitalize } from '@nx/devkit/src/utils/string-utils'; import { checkFilesDoNotExist, checkFilesExist, cleanupProject, - getPackageManagerCommand, - isNotWindows, killPort, killPorts, newProject, - packageManagerLockFile, readFile, - rmDist, runCLI, - runCommand, runCommandUntil, - tmpProjPath, uniq, updateFile, - updateProjectConfig, } from '@nx/e2e/utils'; import * as http from 'http'; import { checkApp } from './utils'; -import { removeSync, mkdirSync } from 'fs-extra'; describe('Next.js Applications', () => { let proj: string; let originalEnv: string; - let packageManager; - beforeEach(() => { + beforeAll(() => { proj = newProject(); - packageManager = detectPackageManager(tmpProjPath()); + }); + beforeEach(() => { originalEnv = process.env.NODE_ENV; }); afterEach(() => { process.env.NODE_ENV = originalEnv; - cleanupProject(); }); - it('should generate app + libs', async () => { - // Remove apps/libs folder and use packages. - // Allows us to test other integrated monorepo setup that had a regression. - // See: https://github.com/nrwl/nx/issues/16658 - removeSync(`${tmpProjPath()}/libs`); - removeSync(`${tmpProjPath()}/apps`); - mkdirSync(`${tmpProjPath()}/packages`); - - const appName = uniq('app'); - const nextLib = uniq('nextlib'); - const jsLib = uniq('tslib'); - const buildableLib = uniq('buildablelib'); - - runCLI( - `generate @nx/next:app ${appName} --no-interactive --style=css --appDir=false` - ); - runCLI(`generate @nx/next:lib ${nextLib} --no-interactive`); - runCLI(`generate @nx/js:lib ${jsLib} --no-interactive`); - runCLI( - `generate @nx/js:lib ${buildableLib} --no-interactive --bundler=vite` - ); - - // Create file in public that should be copied to dist - updateFile(`packages/${appName}/public/a/b.txt`, `Hello World!`); - - // Additional assets that should be copied to dist - const sharedLib = uniq('sharedLib'); - await updateProjectConfig(appName, (json) => { - json.targets.build.options.assets = [ - { - glob: '**/*', - input: `packages/${sharedLib}/src/assets`, - output: 'shared/ui', - }, - ]; - return json; - }); - updateFile(`packages/${sharedLib}/src/assets/hello.txt`, 'Hello World!'); - - // create a css file in node_modules so that it can be imported in a lib - // to test that it works as expected - updateFile( - 'node_modules/@nx/next/test-styles.css', - 'h1 { background-color: red; }' - ); - - updateFile( - `packages/${jsLib}/src/lib/${jsLib}.ts`, - ` - export function jsLib(): string { - return 'Hello Nx'; - }; - - // testing whether async-await code in Node / Next.js api routes works as expected - export async function jsLibAsync() { - return await Promise.resolve('hell0'); - } - ` - ); - - updateFile( - `packages/${buildableLib}/src/lib/${buildableLib}.ts`, - ` - export function buildableLib(): string { - return 'Hello Buildable'; - }; - ` - ); - - const mainPath = `packages/${appName}/pages/index.tsx`; - const content = readFile(mainPath); - - updateFile( - `packages/${appName}/pages/api/hello.ts`, - ` - import { jsLibAsync } from '@${proj}/${jsLib}'; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - export default async function handler(_: any, res: any) { - const value = await jsLibAsync(); - res.send(value); - } - ` - ); - - updateFile( - mainPath, - ` - import { jsLib } from '@${proj}/${jsLib}'; - import { buildableLib } from '@${proj}/${buildableLib}'; - /* eslint-disable */ - import dynamic from 'next/dynamic'; - - const TestComponent = dynamic( - () => import('@${proj}/${nextLib}').then(d => d.${capitalize( - nextLib - )}) - ); - ${content.replace( - ``, - ` -
- {jsLib()} - {buildableLib()} - -
- ` - )}` - ); - - const e2eTestPath = `packages/${appName}-e2e/src/e2e/app.cy.ts`; - const e2eContent = readFile(e2eTestPath); - updateFile( - e2eTestPath, - ` - ${ - e2eContent + - ` - it('should successfully call async API route', () => { - cy.request('/api/hello').its('body').should('include', 'hell0'); - }); - ` - } - ` - ); - - await checkApp(appName, { - checkUnitTest: true, - checkLint: true, - checkE2E: isNotWindows(), - checkExport: false, - appsDir: 'packages', - }); - - // public and shared assets should both be copied to dist - checkFilesExist( - `dist/packages/${appName}/public/a/b.txt`, - `dist/packages/${appName}/public/shared/ui/hello.txt` - ); - - // Check that compiled next config does not contain bad imports - const nextConfigPath = `dist/packages/${appName}/next.config.js`; - expect(nextConfigPath).not.toContain(`require("../`); // missing relative paths - expect(nextConfigPath).not.toContain(`require("nx/`); // dev-only packages - expect(nextConfigPath).not.toContain(`require("@nx/`); // dev-only packages - - // Check that `nx serve --prod` works with previous production build (e.g. `nx build `). - const prodServePort = 4000; - const prodServeProcess = await runCommandUntil( - `run ${appName}:serve --prod --port=${prodServePort}`, - (output) => { - return output.includes(`localhost:${prodServePort}`); - } - ); - - // Check that the output is self-contained (i.e. can run with its own package.json + node_modules) - const selfContainedPort = 3000; - runCLI( - `generate @nx/workspace:run-commands serve-prod --project ${appName} --cwd=dist/packages/${appName} --command="npx next start --port=${selfContainedPort}"` - ); - const selfContainedProcess = await runCommandUntil( - `run ${appName}:serve-prod`, - (output) => { - return output.includes(`localhost:${selfContainedPort}`); - } - ); - - prodServeProcess.kill(); - selfContainedProcess.kill(); - await killPort(prodServePort); - await killPort(selfContainedPort); - }, 600_000); + afterAll(() => cleanupProject()); it('should support generating projects with the new name and root format', () => { const appName = uniq('app1'); @@ -254,20 +72,6 @@ describe('Next.js Applications', () => { ); }, 600_000); - it('should build and install pruned lock file', () => { - const appName = uniq('app'); - runCLI(`generate @nx/next:app ${appName} --no-interactive --style=css`); - - const result = runCLI(`build ${appName} --generateLockfile=true`); - expect(result).not.toMatch(/Graph is not consistent/); - checkFilesExist( - `dist/apps/${appName}/${packageManagerLockFile[packageManager]}` - ); - runCommand(`${getPackageManagerCommand().ciInstall}`, { - cwd: joinPathFragments(tmpProjPath(), 'dist/apps', appName), - }); - }, 1_000_000); - it('should build app and .next artifacts at the outputPath if provided by the CLI', () => { const appName = uniq('app'); runCLI(`generate @nx/next:app ${appName} --no-interactive --style=css`); @@ -348,82 +152,10 @@ describe('Next.js Applications', () => { expect(apiData).toContain(`Welcome`); expect(pageData).toContain(`test value from a file`); + await killPort(port); await killPorts(); }, 300_000); - it('should support custom webpack and run-commands using withNx', async () => { - const appName = uniq('app'); - - runCLI( - `generate @nx/next:app ${appName} --no-interactive --style=css --appDir=false` - ); - - updateFile( - `apps/${appName}/next.config.js`, - ` - const { withNx } = require('@nx/next'); - const nextConfig = { - nx: { - svgr: false, - }, - webpack: (config, context) => { - // Make sure SVGR plugin is disabled if nx.svgr === false (see above) - const found = config.module.rules.find(r => { - if (!r.test || !r.test.test('test.svg')) return false; - if (!r.oneOf || !r.oneOf.use) return false; - return r.oneOf.use.some(rr => /svgr/.test(rr.loader)); - }); - if (found) throw new Error('Found SVGR plugin'); - - console.log('NODE_ENV is', process.env.NODE_ENV); - - return config; - } - }; - - module.exports = withNx(nextConfig); - ` - ); - // deleting `NODE_ENV` value, so that it's `undefined`, and not `"test"` - // by the time it reaches the build executor. - // this simulates existing behaviour of running a next.js build executor via Nx - delete process.env.NODE_ENV; - const result = runCLI(`build ${appName}`); - - checkFilesExist(`dist/apps/${appName}/next.config.js`); - expect(result).toContain('NODE_ENV is production'); - - updateFile( - `apps/${appName}/next.config.js`, - ` - const { withNx } = require('@nx/next'); - // Not including "nx" entry should still work. - const nextConfig = {}; - - module.exports = withNx(nextConfig); - ` - ); - rmDist(); - runCLI(`build ${appName}`); - checkFilesExist(`dist/apps/${appName}/next.config.js`); - - // Make sure withNx works with run-commands. - await updateProjectConfig(appName, (json) => { - json.targets.build = { - command: 'npx next build', - outputs: [`apps/${appName}/.next`], - options: { - cwd: `apps/${appName}`, - }, - }; - return json; - }); - expect(() => { - runCLI(`build ${appName}`); - }).not.toThrow(); - checkFilesExist(`apps/${appName}/.next/build-manifest.json`); - }, 300_000); - it('should build in dev mode without errors', async () => { const appName = uniq('app'); @@ -563,6 +295,7 @@ describe('Next.js Applications', () => { } ); selfContainedProcess.kill(); + await killPort(port); await killPorts(); }, 300_000); }); diff --git a/e2e/next/src/utils.ts b/e2e/next-core/src/utils.ts similarity index 100% rename from e2e/next/src/utils.ts rename to e2e/next-core/src/utils.ts diff --git a/e2e/next/tsconfig.json b/e2e/next-core/tsconfig.json similarity index 100% rename from e2e/next/tsconfig.json rename to e2e/next-core/tsconfig.json diff --git a/e2e/next/tsconfig.spec.json b/e2e/next-core/tsconfig.spec.json similarity index 100% rename from e2e/next/tsconfig.spec.json rename to e2e/next-core/tsconfig.spec.json diff --git a/e2e/next-extensions/jest.config.ts b/e2e/next-extensions/jest.config.ts new file mode 100644 index 00000000000000..8fa6724cf219d5 --- /dev/null +++ b/e2e/next-extensions/jest.config.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +export default { + displayName: 'e2e-next-extensions', + transform: { + '^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], + maxWorkers: 1, + globals: {}, + globalSetup: '../utils/global-setup.ts', + globalTeardown: '../utils/global-teardown.ts', + preset: '../../jest.preset.js', +}; diff --git a/e2e/next-extensions/project.json b/e2e/next-extensions/project.json new file mode 100644 index 00000000000000..83d41adbdd9d82 --- /dev/null +++ b/e2e/next-extensions/project.json @@ -0,0 +1,10 @@ +{ + "name": "e2e-next-extensions", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "e2e/next-extensions/src", + "projectType": "application", + "targets": { + "e2e": {} + }, + "implicitDependencies": ["next"] +} diff --git a/e2e/next/src/next-component-tests.test.ts b/e2e/next-extensions/src/next-component-tests.test.ts similarity index 100% rename from e2e/next/src/next-component-tests.test.ts rename to e2e/next-extensions/src/next-component-tests.test.ts diff --git a/e2e/next/src/next-experimental.test.ts b/e2e/next-extensions/src/next-experimental.test.ts similarity index 94% rename from e2e/next/src/next-experimental.test.ts rename to e2e/next-extensions/src/next-experimental.test.ts index de0778b490c056..91062d7a035ff1 100644 --- a/e2e/next/src/next-experimental.test.ts +++ b/e2e/next-extensions/src/next-experimental.test.ts @@ -10,13 +10,9 @@ import { checkApp } from './utils'; describe('Next.js Experimental Features', () => { let proj: string; - beforeEach(() => { - proj = newProject(); - }); + beforeAll(() => (proj = newProject())); - afterEach(() => { - cleanupProject(); - }); + afterAll(() => cleanupProject()); it('should be able to define server actions in workspace libs', async () => { const appName = uniq('app'); diff --git a/e2e/next/src/next-storybook.test.ts b/e2e/next-extensions/src/next-storybook.test.ts similarity index 82% rename from e2e/next/src/next-storybook.test.ts rename to e2e/next-extensions/src/next-storybook.test.ts index e5abbfae9dc750..23fdc2e2e5ef61 100644 --- a/e2e/next/src/next-storybook.test.ts +++ b/e2e/next-extensions/src/next-storybook.test.ts @@ -9,17 +9,12 @@ import { updateJson, } from '@nx/e2e/utils'; -describe('Next.js Applications', () => { +describe('Next.js Storybook', () => { let proj: string; - beforeEach(() => { - proj = newProject({ - name: 'proj', - packageManager: 'npm', - }); - }); + beforeAll(() => (proj = newProject({ name: 'proj', packageManager: 'npm' }))); - afterEach(() => cleanupProject()); + afterAll(() => cleanupProject()); it('should run a Next.js based Storybook setup', async () => { const appName = uniq('app'); diff --git a/e2e/next/src/next-styles.test.ts b/e2e/next-extensions/src/next-styles.test.ts similarity index 98% rename from e2e/next/src/next-styles.test.ts rename to e2e/next-extensions/src/next-styles.test.ts index 5ba0700d4e5e96..19b5f15f270711 100644 --- a/e2e/next/src/next-styles.test.ts +++ b/e2e/next-extensions/src/next-styles.test.ts @@ -1,7 +1,7 @@ import { cleanupProject, newProject, runCLI, uniq } from '@nx/e2e/utils'; import { checkApp } from './utils'; -describe('Next.js apps', () => { +describe('Next.js Styles', () => { let originalEnv: string; beforeAll(() => { diff --git a/e2e/next-extensions/src/utils.ts b/e2e/next-extensions/src/utils.ts new file mode 100644 index 00000000000000..028fcbbb8e66f6 --- /dev/null +++ b/e2e/next-extensions/src/utils.ts @@ -0,0 +1,55 @@ +import { + checkFilesExist, + killPorts, + readJson, + runCLI, + runCLIAsync, + runE2ETests, +} from '../../utils'; + +export async function checkApp( + appName: string, + opts: { + checkUnitTest: boolean; + checkLint: boolean; + checkE2E: boolean; + checkExport: boolean; + appsDir?: string; + } +) { + const appsDir = opts.appsDir ?? 'apps'; + + if (opts.checkLint) { + const lintResults = runCLI(`lint ${appName}`); + expect(lintResults).toContain('All files pass linting.'); + } + + if (opts.checkUnitTest) { + const testResults = await runCLIAsync(`test ${appName}`); + expect(testResults.combinedOutput).toContain( + 'Test Suites: 1 passed, 1 total' + ); + } + + const buildResult = runCLI(`build ${appName}`); + expect(buildResult).toContain(`Successfully ran target build`); + checkFilesExist(`dist/${appsDir}/${appName}/.next/build-manifest.json`); + + const packageJson = readJson(`dist/${appsDir}/${appName}/package.json`); + expect(packageJson.dependencies.react).toBeDefined(); + expect(packageJson.dependencies['react-dom']).toBeDefined(); + expect(packageJson.dependencies.next).toBeDefined(); + + if (opts.checkE2E && runE2ETests()) { + const e2eResults = runCLI( + `e2e ${appName}-e2e --no-watch --configuration=production` + ); + expect(e2eResults).toContain('Successfully ran target e2e for project'); + expect(await killPorts()).toBeTruthy(); + } + + if (opts.checkExport) { + runCLI(`export ${appName}`); + checkFilesExist(`dist/${appsDir}/${appName}/exported/index.html`); + } +} diff --git a/e2e/next-extensions/tsconfig.json b/e2e/next-extensions/tsconfig.json new file mode 100644 index 00000000000000..6d5abf84832009 --- /dev/null +++ b/e2e/next-extensions/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node", "jest"] + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/e2e/next-extensions/tsconfig.spec.json b/e2e/next-extensions/tsconfig.spec.json new file mode 100644 index 00000000000000..1a24bfb0a13536 --- /dev/null +++ b/e2e/next-extensions/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", + "**/*.d.ts", + "jest.config.ts" + ] +}