UNPKG

dependency-cruiser

Version:

Validate and visualize dependencies. With your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.

241 lines (221 loc) 7.22 kB
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; }