feat(nextjs): Use next.js cli for build and serve targets (#16896)
This commit is contained in:
parent
7de80ddb62
commit
3d76d95b29
@ -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"],
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"],
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
"migrations": "./migrations.json"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": "^13.0.0"
|
||||
"next": ">=13.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-decorators": "^7.14.5",
|
||||
|
||||
@ -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<string> {
|
||||
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 {
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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
|
||||
|
||||
67
packages/next/src/executors/server/custom-server.impl.ts
Normal file
67
packages/next/src/executors/server/custom-server.impl.ts
Normal file
@ -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<NextBuildBuilderOptions>(
|
||||
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 };
|
||||
}
|
||||
@ -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"]
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
12
packages/next/src/utils/create-cli-options.ts
Normal file
12
packages/next/src/utils/create-cli-options.ts
Normal file
@ -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();
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user