node-module-license-output 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 myooken
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2025 myooken
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,133 +1,150 @@
1
- # Third-Party License Output for node_modules
2
-
3
- [![npm version](https://img.shields.io/npm/v/@myooken/license-output.svg)](https://www.npmjs.com/package/@myooken/license-output)
4
- [![npm downloads](https://img.shields.io/npm/dm/@myooken/license-output.svg)](https://www.npmjs.com/package/@myooken/license-output)
5
- [![node](https://img.shields.io/node/v/@myooken/license-output.svg)](https://www.npmjs.com/package/@myooken/license-output)
6
-
7
- https://www.npmjs.com/package/@myooken/license-output
8
-
9
- ### What is this?
10
-
11
- A tool to scan `node_modules` and **output third-party licenses in Markdown**.
12
- It generates two files: `THIRD-PARTY-LICENSE.md` (main content) and `THIRD-PARTY-LICENSE-REVIEW.md` (review checklist).
13
-
14
- ### Highlights
15
-
16
- - **ESM / Node.js 18+**, zero dependencies
17
- - **Outputs full license texts** from LICENSE/NOTICE/COPYRIGHT/THIRD-PARTY-NOTICES/THIRD-PARTY-LICENSES/ThirdPartyNoticeText/ThirdPartyText/COPYING files
18
- - **Review file** flags missing Source / license / license files
19
- - `--fail-on-missing` supports CI enforcement
20
-
21
- CLI command: `third-party-license`
22
-
23
- ### Usage
24
-
25
- #### Run without installing (recommended)
26
-
27
- ```bash
28
- npx --package=@myooken/license-output -- third-party-license
29
- ```
30
-
31
- #### Run via npm exec
32
-
33
- ```bash
34
- npm exec --package=@myooken/license-output -- third-party-license
35
- ```
36
-
37
- #### Install globally
38
-
39
- ```bash
40
- npm i -g @myooken/license-output
41
- third-party-license
42
- ```
43
-
44
- ### Options
45
-
46
- | Option | Description | Default |
47
- | ---------------------- | --------------------------------------------------------------------------- | ------------------------------- |
48
- | `--node-modules <dir>` | Path to `node_modules` | `node_modules` |
49
- | `--review [file]` | Write review file only; optional filename | `THIRD-PARTY-LICENSE-REVIEW.md` |
50
- | `--license [file]` | Write main file only; optional filename | `THIRD-PARTY-LICENSE.md` |
51
- | `--recreate` | Regenerate files from current `node_modules` only (drops removed packages) | `true` (default) |
52
- | `--update` | Merge with existing outputs, keep removed packages, and mark their presence | `false` |
53
- | `--fail-on-missing` | Exit with code 1 if LICENSE/NOTICE/COPYRIGHT/THIRD-PARTY-NOTICES/THIRD-PARTY-LICENSES/ThirdPartyNoticeText/ThirdPartyText/COPYING are missing | `false` |
54
- | `-h`, `--help` | Show help | - |
55
-
56
- > If neither `--review` nor `--license` is specified, **both files are generated**.
57
- > Packages in both files are sorted by name@version; `--update` keeps entries for packages no longer in `node_modules` and annotates their usage status.
58
-
59
- ### Examples
60
-
61
- ```bash
62
- # Default (both files)
63
- third-party-license
64
-
65
- # Update existing files without dropping removed packages
66
- third-party-license --update
67
-
68
- # Custom node_modules path
69
- third-party-license --node-modules ./path/to/node_modules
70
-
71
- # Review-only output (optional filename)
72
- third-party-license --review
73
- third-party-license --review ./out/THIRD-PARTY-LICENSE-REVIEW.md
74
-
75
- # Main-only output (optional filename)
76
- third-party-license --license
77
- third-party-license --license ./out/THIRD-PARTY-LICENSE.md
78
-
79
- # Exit with code 1 when something is missing (with --fail-on-missing)
80
- third-party-license --fail-on-missing
81
- ```
82
-
83
- ### Programmatic API
84
-
85
- ```js
86
- import { collectThirdPartyLicenses } from "@myooken/license-output";
87
-
88
- const result = await collectThirdPartyLicenses({
89
- nodeModules: "./node_modules",
90
- outFile: "./THIRD-PARTY-LICENSE.md",
91
- reviewFile: "./THIRD-PARTY-LICENSE-REVIEW.md",
92
- failOnMissing: false,
93
- // mode: "update", // keep packages missing from node_modules when updating files
94
- });
95
-
96
- console.log(result.mainContent);
97
- console.log(result.reviewContent);
98
- ```
99
-
100
- Outputs are sorted by package key. Use `mode: "update"` to merge with existing files and keep packages that are no longer in `node_modules`, with their usage shown in both outputs.
101
-
102
- ### Output overview
103
-
104
- - **THIRD-PARTY-LICENSE.md**
105
- - List of packages
106
- - Source / License info
107
- - Full LICENSE/NOTICE/COPYRIGHT/THIRD-PARTY-NOTICES/THIRD-PARTY-LICENSES/ThirdPartyNoticeText/ThirdPartyText/COPYING texts
108
- - Usage line shows whether the package is present in the current `node_modules`
109
- - **THIRD-PARTY-LICENSE-REVIEW.md**
110
- - Review-oriented checklist
111
- - Usage-aware status (present / not found) for each package
112
- - **Missing summary** section
113
-
114
- ### How it differs from typical npm license tools (general view)
115
-
116
- > Examples: `license-checker`, `license-report`, `license-finder`
117
-
118
- - **Focused on bundling full license texts into a single Markdown file**
119
- - Many existing tools emphasize JSON/CSV reports; this tool emphasizes **ready-to-share license documents**.
120
- - **Separate review file** to track missing metadata
121
- - Easier to integrate into audit workflows.
122
- - **ESM / Node.js 18+ with no dependencies**
123
- - Simple runtime requirements.
124
-
125
- ### Notes
126
-
127
- - Scans all packages under `node_modules` (including nested dependencies); license files are searched only in each package root directory.
128
- - Recognizes LICENSE, NOTICE, COPYRIGHT, THIRD-PARTY-NOTICES, THIRD-PARTY-LICENSES, ThirdPartyNoticeText/ThirdPartyText, and COPYING files (e.g., TypeScript's `ThirdPartyNoticeText.txt`).
129
- - Exit code 0: success.
130
- - Exit code 1: missing license files when `--fail-on-missing` is set, or `node_modules` not found.
131
- - Throws an error if `node_modules` does not exist.
132
- - Missing `license` or `repository` fields are flagged in the review file.
133
- - Paths printed in outputs/logs are shown relative to the current working directory.
1
+ # Third-Party License Output for node_modules
2
+
3
+ Package name: node-module-license-output
4
+
5
+ https://www.npmjs.com/package/node-module-license-output
6
+
7
+ ### What is this?
8
+
9
+ A tool to scan `node_modules` and **output third-party licenses in Markdown**.
10
+ It generates two files: `THIRD-PARTY-LICENSE.md` (main content) and `THIRD-PARTY-LICENSE-REVIEW.md` (review checklist).
11
+
12
+ ### Highlights
13
+
14
+ - **ESM / Node.js 18+**, zero dependencies
15
+ - **Outputs full license texts** from LICENSE/NOTICE/COPYRIGHT/THIRD-PARTY-NOTICES/THIRD-PARTY-LICENSES/ThirdPartyNoticeText/ThirdPartyText/COPYING files
16
+ - **Review file** flags missing Source / license / license files
17
+ - `--fail-on-missing` supports CI enforcement
18
+ - Requires a `package.json` next to the target `node_modules` when using `--dependencies-only`
19
+ - Intended for npm/pnpm usage (node_modules layout)
20
+
21
+ CLI command: `third-party-license`
22
+
23
+ ### Usage
24
+
25
+ #### Run without installing (recommended)
26
+
27
+ ```bash
28
+ npx --package=node-module-license-output -- third-party-license
29
+ ```
30
+
31
+ #### Run via npm exec
32
+
33
+ ```bash
34
+ npm exec --package=node-module-license-output -- third-party-license
35
+ ```
36
+
37
+ #### Install globally
38
+
39
+ ```bash
40
+ npm i -g node-module-license-output
41
+ third-party-license
42
+ ```
43
+
44
+ ### Options
45
+
46
+ | Option | Description | Default |
47
+ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
48
+ | `--node-modules <dir>` | Path to `node_modules` | `node_modules` |
49
+ | `--review [file]` | Write review file only; optional filename | `THIRD-PARTY-LICENSE-REVIEW.md` |
50
+ | `--license [file]` | Write main file only; optional filename | `THIRD-PARTY-LICENSE.md` |
51
+ | `--recreate` | Regenerate files from current `node_modules` only (drops removed packages) | `true` (default) |
52
+ | `--update` | Merge with existing outputs, keep removed packages, and mark their presence | `false` |
53
+ | `--fail-on-missing` | Exit with code 1 if LICENSE/NOTICE/COPYRIGHT/THIRD-PARTY-NOTICES/THIRD-PARTY-LICENSES/ThirdPartyNoticeText/ThirdPartyText/COPYING are missing | `false` |
54
+ | `--dependencies-only` | Limit output to dependency tree rooted at `dependencies` (and `optionalDependencies`) in the project `package.json` | `true` (default) |
55
+ | `--dependencies-all` | Scan all packages under `node_modules` | `false` |
56
+ | `-h`, `--help` | Show help | - |
57
+
58
+ > If neither `--review` nor `--license` is specified, **both files are generated**.
59
+ > Packages in both files are sorted by name@version; `--update` keeps entries for packages no longer in `node_modules` and annotates their usage status.
60
+ > `--dependencies-only` reads the `package.json` next to the target `node_modules` and limits output to the dependency tree rooted at `dependencies` and `optionalDependencies` (not `devDependencies` or `peerDependencies`); it throws if that `package.json` is not found.
61
+ > Operationally, the default (`--dependencies-only`) is intended for day-to-day use, while `--dependencies-all` is intended for SBOM-like, exhaustive reporting.
62
+ > When duplicates are disambiguated by path, `--update` may treat path-changed entries as new.
63
+
64
+ ### Examples
65
+
66
+ ```bash
67
+ # Default (both files)
68
+ third-party-license
69
+
70
+ # Update existing files without dropping removed packages
71
+ third-party-license --update
72
+
73
+ # Custom node_modules path
74
+ third-party-license --node-modules ./path/to/node_modules
75
+
76
+ # Review-only output (optional filename)
77
+ third-party-license --review
78
+ third-party-license --review ./out/THIRD-PARTY-LICENSE-REVIEW.md
79
+
80
+ # Main-only output (optional filename)
81
+ third-party-license --license
82
+ third-party-license --license ./out/THIRD-PARTY-LICENSE.md
83
+
84
+ # Exit with code 1 when something is missing (with --fail-on-missing)
85
+ third-party-license --fail-on-missing
86
+
87
+ # Day-to-day (dependencies only)
88
+ third-party-license --dependencies-only
89
+
90
+ # Audit / SBOM-like (scan all packages under node_modules)
91
+ third-party-license --dependencies-all
92
+ third-party-license --dependencies-all --license ./out/THIRD-PARTY-LICENSE.md --review ./out/THIRD-PARTY-LICENSE-REVIEW.md
93
+ ```
94
+
95
+ ### Programmatic API
96
+
97
+ ```js
98
+ import { collectThirdPartyLicenses } from "node-module-license-output";
99
+
100
+ const result = await collectThirdPartyLicenses({
101
+ nodeModules: "./node_modules",
102
+ outFile: "./THIRD-PARTY-LICENSE.md",
103
+ reviewFile: "./THIRD-PARTY-LICENSE-REVIEW.md",
104
+ failOnMissing: false,
105
+ dependenciesOnly: true,
106
+ // mode: "update", // keep packages missing from node_modules when updating files
107
+ });
108
+
109
+ console.log(result.mainContent);
110
+ console.log(result.reviewContent);
111
+ ```
112
+
113
+ Outputs are sorted by package key. Use `mode: "update"` to merge with existing files and keep packages that are no longer in `node_modules`, with their usage shown in both outputs.
114
+
115
+ ### Output overview
116
+
117
+ - **THIRD-PARTY-LICENSE.md**
118
+ - List of packages (default: only those reachable from `dependencies`/`optionalDependencies`)
119
+ - Source / License info
120
+ - Full LICENSE/NOTICE/COPYRIGHT/THIRD-PARTY-NOTICES/THIRD-PARTY-LICENSES/ThirdPartyNoticeText/ThirdPartyText/COPYING texts
121
+ - Usage line shows whether the package is present in the current `node_modules` (or kept from previous output with `--update`)
122
+ - **THIRD-PARTY-LICENSE-REVIEW.md**
123
+ - Review-oriented checklist
124
+ - Usage-aware status (present / not found) for each package
125
+ - **Missing summary** section
126
+
127
+ ### How it differs from typical npm license tools (general view)
128
+
129
+ > Examples: `license-checker`, `license-report`, `license-finder`
130
+
131
+ - **Focused on bundling full license texts into a single Markdown file**
132
+ - Many existing tools emphasize JSON/CSV reports; this tool emphasizes **ready-to-share license documents**.
133
+ - **Separate review file** to track missing metadata
134
+ - Easier to integrate into audit workflows.
135
+ - **ESM / Node.js 18+ with no dependencies**
136
+ - Simple runtime requirements.
137
+
138
+ ### Notes
139
+
140
+ - Default output is restricted to the dependency tree from `dependencies` and `optionalDependencies`.
141
+ - Use `--dependencies-all` to scan all packages under `node_modules` (including nested dependencies).
142
+ - License files are searched only in each package root directory.
143
+ - If multiple copies of the same name@version exist, dependency-only output disambiguates them by path.
144
+ - pnpm installs may be resolved via `.pnpm` directories under `node_modules`; this tool follows resolved package paths rather than only direct `node_modules/<pkg>` locations.
145
+ - Recognizes LICENSE, NOTICE, COPYRIGHT, THIRD-PARTY-NOTICES, THIRD-PARTY-LICENSES, ThirdPartyNoticeText/ThirdPartyText, and COPYING files (e.g., TypeScript's `ThirdPartyNoticeText.txt`).
146
+ - Exit code 0: success.
147
+ - Exit code 1: missing license files when `--fail-on-missing` is set, or `node_modules` not found.
148
+ - Throws an error if `node_modules` does not exist.
149
+ - Missing `license` or `repository` fields are flagged in the review file.
150
+ - Paths printed in outputs/logs are shown relative to the current working directory.
package/package.json CHANGED
@@ -1,38 +1,38 @@
1
- {
2
- "name": "node-module-license-output",
3
- "version": "0.3.0",
4
- "description": "Generate third-party-license markdown by scanning licenses in node_modules.",
5
- "license": "MIT",
6
- "type": "module",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/myooken/collect-node-modules-licenses.git"
10
- },
11
- "homepage": "https://github.com/myooken/collect-node-modules-licenses#readme",
12
- "bugs": {
13
- "url": "https://github.com/myooken/collect-node-modules-licenses/issues"
14
- },
15
- "keywords": [
16
- "license",
17
- "third-party",
18
- "notices",
19
- "node_modules",
20
- "oss",
21
- "compliance",
22
- "cli"
23
- ],
24
- "exports": {
25
- ".": "./src/core.js"
26
- },
27
- "bin": {
28
- "third-party-license": "./src/cli.js"
29
- },
30
- "files": [
31
- "src",
32
- "README.md",
33
- "LICENSE"
34
- ],
35
- "engines": {
36
- "node": ">=18"
37
- }
38
- }
1
+ {
2
+ "name": "node-module-license-output",
3
+ "version": "1.0.0",
4
+ "description": "Generate third-party-license markdown by scanning licenses in node_modules.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/myooken/collect-node-modules-licenses.git"
10
+ },
11
+ "homepage": "https://github.com/myooken/collect-node-modules-licenses#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/myooken/collect-node-modules-licenses/issues"
14
+ },
15
+ "keywords": [
16
+ "license",
17
+ "third-party",
18
+ "notices",
19
+ "node_modules",
20
+ "oss",
21
+ "compliance",
22
+ "cli"
23
+ ],
24
+ "exports": {
25
+ ".": "./src/core.js"
26
+ },
27
+ "bin": {
28
+ "third-party-license": "./src/cli.js"
29
+ },
30
+ "files": [
31
+ "src",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "engines": {
36
+ "node": ">=18"
37
+ }
38
+ }
package/src/cli.js CHANGED
@@ -39,6 +39,10 @@ function parseArgs(argv) {
39
39
  args.mode = "update";
40
40
  } else if (a === "--fail-on-missing") {
41
41
  args.failOnMissing = true;
42
+ } else if (a === "--dependencies-only") {
43
+ args.dependenciesOnly = true;
44
+ } else if (a === "--dependencies-all") {
45
+ args.dependenciesOnly = false;
42
46
  } else if (a === "-h" || a === "--help") {
43
47
  showHelp();
44
48
  process.exit(0);
@@ -71,7 +75,7 @@ function applyOutputMode(mode, args) {
71
75
 
72
76
  function showHelp() {
73
77
  console.log(`Usage:
74
- third-party-license [--node-modules <dir>] [--review [file]] [--license [file]] [--recreate|--update] [--fail-on-missing]
78
+ third-party-license [--node-modules <dir>] [--review [file]] [--license [file]] [--recreate|--update] [--fail-on-missing] [--dependencies-only|--dependencies-all]
75
79
  `);
76
80
  }
77
81
 
package/src/constants.js CHANGED
@@ -5,6 +5,7 @@ export const DEFAULT_OPTIONS = {
5
5
  reviewFile: "THIRD-PARTY-LICENSE-REVIEW.md",
6
6
  failOnMissing: false,
7
7
  mode: "recreate", // "recreate" | "update"
8
+ dependenciesOnly: true,
8
9
  };
9
10
 
10
11
  // ライセンスらしいファイル名を検出する正規表現
package/src/core.js CHANGED
@@ -73,6 +73,9 @@ function normalizeOptions(options) {
73
73
  failOnMissing: Boolean(
74
74
  options.failOnMissing ?? DEFAULT_OPTIONS.failOnMissing
75
75
  ),
76
+ dependenciesOnly: Boolean(
77
+ options.dependenciesOnly ?? DEFAULT_OPTIONS.dependenciesOnly
78
+ ),
76
79
  writeMain: options.writeMain ?? true,
77
80
  writeReview: options.writeReview ?? true,
78
81
  warn: options.onWarn ?? defaultWarn,
@@ -0,0 +1,123 @@
1
+ import fsp from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { readPackageJson, uniqSorted } from "./fs-utils.js";
4
+ // Cache realpath resolutions to keep dependency traversal fast.
5
+ const realpathCache = new Map();
6
+ // dependencies/optionalDependencies から到達可能なパッケージのディレクトリを収集
7
+ export async function collectDependencyDirs(opts) {
8
+ const nodeModulesReal = await toRealPath(opts.nodeModules);
9
+ const projectDir = path.dirname(path.resolve(opts.nodeModules));
10
+ const projectRootBoundary = projectDir;
11
+ const projectPackageJson = path.join(projectDir, "package.json");
12
+ const rootPkg = await readPackageJson(projectPackageJson);
13
+ if (!rootPkg) {
14
+ throw new Error(`package.json not found: ${projectPackageJson}`);
15
+ }
16
+ const rootDeps = collectDependencyNames(rootPkg);
17
+ if (rootDeps.length === 0) {
18
+ opts.warn("No dependencies found in package.json; output will be empty.");
19
+ return new Set();
20
+ }
21
+ const allowed = new Set();
22
+ const visitedDirs = new Set();
23
+ const stack = [];
24
+ for (const depName of rootDeps) {
25
+ const depDir = await resolvePackageDir(
26
+ projectDir,
27
+ depName,
28
+ nodeModulesReal,
29
+ projectRootBoundary
30
+ );
31
+ if (!depDir) {
32
+ opts.warn(`Dependency not found in node_modules: ${depName}`);
33
+ continue;
34
+ }
35
+ stack.push(depDir);
36
+ }
37
+ while (stack.length > 0) {
38
+ const pkgDir = stack.pop();
39
+ if (!pkgDir || visitedDirs.has(pkgDir)) continue;
40
+ visitedDirs.add(pkgDir);
41
+ const pkg = await readPackageJson(path.join(pkgDir, "package.json"));
42
+ if (!pkg) continue;
43
+ const name =
44
+ typeof pkg.name === "string" && pkg.name.trim().length > 0
45
+ ? pkg.name.trim()
46
+ : "";
47
+ const version =
48
+ typeof pkg.version === "string" && pkg.version.trim().length > 0
49
+ ? pkg.version.trim()
50
+ : "";
51
+ if (!name || !version) continue;
52
+ allowed.add(pkgDir);
53
+ for (const depName of collectDependencyNames(pkg)) {
54
+ const depDir = await resolvePackageDir(
55
+ pkgDir,
56
+ depName,
57
+ nodeModulesReal,
58
+ projectRootBoundary
59
+ );
60
+ if (!depDir) {
61
+ opts.warn(
62
+ `Dependency not found in node_modules: ${depName} (required by ${name}@${version})`
63
+ );
64
+ continue;
65
+ }
66
+ stack.push(depDir);
67
+ }
68
+ }
69
+ return allowed;
70
+ }
71
+ // node_modules からの相対パスで同名同バージョンの区別をつける
72
+ export function makePackagePathLabel(pkgDir, nodeModulesRoot) {
73
+ const rel = path.relative(nodeModulesRoot, pkgDir).replace(/\\/g, "/");
74
+ return rel || path.basename(pkgDir);
75
+ }
76
+ function collectDependencyNames(pkg) {
77
+ const deps =
78
+ pkg?.dependencies && typeof pkg.dependencies === "object"
79
+ ? Object.keys(pkg.dependencies)
80
+ : [];
81
+ const optionalDeps =
82
+ pkg?.optionalDependencies && typeof pkg.optionalDependencies === "object"
83
+ ? Object.keys(pkg.optionalDependencies)
84
+ : [];
85
+ return uniqSorted([...deps, ...optionalDeps]);
86
+ }
87
+ async function resolvePackageDir(
88
+ fromDir,
89
+ depName,
90
+ nodeModulesRoot,
91
+ projectRootBoundary
92
+ ) {
93
+ const rootParent = path.dirname(nodeModulesRoot);
94
+ const boundary = path.resolve(projectRootBoundary);
95
+ let dir = fromDir;
96
+ while (true) {
97
+ const nmDir = path.join(dir, "node_modules");
98
+ const candidateDir = path.join(nmDir, depName);
99
+ const candidatePkg = path.join(candidateDir, "package.json");
100
+ const stat = await fsp.stat(candidatePkg).catch(() => null);
101
+ if (stat && stat.isFile()) return toRealPath(candidateDir);
102
+ if (dir === rootParent) break;
103
+ if (path.resolve(dir) === boundary) break;
104
+ const parent = path.dirname(dir);
105
+ if (parent === dir) break;
106
+ dir = parent;
107
+ }
108
+ return null;
109
+ }
110
+
111
+ async function toRealPath(targetPath) {
112
+ const cached = realpathCache.get(targetPath);
113
+ if (cached) return cached;
114
+ try {
115
+ const resolved = await fsp.realpath(targetPath);
116
+ realpathCache.set(targetPath, resolved);
117
+ return resolved;
118
+ } catch {
119
+ const resolved = path.resolve(targetPath);
120
+ realpathCache.set(targetPath, resolved);
121
+ return resolved;
122
+ }
123
+ }
package/src/fs-utils.js CHANGED
@@ -1,114 +1,114 @@
1
- import fsp from "node:fs/promises";
2
- import path from "node:path";
3
- import { LICENSE_LIKE_RE } from "./constants.js";
4
-
5
- // 文字コードを判定しつつ文字列へデコードする
6
- export function decodeSmart(buf) {
7
- if (buf.length >= 2) {
8
- const b0 = buf[0];
9
- const b1 = buf[1];
10
- if (b0 === 0xff && b1 === 0xfe) {
11
- return new TextDecoder("utf-16le").decode(buf.subarray(2));
12
- }
13
- if (b0 === 0xfe && b1 === 0xff) {
14
- const be = buf.subarray(2);
15
- const swapped = Buffer.allocUnsafe(be.length);
16
- for (let i = 0; i + 1 < be.length; i += 2) {
17
- swapped[i] = be[i + 1];
18
- swapped[i + 1] = be[i];
19
- }
20
- return new TextDecoder("utf-16le").decode(swapped);
21
- }
22
- }
23
- if (
24
- buf.length >= 3 &&
25
- buf[0] === 0xef &&
26
- buf[1] === 0xbb &&
27
- buf[2] === 0xbf
28
- ) {
29
- return new TextDecoder("utf-8").decode(buf.subarray(3));
30
- }
31
-
32
- try {
33
- return new TextDecoder("utf-8", { fatal: true }).decode(buf);
34
- } catch {
35
- return new TextDecoder("latin1").decode(buf);
36
- }
37
- }
38
-
39
- // LICENSE/NOTICE などのライセンス系ファイルを探す
40
- export async function getLicenseLikeFilesInFolderRoot(pkgDir) {
41
- try {
42
- const ents = await fsp.readdir(pkgDir, { withFileTypes: true });
43
- return ents
44
- .filter((e) => e.isFile() && LICENSE_LIKE_RE.test(e.name))
45
- .map((e) => path.join(pkgDir, e.name))
46
- .sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
47
- } catch {
48
- return [];
49
- }
50
- }
51
-
52
- // package.json を深さ優先で探す(node_modules/.bin は除外)
53
- export async function* walkForPackageJson(rootDir) {
54
- const stack = [rootDir];
55
-
56
- while (stack.length) {
57
- const dir = stack.pop();
58
- if (!dir) continue;
59
-
60
- let ents;
61
- try {
62
- ents = await fsp.readdir(dir, { withFileTypes: true });
63
- } catch {
64
- continue;
65
- }
66
-
67
- for (const e of ents) {
68
- const full = path.join(dir, e.name);
69
- if (e.isDirectory()) {
70
- if (e.name === ".bin") continue;
71
- stack.push(full);
72
- continue;
73
- }
74
- if (e.isFile() && e.name === "package.json") {
75
- if (full.includes(`${path.sep}.bin${path.sep}`)) continue;
76
- yield full;
77
- }
78
- }
79
- }
80
- }
81
-
82
- export async function readPackageJson(pjPath) {
83
- try {
84
- const txt = await fsp.readFile(pjPath, "utf8");
85
- return JSON.parse(txt);
86
- } catch {
87
- return null;
88
- }
89
- }
90
-
91
- // BOM や UTF-16 を考慮してテキストを読み込む
92
- export async function readTextFileSmart(filePath) {
93
- const buf = await fsp.readFile(filePath);
94
- return decodeSmart(buf);
95
- }
96
-
97
- export function mdSafeText(s) {
98
- return String(s).replace(/```/g, "``\u200b`");
99
- }
100
-
101
- export function uniqSorted(arr) {
102
- return [...new Set(arr)].sort();
103
- }
104
-
105
- // アンカー用の安全な ID を作る
106
- export function makeAnchorId(key) {
107
- return (
108
- "pkg-" +
109
- String(key)
110
- .toLowerCase()
111
- .replace(/[^a-z0-9]+/g, "-")
112
- .replace(/^-+|-+$/g, "")
113
- );
114
- }
1
+ import fsp from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { LICENSE_LIKE_RE } from "./constants.js";
4
+
5
+ // 文字コードを判定しつつ文字列へデコードする
6
+ export function decodeSmart(buf) {
7
+ if (buf.length >= 2) {
8
+ const b0 = buf[0];
9
+ const b1 = buf[1];
10
+ if (b0 === 0xff && b1 === 0xfe) {
11
+ return new TextDecoder("utf-16le").decode(buf.subarray(2));
12
+ }
13
+ if (b0 === 0xfe && b1 === 0xff) {
14
+ const be = buf.subarray(2);
15
+ const swapped = Buffer.allocUnsafe(be.length);
16
+ for (let i = 0; i + 1 < be.length; i += 2) {
17
+ swapped[i] = be[i + 1];
18
+ swapped[i + 1] = be[i];
19
+ }
20
+ return new TextDecoder("utf-16le").decode(swapped);
21
+ }
22
+ }
23
+ if (
24
+ buf.length >= 3 &&
25
+ buf[0] === 0xef &&
26
+ buf[1] === 0xbb &&
27
+ buf[2] === 0xbf
28
+ ) {
29
+ return new TextDecoder("utf-8").decode(buf.subarray(3));
30
+ }
31
+
32
+ try {
33
+ return new TextDecoder("utf-8", { fatal: true }).decode(buf);
34
+ } catch {
35
+ return new TextDecoder("latin1").decode(buf);
36
+ }
37
+ }
38
+
39
+ // LICENSE/NOTICE などのライセンス系ファイルを探す
40
+ export async function getLicenseLikeFilesInFolderRoot(pkgDir) {
41
+ try {
42
+ const ents = await fsp.readdir(pkgDir, { withFileTypes: true });
43
+ return ents
44
+ .filter((e) => e.isFile() && LICENSE_LIKE_RE.test(e.name))
45
+ .map((e) => path.join(pkgDir, e.name))
46
+ .sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
47
+ } catch {
48
+ return [];
49
+ }
50
+ }
51
+
52
+ // package.json を深さ優先で探す(node_modules/.bin は除外)
53
+ export async function* walkForPackageJson(rootDir) {
54
+ const stack = [rootDir];
55
+
56
+ while (stack.length) {
57
+ const dir = stack.pop();
58
+ if (!dir) continue;
59
+
60
+ let ents;
61
+ try {
62
+ ents = await fsp.readdir(dir, { withFileTypes: true });
63
+ } catch {
64
+ continue;
65
+ }
66
+
67
+ for (const e of ents) {
68
+ const full = path.join(dir, e.name);
69
+ if (e.isDirectory()) {
70
+ if (e.name === ".bin") continue;
71
+ stack.push(full);
72
+ continue;
73
+ }
74
+ if (e.isFile() && e.name === "package.json") {
75
+ if (full.includes(`${path.sep}.bin${path.sep}`)) continue;
76
+ yield full;
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ export async function readPackageJson(pjPath) {
83
+ try {
84
+ const txt = await fsp.readFile(pjPath, "utf8");
85
+ return JSON.parse(txt);
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ // BOM や UTF-16 を考慮してテキストを読み込む
92
+ export async function readTextFileSmart(filePath) {
93
+ const buf = await fsp.readFile(filePath);
94
+ return decodeSmart(buf);
95
+ }
96
+
97
+ export function mdSafeText(s) {
98
+ return String(s).replace(/```/g, "``\u200b`");
99
+ }
100
+
101
+ export function uniqSorted(arr) {
102
+ return [...new Set(arr)].sort();
103
+ }
104
+
105
+ // アンカー用の安全な ID を作る
106
+ export function makeAnchorId(key) {
107
+ return (
108
+ "pkg-" +
109
+ String(key)
110
+ .toLowerCase()
111
+ .replace(/[^a-z0-9]+/g, "-")
112
+ .replace(/^-+|-+$/g, "")
113
+ );
114
+ }
@@ -0,0 +1,40 @@
1
+ // license フィールドを人間可読にまとめる(文字列/オブジェクト/配列に対応)
2
+ export function formatLicense(raw) {
3
+ const parts = [];
4
+
5
+ const pushMaybe = (v) => {
6
+ if (typeof v === "string" && v.trim()) parts.push(v.trim());
7
+ };
8
+
9
+ const handleObj = (licObj) => {
10
+ if (!licObj || typeof licObj !== "object") return;
11
+ const type =
12
+ typeof licObj.type === "string" && licObj.type.trim()
13
+ ? licObj.type.trim()
14
+ : "";
15
+ const url =
16
+ typeof licObj.url === "string" && licObj.url.trim()
17
+ ? licObj.url.trim()
18
+ : "";
19
+ if (type && url) {
20
+ parts.push(`${type} (${url})`);
21
+ } else {
22
+ pushMaybe(type);
23
+ pushMaybe(url);
24
+ }
25
+ };
26
+
27
+ if (typeof raw === "string") {
28
+ pushMaybe(raw);
29
+ } else if (Array.isArray(raw)) {
30
+ for (const lic of raw) {
31
+ if (typeof lic === "string") pushMaybe(lic);
32
+ else handleObj(lic);
33
+ }
34
+ } else {
35
+ handleObj(raw);
36
+ }
37
+
38
+ if (parts.length === 0) return null;
39
+ return [...new Set(parts)].join(" | ");
40
+ }
@@ -0,0 +1,68 @@
1
+ import path from "node:path";
2
+ import {
3
+ getLicenseLikeFilesInFolderRoot,
4
+ readTextFileSmart,
5
+ } from "./fs-utils.js";
6
+ import { getRepositoryUrl } from "./url.js";
7
+ import { LICENSE_FILES_LABEL } from "./constants.js";
8
+ import { formatLicense } from "./license-utils.js";
9
+
10
+ // ライセンス情報と警告フラグをまとめてエントリ化する
11
+ export async function buildPackageEntry({
12
+ pkg,
13
+ pkgDir,
14
+ key,
15
+ baseKey,
16
+ }) {
17
+ const source = getRepositoryUrl(pkg);
18
+ const license = formatLicense(pkg.license);
19
+ const flags = [];
20
+ const missing = {
21
+ source: false,
22
+ licenseField: false,
23
+ licenseFiles: false,
24
+ };
25
+
26
+ if (!source) {
27
+ missing.source = true;
28
+ flags.push("Missing Source");
29
+ }
30
+ if (!license) {
31
+ missing.licenseField = true;
32
+ flags.push("Missing package.json license");
33
+ }
34
+
35
+ const licFiles = await getLicenseLikeFilesInFolderRoot(pkgDir);
36
+ const fileNames = licFiles.map((f) => path.basename(f));
37
+
38
+ if (licFiles.length === 0) {
39
+ missing.licenseFiles = true;
40
+ const missingMsg = `Missing ${LICENSE_FILES_LABEL} files`;
41
+ flags.push(missingMsg);
42
+ }
43
+
44
+ const licenseTexts =
45
+ licFiles.length > 0
46
+ ? await Promise.all(
47
+ licFiles.map(async (filePath) => ({
48
+ name: path.basename(filePath),
49
+ text: await readTextFileSmart(filePath),
50
+ }))
51
+ )
52
+ : [];
53
+
54
+ return {
55
+ entry: {
56
+ key,
57
+ baseKey: baseKey ?? key,
58
+ source,
59
+ license,
60
+ fileNames,
61
+ flags,
62
+ licenseTexts,
63
+ dir: pkgDir,
64
+ missing,
65
+ },
66
+ missing,
67
+ };
68
+ }
package/src/scan.js CHANGED
@@ -1,89 +1,71 @@
1
+ import fsp from "node:fs/promises";
1
2
  import path from "node:path";
2
3
  import {
3
- getLicenseLikeFilesInFolderRoot,
4
4
  makeAnchorId,
5
5
  readPackageJson,
6
- readTextFileSmart,
7
6
  uniqSorted,
8
7
  walkForPackageJson,
9
8
  } from "./fs-utils.js";
10
- import { getRepositoryUrl } from "./url.js";
9
+ import {
10
+ collectDependencyDirs,
11
+ makePackagePathLabel,
12
+ } from "./dependency-tree.js";
13
+ import { buildPackageEntry } from "./package-entry.js";
11
14
  import { LICENSE_FILES_LABEL } from "./constants.js";
12
-
15
+ // Cache realpath resolutions to avoid repeated fs calls during large scans.
16
+ const realpathCache = new Map();
13
17
  // node_modules を走査してパッケージ情報を集約する
14
18
  export async function gatherPackages(opts) {
15
- const missingFiles = [];
16
- const missingSource = [];
17
- const missingLicenseField = [];
19
+ const nodeModulesReal = await toRealPath(opts.nodeModules);
20
+ const allowedDirs = opts.dependenciesOnly
21
+ ? await collectDependencyDirs(opts)
22
+ : null;
18
23
  const packages = [];
19
24
  const seen = new Set();
20
-
21
25
  for await (const pj of walkForPackageJson(opts.nodeModules)) {
22
- const pkgDir = path.dirname(pj);
26
+ const pkgDir = await toRealPath(path.dirname(pj));
27
+ if (allowedDirs && !allowedDirs.has(pkgDir)) continue;
23
28
  const pkg = await readPackageJson(pj);
24
29
  if (!pkg) continue;
30
+ const ident = getPackageIdentity(pkg);
31
+ if (!ident) continue;
32
+ const baseKey = `${ident.name}@${ident.version}`;
33
+ const seenKey = pkgDir;
34
+ if (seen.has(seenKey)) continue;
35
+ seen.add(seenKey);
36
+ const key = baseKey;
37
+ const { entry } = await buildPackageEntry({
38
+ pkg,
39
+ pkgDir,
40
+ key,
41
+ baseKey,
42
+ });
43
+ packages.push(entry);
44
+ }
25
45
 
26
- const name =
27
- typeof pkg.name === "string" && pkg.name.trim().length > 0
28
- ? pkg.name.trim()
29
- : "";
30
- const version =
31
- typeof pkg.version === "string" && pkg.version.trim().length > 0
32
- ? pkg.version.trim()
33
- : "";
34
- if (!name || !version) continue;
35
-
36
- const key = `${name}@${version}`;
37
- if (seen.has(key)) continue;
38
- seen.add(key);
39
-
40
- const anchor = makeAnchorId(key);
41
- const source = getRepositoryUrl(pkg);
42
- const license = formatLicense(pkg.license); // 文字列/オブジェクト/配列すべてを受け付ける
43
-
44
- const flags = [];
45
- if (!source) {
46
- missingSource.push(key);
47
- flags.push("Missing Source");
48
- opts.warn(`Unknown source: ${key}`);
46
+ // 同名同バージョンが複数ある場合はパスで区別する
47
+ disambiguateDuplicateKeys(packages, nodeModulesReal);
48
+ for (const pkg of packages) {
49
+ pkg.anchor = makeAnchorId(pkg.key);
50
+ }
51
+ ensureUniqueAnchors(packages, nodeModulesReal);
52
+ const missingFiles = [];
53
+ const missingSource = [];
54
+ const missingLicenseField = [];
55
+ for (const pkg of packages) {
56
+ if (pkg.missing?.licenseFiles) {
57
+ missingFiles.push(pkg.key);
58
+ opts.warn(`Missing ${LICENSE_FILES_LABEL} in ${pkg.dir} (${pkg.key})`);
49
59
  }
50
- if (!license) {
51
- missingLicenseField.push(key);
52
- flags.push("Missing package.json license");
53
- opts.warn(`Missing license in package.json: ${key}`);
60
+ if (pkg.missing?.source) {
61
+ missingSource.push(pkg.key);
62
+ opts.warn(`Unknown source: ${pkg.key}`);
54
63
  }
55
-
56
- const licFiles = await getLicenseLikeFilesInFolderRoot(pkgDir);
57
- const fileNames = licFiles.map((f) => path.basename(f));
58
-
59
- if (licFiles.length === 0) {
60
- missingFiles.push(key);
61
- const missingMsg = `Missing ${LICENSE_FILES_LABEL} files`;
62
- flags.push(missingMsg);
63
- opts.warn(`Missing ${LICENSE_FILES_LABEL} in ${pkgDir} (${key})`);
64
+ if (pkg.missing?.licenseField) {
65
+ missingLicenseField.push(pkg.key);
66
+ opts.warn(`Missing license in package.json: ${pkg.key}`);
64
67
  }
65
-
66
- const licenseTexts =
67
- licFiles.length > 0
68
- ? await Promise.all(
69
- licFiles.map(async (filePath) => ({
70
- name: path.basename(filePath),
71
- text: await readTextFileSmart(filePath),
72
- }))
73
- )
74
- : [];
75
-
76
- packages.push({
77
- key,
78
- anchor,
79
- source,
80
- license,
81
- fileNames,
82
- flags,
83
- licenseTexts,
84
- });
85
68
  }
86
-
87
69
  return {
88
70
  packages,
89
71
  missingFiles: uniqSorted(missingFiles),
@@ -92,44 +74,72 @@ export async function gatherPackages(opts) {
92
74
  seenCount: seen.size,
93
75
  };
94
76
  }
77
+ function getPackageIdentity(pkg) {
78
+ const name =
79
+ typeof pkg.name === "string" && pkg.name.trim().length > 0
80
+ ? pkg.name.trim()
81
+ : "";
82
+ const version =
83
+ typeof pkg.version === "string" && pkg.version.trim().length > 0
84
+ ? pkg.version.trim()
85
+ : "";
86
+ if (!name || !version) return null;
87
+ return { name, version };
88
+ }
95
89
 
96
- // license フィールドを人間可読にまとめる(文字列/オブジェクト/配列に対応)
97
- function formatLicense(raw) {
98
- const parts = [];
99
-
100
- const pushMaybe = (v) => {
101
- if (typeof v === "string" && v.trim()) parts.push(v.trim());
102
- };
90
+ function disambiguateDuplicateKeys(packages, nodeModulesRoot) {
91
+ const groups = new Map();
92
+ for (const pkg of packages) {
93
+ const list = groups.get(pkg.baseKey) ?? [];
94
+ list.push(pkg);
95
+ groups.set(pkg.baseKey, list);
96
+ }
103
97
 
104
- const handleObj = (licObj) => {
105
- if (!licObj || typeof licObj !== "object") return;
106
- const type =
107
- typeof licObj.type === "string" && licObj.type.trim()
108
- ? licObj.type.trim()
109
- : "";
110
- const url =
111
- typeof licObj.url === "string" && licObj.url.trim()
112
- ? licObj.url.trim()
113
- : "";
114
- if (type && url) {
115
- parts.push(`${type} (${url})`);
116
- } else {
117
- pushMaybe(type);
118
- pushMaybe(url);
98
+ for (const list of groups.values()) {
99
+ if (list.length < 2) continue;
100
+ for (const pkg of list) {
101
+ const label = makePackagePathLabel(pkg.dir, nodeModulesRoot);
102
+ const key = `${pkg.baseKey} (${label})`;
103
+ pkg.key = key;
119
104
  }
120
- };
105
+ }
106
+ }
107
+
108
+ function hashStableSuffix(value) {
109
+ let hash = 5381;
110
+ for (let i = 0; i < value.length; i += 1) {
111
+ hash = (hash * 33) ^ value.charCodeAt(i);
112
+ }
113
+ return (hash >>> 0).toString(36);
114
+ }
121
115
 
122
- if (typeof raw === "string") {
123
- pushMaybe(raw);
124
- } else if (Array.isArray(raw)) {
125
- for (const lic of raw) {
126
- if (typeof lic === "string") pushMaybe(lic);
127
- else handleObj(lic);
116
+ function ensureUniqueAnchors(packages, nodeModulesRoot) {
117
+ const groups = new Map();
118
+ for (const pkg of packages) {
119
+ const list = groups.get(pkg.anchor) ?? [];
120
+ list.push(pkg);
121
+ groups.set(pkg.anchor, list);
122
+ }
123
+
124
+ for (const list of groups.values()) {
125
+ if (list.length < 2) continue;
126
+ for (const pkg of list) {
127
+ const label = makePackagePathLabel(pkg.dir, nodeModulesRoot);
128
+ pkg.anchor = `${pkg.anchor}-${hashStableSuffix(label)}`;
128
129
  }
129
- } else {
130
- handleObj(raw);
131
130
  }
131
+ }
132
132
 
133
- if (parts.length === 0) return null;
134
- return [...new Set(parts)].join(" | ");
133
+ async function toRealPath(targetPath) {
134
+ const cached = realpathCache.get(targetPath);
135
+ if (cached) return cached;
136
+ try {
137
+ const resolved = await fsp.realpath(targetPath);
138
+ realpathCache.set(targetPath, resolved);
139
+ return resolved;
140
+ } catch {
141
+ const resolved = path.resolve(targetPath);
142
+ realpathCache.set(targetPath, resolved);
143
+ return resolved;
144
+ }
135
145
  }
package/src/url.js CHANGED
@@ -1,8 +1,8 @@
1
- // package.jsonのrepositoryからURLを取り出す
2
- export function getRepositoryUrl(pkg) {
3
- const repo = pkg?.repository;
4
- if (!repo) return null;
5
- if (typeof repo === "string") return repo;
6
- if (typeof repo === "object" && typeof repo.url === "string") return repo.url;
7
- return null;
8
- }
1
+ // package.jsonのrepositoryからURLを取り出す
2
+ export function getRepositoryUrl(pkg) {
3
+ const repo = pkg?.repository;
4
+ if (!repo) return null;
5
+ if (typeof repo === "string") return repo;
6
+ if (typeof repo === "object" && typeof repo.url === "string") return repo.url;
7
+ return null;
8
+ }