import type { Plugin } from "rollup"; import { DEFAULT_BUILD_FEATURES } from "./defines.ts"; /** * Collect enabled feature flags from defaults + env vars. */ export function getEnabledFeatures(): Set { const envFeatures = Object.keys(process.env) .filter((k) => k.startsWith("FEATURE_")) .map((k) => k.replace("FEATURE_", "")); return new Set([...DEFAULT_BUILD_FEATURES, ...envFeatures]); } // Regex to match feature('FLAG_NAME') calls with string literal arguments const FEATURE_CALL_RE = /feature\s*\(\s*['"]([\w]+)['"]\s*\)/g; /** * Vite/Rollup plugin that replaces `feature('X')` calls with boolean literals * at the transform stage, BEFORE the bundler resolves imports. * * This approach is necessary because some feature-gated code blocks contain * require() calls to files that don't exist (e.g. hunter.js inside * feature('REVIEW_ARTIFACT')). The bundler must see these as dead code * (`if (false) { ... }`) before attempting import resolution. * * Also resolves `import { feature } from 'bun:bundle'` as a virtual module * to prevent "module not found" errors. */ export default function featureFlagsPlugin(): Plugin { const features = getEnabledFeatures(); const virtualModuleId = "bun:bundle"; const resolvedVirtualModuleId = "\0" + virtualModuleId; return { name: "feature-flags", // Resolve bun:bundle as a virtual module (prevents "module not found") resolveId(id) { if (id === virtualModuleId) { return resolvedVirtualModuleId; } }, // Provide a stub export for bun:bundle (unused at runtime after transform) load(id) { if (id === resolvedVirtualModuleId) { return "export function feature(name) { return false; }"; } }, // Replace feature('X') calls with true/false literals at transform time, // and transpile `using` declarations for Node.js compatibility. transform(code, id) { // Skip node_modules if (id.includes("node_modules")) return null; let modified = false; // 1. Replace feature('X') calls with boolean literals let matchCount = 0; let transformed = code.replace(FEATURE_CALL_RE, (match, flagName) => { matchCount++; return features.has(flagName) ? "true" : "false"; }); if (matchCount > 0) modified = true; // 2. Transpile `using _ = expr;` to `const _ = expr;` for Node.js compat. // Node.js v22 does not support `using` declarations (Explicit Resource Management). // Safe because: SLOW_OPERATION_LOGGING is not enabled, so slowLogging returns // a no-op disposable whose [Symbol.dispose]() is empty. if (transformed.includes("using _")) { transformed = transformed.replace(/\busing\s+(_\w*)\s*=/g, "const $1 ="); modified = true; } if (!modified) return null; return { code: transformed, map: null }; }, }; }