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:
Merge Sim
2026-09-06 01:40:00 -07:00
parent adbb269dc1
commit da33dda26a
2 changed files with 69 additions and 7 deletions
+22 -7
View File
@@ -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'