@astropub/vite-plugin-file
Version:
A Vite plugin for seamless access to file metadata and data streams, supporting modern Web API interfaces like Blob, File, and ArrayBuffer
138 lines (120 loc) • 3.96 kB
JavaScript
const prefix = "\x00file:"
const suffix = "?file"
export default function viteFilePlugin() {
/** @type {import('vite').Plugin} */
const plugin = {
name: "vite-plugin-file",
enforce: "pre",
async resolveId(source, importer, options) {
if (source.endsWith(suffix)) {
/** @type {{ id: string | null | undefined }} */
const { id } = Object(
await this.resolve(source.slice(0, -suffix.length), importer, {
skipSelf: true,
...Object(options),
}),
)
if (id) {
return prefix + id + suffix
}
}
},
load(id) {
if (id.startsWith(prefix) && id.endsWith(suffix)) {
const path = id.slice(prefix.length, -suffix.length)
const name = path.split("/").pop()
const extn = path.replace(/.*(\.[^.]+)$/, "$1").toLowerCase()
const type = contentTypes[extn] || null
const code = [
`import { createReadStream, promises } from "node:fs"`,
`import { Readable } from "node:stream"`,
// internal properties
`const path = ${JSON.stringify(path)}`,
// properties
`export const name = ${JSON.stringify(name)}`,
`export const type = ${JSON.stringify(type)}`,
`export const { mtimeMs: lastModified, size } = await promises.stat(path).catch(() => ({ mtimeMs: Date.now(), size: 0 })) // ""`,
// computed properties
`export const headers = new Headers({ "content-type": type, "content-length": size.toString(), "last-modified": new Date(lastModified).toUTCString() })`,
// methods
`export const arrayBuffer = () => promises.readFile(path).then(buffer => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)) // ""`,
`export const blob = () => arrayBuffer().then(arrayBuffer => new Blob([arrayBuffer], { type })) // ""`,
`export const file = () => arrayBuffer().then(arrayBuffer => new File([arrayBuffer], name, { type, lastModified })) // ""`,
`export const json = () => text().then(JSON.parse) // ""`,
`export const response = () => new Response(stream(), { headers }) // ""`,
`export const stream = () => Readable.toWeb(createReadStream(path)) // ""`,
`export const text = () => promises.readFile(path, "utf-8")`,
// default export
`export default { name, type, size, lastModified, headers, arrayBuffer, blob, file, json, response, stream, text } // ""`,
].join("\n")
return {
code,
}
}
},
}
return plugin
}
/** @type {Record<string, string>} */
const contentTypes = {
// text
".css": "text/css",
".csv": "text/csv",
".html": "text/html",
".js": "text/javascript",
".md": "text/markdown",
".txt": "text/plain",
".xml": "text/xml",
".yaml": "text/yaml",
".yml": "text/yaml",
// images
".avif": "image/avif",
".gif": "image/gif",
".heic": "image/heic",
".ico": "image/x-icon",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".jxl": "image/jxl",
".png": "image/png",
".svg": "image/svg+xml",
".tif": "image/tiff",
".tiff": "image/tiff",
".webp": "image/webp",
// video
".mkv": "video/x-matroska",
".mov": "video/quicktime",
".mp4": "video/mp4",
".webm": "video/webm",
// audio
".aac": "audio/aac",
".flac": "audio/flac",
".m4a": "audio/x-m4a",
".mp3": "audio/mpeg",
".ogg": "audio/ogg",
".opus": "audio/opus",
".wav": "audio/wav",
".weba": "audio/webm",
// model
".glb": "model/gltf-binary",
".gltf": "model/gltf+json",
".stl": "model/stl",
// fonts
".otf": "font/otf",
".ttf": "font/ttf",
".woff": "font/woff",
".woff2": "font/woff2",
// application data
".7z": "application/x-7z-compressed",
".apk": "application/vnd.android.package-archive",
".br": "application/x-brotli",
".deb": "application/vnd.debian.binary-package",
".gz": "application/gzip",
".json": "application/json",
".jsonld": "application/ld+json",
".pdf": "application/pdf",
".rpm": "application/x-rpm",
".tar": "application/x-tar",
".wasm": "application/wasm",
".zip": "application/zip",
".zst": "application/zstd",
}