diff --git a/config/packaged-runtime-node-modules.cjs b/config/packaged-runtime-node-modules.cjs index 9b4a84df54c..c118769c0b0 100644 --- a/config/packaged-runtime-node-modules.cjs +++ b/config/packaged-runtime-node-modules.cjs @@ -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( + /(? { } }) + 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'