dependency-cruiser
Version:
Validate and visualize dependencies. With your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.
241 lines (221 loc) • 7.22 kB
JavaScript
import { extname, resolve as path_resolve, relative } from "node:path";
import monkeyPatchedModule from "node:module";
import { isRelativeModuleName } from "./module-classifiers.mjs";
import { resolveAMD } from "./resolve-amd.mjs";
import resolveCommonJS from "./resolve-cjs.mjs";
import { stripToModuleName, addLicenseAttribute } from "./resolve-helpers.mjs";
import determineDependencyTypes from "./determine-dependency-types.mjs";
import { getManifest } from "./get-manifest.mjs";
import pathToPosix from "#utl/path-to-posix.mjs";
/**
* @import { IModule, IResolveOptions } from "../../../types/dependency-cruiser.mjs";
* @import { IResolveOptions } from "../../../types/resolve-options.mjs";
* @import { IDependency } from "../../../types/cruise-result.mjs";
*/
const COMMONJS_RESOLVABLE_MODULE_SYSTEMS = new Set(["cjs", "es6", "tsd"]);
/**
*
* @param {IModule} pModule
* @param {string} pBaseDirectory
* @param {string} pFileDirectory
* @param {IResolveOptions} pResolveOptions
* @returns {any}
*/
function resolveModule(
pModule,
pBaseDirectory,
pFileDirectory,
pResolveOptions,
) {
let lReturnValue = null;
const lStrippedModuleName = stripToModuleName(pModule.module);
if (
isRelativeModuleName(lStrippedModuleName) ||
COMMONJS_RESOLVABLE_MODULE_SYSTEMS.has(pModule.moduleSystem)
) {
lReturnValue = resolveCommonJS(
lStrippedModuleName,
pBaseDirectory,
pFileDirectory,
pResolveOptions,
);
} else {
lReturnValue = resolveAMD(
lStrippedModuleName,
pBaseDirectory,
pFileDirectory,
pResolveOptions,
);
}
return lReturnValue;
}
const RESOLVABLE_TO_TS_VARIANT_EXTENSIONS = new Set([
".js",
".jsx",
".mjs",
".cjs",
]);
function canBeResolvedToTsVariant(pModuleName) {
return RESOLVABLE_TO_TS_VARIANT_EXTENSIONS.has(extname(pModuleName));
}
const TYPESCRIPT_ISH_EXTENSIONS = new Set([".ts", ".tsx", ".cts", ".mts"]);
function isTypeScriptIshExtension(pModuleName) {
return TYPESCRIPT_ISH_EXTENSIONS.has(extname(pModuleName));
}
function resolveYarnVirtual(pBaseDirectory, pPath) {
const pnpAPI = (monkeyPatchedModule?.findPnpApi ?? (() => false))(pPath);
// the pnp api only works in plug'n play environments, and resolveVirtual
// only under yarn(berry). As we can't run a 'regular' nodejs environment
// and a yarn(berry) one at the same time, ignore in the test coverage and
// cover it in a separate integration test.
/* c8 ignore start */
if (pnpAPI && (pnpAPI?.VERSIONS?.resolveVirtual ?? 0) === 1) {
// resolveVirtual takes absolute paths, hence the path.resolve:
const lResolvedAbsolute = path_resolve(pBaseDirectory, pPath);
const lResolvedVirtual = pnpAPI.resolveVirtual(lResolvedAbsolute);
if (lResolvedVirtual) {
const lResolvedRelative = relative(pBaseDirectory, lResolvedVirtual);
// in win32 environments resolveVirtual might return win32 paths,
// so we have to convert them to posix paths again
return pathToPosix(lResolvedRelative);
}
}
/* c8 ignore stop */
return pPath;
}
const JS_TO_TS_MAP = new Map([
[".js", [".ts", ".tsx", ".d.ts"]],
[".jsx", [".ts", ".tsx", ".d.ts"]],
[".cjs", [".cts", ".d.cts"]],
[".mjs", [".mts", ".d.mts"]],
]);
/**
*
* @param {string} pJavaScriptExtension
* @returns {string[]}
*/
function getTypeScriptExtensionsToTry(pJavaScriptExtension) {
return JS_TO_TS_MAP.get(pJavaScriptExtension) ?? [];
}
// eslint-disable-next-line max-lines-per-function
function resolveWithRetry(
pModule,
pBaseDirectory,
pFileDirectory,
pResolveOptions,
) {
let lReturnValue = resolveModule(
pModule,
pBaseDirectory,
pFileDirectory,
pResolveOptions,
);
const lStrippedModuleName = stripToModuleName(pModule.module);
// when we feed the typescript compiler an import with an explicit .js(x) extension
// and the .js(x) file does not exist, it tries files with the .ts, .tsx or
// .d.ts extensions (this a.o. means ./hello.jsx can resolve to ./hello.ts and
// ./wut.js to ./wut.tsx).
//
// This behavior is very specific:
// - tsc only (doesn't work in ts-node for instance)
// - until TypeScript 4.6 only for the .js and .jsx extensions
// - since TypeScript 4.7 also for .cjs and .mjs (=> .cts, .mts) extensions,
// which work subtly different;
// .cjs resolves to .cts or .d.cts (in that order)
// .mjs resolves to .mts or .d.mts (in that order)
// ref: https://www.typescriptlang.org/docs/handbook/esm-node.html#new-file-extensions
//
// Hence also this oddly specific looking check & retry.
//
// This should eventually probably land in either enhanced_resolve or in a
// plugin/ extension for it (tsconfig-paths-webpack-plugin?)
if (
lReturnValue.couldNotResolve &&
canBeResolvedToTsVariant(lStrippedModuleName)
) {
const lModuleWithoutExtension = lStrippedModuleName.replaceAll(
/\.(js|jsx|cjs|mjs)$/g,
"",
);
const lExtensionsToTry = getTypeScriptExtensionsToTry(
extname(lStrippedModuleName),
);
const lReturnValueCandidate = resolveModule(
{ ...pModule, module: lModuleWithoutExtension },
pBaseDirectory,
pFileDirectory,
{ ...pResolveOptions, extensions: lExtensionsToTry },
);
if (isTypeScriptIshExtension(lReturnValueCandidate.resolved)) {
lReturnValue = lReturnValueCandidate;
}
}
return lReturnValue;
}
/**
* resolves the module name of the pDependency to a file on disk.
*
* @param {Partial <IDependency>} pDependency
* @param {string} pBaseDirectory the directory to consider as base (or 'root')
* for resolved files.
* @param {string} pFileDirectory the directory of the file the dependency was
* detected in
* @param {IResolveOptions} pResolveOptions
* @param {any} pTranspileOptions
* @return {Partial <IDependency>}
*
*
*/
// eslint-disable-next-line max-lines-per-function, max-params
export default function resolve(
pDependency,
pBaseDirectory,
pFileDirectory,
pResolveOptions,
pTranspileOptions,
) {
let lResolvedDependency = resolveWithRetry(
pDependency,
pBaseDirectory,
pFileDirectory,
pResolveOptions,
);
const lStrippedModuleName = stripToModuleName(pDependency.module);
lResolvedDependency = {
...lResolvedDependency,
...addLicenseAttribute(
lStrippedModuleName,
lResolvedDependency.resolved,
{ baseDirectory: pBaseDirectory, fileDirectory: pFileDirectory },
pResolveOptions,
),
dependencyTypes: determineDependencyTypes(
{ ...pDependency, ...lResolvedDependency },
lStrippedModuleName,
getManifest(
pFileDirectory,
pBaseDirectory,
pResolveOptions.combinedDependencies,
),
pFileDirectory,
pResolveOptions,
pBaseDirectory,
pTranspileOptions,
),
};
if (!lResolvedDependency.coreModule && !lResolvedDependency.couldNotResolve) {
// enhanced-resolve inserts a NULL character in front of any `#` character.
// This wonky replace corrects that so the filename again corresponds
// with a real file on disk
const lResolvedEHRCorrected = lResolvedDependency.resolved.replaceAll(
"\u0000#",
"#",
);
const lResolvedYarnVirtual = resolveYarnVirtual(
pBaseDirectory,
lResolvedEHRCorrected,
);
lResolvedDependency.resolved = lResolvedYarnVirtual;
}
return lResolvedDependency;
}