bun-plugin-dtsx
Version:
A Bun Bundler plugin that auto generates your DTS types extremely fast.
336 lines (331 loc) • 9.55 kB
JavaScript
// @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
};