@hypernym/bundler
Version:
ESM & TS module bundler.
252 lines (251 loc) • 7.95 kB
JavaScript
import process, { cwd } from "node:process";
import { parse, resolve } from "node:path";
import { stat } from "node:fs/promises";
import { dim } from "@hypernym/colors";
import { isUndefined } from "@hypernym/utils";
import { copy, readdir, write } from "@hypernym/utils/fs";
import { rolldown } from "rolldown";
import { dts } from "rolldown-plugin-dts";
import { outputPaths } from "../plugins/index.js";
//#region src/bin/meta.ts
const name = `Hyperbundler`;
//#endregion
//#region src/utils/logger.ts
const cl = console.log;
const separator = `|`;
const logger = {
info: (...args) => {
cl(name, dim(separator), ...args);
},
error: (...args) => {
cl();
cl(name, dim(separator), ...args);
cl();
},
exit: (message) => {
cl();
cl(name, dim(separator), message);
cl();
return process.exit();
}
};
//#endregion
//#region src/utils/error.ts
function error(err) {
logger.error("Something went wrong...");
console.error(err);
return process.exit();
}
//#endregion
//#region src/utils/format-ms.ts
function formatMs(ms) {
const s = 1e3;
const m = s * 60;
const h = m * 60;
const msAbs = Math.abs(ms);
if (msAbs >= h) return `${(ms / h).toFixed(2)}h`;
if (msAbs >= m) return `${(ms / m).toFixed(2)}m`;
if (msAbs >= s) return `${(ms / s).toFixed(2)}s`;
return `${ms}ms`;
}
//#endregion
//#region src/utils/format-bytes.ts
function formatBytes(bytes) {
const decimals = 2;
const units = [
"B",
"KB",
"MB",
"GB",
"TB"
];
if (bytes === 0) return `0 B`;
const k = 1024;
const dm = decimals;
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${units[i]}`;
}
//#endregion
//#region src/utils/get-output-path.ts
function getOutputPath(outDir, input, { extension = "auto" } = {}) {
const src = input.startsWith("./") ? input.slice(2) : input;
let output = src.includes("/") ? src.replace(src.split("/")[0], outDir) : `${outDir}/${src}`;
if (extension !== "original") {
const ext = output.match(/\.[^/.]+$/)?.[0] ?? "";
const esm = [
".js",
".mjs",
".ts",
".mts"
];
const legacy = [".cjs", ".cts"];
let newExt = "";
if (esm.includes(ext)) newExt = extension === "dts" ? ".d.ts" : ".js";
else if (legacy.includes(ext)) newExt = extension === "dts" ? ".d.cts" : ".cjs";
if (newExt) output = `${output.slice(0, -ext.length)}${newExt}`;
}
return outDir.startsWith("./") || outDir.startsWith("../") ? output : `./${output}`;
}
function parseOutputPath(path) {
if (!path) return;
if (path.startsWith("./")) return path;
else return `./${path}`;
}
//#endregion
//#region src/bin/build.ts
function logEntryStats(stats) {
const cl = console.log;
const base = parse(stats.path).base;
const path = stats.path.replace(base, "");
let format = stats.format;
if (format === "commonjs") format = "cjs";
if (format === "es" || format === "module") format = "esm";
const output = dim(path) + base;
const outputLength = output.length + 2;
cl(dim("+"), format.padEnd(5), output.padEnd(outputLength), dim(`[${formatBytes(stats.size)}, ${formatMs(stats.buildTime)}]`));
if (stats.logs) for (const log of stats.logs) cl("!", log.level.padEnd(5), output.padEnd(outputLength), dim(log.log.message));
}
async function build(options) {
const { cwd: cwdir = cwd(), outDir = "dist", entries, externals, tsconfig, hooks, minify, comments } = options;
let start = 0;
const buildStats = {
cwd: cwdir,
size: 0,
buildTime: 0,
files: []
};
await hooks?.["build:start"]?.(options, buildStats);
if (entries) {
start = Date.now();
for (const entry of entries) {
const entryStart = Date.now();
if (entry.input || entry.dts) {
const buildLogs = [];
const isChunk = !isUndefined(entry.input);
const outputRawPath = parseOutputPath(entry.output) || getOutputPath(outDir, entry.input || entry.dts, { extension: isChunk ? "auto" : "dts" });
const outputResolvePath = resolve(cwdir, outputRawPath);
let format = entry.format || "esm";
if (outputRawPath.endsWith(".cjs")) format = "cjs";
const entryInput = {
input: resolve(cwdir, entry.input || entry.dts),
external: entry.externals || externals,
plugins: isChunk ? entry.plugins : entry.plugins || [dts({
...entry.dtsPlugin,
emitDtsOnly: true
})],
checks: { pluginTimings: false },
onLog: (level, log, handler) => {
if (entry.onLog) entry.onLog(level, log, handler, buildLogs);
else buildLogs.push({
level,
log
});
},
resolve: entry.resolve,
transform: {
define: entry.define,
inject: entry.inject
},
tsconfig: entry.tsconfig || tsconfig
};
const entryOutput = {
file: isChunk ? outputResolvePath : void 0,
minify: isChunk ? !isUndefined(entry.minify) ? entry.minify : minify : void 0,
format,
banner: entry.banner,
postBanner: entry.postBanner,
footer: entry.footer,
postFooter: entry.postFooter,
intro: entry.intro,
outro: entry.outro,
name: entry.name,
globals: entry.globals,
extend: entry.extend,
plugins: entry.paths ? [outputPaths(entry.paths)] : void 0,
comments: entry.comments || comments || {
legal: true,
annotation: true,
jsdoc: false
}
};
const entryOptions = {
...entryInput,
...entryOutput,
externals: entryInput.external
};
const entryStats = {
cwd: cwdir,
path: outputRawPath,
size: 0,
buildTime: entryStart,
format: isChunk ? format : "dts",
logs: buildLogs
};
await hooks?.["build:entry:start"]?.(entryOptions, entryStats);
const bundle = await rolldown(entryInput);
if (isChunk) await bundle.write(entryOutput);
else await write(outputResolvePath, (await bundle.generate(entryOutput)).output[0].code);
const stats = await stat(outputResolvePath);
entryStats.size = stats.size;
entryStats.buildTime = Date.now() - entryStart;
entryStats.logs = buildLogs;
buildStats.files.push(entryStats);
buildStats.size = buildStats.size + stats.size;
logEntryStats(entryStats);
await hooks?.["build:entry:end"]?.(entryOptions, entryStats);
}
if (entry.copy) {
const inputResolvePath = resolve(cwdir, entry.copy);
const outputRawPath = parseOutputPath(entry.output) || getOutputPath(outDir, entry.copy, { extension: "original" });
const outputResolvePath = resolve(cwdir, outputRawPath);
await copy(inputResolvePath, outputResolvePath, {
recursive: entry.recursive || true,
filter: entry.filter
}).catch(error);
const stats = await stat(outputResolvePath);
let totalSize = 0;
if (!stats.isDirectory()) totalSize = stats.size;
else {
const files = await readdir(outputResolvePath);
for (const file of files) {
const fileStat = await stat(resolve(outputResolvePath, file));
totalSize = totalSize + fileStat.size;
}
}
const entryStats = {
cwd: cwdir,
path: outputRawPath,
size: totalSize,
buildTime: Date.now() - entryStart,
format: "copy",
logs: []
};
buildStats.files.push(entryStats);
buildStats.size = buildStats.size + stats.size;
logEntryStats(entryStats);
}
if (entry.template && entry.output) {
const outputRawPath = parseOutputPath(entry.output);
const outputResolvePath = resolve(cwdir, outputRawPath);
await write(outputResolvePath, entry.template);
const stats = await stat(outputResolvePath);
const entryStats = {
cwd: cwdir,
path: outputRawPath,
size: stats.size,
buildTime: Date.now() - entryStart,
format: "tmp",
logs: []
};
buildStats.files.push(entryStats);
buildStats.size = buildStats.size + stats.size;
logEntryStats(entryStats);
}
}
buildStats.buildTime = Date.now() - start;
}
await hooks?.["build:end"]?.(options, buildStats);
return buildStats;
}
//#endregion
export { build };