From a36ae22f4fd4e611a6f701149f2792ad273949d9 Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:06:37 +0000 Subject: [PATCH 1/2] feat: add blob-url support and integrate blob asset plugin for Vite --- bun.lock | 3 + js/ffmpeg.js | 11 +-- js/global.d.ts | 5 ++ js/taglib.ts | 4 +- package.json | 1 + vite-plugin-blob.ts | 128 +++++++++++++++++++++++++++++++ vite.config.js => vite.config.ts | 2 + 7 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 vite-plugin-blob.ts rename vite.config.js => vite.config.ts (97%) diff --git a/bun.lock b/bun.lock index 7df198d..75cff93 100644 --- a/bun.lock +++ b/bun.lock @@ -20,6 +20,7 @@ "fuse.js": "^7.1.0", "hls.js": "^1.6.15", "jose": "^6.2.0", + "mime": "^4.1.0", "npm": "^11.11.1", "pocketbase": "^0.26.8", "taglib-wasm": "^1.0.5", @@ -1073,6 +1074,8 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="], + "miniflare": ["miniflare@4.20260301.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260301.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-fqkHx0QMKswRH9uqQQQOU/RoaS3Wjckxy3CUX3YGJr0ZIMu7ObvI+NovdYi6RIsSPthNtq+3TPmRNxjeRiasog=="], "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], diff --git a/js/ffmpeg.js b/js/ffmpeg.js index 90be0b9..4bd480a 100644 --- a/js/ffmpeg.js +++ b/js/ffmpeg.js @@ -1,10 +1,7 @@ -import { fetchBlobURL } from './utils'; import FfmpegWorker from './ffmpeg.worker.js?worker'; +import coreJs from '!/@ffmpeg/core/dist/esm/ffmpeg-core.js?blob-url'; +import coreWasm from '!/@ffmpeg/core/dist/esm/ffmpeg-core.wasm?blob-url'; import { FfmpegProgress } from './ffmpeg.types'; -import { ProgressMessage } from './progressEvents'; -const ffmpegBase = 'https://unpkg.com/@ffmpeg/core/dist/esm'; -const coreJs = `${ffmpegBase}/ffmpeg-core.js`; -const coreWasm = `${ffmpegBase}/ffmpeg-core.wasm`; class FfmpegError extends Error { constructor(message) { @@ -19,8 +16,8 @@ export function loadFfmpeg() { loadFfmpeg.promise || (loadFfmpeg.promise = (async () => { const data = { - coreURL: await fetchBlobURL(coreJs), - wasmURL: await fetchBlobURL(coreWasm), + coreURL: await coreJs(), + wasmURL: await coreWasm(), }; return data; diff --git a/js/global.d.ts b/js/global.d.ts index b641500..735d7e0 100644 --- a/js/global.d.ts +++ b/js/global.d.ts @@ -3,6 +3,11 @@ declare module '*?url' { export default content; } +declare module '*?blob-url' { + const urlPromise: () => Promise; + export default urlPromise; +} + declare module 'https://cdn.jsdelivr.net/npm/client-zip@2.4.5/+esm' { /** Creates a ZIP stream from an async iterable of file entries. */ export function downloadZip(files: AsyncIterable): Response; diff --git a/js/taglib.ts b/js/taglib.ts index f7ee443..a8b63f4 100644 --- a/js/taglib.ts +++ b/js/taglib.ts @@ -1,6 +1,6 @@ import { TagLib } from 'taglib-wasm'; import { fetchBlobURL } from './utils'; -import _TagLibWasm from '!/taglib-wasm/dist/taglib-web.wasm?url'; +import _TagLibWasm from '!/taglib-wasm/dist/taglib-web.wasm?blob-url'; import type { AddMetadataMessage, TagLibFileResponse, @@ -12,7 +12,7 @@ import TagLibWorker from './taglib.worker?worker'; let tagLib: Promise | null = null; async function fetchTagLib(): Promise { - return fetchTagLib.blobUrl || (fetchTagLib.blobUrl = await fetchBlobURL(_TagLibWasm)); + return fetchTagLib.blobUrl || (fetchTagLib.blobUrl = await _TagLibWasm()); } namespace fetchTagLib { diff --git a/package.json b/package.json index 912e909..cfa5d4f 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "fuse.js": "^7.1.0", "hls.js": "^1.6.15", "jose": "^6.2.0", + "mime": "^4.1.0", "npm": "^11.11.1", "pocketbase": "^0.26.8", "taglib-wasm": "^1.0.5", diff --git a/vite-plugin-blob.ts b/vite-plugin-blob.ts new file mode 100644 index 0000000..f1e9c44 --- /dev/null +++ b/vite-plugin-blob.ts @@ -0,0 +1,128 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { gzipSync, constants as zlibConstants } from 'zlib'; +import type { Plugin } from 'vite'; +import mime from 'mime'; +import { createHash } from 'crypto'; + +function hashString(input: string, algorithm = "sha256"): string { + return createHash(algorithm) + .update(input, "utf8") // specify string encoding + .digest("hex"); // return as hex +} + +/** + * Vite plugin enabling `?blob-url` imports. + * + * Example: + * import getBlobUrl from "./file.bin?blob-url"; + * const blobUrl = await getBlobUrl(); + * + * Behavior: + * - Compresses the asset using max gzip compression + * - Build: emits compressed asset + * - Dev: serves compressed asset from middleware + * - Runtime fetches + decompresses it and returns an object URL + */ +export default function blobAssetPlugin(): Plugin { + const devAssets = new Map(); + + return { + name: 'vite-blob-asset', + + async load(id) { + if (!id.includes('?blob-url')) return; + + const [filepath] = id.split('?'); + const absPath = path.resolve(filepath); + + const input = await fs.readFile(absPath); + + /** gzip with maximum compression */ + const compressed = gzipSync(input, { + level: zlibConstants.Z_BEST_COMPRESSION, + }); + + let assetUrl: string; + + if (this.meta.watchMode) { + /** dev server path */ + assetUrl = `/@blob/${hashString(absPath)}/${path.basename(filepath)}.gz`; + devAssets.set(assetUrl, compressed); + } else { + /** build asset */ + const refId = this.emitFile({ + type: 'asset', + name: path.basename(filepath) + '.gz', + source: compressed, + }); + + assetUrl = `__BLOB_ASSET_${refId}__`; + } + + return ` +const assetUrl = ${JSON.stringify(assetUrl)}; + +/** + * Decompress gzip data using browser DecompressionStream + */ +async function decompress(buffer) { + const ds = new DecompressionStream("gzip"); + const stream = new Response(buffer).body.pipeThrough(ds); + return new Response(stream).arrayBuffer(); +} + +let blobPromise = null; + +export default function getBlobUrl() { + if (blobPromise) return blobPromise; + + return blobPromise = (async () => { + try { + const res = await fetch(assetUrl); + const compressed = await res.arrayBuffer(); + + const decompressed = await decompress(compressed); + + const blob = new Blob([decompressed], ${JSON.stringify({ + type: mime.getType(filepath), + })}); + return URL.createObjectURL(blob); + } catch (err) { + console.error("Error loading blob asset:", err); + blobPromise = null; + throw err; + } + })() +} +`; + }, + + resolveFileUrl({ referenceId }) { + return `"${this.getFileName(referenceId)}"`; + }, + + generateBundle(_, bundle) { + for (const chunk of Object.values(bundle)) { + if (chunk.type !== 'chunk') continue; + + chunk.code = chunk.code.replace( + /"__BLOB_ASSET_(.*?)__"/g, + (_, refId) => `"${this.getFileName(refId)}"` + ); + } + }, + + configureServer(server) { + server.middlewares.use((req, res, next) => { + if (!req.url?.startsWith('/@blob/')) return next(); + + const data = devAssets.get(req.url); + if (!data) return next(); + + res.setHeader('Content-Type', 'application/gzip'); + res.end(data); + }); + }, + }; +} diff --git a/vite.config.js b/vite.config.ts similarity index 97% rename from vite.config.js rename to vite.config.ts index 9ecd444..8b2475f 100644 --- a/vite.config.js +++ b/vite.config.ts @@ -4,6 +4,7 @@ import neutralino from 'vite-plugin-neutralino'; import authGatePlugin from './vite-plugin-auth-gate.js'; import path from 'path'; import uploadPlugin from './vite-plugin-upload.js'; +import blobAssetPlugin from './vite-plugin-blob.js'; export default defineConfig(({ mode }) => { const IS_NEUTRALINO = mode === 'neutralino'; @@ -42,6 +43,7 @@ export default defineConfig(({ mode }) => { IS_NEUTRALINO && neutralino(), authGatePlugin(), uploadPlugin(), + blobAssetPlugin(), VitePWA({ registerType: 'prompt', workbox: { From f6fac62593e8f25c3f9bf2564067021c811a24b8 Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:46:55 -0500 Subject: [PATCH 2/2] Fix assetUrl assignment in fetch call --- vite-plugin-blob.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/vite-plugin-blob.ts b/vite-plugin-blob.ts index f1e9c44..e5bfff1 100644 --- a/vite-plugin-blob.ts +++ b/vite-plugin-blob.ts @@ -61,8 +61,6 @@ export default function blobAssetPlugin(): Plugin { } return ` -const assetUrl = ${JSON.stringify(assetUrl)}; - /** * Decompress gzip data using browser DecompressionStream */ @@ -79,7 +77,7 @@ export default function getBlobUrl() { return blobPromise = (async () => { try { - const res = await fetch(assetUrl); + const res = await fetch(${JSON.stringify(assetUrl)}); const compressed = await res.arrayBuffer(); const decompressed = await decompress(compressed);