diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index db5c627444d..25de0c943a8 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -21,6 +21,12 @@ Install Node 24, pnpm, and the Bun version in [`config/.bun-version`](../config/ native PTYs, socket liveness, and V8 memory behavior. `pnpm test:node` runs the same suites entirely on Node. Builds and dependency installation still use Node and pnpm. +The server, its workers and its packaged dependencies target Node 24. The compatibility +launcher and CLI still target Node 18; the launcher hands off to bundled Node 24 before +loading server code. WSL guest programs and host discovery retain their Node 18 minimum. +Keep Node 24 APIs and server imports out of these host compatibility paths. Running an +unpackaged server directly requires Node 24 or newer. + Use the pnpm version in `packageManager` in [`package.json`](../package.json): `corepack enable` needs Corepack 0.34.5 or newer to start the pinned pnpm (`npm install -g corepack@latest`). On Windows it writes shims next to `node.exe` and fails with `EPERM` outside an elevated shell when diff --git a/config/scripts/build-orcad.mjs b/config/scripts/build-orcad.mjs index 8f467d551d5..e00b67c8105 100644 --- a/config/scripts/build-orcad.mjs +++ b/config/scripts/build-orcad.mjs @@ -6,6 +6,7 @@ import { buildOrcadEntry, buildOrcadCli, buildOrcadLauncher, + ORCAD_SERVER_BUILD_TARGET, ORCAD_BUNDLE_PLUGINS, ORCAD_EXTERNAL_MODULES, ORCAD_CHILD_ENTRY_POINTS, @@ -117,7 +118,7 @@ async function stageParcelWatcher(target) { }, bundle: true, platform: 'node', - target: 'node18', + target: ORCAD_SERVER_BUILD_TARGET, format: 'cjs', outfile: join(OUT_DIR, ORCAD_PARCEL_WATCHER_ENTRY), external: ['./watcher.node'], @@ -222,7 +223,7 @@ function buildForkedChild(entryPoint, outfile) { entryPoints: [entryPoint], bundle: true, platform: 'node', - target: 'node18', + target: ORCAD_SERVER_BUILD_TARGET, format: 'cjs', outfile, external: ORCAD_EXTERNAL_MODULES, diff --git a/config/scripts/orcad-entry-build.mjs b/config/scripts/orcad-entry-build.mjs index e07a2c55270..9f45f38dff8 100644 --- a/config/scripts/orcad-entry-build.mjs +++ b/config/scripts/orcad-entry-build.mjs @@ -10,6 +10,9 @@ const { turnedOnDarkBuildDefines } = createRequire(import.meta.url)('../dark-bui export const ORCAD_ENTRY_POINT = 'src/main/orcad/main.ts' export const ORCAD_LAUNCHER_ENTRY_POINT = 'src/main/orcad/launcher.ts' +export const ORCAD_SERVER_BUILD_TARGET = 'node24' +// Host entry points must load before Orca selects its bundled runtime. +const ORCAD_COMPAT_BUILD_TARGET = 'node18' export const ORCAD_CHILD_ENTRY_POINTS = { watcher: 'src/main/ipc/filesystem/watcher-process/parcel-watcher-process-entry.ts', daemon: 'src/main/daemon/daemon-entry.ts', @@ -141,7 +144,7 @@ function buildOrcadEntryBundle(outfile, minify) { entryPoints: [join(root, ORCAD_ENTRY_POINT)], bundle: true, platform: 'node', - target: 'node18', + target: ORCAD_SERVER_BUILD_TARGET, format: 'cjs', outfile, external: ORCAD_EXTERNAL_MODULES, @@ -164,7 +167,7 @@ export function buildOrcadCli(outfile) { entryPoints: [join(root, 'src/cli/index.ts')], bundle: true, platform: 'node', - target: 'node18', + target: ORCAD_COMPAT_BUILD_TARGET, format: 'cjs', outfile, plugins: ORCAD_BUNDLE_PLUGINS, @@ -184,7 +187,7 @@ export function buildOrcadLauncher(outfile) { entryPoints: [join(root, ORCAD_LAUNCHER_ENTRY_POINT)], bundle: true, platform: 'node', - target: 'node18', + target: ORCAD_COMPAT_BUILD_TARGET, format: 'cjs', outfile, metafile: true, diff --git a/config/scripts/orcad-linear-sdk-package.mjs b/config/scripts/orcad-linear-sdk-package.mjs index 9426cb2bb8c..abcc17932dd 100644 --- a/config/scripts/orcad-linear-sdk-package.mjs +++ b/config/scripts/orcad-linear-sdk-package.mjs @@ -1,6 +1,7 @@ import { build } from 'esbuild' import { join } from 'node:path' import { ORCAD_LINEAR_SDK_ENTRY } from '../../src/shared/orcad-artifacts.ts' +import { ORCAD_SERVER_BUILD_TARGET } from './orcad-entry-build.mjs' // Bundle @linear/sdk into one CJS file for lazy require: copying would expose chunk names that change each release. export async function stageOrcadLinearSdk(root, outputDir) { @@ -12,7 +13,7 @@ export async function stageOrcadLinearSdk(root, outputDir) { }, bundle: true, platform: 'node', - target: 'node18', + target: ORCAD_SERVER_BUILD_TARGET, format: 'cjs', outfile: join(outputDir, ORCAD_LINEAR_SDK_ENTRY), minify: true, diff --git a/src/main/orcad/orcad-bundled-runtime.test.ts b/src/main/orcad/orcad-bundled-runtime.test.ts index ae7ebacb7cd..1aec77d7a04 100644 --- a/src/main/orcad/orcad-bundled-runtime.test.ts +++ b/src/main/orcad/orcad-bundled-runtime.test.ts @@ -244,21 +244,21 @@ describe('bundled Orca runtime handoff', () => { }) describe('server runtime admission', () => { - it.each(['14.21.3', '16.20.2'])('refuses host Node %s before loading profile state', (node) => { - fixture.exists.mockReturnValue(false) - vi.spyOn(process, 'versions', 'get').mockReturnValue({ ...process.versions, node }) - expect(() => assertOrcadServerRuntime()).toThrow('requires Node.js 18') - }) - - it.each(['18.20.8', '20.19.0', '22.14.0', '23.0.0', '24.0.0', '26.0.0'])( - 'accepts unpackaged host Node %s', + it.each(['14.21.3', '16.20.2', '18.20.8', '20.19.0', '22.14.0', '23.0.0'])( + 'refuses host Node %s before loading profile state', (node) => { fixture.exists.mockReturnValue(false) vi.spyOn(process, 'versions', 'get').mockReturnValue({ ...process.versions, node }) - expect(() => assertOrcadServerRuntime()).not.toThrow() + expect(() => assertOrcadServerRuntime()).toThrow('requires Node.js 24') } ) + it.each(['24.0.0', '26.0.0'])('accepts unpackaged host Node %s', (node) => { + fixture.exists.mockReturnValue(false) + vi.spyOn(process, 'versions', 'get').mockReturnValue({ ...process.versions, node }) + expect(() => assertOrcadServerRuntime()).not.toThrow() + }) + it('refuses host Node 24 for a packaged slot that owns a bundled runtime', () => { vi.spyOn(process, 'versions', 'get').mockReturnValue({ ...process.versions, node: '24.0.0' }) expect(() => assertOrcadServerRuntime()).toThrow('Start the Orca server with its bundled') diff --git a/src/main/orcad/orcad-bundled-runtime.ts b/src/main/orcad/orcad-bundled-runtime.ts index 6b469afbd4b..59785312057 100644 --- a/src/main/orcad/orcad-bundled-runtime.ts +++ b/src/main/orcad/orcad-bundled-runtime.ts @@ -61,8 +61,8 @@ export function isRunningAsBundledOrcadRuntime(directory: string): boolean { /** Reject substituted runtimes before opening profile state. */ export function assertOrcadServerRuntime(): void { - if (Number(process.versions.node.split('.')[0]) < 18) { - throw new OrcadBundledRuntimeError('The Orca server requires Node.js 18 or newer') + if (Number(process.versions.node.split('.')[0]) < 24) { + throw new OrcadBundledRuntimeError('The Orca server requires Node.js 24 or newer') } const directory = resolveBundledOrcadSlot() const runtime = resolveBundledOrcadRuntime(directory) diff --git a/src/main/orcad/orcad-launcher-isolation.integration.test.ts b/src/main/orcad/orcad-launcher-isolation.integration.test.ts index af69047ef94..8d8a203616d 100644 --- a/src/main/orcad/orcad-launcher-isolation.integration.test.ts +++ b/src/main/orcad/orcad-launcher-isolation.integration.test.ts @@ -89,10 +89,13 @@ it('runs an unpackaged server on the supported host runtime and preserves argume 'console.log(JSON.stringify({ node: process.versions.node, args: process.argv.slice(2) }))' ) await buildLauncher(entry) - const version = await runProcess({ program: launcherNode, args: ['-p', 'process.versions.node'] }) + const version = await runProcess({ + program: process.execPath, + args: ['-p', 'process.versions.node'] + }) const args = ['a path with spaces', '--port', '0'] const result = await runProcess({ - program: launcherNode, + program: process.execPath, args: [entry, ...args], env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' } }) @@ -100,6 +103,25 @@ it('runs an unpackaged server on the supported host runtime and preserves argume expect(JSON.parse(result.stdout)).toEqual({ node: version.stdout.trim(), args }) }) +it('refuses an old host runtime without a bundle before parsing the server', async ({ skip }) => { + const version = await runProcess({ + program: launcherNode, + args: ['-p', 'process.versions.node'] + }) + if (Number(version.stdout.trim().split('.')[0]) >= 24) { + skip() + } + const result = await runProcess({ + program: launcherNode, + args: [launcher], + env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' } + }) + expect(result.code).toBe(78) + expect(result.stderr).toContain('requires Node.js 24') + expect(result.stderr).not.toContain('SyntaxError') + expect(result.stdout).toBe('') +}) + it('preserves the crash exit code when the server fails while loading', async () => { const directory = join(root, 'server-crash') await mkdir(directory) @@ -159,23 +181,4 @@ describe.skipIf(skip)('split launcher with the real bundled runtime', () => { expect(result.stderr).toContain('server does not match its launcher') expect(result.stderr).not.toContain('server was loaded') }) - - it('refuses an old host runtime without a bundle before loading the server', async ({ skip }) => { - const version = await runProcess({ - program: launcherNode, - args: ['-p', 'process.versions.node'] - }) - if (Number(version.stdout.trim().split('.')[0]) >= 18) { - skip() - } - await writeFile(join(root, ORCAD_SERVER_ENTRY_FILENAME), 'throw new Error("server was loaded")') - const result = await runProcess({ - program: launcherNode, - args: [launcher], - env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' } - }) - expect(result.code).toBe(78) - expect(result.stderr).toContain('requires Node.js 18') - expect(result.stderr).not.toContain('server was loaded') - }) }) diff --git a/src/main/orcad/orcad-runtime-entry-artifacts.test.ts b/src/main/orcad/orcad-runtime-entry-artifacts.test.ts index 9deff1fdcc2..6cfcec11efe 100644 --- a/src/main/orcad/orcad-runtime-entry-artifacts.test.ts +++ b/src/main/orcad/orcad-runtime-entry-artifacts.test.ts @@ -26,7 +26,7 @@ function buildOrcadBundles(): string[] { const out = ${JSON.stringify(directory)} await entries.buildOrcadEntry(join(out, 'orcad.js')) await Promise.all(Object.values(entries.ORCAD_CHILD_ENTRY_POINTS).map((entry) => build({ - entryPoints: [entry], bundle: true, platform: 'node', target: 'node18', format: 'cjs', + entryPoints: [entry], bundle: true, platform: 'node', target: entries.ORCAD_SERVER_BUILD_TARGET, format: 'cjs', outfile: join(out, entries.orcadChildOutputFilename(entry)), external: entries.ORCAD_EXTERNAL_MODULES, plugins: [entries.externalNativeAddons], logLevel: 'error'