build(runtime): target Node 24 while retaining host compatibility (#34177)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-27 14:40:37 -07:00
committed by GitHub
co-authored by OrcaWin
parent 54411a9a96
commit 7b8ca17c64
8 changed files with 53 additions and 39 deletions
+6
View File
@@ -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
+3 -2
View File
@@ -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,
+6 -3
View File
@@ -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,
+2 -1
View File
@@ -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,
+9 -9
View File
@@ -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')
+2 -2
View File
@@ -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)
@@ -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')
})
})
@@ -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'