tsdown
Version:
The Elegant Bundler for Libraries
225 lines (218 loc) • 7.71 kB
JavaScript
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 };