| | #!/usr/bin/env node |
| |
|
| | import '../server/lib/cpu-profile' |
| | import { saveCpuProfile } from '../server/lib/cpu-profile' |
| | import type { StartServerOptions } from '../server/lib/start-server' |
| | import { |
| | RESTART_EXIT_CODE, |
| | getNodeDebugType, |
| | getParsedDebugAddress, |
| | getMaxOldSpaceSize, |
| | printAndExit, |
| | formatNodeOptions, |
| | formatDebugAddress, |
| | getParsedNodeOptions, |
| | type DebugAddress, |
| | } from '../server/lib/utils' |
| | import * as Log from '../build/output/log' |
| | import { getProjectDir } from '../lib/get-project-dir' |
| | import path from 'path' |
| | import { traceGlobals } from '../trace/shared' |
| | import { Telemetry } from '../telemetry/storage' |
| | import { findPagesDir } from '../lib/find-pages-dir' |
| | import { fileExists, FileType } from '../lib/file-exists' |
| | import { getNpxCommand } from '../lib/helpers/get-npx-command' |
| | import { createSelfSignedCertificate } from '../lib/mkcert' |
| | import type { SelfSignedCertificate } from '../lib/mkcert' |
| | import uploadTrace from '../trace/upload-trace' |
| | import { initialEnv } from '@next/env' |
| | import { fork } from 'child_process' |
| | import type { ChildProcess } from 'child_process' |
| | import { |
| | getReservedPortExplanation, |
| | isPortIsReserved, |
| | } from '../lib/helpers/get-reserved-port' |
| | import os from 'os' |
| | import { once } from 'node:events' |
| | import { clearTimeout } from 'timers' |
| | import { flushAllTraces, trace } from '../trace' |
| | import { traceId } from '../trace/shared' |
| | import { Bundler, parseBundlerArgs } from '../lib/bundler' |
| |
|
| | export type NextDevOptions = { |
| | disableSourceMaps: boolean |
| | |
| | inspect?: DebugAddress | true |
| | turbo?: boolean |
| | turbopack?: boolean |
| | webpack?: boolean |
| | port: number |
| | hostname?: string |
| | experimentalHttps?: boolean |
| | experimentalHttpsKey?: string |
| | experimentalHttpsCert?: string |
| | experimentalHttpsCa?: string |
| | experimentalUploadTrace?: string |
| | experimentalNextConfigStripTypes?: boolean |
| | experimentalCpuProf?: boolean |
| | } |
| |
|
| | type PortSource = 'cli' | 'default' | 'env' |
| |
|
| | let dir: string |
| | let child: undefined | ChildProcess |
| | |
| | let distDir: string | undefined |
| | let isTurbopack: boolean |
| | let traceUploadUrl: string |
| | let sessionStopHandled = false |
| | const sessionStarted = Date.now() |
| | const sessionSpan = trace('next-dev') |
| |
|
| | |
| | |
| | const CHILD_EXIT_TIMEOUT_MS = parseInt( |
| | process.env.NEXT_EXIT_TIMEOUT_MS ?? '100', |
| | 10 |
| | ) |
| |
|
| | const handleSessionStop = async (signal: NodeJS.Signals | number | null) => { |
| | if (signal != null && child?.pid) child.kill(signal) |
| | if (sessionStopHandled) return |
| | sessionStopHandled = true |
| |
|
| | |
| | |
| | const exitCode = child?.exitCode || 0 |
| |
|
| | if ( |
| | signal != null && |
| | child?.pid && |
| | child.exitCode === null && |
| | child.signalCode === null |
| | ) { |
| | let exitTimeout = setTimeout(() => { |
| | child?.kill('SIGKILL') |
| | }, CHILD_EXIT_TIMEOUT_MS) |
| | await once(child, 'exit').catch(() => {}) |
| | clearTimeout(exitTimeout) |
| | } |
| |
|
| | sessionSpan.stop() |
| | await flushAllTraces({ end: true }) |
| |
|
| | try { |
| | const { eventCliSessionStopped } = |
| | require('../telemetry/events/session-stopped') as typeof import('../telemetry/events/session-stopped') |
| |
|
| | let pagesDir: boolean = !!traceGlobals.get('pagesDir') |
| | let appDir: boolean = !!traceGlobals.get('appDir') |
| |
|
| | if ( |
| | typeof traceGlobals.get('pagesDir') === 'undefined' || |
| | typeof traceGlobals.get('appDir') === 'undefined' |
| | ) { |
| | const pagesResult = findPagesDir(dir) |
| | appDir = !!pagesResult.appDir |
| | pagesDir = !!pagesResult.pagesDir |
| | } |
| |
|
| | let telemetry = |
| | (traceGlobals.get('telemetry') as InstanceType< |
| | typeof import('../telemetry/storage').Telemetry |
| | >) || |
| | new Telemetry({ |
| | distDir: path.join(dir, distDir || '.next'), |
| | }) |
| |
|
| | telemetry.record( |
| | eventCliSessionStopped({ |
| | cliCommand: 'dev', |
| | turboFlag: isTurbopack, |
| | durationMilliseconds: Date.now() - sessionStarted, |
| | pagesDir, |
| | appDir, |
| | }), |
| | true |
| | ) |
| | telemetry.flushDetached('dev', dir) |
| | } catch (_) { |
| | |
| | |
| | } |
| |
|
| | if (traceUploadUrl && distDir) { |
| | uploadTrace({ |
| | traceUploadUrl, |
| | mode: 'dev', |
| | projectDir: dir, |
| | distDir, |
| | isTurboSession: isTurbopack, |
| | }) |
| | } |
| |
|
| | |
| | saveCpuProfile() |
| |
|
| | |
| | |
| | process.stdout.write('\x1B[?25h') |
| | process.stdout.write('\n') |
| | process.exit(exitCode) |
| | } |
| |
|
| | process.on('SIGINT', () => handleSessionStop('SIGINT')) |
| | process.on('SIGTERM', () => handleSessionStop('SIGTERM')) |
| |
|
| | |
| | process.on('exit', () => child?.kill('SIGKILL')) |
| |
|
| | const nextDev = async ( |
| | options: NextDevOptions, |
| | portSource: PortSource, |
| | directory?: string |
| | ) => { |
| | |
| | |
| | isTurbopack = parseBundlerArgs(options) === Bundler.Turbopack |
| |
|
| | dir = getProjectDir(process.env.NEXT_PRIVATE_DEV_DIR || directory) |
| |
|
| | |
| | if (!(await fileExists(dir, FileType.Directory))) { |
| | printAndExit(`> No such directory exists as the project root: ${dir}`) |
| | } |
| |
|
| | if (options.experimentalCpuProf) { |
| | Log.info( |
| | `CPU profiling enabled. Profile will be saved to .next/cpu-profiles/ on exit (Ctrl+C).` |
| | ) |
| | } |
| |
|
| | async function preflight(skipOnReboot: boolean) { |
| | const { getPackageVersion, getDependencies } = (await Promise.resolve( |
| | require('../lib/get-package-version') as typeof import('../lib/get-package-version') |
| | )) as typeof import('../lib/get-package-version') |
| |
|
| | const [sassVersion, nodeSassVersion] = await Promise.all([ |
| | getPackageVersion({ cwd: dir, name: 'sass' }), |
| | getPackageVersion({ cwd: dir, name: 'node-sass' }), |
| | ]) |
| | if (sassVersion && nodeSassVersion) { |
| | Log.warn( |
| | 'Your project has both `sass` and `node-sass` installed as dependencies, but should only use one or the other. ' + |
| | 'Please remove the `node-sass` dependency from your project. ' + |
| | ' Read more: https://nextjs.org/docs/messages/duplicate-sass' |
| | ) |
| | } |
| |
|
| | if (!skipOnReboot) { |
| | const { dependencies, devDependencies } = await getDependencies({ |
| | cwd: dir, |
| | }) |
| |
|
| | |
| | if ( |
| | dependencies['@next/font'] || |
| | (devDependencies['@next/font'] && |
| | devDependencies['@next/font'] !== 'workspace:*') |
| | ) { |
| | const command = getNpxCommand(dir) |
| | Log.warn( |
| | 'Your project has `@next/font` installed as a dependency, please use the built-in `next/font` instead. ' + |
| | 'The `@next/font` package will be removed in Next.js 14. ' + |
| | `You can migrate by running \`${command} @next/codemod@latest built-in-next-font .\`. Read more: https://nextjs.org/docs/messages/built-in-next-font` |
| | ) |
| | } |
| | } |
| | } |
| |
|
| | let port = options.port |
| |
|
| | if (isPortIsReserved(port)) { |
| | printAndExit(getReservedPortExplanation(port), 1) |
| | } |
| |
|
| | |
| | const allowRetry = portSource === 'default' |
| |
|
| | |
| | |
| | const host = options.hostname |
| |
|
| | if ( |
| | options.experimentalUploadTrace && |
| | !process.env.NEXT_TRACE_UPLOAD_DISABLED |
| | ) { |
| | traceUploadUrl = options.experimentalUploadTrace |
| | } |
| |
|
| | const devServerOptions: StartServerOptions = { |
| | dir, |
| | port, |
| | allowRetry, |
| | isDev: true, |
| | hostname: host, |
| | } |
| |
|
| | const startServerPath = require.resolve('../server/lib/start-server') |
| |
|
| | async function startServer(startServerOptions: StartServerOptions) { |
| | return new Promise<void>((resolve) => { |
| | let resolved = false |
| | const defaultEnv = (initialEnv || process.env) as typeof process.env |
| |
|
| | const nodeOptions = getParsedNodeOptions() |
| |
|
| | let maxOldSpaceSize: string | number | undefined = getMaxOldSpaceSize() |
| | if (!maxOldSpaceSize && !process.env.NEXT_DISABLE_MEM_OVERRIDE) { |
| | const totalMem = os.totalmem() |
| | const totalMemInMB = Math.floor(totalMem / 1024 / 1024) |
| | maxOldSpaceSize = Math.floor(totalMemInMB * 0.5).toString() |
| |
|
| | nodeOptions['max-old-space-size'] = maxOldSpaceSize |
| |
|
| | |
| | delete nodeOptions['max_old_space_size'] |
| | } |
| |
|
| | if (options.disableSourceMaps) { |
| | delete nodeOptions['enable-source-maps'] |
| | } else { |
| | nodeOptions['enable-source-maps'] = true |
| | } |
| |
|
| | const nodeDebugType = getNodeDebugType(nodeOptions) |
| | const originalAddress = |
| | nodeDebugType === undefined ? undefined : nodeOptions[nodeDebugType] |
| | delete nodeOptions.inspect |
| | delete nodeOptions['inspect-brk'] |
| | delete nodeOptions['inspect_brk'] |
| | if (nodeDebugType !== undefined) { |
| | const address = getParsedDebugAddress(originalAddress) |
| | address.port = address.port === 0 ? 0 : address.port + 1 |
| | nodeOptions[nodeDebugType] = formatDebugAddress(address) |
| | } else if (options.inspect) { |
| | const address: DebugAddress = |
| | options.inspect === true |
| | ? getParsedDebugAddress(true) |
| | : options.inspect |
| | nodeOptions.inspect = formatDebugAddress(address) |
| | } |
| |
|
| | child = fork(startServerPath, { |
| | stdio: 'inherit', |
| | env: { |
| | ...defaultEnv, |
| | ...(isTurbopack ? { TURBOPACK: process.env.TURBOPACK } : undefined), |
| | NEXT_PRIVATE_START_TIME: process.env.NEXT_PRIVATE_START_TIME, |
| | NEXT_PRIVATE_WORKER: '1', |
| | NEXT_PRIVATE_TRACE_ID: traceId, |
| | NODE_EXTRA_CA_CERTS: startServerOptions.selfSignedCertificate |
| | ? startServerOptions.selfSignedCertificate.rootCA |
| | : defaultEnv.NODE_EXTRA_CA_CERTS, |
| | NODE_OPTIONS: formatNodeOptions(nodeOptions), |
| | |
| | |
| | |
| | WATCHPACK_WATCHER_LIMIT: |
| | os.platform() === 'darwin' ? '20' : undefined, |
| | |
| | ...(options.experimentalCpuProf |
| | ? { |
| | NEXT_CPU_PROF: '1', |
| | NEXT_CPU_PROF_DIR: path.join(dir, '.next', 'cpu-profiles'), |
| | __NEXT_PRIVATE_CPU_PROFILE: 'dev-server', |
| | } |
| | : undefined), |
| | }, |
| | }) |
| |
|
| | child.on('message', (msg: any) => { |
| | if (msg && typeof msg === 'object') { |
| | if (msg.nextWorkerReady) { |
| | child?.send({ nextWorkerOptions: startServerOptions }) |
| | } else if (msg.nextServerReady && !resolved) { |
| | if (msg.port) { |
| | |
| | |
| | port = parseInt(msg.port, 10) |
| | } |
| | if (msg.distDir) { |
| | |
| | distDir = msg.distDir |
| | } |
| |
|
| | resolved = true |
| | resolve() |
| | } |
| | } |
| | }) |
| |
|
| | child.on('exit', async (code, signal) => { |
| | if (sessionStopHandled || signal) { |
| | return |
| | } |
| | if (code === RESTART_EXIT_CODE) { |
| | |
| | |
| | |
| | if (traceUploadUrl && distDir) { |
| | uploadTrace({ |
| | traceUploadUrl, |
| | mode: 'dev', |
| | projectDir: dir, |
| | distDir, |
| | isTurboSession: isTurbopack, |
| | sync: true, |
| | }) |
| | } |
| |
|
| | return startServer({ ...startServerOptions, port }) |
| | } |
| | |
| | |
| | await handleSessionStop( null) |
| | }) |
| | }) |
| | } |
| |
|
| | const runDevServer = async (reboot: boolean) => { |
| | try { |
| | if (!!options.experimentalHttps) { |
| | Log.warn( |
| | 'Self-signed certificates are currently an experimental feature, use with caution.' |
| | ) |
| |
|
| | let certificate: SelfSignedCertificate | undefined |
| |
|
| | const key = options.experimentalHttpsKey |
| | const cert = options.experimentalHttpsCert |
| | const rootCA = options.experimentalHttpsCa |
| |
|
| | if (key && cert) { |
| | certificate = { |
| | key: path.resolve(key), |
| | cert: path.resolve(cert), |
| | rootCA: rootCA ? path.resolve(rootCA) : undefined, |
| | } |
| | } else { |
| | certificate = await createSelfSignedCertificate(host) |
| | } |
| |
|
| | await startServer({ |
| | ...devServerOptions, |
| | selfSignedCertificate: certificate, |
| | }) |
| | } else { |
| | await startServer(devServerOptions) |
| | } |
| |
|
| | await preflight(reboot) |
| | } catch (err) { |
| | console.error(err) |
| | process.exit(1) |
| | } |
| | } |
| |
|
| | await runDevServer(false) |
| | } |
| |
|
| | export { nextDev } |
| |
|