UNPKG

@commitlint/resolve-extends

Version:
329 lines 12.2 kB
import { createRequire } from "node:module"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { pathToFileURL, fileURLToPath } from "node:url"; import globalDirectory from "global-directory"; import { mergeWith } from "es-toolkit/compat"; import resolveFrom_ from "resolve-from"; import { validateConfig } from "@commitlint/config-validator"; const require = createRequire(import.meta.url); const dynamicImport = async (id) => { if (id.endsWith(".json")) { return require(id); } const imported = await import(path.isAbsolute(id) ? pathToFileURL(id).toString() : id); return ("default" in imported && imported.default) || imported; }; const pathSuffixes = ["", ".js", ".json", `${path.sep}index.js`, `${path.sep}index.json`]; const specifierSuffixes = ["", ".js", ".json", "/index.js", "/index.json"]; /** * Recover the entry point from a package manifest, preferring the ESM * (`import`) condition of the `exports` map and falling back to `module`/`main`. */ const resolveExportsEntry = (manifest) => { const fromConditions = (value) => { if (typeof value === "string") { return value; } if (value && typeof value === "object" && !Array.isArray(value)) { const conditions = value; for (const key of ["import", "module", "node", "default"]) { if (key in conditions) { const resolved = fromConditions(conditions[key]); if (resolved) { return resolved; } } } } return undefined; }; let exp = manifest.exports; if (exp && typeof exp === "object" && !Array.isArray(exp) && "." in exp) { exp = exp["."]; } return (fromConditions(exp) || (typeof manifest.module === "string" ? manifest.module : undefined) || (typeof manifest.main === "string" ? manifest.main : undefined) || "index.js"); }; /** * Pure-ESM presets such as conventional-changelog-angular@>=9 and * conventional-changelog-conventionalcommits@>=10 declare only the `import` * condition in their `exports` map (no `require`/`default`/`main`), so the * CommonJS resolvers above fail with ERR_PACKAGE_PATH_NOT_EXPORTED. Walk * node_modules from the requesting location and read the manifest ourselves. */ const resolveEsmOnly = (lookup, fromDir) => { if (path.isAbsolute(lookup) || lookup.startsWith(".")) { return undefined; } const segments = lookup.split("/"); const pkgName = lookup.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0]; const subpath = lookup.slice(pkgName.length).replace(/^\//, ""); let dir = fromDir; for (;;) { const pkgDir = path.join(dir, "node_modules", pkgName); const manifestPath = path.join(pkgDir, "package.json"); if (fs.existsSync(manifestPath)) { if (subpath) { for (const suffix of pathSuffixes) { const filename = path.join(pkgDir, subpath) + suffix; if (fs.existsSync(filename)) { return filename; } } return undefined; } const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")); const resolved = path.join(pkgDir, resolveExportsEntry(manifest)); if (fs.existsSync(resolved)) { return resolved; } return undefined; } const parentDir = path.dirname(dir); if (parentDir === dir) { return undefined; } dir = parentDir; } }; export const resolveFrom = (lookup, parent) => { if (path.isAbsolute(lookup)) { for (const suffix of pathSuffixes) { const filename = lookup + suffix; if (fs.existsSync(filename)) { return filename; } } } let resolveError; const parentPath = parent ? fs.statSync(parent).isDirectory() ? path.join(parent, "noop.js") : parent : fileURLToPath(import.meta.url); const localRequire = createRequire(parentPath); for (const suffix of specifierSuffixes) { try { return localRequire.resolve(lookup + suffix); } catch (err) { if (!resolveError) { resolveError = err; } } } try { /** * Yarn P'n'P does not support pure ESM well, this is only a workaround for * @see https://github.com/conventional-changelog/commitlint/issues/3936 */ return resolveFrom_(path.dirname(parentPath), lookup); } catch { const esmResolved = resolveEsmOnly(lookup, path.dirname(parentPath)); if (esmResolved) { return esmResolved; } throw resolveError; } }; /** * * @param resolvedParserPreset path resolved by {@link resolveFrom} * @returns path and parserOpts function retrieved from `resolvedParserPreset` */ export const loadParserPreset = async (resolvedParserPreset) => { const finalParserOpts = await dynamicImport(resolvedParserPreset); const relativeParserPath = path.relative(process.cwd(), resolvedParserPreset); return { path: `./${relativeParserPath}`.split(path.sep).join("/"), parserOpts: finalParserOpts, }; }; export default async function resolveExtends(config = {}, context = {}) { const { extends: e } = config; const extended = await loadExtends(config, context); extended.push(config); return extended.reduce((r, { extends: _, ...c }) => mergeWith(r, c, (objValue, srcValue, key) => { if (key === "plugins") { if (Array.isArray(objValue)) { return objValue.concat(srcValue); } } else if (Array.isArray(objValue)) { return srcValue; } }), e ? { extends: e } : {}); } async function loadExtends(config = {}, context = {}) { const { extends: e } = config; const ext = e ? (Array.isArray(e) ? e : [e]) : []; return await ext.reduce(async (configs, raw) => { const resolved = resolveConfig(raw, context); // Shallow-copy so we never mutate an ESM namespace object (#4647). const c = { ...(await (context.dynamicImport || dynamicImport)(resolved)), }; const cwd = path.dirname(resolved); const ctx = { ...context, cwd }; // Always resolve string parser presets from extended configs so that // their parserOpts (headerPattern, etc.) are available for merging. // Previously this was skipped when the user provided any parserPreset, // which caused partial user overrides (e.g. just issuePrefixes) to // lose the extended preset's headerPattern (see #4640). if (typeof c === "object" && typeof c.parserPreset === "string") { const resolvedParserPreset = resolveFrom(c.parserPreset, cwd); const parserPreset = { name: c.parserPreset, ...(await loadParserPreset(resolvedParserPreset)), }; ctx.parserPreset = parserPreset; c.parserPreset = parserPreset; } validateConfig(resolved, config); return [...(await configs), ...(await loadExtends(c, ctx)), c]; }, Promise.resolve([])); } function getId(raw = "", prefix = "") { const first = raw.charAt(0); const scoped = first === "@"; const relative = first === "."; const absolute = path.isAbsolute(raw); if (scoped) { return raw.includes("/") ? raw : [raw, prefix].filter(String).join("/"); } return relative || absolute ? raw : [prefix, raw].filter(String).join("-"); } function resolveConfig(raw, context = {}) { const resolve = context.resolve || resolveId; const id = getId(raw, context.prefix); let resolved; try { resolved = resolve(id, context); } catch (err) { const legacy = getId(raw, "conventional-changelog-lint-config"); resolved = resolve(legacy, context); console.warn(`Resolving ${raw} to legacy config ${legacy}. To silence this warning raise an issue at 'npm repo ${legacy}' to rename to ${id}.`); } return resolved; } function resolveId(specifier, context = {}) { const cwd = context.cwd || process.cwd(); const localPath = resolveFromSilent(specifier, cwd); if (typeof localPath === "string") { return localPath; } const resolveGlobal = context.resolveGlobal || resolveGlobalSilent; const globalPath = resolveGlobal(specifier); if (typeof globalPath === "string") { return globalPath; } const err = new Error(`Cannot find module "${specifier}" from "${cwd}"`); throw Object.assign(err, { code: "MODULE_NOT_FOUND" }); } export function resolveFromSilent(specifier, parent) { try { return resolveFrom(specifier, parent); } catch { } } /** * Get the npm cache directory. * Respects npm config (npm_config_cache env var or npm config get cache). */ function getNpmCacheDir() { if (process.env.npm_config_cache) { return process.env.npm_config_cache; } try { const { execSync } = require("child_process"); const cacheDir = execSync("npm config get cache", { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"], }).trim(); if (cacheDir) { return cacheDir; } } catch { // Ignore errors } const home = os.homedir(); return path.join(home, ".npm"); } let npxCachePathsCache; /** * Get the npx cache directory paths. * npx stores packages in a subdirectory of the npm cache (e.g., ~/.npm/_npx). * Results are memoized and sorted by mtime (most recent first) for deterministic resolution. */ function getNpxCachePaths() { if (npxCachePathsCache) { return npxCachePathsCache; } const npmCache = getNpmCacheDir(); const npxPath = path.join(npmCache, "_npx"); if (!fs.existsSync(npxPath)) { npxCachePathsCache = []; return []; } try { const entries = fs.readdirSync(npxPath, { withFileTypes: true }); const dirs = entries .filter((entry) => entry.isDirectory()) .map((entry) => ({ path: path.join(npxPath, entry.name, "node_modules"), mtime: fs.statSync(path.join(npxPath, entry.name)).mtime, })) .sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); npxCachePathsCache = dirs.map((d) => d.path); } catch (err) { if (process.env.DEBUG === "true") { console.debug(`Failed to read npx cache: ${err.message}`); } npxCachePathsCache = []; } return npxCachePathsCache; } /** * Resolve a module specifier from npx cache directories. * Iterates all npx cache directories and returns the first successful resolution. * Uses require.resolve for proper Node module resolution (respects package.json main/exports). */ export function resolveFromNpxCache(specifier) { for (const npxDir of getNpxCachePaths()) { try { return require.resolve(specifier, { paths: [npxDir] }); } catch (err) { if (process.env.DEBUG === "true") { console.debug(`Failed to resolve ${specifier} from ${npxDir}: ${err.message}`); } } } return undefined; } /** * @see https://github.com/sindresorhus/resolve-global/blob/682a6bb0bd8192b74a6294219bb4c536b3708b65/index.js#L7 */ export function resolveGlobalSilent(specifier) { for (const globalPackages of [globalDirectory.npm.packages, globalDirectory.yarn.packages]) { try { return resolveFrom(specifier, globalPackages); } catch (err) { if (process.env.DEBUG === "true") { console.debug(`Failed to resolve ${specifier} from global: ${err.message}`); } } } // Check npx cache directories return resolveFromNpxCache(specifier); } //# sourceMappingURL=index.js.map