UNPKG

vite-plugin-trunk

Version:

Seamlessly embedding WASM components in a Vite project via Trunk.

241 lines (240 loc) 7.9 kB
// src/index.ts import { $ } from "execa"; import fs from "fs-extra"; import path from "path"; import { load as jsTomlLoad } from "js-toml"; function vitePluginTrunk(vitePluginTruckConfig) { let config; let logger; const name = "vite-plugin-trunk"; const debug = Boolean(vitePluginTruckConfig?.debug); const getTrunkDistDir = () => ( // default to /node_modules/.vite/.trunk `${config.cacheDir}/.trunk` ); const getTrunkDevArgs = () => [ "--no-minification", `--dist=${getTrunkDistDir()}`, "--no-sri", `--filehash=true` ]; const getTrunkProdArgs = () => [ "--release", `--dist=${getTrunkDistDir()}`, `--filehash=true` ]; const getTrunkCache = async () => { const files = await fs.readdir(getTrunkDistDir(), { withFileTypes: true }); const wasmFiles = files.filter((file) => file.name.endsWith(".wasm")); const jsFiles = files.filter((file) => file.name.endsWith(".js")); return { wasmFiles, jsFiles }; }; const getTrunkCacheJsSync = () => { const files = fs.readdirSync(getTrunkDistDir(), { withFileTypes: true }); const jsFiles = files.filter((file) => file.name.endsWith(".js")); return jsFiles; }; const getTrunkCacheWasmSync = () => { const files = fs.readdirSync(getTrunkDistDir(), { withFileTypes: true }); const wasmFiles = files.filter((file) => file.name.endsWith(".wasm")); return wasmFiles; }; let cargoPackageName = void 0; return { name, async config() { try { const cargoToml = jsTomlLoad( (await fs.readFile(path.resolve(process.cwd(), "Cargo.toml"))).toString() ); if (!cargoToml?.package?.name) { throw new Error("Cargo.toml is missing package name"); } cargoPackageName = cargoToml.package.name; return {}; } catch (error) { throw new Error("Can't find Cargo.toml at project root", { cause: error }); } }, configResolved(resolvedConfig) { config = resolvedConfig; logger = config.logger; }, async transformIndexHtml(html) { if (config.command === "serve") { const { wasmFiles, jsFiles } = await getTrunkCache(); if (wasmFiles[0]?.name && jsFiles[0]?.name) { if (debug) { console.debug( `[${name}] injecting wasm file ${wasmFiles[0].name} and js file ${jsFiles[0].name} into index.html` ); } return { html, tags: [ { tag: "link", attrs: { rel: "preload", href: `/${wasmFiles[0].name}`, as: "fetch", type: "application/wasm" }, injectTo: "head" }, { tag: "link", attrs: { rel: "modulepreload", href: `/${jsFiles[0].name}` }, injectTo: "head" }, { tag: "script", attrs: { type: "module" }, children: `import init, * as bindings from '/${jsFiles[0].name}'; init('/${wasmFiles[0].name}'); window.wasmBindings = bindings;`, injectTo: "body" } ] }; } } return html; }, async buildStart() { if (config.command === "serve") { await fs.ensureDir(getTrunkDistDir()); logger.info( `trunk building wasm modules (can be slow on first start)`, { timestamp: true } ); await $`trunk build ${getTrunkDevArgs()}`; logger.info(`\u{1F980} trunk successfully built wasm on start`, { timestamp: true, clear: true }); } }, async handleHotUpdate({ file, modules, server }) { if (file.indexOf(".rs") > 0 && file.indexOf("target") < 0) { if (debug) { console.debug(`[${name}] recompiling ${file}`); } try { await $`trunk build ${getTrunkDevArgs()}`; logger.info(`${file} recompiled successfully.`, { timestamp: true }); server.ws.send({ type: "full-reload", path: "*" }); } catch (error) { logger.error(error?.stderr ?? "unknown trunk error", { timestamp: true }); server.ws.send({ type: "error", err: { message: error?.stderr, stack: "", plugin: name } }); } return []; } return modules; }, configureServer(server) { return () => { server.middlewares.use((req, res, next) => { if (cargoPackageName && req.originalUrl?.includes(cargoPackageName) && req.originalUrl.includes(".wasm")) { const wasmName = getTrunkCacheWasmSync()[0]?.name; if (wasmName) { if (debug) { console.debug(`[${name}] serving wasm file ${wasmName}`); } res.setHeader( "Cache-Control", "no-cache, no-store, must-revalidate" ); res.writeHead(200, { "Content-Type": "application/wasm" }); res.end(fs.readFileSync(`${getTrunkDistDir()}/${wasmName}`)); next(); } } if (cargoPackageName && req.originalUrl?.includes(cargoPackageName) && req.originalUrl.includes(".js")) { const jsName = getTrunkCacheJsSync()[0]?.name; if (jsName) { if (debug) { console.debug(`[${name}] serving js file ${jsName}`); } res.setHeader( "Cache-Control", "no-cache, no-store, must-revalidate" ); res.writeHead(200, { "Content-Type": "application/javascript" }); res.end(fs.readFileSync(`${getTrunkDistDir()}/${jsName}`)); next(); } } next(); }); }; }, async closeBundle() { if (config.command === "build") { logger.info(`\u{1F980} copying intermediate artifact _index.html`, { timestamp: true }); await fs.copyFile(`${config.build.outDir}/index.html`, `_index.html`); logger.info(`\u{1F980} trunk creating production build...`, { timestamp: true }); await $`trunk build ${getTrunkProdArgs()} _index.html`; logger.info(`\u{1F980} removing intermediate artifact _index.html`, { timestamp: true }); await fs.remove("_index.html"); logger.info(`\u{1F980} copying built index.html to ${config.build.outDir}`, { timestamp: true }); await fs.copyFile( `${getTrunkDistDir()}/index.html`, `${config.build.outDir}/index.html` ); const { wasmFiles, jsFiles } = await getTrunkCache(); for (const wasmFile of wasmFiles) { logger.info(`\u{1F980} copying built wasm to ${config.build.outDir}`, { timestamp: true }); await fs.copyFile( `${getTrunkDistDir()}/${wasmFile.name}`, `${config.build.outDir}/${wasmFile.name}` ); } for (const jsFile of jsFiles) { logger.info(`\u{1F980} copying built js to ${config.build.outDir}`, { timestamp: true }); await fs.copyFile( `${getTrunkDistDir()}/${jsFile.name}`, `${config.build.outDir}/${jsFile.name}` ); } logger.info(`\u{1F980} trunk successfully built wasm modules`, { timestamp: true }); } } }; } export { vitePluginTrunk };