@hypernym/bundler
Version:
ESM & TS module bundler.
507 lines (493 loc) • 16.6 kB
JavaScript
import process, { cwd } from 'node:process';
import { createArgs } from '@hypernym/args';
import { resolve, dirname, parse } from 'node:path';
import { read, exists, write, copy, readdir } from '@hypernym/utils/fs';
import { dim, cyan } from '@hypernym/colors';
import { build as build$1, transform } from 'esbuild';
import { stat } from 'node:fs/promises';
import { isString, isUndefined, isObject } from '@hypernym/utils';
import { rollup } from 'rollup';
import { getLogFilter } from 'rollup/getLogFilter';
import replacePlugin from '@rollup/plugin-replace';
import jsonPlugin from '@rollup/plugin-json';
import resolvePlugin from '@rollup/plugin-node-resolve';
import aliasPlugin from '@rollup/plugin-alias';
import { dts } from 'rollup-plugin-dts';
import { createFilter } from '@rollup/pluginutils';
const externals = [
/^node:/,
/^@types/,
/^@rollup/,
/^@hypernym/,
/^rollup/
];
const name = `Hyperbundler`;
const version = `0.14.4`;
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();
}
};
function error(err) {
logger.error("Something went wrong...");
console.error(err);
return process.exit();
}
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`;
}
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]}`;
}
function getOutputPath(outDir, input, dts) {
const _input = input.startsWith("./") ? input.slice(2) : input;
let output = _input.replace(_input.split("/")[0], outDir);
const ext = dts ? "d.mts" : "mjs";
const cts = dts ? "d.cts" : "cjs";
if (output.endsWith(".js")) output = `${output.slice(0, -2)}${ext}`;
else if (output.endsWith(".ts")) output = `${output.slice(0, -2)}${ext}`;
else if (output.endsWith(".mts")) output = `${output.slice(0, -3)}${ext}`;
else if (output.endsWith(".cts")) output = `${output.slice(0, -3)}${cts}`;
if (outDir.startsWith("./") || outDir.startsWith("../")) return output;
else return `./${output}`;
}
function getLongestOutput(outDir, entries) {
const outputs = [];
for (const entry of entries) {
if (entry.copy) outputs.push(entry.copy.output);
if (entry.input) {
const out = entry.output || getOutputPath(outDir, entry.input);
outputs.push(out);
}
if (entry.declaration || entry.dts) {
const dts = entry.declaration || entry.dts;
const out = entry.output || getOutputPath(outDir, dts, true);
outputs.push(out);
}
if (entry.template) outputs.push(entry.output);
}
return Math.max(...outputs.map((v) => v.length));
}
async function loadConfig(cwd, filePath, defaults) {
const result = await build$1({
entryPoints: [resolve(cwd, filePath)],
bundle: true,
write: false,
format: "esm",
target: "esnext",
packages: "external"
});
const code = result.outputFiles[0].text;
const tempConfig = resolve(cwd, "node_modules/.hypernym/bundler/config.mjs");
await write(tempConfig, code);
const content = await import(tempConfig);
const config = {
...defaults,
...content.default
};
return { options: config, path: filePath };
}
async function createConfigLoader(cwd, args) {
const pkgPath = resolve(cwd, "package.json");
const pkg = await read(pkgPath).catch(error);
const { dependencies } = JSON.parse(pkg);
const warnMessage = `Missing required configuration. To start bundling, add the ${cyan(
`'bundler.config.{js,mjs,ts,mts}'`
)} file to the project's root.`;
const defaults = {
externals: [...Object.keys(dependencies || {}), ...externals],
entries: []
};
if (args.config) {
const path = args.config;
const isConfig = await exists(path);
if (isConfig) return await loadConfig(cwd, path, defaults);
else return logger.exit(warnMessage);
}
const configName = "bundler.config";
const configExts = [".ts", ".js", ".mts", ".mjs"];
for (const ext of configExts) {
const path = `${configName}${ext}`;
const isConfig = await exists(path);
if (isConfig) return await loadConfig(cwd, path, defaults);
}
return logger.exit(warnMessage);
}
async function resolvePath(path, index = false) {
const extensions = [".js", ".ts", "jsx", ".tsx"];
const fileWithoutExt = path.replace(/\.[jt]sx?$/, "");
for (const ext of extensions) {
const file = index ? `${path}/index${ext}` : `${fileWithoutExt}${ext}`;
if (await exists(file)) return file;
}
return null;
}
function esbuild(options) {
const filter = createFilter(/\.([cm]?ts|[jt]sx)$/);
return {
name: "esbuild",
async resolveId(id, importer) {
if (importer) {
const resolved = resolve(importer ? dirname(importer) : cwd(), id);
let file = await resolvePath(resolved);
if (file) return file;
if (!file && await exists(resolved) && (await stat(resolved)).isDirectory()) {
file = await resolvePath(resolved, true);
if (file) return file;
}
}
return null;
},
async transform(code, id) {
if (!filter(id)) return null;
const result = await transform(code, {
loader: "default",
...options,
sourcefile: id
});
return {
code: result.code,
map: result.map || null
};
},
async renderChunk(code, { fileName }) {
if (!options?.minify) return null;
if (/\.d\.(c|m)?tsx?$/.test(fileName)) return null;
const result = await transform(code, {
...options,
sourcefile: fileName,
minify: true
});
return {
code: result.code,
map: result.map || null
};
}
};
}
function logModuleStats(file, longestOutput) {
const cl = console.log;
const base = parse(file.path).base;
const path = file.path.replace(base, "");
let format = file.format;
if (format.includes("system")) format = "sys";
if (format === "commonjs") format = "cjs";
if (format === "module") format = "esm";
longestOutput = longestOutput + 2;
const ansiCode = 9;
const pathDim = dim(path);
const output = pathDim + base;
const pathDimNoAnsi = pathDim.length - ansiCode;
const difference = longestOutput - pathDimNoAnsi - base.length;
const padLength = output.length + difference;
cl(
dim("+"),
format.padEnd(5),
output.padEnd(padLength),
dim("time"),
formatMs(file.buildTime).padEnd(7),
dim("size"),
formatBytes(file.size)
);
if (file.logs) {
for (const log of file.logs) {
cl(
dim("!"),
log.level.padEnd(5),
output.padEnd(padLength),
dim(log.log.message)
);
}
}
}
async function build(cwd, options) {
const { outDir = "dist", hooks } = options;
let start = 0;
const buildStats = {
cwd,
size: 0,
buildTime: 0,
files: []
};
await hooks?.["build:start"]?.(options, buildStats);
if (options.entries) {
const longestOutput = getLongestOutput(outDir, options.entries);
start = Date.now();
const aliasDir = resolve(cwd, "./src");
let aliasOptions = {
entries: options.alias || [
{ find: "@", replacement: aliasDir },
{ find: "~", replacement: aliasDir }
]
};
for (const entry of options.entries) {
const entryStart = Date.now();
if (entry.copy) {
const _entry = {
input: isString(entry.copy.input) ? [entry.copy.input] : entry.copy.input,
output: entry.copy.output,
recursive: entry.copy.recursive || true,
filter: entry.copy.filter
};
const buildLogs = [];
for (const copyInput of _entry.input) {
const fileSrc = resolve(cwd, copyInput);
const fileDist = resolve(cwd, _entry.output, copyInput);
await copy(fileSrc, fileDist, {
recursive: _entry.recursive,
filter: _entry.filter
}).catch(error);
const stats = await stat(fileDist);
let totalSize = 0;
if (!stats.isDirectory()) totalSize = stats.size;
else {
const files = await readdir(fileDist);
for (const file of files) {
const filePath = resolve(fileDist, file);
const fileStat = await stat(filePath);
totalSize = totalSize + fileStat.size;
}
}
const parseInput = (path) => {
if (path.startsWith("./")) return path.slice(2);
else return path;
};
const parseOutput = (path) => {
if (path.startsWith("./")) return path;
else return `./${path}`;
};
const fileStats = {
cwd,
path: `${parseOutput(_entry.output)}/${parseInput(copyInput)}`,
size: totalSize,
buildTime: Date.now() - entryStart,
format: "copy",
logs: buildLogs
};
buildStats.files.push(fileStats);
buildStats.size = buildStats.size + stats.size;
logModuleStats(fileStats, longestOutput);
}
}
if (entry.input) {
const logFilter = getLogFilter(entry.logFilter || []);
const _output = entry.output || getOutputPath(outDir, entry.input);
let _format = "esm";
if (_output.endsWith(".cjs")) _format = "cjs";
const buildLogs = [];
const _entry = {
input: entry.input,
output: _output,
externals: entry.externals || options.externals,
format: entry.format || _format,
...entry,
defaultPlugins: [
esbuild({
minify: !isUndefined(entry.minify) ? entry.minify : options.minify,
...entry.transformers?.esbuild
})
]
};
if (!entry.plugins) {
if (_entry.transformers?.json) {
const jsonOptions = isObject(_entry.transformers.json) ? _entry.transformers.json : void 0;
_entry.defaultPlugins.push(jsonPlugin(jsonOptions));
}
if (_entry.transformers?.replace) {
_entry.defaultPlugins.unshift(
replacePlugin({
preventAssignment: true,
..._entry.transformers.replace
})
);
}
if (_entry.transformers?.resolve) {
const resolveOptions = isObject(_entry.transformers.resolve) ? _entry.transformers.resolve : void 0;
_entry.defaultPlugins.unshift(resolvePlugin(resolveOptions));
}
_entry.defaultPlugins.unshift(
aliasPlugin(_entry.transformers?.alias || aliasOptions)
);
}
const fileStats = {
cwd,
path: _entry.output,
size: 0,
buildTime: entryStart,
format: _entry.format,
logs: buildLogs
};
await hooks?.["build:entry:start"]?.(_entry, fileStats);
const _build = await rollup({
input: resolve(cwd, _entry.input),
external: _entry.externals,
plugins: _entry.plugins || _entry.defaultPlugins,
onLog: (level, log) => {
if (logFilter(log)) buildLogs.push({ level, log });
}
});
await _build.write({
file: resolve(cwd, _entry.output),
format: _entry.format,
banner: _entry.banner,
footer: _entry.footer,
intro: _entry.intro,
outro: _entry.outro,
paths: _entry.paths,
name: _entry.name,
globals: _entry.globals,
extend: _entry.extend
});
const stats = await stat(resolve(cwd, _entry.output));
fileStats.size = stats.size;
fileStats.buildTime = Date.now() - entryStart;
fileStats.logs = buildLogs;
buildStats.files.push(fileStats);
buildStats.size = buildStats.size + stats.size;
logModuleStats(fileStats, longestOutput);
await hooks?.["build:entry:end"]?.(_entry, fileStats);
}
if (entry.dts || entry.declaration) {
const logFilter = getLogFilter(entry.logFilter || []);
const buildLogs = [];
const dts$1 = entry.dts || entry.declaration;
const _entry = {
dts: dts$1,
output: entry.output || getOutputPath(outDir, dts$1, true),
externals: entry.externals || options.externals,
format: entry.format || "esm",
...entry,
defaultPlugins: [dts(entry.transformers?.dts)]
};
if (!entry.plugins) {
_entry.defaultPlugins.unshift(
aliasPlugin(_entry.transformers?.alias || aliasOptions)
);
}
const fileStats = {
cwd,
path: _entry.output,
size: 0,
buildTime: entryStart,
format: "dts",
logs: buildLogs
};
await hooks?.["build:entry:start"]?.(_entry, fileStats);
const _build = await rollup({
input: resolve(cwd, _entry.dts),
external: _entry.externals,
plugins: _entry.plugins || _entry.defaultPlugins,
onLog: (level, log) => {
if (logFilter(log)) buildLogs.push({ level, log });
}
});
await _build.write({
file: resolve(cwd, _entry.output),
format: _entry.format,
banner: _entry.banner,
footer: _entry.footer,
intro: _entry.intro,
outro: _entry.outro,
paths: _entry.paths
});
const stats = await stat(resolve(cwd, _entry.output));
fileStats.size = stats.size;
fileStats.buildTime = Date.now() - entryStart;
fileStats.logs = buildLogs;
buildStats.files.push(fileStats);
buildStats.size = buildStats.size + stats.size;
logModuleStats(fileStats, longestOutput);
await hooks?.["build:entry:end"]?.(_entry, fileStats);
}
if (entry.template && entry.output) {
const buildLogs = [];
await write(entry.output, entry.template);
const stats = await stat(resolve(cwd, entry.output));
const fileStats = {
cwd,
path: entry.output,
size: stats.size,
buildTime: Date.now() - entryStart,
format: "tmp",
logs: buildLogs
};
buildStats.files.push(fileStats);
buildStats.size = buildStats.size + stats.size;
logModuleStats(fileStats, longestOutput);
}
}
buildStats.buildTime = Date.now() - start;
}
await hooks?.["build:end"]?.(options, buildStats);
return buildStats;
}
async function createBuilder(cwd, config) {
const { options, path: configPath } = config;
const { hooks } = options;
const cl = console.log;
await hooks?.["bundle:start"]?.(options);
logger.info(dim(`v${version}`));
cl("Config", dim(configPath));
cl();
cl("Bundling started...");
cl(
"Processing",
dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}]`),
"Transforming files"
);
cl();
await build(cwd, options).then((stats) => {
const buildTime = dim(formatMs(stats.buildTime));
const buildSize = dim(formatBytes(stats.size));
const totalModules = stats.files.length;
const modules = totalModules > 1 ? `${totalModules} modules` : `${totalModules} module`;
cl();
cl(
"Succeeded",
dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}]`),
"Module transformation is done"
);
cl(`Bundling fully completed in ${buildTime}`);
cl();
cl(`${modules} transformed. Total size is ${buildSize}`);
cl(`Bundle is generated and ready for production`);
cl();
}).catch(error);
await hooks?.["bundle:end"]?.(options);
}
async function main() {
const cwd$1 = cwd();
const args = createArgs({
alias: { config: "c" }
});
const config = await createConfigLoader(cwd$1, args);
await createBuilder(cwd$1, config);
}
main().catch(error);