// ProjectFilePlane over an already-built bundle held in memory. // // a prebuilt Box has no sources, so there is no checkout to index and nothing // on disk to stay in step with: the whole project is the bundle the page boots. // CheckoutFilePlane cannot serve this because it asks git what belongs to the // project, and a bundle produced by a build lives outside any checkout. // // the bundle is split here rather than by the caller, so the one place that // decides where the parts go is the one place that decides how big they are. import { PREBUILT_BUNDLE_PART_BYTES, prebuiltBundlePartPath } from '@rnx/box/template' import type { ProjectFileMeta, ProjectFilePlane } from '@rnx/box/plane' export class BundleFilePlane implements ProjectFilePlane { private readonly parts = new Map() private readonly entries = new Map() constructor(bundle: Uint8Array) { if (bundle.byteLength === 0) throw new Error('a Box bundle cannot be empty') for ( let offset = 0, index = 0; offset < bundle.byteLength; offset += PREBUILT_BUNDLE_PART_BYTES, index++ ) { const part = bundle.subarray(offset, offset + PREBUILT_BUNDLE_PART_BYTES) this.record(prebuiltBundlePartPath(index), part) } } private record(path: string, content: Uint8Array): void { this.parts.set(path, content) this.entries.set(path, { size: content.byteLength, readOnly: false, // a Metro bundle is JavaScript. the flag is what decides whether a shell // may overwrite a file with text, and marking it binary would make the // bundle unwritable from inside the box for no reason. binary: false, }) } index(): ReadonlyMap { return this.entries } // nothing else can change this plane, so the index is already current. refresh(): Promise { return Promise.resolve() } async read(path: string): Promise { return new TextDecoder().decode(await this.readBytes(path)) } readBytes(path: string): Promise { const part = this.parts.get(path) if (!part) return Promise.reject(new Error(`no such file: ${path}`)) return Promise.resolve(part) } write(path: string, content: Uint8Array): Promise { this.record(path, content) return Promise.resolve() } delete(path: string): Promise { this.parts.delete(path) this.entries.delete(path) return Promise.resolve() } }