dependency-cruiser
Version:
Validate and visualize dependencies. With your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.
171 lines (157 loc) • 5.88 kB
JavaScript
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { resolve } from "./resolve.mjs";
import { isScoped, isRelativeModuleName } from "./module-classifiers.mjs";
const PACKAGE_JSON_CACHE = new Map();
/**
* Returns the 'root' of the package - the spot where we can probably find
* the package.json of that package, and the reference we'd usualy have
* in our own package.json to declare the dependency.
*
* Some samples:
* - for unscoped packages this would be everything up to the first '/':
* - 'godash/fp' => 'godash'
* - 'mypackage/some/module' => 'mypackage'
* - for scoped packages this would be everything up to the second '/',
* if it's there (which should normaly be the case, but nevertheless we
* can't assume that)
* - @angular/common => @angular/common
* - @somescope/some/module => @somescope/some
* - @imdoingweirdvoodoo => @imdoingweirdvoodoo
* - for local packages it doesn't make sense to call this function, but
* if consumers insist it'll return the local package name unchanged:
* - './myThing' => './myThing'
* - './some/other/thing' => './some/other/thing'
*
* At this time we don't take situations into account where the caller includes
* a node module through a local path (which could make sense if you're on
* non-commonJS and are still using node_modules) e.g. '../node_modules/oldash/fp'
*
* @param {string} pModule a module name
* @return {string} the module name root
*/
export function getPackageRoot(pModule) {
if (!pModule || isRelativeModuleName(pModule)) {
return pModule;
}
let lPathElements = pModule.split("/");
if (isScoped(pModule)) {
// '@imdoingweirdvoodoo'
if (lPathElements.length === 1) {
return pModule;
}
// @scope/package
// @scope/package/some/thing
return `${lPathElements[0]}/${lPathElements[1]}`;
}
// godash
// godash/fp
return lPathElements[0];
}
/**
* returns the contents of the package.json of the given pModule as it would
* resolve from pBaseDirectory
*
* e.g. to get the package.json of `oldash` that is required bya
* `src/report/something.js` use `getPackageJSON('oldash', 'src/report/');`
*
* The pBaseDirectory parameter is necessary because dependency-cruiser/ this module
* will have a different base dir, and will hence resolve either to the
* wrong package or to no package.
*
* @param {string} pModuleName The name of the module to get the package.json of
* @param {string} pFileDirectory The folder the module resides in. Defaults to the current working directory
* @param {any} pResolveOptions options for the resolver
* @return {Record<string, any>} The package.json as a javascript object, or
* null if either module or package.json could
* not be found
*/
export function getPackageJson(pModuleName, pFileDirectory, pResolveOptions) {
const lCacheKey = `${pModuleName}|${pFileDirectory}`;
if (PACKAGE_JSON_CACHE.has(lCacheKey)) {
return PACKAGE_JSON_CACHE.get(lCacheKey);
}
let lReturnValue = null;
try {
const lPackageJsonFilename = resolve(
join(getPackageRoot(pModuleName), "package.json"),
pFileDirectory ?? process.cwd(),
{
...pResolveOptions,
// if a module has exports fields _and_ does not expose package.json
// in those exports, enhanced-resolve (nr node!) won't be able to
// resolve the package.json if it actually heeds those exports fields.
// We can instruct enhanced-resolve to ignore them by passing
// it the empty array for exports fields (overriding anything in
// the pResvolveOptions)
exportsFields: [],
// we don't need to try any extensions; we already
// know it as we have passed the complete module name to resolve =>
// override whatever the default is with [""] ('use no extensions please')
extensions: [""],
},
// we need a separate caching context so as not to **** up the regular
// cruise, which might actually want to utilize the exportsFields
// and an array of extensions
"manifest-resolution",
);
lReturnValue = JSON.parse(readFileSync(lPackageJsonFilename, "utf8"));
} catch (pError) {
// left empty on purpose
}
PACKAGE_JSON_CACHE.set(lCacheKey, lReturnValue);
return lReturnValue;
}
/**
* Tells whether the pModule as resolved to pBaseDirectory is deprecated
*
* @param {string} pModuleName The name of the module to get the deprecation status of
* @param {string} pFileDirectory The folder the module resides in.
* @param {any} pResolveOptions options for the resolver
* @return {boolean} true when deprecated, false in all other cases
*/
export function dependencyIsDeprecated(
pModuleName,
pFileDirectory,
pResolveOptions,
) {
let lReturnValue = false;
let lPackageJson = getPackageJson(
pModuleName,
pFileDirectory,
pResolveOptions,
);
if (lPackageJson) {
lReturnValue =
Object.hasOwn(lPackageJson, "deprecated") && lPackageJson.deprecated;
}
return lReturnValue;
}
/**
* Returns the license of pModule as resolved to pBaseDirectory - if any
*
* @param {string} pModuleName The name of the module to get the license of
* @param {string} pFileDirectory The folder the module resides in.
* @param {any} pResolveOptions options for the resolver
* @return {string} The module's license string, or '' in case
* there is no package.json or no license field
*/
export function getLicense(pModuleName, pFileDirectory, pResolveOptions) {
let lReturnValue = "";
let lPackageJson = getPackageJson(
pModuleName,
pFileDirectory,
pResolveOptions,
);
if (
lPackageJson &&
Object.hasOwn(lPackageJson, "license") &&
typeof lPackageJson.license === "string"
) {
lReturnValue = lPackageJson.license;
}
return lReturnValue;
}
export function clearCache() {
PACKAGE_JSON_CACHE.clear();
}