UNPKG

bun-plugin-dtsx

Version:

A Bun Bundler plugin that auto generates your DTS types extremely fast.

336 lines (331 loc) 9.55 kB
// @bun var __require = import.meta.require; // src/index.ts import { createHash } from "crypto"; import { existsSync, readFileSync, writeFileSync } from "fs"; import { dirname, join, relative, resolve, sep } from "path"; import process from "process"; import { generate } from "@stacksjs/dtsx/generator"; var PluginErrorCodes = { CONFIG_ERROR: "CONFIG_ERROR", GENERATION_ERROR: "GENERATION_ERROR", FILE_ERROR: "FILE_ERROR", CACHE_ERROR: "CACHE_ERROR", TIMEOUT_ERROR: "TIMEOUT_ERROR" }; class DtsxPluginError extends Error { code; context; cause; constructor(message, code, context, cause) { super(message); this.name = "DtsxPluginError"; this.code = code; this.context = context; this.cause = cause; if (Error.captureStackTrace) { Error.captureStackTrace(this, this.constructor); } } toJSON() { return { name: this.name, code: this.code, message: this.message, context: this.context, stack: this.stack }; } } class BuildEventEmitter { listeners = new Map; on(type, listener) { const existing = this.listeners.get(type) || []; existing.push(listener); this.listeners.set(type, existing); } async emit(type, data) { const event = { type, timestamp: Date.now(), data }; const listeners = this.listeners.get(type) || []; for (const listener of listeners) { await listener(event); } } } class IncrementalCache { manifest; cacheDir; manifestPath; constructor(cacheDir, configHash) { this.cacheDir = cacheDir; this.manifestPath = join(cacheDir, "manifest.json"); this.manifest = this.loadManifest(configHash); } loadManifest(configHash) { try { if (existsSync(this.manifestPath)) { const data = JSON.parse(readFileSync(this.manifestPath, "utf-8")); if (data.configHash === configHash && data.version === "1.0") { return data; } } } catch {} return { version: "1.0", configHash, entries: {} }; } save() { try { const { mkdirSync } = __require("fs"); mkdirSync(this.cacheDir, { recursive: true }); writeFileSync(this.manifestPath, JSON.stringify(this.manifest, null, 2)); } catch {} } getEntry(filePath) { return this.manifest.entries[filePath]; } setEntry(filePath, entry) { this.manifest.entries[filePath] = entry; } isValid(filePath, currentHash) { const entry = this.getEntry(filePath); return entry?.hash === currentHash; } clear() { this.manifest.entries = {}; this.save(); } } function computeConfigHash(config) { const relevantConfig = { root: config.root, outdir: config.outdir, entrypoints: config.entrypoints, clean: config.clean, tsconfigPath: config.tsconfigPath }; return createHash("md5").update(JSON.stringify(relevantConfig)).digest("hex"); } var cleanedOutdirs = new Set; function dts(options = {}) { const { onSuccess, onError, failOnError = true, incremental = false, cacheDir = ".dtsx-cache", on, timeout = 60000, continueOnError = false, verbose = false, ...dtsOptions } = options; const emitter = new BuildEventEmitter; if (on) { for (const [type, listener] of Object.entries(on)) { if (listener) { emitter.on(type, listener); } } } return { name: "bun-plugin-dtsx", async setup(build) { const startTime = Date.now(); let cache = null; const fromCache = 0; try { const config = normalizeConfig(dtsOptions, build); const configHash = computeConfigHash(config); if (config.clean) { const outdirPath = resolve(config.cwd ?? process.cwd(), config.outdir ?? "./dist"); if (cleanedOutdirs.has(outdirPath)) { config.clean = false; } else { cleanedOutdirs.add(outdirPath); } } if (incremental) { cache = new IncrementalCache(resolve(cacheDir), configHash); } await emitter.emit("start", { type: "start", files: config.entrypoints || [], config }); if (verbose) { console.log("[bun-plugin-dtsx] Starting declaration generation..."); if (incremental) { console.log("[bun-plugin-dtsx] Incremental mode enabled"); } } const generateWithTimeout = async () => { return new Promise((resolvePromise, rejectPromise) => { const timeoutId = setTimeout(() => { rejectPromise(new DtsxPluginError(`Generation timed out after ${timeout}ms`, "TIMEOUT_ERROR", { timeout })); }, timeout); generate(config).then((stats2) => { clearTimeout(timeoutId); resolvePromise(stats2); }).catch((err) => { clearTimeout(timeoutId); rejectPromise(err); }); }); }; const stats = await generateWithTimeout(); const duration = Date.now() - startTime; if (cache) { cache.save(); } await emitter.emit("complete", { type: "complete", stats, duration, fromCache }); if (verbose) { console.log(`[bun-plugin-dtsx] Generation complete in ${duration}ms`); if (fromCache > 0) { console.log(`[bun-plugin-dtsx] ${fromCache} files served from cache`); } } if (onSuccess) { await onSuccess(stats); } } catch (error) { const pluginError = wrapError(error); await emitter.emit("error", { type: "error", error: pluginError }); if (onError) { await onError(pluginError); } else { console.error("[bun-plugin-dtsx] Error generating declarations:"); console.error(` Code: ${pluginError.code}`); console.error(` Message: ${pluginError.message}`); if (pluginError.context) { console.error(` Context: ${JSON.stringify(pluginError.context)}`); } if (verbose && pluginError.stack) { console.error(` Stack: ${pluginError.stack}`); } } if (failOnError && !continueOnError) { throw pluginError; } } } }; } function wrapError(error) { if (error instanceof DtsxPluginError) { return error; } if (error instanceof Error) { let code = "GENERATION_ERROR"; if (error.message.includes("config") || error.message.includes("Config")) { code = "CONFIG_ERROR"; } else if (error.message.includes("file") || error.message.includes("File") || error.message.includes("ENOENT")) { code = "FILE_ERROR"; } return new DtsxPluginError(error.message, code, { originalError: error.name }, error); } return new DtsxPluginError(String(error), "GENERATION_ERROR"); } function commonParentDir(paths) { const dirs = paths.map((p) => dirname(p).split(sep)); let common = dirs[0]; for (const parts of dirs.slice(1)) { let i = 0; while (i < common.length && i < parts.length && common[i] === parts[i]) i++; common = common.slice(0, i); } return common.join(sep) || sep; } function normalizeConfig(options, build) { const cwd = options.cwd || process.cwd(); const bunEntrypoints = build?.config?.entrypoints; const outdir = options.outdir || options.build?.config.outdir || build?.config.outdir || "./dist"; let root = options.root || options.build?.config.root || build?.config.root; if (!root && bunEntrypoints?.length) { const ancestor = commonParentDir(bunEntrypoints.map((ep) => resolve(cwd, ep))); root = relative(cwd, ancestor) || "."; } root = root || "./src"; let entrypoints = options.entrypoints; if (!entrypoints) { if (bunEntrypoints?.length) { const resolvedRoot = resolve(cwd, root); const inside = []; for (const ep of bunEntrypoints) { const resolved = resolve(cwd, ep); if (resolved === resolvedRoot || resolved.startsWith(resolvedRoot + sep)) { inside.push(relative(resolvedRoot, resolved)); } else { console.warn(`[bun-plugin-dtsx] Skipping entrypoint outside root "${root}": ${ep}`); } } entrypoints = inside.length ? inside : ["index.ts"]; } else { entrypoints = ["index.ts"]; } } return { ...options, cwd, root, entrypoints, outdir, clean: options.clean ?? true, tsconfigPath: options.tsconfigPath }; } function dtsWatch(options = {}) { return dts({ ...options, incremental: true, verbose: options.verbose ?? true }); } function dtsCheck(options) { return { name: "bun-plugin-dtsx-check", async setup(build) { const config = normalizeConfig({ ...options, outdir: "" }, build); try { await generate({ ...config, clean: false }); } catch (error) { const pluginError = wrapError(error); if (options.onError) { await options.onError(pluginError); } if (options.failOnError !== false) { throw pluginError; } } } }; } function clearCache(cacheDir = ".dtsx-cache") { const cache = new IncrementalCache(resolve(cacheDir), ""); cache.clear(); } var src_default = dts; export { dtsWatch, dtsCheck, dts, src_default as default, clearCache, PluginErrorCodes, DtsxPluginError };