@commitlint/resolve-extends
Version:
Lint your commit messages
265 lines • 9.38 kB
JavaScript
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 { moduleResolve } from "import-meta-resolve";
import mergeWith from "lodash.mergewith";
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"];
const conditions = new Set(["import", "node"]);
/**
* @see moduleResolve
*/
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 base = pathToFileURL(parent
? fs.statSync(parent).isDirectory()
? path.join(parent, "noop.js")
: parent
: import.meta.url);
for (const suffix of specifierSuffixes) {
try {
return fileURLToPath(moduleResolve(lookup + suffix, base, conditions));
}
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(fileURLToPath(base)), lookup);
}
catch {
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