mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
fix(packaging): verify every emitted main file for bare runtime imports
The packaged-main verifier read two fixed entry files, but rolldown hoists
modules shared by two entries into out/main/chunks. jsonc-parser is reached
only from a chunk today, so nothing verified it, and the agent-hooks entry
the list names contributes no coverage at all. An import that migrates into
a chunk would silently stop being checked -- the same blindness that let the
missing Claude agent SDK ship.
Scan every out/main/**/*.js entry in the asar instead, keeping the two
required-file assertions as a build-integrity check. Measured against the
shipped 1.4.198 app: 93 entries in 72ms, reporting the absent SDK and
nothing else.
Also tighten the specifier match with a (?<![.\w]) lookbehind. Orca has
three registry methods of its own named require(), two taking a string key,
so a minified registry.require('public-a') otherwise reads as a bare module
specifier and fails packaging with a confusing error -- a risk the wider
file set would have multiplied. The lookbehind drops nothing real: detection
over the shipped bundle is identical with and without it.
This commit is contained in:
@@ -57,6 +57,11 @@ const ELECTRON_ARCHITECTURE_BY_ENUM = {
|
||||
4: 'universal'
|
||||
}
|
||||
const PACKAGED_NATIVE_ARCHITECTURES = new Set(['ia32', 'x64', 'arm', 'arm64'])
|
||||
const PACKAGED_MAIN_REQUIRED_FILES = [
|
||||
'out/main/index.js',
|
||||
'out/main/agent-hooks/managed-agent-hook-controls.js'
|
||||
]
|
||||
const PACKAGED_MAIN_SOURCE_RE = /^out\/main\/.+\.js$/
|
||||
const TYPE_DECLARATION_ARTIFACT_RE = /\.d\.(?:c|m)?ts(?:\.map)?$/
|
||||
const JS_SOURCE_MAP_ARTIFACT_RE = /\.(?:c|m)?js\.map$/
|
||||
const VERSIONED_ONNXRUNTIME_DYLIB_RE = /^libonnxruntime\.\d[\d.]*\.dylib$/
|
||||
@@ -224,21 +229,31 @@ function verifyPackagedMainRuntimeDeps(resourcesDir, asar = require('@electron/a
|
||||
return
|
||||
}
|
||||
|
||||
const mainFiles = ['out/main/index.js', 'out/main/agent-hooks/managed-agent-hook-controls.js']
|
||||
const entries = asar.listPackage(asarPath)
|
||||
const missing = new Set()
|
||||
|
||||
for (const file of mainFiles) {
|
||||
const entry = findAsarEntry(entries, file)
|
||||
if (!entry) {
|
||||
for (const file of PACKAGED_MAIN_REQUIRED_FILES) {
|
||||
if (!findAsarEntry(entries, file)) {
|
||||
throw new Error(`Packaged main file ${file} was not found in ${asarPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
const missing = new Set()
|
||||
// Why every emitted main file rather than the entry points alone: rolldown hoists
|
||||
// modules shared by two entries into out/main/chunks, so an entry's own bare imports
|
||||
// move out from under a fixed file list and silently stop being checked.
|
||||
for (const entry of entries) {
|
||||
if (!PACKAGED_MAIN_SOURCE_RE.test(normalizeAsarEntryPath(entry))) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Why: @electron/asar lists entries with host separators; Windows returns
|
||||
// backslashes, and extractFile expects that same host-style path.
|
||||
const internalPath = entry.replace(/^[\\/]+/, '')
|
||||
const source = asar.extractFile(asarPath, internalPath).toString('utf8')
|
||||
for (const match of source.matchAll(/\b(?:require|import)\s*\(\s*(["'`])([^"'`$]+)\1\s*\)/g)) {
|
||||
// Why the lookbehind: Orca has its own registry methods named `require`, so a
|
||||
// minified `registry.require('some-id')` must not read as a bare specifier.
|
||||
for (const match of source.matchAll(
|
||||
/(?<![.\w])(?:require|import)\s*\(\s*(["'`])([^"'`$]+)\1\s*\)/g
|
||||
)) {
|
||||
const specifier = match[2]
|
||||
if (!isPackagedExternalSpecifier(specifier)) {
|
||||
continue
|
||||
|
||||
@@ -71,6 +71,53 @@ describe('packaged runtime resources', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('verifies bare imports that rolldown hoisted into a shared main chunk', async () => {
|
||||
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-chunk-imports-'))
|
||||
try {
|
||||
await writeFile(join(resourcesDir, 'app.asar'), '', 'utf8')
|
||||
|
||||
// The entry points themselves carry no specifier; only the shared chunk does.
|
||||
const sources = new Map([
|
||||
['out/main/index.js', ''],
|
||||
['out/main/agent-hooks/managed-agent-hook-controls.js', ''],
|
||||
['out/main/chunks/managed-agent-hook-controls-CWf8D-KR.js', 'require(`jsonc-parser`)']
|
||||
])
|
||||
const asar = {
|
||||
listPackage: () => [...sources.keys()].map((entry) => `/${entry}`),
|
||||
extractFile: (_asarPath, internalPath) => Buffer.from(sources.get(internalPath), 'utf8')
|
||||
}
|
||||
|
||||
expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).toThrow(/jsonc-parser/)
|
||||
|
||||
await mkdir(join(resourcesDir, 'node_modules', 'jsonc-parser'), { recursive: true })
|
||||
expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).not.toThrow()
|
||||
} finally {
|
||||
await rm(resourcesDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('ignores member calls onto Orca methods that are themselves named require', async () => {
|
||||
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-member-require-'))
|
||||
try {
|
||||
await writeFile(join(resourcesDir, 'app.asar'), '', 'utf8')
|
||||
|
||||
// electron-sidecar-tab-registry and browser-execution-host-grant-registry both
|
||||
// expose require(key); a literal key must never read as a packaged specifier.
|
||||
const sources = new Map([
|
||||
['out/main/index.js', 'registry.require("public-a");grants.require(`host-key`)'],
|
||||
['out/main/agent-hooks/managed-agent-hook-controls.js', 'state.import("android-sdk")']
|
||||
])
|
||||
const asar = {
|
||||
listPackage: () => [...sources.keys()].map((entry) => `/${entry}`),
|
||||
extractFile: (_asarPath, internalPath) => Buffer.from(sources.get(internalPath), 'utf8')
|
||||
}
|
||||
|
||||
expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).not.toThrow()
|
||||
} finally {
|
||||
await rm(resourcesDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('normalizes host-specific asar entry separators', () => {
|
||||
expect(findAsarEntry(['\\out\\main\\index.js'], 'out/main/index.js')).toBe(
|
||||
'\\out\\main\\index.js'
|
||||
|
||||
Reference in New Issue
Block a user