From 937d2d38064ce1bd1c5c9cb7fd3d5be946e4e9a5 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:31:59 -0700 Subject: [PATCH] Share installed desktop and browser assets (#31010) * Share installed browser assets with the desktop renderer * Update packaged runtime fixtures for shared renderer assets * Verify retained cloud clients in the shared renderer layout --- config/electron-builder.config.cjs | 6 +- .../scripts/electron-builder-config.test.mjs | 9 +- ...lectron-builder-runtime-resources.test.mjs | 6 +- .../packaged-cloud-source-exclusion.test.mjs | 2 +- .../scripts/project-renderer-web-client.mjs | 5 ++ .../project-renderer-web-client.test.mjs | 17 ++++ .../scripts/release-javascript-artifact.mjs | 1 + .../release-javascript-artifact.test.mjs | 1 + .../scripts/verify-packaged-serve-payload.cjs | 35 +++++++- .../verify-packaged-serve-payload.test.mjs | 48 +++++++++- .../runtime/rpc/static-web-client-handler.ts | 12 ++- .../static-web-client-shared-assets.test.ts | 88 +++++++++++++++++++ .../bundled-web-client-manifest.test.ts | 70 +++++++++++++++ src/shared/bundled-web-client-manifest.ts | 29 ++++++ src/shared/bundled-web-client-root.ts | 16 +++- 15 files changed, 331 insertions(+), 14 deletions(-) create mode 100644 src/main/runtime/rpc/static-web-client-shared-assets.test.ts create mode 100644 src/shared/bundled-web-client-manifest.test.ts create mode 100644 src/shared/bundled-web-client-manifest.ts diff --git a/config/electron-builder.config.cjs b/config/electron-builder.config.cjs index 11e8df74bb9..ff5c20876f1 100644 --- a/config/electron-builder.config.cjs +++ b/config/electron-builder.config.cjs @@ -302,6 +302,8 @@ module.exports = { '!out/**/*.map', // Why: Vite's manifest is only used to project the paired web client. '!out/renderer/.vite{,/**/*}', + // The installed browser serves its allowlisted assets from the unpacked renderer. + '!out/web{,/**/*}', // Why: out/electron-dev caches `pnpm dev`'s per-branch Electron.app copies (~270MB each). // CI never creates it, but packaging on a machine that has run dev would pack them all. '!out/electron-dev{,/**/*}', @@ -356,7 +358,7 @@ module.exports = { // entries reached solely from the Electron main process stay packed, since // asar redirects their app.asar paths. // Why: `orca serve` runs the orcad selection entry under ELECTRON_RUN_AS_NODE too (#27307). - // Why out/web and out/mobile-web: their assets are opened by path, and opening a packed file + // Why out/renderer and out/mobile-web: their assets are opened by path, and opening a packed file // reads a /tmp extract Electron makes once and never remakes, so a /tmp sweep broke them (#27976). asarUnpack: [ 'out/package.json', @@ -386,7 +388,7 @@ module.exports = { 'out/main/parcel-watcher-process-entry.js', 'out/main/orcad/orcad-local-serve-selection-entry.js', 'out/main/chunks/**', - 'out/web/**', + 'out/renderer/**', 'out/mobile-web/**', 'resources/**', 'node_modules/ws/**', diff --git a/config/scripts/electron-builder-config.test.mjs b/config/scripts/electron-builder-config.test.mjs index d7a6e0235a3..4c76725621a 100644 --- a/config/scripts/electron-builder-config.test.mjs +++ b/config/scripts/electron-builder-config.test.mjs @@ -83,6 +83,10 @@ describe('electron-builder config', () => { expect(packs(toolingPath)).toBe(false) } expect(packs('out/main/index.js')).toBe(true) + expect(packs('out/web/web-index.html')).toBe(false) + expect(packs('out/web/assets/shared.js')).toBe(false) + expect(packs('out/renderer/web-index.html')).toBe(true) + expect(packs('out/renderer/.orca-web-client-files.json')).toBe(true) }) it.each(['file', 'directory'])('keeps a root notes %s out of app.asar', async (kind) => { @@ -425,8 +429,9 @@ describe('electron-builder config', () => { ) const isUnpacked = matcher.createFilter() for (const webPath of [ - 'out/web/web-index.html', - 'out/web/assets/index-abc123.js', + 'out/renderer/web-index.html', + 'out/renderer/assets/index-abc123.js', + 'out/renderer/.orca-web-client-files.json', 'out/mobile-web/manifest.json', 'out/mobile-web/assets/index-abc123.js' ]) { diff --git a/config/scripts/electron-builder-runtime-resources.test.mjs b/config/scripts/electron-builder-runtime-resources.test.mjs index 021711ab574..69f5acf2b4c 100644 --- a/config/scripts/electron-builder-runtime-resources.test.mjs +++ b/config/scripts/electron-builder-runtime-resources.test.mjs @@ -561,11 +561,15 @@ async function seedPackagedServePayload(resourcesDir) { const entry = join(resourcesDir, 'app.asar.unpacked', 'out', ...SERVE_SELECTION_ENTRY.split('/')) await mkdir(dirname(entry), { recursive: true }) await writeFile(entry, '') - const webDir = join(resourcesDir, 'app.asar.unpacked', 'out', 'web') + const webDir = join(resourcesDir, 'app.asar.unpacked', 'out', 'renderer') await mkdir(join(webDir, 'assets'), { recursive: true }) await mkdir(join(resourcesDir, 'app.asar.unpacked', 'out', 'cli'), { recursive: true }) await writeFile(join(webDir, 'web-index.html'), '') await writeFile(join(webDir, 'assets', 'web.js'), '') + await writeFile( + join(webDir, '.orca-web-client-files.json'), + JSON.stringify(['web-index.html', 'assets/web.js']) + ) } async function seedBundledRipgrep(resourcesDir) { diff --git a/config/scripts/packaged-cloud-source-exclusion.test.mjs b/config/scripts/packaged-cloud-source-exclusion.test.mjs index 53e3fe8dd09..e53b4be4954 100644 --- a/config/scripts/packaged-cloud-source-exclusion.test.mjs +++ b/config/scripts/packaged-cloud-source-exclusion.test.mjs @@ -30,7 +30,7 @@ describe('packaged cloud source exclusion', () => { 'out/main/runtime/push/push-host-proof.js', 'out/shared/mobile-push-contract.js', 'out/renderer/index.html', - 'out/web/assets/cloud/client.js', + 'out/renderer/assets/cloud/client.js', 'resources/cloud/icon.png' ] try { diff --git a/config/scripts/project-renderer-web-client.mjs b/config/scripts/project-renderer-web-client.mjs index a3df2d9b1b5..50fe78376ad 100644 --- a/config/scripts/project-renderer-web-client.mjs +++ b/config/scripts/project-renderer-web-client.mjs @@ -181,6 +181,11 @@ try { rmSync(webOutput, { force: true, recursive: true }) renameSync(stagingOutput, webOutput) + // The installed browser client shares renderer bytes; only its selected files may be served. + writeFileSync( + join(rendererOutput, '.orca-web-client-files.json'), + JSON.stringify([...selectedFiles].sort()) + ) } finally { rmSync(stagingOutput, { force: true, recursive: true }) } diff --git a/config/scripts/project-renderer-web-client.test.mjs b/config/scripts/project-renderer-web-client.test.mjs index 0b7525fb378..55ca40ce6a7 100644 --- a/config/scripts/project-renderer-web-client.test.mjs +++ b/config/scripts/project-renderer-web-client.test.mjs @@ -103,6 +103,23 @@ describe('renderer web client projection', () => { expect(existsSync(join(root, 'out/web/assets/logo.png'))).toBe(true) expect(existsSync(join(root, 'out/web/assets/desktop-entry.js'))).toBe(false) expect(existsSync(join(root, 'out/web/stale.js'))).toBe(false) + const sharedFiles = JSON.parse( + readFileSync(join(root, 'out/renderer/.orca-web-client-files.json'), 'utf8') + ) + expect(sharedFiles).toEqual( + expect.arrayContaining([ + 'web-index.html', + 'assets/web-entry.js', + 'assets/web-shared.js', + 'assets/editor.worker-fixture.js', + 'assets/web.css', + 'assets/lazy.js', + 'assets/logo.png' + ]) + ) + expect(sharedFiles).not.toContain('assets/desktop-entry.js') + expect(sharedFiles).not.toContain('.vite/manifest.json') + expect(sharedFiles).not.toContain('index.html') expect(readFileSync(join(root, 'out/web/assets/web.css'), 'utf8')).toBe('.root{color:red}\n') }) diff --git a/config/scripts/release-javascript-artifact.mjs b/config/scripts/release-javascript-artifact.mjs index 5a7f610594b..a04cf2445ef 100644 --- a/config/scripts/release-javascript-artifact.mjs +++ b/config/scripts/release-javascript-artifact.mjs @@ -24,6 +24,7 @@ const REQUIRED_FILES = [ 'preload/index.js', 'renderer/index.html', 'renderer/.vite/manifest.json', + 'renderer/.orca-web-client-files.json', 'web/web-index.html', 'mobile-web/manifest.json', 'package.json' diff --git a/config/scripts/release-javascript-artifact.test.mjs b/config/scripts/release-javascript-artifact.test.mjs index d0faff99f29..c98efdb5456 100644 --- a/config/scripts/release-javascript-artifact.test.mjs +++ b/config/scripts/release-javascript-artifact.test.mjs @@ -34,6 +34,7 @@ function fixture() { 'preload/index.js', 'renderer/index.html', 'renderer/.vite/manifest.json', + 'renderer/.orca-web-client-files.json', 'web/web-index.html', 'mobile-web/manifest.json', 'package.json', diff --git a/config/scripts/verify-packaged-serve-payload.cjs b/config/scripts/verify-packaged-serve-payload.cjs index a60ec33a92a..f9f361e9715 100644 --- a/config/scripts/verify-packaged-serve-payload.cjs +++ b/config/scripts/verify-packaged-serve-payload.cjs @@ -2,7 +2,7 @@ const { existsSync, readFileSync, statSync } = require('node:fs') const { dirname, join, posix } = require('node:path') const WEB_CLIENT_INDEX = 'web-index.html' -const REMEDY = 'Run pnpm build:web (build:desktop and build:release already do).' +const REMEDY = 'Run pnpm build:web-from-renderer (build:desktop and build:release already do).' function failure(message) { return new Error(`[verify-packaged-serve-payload] ${message}`) @@ -27,7 +27,33 @@ function assertPackagedWebClient(resourcesDir) { if (!existsSync(join(outDir, 'cli'))) { throw failure(`no unpacked CLI at ${join(outDir, 'cli')}; the web client root resolves from it`) } - const webRoot = join(outDir, 'web') + const webRoot = join(outDir, 'renderer') + const manifestPath = join(webRoot, '.orca-web-client-files.json') + if (!existsSync(manifestPath)) { + throw failure(`missing unpacked web file manifest at ${manifestPath}. ${REMEDY}`) + } + const files = JSON.parse(readFileSync(manifestPath, 'utf8')) + if ( + !Array.isArray(files) || + !files.includes(WEB_CLIENT_INDEX) || + !files.every( + (file) => + typeof file === 'string' && + (file === WEB_CLIENT_INDEX || /^(?:assets|cmaps|standard_fonts|wasm)\//.test(file)) && + !file.includes('\\') && + !file.includes('\0') && + file.split('/').every((part) => part !== '' && part !== '.' && part !== '..') && + !file.endsWith('.map') + ) + ) { + throw failure(`invalid web file manifest at ${manifestPath}`) + } + const absent = files.filter( + (file) => !statSync(join(webRoot, file), { throwIfNoEntry: false })?.isFile() + ) + if (absent.length > 0) { + throw failure(`missing unpacked web files: ${absent.join(', ')}`) + } const indexPath = join(webRoot, WEB_CLIENT_INDEX) if (!existsSync(indexPath)) { throw failure(`missing unpacked ${indexPath}. ${REMEDY}`) @@ -37,7 +63,10 @@ function assertPackagedWebClient(resourcesDir) { throw failure(`${indexPath} references no ./ assets. ${REMEDY}`) } const missing = assets.filter( - (asset) => asset.startsWith('..') || !existsSync(join(webRoot, ...asset.split('/'))) + (asset) => + asset.startsWith('..') || + !files.includes(asset) || + !existsSync(join(webRoot, ...asset.split('/'))) ) if (missing.length > 0) { throw failure(`${indexPath} loads files missing from ${webRoot}: ${missing.join(', ')}`) diff --git a/config/scripts/verify-packaged-serve-payload.test.mjs b/config/scripts/verify-packaged-serve-payload.test.mjs index 37bfe660df5..b683174df34 100644 --- a/config/scripts/verify-packaged-serve-payload.test.mjs +++ b/config/scripts/verify-packaged-serve-payload.test.mjs @@ -21,8 +21,12 @@ function packagedResources({ index, assets = [], cli = true }) { if (cli) { mkdirSync(join(outDir, 'cli'), { recursive: true }) } - const webDir = join(outDir, 'web') + const webDir = join(outDir, 'renderer') mkdirSync(join(webDir, 'assets'), { recursive: true }) + writeFileSync( + join(webDir, '.orca-web-client-files.json'), + JSON.stringify(['web-index.html', ...assets]) + ) if (index !== undefined) { writeFileSync(join(webDir, 'web-index.html'), index) } @@ -66,6 +70,48 @@ describe('assertPackagedWebClient', () => { ) }) + it.each([ + null, + {}, + ['assets/web.js'], + ['web-index.html', 'index.html'], + ['web-index.html', 'assets/../private'], + ['web-index.html', 'assets//private'], + ['web-index.html', 'assets/./private'], + ['web-index.html', 'assets/a\\private'], + ['web-index.html', 'assets/private.js.map'], + ['web-index.html', '/assets/a'], + ['web-index.html', 'assets/a\0.js'], + ['web-index.html', 3] + ])('rejects malformed shared web manifests: %j', (files) => { + const resourcesDir = packagedResources({ + index: INDEX, + assets: ['assets/web.js', 'assets/web.css'] + }) + writeFileSync( + join(resourcesDir, 'app.asar.unpacked/out/renderer/.orca-web-client-files.json'), + JSON.stringify(files) + ) + expect(() => assertPackagedWebClient(resourcesDir)).toThrow(/invalid web file manifest/) + }) + + it('fails when a transitive worker is missing even though HTML assets exist', () => { + const resourcesDir = packagedResources({ + index: INDEX, + assets: ['assets/web.js', 'assets/web.css'] + }) + writeFileSync( + join(resourcesDir, 'app.asar.unpacked/out/renderer/.orca-web-client-files.json'), + JSON.stringify([ + 'web-index.html', + 'assets/web.js', + 'assets/web.css', + 'assets/editor.worker.js' + ]) + ) + expect(() => assertPackagedWebClient(resourcesDir)).toThrow(/editor.worker.js/) + }) + it('fails when the CLI the web root resolves from is not unpacked', () => { const resourcesDir = packagedResources({ index: INDEX, diff --git a/src/main/runtime/rpc/static-web-client-handler.ts b/src/main/runtime/rpc/static-web-client-handler.ts index c0b3181bdad..cd0b700f880 100644 --- a/src/main/runtime/rpc/static-web-client-handler.ts +++ b/src/main/runtime/rpc/static-web-client-handler.ts @@ -1,7 +1,8 @@ import { createReadStream } from 'node:fs' import { stat } from 'node:fs/promises' import type { IncomingMessage, RequestListener, ServerResponse } from 'node:http' -import { extname, isAbsolute, posix, relative, resolve } from 'node:path' +import { basename, extname, isAbsolute, posix, relative, resolve } from 'node:path' +import { readBundledWebClientFiles } from '../../../shared/bundled-web-client-manifest' const STATIC_WEB_ALLOWED_PATHS = new Set(['/web-index.html']) const STATIC_WEB_ALLOWED_PREFIXES = ['/assets/', '/cmaps/', '/standard_fonts/', '/wasm/'] @@ -24,13 +25,18 @@ export const answerWithoutWebClient: RequestListener = (_request, response) => { export function createStaticWebClientHandler(staticRoot: string): RequestListener { const resolvedRoot = resolve(staticRoot) + const files = readBundledWebClientFiles(resolvedRoot) + if (basename(resolvedRoot) === 'renderer' && !files) { + return answerWithoutWebClient + } return (request, response) => { - void handleStaticRequest(resolvedRoot, request, response) + void handleStaticRequest(resolvedRoot, files, request, response) } } async function handleStaticRequest( staticRoot: string, + files: ReadonlySet | undefined, request: IncomingMessage, response: ServerResponse ): Promise { @@ -48,7 +54,7 @@ async function handleStaticRequest( writeHttpStatus(response, 400) return } - if (!isAllowedStaticWebPath(pathname)) { + if (!isAllowedStaticWebPath(pathname) || (files && !files.has(pathname.slice(1)))) { writeHttpStatus(response, 404) return } diff --git a/src/main/runtime/rpc/static-web-client-shared-assets.test.ts b/src/main/runtime/rpc/static-web-client-shared-assets.test.ts new file mode 100644 index 00000000000..710e895c6da --- /dev/null +++ b/src/main/runtime/rpc/static-web-client-shared-assets.test.ts @@ -0,0 +1,88 @@ +import { once } from 'node:events' +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { createServer, type Server } from 'node:http' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { afterEach, expect, it } from 'vitest' +import { createStaticWebClientHandler } from './static-web-client-handler' + +const roots: string[] = [] +const servers: Server[] = [] +afterEach(async () => { + await Promise.all( + servers.splice(0).map((server) => { + server.closeAllConnections() + return new Promise((resolve) => server.close(() => resolve())) + }) + ) + roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })) +}) +function fixture() { + const root = mkdtempSync(join(tmpdir(), 'orca-shared-static-')) + roots.push(root) + const renderer = join(root, 'renderer') + const files = [ + 'web-index.html', + 'assets/shared.js', + 'assets/editor.worker.js', + 'cmaps/Font.bcmap', + 'standard_fonts/Font.pfb', + 'wasm/viewer.wasm' + ] + for (const file of [ + ...files, + 'index.html', + 'popout.html', + 'space.html', + 'assets/desktop.js', + 'assets/shared.js.map' + ]) { + mkdirSync(dirname(join(renderer, file)), { recursive: true }) + writeFileSync(join(renderer, file), file) + } + writeFileSync(join(renderer, '.orca-web-client-files.json'), JSON.stringify(files)) + return { renderer, files } +} +async function serve(root: string) { + const server = createServer(createStaticWebClientHandler(root)) + servers.push(server) + server.listen(0, '127.0.0.1') + await once(server, 'listening') + const address = server.address() + if (!address || typeof address === 'string') { + throw new Error('No address') + } + return `http://127.0.0.1:${address.port}` +} +it('serves only the web closure at root and through a reverse proxy, including worker/PDF files', async () => { + const { renderer, files } = fixture() + const endpoint = await serve(renderer) + for (const prefix of ['', '/proxy/orca']) { + for (const file of files) { + const response = await fetch(`${endpoint}${prefix}/${file}`) + expect(response.status, `${prefix}/${file}`).toBe(200) + expect(await response.text()).toBe(file) + } + for (const file of [ + 'popout.html', + 'space.html', + 'assets/desktop.js', + 'assets/shared.js.map', + '.orca-web-client-files.json' + ]) { + expect((await fetch(`${endpoint}${prefix}/${file}`)).status, file).toBe(404) + } + } + expect(await (await fetch(endpoint)).text()).toBe('web-index.html') + const head = await fetch(`${endpoint}/assets/shared.js`, { method: 'HEAD' }) + expect(head.headers.get('content-length')).toBe(String('assets/shared.js'.length)) + expect(await head.text()).toBe('') +}) +it('fails closed when a renderer has no manifest or a corrupt manifest', async () => { + const { renderer } = fixture() + rmSync(join(renderer, '.orca-web-client-files.json')) + const endpoint = await serve(renderer) + expect((await fetch(`${endpoint}/assets/desktop.js`)).status).toBe(404) + writeFileSync(join(renderer, '.orca-web-client-files.json'), '{') + expect(() => createStaticWebClientHandler(renderer)).toThrow() +}) diff --git a/src/shared/bundled-web-client-manifest.test.ts b/src/shared/bundled-web-client-manifest.test.ts new file mode 100644 index 00000000000..893af84eb75 --- /dev/null +++ b/src/shared/bundled-web-client-manifest.test.ts @@ -0,0 +1,70 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { readBundledWebClientFiles, WEB_CLIENT_FILES_FILENAME } from './bundled-web-client-manifest' +import { findBundledWebClientRoot } from './bundled-web-client-root' + +const roots: string[] = [] +function fixture() { + const root = mkdtempSync(join(tmpdir(), 'orca-shared-web-')) + roots.push(root) + return root +} +function manifest(root: string, files: unknown) { + mkdirSync(root, { recursive: true }) + writeFileSync(join(root, WEB_CLIENT_FILES_FILENAME), JSON.stringify(files)) +} +afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }))) + +describe('bundled web file manifest', () => { + it('retains standalone bundles and validates every shared path', () => { + const root = fixture() + expect(readBundledWebClientFiles(root)).toBeUndefined() + const files = [ + 'web-index.html', + 'assets/index.js', + 'assets/editor.worker.js', + 'cmaps/Font.bcmap', + 'standard_fonts/Font.pfb', + 'wasm/viewer.wasm' + ] + manifest(root, files) + expect([...(readBundledWebClientFiles(root) ?? [])]).toEqual(files) + }) + it.each([ + null, + {}, + ['assets/index.js'], + ['web-index.html', 'index.html'], + ['web-index.html', 'assets/../private'], + ['web-index.html', 'assets//private'], + ['web-index.html', 'assets/./private'], + ['web-index.html', 'assets/a\\private'], + ['web-index.html', 'assets/private.js.map'], + ['web-index.html', '/assets/a'], + ['web-index.html', 'assets/a\0.js'], + ['web-index.html', 3] + ])('rejects malformed manifests without broadening access: %j', (files) => { + const root = fixture() + manifest(root, files) + expect(() => readBundledWebClientFiles(root)).toThrow() + }) + it('finds real shared renderer files from Electron and plain Node roots', () => { + const root = fixture() + const appRoot = join(root, 'app.asar') + const unpacked = `${appRoot}.unpacked` + const renderer = join(unpacked, 'out/renderer') + mkdirSync(renderer, { recursive: true }) + writeFileSync(join(renderer, 'web-index.html'), 'web') + expect(findBundledWebClientRoot(appRoot)).toBeUndefined() + manifest(renderer, ['web-index.html']) + expect(findBundledWebClientRoot(appRoot)).toBe(renderer) + expect(findBundledWebClientRoot(unpacked)).toBe(renderer) + expect(findBundledWebClientRoot(join(unpacked, 'out/main'))).toBe(renderer) + const web = join(unpacked, 'out/web') + mkdirSync(web) + writeFileSync(join(web, 'web-index.html'), 'standalone') + expect(findBundledWebClientRoot(unpacked)).toBe(web) + }) +}) diff --git a/src/shared/bundled-web-client-manifest.ts b/src/shared/bundled-web-client-manifest.ts new file mode 100644 index 00000000000..e174fa0038a --- /dev/null +++ b/src/shared/bundled-web-client-manifest.ts @@ -0,0 +1,29 @@ +import { existsSync, readFileSync } from 'node:fs' +import { join } from 'node:path' + +export const WEB_CLIENT_FILES_FILENAME = '.orca-web-client-files.json' + +/** Limits HTTP access when desktop and browser assets share one installed directory. */ +export function readBundledWebClientFiles(root: string): ReadonlySet | undefined { + const manifestPath = join(root, WEB_CLIENT_FILES_FILENAME) + if (!existsSync(manifestPath)) { + return undefined + } + const files: unknown = JSON.parse(readFileSync(manifestPath, 'utf8')) + if ( + !Array.isArray(files) || + !files.every( + (file): file is string => + typeof file === 'string' && + (file === 'web-index.html' || /^(?:assets|cmaps|standard_fonts|wasm)\//.test(file)) && + !file.includes('\\') && + !file.includes('\0') && + file.split('/').every((segment) => segment !== '' && segment !== '.' && segment !== '..') && + !file.endsWith('.map') + ) || + !files.includes('web-index.html') + ) { + throw new Error('Invalid bundled web client file manifest') + } + return new Set(files) +} diff --git a/src/shared/bundled-web-client-root.ts b/src/shared/bundled-web-client-root.ts index 959e8b5b6ac..364bc822aad 100644 --- a/src/shared/bundled-web-client-root.ts +++ b/src/shared/bundled-web-client-root.ts @@ -1,5 +1,6 @@ import { existsSync } from 'node:fs' import { join } from 'node:path' +import { WEB_CLIENT_FILES_FILENAME } from './bundled-web-client-manifest' export const WEB_CLIENT_INDEX_FILENAME = 'web-index.html' @@ -13,5 +14,18 @@ export function findBundledWebClientRoot(appPath: string): string | undefined { // Why: unpacked electron-vite entrypoints set appPath to out/main, next to the web bundle. join(appPath, '..', 'web') ] - return roots.find((root) => existsSync(join(root, WEB_CLIENT_INDEX_FILENAME))) + const standalone = roots.find((root) => existsSync(join(root, WEB_CLIENT_INDEX_FILENAME))) + if (standalone) { + return standalone + } + const sharedAppPath = appPath.endsWith('.asar') ? `${appPath}.unpacked` : appPath + const sharedRoots = [ + join(sharedAppPath, 'out', 'renderer'), + join(sharedAppPath, '..', 'renderer') + ] + return sharedRoots.find( + (root) => + existsSync(join(root, WEB_CLIENT_INDEX_FILENAME)) && + existsSync(join(root, WEB_CLIENT_FILES_FILENAME)) + ) }