@myooken/license-output 0.1.0 → 0.2.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/README.md CHANGED
@@ -12,58 +12,66 @@ It generates two files: `THIRD-PARTY-LICENSE.md` (main content) and `THIRD-PARTY
12
12
  - **Review file** flags missing Source / license / license files
13
13
  - `--fail-on-missing` supports CI enforcement
14
14
 
15
- ### Options
16
-
17
- | Option | Description | Default |
18
- | ---------------------- | ------------------------------------------------------ | ------------------------------- |
19
- | `--node-modules <dir>` | Path to `node_modules` | `node_modules` |
20
- | `--review [file]` | Write review file only; optional filename | `THIRD-PARTY-LICENSE-REVIEW.md` |
21
- | `--license [file]` | Write main file only; optional filename | `THIRD-PARTY-LICENSE.md` |
22
- | `--fail-on-missing` | Exit with code 1 if LICENSE/NOTICE/COPYING are missing | `false` |
23
- | `-h`, `--help` | Show help | - |
24
-
25
- > If neither `--review` nor `--license` is specified, **both files are generated**.
15
+ CLI command: `third-party-license`
26
16
 
27
17
  ### Usage
28
18
 
29
19
  #### Run without installing (recommended)
30
20
 
31
21
  ```bash
32
- npx --package=@myooken/license-output -- third-party-notices [args...]
22
+ npx --package=@myooken/license-output -- third-party-license
33
23
  ```
34
24
 
35
25
  #### Run via npm exec
36
26
 
37
27
  ```bash
38
- npm exec --package=@myooken/license-output -- third-party-notices [args...]
28
+ npm exec --package=@myooken/license-output -- third-party-license
39
29
  ```
40
30
 
41
31
  #### Install globally
42
32
 
43
33
  ```bash
44
34
  npm i -g @myooken/license-output
45
- third-party-notices [args...]
35
+ third-party-license
46
36
  ```
47
37
 
38
+ ### Options
39
+
40
+ | Option | Description | Default |
41
+ | ---------------------- | --------------------------------------------------------------------------- | ------------------------------- |
42
+ | `--node-modules <dir>` | Path to `node_modules` | `node_modules` |
43
+ | `--review [file]` | Write review file only; optional filename | `THIRD-PARTY-LICENSE-REVIEW.md` |
44
+ | `--license [file]` | Write main file only; optional filename | `THIRD-PARTY-LICENSE.md` |
45
+ | `--recreate` | Regenerate files from current `node_modules` only (drops removed packages) | `true` (default) |
46
+ | `--update` | Merge with existing outputs, keep removed packages, and mark their presence | `false` |
47
+ | `--fail-on-missing` | Exit with code 1 if LICENSE/NOTICE/COPYING are missing | `false` |
48
+ | `-h`, `--help` | Show help | - |
49
+
50
+ > If neither `--review` nor `--license` is specified, **both files are generated**.
51
+ > Packages in both files are sorted by name@version; `--update` keeps entries for packages no longer in `node_modules` and annotates their usage status.
52
+
48
53
  ### Examples
49
54
 
50
55
  ```bash
51
56
  # Default (both files)
52
- third-party-notices
57
+ third-party-license
58
+
59
+ # Update existing files without dropping removed packages
60
+ third-party-license --update
53
61
 
54
62
  # Custom node_modules path
55
- third-party-notices --node-modules ./path/to/node_modules
63
+ third-party-license --node-modules ./path/to/node_modules
56
64
 
57
65
  # Review-only output (optional filename)
58
- third-party-notices --review
59
- third-party-notices --review ./out/THIRD-PARTY-LICENSE-REVIEW.md
66
+ third-party-license --review
67
+ third-party-license --review ./out/THIRD-PARTY-LICENSE-REVIEW.md
60
68
 
61
69
  # Main-only output (optional filename)
62
- third-party-notices --license
63
- third-party-notices --license ./out/THIRD-PARTY-LICENSE.md
70
+ third-party-license --license
71
+ third-party-license --license ./out/THIRD-PARTY-LICENSE.md
64
72
 
