Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8cf0716
Initial plan
Copilot Feb 9, 2026
4ae8ca5
Add workspace parent detection using resolve-workspace-root
Copilot Feb 9, 2026
d7ea7f0
Add documentation for nested workspace support
Copilot Feb 9, 2026
636eaca
Improve workspace parent detection to support nested workspaces
Copilot Feb 9, 2026
21dd54f
Address code review feedback: improve workspace detection logic and f…
Copilot Feb 9, 2026
572af97
Refactor: move findUltimateWorkspaceRoot to lib/utils.js
Copilot Feb 9, 2026
2a740a4
Simplify: use resolveWorkspaceRootAsync directly instead of findUltim…
Copilot Feb 9, 2026
24cf5cc
Add --no-parent-workspace flag and improve parent workspace detection…
Copilot Feb 9, 2026
6f4a3dc
Add debug logging for parent workspace detection
Copilot Feb 9, 2026
d8a63a6
Refactor import for resolveWorkspaceRootAsync
voxpelli Feb 9, 2026
b32653b
Change debug messages to use console.error instead of console.log
Copilot Feb 9, 2026
3ceffca
Add type annotation for reasons array to achieve 100% type coverage
Copilot Feb 9, 2026
f4f891c
Set includeWorkspaceRoot to false when using parent workspace detection
Copilot Feb 9, 2026
c9c2483
Support parent workspace detection with custom paths and add debug he…
Copilot Feb 9, 2026
8b88590
Add examples and integration tests for monorepo workspace support
Copilot Feb 9, 2026
8ad8f14
Improve examples and tests with unified/remark parsing
Copilot Feb 9, 2026
fa70920
Refactor integration tests for better reusability and add monorepo ex…
Copilot Feb 9, 2026
679e98a
Fix critical bug: always set cwd in lookupOptions + split monorepo RE…
Copilot Feb 9, 2026
062725e
Fix critical bug: always set cwd in lookupOptions + split monorepo RE…
Copilot Feb 9, 2026
e478577
Refactor integration tests and enhance examples
Copilot Feb 9, 2026
52202d5
Fix glob pattern in package.json scripts
Copilot Feb 9, 2026
4c7f990
Fix test-ci script to avoid recursive call
Copilot Feb 9, 2026
33ce039
Address code review feedback: improve assertion style
Copilot Feb 9, 2026
05366d7
Add distinct errors to all three examples
Copilot Feb 9, 2026
53758c4
Add test/helpers.js to tsconfig and revert test script to explicit form
Copilot Feb 9, 2026
9028ce9
Fix CI failures: normalize line endings and fix helper function signa…
Copilot Feb 9, 2026
463880e
Fix CI failures: normalize line endings and fix helper function signa…
Copilot Feb 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"extends": "@voxpelli/eslint-config/esm",
"root": true
"root": true,
"ignorePatterns": ["examples/**"]
}
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,33 @@ Use [installed-check-core](https://github.com/voxpelli/node-installed-check-core
## Workspace options

* `--no-include-workspace-root` – excludes the workspace root package. Negated equivalent of npm's [`--include-workspace-root`](https://docs.npmjs.com/cli/v10/commands/npm-run-script#include-workspace-root)
* `--no-parent-workspace` – disables automatic detection and use of parent workspace root for module resolution
* `--no-workspaces` – excludes workspace packages. Negated equivalent of npm's [`--workspaces`](https://docs.npmjs.com/cli/v10/commands/npm-run-script#workspaces)
* `--workspace=ARG` / `-w ARG` – excludes all workspace packages not matching these names / paths. Equivalent to npm's [`--workspace` / `-w`](https://docs.npmjs.com/cli/v10/commands/npm-run-script#workspace)
* `--workspace-ignore=ARG` – xcludes the specified paths from workspace lookup. (Supports globs)
* `--workspace-ignore=ARG` – excludes the specified paths from workspace lookup. (Supports globs)

### Nested workspace support

When running `installed-check` within a workspace that is part of a larger monorepo (e.g., when using `git subtree` or working in a nested monorepo structure), the tool will automatically detect the parent workspace root and include modules from the parent's `node_modules` directory. This ensures that dependencies hosted at the monorepo root level are properly detected and checked.

This behavior is automatic when:
- No explicit workspace filters are provided (`--workspace`)
- The `--no-parent-workspace` flag is not used

For example, if you have a structure like:
```
/parent-monorepo
/node_modules (shared dependencies)
/packages
/my-workspace
/package.json
```

Running `installed-check` in `/parent-monorepo/packages/my-workspace` will automatically detect the parent monorepo and check dependencies from both the workspace and the parent's `node_modules`.

You can also explicitly specify a path to a workspace within a monorepo, and parent workspace detection will still work:

To disable this behavior, use the `--no-parent-workspace` flag.

### Additional command line options

Expand All @@ -80,6 +104,14 @@ Use [installed-check-core](https://github.com/voxpelli/node-installed-check-core
* `--help` / `-h` – prints help and exits
* `--version` – prints current version and exits

## Examples

The repository includes practical examples demonstrating `installed-check` usage:

* **[Basic Example](examples/basic/README.md)** – Simple project showing engine range validation
* **[Monorepo Example](examples/monorepo/README.md)** – Workspace root with dependency issues
* **[Workspace Example](examples/monorepo/packages/workspace-a/README.md)** – Individual workspace with parent workspace detection

## Similar modules

* [`knip`](https://github.com/webpro/knip) – finds unused files, dependencies and exports in your JavaScript and TypeScript projects – a great companion module to `installed-check`
78 changes: 72 additions & 6 deletions cli.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
/* eslint-disable no-console, unicorn/no-process-exit */

import { resolve } from 'node:path';
import chalk from 'chalk';
import meow from 'meow';
import { messageWithCauses, stackWithCauses } from 'pony-cause';
import { installedCheck, ROOT } from 'installed-check-core';
import resolveWorkspaceRootPkg from 'resolve-workspace-root';

const { resolveWorkspaceRootAsync } = resolveWorkspaceRootPkg;

const EXIT_CODE_ERROR_RESULT = 1;
const EXIT_CODE_INVALID_INPUT = 2;
const EXIT_CODE_UNEXPECTED_ERROR = 4;

/**
* Log a debug message to stderr if debug mode is enabled
*
* @param {boolean | undefined} debug
* @param {string} label
* @param {string} message
*/
function debugLog (debug, label, message) {
if (debug) {
console.error(chalk.blue(label + ':') + ' ' + message);
}
}

const cli = meow(`
Usage
$ installed-check <path to module folder>
Expand All @@ -30,6 +47,7 @@ const cli = meow(`

Workspace options
--no-include-workspace-root Will exclude the workspace root package
--no-parent-workspace Will not detect and use parent workspace root for module resolution
--no-workspaces Will exclude workspace packages
-w ARG, --workspace=ARG Excludes all workspace packages not matching these names / paths
--workspace-ignore=ARG Excludes the specified paths from workspace lookup. (Supports globs)
Expand All @@ -52,6 +70,7 @@ const cli = meow(`
ignore: { shortFlag: 'i', type: 'string', isMultiple: true },
ignoreDev: { shortFlag: 'd', type: 'boolean' },
includeWorkspaceRoot: { type: 'boolean', 'default': true },
parentWorkspace: { type: 'boolean', 'default': true },
peerCheck: { shortFlag: 'p', type: 'boolean' },
strict: { shortFlag: 's', type: 'boolean' },
verbose: { shortFlag: 'v', type: 'boolean' },
Expand All @@ -75,6 +94,7 @@ const {
engineNoDev, // deprecated
fix = false,
includeWorkspaceRoot,
parentWorkspace,
peerCheck,
strict,
verbose,
Expand Down Expand Up @@ -106,13 +126,59 @@ let checks = [
...versionCheck ? /** @type {const} */ (['version']) : [],
];

// Detect if we're in a workspace within a larger monorepo
// If so, use the parent workspace root to enable access to parent's node_modules
const requestedCwd = resolve(cli.input[0] || process.cwd());

let resolvedCwd = requestedCwd;
let workspaceFilter = workspace;
let resolvedIncludeWorkspaceRoot = includeWorkspaceRoot;

// Only detect parent workspace if:
// - User hasn't explicitly opted out with --no-parent-workspace
// - User hasn't provided explicit workspace filters (which would be incompatible)
if (parentWorkspace && !workspace?.length) {
debugLog(debug, 'Parent workspace detection', 'Attempting to resolve parent workspace root');

const parentWorkspaceRoot = await resolveWorkspaceRootAsync(requestedCwd);

if (parentWorkspaceRoot) {
debugLog(debug, 'Parent workspace detection', 'Found parent workspace root: ' + parentWorkspaceRoot);
} else {
debugLog(debug, 'Parent workspace detection', 'No parent workspace root found');
}

// If we found a parent workspace root different from our requested cwd,
// we're in a workspace situation
if (parentWorkspaceRoot && parentWorkspaceRoot !== requestedCwd) {
// Use the parent workspace root as cwd to get access to its node_modules
resolvedCwd = parentWorkspaceRoot;

// Filter to just the current workspace to avoid checking all workspaces in the parent monorepo
workspaceFilter = [requestedCwd];

// Don't include the workspace root (parent) in checks, only the filtered workspace
resolvedIncludeWorkspaceRoot = false;

debugLog(debug, 'Parent workspace detection', 'Using parent workspace root, filtering to current workspace');
} else if (parentWorkspaceRoot === requestedCwd) {
debugLog(debug, 'Parent workspace detection', 'Parent workspace root is same as requested cwd, not applying');
}
} else if (debug) {
/** @type {string[]} */
const reasons = [];
if (!parentWorkspace) reasons.push('--no-parent-workspace flag is set');
if (workspace?.length) reasons.push('explicit workspace filters provided');
debugLog(debug, 'Parent workspace detection', 'Skipped (' + reasons.join(', ') + ')');
}

/** @type {import('installed-check-core').LookupOptions} */
const lookupOptions = {
cwd: cli.input[0],
cwd: resolvedCwd,
ignorePaths: workspaceIgnore,
includeWorkspaceRoot,
includeWorkspaceRoot: resolvedIncludeWorkspaceRoot,
skipWorkspaces: !workspaces,
workspace,
workspace: workspaceFilter,
};

/** @type {import('installed-check-core').InstalledCheckOptions} */
Expand All @@ -128,9 +194,9 @@ if (checks.length === 0) {

if (debug) {
const { inspect } = await import('node:util');
console.log(chalk.blue('Checks:') + ' ' + inspect(checks, { colors: true, compact: true }));
console.log(chalk.blue('Lookup options:') + ' ' + inspect(lookupOptions, { colors: true, compact: true }));
console.log(chalk.blue('Check options:') + ' ' + inspect(checkOptions, { colors: true, compact: true }));
debugLog(debug, 'Checks', inspect(checks, { colors: true, compact: true }));
debugLog(debug, 'Lookup options', inspect(lookupOptions, { colors: true, compact: true }));
debugLog(debug, 'Check options', inspect(checkOptions, { colors: true, compact: true }));
}

try {
Expand Down
2 changes: 2 additions & 0 deletions examples/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
package-lock.json
35 changes: 35 additions & 0 deletions examples/basic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Basic Example

This is a simple example showing how `installed-check` works with a basic Node.js project.

## What's in This Example

This example demonstrates a common issue - a dependency (`meow`) has a stricter `engines.node` requirement than the package itself:
- `chalk@^4.0.0` - A popular terminal coloring library
- `meow@^14.0.0` - A CLI helper library (requires Node >=20, but package specifies >=18.6.0)

## Usage

```bash
# From the repository root
cd examples/basic
npm install
cd ../..

# Run installed-check
node cli-wrapper.cjs examples/basic
```

## Example Output

<!-- BEGIN EXPECTED OUTPUT -->
```
Errors:

meow: Narrower "engines.node" is needed: >=20.0.0

Suggestions:

Combined "engines.node" needs to be narrower: >=20.0.0
```
<!-- END EXPECTED OUTPUT -->
13 changes: 13 additions & 0 deletions examples/basic/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "example-basic",
"version": "1.0.0",
"private": true,
"description": "Basic example for installed-check - intentionally has issues for demonstration",
"dependencies": {
"chalk": "^4.0.0",
"meow": "^14.0.0"
},
"engines": {
"node": ">=18.6.0"
}
}
52 changes: 52 additions & 0 deletions examples/monorepo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Monorepo Example

This example demonstrates how `installed-check` works with monorepo workspaces.

## Structure

```
monorepo/
├── package.json (workspace root with chalk@^4.0.0 and meow@^14.0.0 - has engine issue)
├── node_modules/ (shared dependencies installed here)
└── packages/
├── workspace-a/
│ └── package.json (depends on chalk@^4.0.0)
└── workspace-b/
└── package.json (depends on chalk@^4.0.0, has typescript in devDeps)
```

## Usage

### Running from Monorepo Root

This checks all workspaces at once including the root:

```bash
# From the repository root
cd examples/monorepo
npm install
cd ../..

# Check the entire monorepo
node cli-wrapper.cjs examples/monorepo
```

## Example Output

<!-- BEGIN EXPECTED OUTPUT -->
```
Errors:

root: meow: Narrower "engines.node" is needed: >=20.0.0
workspace-a: knip: Narrower "engines.node" is needed: >=18.18.0

Suggestions:

root: Combined "engines.node" needs to be narrower: >=20.0.0
workspace-a: Combined "engines.node" needs to be narrower: >=18.18.0
```
<!-- END EXPECTED OUTPUT -->

### Running from Individual Workspace

See [workspace-a/README.md](./packages/workspace-a/README.md) for examples of running `installed-check` on an individual workspace with automatic parent workspace detection.
16 changes: 16 additions & 0 deletions examples/monorepo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "example-monorepo",
"version": "1.0.0",
"private": true,
"description": "Monorepo example for installed-check parent workspace detection",
"workspaces": [
"packages/*"
],
"dependencies": {
"chalk": "^4.0.0",
"meow": "^14.0.0"
},
"engines": {
"node": ">=18.6.0"
}
}
62 changes: 62 additions & 0 deletions examples/monorepo/packages/workspace-a/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Workspace A

This workspace is part of the monorepo example and demonstrates automatic parent workspace detection.

## What's in This Workspace

This workspace:
- Depends on `chalk@^4.0.0` (installed in parent's node_modules)
- Has `knip@^5.0.0` in devDependencies (requires Node >=18.18.0, narrower than package's >=18.6.0)

The parent monorepo root has a `meow` dependency with an engine issue, but that issue does NOT appear when checking this workspace because `includeWorkspaceRoot` is set to `false` when using parent workspace detection.

## Usage

```bash
# From the repository root
cd examples/monorepo
npm install
cd ../..

# Run installed-check on this workspace
node cli-wrapper.cjs examples/monorepo/packages/workspace-a

# With debug output
node cli-wrapper.cjs --debug examples/monorepo/packages/workspace-a
```

## What Happens

When you run `installed-check` in this workspace:
1. **Parent detection**: Automatically detects the parent monorepo at `examples/monorepo`
2. **Module resolution**: Uses the parent's `node_modules` for finding dependencies
3. **Filtered checking**: Only checks this workspace package, not the parent (includeWorkspaceRoot: false)
4. **Validation**: Shows knip engine issue - but the parent's meow issue is correctly excluded

## Example Output

<!-- BEGIN EXPECTED OUTPUT -->
```
Errors:

workspace-a: knip: Narrower "engines.node" is needed: >=18.18.0

Suggestions:

workspace-a: Combined "engines.node" needs to be narrower: >=18.18.0
```
<!-- END EXPECTED OUTPUT -->

This shows the knip engine requirement issue in this workspace. Note that the parent's meow issue does NOT appear because `includeWorkspaceRoot: false` excludes the parent from checks.

### Debug Output

When run with `--debug`, you'll see parent workspace detection:

<!-- BEGIN DEBUG OUTPUT -->
```
Parent workspace detection: Attempting to resolve parent workspace root
Parent workspace detection: Found parent workspace root: /absolute/path/to/examples/monorepo
Parent workspace detection: Using parent workspace root, filtering to current workspace
```
<!-- END DEBUG OUTPUT -->
15 changes: 15 additions & 0 deletions examples/monorepo/packages/workspace-a/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "workspace-a",
"version": "1.0.0",
"private": true,
"description": "Workspace A in monorepo example",
"dependencies": {
"chalk": "^4.0.0"
},
"devDependencies": {
"knip": "^5.0.0"
},
"engines": {
"node": ">=18.6.0"
}
}
Loading
Loading