From 34ee954502303ab47e6226adae0642461b3f6055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Wed, 1 Apr 2026 21:34:44 -0500 Subject: [PATCH] feat: local vfs --- packages/scripts/src/buildAssets/constants.ts | 7 +++- .../scripts/src/core/github/assetUploader.ts | 20 ++++++------ packages/scripts/src/core/vfs/abstract.ts | 32 +++++-------------- packages/scripts/src/core/vfs/local.ts | 31 ++++++++++++++++++ packages/scripts/src/core/vfs/steam.ts | 18 +++++++++-- packages/scripts/src/core/vfs/zip.ts | 23 ++++++++++--- 6 files changed, 89 insertions(+), 42 deletions(-) create mode 100644 packages/scripts/src/core/vfs/local.ts diff --git a/packages/scripts/src/buildAssets/constants.ts b/packages/scripts/src/buildAssets/constants.ts index d189bbd3b..7516210da 100644 --- a/packages/scripts/src/buildAssets/constants.ts +++ b/packages/scripts/src/buildAssets/constants.ts @@ -1,10 +1,12 @@ import { assertSecret } from "@blitzkit/core"; import { AbstractVFS } from "../core/vfs/abstract"; +import { LocalVFS } from "../core/vfs/local"; import { SteamVFS } from "../core/vfs/steam"; import { ZipVFS } from "../core/vfs/zip"; const steamPattern = /steam:(\d+)\/(\d+)/; const zipPattern = /zip:(.+)/; +const localePattern = /local:"?(.+)"?/; const source = Bun.argv .find((arg) => arg.startsWith("--source=")) @@ -21,11 +23,14 @@ if (!source) { assertSecret(import.meta.env.STEAM_USERNAME), assertSecret(import.meta.env.STEAM_PASSWORD), app, - depot + depot, ).init(); } else if (zipPattern.test(source)) { const [, url] = source.match(zipPattern)!; resolved = await new ZipVFS(url).init(); +} else if (localePattern.test(source)) { + const [, path] = source.match(localePattern)!; + resolved = await new LocalVFS(path).init(); } else { throw new Error("No valid source specified"); } diff --git a/packages/scripts/src/core/github/assetUploader.ts b/packages/scripts/src/core/github/assetUploader.ts index ce24de2e3..858339b44 100644 --- a/packages/scripts/src/core/github/assetUploader.ts +++ b/packages/scripts/src/core/github/assetUploader.ts @@ -43,7 +43,7 @@ export class AssetUploader { constructor(public message: string) { [this.owner, this.repo] = assertSecret( - import.meta.env.PUBLIC_ASSET_REPO + import.meta.env.PUBLIC_ASSET_REPO, ).split("/"); this.branch = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH); } @@ -52,7 +52,7 @@ export class AssetUploader { const blobPath = `${this.owner}/${this.repo}/${this.branch}/${change.path}`; const response = await fetch( `https://raw.githubusercontent.com/${blobPath}`, - { headers: { "Accept-Encoding": "identity" } } + { headers: { "Accept-Encoding": "identity" } }, ); let diff: { change: number; status: DiffStatus }; @@ -71,7 +71,7 @@ export class AssetUploader { } else if (response.status === 200) { if ( this.heuristicFormats.some((format) => - change.path.endsWith(`.${format}`) + change.path.endsWith(`.${format}`), ) ) { /** @@ -94,7 +94,7 @@ export class AssetUploader { } } else { throw new Error( - `Unexpected status code ${response.status} for ${change.path}` + `Unexpected status code ${response.status} for ${change.path}`, ); } @@ -104,7 +104,7 @@ export class AssetUploader { console.log( `${diff.status === DiffStatus.New ? "🟢" : "🟡"} (${ diff.change > 0 ? "+" : "" - }${diff.change.toLocaleString()}B) ${change.path}` + }${diff.change.toLocaleString()}B) ${change.path}`, ); // flush BEFORE going over limit @@ -160,8 +160,8 @@ export class AssetUploader { await new Promise((resolve) => setTimeout( resolve, - Math.max(0, this.time_between_blobs + (start - end)) - ) + Math.max(0, this.time_between_blobs + (start - end)), + ), ); blobs.push({ sha, path: change.path, mode: "100644", type: "blob" }); @@ -170,11 +170,11 @@ export class AssetUploader { break; } catch { console.warn( - ` failed ${change.path}; retrying in ${this.time_per_blob}ms...` + ` failed ${change.path}; retrying in ${this.time_per_blob}ms...`, ); await new Promise((resolve) => - setTimeout(resolve, this.time_per_blob) + setTimeout(resolve, this.time_per_blob), ); } } @@ -209,7 +209,7 @@ export class AssetUploader { [Symbol.dispose]() { if (this.changes.length > 0) { throw new Error( - `${this.changes.length} uncommitted changes. Did you forget to flush?` + `${this.changes.length} uncommitted changes. Did you forget to flush?`, ); } } diff --git a/packages/scripts/src/core/vfs/abstract.ts b/packages/scripts/src/core/vfs/abstract.ts index 19aa59aa1..7f41c1ffe 100644 --- a/packages/scripts/src/core/vfs/abstract.ts +++ b/packages/scripts/src/core/vfs/abstract.ts @@ -11,24 +11,22 @@ export abstract class AbstractVFS { abstract dispose(): void; - abstract has(path: string): boolean; + abstract has(path: string): Promise; abstract raw(path: string): Promise; - abstract paths(): string[]; - - resolve(path: string) { + async resolve(path: string) { const normalized = normalize(path); - if (this.has(normalized)) return normalized; + if (await this.has(normalized)) return normalized; const dvplPath = `${normalized}.dvpl`; - if (this.has(dvplPath)) return dvplPath; + if (await this.has(dvplPath)) return dvplPath; return null; } - assert(path: string) { - const resolved = this.resolve(path); + async assert(path: string) { + const resolved = await this.resolve(path); if (resolved === null) throw new Error(`File not found: ${path}`); @@ -36,7 +34,7 @@ export abstract class AbstractVFS { } async file(file: string) { - const resolved = this.assert(file); + const resolved = await this.assert(file); const raw = await this.raw(resolved); let buffer = raw; @@ -47,21 +45,7 @@ export abstract class AbstractVFS { return buffer; } - dir(path: string) { - const parentSegments = path.split("/").length; - const children = new Set(); - - for (const child of this.paths()) { - if (!child.startsWith(path) || child === path) continue; - - const childSegments = child.split("/"); - const nextSegment = childSegments[parentSegments]; - - children.add(nextSegment); - } - - return Array.from(children); - } + abstract dir(path: string): Promise; async text(path: string) { const file = await this.file(path); diff --git a/packages/scripts/src/core/vfs/local.ts b/packages/scripts/src/core/vfs/local.ts new file mode 100644 index 000000000..7a4dd05ac --- /dev/null +++ b/packages/scripts/src/core/vfs/local.ts @@ -0,0 +1,31 @@ +import { readdir } from "node:fs/promises"; +import { AbstractVFS } from "./abstract"; + +export class LocalVFS extends AbstractVFS { + constructor(private base: string) { + super(); + } + + async init() { + return this; + } + + async has(path: string) { + const file = Bun.file(`${this.base}/${path}`); + return await file.exists(); + } + + async raw(path: string) { + const file = Bun.file(`${this.base}/${path}`); + const buffer = await file.arrayBuffer(); + const array = new Uint8Array(buffer); + + return array; + } + + async dir(path: string) { + return await readdir(`${this.base}/${path}`); + } + + dispose() {} +} diff --git a/packages/scripts/src/core/vfs/steam.ts b/packages/scripts/src/core/vfs/steam.ts index f5dfc25e9..79acd450d 100644 --- a/packages/scripts/src/core/vfs/steam.ts +++ b/packages/scripts/src/core/vfs/steam.ts @@ -76,7 +76,7 @@ export class SteamVFS extends AbstractVFS { return this; } - has(path: string) { + async has(path: string) { return this.manifest.has(path); } @@ -89,8 +89,20 @@ export class SteamVFS extends AbstractVFS { return new Uint8Array(downloaded.file); } - paths() { - return Array.from(this.manifest.keys()); + async dir(path: string) { + const parentSegments = path.split("/").length; + const children = new Set(); + + for (const child of this.manifest.keys()) { + if (!child.startsWith(path) || child === path) continue; + + const childSegments = child.split("/"); + const nextSegment = childSegments[parentSegments]; + + children.add(nextSegment); + } + + return Array.from(children); } dispose() { diff --git a/packages/scripts/src/core/vfs/zip.ts b/packages/scripts/src/core/vfs/zip.ts index 80169182e..e181df62a 100644 --- a/packages/scripts/src/core/vfs/zip.ts +++ b/packages/scripts/src/core/vfs/zip.ts @@ -5,7 +5,10 @@ import { AbstractVFS } from "./abstract"; export class ZipVFS extends AbstractVFS { entries = new Map(); - constructor(private url: string, private root = "app/") { + constructor( + private url: string, + private root = "app/", + ) { super(); } @@ -21,7 +24,7 @@ export class ZipVFS extends AbstractVFS { return this; } - has(path: string) { + async has(path: string) { return this.entries.has(path); } @@ -30,8 +33,20 @@ export class ZipVFS extends AbstractVFS { return new Uint8Array(arrayBuffer); } - paths() { - return Array.from(this.entries.keys()); + async dir(path: string) { + const parentSegments = path.split("/").length; + const children = new Set(); + + for (const child of this.entries.keys()) { + if (!child.startsWith(path) || child === path) continue; + + const childSegments = child.split("/"); + const nextSegment = childSegments[parentSegments]; + + children.add(nextSegment); + } + + return Array.from(children); } dispose() {}