diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7552898bd0..36b4114efe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -203,7 +203,7 @@ jobs: env: CI: true - NODE_OPTIONS: --max-old-space-size=8192 --no-node-snapshot --experimental-vm-modules + NODE_OPTIONS: --max-old-space-size=8192 --experimental-vm-modules INTEGRATION_TEST_GITHUB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITHUB_TOKEN }} INTEGRATION_TEST_GITLAB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITLAB_TOKEN }} INTEGRATION_TEST_BITBUCKET_TOKEN: ${{ secrets.INTEGRATION_TEST_BITBUCKET_TOKEN }} diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index 933bbb0840..f63eb5d61b 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -28,7 +28,7 @@ jobs: env: CI: true - NODE_OPTIONS: --max-old-space-size=8192 --no-node-snapshot --experimental-vm-modules + NODE_OPTIONS: --max-old-space-size=8192 --experimental-vm-modules name: E2E Windows ${{ matrix.node-version }} steps: diff --git a/.github/workflows/verify_windows.yml b/.github/workflows/verify_windows.yml index ae2ec477d3..f7de04772d 100644 --- a/.github/workflows/verify_windows.yml +++ b/.github/workflows/verify_windows.yml @@ -21,7 +21,7 @@ jobs: env: CI: true - NODE_OPTIONS: --max-old-space-size=8192 --no-node-snapshot --experimental-vm-modules + NODE_OPTIONS: --max-old-space-size=8192 --experimental-vm-modules INTEGRATION_TEST_GITHUB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITHUB_TOKEN }} INTEGRATION_TEST_GITLAB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITLAB_TOKEN }} INTEGRATION_TEST_BITBUCKET_TOKEN: ${{ secrets.INTEGRATION_TEST_BITBUCKET_TOKEN }} diff --git a/docs/tooling/cli/02-build-system.md b/docs/tooling/cli/02-build-system.md index c8d9034955..25945f73dd 100644 --- a/docs/tooling/cli/02-build-system.md +++ b/docs/tooling/cli/02-build-system.md @@ -610,7 +610,7 @@ With that in mind, here are some IDEs configurations to run backstage components 1. Click on "Edit Configurations" on top panel 2. In the modal dialog click on link "Edit configuration templates..." located in the bottom left corner. 3. "Configuration file": leave empty (`backstage-cli` adds the config) - 4. "Node options": `--no-node-snapshot --experimental-vm-modules` + 4. "Node options": ` --experimental-vm-modules` 5. "Jest package": `~/workspace/backstage/node_modules/@backstage/cli` - the location of the backstage cli package. 6. "Working directory": `~/workspace/backstage` 7. "Jest Options": `repo test --runInBand --watch=false` diff --git a/packages/cli/src/modules/build/lib/runner/runBackend.test.ts b/packages/cli/src/modules/build/lib/runner/runBackend.test.ts index 49fb59ecd9..1e2c2190cf 100644 --- a/packages/cli/src/modules/build/lib/runner/runBackend.test.ts +++ b/packages/cli/src/modules/build/lib/runner/runBackend.test.ts @@ -15,6 +15,7 @@ */ import { runBackend } from './runBackend'; +import spawn from 'cross-spawn'; // Mock external dependencies jest.mock('chokidar', () => ({ @@ -51,8 +52,12 @@ jest.mock('ctrlc-windows', () => ({ describe('runBackend', () => { let originalEnv: NodeJS.ProcessEnv; let originalPlatform: string; + const mockSpawn = spawn as jest.MockedFunction; beforeEach(() => { + // Use fake timers to control debounce + jest.useFakeTimers(); + // Save original environment originalEnv = { ...process.env }; originalPlatform = process.platform; @@ -76,44 +81,56 @@ describe('runBackend', () => { }); jest.clearAllMocks(); + jest.useRealTimers(); }); - describe('NODE_OPTIONS environment variable', () => { - it('should add --no-node-snapshot when NODE_OPTIONS is not set', async () => { + describe('--no-node-snapshot argument handling', () => { + it('should pass --no-node-snapshot when NODE_OPTIONS is not set', () => { delete process.env.NODE_OPTIONS; runBackend({ entry: 'src/index', }); - expect(process.env.NODE_OPTIONS).toBe('--no-node-snapshot'); + // Fast-forward past the debounce delay (100ms) + jest.advanceTimersByTime(100); + + expect(mockSpawn).toHaveBeenCalled(); + const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; + expect(spawnArgs).toContain('--no-node-snapshot'); }); - it('should append --no-node-snapshot when NODE_OPTIONS exists without it', async () => { + it('should pass --no-node-snapshot when NODE_OPTIONS exists without --node-snapshot', () => { process.env.NODE_OPTIONS = '--max-old-space-size=4096'; runBackend({ entry: 'src/index', }); - expect(process.env.NODE_OPTIONS).toBe( - '--max-old-space-size=4096 --no-node-snapshot', - ); + // Fast-forward past the debounce delay (100ms) + jest.advanceTimersByTime(100); + + expect(mockSpawn).toHaveBeenCalled(); + const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; + expect(spawnArgs).toContain('--no-node-snapshot'); }); - it('should not add --no-node-snapshot when --node-snapshot already exists', async () => { + it('should not pass --no-node-snapshot when --node-snapshot already exists in NODE_OPTIONS', () => { process.env.NODE_OPTIONS = '--node-snapshot --max-old-space-size=4096'; runBackend({ entry: 'src/index', }); - expect(process.env.NODE_OPTIONS).toBe( - '--node-snapshot --max-old-space-size=4096', - ); + // Fast-forward past the debounce delay (100ms) + jest.advanceTimersByTime(100); + + expect(mockSpawn).toHaveBeenCalled(); + const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; + expect(spawnArgs).not.toContain('--no-node-snapshot'); }); - it('should not add --no-node-snapshot when --node-snapshot exists in the middle of NODE_OPTIONS', async () => { + it('should not pass --no-node-snapshot when --node-snapshot exists in the middle of NODE_OPTIONS', () => { process.env.NODE_OPTIONS = '--max-old-space-size=4096 --node-snapshot --inspect'; @@ -121,26 +138,49 @@ describe('runBackend', () => { entry: 'src/index', }); - expect(process.env.NODE_OPTIONS).toBe( - '--max-old-space-size=4096 --node-snapshot --inspect', - ); + // Fast-forward past the debounce delay (100ms) + jest.advanceTimersByTime(100); + + expect(mockSpawn).toHaveBeenCalled(); + const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; + expect(spawnArgs).not.toContain('--no-node-snapshot'); }); - it('should handle NODE_OPTIONS with trailing spaces', async () => { + it('should pass --no-node-snapshot even with trailing spaces in NODE_OPTIONS', () => { process.env.NODE_OPTIONS = '--max-old-space-size=4096 '; runBackend({ entry: 'src/index', }); - expect(process.env.NODE_OPTIONS).toBe( - '--max-old-space-size=4096 --no-node-snapshot', - ); + // Fast-forward past the debounce delay (100ms) + jest.advanceTimersByTime(100); + + expect(mockSpawn).toHaveBeenCalled(); + const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; + expect(spawnArgs).toContain('--no-node-snapshot'); + }); + + it('should pass --no-node-snapshot alongside other option args like --inspect', () => { + delete process.env.NODE_OPTIONS; + + runBackend({ + entry: 'src/index', + inspectEnabled: true, + }); + + // Fast-forward past the debounce delay (100ms) + jest.advanceTimersByTime(100); + + expect(mockSpawn).toHaveBeenCalled(); + const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; + expect(spawnArgs).toContain('--no-node-snapshot'); + expect(spawnArgs).toContain('--inspect'); }); }); describe('NODE_ENV environment variable', () => { - it('should set NODE_ENV to development when not set', async () => { + it('should set NODE_ENV to development when not set', () => { delete process.env.NODE_ENV; runBackend({ @@ -150,7 +190,7 @@ describe('runBackend', () => { expect(process.env.NODE_ENV).toBe('development'); }); - it('should not override existing NODE_ENV', async () => { + it('should not override existing NODE_ENV', () => { process.env.NODE_ENV = 'production'; runBackend({ @@ -160,30 +200,4 @@ describe('runBackend', () => { expect(process.env.NODE_ENV).toBe('production'); }); }); - - describe('combined environment setup', () => { - it('should set both NODE_ENV and NODE_OPTIONS when neither is set', async () => { - delete process.env.NODE_ENV; - delete process.env.NODE_OPTIONS; - - runBackend({ - entry: 'src/index', - }); - - expect(process.env.NODE_ENV).toBe('development'); - expect(process.env.NODE_OPTIONS).toBe('--no-node-snapshot'); - }); - - it('should handle both environment variables independently', async () => { - process.env.NODE_ENV = 'test'; - process.env.NODE_OPTIONS = '--inspect'; - - runBackend({ - entry: 'src/index', - }); - - expect(process.env.NODE_ENV).toBe('test'); - expect(process.env.NODE_OPTIONS).toBe('--inspect --no-node-snapshot'); - }); - }); }); diff --git a/packages/cli/src/modules/build/lib/runner/runBackend.ts b/packages/cli/src/modules/build/lib/runner/runBackend.ts index d48fb34fc2..a3072c5dd7 100644 --- a/packages/cli/src/modules/build/lib/runner/runBackend.ts +++ b/packages/cli/src/modules/build/lib/runner/runBackend.ts @@ -52,14 +52,6 @@ export async function runBackend(options: RunBackendOptions) { envEnv.NODE_ENV = 'development'; } - // Unless the user explicitly toggles node-snapshot, default to provide --no-node-snapshot to reduce number of steps to run scaffolder - // on Node LTS. - if (!envEnv.NODE_OPTIONS?.includes('--node-snapshot')) { - envEnv.NODE_OPTIONS = - (envEnv.NODE_OPTIONS ? envEnv.NODE_OPTIONS + ' ' : '') + - '--no-node-snapshot'; - } - // Set up the parent IPC server and bind the available services const server = new IpcServer(); ServerDataStore.bind(server); @@ -123,6 +115,12 @@ export async function runBackend(options: RunBackendOptions) { } } + // Unless the user explicitly toggles node-snapshot, default to provide --no-node-snapshot to reduce number of steps to run scaffolder + // on Node LTS. + if (!envEnv.NODE_OPTIONS?.includes('--node-snapshot')) { + optionArgs.push('--no-node-snapshot'); + } + const userArgs = process.argv .slice(['node', 'backstage-cli', 'package', 'start'].length) .filter(arg => !optionArgs.includes(arg));