-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvite-plugin-blob.ts
More file actions
126 lines (104 loc) · 3.63 KB
/
Copy pathvite-plugin-blob.ts
File metadata and controls
126 lines (104 loc) · 3.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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 `
/**
* 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(${JSON.stringify(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);
});
},
};
}