diff --git a/packages/next/src/server/lib/utils.test.ts b/packages/next/src/server/lib/utils.test.ts index fca2e0d7c5eae7..8fc199d834338d 100644 --- a/packages/next/src/server/lib/utils.test.ts +++ b/packages/next/src/server/lib/utils.test.ts @@ -2,6 +2,7 @@ import { getFormattedNodeOptionsWithoutInspect, getParsedDebugAddress, formatNodeOptions, + tokenizeArgs, } from './utils' const originalNodeOptions = process.env.NODE_OPTIONS @@ -10,6 +11,34 @@ afterAll(() => { process.env.NODE_OPTIONS = originalNodeOptions }) +describe('tokenizeArgs', () => { + it('splits arguments by spaces', () => { + const result = tokenizeArgs('--spaces "thing with spaces" --normal 1234') + + expect(result).toEqual([ + '--spaces', + 'thing with spaces', + '--normal', + '1234', + ]) + }) + + it('supports quoted values', () => { + const result = tokenizeArgs( + '--spaces "thing with spaces" --spacesAndQuotes "thing with \\"spaces\\"" --normal 1234' + ) + + expect(result).toEqual([ + '--spaces', + 'thing with spaces', + '--spacesAndQuotes', + 'thing with "spaces"', + '--normal', + '1234', + ]) + }) +}) + describe('formatNodeOptions', () => { it('wraps values with spaces in quotes', () => { const result = formatNodeOptions({ @@ -63,6 +92,16 @@ describe('getFormattedNodeOptionsWithoutInspect', () => { ) }) + it('handles options with quotes', () => { + process.env.NODE_OPTIONS = + '--require "./file with spaces to-require-with-node-require-option.js"' + const result = getFormattedNodeOptionsWithoutInspect() + + expect(result).toBe( + '--require="./file with spaces to-require-with-node-require-option.js"' + ) + }) + it('removes --inspect option with parameters', () => { process.env.NODE_OPTIONS = '--other --inspect=0.0.0.0:1234 --additional' const result = getFormattedNodeOptionsWithoutInspect() diff --git a/packages/next/src/server/lib/utils.ts b/packages/next/src/server/lib/utils.ts index b5dc16a18e7890..d46f36bed8a918 100644 --- a/packages/next/src/server/lib/utils.ts +++ b/packages/next/src/server/lib/utils.ts @@ -56,14 +56,72 @@ const parseNodeArgs = (args: string[]) => { return values } +/** + * Tokenizes the arguments string into an array of strings, supporting quoted + * values and escaped characters. + * + * @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. */ -const getNodeOptionsArgs = () => - process.env.NODE_OPTIONS?.split(' ').map((arg) => arg.trim()) ?? [] +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.