65
- # Exit with code 1 when something is missing
66
- third-party-notices --fail-on-missing
73
+ # Exit with code 1 when something is missing (with --fail-on-missing)
74
+ third-party-license --fail-on-missing
67
75
  ```
68
76
 
69
77
  ### Programmatic API
@@ -76,20 +84,25 @@ const result = await collectThirdPartyLicenses({
76
84
  outFile: "./THIRD-PARTY-LICENSE.md",
77
85
  reviewFile: "./THIRD-PARTY-LICENSE-REVIEW.md",
78
86
  failOnMissing: false,
87
+ // mode: "update", // keep packages missing from node_modules when updating files
79
88
  });
80
89
 
81
90
  console.log(result.mainContent);
82
91
  console.log(result.reviewContent);
83
92
  ```
84
93
 
94
+ 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.
95
+
85
96
  ### Output overview
86
97
 
87
98
  - **THIRD-PARTY-LICENSE.md**
88
99
  - List of packages
89
100
  - Source / License info
90
101
  - Full LICENSE/NOTICE/COPYING texts
102
+ - Usage line shows whether the package is present in the current `node_modules`
91
103
  - **THIRD-PARTY-LICENSE-REVIEW.md**
92
104
  - Review-oriented checklist
105
+ - Usage-aware status (present / not found) for each package
93
106
  - **Missing summary** section
94
107
 
95
108
  ### How it differs from typical npm license tools (general view)
@@ -105,5 +118,8 @@ console.log(result.reviewContent);
105
118
 
106
119
  ### Notes
107
120
 
121
+ - Scans all packages under `node_modules` (including nested dependencies); license files are searched only in each package root directory.
122
+ - Exit code 0: success.
123
+ - Exit code 1: missing license files when `--fail-on-missing` is set, or `node_modules` not found.
108
124
  - Throws an error if `node_modules` does not exist.
109
125
  - Missing `license` or `repository` fields are flagged in the review file.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@myooken/license-output",
3
- "version": "0.1.0",
4
- "description": "Generate THIRD-PARTY-NOTICES markdown by scanning licenses in node_modules.",
3
+ "version": "0.2.0",
4
+ "description": "Generate third-party-license markdown by scanning licenses in node_modules.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {
@@ -25,7 +25,7 @@
25
25
  ".": "./src/core.js"
26
26
  },
27
27
  "bin": {
28
- "third-party-notices": "./src/cli.js"
28
+ "third-party-license": "./src/cli.js"
29
29
  },
