@myooken/license-output 0.1.1 → 0.2.1
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/README.md +28 -10
- package/package.json +1 -1
- package/src/cli.js +15 -4
- package/src/constants.js +15 -11
- package/src/core.js +121 -20
- package/src/existing.js +168 -0
- package/src/render.js +42 -18
- package/src/scan.js +4 -2
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Third-Party License Output for node_modules
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@myooken/license-output)
|
|
4
|
+
[](https://www.npmjs.com/package/@myooken/license-output)
|
|
5
|
+
[](https://www.npmjs.com/package/@myooken/license-output)
|
|
6
|
+
|
|
7
|
+
https://www.npmjs.com/package/@myooken/license-output
|
|
8
|
+
|
|
3
9
|
### What is this?
|
|
4
10
|
|
|
5
11
|
A tool to scan `node_modules` and **output third-party licenses in Markdown**.
|
|
@@ -8,7 +14,7 @@ It generates two files: `THIRD-PARTY-LICENSE.md` (main content) and `THIRD-PARTY
|
|
|
8
14
|
### Highlights
|
|
9
15
|
|
|
10
16
|
- **ESM / Node.js 18+**, zero dependencies
|
|
11
|
-
- **Outputs full license texts** from LICENSE/NOTICE/COPYING files
|
|
17
|
+
- **Outputs full license texts** from LICENSE/NOTICE/THIRD-PARTY-NOTICES/COPYING files
|
|
12
18
|
- **Review file** flags missing Source / license / license files
|
|
13
19
|
- `--fail-on-missing` supports CI enforcement
|
|
14
20
|
|
|
@@ -37,15 +43,18 @@ third-party-license
|
|
|
37
43
|
|
|
38
44
|
### Options
|
|
39
45
|
|
|
40
|
-
| Option | Description
|
|
41
|
-
| ---------------------- |
|
|
42
|
-
| `--node-modules <dir>` | Path to `node_modules`
|
|
43
|
-
| `--review [file]` | Write review file only; optional filename
|
|
44
|
-
| `--license [file]` | Write main file only; optional filename
|
|
45
|
-
| `--
|
|
46
|
-
|
|
|
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/THIRD-PARTY-NOTICES/COPYING are missing | `false` |
|
|
54
|
+
| `-h`, `--help` | Show help | - |
|
|
47
55
|
|
|
48
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.
|
|
49
58
|
|
|
50
59
|
### Examples
|
|
51
60
|
|
|
@@ -53,6 +62,9 @@ third-party-license
|
|
|
53
62
|
# Default (both files)
|
|
54
63
|
third-party-license
|
|
55
64
|
|
|
65
|
+
# Update existing files without dropping removed packages
|
|
66
|
+
third-party-license --update
|
|
67
|
+
|
|
56
68
|
# Custom node_modules path
|
|
57
69
|
third-party-license --node-modules ./path/to/node_modules
|
|
58
70
|
|
|
@@ -71,27 +83,32 @@ third-party-license --fail-on-missing
|
|
|
71
83
|
### Programmatic API
|
|
72
84
|
|
|
73
85
|
```js
|
|
74
|
-
import { collectThirdPartyLicenses } from "@myooken/
|
|
86
|
+
import { collectThirdPartyLicenses } from "@myooken/license-output";
|
|
75
87
|
|
|
76
88
|
const result = await collectThirdPartyLicenses({
|
|
77
89
|
nodeModules: "./node_modules",
|
|
78
90
|
outFile: "./THIRD-PARTY-LICENSE.md",
|
|
79
91
|
reviewFile: "./THIRD-PARTY-LICENSE-REVIEW.md",
|
|
80
92
|
failOnMissing: false,
|
|
93
|
+
// mode: "update", // keep packages missing from node_modules when updating files
|
|
81
94
|
});
|
|
82
95
|
|
|
83
96
|
console.log(result.mainContent);
|
|
84
97
|
console.log(result.reviewContent);
|
|
85
98
|
```
|
|
86
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
|
+
|
|
87
102
|
### Output overview
|
|
88
103
|
|
|
89
104
|
- **THIRD-PARTY-LICENSE.md**
|
|
90
105
|
- List of packages
|
|
91
106
|
- Source / License info
|
|
92
|
-
- Full LICENSE/NOTICE/COPYING texts
|
|
107
|
+
- Full LICENSE/NOTICE/THIRD-PARTY-NOTICES/COPYING texts
|
|
108
|
+
- Usage line shows whether the package is present in the current `node_modules`
|
|
93
109
|
- **THIRD-PARTY-LICENSE-REVIEW.md**
|
|
94
110
|
- Review-oriented checklist
|
|
111
|
+
- Usage-aware status (present / not found) for each package
|
|
95
112
|
- **Missing summary** section
|
|
96
113
|
|
|
97
114
|
### How it differs from typical npm license tools (general view)
|
|
@@ -112,3 +129,4 @@ console.log(result.reviewContent);
|
|
|
112
129
|
- Exit code 1: missing license files when `--fail-on-missing` is set, or `node_modules` not found.
|
|
113
130
|
- Throws an error if `node_modules` does not exist.
|
|
114
131
|
- Missing `license` or `repository` fields are flagged in the review file.
|
|
132
|
+
- Paths printed in outputs/logs are shown relative to the current working directory.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import fsp from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { collectThirdPartyLicenses, DEFAULT_OPTIONS } from "./core.js";
|
|
7
|
+
import { LICENSE_FILES_LABEL } from "./constants.js";
|
|
7
8
|
|
|
8
9
|
// 引数パース: --review / --license は最後に指定されたものを優先し、直後の値があれば出力ファイル名として扱う
|
|
9
10
|
function parseArgs(argv) {
|
|
@@ -32,6 +33,10 @@ function parseArgs(argv) {
|
|
|
32
33
|
args.outFile = file;
|
|
33
34
|
i += 1;
|
|
34
35
|
}
|
|
36
|
+
} else if (a === "--recreate") {
|
|
37
|
+
args.mode = "recreate";
|
|
38
|
+
} else if (a === "--update") {
|
|
39
|
+
args.mode = "update";
|
|
35
40
|
} else if (a === "--fail-on-missing") {
|
|
36
41
|
args.failOnMissing = true;
|
|
37
42
|
} else if (a === "-h" || a === "--help") {
|
|
@@ -66,7 +71,7 @@ function applyOutputMode(mode, args) {
|
|
|
66
71
|
|
|
67
72
|
function showHelp() {
|
|
68
73
|
console.log(`Usage:
|
|
69
|
-
third-party-license [--node-modules <dir>] [--review [file]] [--license [file]] [--fail-on-missing]
|
|
74
|
+
third-party-license [--node-modules <dir>] [--review [file]] [--license [file]] [--recreate|--update] [--fail-on-missing]
|
|
70
75
|
`);
|
|
71
76
|
}
|
|
72
77
|
|
|
@@ -103,12 +108,18 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
103
108
|
await Promise.all(writeTasks);
|
|
104
109
|
|
|
105
110
|
if (result.options.writeMain)
|
|
106
|
-
console.log(
|
|
111
|
+
console.log(
|
|
112
|
+
`Generated: ${result.options.outFileDisplay ?? result.options.outFile}`
|
|
113
|
+
);
|
|
107
114
|
if (result.options.writeReview)
|
|
108
|
-
console.log(
|
|
115
|
+
console.log(
|
|
116
|
+
`Review: ${
|
|
117
|
+
result.options.reviewFileDisplay ?? result.options.reviewFile
|
|
118
|
+
}`
|
|
119
|
+
);
|
|
109
120
|
console.log(`Packages: ${result.stats.packages}`);
|
|
110
121
|
console.log(
|
|
111
|
-
`Missing
|
|
122
|
+
`Missing ${LICENSE_FILES_LABEL}: ${result.stats.missingFiles.length}`
|
|
112
123
|
);
|
|
113
124
|
|
|
114
125
|
if (result.stats.missingFiles.length > 0 && result.options.failOnMissing) {
|
package/src/constants.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
// デフォルト値と定数群
|
|
2
|
-
export const DEFAULT_OPTIONS = {
|
|
3
|
-
nodeModules: "node_modules",
|
|
4
|
-
outFile: "THIRD-PARTY-LICENSE.md",
|
|
5
|
-
reviewFile: "THIRD-PARTY-LICENSE-REVIEW.md",
|
|
6
|
-
failOnMissing: false,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
// デフォルト値と定数群
|
|
2
|
+
export const DEFAULT_OPTIONS = {
|
|
3
|
+
nodeModules: "node_modules",
|
|
4
|
+
outFile: "THIRD-PARTY-LICENSE.md",
|
|
5
|
+
reviewFile: "THIRD-PARTY-LICENSE-REVIEW.md",
|
|
6
|
+
failOnMissing: false,
|
|
7
|
+
mode: "recreate", // "recreate" | "update"
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// ライセンスらしいファイル名を検出する正規表現
|
|
11
|
+
export const LICENSE_LIKE_RE =
|
|
12
|
+
/^(LICEN[CS]E|COPYING|NOTICE|THIRD[-_. ]?PARTY[-_. ]?NOTICES?)(\..*)?$|^(LICEN[CS]E|COPYING|NOTICE|THIRD[-_. ]?PARTY[-_. ]?NOTICES?)-/i;
|
|
13
|
+
|
|
14
|
+
export const LICENSE_FILES_LABEL =
|
|
15
|
+
"LICENSE/NOTICE/THIRD-PARTY-NOTICES/COPYING";
|
package/src/core.js
CHANGED
|
@@ -3,27 +3,39 @@ import path from "node:path";
|
|
|
3
3
|
import { DEFAULT_OPTIONS } from "./constants.js";
|
|
4
4
|
import { gatherPackages } from "./scan.js";
|
|
5
5
|
import { renderMain, renderReview } from "./render.js";
|
|
6
|
-
import { uniqSorted } from "./fs-utils.js";
|
|
6
|
+
import { makeAnchorId, uniqSorted } from "./fs-utils.js";
|
|
7
|
+
import {
|
|
8
|
+
parseExistingMainFile,
|
|
9
|
+
parseExistingReviewFile,
|
|
10
|
+
} from "./existing.js";
|
|
7
11
|
|
|
8
|
-
//
|
|
12
|
+
// Default warning handler (prints to console)
|
|
9
13
|
const defaultWarn = (msg) => {
|
|
10
14
|
console.warn(`warning: ${msg}`);
|
|
11
15
|
};
|
|
12
16
|
|
|
13
|
-
//
|
|
17
|
+
// Public API: scan licenses and render markdown
|
|
14
18
|
export async function collectThirdPartyLicenses(options = {}) {
|
|
15
19
|
const opts = normalizeOptions(options);
|
|
16
|
-
await assertNodeModulesExists(opts.nodeModules); // node_modules
|
|
20
|
+
await assertNodeModulesExists(opts.nodeModules); // fail fast when node_modules is missing
|
|
17
21
|
|
|
18
|
-
const
|
|
22
|
+
const scanResult = await gatherPackages(opts);
|
|
23
|
+
const presentPackages = withPresentUsage(scanResult.packages);
|
|
19
24
|
|
|
20
|
-
const
|
|
25
|
+
const packages =
|
|
26
|
+
opts.mode === "update"
|
|
27
|
+
? await mergeWithExistingOutputs(presentPackages, opts)
|
|
28
|
+
: presentPackages;
|
|
29
|
+
|
|
30
|
+
const sortedPackages = sortPackages(packages);
|
|
31
|
+
|
|
32
|
+
const mainContent = renderMain(sortedPackages, opts);
|
|
21
33
|
const reviewContent = renderReview(
|
|
22
|
-
|
|
34
|
+
sortedPackages,
|
|
23
35
|
opts,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
scanResult.missingFiles,
|
|
37
|
+
scanResult.missingSource,
|
|
38
|
+
scanResult.missingLicenseField
|
|
27
39
|
);
|
|
28
40
|
|
|
29
41
|
return {
|
|
@@ -31,10 +43,10 @@ export async function collectThirdPartyLicenses(options = {}) {
|
|
|
31
43
|
reviewContent,
|
|
32
44
|
options: opts,
|
|
33
45
|
stats: {
|
|
34
|
-
packages:
|
|
35
|
-
missingFiles: uniqSorted(
|
|
36
|
-
missingSource: uniqSorted(
|
|
37
|
-
missingLicenseField: uniqSorted(
|
|
46
|
+
packages: scanResult.seenCount,
|
|
47
|
+
missingFiles: uniqSorted(scanResult.missingFiles),
|
|
48
|
+
missingSource: uniqSorted(scanResult.missingSource),
|
|
49
|
+
missingLicenseField: uniqSorted(scanResult.missingLicenseField),
|
|
38
50
|
},
|
|
39
51
|
};
|
|
40
52
|
}
|
|
@@ -42,25 +54,114 @@ export async function collectThirdPartyLicenses(options = {}) {
|
|
|
42
54
|
export { DEFAULT_OPTIONS } from "./constants.js";
|
|
43
55
|
|
|
44
56
|
function normalizeOptions(options) {
|
|
57
|
+
const cwd = process.cwd();
|
|
58
|
+
const nodeModules = path.resolve(
|
|
59
|
+
options.nodeModules ?? DEFAULT_OPTIONS.nodeModules
|
|
60
|
+
);
|
|
61
|
+
const outFile = path.resolve(options.outFile ?? DEFAULT_OPTIONS.outFile);
|
|
62
|
+
const reviewFile = path.resolve(
|
|
63
|
+
options.reviewFile ?? DEFAULT_OPTIONS.reviewFile
|
|
64
|
+
);
|
|
65
|
+
|
|
45
66
|
return {
|
|
46
|
-
nodeModules
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
67
|
+
nodeModules,
|
|
68
|
+
outFile,
|
|
69
|
+
reviewFile,
|
|
70
|
+
nodeModulesDisplay: makeDisplayPath(nodeModules, cwd),
|
|
71
|
+
outFileDisplay: makeDisplayPath(outFile, cwd),
|
|
72
|
+
reviewFileDisplay: makeDisplayPath(reviewFile, cwd),
|
|
51
73
|
failOnMissing: Boolean(
|
|
52
74
|
options.failOnMissing ?? DEFAULT_OPTIONS.failOnMissing
|
|
53
75
|
),
|
|
54
76
|
writeMain: options.writeMain ?? true,
|
|
55
77
|
writeReview: options.writeReview ?? true,
|
|
56
78
|
warn: options.onWarn ?? defaultWarn,
|
|
79
|
+
mode: normalizeMode(options.mode),
|
|
57
80
|
};
|
|
58
81
|
}
|
|
59
82
|
|
|
83
|
+
function normalizeMode(mode) {
|
|
84
|
+
const m = typeof mode === "string" ? mode.toLowerCase() : "";
|
|
85
|
+
return m === "update" ? "update" : DEFAULT_OPTIONS.mode;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function makeDisplayPath(targetPath, cwd) {
|
|
89
|
+
const rel = path.relative(cwd, targetPath);
|
|
90
|
+
return rel || ".";
|
|
91
|
+
}
|
|
92
|
+
|
|
60
93
|
async function assertNodeModulesExists(dir) {
|
|
61
|
-
// node_modules の有無を事前チェックし、無ければ例外を投げてCIなどで失敗させる
|
|
62
94
|
const stat = await fsp.stat(dir).catch(() => null);
|
|
63
95
|
if (!stat || !stat.isDirectory()) {
|
|
64
96
|
throw new Error(`not found node_modules: ${dir}`);
|
|
65
97
|
}
|
|
66
98
|
}
|
|
99
|
+
|
|
100
|
+
function withPresentUsage(packages) {
|
|
101
|
+
return packages.map((pkg) => ({
|
|
102
|
+
...pkg,
|
|
103
|
+
usage: "present",
|
|
104
|
+
notes: "",
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function mergeWithExistingOutputs(currentPackages, opts) {
|
|
109
|
+
const [existingMain, existingReview] = await Promise.all([
|
|
110
|
+
parseExistingMainFile(opts.outFile),
|
|
111
|
+
parseExistingReviewFile(opts.reviewFile),
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
// Start with previously known packages as "missing" (not found in this scan)
|
|
115
|
+
const merged = new Map();
|
|
116
|
+
for (const [key, prevReview] of existingReview.entries()) {
|
|
117
|
+
const prevMain = existingMain.get(key);
|
|
118
|
+
merged.set(key, toMissingPackage(key, prevMain, prevReview));
|
|
119
|
+
}
|
|
120
|
+
for (const [key, prevMain] of existingMain.entries()) {
|
|
121
|
+
if (!merged.has(key)) {
|
|
122
|
+
merged.set(key, toMissingPackage(key, prevMain, null));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Override with current scan (present in node_modules), keeping previous notes when available
|
|
127
|
+
for (const pkg of currentPackages) {
|
|
128
|
+
const prevReview = existingReview.get(pkg.key);
|
|
129
|
+
merged.set(pkg.key, toPresentPackage(pkg, prevReview));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return [...merged.values()];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function toPresentPackage(pkg, prevReview) {
|
|
136
|
+
return {
|
|
137
|
+
...pkg,
|
|
138
|
+
usage: "present",
|
|
139
|
+
notes: prevReview?.notes ?? "",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function toMissingPackage(key, prevMain, prevReview) {
|
|
144
|
+
return {
|
|
145
|
+
key,
|
|
146
|
+
anchor: makeAnchorId(key),
|
|
147
|
+
source: prevMain?.source ?? prevReview?.source ?? null,
|
|
148
|
+
license: prevMain?.license ?? prevReview?.license ?? null,
|
|
149
|
+
fileNames: deriveFileNames(prevMain, prevReview),
|
|
150
|
+
flags: [],
|
|
151
|
+
licenseTexts: prevMain?.licenseTexts ?? [],
|
|
152
|
+
usage: "missing",
|
|
153
|
+
notes: prevReview?.notes ?? "",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function deriveFileNames(prevMain, prevReview) {
|
|
158
|
+
const names =
|
|
159
|
+
(prevMain?.fileNames && prevMain.fileNames.length > 0
|
|
160
|
+
? prevMain.fileNames
|
|
161
|
+
: prevReview?.fileNames) ?? [];
|
|
162
|
+
return uniqSorted(names);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function sortPackages(packages) {
|
|
166
|
+
return [...packages].sort((a, b) => a.key.localeCompare(b.key));
|
|
167
|
+
}
|
package/src/existing.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
import { makeAnchorId, uniqSorted } from "./fs-utils.js";
|
|
3
|
+
import { LICENSE_FILES_LABEL } from "./constants.js";
|
|
4
|
+
|
|
5
|
+
export async function parseExistingMainFile(filePath) {
|
|
6
|
+
const content = await readFileSafe(filePath);
|
|
7
|
+
if (!content) return new Map();
|
|
8
|
+
|
|
9
|
+
const map = new Map();
|
|
10
|
+
for (const { key, body } of splitPackageBlocks(content)) {
|
|
11
|
+
map.set(key, parseMainBlock(key, body));
|
|
12
|
+
}
|
|
13
|
+
return map;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function parseExistingReviewFile(filePath) {
|
|
17
|
+
const content = await readFileSafe(filePath);
|
|
18
|
+
if (!content) return new Map();
|
|
19
|
+
|
|
20
|
+
const map = new Map();
|
|
21
|
+
for (const { key, body } of splitPackageBlocks(content)) {
|
|
22
|
+
map.set(key, parseReviewBlock(key, body));
|
|
23
|
+
}
|
|
24
|
+
return map;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readFileSafe(filePath) {
|
|
28
|
+
try {
|
|
29
|
+
return await fsp.readFile(filePath, "utf8");
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function splitPackageBlocks(content) {
|
|
36
|
+
const blocks = [];
|
|
37
|
+
const headingRe = /^##\s+(.+)$/gm;
|
|
38
|
+
let match;
|
|
39
|
+
let current = null;
|
|
40
|
+
|
|
41
|
+
while ((match = headingRe.exec(content))) {
|
|
42
|
+
if (current) {
|
|
43
|
+
blocks.push({
|
|
44
|
+
key: current.key,
|
|
45
|
+
body: content.slice(current.start, match.index),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
current = { key: match[1].trim(), start: match.index };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (current) {
|
|
52
|
+
blocks.push({
|
|
53
|
+
key: current.key,
|
|
54
|
+
body: content.slice(current.start),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return blocks;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseMainBlock(key, body) {
|
|
62
|
+
const source = pickLine(body, /^- Source:\s*(.+)$/m);
|
|
63
|
+
const license = pickLine(body, /^- License:\s*(.+)$/m);
|
|
64
|
+
const usage = pickLine(body, /^- Usage:\s*(.+)$/m);
|
|
65
|
+
|
|
66
|
+
const licenseTexts = [];
|
|
67
|
+
const licRe = /###\s+(.+?)\s*\r?\n```text\r?\n([\s\S]*?)```/g;
|
|
68
|
+
let licMatch;
|
|
69
|
+
while ((licMatch = licRe.exec(body))) {
|
|
70
|
+
licenseTexts.push({
|
|
71
|
+
name: licMatch[1].trim(),
|
|
72
|
+
text: licMatch[2],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fileNames = [];
|
|
77
|
+
for (const m of body.matchAll(/^- (.+)$/gm)) {
|
|
78
|
+
const val = m[1].trim();
|
|
79
|
+
if (
|
|
80
|
+
val.startsWith("Source:") ||
|
|
81
|
+
val.startsWith("License:") ||
|
|
82
|
+
val.startsWith("Usage:")
|
|
83
|
+
) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (
|
|
87
|
+
val.startsWith("(no LICENSE/NOTICE/COPYING files)") ||
|
|
88
|
+
val.startsWith(`(no ${LICENSE_FILES_LABEL} files)`)
|
|
89
|
+
) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
fileNames.push(val);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const derivedNames =
|
|
96
|
+
fileNames.length > 0 ? fileNames : licenseTexts.map((x) => x.name);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
key,
|
|
100
|
+
anchor: makeAnchorId(key),
|
|
101
|
+
source: source || null,
|
|
102
|
+
license: license || null,
|
|
103
|
+
fileNames: uniqSorted(derivedNames),
|
|
104
|
+
flags: [],
|
|
105
|
+
licenseTexts,
|
|
106
|
+
usage: usage || "",
|
|
107
|
+
notes: "",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseReviewBlock(key, body) {
|
|
112
|
+
const source = pickLine(body, /^- Source:\s*(.+)$/m);
|
|
113
|
+
const license = pickLine(body, /^- License:\s*(.+)$/m);
|
|
114
|
+
|
|
115
|
+
const lines = body.split(/\r?\n/);
|
|
116
|
+
const fileNames = [];
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
119
|
+
const line = lines[i];
|
|
120
|
+
if (!line.startsWith("- Files:")) continue;
|
|
121
|
+
|
|
122
|
+
let j = i + 1;
|
|
123
|
+
while (j < lines.length && lines[j].startsWith(" -")) {
|
|
124
|
+
fileNames.push(lines[j].replace(/^ -\s*/, ""));
|
|
125
|
+
j += 1;
|
|
126
|
+
}
|
|
127
|
+
i = j - 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let notes = "";
|
|
131
|
+
const notesIdx = lines.findIndex((l) => l.startsWith("- Notes:"));
|
|
132
|
+
if (notesIdx !== -1) {
|
|
133
|
+
const noteLines = [];
|
|
134
|
+
for (let i = notesIdx + 1; i < lines.length; i += 1) {
|
|
135
|
+
const line = lines[i];
|
|
136
|
+
if (line.startsWith("## ")) break;
|
|
137
|
+
if (line.startsWith("- ")) break;
|
|
138
|
+
if (line.startsWith("---")) break;
|
|
139
|
+
if (line.startsWith(" ")) {
|
|
140
|
+
noteLines.push(line.slice(2));
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (line.trim() === "") {
|
|
144
|
+
noteLines.push("");
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
notes = noteLines.join("\n").replace(/\s+$/, "");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
key,
|
|
154
|
+
anchor: makeAnchorId(key),
|
|
155
|
+
source: source || null,
|
|
156
|
+
license: license || null,
|
|
157
|
+
fileNames: uniqSorted(fileNames),
|
|
158
|
+
flags: [],
|
|
159
|
+
licenseTexts: [],
|
|
160
|
+
usage: "",
|
|
161
|
+
notes,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function pickLine(body, re) {
|
|
166
|
+
const m = body.match(re);
|
|
167
|
+
return m ? m[1].trim() : "";
|
|
168
|
+
}
|
package/src/render.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { mdSafeText, uniqSorted } from "./fs-utils.js";
|
|
4
|
+
import { LICENSE_FILES_LABEL } from "./constants.js";
|
|
4
5
|
|
|
5
6
|
// メインのTHIRD-PARTY-LICENSE.mdを描画する
|
|
6
7
|
export function renderMain(packages, opts) {
|
|
@@ -9,7 +10,7 @@ export function renderMain(packages, opts) {
|
|
|
9
10
|
|
|
10
11
|
push("# Third-Party Licenses");
|
|
11
12
|
push("");
|
|
12
|
-
push(`Generated from: ${opts.nodeModules}`);
|
|
13
|
+
push(`Generated from: ${opts.nodeModulesDisplay ?? opts.nodeModules}`);
|
|
13
14
|
push("");
|
|
14
15
|
|
|
15
16
|
for (const pkg of packages) {
|
|
@@ -17,19 +18,21 @@ export function renderMain(packages, opts) {
|
|
|
17
18
|
push(`## ${pkg.key}`);
|
|
18
19
|
push(`- Source: ${pkg.source ?? "(missing)"}`);
|
|
19
20
|
push(`- License: ${pkg.license ?? "(missing)"}`);
|
|
21
|
+
push(`- Usage: ${describeUsage(pkg.usage)}`);
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
const fileNames = pkg.fileNames ?? [];
|
|
24
|
+
if (fileNames.length === 0) {
|
|
25
|
+
push(`- (no ${LICENSE_FILES_LABEL} files)`);
|
|
23
26
|
push("");
|
|
24
|
-
push(
|
|
27
|
+
push(`_No ${LICENSE_FILES_LABEL} file found in package directory._`);
|
|
25
28
|
push("");
|
|
26
29
|
continue;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
for (const n of
|
|
32
|
+
for (const n of fileNames) push(`- ${n}`);
|
|
30
33
|
push("");
|
|
31
34
|
|
|
32
|
-
for (const lic of pkg.licenseTexts) {
|
|
35
|
+
for (const lic of pkg.licenseTexts ?? []) {
|
|
33
36
|
push(`### ${lic.name}`);
|
|
34
37
|
push("```text");
|
|
35
38
|
push(mdSafeText(lic.text).replace(/\s+$/, ""));
|
|
@@ -51,34 +54,41 @@ export function renderReview(
|
|
|
51
54
|
) {
|
|
52
55
|
const lines = [];
|
|
53
56
|
const push = (s = "") => lines.push(s);
|
|
54
|
-
const
|
|
57
|
+
const mainPath = makeMainLinkPath(opts);
|
|
55
58
|
|
|
56
59
|
push("# THIRD-PARTY-LICENSE-REVIEW");
|
|
57
60
|
push("");
|
|
58
|
-
push(`Generated from: ${opts.nodeModules}`);
|
|
59
|
-
push(`Main file: ${
|
|
61
|
+
push(`Generated from: ${opts.nodeModulesDisplay ?? opts.nodeModules}`);
|
|
62
|
+
push(`Main file: ${mainPath}`);
|
|
60
63
|
push("");
|
|
61
64
|
|
|
62
65
|
for (const it of [...packages].sort((a, b) => a.key.localeCompare(b.key))) {
|
|
63
66
|
push(`## ${it.key}`);
|
|
64
|
-
push(`- Main: ${
|
|
67
|
+
push(`- Main: ${mainPath}#${it.anchor}`);
|
|
65
68
|
push(`- Source: ${it.source ?? "(missing)"}`);
|
|
66
69
|
push(`- License: ${it.license ?? "(missing)"}`);
|
|
67
70
|
|
|
68
71
|
push("- Files:");
|
|
69
|
-
|
|
72
|
+
const fileNames = it.fileNames ?? [];
|
|
73
|
+
if (fileNames.length === 0) {
|
|
70
74
|
push(" - (none)");
|
|
71
75
|
} else {
|
|
72
|
-
for (const f of
|
|
76
|
+
for (const f of fileNames) push(` - ${f}`);
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
const statusParts = [describeUsage(it.usage), ...(it.flags ?? [])].filter(
|
|
80
|
+
Boolean
|
|
81
|
+
);
|
|
82
|
+
const status =
|
|
83
|
+
statusParts.length === 0 ? "Check manually" : statusParts.join(" / ");
|
|
84
|
+
push(`- Status: ${status}`);
|
|
80
85
|
|
|
81
86
|
push("- Notes:");
|
|
87
|
+
if (it.notes && it.notes.length > 0) {
|
|
88
|
+
for (const line of it.notes.split(/\r?\n/)) {
|
|
89
|
+
push(` ${line}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
82
92
|
push("");
|
|
83
93
|
}
|
|
84
94
|
|
|
@@ -98,7 +108,21 @@ export function renderReview(
|
|
|
98
108
|
|
|
99
109
|
writeList("Missing Source", missingSource);
|
|
100
110
|
writeList("Missing package.json license field", missingLicenseField);
|
|
101
|
-
writeList(
|
|
111
|
+
writeList(`Missing ${LICENSE_FILES_LABEL} files`, missingFiles);
|
|
102
112
|
|
|
103
113
|
return lines.join(os.EOL) + os.EOL;
|
|
104
114
|
}
|
|
115
|
+
|
|
116
|
+
function makeMainLinkPath(opts) {
|
|
117
|
+
const baseDir = path.dirname(opts.reviewFile);
|
|
118
|
+
const rel = path.relative(baseDir, opts.outFile);
|
|
119
|
+
const normalized = rel.replace(/\\/g, "/");
|
|
120
|
+
return normalized || path.basename(opts.outFile);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function describeUsage(usage) {
|
|
124
|
+
if (usage === "missing") {
|
|
125
|
+
return "Not found in node_modules (kept from previous output)";
|
|
126
|
+
}
|
|
127
|
+
return "Present in node_modules";
|
|
128
|
+
}
|
package/src/scan.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
walkForPackageJson,
|
|
9
9
|
} from "./fs-utils.js";
|
|
10
10
|
import { getRepositoryUrl } from "./url.js";
|
|
11
|
+
import { LICENSE_FILES_LABEL } from "./constants.js";
|
|
11
12
|
|
|
12
13
|
// node_modules を走査してパッケージ情報を集約する
|
|
13
14
|
export async function gatherPackages(opts) {
|
|
@@ -57,8 +58,9 @@ export async function gatherPackages(opts) {
|
|
|
57
58
|
|
|
58
59
|
if (licFiles.length === 0) {
|
|
59
60
|
missingFiles.push(key);
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
const missingMsg = `Missing ${LICENSE_FILES_LABEL} files`;
|
|
62
|
+
flags.push(missingMsg);
|
|
63
|
+
opts.warn(`Missing ${LICENSE_FILES_LABEL} in ${pkgDir} (${key})`);
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
const licenseTexts =
|