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
@@ -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');