UNPKG

@hypernym/bundler

Version:
252 lines (251 loc) 7.95 kB
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 };