feat: add blob-url support and integrate blob asset plugin for Vite
This commit is contained in:
parent
39b5090a67
commit
a36ae22f4f
7 changed files with 145 additions and 9 deletions
3
bun.lock
3
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=="],
|
||||
|
|
|
|||
11
js/ffmpeg.js
11
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;
|
||||
|
|
|
|||
5
js/global.d.ts
vendored
5
js/global.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
128
vite-plugin-blob.ts
Normal 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);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -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: {
|
||||
Loading…
Reference in a new issue