UNPKG

@hypernym/bundler

Version:
507 lines (493 loc) 16.6 kB
#!/usr/bin/env node 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);