30
30
  "files": [
31
31
  "src",
package/src/cli.js CHANGED
@@ -32,6 +32,10 @@ function parseArgs(argv) {
32
32
  args.outFile = file;
33
33
  i += 1;
34
34
  }
35
+ } else if (a === "--recreate") {
36
+ args.mode = "recreate";
37
+ } else if (a === "--update") {
38
+ args.mode = "update";
35
39
  } else if (a === "--fail-on-missing") {
36
40
  args.failOnMissing = true;
37
41
  } else if (a === "-h" || a === "--help") {
@@ -66,7 +70,7 @@ function applyOutputMode(mode, args) {
66
70
 
67
71
  function showHelp() {
68
72
  console.log(`Usage:
69
- third-party-notices [--node-modules <dir>] [--review [file]] [--license [file]] [--fail-on-missing]
73
+ third-party-license [--node-modules <dir>] [--review [file]] [--license [file]] [--recreate|--update] [--fail-on-missing]
70
74
  `);
71
75
  }
72
76
 
@@ -129,7 +133,7 @@ function isCliExecution() {
129
133
  if (resolvedArg === self) return true;
130
134
 
131
135
  const base = path.basename(resolvedArg).toLowerCase();
132
- if (base === "third-party-notices" || base === "third-party-notices.cmd") {
136
+ if (base === "third-party-license" || base === "third-party-license.cmd") {
133
137
  return true;
134
138
  }
135
139
  if (resolvedArg.includes(`${path.sep}.bin${path.sep}`)) return true;
package/src/constants.js CHANGED
@@ -1,11 +1,12 @@
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
- export const LICENSE_LIKE_RE =
11
- /^(LICEN[CS]E|COPYING|NOTICE)(\..*)?$|^(LICEN[CS]E|COPYING|NOTICE)-/i;
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)(\..*)?$|^(LICEN[CS]E|COPYING|NOTICE)-/i;
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
- // 警告の出力先(デフォルトは console
12
+ // Default warning handler (prints to console)
9
13
  const defaultWarn = (msg) => {
10
14
  console.warn(`warning: ${msg}`);
11
15
  };
12
16
 
13
- // 公開API: ライセンス情報を収集し、マークダウン文字列を返す
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 result = await gatherPackages(opts);
22
+ const scanResult = await gatherPackages(opts);
23
+ const presentPackages = withPresentUsage(scanResult.packages);
19
24
 
20
- const mainContent = renderMain(result.packages, opts);
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
- result.packages,
34
+ sortedPackages,
23
35
  opts,
24
- result.missingFiles,
25
- result.missingSource,
26
- result.missingLicenseField
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: result.seenCount,
35
- missingFiles: uniqSorted(result.missingFiles),
36
- missingSource: uniqSorted(result.missingSource),
37
- missingLicenseField: uniqSorted(result.missingLicenseField),
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
  }
@@ -54,13 +66,87 @@ function normalizeOptions(options) {
54
66
  writeMain: options.writeMain ?? true,
55
67
  writeReview: options.writeReview ?? true,
56
68
  warn: options.onWarn ?? defaultWarn,
69
+ mode: normalizeMode(options.mode),
57
70
  };
58
71
  }
59
72
 
73
+ function normalizeMode(mode) {
74
+ const m = typeof mode === "string" ? mode.toLowerCase() : "";
75
+ return m === "update" ? "update" : DEFAULT_OPTIONS.mode;
76
+ }
77
+
60
78
  async function assertNodeModulesExists(dir) {
61
- // node_modules の有無を事前チェックし、無ければ例外を投げてCIなどで失敗させる
62
79
  const stat = await fsp.stat(dir).catch(() => null);
63
80
  if (!stat || !stat.isDirectory()) {
64
81
  throw new Error(`not found node_modules: ${dir}`);
65
82
  }
66
83
  }
84
+
85
+ function withPresentUsage(packages) {
86
+ return packages.map((pkg) => ({
87
+ ...pkg,
88
+ usage: "present",
89
+ notes: "",
90
+ }));
91
+ }
92
+
93
+ async function mergeWithExistingOutputs(currentPackages, opts) {
94
+ const [existingMain, existingReview] = await Promise.all([
95
+ parseExistingMainFile(opts.outFile),
96
+ parseExistingReviewFile(opts.reviewFile),
97
+ ]);
98
+
99
+ // Start with previously known packages as "missing" (not found in this scan)
100
+ const merged = new Map();
101
+ for (const [key, prevReview] of existingReview.entries()) {
102
+ const prevMain = existingMain.get(key);
103
+ merged.set(key, toMissingPackage(key, prevMain, prevReview));
104
+ }
105
+ for (const [key, prevMain] of existingMain.entries()) {
106
+ if (!merged.has(key)) {
107
+ merged.set(key, toMissingPackage(key, prevMain, null));
108
+ }
109
+ }
110
+
111
+ // Override with current scan (present in node_modules), keeping previous notes when available
112
+ for (const pkg of currentPackages) {
113
+ const prevReview = existingReview.get(pkg.key);
114
+ merged.set(pkg.key, toPresentPackage(pkg, prevReview));
115
+ }
116
+
117
+ return [...merged.values()];
118
+ }
119
+
120
+ function toPresentPackage(pkg, prevReview) {
121
+ return {
122
+ ...pkg,
123
+ usage: "present",
124
+ notes: prevReview?.notes ?? "",
125
+ };
126
+ }
127
+
128
+ function toMissingPackage(key, prevMain, prevReview) {
129
+ return {
130
+ key,
131
+ anchor: makeAnchorId(key),
132
+ source: prevMain?.source ?? prevReview?.source ?? null,
133
+ license: prevMain?.license ?? prevReview?.license ?? null,
134
+ fileNames: deriveFileNames(prevMain, prevReview),
135
+ flags: [],
136
+ licenseTexts: prevMain?.licenseTexts ?? [],
137
+ usage: "missing",
138
+ notes: prevReview?.notes ?? "",
139
+ };
140
+ }
141
+
142
+ function deriveFileNames(prevMain, prevReview) {
143
+ const names =
144
+ (prevMain?.fileNames && prevMain.fileNames.length > 0
145
+ ? prevMain.fileNames
146
+ : prevReview?.fileNames) ?? [];
147
+ return uniqSorted(names);
148
+ }
149
+
150
+ function sortPackages(packages) {
151
+ return [...packages].sort((a, b) => a.key.localeCompare(b.key));
152
+ }
@@ -0,0 +1,162 @@
1
+ import fsp from "node:fs/promises";
2
+ import { makeAnchorId, uniqSorted } from "./fs-utils.js";
3
+
4
+ export async function parseExistingMainFile(filePath) {
5
+ const content = await readFileSafe(filePath);
6
+ if (!content) return new Map();
7
+
8
+ const map = new Map();
9
+ for (const { key, body } of splitPackageBlocks(content)) {
10
+ map.set(key, parseMainBlock(key, body));
11
+ }
12
+ return map;
13
+ }
14
+
15
+ export async function parseExistingReviewFile(filePath) {
16
+ const content = await readFileSafe(filePath);
17
+ if (!content) return new Map();
18
+
19
+ const map = new Map();
20
+ for (const { key, body } of splitPackageBlocks(content)) {
21
+ map.set(key, parseReviewBlock(key, body));
22
+ }
23
+ return map;
24
+ }
25
+
26
+ async function readFileSafe(filePath) {
27
+ try {
28
+ return await fsp.readFile(filePath, "utf8");
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ function splitPackageBlocks(content) {
35
+ const blocks = [];
36
+ const headingRe = /^##\s+(.+)$/gm;
37
+ let match;
38
+ let current = null;
39
+
40
+ while ((match = headingRe.exec(content))) {
41
+ if (current) {
42
+ blocks.push({
43
+ key: current.key,
44
+ body: content.slice(current.start, match.index),
45
+ });
46
+ }
47
+ current = { key: match[1].trim(), start: match.index };
48
+ }
49
+
50
+ if (current) {
51
+ blocks.push({
52
+ key: current.key,
53
+ body: content.slice(current.start),
54
+ });
55
+ }
56
+
57
+ return blocks;
58
+ }
59
+
60
+ function parseMainBlock(key, body) {
61
+ const source = pickLine(body, /^- Source:\s*(.+)$/m);
62
+ const license = pickLine(body, /^- License:\s*(.+)$/m);
63
+ const usage = pickLine(body, /^- Usage:\s*(.+)$/m);
64
+
65
+ const licenseTexts = [];
66
+ const licRe = /###\s+(.+?)\s*\r?\n```text\r?\n([\s\S]*?)```/g;
67
+ let licMatch;
68
+ while ((licMatch = licRe.exec(body))) {
69
+ licenseTexts.push({
70
+ name: licMatch[1].trim(),
71
+ text: licMatch[2],
72
+ });
73
+ }
74
+
75
+ const fileNames = [];
76
+ for (const m of body.matchAll(/^- (.+)$/gm)) {
77
+ const val = m[1].trim();
78
+ if (
79
+ val.startsWith("Source:") ||
80
+ val.startsWith("License:") ||
81
+ val.startsWith("Usage:")
82
+ ) {
83
+ continue;
84
+ }
85
+ if (val.startsWith("(no LICENSE/NOTICE/COPYING files)")) continue;
86
+ fileNames.push(val);
87
+ }
88
+
89
+ const derivedNames =
90
+ fileNames.length > 0 ? fileNames : licenseTexts.map((x) => x.name);
91
+
92
+ return {
93
+ key,
94
+ anchor: makeAnchorId(key),
95
+ source: source || null,
96
+ license: license || null,
97
+ fileNames: uniqSorted(derivedNames),
98
+ flags: [],
99
+ licenseTexts,
100
+ usage: usage || "",
101
+ notes: "",
102
+ };
103
+ }
104
+
105
+ function parseReviewBlock(key, body) {
106
+ const source = pickLine(body, /^- Source:\s*(.+)$/m);
107
+ const license = pickLine(body, /^- License:\s*(.+)$/m);
108
+
109
+ const lines = body.split(/\r?\n/);
110
+ const fileNames = [];
111
+
112
+ for (let i = 0; i < lines.length; i += 1) {
113
+ const line = lines[i];
114
+ if (!line.startsWith("- Files:")) continue;
115
+
116
+ let j = i + 1;
117
+ while (j < lines.length && lines[j].startsWith(" -")) {
118
+ fileNames.push(lines[j].replace(/^ -\s*/, ""));
119
+ j += 1;
120
+ }
121
+ i = j - 1;
122
+ }
123
+
124
+ let notes = "";
125
+ const notesIdx = lines.findIndex((l) => l.startsWith("- Notes:"));
126
+ if (notesIdx !== -1) {
127
+ const noteLines = [];
128
+ for (let i = notesIdx + 1; i < lines.length; i += 1) {
129
+ const line = lines[i];
130
+ if (line.startsWith("## ")) break;
131
+ if (line.startsWith("- ")) break;
132
+ if (line.startsWith("---")) break;
133
+ if (line.startsWith(" ")) {
134
+ noteLines.push(line.slice(2));
135
+ continue;
136
+ }
137
+ if (line.trim() === "") {
138
+ noteLines.push("");
139
+ continue;
140
+ }
141
+ break;
142
+ }
143
+ notes = noteLines.join("\n").replace(/\s+$/, "");
144
+ }
145
+
146
+ return {
147
+ key,
148
+ anchor: makeAnchorId(key),
149
+ source: source || null,
150
+ license: license || null,
151
+ fileNames: uniqSorted(fileNames),
152
+ flags: [],
153
+ licenseTexts: [],
154
+ usage: "",
155
+ notes,
156
+ };
157
+ }
158
+
159
+ function pickLine(body, re) {
160
+ const m = body.match(re);
161
+ return m ? m[1].trim() : "";
162
+ }
package/src/render.js CHANGED
@@ -17,8 +17,10 @@ export function renderMain(packages, opts) {
17
17
  push(`## ${pkg.key}`);
18
18
  push(`- Source: ${pkg.source ?? "(missing)"}`);
19
19
  push(`- License: ${pkg.license ?? "(missing)"}`);
20
+ push(`- Usage: ${describeUsage(pkg.usage)}`);
20
21
 
21
- if (pkg.fileNames.length === 0) {
22
+ const fileNames = pkg.fileNames ?? [];
23
+ if (fileNames.length === 0) {
22
24
  push("- (no LICENSE/NOTICE/COPYING files)");
23
25
  push("");
24
26
  push("_No LICENSE/NOTICE/COPYING file found in package directory._");
@@ -26,10 +28,10 @@ export function renderMain(packages, opts) {
26
28
  continue;
27
29
  }
28
30
 
29
- for (const n of pkg.fileNames) push(`- ${n}`);
31
+ for (const n of fileNames) push(`- ${n}`);
30
32
  push("");
31
33
 
32
- for (const lic of pkg.licenseTexts) {
34
+ for (const lic of pkg.licenseTexts ?? []) {
33
35
  push(`### ${lic.name}`);
34
36
  push("```text");
35
37
  push(mdSafeText(lic.text).replace(/\s+$/, ""));
@@ -66,19 +68,26 @@ export function renderReview(
66
68
  push(`- License: ${it.license ?? "(missing)"}`);
67
69
 
68
70
  push("- Files:");
69
- if (it.fileNames.length === 0) {
71
+ const fileNames = it.fileNames ?? [];
72
+ if (fileNames.length === 0) {
70
73
  push(" - (none)");
71
74
  } else {
72
- for (const f of it.fileNames) push(` - ${f}`);
75
+ for (const f of fileNames) push(` - ${f}`);
73
76
  }
74
77
 
75
- if (it.flags.length === 0) {
76
- push("- Status: Check manually");
77
- } else {
78
- push(`- Status: ${it.flags.join(" / ")}`);
79
- }
78
+ const statusParts = [describeUsage(it.usage), ...(it.flags ?? [])].filter(
79
+ Boolean
80
+ );
81
+ const status =
82
+ statusParts.length === 0 ? "Check manually" : statusParts.join(" / ");
83
+ push(`- Status: ${status}`);
80
84
 
81
85
  push("- Notes:");
86
+ if (it.notes && it.notes.length > 0) {
87
+ for (const line of it.notes.split(/\r?\n/)) {
88
+ push(` ${line}`);
89
+ }
90
+ }
82
91
  push("");
83
92
  }
84
93
 
@@ -102,3 +111,10 @@ export function renderReview(
102
111
 
103
112
  return lines.join(os.EOL) + os.EOL;
104
113
  }
114
+
115
+ function describeUsage(usage) {
116
+ if (usage === "missing") {
117
+ return "Not found in node_modules (kept from previous output)";
118
+ }
119
+ return "Present in node_modules";
120
+ }