feat: add blob-url support and integrate blob asset plugin for Vite

This commit is contained in:
Daniel 2026-03-12 22:06:37 +00:00 committed by GitHub
parent 39b5090a67
commit a36ae22f4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 145 additions and 9 deletions

View file

@ -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=="],

View file

@ -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;

5
js/global.d.ts vendored
View file

@ -3,6 +3,11 @@ declare module '*?url' {
export default content;
}
declare module '*?blob-url' {
const urlPromise: () => Promise<string>;
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<object>): Response;

View file

@ -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<TagLib> | null = null;
async function fetchTagLib(): Promise<string> {
return fetchTagLib.blobUrl || (fetchTagLib.blobUrl = await fetchBlobURL(_TagLibWasm));
return fetchTagLib.blobUrl || (fetchTagLib.blobUrl = await _TagLibWasm());
}
namespace fetchTagLib {

View file

@ -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",

128
vite-plugin-blob.ts Normal file
View file

@ -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<string, Buffer>();
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);
});
},
};
}

View file

@ -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: {