diff --git a/docs/generated/packages/next/executors/build.json b/docs/generated/packages/next/executors/build.json index caf2522900..127ae5cdce 100644 --- a/docs/generated/packages/next/executors/build.json +++ b/docs/generated/packages/next/executors/build.json @@ -68,8 +68,15 @@ }, "debug": { "type": "boolean", - "description": "Enable Next.js debug build logging", - "default": false + "description": "Enable Next.js debug build logging" + }, + "profile": { + "type": "boolean", + "description": "Used to enable React Production Profiling" + }, + "experimentalAppOnly": { + "type": "boolean", + "description": "Only build 'app' routes" } }, "required": ["root", "outputPath"], diff --git a/docs/generated/packages/next/executors/export.json b/docs/generated/packages/next/executors/export.json index 568aa1c7c1..4ced6f64b3 100644 --- a/docs/generated/packages/next/executors/export.json +++ b/docs/generated/packages/next/executors/export.json @@ -33,6 +33,7 @@ "presets": [] }, "description": "Export a Next.js application. The exported application is located at `dist/$outputPath/exported`.", + "x-deprecated": "Use static exports in next.config.js instead. See: https://nextjs.org/docs/pages/building-your-application/deploying/static-exports.", "aliases": [], "hidden": false, "path": "/packages/next/src/executors/export/schema.json", diff --git a/docs/generated/packages/next/executors/server.json b/docs/generated/packages/next/executors/server.json index d269906a9d..eedb9c624d 100644 --- a/docs/generated/packages/next/executors/server.json +++ b/docs/generated/packages/next/executors/server.json @@ -53,6 +53,10 @@ "type": "boolean", "description": "Read buildable libraries from source instead of building them separately.", "default": true + }, + "keepAliveTimeout": { + "type": "number", + "description": "Max milliseconds to wait before closing inactive connection." } }, "required": ["buildTarget"], diff --git a/e2e/next/src/utils.ts b/e2e/next/src/utils.ts index b0351ac4d5..174bfa10ad 100644 --- a/e2e/next/src/utils.ts +++ b/e2e/next/src/utils.ts @@ -1,6 +1,7 @@ +import { execSync } from 'child_process'; import { checkFilesExist, - killPorts, + killPort, readJson, runCLI, runCLIAsync, @@ -42,10 +43,10 @@ export async function checkApp( if (opts.checkE2E && runCypressTests()) { const e2eResults = runCLI( - `e2e ${appName}-e2e --no-watch --configuration=production` + `e2e ${appName}-e2e --no-watch --configuration=production --port=9000` ); expect(e2eResults).toContain('All specs passed!'); - expect(await killPorts()).toBeTruthy(); + await killPort(9000); } if (opts.checkExport) { diff --git a/packages/next/executors.json b/packages/next/executors.json index 35f5ee9375..75ceca37a6 100644 --- a/packages/next/executors.json +++ b/packages/next/executors.json @@ -13,7 +13,8 @@ "export": { "implementation": "./src/executors/export/export.impl", "schema": "./src/executors/export/schema.json", - "description": "Export a Next.js application. The exported application is located at `dist/$outputPath/exported`." + "description": "Export a Next.js application. The exported application is located at `dist/$outputPath/exported`.", + "x-deprecated": "Use static exports in next.config.js instead. See: https://nextjs.org/docs/pages/building-your-application/deploying/static-exports." } }, "builders": { @@ -30,7 +31,8 @@ "export": { "implementation": "./src/executors/export/compat", "schema": "./src/executors/export/schema.json", - "description": "Export a Next.js application. The exported application is located at `dist/$outputPath/exported`." + "description": "Export a Next.js application. The exported application is located at `dist/$outputPath/exported`.", + "x-deprecated": "Use static exports in next.config.js instead. See: https://nextjs.org/docs/pages/building-your-application/deploying/static-exports." } } } diff --git a/packages/next/package.json b/packages/next/package.json index ee46671a84..d514b122d5 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -31,7 +31,7 @@ "migrations": "./migrations.json" }, "peerDependencies": { - "next": "^13.0.0" + "next": ">=13.0.0" }, "dependencies": { "@babel/plugin-proposal-decorators": "^7.14.5", diff --git a/packages/next/plugins/with-nx.ts b/packages/next/plugins/with-nx.ts index 6397720b84..5733357e4b 100644 --- a/packages/next/plugins/with-nx.ts +++ b/packages/next/plugins/with-nx.ts @@ -113,55 +113,10 @@ function getNxContext( /** * Try to read output dir from project, and default to '.next' if executing outside of Nx (e.g. dist is added to a docker image). */ -async function determineDistDirForProdServer( - nextConfig: NextConfig -): Promise { - const project = process.env.NX_TASK_TARGET_PROJECT; - const target = process.env.NX_TASK_TARGET_TARGET; - const configuration = process.env.NX_TASK_TARGET_CONFIGURATION; - - try { - if (project && target) { - // If NX env vars are set, then devkit must be available. - const { - createProjectGraphAsync, - joinPathFragments, - offsetFromRoot, - } = require('@nx/devkit'); - const originalTarget = { project, target, configuration }; - const graph = await createProjectGraphAsync(); - - const { options, node: projectNode } = getNxContext( - graph, - originalTarget - ); - const outputDir = `${offsetFromRoot(projectNode.data.root)}${ - options.outputPath - }`; - return nextConfig.distDir && nextConfig.distDir !== '.next' - ? joinPathFragments(outputDir, nextConfig.distDir) - : joinPathFragments(outputDir, '.next'); - } - } catch { - // ignored -- fallback to Next.js default of '.next' - } - - return nextConfig.distDir || '.next'; -} - function withNx( _nextConfig = {} as WithNxOptions, context: WithNxContext = getWithNxContext() ): NextConfigFn { - // If this is not set user will see compile errors in Next.js 13.4. - // See: https://github.com/nrwl/nx/issues/16692, https://github.com/vercel/next.js/issues/49169 - // TODO(jack): Remove this once Nx is refactored to invoke CLI directly. - forNextVersion('>=13.4.0', () => { - process.env['__NEXT_PRIVATE_PREBUNDLED_REACT'] = - // Not in Next 13.3 or earlier, so need to access config via string - _nextConfig.experimental?.['serverActions'] ? 'experimental' : 'next'; - }); - return async (phase: string) => { const { PHASE_PRODUCTION_SERVER } = await import('next/constants'); if (phase === PHASE_PRODUCTION_SERVER) { @@ -169,8 +124,8 @@ function withNx( // NOTE: Avoid any `require(...)` or `import(...)` statements here. Development dependencies are not available at production runtime. const { nx, ...validNextConfig } = _nextConfig; return { + distDir: '.next', ...validNextConfig, - distDir: await determineDistDirForProdServer(_nextConfig), }; } else { const { diff --git a/packages/next/src/executors/build/build.impl.ts b/packages/next/src/executors/build/build.impl.ts index 0eba85430b..7249ea9334 100644 --- a/packages/next/src/executors/build/build.impl.ts +++ b/packages/next/src/executors/build/build.impl.ts @@ -4,9 +4,9 @@ import { readJsonFile, workspaceRoot, writeJsonFile, + logger, } from '@nx/devkit'; import { createLockFile, createPackageJson, getLockFileName } from '@nx/js'; -import build from 'next/dist/build'; import { join, resolve } from 'path'; import { copySync, existsSync, mkdir, writeFileSync } from 'fs-extra'; import { lt, gte } from 'semver'; @@ -17,6 +17,8 @@ import { updatePackageJson } from './lib/update-package-json'; import { createNextConfigFile } from './lib/create-next-config-file'; import { checkPublicDirectory } from './lib/check-project'; import { NextBuildBuilderOptions } from '../../utils/types'; +import { ExecSyncOptions, execSync } from 'child_process'; +import { createCliOptions } from '../../utils/create-cli-options'; export default async function buildExecutor( options: NextBuildBuilderOptions, @@ -42,22 +44,24 @@ export default async function buildExecutor( reactDomVersion && gte(checkAndCleanWithSemver('react-dom', reactDomVersion), '18.0.0'); if (hasReact18) { - (process.env as any).__NEXT_REACT_ROOT ||= 'true'; + process.env['__NEXT_REACT_ROOT'] ||= 'true'; } - // Get the installed Next.js version (will be removed after Nx 16 and Next.js update) - const nextVersion = require('next/package.json').version; + const { experimentalAppOnly, profile, debug } = options; - const debug = !!process.env.NX_VERBOSE_LOGGING || options.debug; - - // Check the major and minor version numbers - if (lt(nextVersion, '13.2.0')) { - // If the version is lower than 13.2.0, use the second parameter as the config object - await build(root, null, false, debug); - } else { - // Otherwise, use the third parameter as a boolean flag for verbose logging - // @ts-ignore - await build(root, false, debug); + const args = createCliOptions({ experimentalAppOnly, profile, debug }); + const command = `npx next build ${args}`; + const execSyncOptions: ExecSyncOptions = { + stdio: 'inherit', + encoding: 'utf-8', + cwd: root, + }; + try { + execSync(command, execSyncOptions); + } catch (error) { + logger.error(`Error occurred while trying to run the ${command}`); + logger.error(error); + return { success: false }; } if (!directoryExists(options.outputPath)) { diff --git a/packages/next/src/executors/build/schema.json b/packages/next/src/executors/build/schema.json index 1a669e4b86..713e0cbc38 100644 --- a/packages/next/src/executors/build/schema.json +++ b/packages/next/src/executors/build/schema.json @@ -65,8 +65,15 @@ }, "debug": { "type": "boolean", - "description": "Enable Next.js debug build logging", - "default": false + "description": "Enable Next.js debug build logging" + }, + "profile": { + "type": "boolean", + "description": "Used to enable React Production Profiling" + }, + "experimentalAppOnly": { + "type": "boolean", + "description": "Only build 'app' routes" } }, "required": ["root", "outputPath"] diff --git a/packages/next/src/executors/export/export.impl.ts b/packages/next/src/executors/export/export.impl.ts index c397ccfea6..75eeb080b9 100644 --- a/packages/next/src/executors/export/export.impl.ts +++ b/packages/next/src/executors/export/export.impl.ts @@ -16,7 +16,7 @@ import { NextBuildBuilderOptions, NextExportBuilderOptions, } from '../../utils/types'; -import { PHASE_EXPORT } from '../../utils/constants'; + import nextTrace = require('next/dist/trace'); import { platform } from 'os'; import { execFileSync } from 'child_process'; @@ -25,6 +25,18 @@ import * as chalk from 'chalk'; // platform specific command name const pmCmd = platform() === 'win32' ? `npx.cmd` : 'npx'; +/** + * @deprecated use output inside of your next.config.js + * Example + * const nextConfig = { + nx: { + svgr: false, + }, + + output: 'export' +}; + * Read https://nextjs.org/docs/pages/building-your-application/deploying/static-exports + **/ export default async function exportExecutor( options: NextExportBuilderOptions, context: ExecutorContext @@ -41,7 +53,6 @@ export default async function exportExecutor( dependencies = result.dependencies; } - const libsDir = join(context.root, workspaceLayout().libsDir); const buildTarget = parseTargetString( options.buildTarget, context.projectGraph diff --git a/packages/next/src/executors/server/custom-server.impl.ts b/packages/next/src/executors/server/custom-server.impl.ts new file mode 100644 index 0000000000..cf2e89fcd8 --- /dev/null +++ b/packages/next/src/executors/server/custom-server.impl.ts @@ -0,0 +1,67 @@ +import 'dotenv/config'; +import { + ExecutorContext, + parseTargetString, + readTargetOptions, + runExecutor, +} from '@nx/devkit'; +import { join, resolve } from 'path'; + +import { + NextBuildBuilderOptions, + NextServeBuilderOptions, +} from '../../utils/types'; + +export default async function* serveExecutor( + options: NextServeBuilderOptions, + context: ExecutorContext +) { + // Cast to any to overwrite NODE_ENV + (process.env as any).NODE_ENV = process.env.NODE_ENV + ? process.env.NODE_ENV + : options.dev + ? 'development' + : 'production'; + + // Setting port that the custom server should use. + (process.env as any).PORT = options.port; + + const buildOptions = readTargetOptions( + parseTargetString(options.buildTarget, context.projectGraph), + context + ); + const root = resolve(context.root, buildOptions.root); + + yield* runCustomServer(root, options, context); +} + +async function* runCustomServer( + root: string, + options: NextServeBuilderOptions, + context: ExecutorContext +) { + process.env.NX_NEXT_DIR = root; + process.env.NX_NEXT_PUBLIC_DIR = join(root, 'public'); + + const baseUrl = `http://${options.hostname || 'localhost'}:${options.port}`; + + const customServerBuild = await runExecutor( + parseTargetString(options.customServerTarget, context.projectGraph), + { + watch: options.dev ? true : false, + }, + context + ); + + for await (const result of customServerBuild) { + if (!result.success) { + return result; + } + yield { + success: true, + baseUrl, + }; + } + + return { success: true }; +} diff --git a/packages/next/src/executors/server/schema.json b/packages/next/src/executors/server/schema.json index b060e03f46..8e82a96d05 100644 --- a/packages/next/src/executors/server/schema.json +++ b/packages/next/src/executors/server/schema.json @@ -50,6 +50,10 @@ "type": "boolean", "description": "Read buildable libraries from source instead of building them separately.", "default": true + }, + "keepAliveTimeout": { + "type": "number", + "description": "Max milliseconds to wait before closing inactive connection." } }, "required": ["buildTarget"] diff --git a/packages/next/src/executors/server/server.impl.ts b/packages/next/src/executors/server/server.impl.ts index a532e0de7d..99d658670c 100644 --- a/packages/next/src/executors/server/server.impl.ts +++ b/packages/next/src/executors/server/server.impl.ts @@ -1,27 +1,29 @@ import 'dotenv/config'; +import * as net from 'net'; import { ExecutorContext, logger, parseTargetString, readTargetOptions, - runExecutor, } from '@nx/devkit'; -import * as chalk from 'chalk'; -import { existsSync } from 'fs'; -import { join, resolve } from 'path'; +import { resolve } from 'path'; import { NextBuildBuilderOptions, NextServeBuilderOptions, - NextServerOptions, - ProxyConfig, } from '../../utils/types'; -import { defaultServer } from './lib/default-server'; +import { spawn } from 'child_process'; +import customServer from './custom-server.impl'; +import { createCliOptions } from '../../utils/create-cli-options'; +import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable'; export default async function* serveExecutor( options: NextServeBuilderOptions, context: ExecutorContext ) { + if (options.customServerTarget) { + return yield* customServer(options, context); + } // Cast to any to overwrite NODE_ENV (process.env as any).NODE_ENV = process.env.NODE_ENV ? process.env.NODE_ENV @@ -38,96 +40,74 @@ export default async function* serveExecutor( ); const root = resolve(context.root, buildOptions.root); - if (options.customServerTarget) { - yield* runCustomServer(root, options, buildOptions, context); - } else { - yield* runNextDevServer(root, options, context); - } -} + const { port, keepAliveTimeout, hostname } = options; -async function* runNextDevServer( - root: string, - options: NextServeBuilderOptions, - context: ExecutorContext -) { - const baseUrl = `http://${options.hostname || 'localhost'}:${options.port}`; - const settings: NextServerOptions = { - dev: options.dev, - dir: root, - staticMarkup: options.staticMarkup, - quiet: options.quiet, - port: options.port, - customServer: !!options.customServerTarget, - hostname: options.hostname || 'localhost', - }; + const args = createCliOptions({ port, keepAliveTimeout, hostname }); + const nextDir = resolve(context.root, buildOptions.outputPath); - // look for the proxy.conf.json - let proxyConfig: ProxyConfig; - const proxyConfigPath = options.proxyConfig - ? join(context.root, options.proxyConfig) - : join(root, 'proxy.conf.json'); + const command = `npx next ${options.dev ? `dev ${args}` : `start ${args}`}`; - // TODO(v16): Remove proxy support. - if (existsSync(proxyConfigPath)) { - logger.warn( - `The "proxyConfig" option will be removed in Nx 16. Use the "rewrites" feature from Next.js instead. See: https://nextjs.org/docs/api-reference/next.config.js/rewrites` - ); - proxyConfig = require(proxyConfigPath); - } + yield* createAsyncIterable<{ success: boolean; baseUrl: string }>( + ({ done, next, error }) => { + // Client to check if server is ready. + const client = new net.Socket(); + const cleanupClient = () => { + client.removeAllListeners('connect'); + client.removeAllListeners('error'); + client.end(); + client.destroy(); + client.unref(); + }; - try { - await defaultServer(settings, proxyConfig); - logger.info(`[ ${chalk.green('ready')} ] on ${baseUrl}`); + const waitForServerReady = (retries = 30) => { + const allowedErrorCodes = ['ECONNREFUSED', 'ECONNRESET']; - yield { - baseUrl, - success: true, - }; + client.once('connect', () => { + cleanupClient(); + next({ + success: true, + baseUrl: `http://${options.hostname ?? 'localhost'}:${port}`, + }); + }); - // This Promise intentionally never resolves, leaving the process running - await new Promise<{ success: boolean }>(() => {}); - } catch (e) { - if (options.dev) { - throw e; - } else { - if (process.env.NX_VERBOSE_LOGGING) { - console.error(e); - } - throw new Error( - `Could not start production server. Try building your app with \`nx build ${context.projectName}\`.` - ); + client.on('error', (err) => { + if (retries === 0 || !allowedErrorCodes.includes(err['code'])) { + cleanupClient(); + error(err); + } else { + setTimeout(() => waitForServerReady(retries - 1), 1000); + } + }); + + client.connect({ port, host: '127.0.0.1' }); + }; + + const server = spawn(command, { + cwd: options.dev ? root : nextDir, + stdio: 'inherit', + shell: true, + }); + + waitForServerReady(); + + server.once('exit', (code) => { + cleanupClient(); + if (code === 0) { + done(); + } else { + error(new Error(`Next.js app exited with code ${code}`)); + } + }); + + process.on('exit', async (code) => { + if (code === 128 + 2) { + server.kill('SIGINT'); + } else if (code === 128 + 1) { + server.kill('SIGHUP'); + } else { + server.kill('SIGTERM'); + } + }); } - } -} - -async function* runCustomServer( - root: string, - options: NextServeBuilderOptions, - buildOptions: NextBuildBuilderOptions, - context: ExecutorContext -) { - process.env.NX_NEXT_DIR = root; - process.env.NX_NEXT_PUBLIC_DIR = join(root, 'public'); - - const baseUrl = `http://${options.hostname || 'localhost'}:${options.port}`; - - const customServerBuild = await runExecutor( - parseTargetString(options.customServerTarget, context.projectGraph), - { - watch: options.dev ? true : false, - }, - context ); - - for await (const result of customServerBuild) { - if (!result.success) { - return result; - } - yield { - success: true, - baseUrl, - }; - } - - return { success: true }; } diff --git a/packages/next/src/utils/config.ts b/packages/next/src/utils/config.ts index b1be952f4d..409a1b64bb 100644 --- a/packages/next/src/utils/config.ts +++ b/packages/next/src/utils/config.ts @@ -41,7 +41,7 @@ export function createWebpackConfig( ): Configuration { const mainFields = ['es2015', 'module', 'main']; const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx']; - let tsConfigPath = join(projectRoot, 'tsconfig.json'); + let tsConfigPath = join(workspaceRoot, projectRoot, 'tsconfig.json'); if (dependencies.length > 0) { tsConfigPath = createTmpTsConfig( join(workspaceRoot, tsConfigPath), diff --git a/packages/next/src/utils/create-cli-options.ts b/packages/next/src/utils/create-cli-options.ts new file mode 100644 index 0000000000..41c86b3a14 --- /dev/null +++ b/packages/next/src/utils/create-cli-options.ts @@ -0,0 +1,12 @@ +export function createCliOptions(obj: { [key: string]: any }): string { + return Object.entries(obj) + .reduce((arr, [key, value]) => { + if (value !== undefined) { + const kebabCase = key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()); + return `${arr}--${kebabCase}=${value} `; + } else { + return arr; + } + }, '') + .trim(); +} diff --git a/packages/next/src/utils/types.ts b/packages/next/src/utils/types.ts index 432b483fc2..211db13b4b 100644 --- a/packages/next/src/utils/types.ts +++ b/packages/next/src/utils/types.ts @@ -38,6 +38,8 @@ export interface NextBuildBuilderOptions { generateLockfile?: boolean; watch?: boolean; debug?: boolean; + profile?: boolean; + experimentalAppOnly?: boolean; } export interface NextServeBuilderOptions { @@ -50,6 +52,7 @@ export interface NextServeBuilderOptions { hostname?: string; proxyConfig?: string; buildLibsFromSource?: boolean; + keepAliveTimeout?: number; } export interface NextExportBuilderOptions {