mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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
This commit is contained in:
@@ -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/**',
|
||||
|
||||
@@ -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'
|
||||
]) {
|
||||
|
||||
@@ -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'), '<script src="./assets/web.js"></script>')
|
||||
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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(', ')}`)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string> | undefined,
|
||||
request: IncomingMessage,
|
||||
response: ServerResponse
|
||||
): Promise<void> {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<void>((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()
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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<string> | 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)
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user