UNPKG

tsdown

Version:

The Elegant Bundler for Libraries

225 lines (218 loc) 7.71 kB
import { logger, noop, resolveComma, toArray } from "./general-C06aMSSY.js"; import path, { dirname, normalize, sep } from "node:path"; import { blue, bold, dim, green, underline, yellow } from "ansis"; import Debug from "debug"; import { access, chmod, readFile, rm } from "node:fs/promises"; import { up } from "empathic/package"; import { Buffer } from "node:buffer"; import { promisify } from "node:util"; import { brotliCompress, gzip } from "node:zlib"; //#region src/utils/fs.ts function fsExists(path$1) { return access(path$1).then(() => true, () => false); } function fsRemove(path$1) { return rm(path$1, { force: true, recursive: true }).catch(() => {}); } function lowestCommonAncestor(...filepaths) { if (filepaths.length === 0) return ""; if (filepaths.length === 1) return dirname(filepaths[0]); filepaths = filepaths.map(normalize); const [first, ...rest] = filepaths; let ancestor = first.split(sep); for (const filepath of rest) { const directories = filepath.split(sep, ancestor.length); let index = 0; for (const directory of directories) if (directory === ancestor[index]) index += 1; else { ancestor = ancestor.slice(0, index); break; } ancestor = ancestor.slice(0, index); } return ancestor.length <= 1 && ancestor[0] === "" ? sep + ancestor[0] : ancestor.join(sep); } //#endregion //#region src/features/external.ts const debug$2 = Debug("tsdown:external"); const RE_DTS$1 = /\.d\.[cm]?ts$/; function ExternalPlugin(options) { const deps = options.pkg && Array.from(getProductionDeps(options.pkg)); return { name: "tsdown:external", async resolveId(id, importer, { isEntry }) { if (isEntry) return; if (importer && RE_DTS$1.test(importer)) return; const { noExternal } = options; if (typeof noExternal === "function" && noExternal(id, importer)) return; if (noExternal) { const noExternalPatterns = toArray(noExternal); if (noExternalPatterns.some((pattern) => { return pattern instanceof RegExp ? pattern.test(id) : id === pattern; })) return; } let shouldExternal = false; if (options.skipNodeModulesBundle) { const resolved = await this.resolve(id); if (!resolved) return; shouldExternal = resolved.external || /[\\/]node_modules[\\/]/.test(resolved.id); } if (deps) shouldExternal ||= deps.some((dep) => id === dep || id.startsWith(`${dep}/`)); if (shouldExternal) { debug$2("External dependency:", id); return { id, external: shouldExternal }; } } }; } function getProductionDeps(pkg) { return new Set([...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})]); } //#endregion //#region src/utils/package.ts const debug$1 = Debug("tsdown:package"); async function readPackageJson(dir) { const packageJsonPath = up({ cwd: dir }); if (!packageJsonPath) return; debug$1("Reading package.json:", packageJsonPath); const contents = await readFile(packageJsonPath, "utf8"); return JSON.parse(contents); } function getPackageType(pkg) { if (pkg?.type) { if (!["module", "commonjs"].includes(pkg.type)) throw new Error(`Invalid package.json type: ${pkg.type}`); return pkg.type; } } function normalizeFormat(format) { return resolveComma(toArray(format, "es")).map((format$1) => { switch (format$1) { case "es": case "esm": case "module": return "es"; case "cjs": case "commonjs": return "cjs"; default: return format$1; } }); } function prettyFormat(format) { const formatColor = format === "es" ? blue : format === "cjs" ? yellow : noop; let formatText; switch (format) { case "es": formatText = "ESM"; break; default: formatText = format.toUpperCase(); break; } return formatColor(`[${formatText}]`); } //#endregion //#region src/utils/format.ts function formatBytes(bytes) { if (bytes === Infinity) return "too large"; const numberFormatter = new Intl.NumberFormat("en", { maximumFractionDigits: 2, minimumFractionDigits: 2 }); return `${numberFormatter.format(bytes / 1e3)} kB`; } //#endregion //#region src/features/report.ts const debug = Debug("tsdown:report"); const brotliCompressAsync = promisify(brotliCompress); const gzipAsync = promisify(gzip); const RE_DTS = /\.d\.[cm]?ts$/; function ReportPlugin(options, cwd, cjsDts) { return { name: "tsdown:report", async writeBundle(outputOptions, bundle) { const outDir = path.relative(cwd, outputOptions.file ? path.resolve(cwd, outputOptions.file, "..") : path.resolve(cwd, outputOptions.dir)); const sizes = []; for (const chunk of Object.values(bundle)) { const size = await calcSize(options, chunk); sizes.push(size); } const filenameLength = Math.max(...sizes.map((size) => size.filename.length)); const rawTextLength = Math.max(...sizes.map((size) => size.rawText.length)); const gzipTextLength = Math.max(...sizes.map((size) => size.gzipText.length)); const brotliTextLength = Math.max(...sizes.map((size) => size.brotliText.length)); let totalRaw = 0; for (const size of sizes) { size.rawText = size.rawText.padStart(rawTextLength); size.gzipText = size.gzipText.padStart(gzipTextLength); size.brotliText = size.brotliText.padStart(brotliTextLength); totalRaw += size.raw; } sizes.sort((a, b) => { if (a.dts !== b.dts) return a.dts ? 1 : -1; if (a.isEntry !== b.isEntry) return a.isEntry ? -1 : 1; return b.raw - a.raw; }); const formatLabel = prettyFormat(cjsDts ? "cjs" : outputOptions.format); for (const size of sizes) { const filenameColor = size.dts ? green : noop; logger.info(formatLabel, dim(`${outDir}/`) + filenameColor((size.isEntry ? bold : noop)(size.filename)), ` `.repeat(filenameLength - size.filename.length), dim`${size.rawText} │ gzip: ${size.gzipText}`, options.brotli ? dim` │ brotli: ${size.brotliText}` : ""); } const totalSizeText = formatBytes(totalRaw); logger.info(formatLabel, `${sizes.length} files, total: ${totalSizeText}`); } }; } async function calcSize(options, chunk) { debug(`Calculating size for`, chunk.fileName); const content = chunk.type === "chunk" ? chunk.code : chunk.source; const raw = Buffer.byteLength(content, "utf8"); debug("[size]", chunk.fileName, raw); let gzip$1 = Infinity; let brotli = Infinity; if (raw > (options.maxCompressSize ?? 1e6)) debug(chunk.fileName, "file size exceeds limit, skip gzip/brotli"); else { gzip$1 = (await gzipAsync(content)).length; debug("[gzip]", chunk.fileName, gzip$1); if (options.brotli) { brotli = (await brotliCompressAsync(content)).length; debug("[brotli]", chunk.fileName, brotli); } } return { filename: chunk.fileName, dts: RE_DTS.test(chunk.fileName), isEntry: chunk.type === "chunk" && chunk.isEntry, raw, rawText: formatBytes(raw), gzip: gzip$1, gzipText: formatBytes(gzip$1), brotli, brotliText: formatBytes(brotli) }; } //#endregion //#region src/features/shebang.ts const RE_SHEBANG = /^#!.*/; function ShebangPlugin(cwd) { return { name: "tsdown:shebang", async writeBundle(options, bundle) { for (const chunk of Object.values(bundle)) { if (chunk.type !== "chunk" || !chunk.isEntry) continue; if (!RE_SHEBANG.test(chunk.code)) continue; const filepath = path.resolve(cwd, options.file || path.join(options.dir, chunk.fileName)); if (await fsExists(filepath)) { logger.info(prettyFormat(options.format), `Granting execute permission to ${underline(path.relative(cwd, filepath))}`); await chmod(filepath, 493); } } } }; } //#endregion export { ExternalPlugin, ReportPlugin, ShebangPlugin, fsExists, fsRemove, getPackageType, lowestCommonAncestor, normalizeFormat, prettyFormat, readPackageJson };