Merge pull request #32075 from backstage/sennyeya/node-snapshot

feat(cli): provide --no-node-snapshot by default
This commit is contained in:
Patrik Oldsberg
2025-12-12 16:54:56 +01:00
committed by GitHub
9 changed files with 210 additions and 5 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': minor
---
Provide `--no-node-snapshot` by default when running the `package start` or `package test`. You can disable this behavior by providing `NODE_OPTIONS='--node-snapshot'`.
+1 -1
View File
@@ -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 }}
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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 }}
+1 -1
View File
@@ -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`
@@ -0,0 +1,178 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { runBackend } from './runBackend';
import spawn from 'cross-spawn';
// Mock external dependencies
jest.mock('chokidar', () => ({
watch: jest.fn(() => ({
on: jest.fn().mockReturnThis(),
add: jest.fn(),
})),
}));
jest.mock('cross-spawn', () =>
jest.fn(() => ({
on: jest.fn().mockReturnThis(),
once: jest.fn().mockReturnThis(),
kill: jest.fn(),
killed: false,
exitCode: null,
pid: 12345,
})),
);
jest.mock('../ipc', () => ({
IpcServer: jest.fn().mockImplementation(() => ({
addChild: jest.fn(),
})),
ServerDataStore: {
bind: jest.fn(),
},
}));
jest.mock('ctrlc-windows', () => ({
ctrlc: jest.fn(),
}));
describe('runBackend', () => {
let originalEnv: NodeJS.ProcessEnv;
let originalPlatform: string;
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
beforeEach(() => {
// Use fake timers to control debounce
jest.useFakeTimers();
// Save original environment
originalEnv = { ...process.env };
process.env = { NODE_ENV: 'test' };
originalPlatform = process.platform;
// Mock process.stdin.on to prevent actual stdin reading
jest.spyOn(process.stdin, 'on').mockReturnValue(process.stdin);
// Mock process.once to prevent actual signal handling
jest.spyOn(process, 'once').mockReturnValue(process);
});
afterEach(() => {
// Restore original environment
process.env = originalEnv;
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
jest.clearAllMocks();
jest.useRealTimers();
});
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',
});
// 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 when NODE_OPTIONS exists without --node-snapshot', () => {
process.env.NODE_OPTIONS = '--max-old-space-size=4096';
runBackend({
entry: 'src/index',
});
// 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 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',
});
// 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 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';
runBackend({
entry: 'src/index',
});
// 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 pass --no-node-snapshot even with trailing spaces in NODE_OPTIONS', () => {
process.env.NODE_OPTIONS = '--max-old-space-size=4096 ';
runBackend({
entry: 'src/index',
});
// 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');
});
});
});
@@ -47,7 +47,7 @@ export type RunBackendOptions = {
};
export async function runBackend(options: RunBackendOptions) {
const envEnv = process.env as { NODE_ENV: string };
const envEnv = process.env as { NODE_ENV: string; NODE_OPTIONS?: string };
if (!envEnv.NODE_ENV) {
envEnv.NODE_ENV = 'development';
}
@@ -115,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));
@@ -78,6 +78,14 @@ export default async (_opts: OptionValues, cmd: Command) => {
process.env.TZ = 'UTC';
}
// 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 (!process.env.NODE_OPTIONS?.includes('--node-snapshot')) {
process.env.NODE_OPTIONS = `${
process.env.NODE_OPTIONS ? `${process.env.NODE_OPTIONS} ` : ''
}--no-node-snapshot`;
}
// This ensures that the process doesn't exit too early before stdout is flushed
if (args.includes('--help')) {
(process.stdout as any)._handle.setBlocking(true);
@@ -289,6 +289,14 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
process.env.TZ = 'UTC';
}
// 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 (!process.env.NODE_OPTIONS?.includes('--node-snapshot')) {
process.env.NODE_OPTIONS = `${
process.env.NODE_OPTIONS ? `${process.env.NODE_OPTIONS} ` : ''
}--no-node-snapshot`;
}
// This ensures that the process doesn't exit too early before stdout is flushed
if (args.includes('--jest-help')) {
removeOptionArg(args, '--jest-help');