mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix: preserve module URLs in the headless server bundle (#26450)
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
externalNativeAddons,
|
||||
ORCAD_EXTERNAL_MODULES,
|
||||
ORCAD_CHILD_ENTRY_POINTS,
|
||||
ORCAD_COMMONJS_MODULE_OPTIONS,
|
||||
orcadChildOutputFilename
|
||||
} from './orcad-entry-build.mjs'
|
||||
import { createRequire } from 'node:module'
|
||||
@@ -52,6 +53,8 @@ import { computeOrcadFullVersion } from './orcad-artifact-version.mjs'
|
||||
import { NODE_RUNTIME_ASSETS, NODE_RUNTIME_PIN } from '../../src/shared/node-runtime-pin.ts'
|
||||
import { findSlotProblems, readManifest } from './orcad-prebuild-slot-contents.mjs'
|
||||
import { orcadAgentBrowserNativeName } from '../../src/shared/orcad-agent-browser-name.ts'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { describeProcessFailure } from './process-failure-message.mjs'
|
||||
|
||||
const ROOT = join(import.meta.dirname, '..', '..')
|
||||
const OUT_DIR = process.env.ORCAD_OUT_DIR
|
||||
@@ -99,6 +102,7 @@ async function stageParcelWatcher(target) {
|
||||
const wrapperSource = requireFromWatcher.resolve('@parcel/watcher/wrapper.js')
|
||||
mkdirSync(WATCHER_MODULE_DIR, { recursive: true })
|
||||
await build({
|
||||
...ORCAD_COMMONJS_MODULE_OPTIONS,
|
||||
stdin: {
|
||||
contents:
|
||||
`const {createWrapper}=require(${JSON.stringify(wrapperSource)});` +
|
||||
@@ -197,6 +201,7 @@ cpSync(join(ROOT, 'resources', 'native-chat-visuals'), join(OUT_DIR, 'native-cha
|
||||
* their runtime resolvers look for them. */
|
||||
function buildForkedChild(entryPoint, outfile) {
|
||||
return build({
|
||||
...ORCAD_COMMONJS_MODULE_OPTIONS,
|
||||
entryPoints: [entryPoint],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
@@ -209,6 +214,7 @@ function buildForkedChild(entryPoint, outfile) {
|
||||
minify: true,
|
||||
sourcemap: false,
|
||||
define: {
|
||||
...ORCAD_COMMONJS_MODULE_OPTIONS.define,
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
},
|
||||
logLevel: 'error'
|
||||
@@ -312,6 +318,17 @@ if (graphErrors.length > 0) {
|
||||
)
|
||||
process.exitCode = 1
|
||||
}
|
||||
const providerSmoke = runProcessSync({
|
||||
program: process.execPath,
|
||||
args: [SERVER_OUT_FILE, '--orcad-structured-provider-load-check'],
|
||||
timeoutMs: 60_000
|
||||
})
|
||||
if (providerSmoke.code !== 0 || providerSmoke.timedOut || providerSmoke.outputTruncated) {
|
||||
console.error(
|
||||
`[build-orcad] the packaged structured provider failed to load.\n${describeProcessFailure(providerSmoke)}`
|
||||
)
|
||||
process.exitCode = 1
|
||||
}
|
||||
// Why require + parseArgs and not a real daemon: requiring the bundle evaluates every
|
||||
// top-level import, and calling its exported argv parser proves the entry's own code is
|
||||
// there rather than a graph that merely resolved. Booting one would need a socket, a
|
||||
|
||||
@@ -25,6 +25,15 @@ export function orcadChildOutputFilename(entryPoint) {
|
||||
|
||||
export const ORCAD_EXTERNAL_MODULES = ['electron', 'node-pty', '@parcel/watcher', 'fsevents']
|
||||
|
||||
// Bundled ESM resolves from the deployed bundle, like CommonJS __filename.
|
||||
export const ORCAD_COMMONJS_MODULE_OPTIONS = {
|
||||
banner: {
|
||||
js: 'var __orcadModuleUrl = require("node:url").pathToFileURL(__filename).href;'
|
||||
},
|
||||
define: { 'import.meta.url': '__orcadModuleUrl' },
|
||||
logOverride: { 'empty-import-meta': 'error' }
|
||||
}
|
||||
|
||||
// Native binaries are staged separately from every JavaScript entry.
|
||||
export const externalNativeAddons = {
|
||||
name: 'external-native-addons',
|
||||
@@ -45,6 +54,7 @@ const jsoncParserEsm = {
|
||||
|
||||
export function buildOrcadEntry(outfile) {
|
||||
return build({
|
||||
...ORCAD_COMMONJS_MODULE_OPTIONS,
|
||||
entryPoints: [join(root, ORCAD_ENTRY_POINT)],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
@@ -56,7 +66,10 @@ export function buildOrcadEntry(outfile) {
|
||||
metafile: true,
|
||||
minify: true,
|
||||
sourcemap: false,
|
||||
define: { 'process.env.NODE_ENV': '"production"' },
|
||||
define: {
|
||||
...ORCAD_COMMONJS_MODULE_OPTIONS.define,
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
},
|
||||
logLevel: 'error'
|
||||
})
|
||||
}
|
||||
@@ -81,6 +94,7 @@ export function buildOrcadLauncher(outfile) {
|
||||
.update(readFileSync(join(dirname(outfile), ORCAD_SERVER_ENTRY_FILENAME)))
|
||||
.digest('hex')
|
||||
return build({
|
||||
...ORCAD_COMMONJS_MODULE_OPTIONS,
|
||||
entryPoints: [join(root, ORCAD_LAUNCHER_ENTRY_POINT)],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
@@ -90,7 +104,10 @@ export function buildOrcadLauncher(outfile) {
|
||||
metafile: true,
|
||||
minify: true,
|
||||
sourcemap: false,
|
||||
define: { ORCAD_SERVER_SHA256: JSON.stringify(serverSha256) },
|
||||
define: {
|
||||
...ORCAD_COMMONJS_MODULE_OPTIONS.define,
|
||||
ORCAD_SERVER_SHA256: JSON.stringify(serverSha256)
|
||||
},
|
||||
logLevel: 'error'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { build } from 'esbuild'
|
||||
import { mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterAll, beforeAll, expect, it } from 'vitest'
|
||||
import { buildOrcadEntry, ORCAD_COMMONJS_MODULE_OPTIONS } from './orcad-entry-build.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-structured-provider-build-'))
|
||||
const bundle = join(directory, 'orcad-server.js')
|
||||
|
||||
beforeAll(async () => {
|
||||
await buildOrcadEntry(bundle)
|
||||
}, 60_000)
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('loads the structured Claude SDK through the packaged server under plain Node', () => {
|
||||
const loaded = runProcessSync({
|
||||
program: process.execPath,
|
||||
args: [bundle, '--orcad-structured-provider-load-check'],
|
||||
cwd: directory,
|
||||
env: { ORCA_BACKGROUND_LAUNCH: '1' },
|
||||
timeoutMs: 30_000
|
||||
})
|
||||
expect(loaded.code, loaded.stderr.slice(-2_000)).toBe(0)
|
||||
}, 40_000)
|
||||
|
||||
it('resolves bundle-relative requires after deployment to a path with URL-special characters', async () => {
|
||||
const buildDirectory = join(directory, 'build')
|
||||
mkdirSync(buildDirectory)
|
||||
const outfile = join(buildDirectory, 'entry.cjs')
|
||||
await build({
|
||||
...ORCAD_COMMONJS_MODULE_OPTIONS,
|
||||
stdin: {
|
||||
contents: `import { createRequire } from 'node:module';
|
||||
console.log(createRequire(import.meta.url)('./asset.cjs'));`,
|
||||
resolveDir: directory
|
||||
},
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
format: 'cjs',
|
||||
outfile,
|
||||
minify: true,
|
||||
logLevel: 'error'
|
||||
})
|
||||
writeFileSync(join(buildDirectory, 'asset.cjs'), 'module.exports = "deployed asset"')
|
||||
const deployedDirectory = join(directory, 'deployed # 100%')
|
||||
renameSync(buildDirectory, deployedDirectory)
|
||||
const loaded = runProcessSync({
|
||||
program: process.execPath,
|
||||
args: [join(deployedDirectory, 'entry.cjs')],
|
||||
cwd: directory,
|
||||
env: { ORCA_BACKGROUND_LAUNCH: '1' },
|
||||
timeoutMs: 30_000
|
||||
})
|
||||
expect(loaded.code, loaded.stderr).toBe(0)
|
||||
expect(loaded.stdout.trim()).toBe('deployed asset')
|
||||
})
|
||||
|
||||
it.each(['import.meta', 'import.meta.dirname', 'import.meta.filename', 'import.meta.resolve'])(
|
||||
'rejects unsupported %s instead of silently replacing it with an empty object',
|
||||
async (expression) => {
|
||||
await expect(
|
||||
build({
|
||||
...ORCAD_COMMONJS_MODULE_OPTIONS,
|
||||
stdin: { contents: `console.log(${expression})`, resolveDir: directory },
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
write: false,
|
||||
logLevel: 'silent'
|
||||
})
|
||||
).rejects.toThrow('"import.meta" is not available')
|
||||
}
|
||||
)
|
||||
@@ -42,7 +42,12 @@ function failStartup(error: unknown): void {
|
||||
}
|
||||
|
||||
// One-shot launch and stop commands must skip server startup and preflights.
|
||||
if (process.argv[2] === WINDOWS_BREAKAWAY_LAUNCH_FLAG) {
|
||||
if (process.argv[2] === '--orcad-structured-provider-load-check') {
|
||||
void import('./orcad-structured-provider-smoke')
|
||||
.then(({ smokeLoadOrcadStructuredProviders }) => smokeLoadOrcadStructuredProviders())
|
||||
.then(() => process.exit(0))
|
||||
.catch(failStartup)
|
||||
} else if (process.argv[2] === WINDOWS_BREAKAWAY_LAUNCH_FLAG) {
|
||||
runWindowsBreakawayLaunchIfRequested(ORCAD_WINDOWS_BREAKAWAY_CONTRACT, process.argv)
|
||||
} else if (
|
||||
process.argv[2] === ORCAD_COMPLETE_MANAGED_STOP_FLAG ||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { openClaudeStreamJsonConnection } from '../claude/claude-stream-json-connection'
|
||||
|
||||
export async function smokeLoadOrcadStructuredProviders(): Promise<void> {
|
||||
const loaded = new Error('structured provider module loaded')
|
||||
try {
|
||||
await openClaudeStreamJsonConnection(
|
||||
{ pathToClaudeCodeExecutable: 'unused', cwd: process.cwd(), options: {} },
|
||||
{},
|
||||
undefined,
|
||||
() => {
|
||||
// The production connection loads the SDK before calling the query implementation.
|
||||
throw loaded
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
if (error === loaded) {
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
throw new Error('structured provider load check did not reach the SDK boundary')
|
||||
}
|
||||
Reference in New Issue
Block a user