next.js/packages/next/src/server/lib/utils.ts
utils.ts323 lines8.8 KB
import { parseArgs } from 'node:util'
import { InvalidArgumentError } from 'next/dist/compiled/commander'

export function printAndExit(message: string, code = 1) {
  if (code === 0) {
    console.log(message)
  } else {
    console.error(message)
  }

  return process.exit(code)
}

export type NodeOptions = Record<string, string | boolean | undefined>

const parseNodeArgs = (args: string[]): NodeOptions => {
  const { values, tokens } = parseArgs({ args, strict: false, tokens: true })

  // For the `NODE_OPTIONS`, we support arguments with values without the `=`
  // sign. We need to parse them manually.
  let orphan = null
  for (let i = 0; i < tokens.length; i++) {
    const token = tokens[i]

    if (token.kind === 'option-terminator') {
      break
    }

    // When we encounter an option, if it's value is undefined, we should check
    // to see if the following tokens are positional parameters. If they are,
    // then the option is orphaned, and we can assign it.
    if (token.kind === 'option') {
      orphan = typeof token.value === 'undefined' ? token : null
      continue
    }

    // If the token isn't a positional one, then we can't assign it to the found
    // orphaned option.
    if (token.kind !== 'positional') {
      orphan = null
      continue
    }

    // If we don't have an orphan, then we can skip this token.
    if (!orphan) {
      continue
    }

    // If the token is a positional one, and it has a value, so add it to the
    // values object. If it already exists, append it with a space.
    if (orphan.name in values && typeof values[orphan.name] === 'string') {
      values[orphan.name] += ` ${token.value}`
    } else {
      values[orphan.name] = token.value
    }
  }

  return values
}

/**
 * Tokenizes the arguments string into an array of strings, supporting quoted
 * values and escaped characters.
 * Converted from: https://github.com/nodejs/node/blob/c29d53c5cfc63c5a876084e788d70c9e87bed880/src/node_options.cc#L1401
 *
 * @param input The arguments string to be tokenized.
 * @returns An array of strings with the tokenized arguments.
 */
export const tokenizeArgs = (input: string): string[] => {
  let args: string[] = []
  let isInString = false
  let willStartNewArg = true

  for (let i = 0; i < input.length; i++) {
    let char = input[i]

    // Skip any escaped characters in strings.
    if (char === '\\' && isInString) {
      // Ensure we don't have an escape character at the end.
      if (input.length === i + 1) {
        throw new Error('Invalid escape character at the end.')
      }

      // Skip the next character.
      char = input[++i]
    }
    // If we find a space outside of a string, we should start a new argument.
    else if (char === ' ' && !isInString) {
      willStartNewArg = true
      continue
    }

    // If we find a quote, we should toggle the string flag.
    else if (char === '"') {
      isInString = !isInString
      continue
    }

    // If we're starting a new argument, we should add it to the array.
    if (willStartNewArg) {
      args.push(char)
      willStartNewArg = false
    }
    // Otherwise, add it to the last argument.
    else {
      args[args.length - 1] += char
    }
  }

  if (isInString) {
    throw new Error('Unterminated string')
  }

  return args
}

/**
 * Get the node options from the environment variable `NODE_OPTIONS` and returns
 * them as an array of strings.
 *
 * @returns An array of strings with the node options.
 */
export const getNodeOptionsArgs = () => {
  if (!process.env.NODE_OPTIONS) return []

  return tokenizeArgs(process.env.NODE_OPTIONS)
}

/**
 * The debug address is in the form of `[host:]port`. The host is optional.
 */
export interface DebugAddress {
  host: string | undefined
  port: number
}

/**
 * Formats the debug address into a string.
 */
export const formatDebugAddress = ({ host, port }: DebugAddress): string => {
  if (host) return `${host}:${port}`
  return `${port}`
}

/**
 * Get's the debug address from the `NODE_OPTIONS` environment variable. If the
 * address is not found, it returns the default host (`undefined`) and port
 * (`9229`).
 *
 * @returns An object with the host and port of the debug address.
 */
export const getParsedDebugAddress = (
  address: string | boolean | undefined
): DebugAddress => {
  if (!address || typeof address !== 'string') {
    return { host: undefined, port: 9229 }
  }

  // The address is in the form of `[host:]port`. Let's parse the address.
  if (address.includes(':')) {
    const [host, port] = address.split(':')
    return { host, port: parseInt(port, 10) }
  }

  return { host: undefined, port: parseInt(address, 10) }
}

