UNPKG

@sanity/pkg-utils

Version:

Simple utilities for modern npm packages.

199 lines (198 loc) 9.52 kB
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