diff --git a/lib/utils/remaining-flags.ts b/lib/utils/remaining-flags.ts index cd40c4c8e..0da5b55e4 100644 --- a/lib/utils/remaining-flags.ts +++ b/lib/utils/remaining-flags.ts @@ -2,13 +2,16 @@ import { Command } from 'commander'; export function getRemainingFlags(cli: Command) { const rawArgs = [...(cli as any).rawArgs]; + + const spliceIndex = rawArgs.findIndex((item: string) => + item.startsWith('--'), + ); + if (spliceIndex === -1) { + return []; + } + return rawArgs - .splice( - Math.max( - rawArgs.findIndex((item: string) => item.startsWith('--')), - 0, - ), - ) + .splice(spliceIndex) .filter((item: string, index: number, array: string[]) => { // If the option is consumed by commander.js, then we skip it if (cli.options.find((o: any) => o.short === item || o.long === item)) { @@ -18,7 +21,7 @@ export function getRemainingFlags(cli: Command) { // If it's an argument of an option consumed by commander.js, then we // skip it too const prevKeyRaw = array[index - 1]; - if (prevKeyRaw) { + if (prevKeyRaw?.startsWith('-')) { const previousKey = camelCase( prevKeyRaw.replace(/--/g, '').replace('no', ''), ); @@ -40,10 +43,13 @@ export function getRemainingFlags(cli: Command) { */ function camelCase(flag: string) { - return flag - .split('-') - .filter((word) => word.length > 0) - .reduce((str, word) => { - return str + word[0].toUpperCase() + word.slice(1); - }); + const words = flag.split('-').filter((word) => word.length > 0); + + if (words.length === 0) { + return ''; + } + + return words.reduce((str, word) => { + return str + word[0].toUpperCase() + word.slice(1); + }); } diff --git a/test/e2e/helpers.ts b/test/e2e/helpers.ts index f5e4d5be5..bb92baf96 100644 --- a/test/e2e/helpers.ts +++ b/test/e2e/helpers.ts @@ -69,8 +69,9 @@ export function spawnNest( args: string, cwd?: string, env?: NodeJS.ProcessEnv, + cliPath?: string, ): { child: ChildProcess; output: () => string; kill: () => void } { - const child = spawn('node', [CLI_PATH, ...args.split(/\s+/)], { + const child = spawn('node', [cliPath ?? CLI_PATH, ...args.split(/\s+/)], { cwd: cwd ?? process.cwd(), env: { ...process.env, ...env }, stdio: ['pipe', 'pipe', 'pipe'], @@ -551,3 +552,22 @@ export function removeLocalCli(appPath: string): void { fs.rmSync(localCli, { recursive: true, force: true }); } } + +/** + * Creates a symlink for the nest cli script path, + * useful when testing different execution paths + * @param linkPath the desired symlink path + */ +export function createCliSymlink(linkPath: string) { + fs.symlinkSync(CLI_PATH, linkPath, 'file'); +} + +/** + * removes an existing link + * @param linkPath the link + */ +export function removelink(linkPath: string) { + if (fs.existsSync(linkPath)) { + fs.unlinkSync(linkPath); + } +} diff --git a/test/e2e/start.command.e2e-spec.ts b/test/e2e/start.command.e2e-spec.ts index e2e0b23f4..462d9f33e 100644 --- a/test/e2e/start.command.e2e-spec.ts +++ b/test/e2e/start.command.e2e-spec.ts @@ -1,5 +1,6 @@ import { execSync } from 'child_process'; import * as path from 'path'; +import * as crypto from 'crypto'; import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; import { convertToCjs, @@ -11,6 +12,7 @@ import { installWebpackDeps, readFileContent, removeLocalCli, + removelink, removeTempDir, runNest, runNestRaw, @@ -20,6 +22,7 @@ import { spawnNest, waitFor, writeFileContent, + createCliSymlink, } from './helpers.js'; describe('Start Command - CJS project (e2e)', () => { @@ -178,6 +181,35 @@ describe('Start Command - CJS project (e2e)', () => { } }); + it('should start when the cli path contains "-no-"', async () => { + const port = 4070; + + const randomChars = crypto.randomBytes(8).toString('hex'); + const pathWithNo = path.join(tmpDir, `nest-no-test-cli${randomChars}`); + + createCliSymlink(pathWithNo); + + const proc = spawnNest( + 'start', + appPath, + { PORT: String(port) }, + pathWithNo, + ); + + try { + await waitFor( + () => proc.output().includes('Nest application successfully started'), + 60_000, + ); + + const response = await httpGet(`http://127.0.0.1:${port}`); + expect(response.status).toBe(200); + } finally { + proc.kill(); + removelink(pathWithNo); + } + }); + describe('with SWC builder', () => { beforeAll(() => { execSync('npm install --save-dev @swc/cli @swc/core', { diff --git a/test/lib/utils/remaining-flags.spec.ts b/test/lib/utils/remaining-flags.spec.ts index 2504379dd..e71b491be 100644 --- a/test/lib/utils/remaining-flags.spec.ts +++ b/test/lib/utils/remaining-flags.spec.ts @@ -42,14 +42,29 @@ describe('getRemainingFlags', () => { expect(result).toEqual([]); }); - it('should return all raw args when no flags starting with "--" are present', () => { - // When no "--" flags exist, findIndex returns -1, Math.max(-1, 0) = 0, - // so splice(0) returns all elements from the rawArgs array + it('should return an empty array when no flags starting with "--" are present', () => { + // When no "--" flags exist, findIndex returns -1, which means no flags are present. + // terminate early when no flags are found const cmd = createCommand(['node', 'nest', 'start'], []); const result = getRemainingFlags(cmd); - expect(result).toEqual(['node', 'nest', 'start']); + expect(result).toEqual([]); + }); + + it('should not crash when flags containing -no- exist', () => { + const cmd = createCommand([ + 'node', + 'nest', + 'start', + '--watch', + '-no-', + 'value', + ]); + + const result = getRemainingFlags(cmd); + + expect(result).toEqual(['--watch', '-no-', 'value']); }); it('should filter out short option flags consumed by commander', () => {