Skip to content

Commit

Permalink
feat(extract): makes support for workspaces explicit
Browse files Browse the repository at this point in the history
  • Loading branch information
sverweij committed Nov 11, 2023
1 parent 6dc0632 commit 0619206
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 15 deletions.
3 changes: 2 additions & 1 deletion doc/rules-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -933,8 +933,9 @@ This is a list of dependency types dependency-cruiser currently detects.
| core | it's a core module | "fs" |
| aliased | the module was imported via an alias - always occurs alongside one of 'aliased-\*' dependency types below _and_ alongside the dependency type of the dependency it's aliased to (so: local, npm, core ,...) | "~/hello.ts" |
| aliased-subpath-import | the module was imported via a [subpath import](https://nodejs.org/api/packages.html#subpath-imports) | "#thing/hello.mjs" |
| aliased-webpack | the module was imported via a [webpack resolve alias](https://webpack.js.org/configuration/resolve/#resolvealias) | "Utilities" |
| aliased-tsconfig | the module was imported via a typescript [compilerOptions.paths setting in tsconfig](https://www.typescriptlang.org/tsconfig#paths) | "@thing/hello" |
| aliased-webpack | the module was imported via a [webpack resolve alias](https://webpack.js.org/configuration/resolve/#resolvealias) | "Utilities" |
| aliased-workspace | the module was imported via a [workspace](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#workspaces) | "local-workspace-package" |
| unknown | it's unknown what kind of dependency type this is - probably because the module could not be resolved in the first place | "loodash" |
| undetermined | the dependency fell through all detection holes. This could happen with amd dependencies - which have a whole Jurassic park of ways to define where to resolve modules to | "veloci!./raptor" |
| type-only | the module was imported as 'type only' (e.g. `import type { IThing } from "./things";`) - only available for TypeScript sources, and only when tsPreCompilationDeps !== false | |
Expand Down
71 changes: 63 additions & 8 deletions src/extract/resolve/module-classifiers.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isAbsolute, resolve as path_resolve } from "node:path";
import { isMatch } from "picomatch";
import getExtension from "#utl/get-extension.mjs";

let gFollowableExtensionsCache = new Set();
Expand Down Expand Up @@ -89,14 +90,7 @@ export function isFollowable(pResolvedFilename, pResolveOptions) {
);
}

function isWebPackAliased(pModuleName, pAliasObject) {
return Object.keys(pAliasObject || {}).some((pAliasLHS) =>
pModuleName.startsWith(pAliasLHS),
);
}

/**
*
* @param {string} pModuleName
* @param {object} pManifest
* @returns {boolean}
Expand All @@ -105,6 +99,11 @@ function isSubpathImport(pModuleName, pManifest) {
return (
pModuleName.startsWith("#") &&
Object.keys(pManifest?.imports ?? {}).some((pImportLHS) => {
// Although they might look as such, the LHS of an import statement
// (a 'subpath pattern') is not a glob. The * functions as string
// replacement only.
// Quoting https://nodejs.org/api/packages.html#subpath-imports :
// > "* maps expose nested subpaths as it is a string replacement syntax only"
const lMatchREasString = pImportLHS.replace(/\*/g, ".+");
// eslint-disable-next-line security/detect-non-literal-regexp
const lMatchRE = new RegExp(`^${lMatchREasString}$`);
Expand All @@ -113,6 +112,60 @@ function isSubpathImport(pModuleName, pManifest) {
);
}

/**
* @param {string} pModuleName
* @param {object} pAliasObject
* @returns {boolean}
*/
function isWebPackAliased(pModuleName, pAliasObject) {
return Object.keys(pAliasObject || {}).some((pAliasLHS) =>
pModuleName.startsWith(pAliasLHS),
);
}

/**
* @param {string} pResolvedModuleName
* @param {object} pManifest
* @returns {boolean}
*/
function isWorkspaceAliased(pResolvedModuleName, pManifest) {
// reference: https://docs.npmjs.com/cli/v10/using-npm/workspaces
return (
// workspaces are an array of globs that match the (sub) workspace
// folder itself only.
//
// workspaces: [
// "packages/*", -> matches packages/a, packages/b, packages/c, ...
// "libs/x", -> matches libs/x
// "libs/y", -> matches libs/y
// "apps" -> matches apps
// ]
//
// By definition this will _never_ match resolved module names.
// E.g. in packages/a => packages/a/dist/main/index.js or
// in libs/x => libs/x/index.js
//
// This is why we chuck a `/**` at the end of each workspace glob, which
// transforms it into a 'starts with' glob. And yeah, you can have a /
// at the end of a glob and because double slashes are taken literally
// we have a ternary operator to prevent that.
pManifest?.workspaces &&
isMatch(
pResolvedModuleName,
pManifest.workspaces.map((pWorkspace) =>
pWorkspace.endsWith("/") ? `${pWorkspace}**` : `${pWorkspace}/**`,
),
)
);
}

/**
* @param {string} pModuleName
* @param {string} pResolvedModuleName
* @param {import("../../../types/resolve-options").IResolveOptions} pResolveOptions
* @param {string} pBaseDirectory
* @returns {boolean}
*/
function isLikelyTSAliased(
pModuleName,
pResolvedModuleName,
Expand All @@ -128,7 +181,6 @@ function isLikelyTSAliased(
}

/**
*
* @param {string} pModuleName
* @param {string} pResolvedModuleName
* @param {import("../../../types/resolve-options").IResolveOptions} pResolveOptions
Expand All @@ -147,6 +199,9 @@ export function getAliasTypes(
if (isWebPackAliased(pModuleName, pResolveOptions.alias)) {
return ["aliased", "aliased-webpack"];
}
if (isWorkspaceAliased(pResolvedModuleName, pManifest)) {
return ["aliased", "aliased-workspace"];
}
if (
isLikelyTSAliased(
pModuleName,
Expand Down
3 changes: 2 additions & 1 deletion src/schema/configuration.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 0619206

Please sign in to comment.