dependency-cruiser
Version:
Validate and visualize dependencies. With your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.
225 lines (211 loc) • 7.06 kB
JavaScript
import { realpathSync } from "node:fs";
import { extname, resolve as path_resolve, relative } from "node:path";
import monkeyPatchedModule from "node:module";
import pathToPosix from "../../utl/path-to-posix.mjs";
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";
/**
*
* @param {import("../../../types/dependency-cruiser.js").IModule} pModule
* @param {string} pBaseDirectory
* @param {string} pFileDirectory
* @param {import("../../../types/dependency-cruiser.js").IResolveOptions} pResolveOptions
* @returns {any}
*/
function resolveModule(
pModule,
pBaseDirectory,
pFileDirectory,
pResolveOptions
) {
let lReturnValue = null;
const lStrippedModuleName = stripToModuleName(pModule.module);
if (
isRelativeModuleName(lStrippedModuleName) ||
["cjs", "es6", "tsd"].includes(pModule.moduleSystem)
) {
lReturnValue = resolveCommonJS(
lStrippedModuleName,
pBaseDirectory,
pFileDirectory,
pResolveOptions
);
} else {
lReturnValue = resolveAMD(
lStrippedModuleName,
pBaseDirectory,
pFileDirectory
);
}
return lReturnValue;
}
function canBeResolvedToTsVariant(pModuleName) {
return [".js", ".jsx", ".mjs", ".cjs"].includes(extname(pModuleName));
}
function isTypeScriptIshExtension(pModuleName) {
return [".ts", ".tsx", ".cts", ".mts"].includes(extname(pModuleName));
}
function resolveYarnVirtual(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) {
return pnpAPI.resolveVirtual(path_resolve(pPath)) || pPath;
}
/* c8 ignore stop */
return pPath;
}
/**
*
* @param {string} pJavaScriptExtension
* @returns {string}
*/
function getTypeScriptExtensionsToTry(pJavaScriptExtension) {
const lJS2TSMap = {
".js": [".ts", ".tsx", ".d.ts"],
".jsx": [".ts", ".tsx", ".d.ts"],
".cjs": [".cts", ".d.cts"],
".mjs": [".mts", ".d.mts"],
};
// eslint-disable-next-line security/detect-object-injection
return lJS2TSMap[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.replace(
/\.(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 <import("../../../types/cruise-result.js").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 {import("../../../types/resolve-options.js").IResolveOptions} pResolveOptions
* @return {Partial <import("../../../types/cruise-result.js").IDependency>}
*
*/
// eslint-disable-next-line max-lines-per-function
export default function resolve(
pDependency,
pBaseDirectory,
pFileDirectory,
pResolveOptions
) {
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
),
};
if (
!pResolveOptions.symlinks &&
!lResolvedDependency.coreModule &&
!lResolvedDependency.couldNotResolve
) {
try {
lResolvedDependency.resolved = pathToPosix(
relative(
pBaseDirectory,
realpathSync(
resolveYarnVirtual(
path_resolve(
pBaseDirectory,
/* enhanced-resolve inserts a NULL character in front of any `#`
character. This wonky replace undoes that so the filename
again corresponds with a real file on disk
*/
// eslint-disable-next-line no-control-regex
lResolvedDependency.resolved.replace(/\u0000#/g, "#")
)
)
)
)
);
} catch (pError) {
lResolvedDependency.couldNotResolve = true;
}
}
return lResolvedDependency;
}