@@ -3,7 +3,7 @@ import { extname, join } from 'node:path'
33import { createLogger } from '@sim/logger'
44import { getErrorMessage } from '@sim/utils/errors'
55import type { Session } from 'electron'
6- import { protocol } from 'electron'
6+ import { app , protocol } from 'electron'
77
88const logger = createLogger ( 'DesktopLocalPages' )
99
@@ -94,10 +94,12 @@ function notFound(): Response {
9494}
9595
9696/**
97- * Serves allowlisted files from `rootDir`. Split from the session wiring so
98- * tests can drive it against a temporary directory .
97+ * Serves allowlisted files from the first of `rootDirs` that has them. Split
98+ * from the session wiring so tests can drive it against temporary directories .
9999 */
100- export function createLocalPageHandler ( rootDir : string ) : ( request : Request ) => Promise < Response > {
100+ export function createLocalPageHandler (
101+ rootDirs : readonly string [ ]
102+ ) : ( request : Request ) => Promise < Response > {
101103 return async ( request ) => {
102104 if ( request . method !== 'GET' ) {
103105 return new Response ( null , { status : 405 } )
@@ -115,41 +117,65 @@ export function createLocalPageHandler(rootDir: string): (request: Request) => P
115117 if ( ! SERVABLE_FILES . has ( name ) ) {
116118 return notFound ( )
117119 }
120+ const file = await readFirst ( rootDirs , name )
121+ if ( ! file ) {
122+ return notFound ( )
123+ }
124+ // A copy into a plain ArrayBuffer: Response bodies take BufferSource, and
125+ // a Node Buffer's backing store is not typed as one.
126+ const body = new Uint8Array ( file . byteLength )
127+ body . set ( file )
128+ return new Response ( body . buffer , {
129+ status : 200 ,
130+ headers : {
131+ 'Content-Type' : CONTENT_TYPES [ extname ( name ) ] ?? 'application/octet-stream' ,
132+ 'X-Content-Type-Options' : 'nosniff' ,
133+ } ,
134+ } )
135+ }
136+ }
137+
138+ async function readFirst ( rootDirs : readonly string [ ] , name : string ) : Promise < Buffer | null > {
139+ for ( const rootDir of rootDirs ) {
118140 try {
119- const file = await readFile ( join ( rootDir , name ) )
120- // A copy into a plain ArrayBuffer: Response bodies take BufferSource, and
121- // a Node Buffer's backing store is not typed as one.
122- const body = new Uint8Array ( file . byteLength )
123- body . set ( file )
124- return new Response ( body . buffer , {
125- status : 200 ,
126- headers : {
127- 'Content-Type' : CONTENT_TYPES [ extname ( name ) ] ?? 'application/octet-stream' ,
128- 'X-Content-Type-Options' : 'nosniff' ,
129- } ,
130- } )
141+ return await readFile ( join ( rootDir , name ) )
131142 } catch ( error ) {
132- logger . error ( 'Could not read a bundled page asset' , { name, error : getErrorMessage ( error ) } )
133- return notFound ( )
143+ if ( ( error as NodeJS . ErrnoException ) . code !== 'ENOENT' ) {
144+ logger . error ( 'Could not read a bundled page asset' , { name, error : getErrorMessage ( error ) } )
145+ return null
146+ }
134147 }
135148 }
149+ logger . error ( 'Bundled page asset is missing' , { name } )
150+ return null
136151}
137152
138153/**
139- * The bundled `static/` directory. `__dirname` is `dist/` in every build, so
140- * this holds inside the packaged asar as well as in an unpackaged checkout.
154+ * Where the pages and their assets live. `__dirname` is `dist/` in every
155+ * build, so `static/` resolves inside the packaged asar as well as in an
156+ * unpackaged checkout. The brand font is copied into `static/` only when
157+ * packaging (electron-builder.yml); an unpackaged run reads it from the web
158+ * app's public fonts instead, so nothing generated has to exist in the tree
159+ * and a cached build restores everything the pages need.
141160 */
142- function localPageRoot ( ) : string {
143- return join ( __dirname , '..' , 'static' )
161+ function localPageRoots ( ) : string [ ] {
162+ const roots = [ join ( __dirname , '..' , 'static' ) ]
163+ if ( ! app . isPackaged ) {
164+ roots . push ( join ( __dirname , '..' , '..' , 'sim' , 'public' , 'brand' , 'fonts' ) )
165+ }
166+ return roots
144167}
145168
146169/**
147170 * Serves the scheme on a session. Handlers are per session, so every partition
148171 * that hosts a bundled page installs one; repeat calls are no-ops.
149172 */
150- export function attachLocalPageProtocol ( ses : Session , rootDir : string = localPageRoot ( ) ) : void {
173+ export function attachLocalPageProtocol (
174+ ses : Session ,
175+ rootDirs : readonly string [ ] = localPageRoots ( )
176+ ) : void {
151177 if ( ses . protocol . isProtocolHandled ( LOCAL_PAGE_SCHEME ) ) {
152178 return
153179 }
154- ses . protocol . handle ( LOCAL_PAGE_SCHEME , createLocalPageHandler ( rootDir ) )
180+ ses . protocol . handle ( LOCAL_PAGE_SCHEME , createLocalPageHandler ( rootDirs ) )
155181}
0 commit comments