forked from edideaur/monochrome
-
-
Notifications
You must be signed in to change notification settings - Fork 421
Expand file tree
/
Copy pathvite-plugin-blob.ts
More file actions
130 lines (108 loc) · 3.82 KB
/
Copy pathvite-plugin-blob.ts
File metadata and controls
130 lines (108 loc) · 3.82 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
127
128
129
130
import fs from 'fs/promises';
import path from 'path';
import { gzipSync, constants as zlibConstants } from 'zlib';
import type { Plugin, ResolvedConfig } 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>();
let resolvedConfig: ResolvedConfig | null = null;
return {
name: 'vite-blob-asset',
async configResolved(config: ResolvedConfig) {
resolvedConfig = config;
},
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 (resolvedConfig?.command === 'serve') {
/** 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: string) => `"${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);
});
},
};
}