@ster5/check-version-consistency
v0.10.0
Published
verify packages consistency for node-opcua
Readme
check-version-consistency
A set of diagnostic tools to help identify and resolve version mismatches, peer dependency issues, and configuration problems in projects using the node-opcua ecosystem (and @sterfive/* packages).
Installation
npm install -g check-version-consistencyTools
This package provides two main CLI tools:
check-package-version-consistencydiagnose-node-opcua
1. check-package-version-consistency
Checks that all modules in the project use consistent versions. This is useful for large monorepos or projects with many related dependencies.
Usage
check-package-version-consistency [projectFolder] [options]| Option | Description |
| :----- | :---------- |
| projectFolder | The root folder to explore (default: current working directory). |
| --fix | Rewrite every manifest so each module is written one way. |
| --dry-run | Show what --fix would rewrite, without touching any file. |
| --no-install | With --fix, skip refreshing the lockfile afterwards. |
| --strict | Treat any difference in how a version is written as an error, even when every package resolves to the same version. |
Example
check-package-version-consistency ./my-projectHow workspaces are discovered
Every tool declares workspace membership somewhere different. Reading only one convention makes a monorepo using any other look like it has no workspaces at all — nothing gets checked and the run still reports success, which is worse than failing. All four are supported, in this order:
| Order | Source | Field | Used by |
| :---- | :----- | :---- | :------ |
| 1 | pnpm-workspace.yaml | packages: | pnpm |
| 2 | package.json | "workspaces": [...] | npm, yarn berry, bun |
| 3 | package.json | "workspaces": { "packages": [...] } | yarn classic |
| 4 | lerna.json | "packages": [...] | lerna |
pnpm-workspace.yaml wins when several exist, because it is the one that
actually installs and therefore cannot be stale. If your repo uses pnpm, the
workspaces field in package.json is dead weight — delete it rather than
maintain two lists that will drift.
Errors vs suggestions
Two very different situations used to be reported identically:
- error — the specifiers cannot all be satisfied at once (
^1.0.0against^2.0.0). Something is genuinely broken; exit code is 1. - suggestion — one version written several ways (
2.175.1against^2.175.1). Worth converging, but nothing is broken, so it never fails a build. The report names the version that satisfies everyone and the single form to prefer. Use--strictto make these errors again.
A specifier that points at a workspace sibling — *, link:../x,
workspace:^, file:../x — is not a version and is never compared as one.
Converging with --fix
--fix rewrites every manifest so each module is written one way:
check-package-version-consistency --dry-run # show what would change
check-package-version-consistency --fix # apply, then refresh the lockfile- dependencies and devDependencies get an exact pin. A pin is the only form that installs the same tree twice.
- peerDependencies get
>=. Pinning a peer forces one exact version on every host — the thing the peer check exists to catch. - The version chosen is the highest floor anyone asked for, so no package is
ever downgraded.
^2.175.1,2.175.2and>=2.175.1all converge on2.175.2. - Workspace links (
*,link:../x) are never rewritten into versions. - Genuine conflicts are not touched. The tool cannot know which of two incompatible majors the code actually wants; that stays a human decision.
Rewriting a manifest invalidates the lockfile, so --fix runs the install
itself — pnpm m install, pnpm install, yarn install or npm install,
whichever the project uses, detected from its lockfile and workspace file. Pass
--no-install to skip it, and the run tells you which command to run instead.
Every --fix (here and in diagnose-node-opcua) writes a manifest back in the
style it was written in: the indent unit it already uses — whichever is most
common, tabs or spaces — its line ending, and a trailing newline only if it had
one. A two-line change stays a two-line diff instead of re-emitting the whole
file in the tool's own style and burying the actual edit.
Exact pins are right for an application, where reproducibility is everything. For a published library they are restrictive: a consumer that resolves any other patch ends up with a duplicate copy in its tree. Consider running
--fixon the app and leaving published packages on ranges.
peerDependencies must be ranges
A pinned peer forces one exact version on every host: any consumer that resolves
a different patch gets an unsatisfiable peer and a duplicate copy in the tree.
1.2.3, =1.2.3, v1.2.3 and 1.2.3-beta.1 are all equally pinned and all
reported, with the >= form to use instead.
2. diagnose-node-opcua
A more advanced diagnostic tool specifically designed for the node-opcua ecosystem.
Features
- Version Mismatch Detection: Scans installed dependencies and flags version inconsistencies against a reference baseline.
- Peer Dependency Validator (
--check-peer): Recursively checks if all peer dependencies required by your dependency tree are correctly declared in your rootpackage.json. - Import Validator (
--check-imports): Scans your source code and checks that everynode-opcua*/@sterfive/*package you actually import is declared in yourpackage.json. Add--workspacesto cover every package of a monorepo in one run. - Online Resolution: Queries the NPM registry to find the latest compatible versions and metadata, ensuring up-to-date checks.
- Thorough Scan: Performs a deep recursive analysis of
node-opcuadependencies without relying solely on localnode_modulesstructure. - Auto-Fix: Can automatically update your
package.jsonto fix missing peer dependencies or undeclared imports. - Private Registry Support: Respects
.npmrcconfiguration for resolving private/scoped packages.
Which check do I want?
The two validators look similar but catch disjoint sets of problems. Neither finds the other's defects, so a project that wants full coverage runs both.
| | Question it answers | Reads | Needs network |
| :--- | :--- | :--- | :--- |
| --check-imports | Do I declare everything I import? | your source tree | no |
| --check-peer | Do I declare everything my dependencies demand? | the NPM registry graph | yes |
A package you import directly is nobody's peer requirement, so no amount of
dependency-graph walking will ever surface it — only reading the sources will.
Conversely, a package required by one of your dependencies' peerDependencies
never appears in your sources.
Because --check-imports is offline and fast, it is the one to put in a
pre-commit or pre-push hook; --check-peer suits CI, where the network is
available.
Usage
Run the script using Node.js (v14+):
diagnose-node-opcua [options]Options
| Option | Alias | Description |
| :----------------- | :---- | :----------------------------------------------------------------------------------------------------------- |
| --package <path> | -p | Path to the package.json to diagnose (default: current directory). |
| --online | -o | Enable online resolution of package versions and metadata from NPM registry. |
| --thorough | -t | Perform a recursive online diagnosis of all node-opcua related packages. Automatically enables --online. |
| --check-peer | | Check that every peer dependency your dependencies require is declared. Automatically enables --online. |
| --check-imports | | Check that every package your sources import is declared. Offline unless --online is given. |
| --workspaces | -w | With --check-imports, check every workspace package instead of one. See the monorepo note below. |
| --include-tests | | Also scan test/, tests/, __tests__/ and report test-only imports separately. Requires --also-in. |
| --source <dir> | | Source directory to scan (repeatable). Defaults to every one of source/, src/, lib/ that exists and is not git-ignored. |
| --also-in <path> | | A second package.json (a monorepo root, typically) that must also declare each imported package. |
| --fix | | Automatically fix the issues found in package.json (use with --check-peer or --check-imports). |
| --exact | | With --fix, write exact versions instead of ^ ranges (for repos that pin strictly). |
| --dry-run | | Preview changes that would be made by --fix without modifying files. |
| --silent | | Suppress output (useful for scripted checks). |
| --help | -h | Show help message. |
Exit codes
| Code | Meaning |
| :--- | :--- |
| 0 | No issues found (or --fix repaired them). |
| 1 | Issues found, or the tool could not run. |
--check-peer and --check-imports exit non-zero when they find something, so
they can be used directly as a CI or git-hook gate.
Examples
1. Quick Local Diagnosis
Checks formatting and basic version consistency in the current project's node_modules.
diagnose-node-opcua2. Online Diagnosis with Recommendations
Fetches latest version info from NPM to ensure your project is up-to-date and consistent with published releases.
diagnose-node-opcua --online3. Deep Thorough Scan
Recursively scans the entire dependency tree (virtualized via NPM metadata) to find deep inconsistencies, even if not locally installed.
diagnose-node-opcua --thorough4. Peer Dependency Check & Fix
Identifies missing peer dependencies (common source of "module not found" or weird behavior) and adds them to your package.json.
# Check issues
diagnose-node-opcua --check-peer
# Preview fixes
diagnose-node-opcua --check-peer --dry-run
# Apply fixes
diagnose-node-opcua --check-peer --fix5. Diagnose a Specific Project
You can run the tool from anywhere against a specific package.json.
diagnose-node-opcua --package /path/to/other/project/package.json --online6. Undeclared Import Check
Finds packages your code imports but never declares — the class of bug that only
shows up when someone installs your package fresh, or when a bundler resolves
the import from a hoisted node_modules that will not exist elsewhere.
# Check (offline, fast enough for a git hook)
diagnose-node-opcua --check-imports
# Non-standard source layout
diagnose-node-opcua --check-imports --source lib --source tools
# Preview and apply
diagnose-node-opcua --check-imports --dry-run
diagnose-node-opcua --check-imports --fixSample output:
Scanned 80 source file(s) in /project/source
Found 14 node-opcua* / @sterfive/* package(s) imported.
=== Import / Declaration Issues ===
[UNDECLARED] Imported by your sources but absent from package.json:
┌────────────────────────────┬──────────────────────────────┬──────────────┐
│ Package │ Imported By │ Add as │
├────────────────────────────┼──────────────────────────────┼──────────────┤
│ node-opcua-nodeid │ source/bindings/file.ts (+6) │ >=2.175.1 │
└────────────────────────────┴──────────────────────────────┴──────────────┘
[UNSATISFIABLE] Declared as a peer with a floor above any published version.
Package managers that auto-install peers fail the entire install on these.
┌────────────────────────────┬───────────────┬───────────────┐
│ Package │ Declared │ Should be │
├────────────────────────────┼───────────────┼───────────────┤
│ node-opcua-variant │ >=2.175.2 │ >=2.175.1 │
└────────────────────────────┴───────────────┴───────────────┘7. Monorepo: package declares a peer, root installs it
A common layout is a library package that declares its node-opcua surface as
peerDependencies, while the workspace root carries the real dependencies
that get installed. --also-in enforces both halves at once:
diagnose-node-opcua \
--check-imports \
--package packages/my-lib/package.json \
--also-in package.jsonEvery imported package must then be declared as a peer in packages/my-lib
and as a dependency at the root. Packages supplied by the workspace itself
(link:/workspace: specifiers, or a node_modules entry symlinked back into
the repository) are excluded — the workspace already provides those.
8. Monorepo: check every package at once (--workspaces)
Naming one package at a time does not scale, and pointing --check-imports at a
monorepo root is worse than useless: the root usually has no source/ of its
own, so the run scans zero files and reports success — a check that can never
fail. --workspaces resolves the members and checks each one:
diagnose-node-opcua --check-imports --workspaces --also-in package.json=== @sterfive/my-lib ===
Scanned 24 source file(s) in packages/my-lib/source
🎉 Every imported package is declared.
...
Checked 13 package(s), 163 source file(s).Members are resolved through the same patterns check-version-consistency uses
(pnpm-workspace.yaml, package.json "workspaces", lerna.json), so a glob
such as packages/* picks up a new package the moment it exists. Members with no
source directory are skipped rather than reported as passing. --fix applies per
member and accumulates into the single --also-in manifest. --source names a
path inside one package, so it cannot be combined with --workspaces.
9. Checking what the tests import (--include-tests)
Test folders are outside the default scan, so a package imported only by a test is
invisible — nothing verifies that anything installs it. --include-tests adds
test/, tests/ and __tests__/:
diagnose-node-opcua --check-imports --workspaces --include-tests --also-in package.jsonTest imports are judged by a different rule, and reported in their own section so the two are never conflated:
A package imported by a test must be declared in the
--also-inmanifest — something has to install it. It is not required in the package's ownpeerDependencies: a test import is not part of what the package exposes to a host.
Scanned 14 source file(s) in packages/node-opcua-json/source
Scanned 14 test file(s) in packages/node-opcua-json/tests
Found 1 package(s) imported by tests only.
[TEST-ONLY] Imported by tests and absent from package.json:
(a test import need not be a peer of the package, but the root must install it)
┌────────────────────┬────────────────────────────────┬────────────┐
│ node-opcua-nodesets│ tests/json_basic_encode_deco… │ 2.175.1 │
└────────────────────┴────────────────────────────────┴────────────┘--fix therefore writes a test-only import to the --also-in manifest's
dependencies and never to the package's peerDependencies. A package
imported by both sources and tests is a source requirement and stays in the normal
sections. The flag is opt-in, so existing gates keep their current exit codes.
--include-tests requires --also-in: without it there is no manifest to check
against.
How the scanned directories are chosen
--source <dir>wins outright, and is never filtered — you named it, so it is scanned.- Otherwise every one of
source/,src/,lib/that exists and is not git-ignored. All of them, not just the first: a package keeping code in bothsource/andlib/used to have half of it silently unchecked. - Git-ignored candidates are dropped because
lib/is the compiled output ofsource/as often as it is a source folder, and scanning the compiled copy yields duplicate findings and stale-artifact false positives. Outside a git repository nothing is treated as ignored, so every candidate is scanned. - Inside each directory the walk is fully recursive, skipping
node_modules,dist,build,out,coverageand.git. It reads.js/.jsx/.ts/.tsx/.mjs/.cjs/.mts/.ctsand ignores.d.ts.
Development
npm test # vitest run
npm run coverage
npm run build # tsup -> dist/Sources live in lib/ and bin/; the tests run against those directly. Only
dist/ is published: tsup bundles each CLI into one self-contained ESM file with
every dependency inlined and minified, so the installed package has no runtime
dependencies at all.
That matters because the usual way to run this is npx from a CI job or a git
hook, where install time is the whole cost:
| | install size | files |
| :------------------ | :----------- | :---- |
| unbundled (0.4.0) | 9.1 MB | 1122 |
| bundled | 0.9 MB | 14 |
Notes for anyone touching the build:
- Sourcemaps are off on purpose. esbuild embeds
sourcesContent, so publishing them would put the entire original source back in the tarball. - The banner in
tsup.config.mjsinjects a realrequireviacreateRequire. Some bundled dependencies are CommonJS and callrequire()at runtime, which an ESM bundle otherwise answers with "Dynamic require of X is not supported". - The shebang comes from that same banner, not from the entry files, because it has to be the first line of the output.
prepackbuilds, sonpm packandnpm publishalways ship a freshdist/.
The suite is offline: test/mocks.mjs intercepts npm view and serves a fake
registry from test/mock-database.mjs, so no test reaches the network.
test/regressions.test.mjs holds one scenario per defect that has actually
shipped, each documenting the symptom it guards against — notably that npm view
specs must be shell-quoted (an unquoted pkg@>=1.2.3 is parsed by the shell as
an output redirection, which silently empties stdout and drops a file named
1.2.3 in the working directory), and that findings must never be reported as
"conformant".
Troubleshooting
- Private Packages: Ensure your
.npmrcis correctly configured in the project root or user home directory. The tool runsnpmcommands using the project root as the working directory to pick up local config. - Network: Online modes require internet access to reach the NPM registry.
- Cache: The tool caches NPM metadata in your system's cache directory (e.g.,
~/.cache/diagnose-node-opcuaor%LOCALAPPDATA%\diagnose-node-opcua). If you suspect stale data, you can clear this directory.
License
MIT
reference:
see also: https://github.com/bmish/check-dependency-version-consistency