/**
 * Node.js CLI flags that are not allowed in NODE_OPTIONS and must be
 * passed as direct CLI arguments via execArgv.
 * This set is the difference between all Node.js CLI flags and the ones **not**
 * allowed in NODE_OPTIONS, as listed in the Node.js documentation:
 * https://nodejs.org/api/cli.html#node_optionsoptions
 *
 * It is not exhaustive since not all options make sense for Next.js (e.g. --test)
 */
const EXEC_ARGV_ONLY_OPTIONS = new Set([
  'experimental-network-inspection',
  'experimental-storage-inspection',
  'experimental-worker-inspection',
  'experimental-inspector-network-resource',
])

function formatArg(
  key: string,
  value: string | boolean | undefined
): string | null {
  if (value === true) {
    return `--${key}`
  }

  if (value) {
    return `--${key}=${
      // Values with spaces need to be quoted. We use JSON.stringify to
      // also escape any nested quotes.
      value.includes(' ') && !value.startsWith('"')
        ? JSON.stringify(value)
        : value
    }`
  }

  return null
}

/**
 * Stringify the arguments to be used in a command line. It will ignore any
 * argument that has a value of `undefined`. Options that are not allowed in
 * NODE_OPTIONS are returned separately as execArgv.
 *
 * @param args The arguments to be stringified.
 * @returns An object with `nodeOptions` string and `execArgv` array.
 */
export function formatNodeOptions(
  args: Record<string, string | boolean | undefined>
): { nodeOptions: string; execArgv: string[] } {
  const nodeOptionsParts: string[] = []
  const execArgv: string[] = []

  for (const [key, value] of Object.entries(args)) {
    const formatted = formatArg(key, value)
    if (formatted === null) continue

    if (EXEC_ARGV_ONLY_OPTIONS.has(key)) {
      execArgv.push(formatted)
    } else {
      nodeOptionsParts.push(formatted)
    }
  }

  return { nodeOptions: nodeOptionsParts.join(' '), execArgv }
}

export function getParsedNodeOptions(): Record<
  string,
  string | boolean | undefined
> {
  const args = [...process.execArgv, ...getNodeOptionsArgs()]
  if (args.length === 0) return {}

  return parseNodeArgs(args)
}

/**
 * Get the node options from the `NODE_OPTIONS` environment variable and parse
 * them into an object without the inspect options.
 *
 * @returns An object with the parsed node options.
 */
export function getParsedNodeOptionsWithoutInspect() {
  const args = getNodeOptionsArgs()
  if (args.length === 0) return {}

  const parsed = parseNodeArgs(args)

  // Remove inspect options.
  delete parsed.inspect
  delete parsed['inspect-brk']
  delete parsed['inspect_brk']

  return parsed
}

/**
 * Get the node options from the `NODE_OPTIONS` environment variable and format
 * them into a string without the inspect options.
 *
 * @returns A string with the formatted node options.
 */
export function getFormattedNodeOptionsWithoutInspect() {
  const args = getParsedNodeOptionsWithoutInspect()
  if (Object.keys(args).length === 0) return ''

  return formatNodeOptions(args).nodeOptions
}

/**
 * Check if the value is a valid positive integer and parse it. If it's not, it will throw an error.
 *
 * @param value The value to be parsed.
 */
export function parseValidPositiveInteger(value: string): number {
  const parsedValue = parseInt(value, 10)

  if (isNaN(parsedValue) || !isFinite(parsedValue) || parsedValue < 0) {
    throw new InvalidArgumentError(`'${value}' is not a non-negative number.`)
  }
  return parsedValue
}

export const RESTART_EXIT_CODE = 77

export type NodeInspectType = 'inspect' | 'inspect-brk' | undefined

/**
 * Get the debug type from the `NODE_OPTIONS` environment variable.
 */
export function getNodeDebugType(nodeOptions: NodeOptions): NodeInspectType {
  if (nodeOptions.inspect) {
    return 'inspect'
  }
  if (nodeOptions['inspect-brk'] || nodeOptions['inspect_brk']) {
    return 'inspect-brk'
  }
}

/**
 * Get the `max-old-space-size` value from the `NODE_OPTIONS` environment
 * variable.
 *
 * @returns The value of the `max-old-space-size` option as a number.
 */
export function getMaxOldSpaceSize() {
  const args = getNodeOptionsArgs()
  if (args.length === 0) return

  const parsed = parseNodeArgs(args)

  const size = parsed['max-old-space-size'] || parsed['max_old_space_size']
  if (!size || typeof size !== 'string') return

  return parseInt(size, 10)
}
Quest for Codev2.0.0
/
SIGN IN