@arcjet/rollup-config
Version:
Custom rollup config for Arcjet projects
221 lines (201 loc) • 7.28 kB
JavaScript
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { isBuiltin } from "node:module";
import replace from "@rollup/plugin-replace";
import typescript from "@rollup/plugin-typescript";
function generateJs(wasm) {
const disclaimer = `
/**
* This file contains an Arcjet Wasm binary inlined as a base64
* [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs)
* with the application/wasm MIME type.
*
* This was chosen to save on storage space over inlining the file directly as
* a Uint8Array, which would take up ~3x the space of the Wasm file. See
* https://blobfolio.com/2019/better-binary-batter-mixing-base64-and-uint8array/
* for more details.
*
* It is then decoded into an ArrayBuffer to be used directly via WebAssembly's
* \`compile()\` function in our entry point file.
*
* This is all done to avoid trying to read or bundle the Wasm asset in various
* ways based on the platform or bundler a user is targeting. One example being
* that Next.js requires special \`asyncWebAssembly\` webpack config to load our
* Wasm file if we don't do this.
*
* In the future, we hope to do away with this workaround when all bundlers
* properly support consistent asset bundling techniques.
*/
`;
return `// @generated by wasm2module - DO NOT EDIT
/* eslint-disable */
// @ts-nocheck
${disclaimer}
const wasmBase64 = "data:application/wasm;base64,${wasm.toString("base64")}";
/**
* Returns a WebAssembly.Module for an Arcjet Wasm binary, decoded from a base64
* Data URL.
*/
// TODO: Switch back to top-level await when our platforms all support it
export async function wasm() {
// This uses fetch to decode the wasm data url, but disabling cache so files
// larger than 2mb don't fail to parse in the Next.js App Router
const wasmDecode = await fetch(wasmBase64, { cache: "no-store" });
const buf = await wasmDecode.arrayBuffer();
// And then we return it as a WebAssembly.Module
return WebAssembly.compile(buf);
}
`;
}
function wasmToModule() {
const idToWasmPath = new Map();
return {
name: "base64-wasm",
resolveId(source) {
if (source.endsWith(".wasm?js")) {
// Slice off the `?js` to make it a valid path
const filepath = source.slice(0, -3);
// Create a "virtual module", prefixed with `\0` as per the Rollup docs,
// for our replacement import
const id = `\0${filepath.replace(/\.wasm$/, ".js")}`;
// Store the actual Wasm path against the virtual module ID.
idToWasmPath.set(id, filepath);
return id;
}
return null;
},
async load(id) {
const wasmPath = idToWasmPath.get(id);
// If we resolved this `id` during the `resolveId` phase, generate the
// JavaScript file with the base64 Wasm and loading helper
if (wasmPath) {
const wasm = await fs.promises.readFile(wasmPath);
return generateJs(wasm);
}
return null;
},
};
}
function removeExtension(filename) {
return path.basename(filename, path.extname(filename));
}
function isTypeScript(file) {
return (
file.isFile() && file.name.endsWith(".ts") && !file.name.endsWith(".d.ts")
);
}
export function createConfig(root, { plugins = [] } = {}) {
const packageJson = fileURLToPath(new URL("./package.json", root));
const pkg = JSON.parse(fs.readFileSync(packageJson));
const dependencies = Object.keys(pkg.dependencies ?? {});
const devDependencies = Object.keys(pkg.devDependencies ?? {});
const peerDependencies = Object.keys(pkg.peerDependencies ?? {});
function isDependency(id) {
return dependencies.some((dep) => id.startsWith(dep));
}
function isDevDependency(id) {
return devDependencies.some((dep) => id.startsWith(dep));
}
function isPeerDependency(id) {
return peerDependencies.some((dep) => id.startsWith(dep));
}
function isBunBuiltin(id) {
return id === "bun";
}
function isSvelteKitBuiltin(id) {
return id === "$env/dynamic/private";
}
const rootDir = fileURLToPath(new URL(".", root));
const testDir = fileURLToPath(new URL("test/", root));
const typescriptDirs = [rootDir, testDir];
// Creates a Rollup input entry that can be used with `Object.fromEntries()`
function toEntry(file) {
return [
path.relative(rootDir, `${file.path}${removeExtension(file.name)}`),
path.relative(rootDir, `${file.path}${file.name}`),
];
}
// Find all TypeScript files in the specified directories
const input = Object.fromEntries(
typescriptDirs.flatMap((dir) => {
// All the directories might not exist in every project
try {
const files = fs.readdirSync(dir, { withFileTypes: true });
return files.filter(isTypeScript).map(toEntry);
} catch {
return [];
}
}),
);
return {
input,
output: {
dir: ".",
format: "es",
// Hoist transitive imports for faster loading. For more details, see
// https://rollupjs.org/faqs/#why-do-additional-imports-turn-up-in-my-entry-chunks-when-code-splitting
hoistTransitiveImports: true,
// Stop rollup from creating additional chunks that it thinks we need
preserveModules: true,
},
external(id) {
return (
isBuiltin(id) ||
isDependency(id) ||
isDevDependency(id) ||
isPeerDependency(id) ||
isBunBuiltin(id) ||
isSvelteKitBuiltin(id)
);
},
plugins: [
// Replace always comes first to ensure the replaced values exist for all
// other plugins
replace({
values: {
// We always replace `__ARCJET_SDK_VERSION__` based on the version
// in package.json
__ARCJET_SDK_VERSION__: pkg.version,
},
preventAssignment: true,
}),
typescript({
tsconfig: "./tsconfig.json",
// Override the `excludes` specified in the tsconfig so we don't
// generate definition files for our tests
exclude: ["node_modules", "test/*.ts"],
declaration: true,
declarationDir: ".",
noEmitOnError: true,
}),
typescript({
tsconfig: "./tsconfig.json",
// Override the `includes` specified in the tsconfig so we don't
// generate definition files for our tests
include: "test/*.ts",
noEmitOnError: true,
}),
{
name: "externalize-wasm",
resolveId(id) {
// Vercel uses the `.wasm?module` suffix to make WebAssembly available
// in their Vercel Functions product.
// https://vercel.com/docs/functions/wasm#using-a-webassembly-file
//
// The Cloudflare docs say they support the `.wasm?module` suffix, but
// that seems to no longer be the case with Wrangler 2 so we need to
// have separate imports for just the `.wasm` files.
//
// https://developers.cloudflare.com/workers/runtime-apis/webassembly/javascript/#bundling
if (id.endsWith(".wasm") || id.endsWith(".wasm?module")) {
return { id, external: true };
}
return null;
},
},
wasmToModule(),
...plugins,
],
};
}