@sanity/pkg-utils
Version:
Simple utilities for modern npm packages.
199 lines (198 loc) • 9.52 kB
JavaScript
import path from "node:path";
import { up } from "empathic/package";
import { loadPkgWithReporting, resolveBuildContext, createConsoleSpy } from "./resolveBuildContext.js";
import { loadConfig } from "./defaults.js";
import { fileExists } from "./fileExists.js";
import { createLogger, handleError } from "./handleError.js";
import chalk from "chalk";
import treeify from "treeify";
import { statSync } from "node:fs";
import prettyBytes from "pretty-bytes";
import { createSpinner } from "./spinner.js";
function getFilesize(file) {
const stats = statSync(file);
return prettyBytes(stats.size);
}
function getFileInfo(cwd, filePath) {
const p = path.resolve(cwd, filePath), exists = fileExists(p), size = exists ? getFilesize(p) : void 0;
return { exists, size };
}
function printPackageTree(ctx) {
const { cwd, exports, logger, pkg } = ctx;
if (!exports) return;
logger.log(`${chalk.blue(pkg.name)}@${chalk.green(pkg.version)}`);
const tree = {};
pkg.type && (tree.type = chalk.yellow(pkg.type)), pkg.bin && (tree.bin = Object.fromEntries(
Object.entries(pkg.bin).map(([name, file]) => [chalk.cyan(name), fileInfo(file)])
));
function fileInfo(file) {
const info = getFileInfo(cwd, file);
return info.size ? `${chalk.yellow(file)} ${chalk.gray(info.size)}` : `${chalk.gray(file)} ${chalk.red("does not exist")}`;
}
tree.exports = Object.fromEntries(
Object.entries(exports).filter(([, entry]) => entry._exported).map(([exportPath, entry]) => {
const exp = {
source: fileInfo(entry.source),
browser: void 0,
require: void 0,
node: void 0,
import: void 0,
default: fileInfo(entry.default)
};
return entry.browser ? (exp.browser = { source: fileInfo(entry.browser.source) }, entry.browser.import && (exp.browser.import = fileInfo(entry.browser.import)), entry.browser.require && (exp.browser.require = fileInfo(entry.browser.require))) : delete exp.browser, entry.require ? exp.require = fileInfo(entry.require) : delete exp.require, entry.node ? (exp.node = {}, entry.node.source && (exp.node.source = fileInfo(entry.node.source)), entry.node.import && (exp.node.import = fileInfo(entry.node.import)), entry.node.require && (exp.node.require = fileInfo(entry.node.require))) : delete exp.node, entry.import ? exp.import = fileInfo(entry.import) : delete exp.import, [chalk.cyan(path.join(pkg.name, exportPath)), exp];
})
), logger.log(treeify.asTree(tree, !0, !0));
}
async function check(options) {
const { cwd, strict = !1, tsconfig: tsconfigOption } = options, logger = createLogger(), spinner = createSpinner("");
try {
const pkgPath = up({ cwd });
if (!pkgPath)
throw new Error("no package.json found", { cause: { cwd } });
const config = await loadConfig({ cwd, pkgPath }), { parseStrictOptions } = await import("./resolveBuildContext.js").then(function(n) {
return n.strict;
}), strictOptions = parseStrictOptions(config?.strictOptions ?? {}), pkg = await loadPkgWithReporting({ pkgPath, logger, strict, strictOptions }), tsconfig = tsconfigOption || config?.tsconfig || "tsconfig.json", ctx = await resolveBuildContext({ config, cwd, logger, pkg, strict, tsconfig });
if (printPackageTree(ctx), strict) {
const missingFiles = [];
for (const [, exp] of Object.entries(ctx.exports || {}))
exp.source && !fileExists(path.resolve(cwd, exp.source)) && missingFiles.push(exp.source), exp.require && !fileExists(path.resolve(cwd, exp.require)) && missingFiles.push(exp.require), exp.import && !fileExists(path.resolve(cwd, exp.import)) && missingFiles.push(exp.import);
ctx.pkg.types && !fileExists(path.resolve(cwd, ctx.pkg.types)) && missingFiles.push(ctx.pkg.types), missingFiles.length && (logger.error(`missing files: ${missingFiles.join(", ")}`), process.exit(1));
const exportPaths = {
require: [],
import: []
};
for (const exp of Object.values(ctx.exports || {}))
exp._exported && (exp.require && exportPaths.require.push(exp.require), exp.import && exportPaths.import.push(exp.import));
const external = [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.devDependencies || {})
], consoleSpy = createConsoleSpy(), checks = [];
exportPaths.import.length && checks.push(checkExports(exportPaths.import, { cwd, external, format: "esm", logger })), exportPaths.require.length && checks.push(checkExports(exportPaths.require, { cwd, external, format: "cjs", logger })), await Promise.all(checks), consoleSpy.restore();
}
ctx.dts === "rolldown" && ctx.config?.extract?.enabled !== !1 && await checkApiExtractorReleaseTags(ctx), spinner.complete();
} catch (err) {
if (spinner.error(), err instanceof Error) {
const RE_CWD = new RegExp(cwd, "g");
logger.error((err.stack || err.message).replace(RE_CWD, ".")), logger.log();
}
process.exit(1);
}
}
async function checkExports(exportPaths, options) {
const { build } = await import("esbuild"), { cwd, external, format, logger } = options, code = exportPaths.map((id) => format ? `import('${id}');` : `require('${id}');`).join(`
`);
try {
const esbuildResult = await build({
bundle: !0,
external,
format,
logLevel: "silent",
// otherwise output maps to stdout as we're using stdin
outfile: "/dev/null",
platform: "node",
// We're not interested in CSS files that might be imported as a side effect, so we'll treat them as empty
loader: { ".css": "empty" },
stdin: {
contents: code,
loader: "js",
resolveDir: cwd
}
});
if (esbuildResult.errors.length > 0) {
for (const msg of esbuildResult.errors)
printEsbuildMessage(logger.warn, msg), logger.log();
process.exit(1);
}
const esbuildWarnings = esbuildResult.warnings.filter((msg) => !(msg.detail || msg.text).includes("does not affect esbuild's own target setting"));
for (const msg of esbuildWarnings)
printEsbuildMessage(logger.warn, msg), logger.log();
} catch (err) {
if (isEsbuildFailure(err)) {
const { errors } = err;
for (const msg of errors)
printEsbuildMessage(logger.error, msg), logger.log();
} else err instanceof Error ? (logger.error(err.stack || err.message), logger.log()) : (logger.error(String(err)), logger.log());
process.exit(1);
}
}
function printEsbuildMessage(log, msg) {
msg.location ? log(
[
`${msg.detail || msg.text}
`,
`${msg.location.line} | ${msg.location.lineText}
`,
`in ./${msg.location.file}:${msg.location.line}:${msg.location.column}`
].join("")
) : log(msg.detail || msg.text);
}
function isEsbuildFailure(err) {
return err instanceof Error && "errors" in err && Array.isArray(err.errors) && err.errors.every(isEsbuildMessage) && "warnings" in err && Array.isArray(err.warnings) && err.warnings.every(isEsbuildMessage);
}
function isEsbuildMessage(msg) {
return typeof msg == "object" && msg !== null && "text" in msg && typeof msg.text == "string" && "location" in msg && (msg.location === null || typeof msg.location == "object");
}
async function checkApiExtractorReleaseTags(ctx) {
const [
{ Extractor, ExtractorConfig },
{ createApiExtractorConfig },
{ createTSDocConfig },
{ getExtractMessagesConfig },
{ printExtractMessages }
] = await Promise.all([
import("@microsoft/api-extractor"),
import("./createApiExtractorConfig.js"),
import("./createTSDocConfig.js"),
import("./getExtractMessagesConfig.js"),
import("./printExtractMessages.js")
]), customTags = ctx.config?.extract?.customTags || [], bundledPackages = ctx.bundledPackages, distPath = ctx.distPath, outDir = ctx.ts.config?.options.outDir, rules = ctx.config?.extract?.rules || {};
if (!outDir)
throw new Error("tsconfig.json is missing `compilerOptions.outDir`");
for (const exp of Object.values(ctx.exports || {})) {
if (!exp._exported || !exp.default.endsWith(".js")) continue;
const dtsPath = exp.default.replace(/\.js$/, ".d.ts"), exportPath = path.resolve(ctx.cwd, dtsPath), tsdocConfigFile = await createTSDocConfig({
customTags
}), extractorConfig = ExtractorConfig.prepare({
configObject: createApiExtractorConfig({
bundledPackages,
distPath,
exportPath,
filePath: path.relative(outDir, dtsPath),
messages: getExtractMessagesConfig({ rules }),
projectFolder: ctx.cwd,
mainEntryPointFilePath: exportPath,
tsconfig: ctx.ts.config,
tsconfigPath: path.resolve(ctx.cwd, ctx.ts.configPath || "tsconfig.json"),
dtsRollupEnabled: !1
}),
configObjectFullPath: void 0,
tsdocConfigFile,
packageJsonFullPath: path.resolve(ctx.cwd, "package.json")
}), messages = [];
Extractor.invoke(extractorConfig, {
// Equivalent to the "--local" command-line parameter
localBuild: !0,
// Equivalent to the "--verbose" command-line parameter
showVerboseMessages: !0,
// handle messages
messageCallback(message) {
messages.push(message), message.handled = !0;
}
}), printExtractMessages(ctx, messages);
}
}
async function checkAction(options) {
try {
await check({
cwd: process.cwd(),
strict: options.strict,
tsconfig: options.tsconfig
});
} catch (err) {
handleError(err);
}
}
export {
checkAction
};
//# sourceMappingURL=checkAction.js.map