@myooken/license-output 0.1.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 ADDED
@@ -0,0 +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.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # Third-Party License Output for node_modules
2
+
3
+ ### What is this?
4
+
5
+ A tool to scan `node_modules` and **output third-party licenses in Markdown**.
6
+ It generates two files: `THIRD-PARTY-LICENSE.md` (main content) and `THIRD-PARTY-LICENSE-REVIEW.md` (review checklist).
7
+
8
+ ### Highlights
9
+
10
+ - **ESM / Node.js 18+**, zero dependencies
11
+ - **Outputs full license texts** from LICENSE/NOTICE/COPYING files
12
+ - **Review file** flags missing Source / license / license files
13
+ - `--fail-on-missing` supports CI enforcement
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**.
26
+
27
+ ### Usage
28
+
29
+ #### Run without installing (recommended)
30
+
31
+ ```bash
32
+ npx --package=@myooken/license-output -- third-party-notices [args...]
33
+ ```
34
+
35
+ #### Run via npm exec
36
+
37
+ ```bash
38
+ npm exec --package=@myooken/license-output -- third-party-notices [args...]
39
+ ```
40
+
41
+ #### Install globally
42
+
43
+ ```bash
44
+ npm i -g @myooken/license-output
45
+ third-party-notices [args...]
46
+ ```
47
+
48
+ ### Examples
49
+
50
+ ```bash
51
+ # Default (both files)
52
+ third-party-notices
53
+
54
+ # Custom node_modules path
55
+ third-party-notices --node-modules ./path/to/node_modules
56
+
57
+ # Review-only output (optional filename)
58
+ third-party-notices --review
59
+ third-party-notices --review ./out/THIRD-PARTY-LICENSE-REVIEW.md
60
+
61
+ # Main-only output (optional filename)
62
+ third-party-notices --license
63
+ third-party-notices --license ./out/THIRD-PARTY-LICENSE.md
64
+
65
+ # Exit with code 1 when something is missing
66
+ third-party-notices --fail-on-missing
67
+ ```
68
+
69
+ ### Programmatic API
70
+
71
+ ```js
72
+ import { collectThirdPartyLicenses } from "@myooken/license-output";
73
+
74
+ const result = await collectThirdPartyLicenses({
75
+ nodeModules: "./node_modules",
76
+ outFile: "./THIRD-PARTY-LICENSE.md",
77
+ reviewFile: "./THIRD-PARTY-LICENSE-REVIEW.md",
78
+ failOnMissing: false,
79
+ });
80
+
81
+ console.log(result.mainContent);
82
+ console.log(result.reviewContent);
83
+ ```
84
+
85
+ ### Output overview
86
+
87
+ - **THIRD-PARTY-LICENSE.md**
88
+ - List of packages
89
+ - Source / License info
90
+ - Full LICENSE/NOTICE/COPYING texts
91
+ - **THIRD-PARTY-LICENSE-REVIEW.md**
92
+ - Review-oriented checklist
93
+ - **Missing summary** section
94
+
95
+ ### How it differs from typical npm license tools (general view)
96
+
97
+ > Examples: `license-checker`, `license-report`, `license-finder`
98
+
99
+ - **Focused on bundling full license texts into a single Markdown file**
100
+ - Many existing tools emphasize JSON/CSV reports; this tool emphasizes **ready-to-share license documents**.
101
+ - **Separate review file** to track missing metadata
102
+ - Easier to integrate into audit workflows.
103
+ - **ESM / Node.js 18+ with no dependencies**
104
+ - Simple runtime requirements.
105
+
106
+ ### Notes
107
+
108
+ - Throws an error if `node_modules` does not exist.
109
+ - Missing `license` or `repository` fields are flagged in the review file.
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@myooken/license-output",
3
+ "version": "0.1.0",
4
+ "description": "Generate THIRD-PARTY-NOTICES 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-notices": "./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 ADDED
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ // CLIエントリーポイント(引数パースとファイル出力を担当)
3
+ import fsp from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { collectThirdPartyLicenses, DEFAULT_OPTIONS } from "./core.js";
7
+
8
+ // 引数パース: --review / --license は最後に指定されたものを優先し、直後の値があれば出力ファイル名として扱う
9
+ function parseArgs(argv) {
10
+ const args = { ...DEFAULT_OPTIONS };
11
+ let outputMode = "both"; // "both" | "review" | "license"
12
+
13
+ for (let i = 0; i < argv.length; i += 1) {
14
+ const a = argv[i];
15
+ if (a === "--node-modules" || a === "--nodeModules") {
16
+ const dir = optionalValue(argv, i + 1);
17
+ if (dir) {
18
+ args.nodeModules = dir;
19
+ i += 1;
20
+ }
21
+ } else if (a === "--review") {
22
+ outputMode = "review";
23
+ const file = optionalValue(argv, i + 1);
24
+ if (file) {
25
+ args.reviewFile = file;
26
+ i += 1;
27
+ }
28
+ } else if (a === "--license") {
29
+ outputMode = "license";
30
+ const file = optionalValue(argv, i + 1);
31
+ if (file) {
32
+ args.outFile = file;
33
+ i += 1;
34
+ }
35
+ } else if (a === "--fail-on-missing") {
36
+ args.failOnMissing = true;
37
+ } else if (a === "-h" || a === "--help") {
38
+ showHelp();
39
+ process.exit(0);
40
+ }
41
+ }
42
+ applyOutputMode(outputMode, args);
43
+ return args;
44
+ }
45
+
46
+ // オプションの直後にファイル名があれば取得(次のトークンが別オプションなら無視)
47
+ function optionalValue(argv, idx) {
48
+ const v = argv[idx];
49
+ if (!v) return null;
50
+ return v.startsWith("-") ? null : v;
51
+ }
52
+
53
+ // 生成対象をまとめて決定(両方/レビューのみ/ライセンスのみ)
54
+ function applyOutputMode(mode, args) {
55
+ if (mode === "review") {
56
+ args.writeMain = false;
57
+ args.writeReview = true;
58
+ } else if (mode === "license") {
59
+ args.writeMain = true;
60
+ args.writeReview = false;
61
+ } else {
62
+ args.writeMain = true;
63
+ args.writeReview = true;
64
+ }
65
+ }
66
+
67
+ function showHelp() {
68
+ console.log(`Usage:
69
+ third-party-notices [--node-modules <dir>] [--review [file]] [--license [file]] [--fail-on-missing]
70
+ `);
71
+ }
72
+
73
+ async function ensureParentDir(filePath) {
74
+ const dir = path.dirname(filePath);
75
+ await fsp.mkdir(dir, { recursive: true });
76
+ }
77
+
78
+ export async function runCli(argv = process.argv.slice(2)) {
79
+ const args = parseArgs(argv);
80
+
81
+ try {
82
+ const result = await collectThirdPartyLicenses(args);
83
+
84
+ const dirsToEnsure = [];
85
+ if (result.options.writeMain)
86
+ dirsToEnsure.push(ensureParentDir(result.options.outFile));
87
+ if (result.options.writeReview) {
88
+ dirsToEnsure.push(ensureParentDir(result.options.reviewFile));
89
+ }
90
+ await Promise.all(dirsToEnsure);
91
+
92
+ const writeTasks = [];
93
+ if (result.options.writeMain) {
94
+ writeTasks.push(
95
+ fsp.writeFile(result.options.outFile, result.mainContent, "utf8")
96
+ );
97
+ }
98
+ if (result.options.writeReview) {
99
+ writeTasks.push(
100
+ fsp.writeFile(result.options.reviewFile, result.reviewContent, "utf8")
101
+ );
102
+ }
103
+ await Promise.all(writeTasks);
104
+
105
+ if (result.options.writeMain)
106
+ console.log(`Generated: ${result.options.outFile}`);
107
+ if (result.options.writeReview)
108
+ console.log(`Review: ${result.options.reviewFile}`);
109
+ console.log(`Packages: ${result.stats.packages}`);
110
+ console.log(
111
+ `Missing LICENSE/NOTICE/COPYING: ${result.stats.missingFiles.length}`
112
+ );
113
+
114
+ if (result.stats.missingFiles.length > 0 && result.options.failOnMissing) {
115
+ process.exitCode = 1;
116
+ }
117
+ } catch (e) {
118
+ console.error(e?.stack || String(e));
119
+ process.exit(2);
120
+ }
121
+ }
122
+
123
+ function isCliExecution() {
124
+ // npmの .bin シム経由でも動作するように、実ファイル一致と .bin 配下を許可
125
+ const argv1 = process.argv[1];
126
+ if (!argv1) return false;
127
+ const self = fileURLToPath(import.meta.url);
128
+ const resolvedArg = path.resolve(argv1);
129
+ if (resolvedArg === self) return true;
130
+
131
+ const base = path.basename(resolvedArg).toLowerCase();
132
+ if (base === "third-party-notices" || base === "third-party-notices.cmd") {
133
+ return true;
134
+ }
135
+ if (resolvedArg.includes(`${path.sep}.bin${path.sep}`)) return true;
136
+ return false;
137
+ }
138
+
139
+ if (isCliExecution()) {
140
+ // eslint-disable-next-line unicorn/prefer-top-level-await
141
+ runCli();
142
+ }
@@ -0,0 +1,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
+ };
8
+
9
+ // ライセンスらしいファイル名を検出する正規表現
10
+ export const LICENSE_LIKE_RE =
11
+ /^(LICEN[CS]E|COPYING|NOTICE)(\..*)?$|^(LICEN[CS]E|COPYING|NOTICE)-/i;
package/src/core.js ADDED
@@ -0,0 +1,66 @@
1
+ import fsp from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { DEFAULT_OPTIONS } from "./constants.js";
4
+ import { gatherPackages } from "./scan.js";
5
+ import { renderMain, renderReview } from "./render.js";
6
+ import { uniqSorted } from "./fs-utils.js";
7
+
8
+ // 警告の出力先(デフォルトは console)
9
+ const defaultWarn = (msg) => {
10
+ console.warn(`warning: ${msg}`);
11
+ };
12
+
13
+ // 公開API: ライセンス情報を収集し、マークダウン文字列を返す
14
+ export async function collectThirdPartyLicenses(options = {}) {
15
+ const opts = normalizeOptions(options);
16
+ await assertNodeModulesExists(opts.nodeModules); // node_modules が無ければ即エラー
17
+
18
+ const result = await gatherPackages(opts);
19
+
20
+ const mainContent = renderMain(result.packages, opts);
21
+ const reviewContent = renderReview(
22
+ result.packages,
23
+ opts,
24
+ result.missingFiles,
25
+ result.missingSource,
26
+ result.missingLicenseField
27
+ );
28
+
29
+ return {
30
+ mainContent,
31
+ reviewContent,
32
+ options: opts,
33
+ stats: {
34
+ packages: result.seenCount,
35
+ missingFiles: uniqSorted(result.missingFiles),
36
+ missingSource: uniqSorted(result.missingSource),
37
+ missingLicenseField: uniqSorted(result.missingLicenseField),
38
+ },
39
+ };
40
+ }
41
+
42
+ export { DEFAULT_OPTIONS } from "./constants.js";
43
+
44
+ function normalizeOptions(options) {
45
+ return {
46
+ nodeModules: path.resolve(
47
+ options.nodeModules ?? DEFAULT_OPTIONS.nodeModules
48
+ ),
49
+ outFile: path.resolve(options.outFile ?? DEFAULT_OPTIONS.outFile),
50
+ reviewFile: path.resolve(options.reviewFile ?? DEFAULT_OPTIONS.reviewFile),
51
+ failOnMissing: Boolean(
52
+ options.failOnMissing ?? DEFAULT_OPTIONS.failOnMissing
53
+ ),
54
+ writeMain: options.writeMain ?? true,
55
+ writeReview: options.writeReview ?? true,
56
+ warn: options.onWarn ?? defaultWarn,
57
+ };
58
+ }
59
+
60
+ async function assertNodeModulesExists(dir) {
61
+ // node_modules の有無を事前チェックし、無ければ例外を投げてCIなどで失敗させる
62
+ const stat = await fsp.stat(dir).catch(() => null);
63
+ if (!stat || !stat.isDirectory()) {
64
+ throw new Error(`not found node_modules: ${dir}`);
65
+ }
66
+ }
@@ -0,0 +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
+ }
package/src/render.js ADDED
@@ -0,0 +1,104 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { mdSafeText, uniqSorted } from "./fs-utils.js";
4
+
5
+ // メインのTHIRD-PARTY-LICENSE.mdを描画する
6
+ export function renderMain(packages, opts) {
7
+ const lines = [];
8
+ const push = (s = "") => lines.push(s);
9
+
10
+ push("# Third-Party Licenses");
11
+ push("");
12
+ push(`Generated from: ${opts.nodeModules}`);
13
+ push("");
14
+
15
+ for (const pkg of packages) {
16
+ push(`<a id="${pkg.anchor}"></a>`);
17
+ push(`## ${pkg.key}`);
18
+ push(`- Source: ${pkg.source ?? "(missing)"}`);
19
+ push(`- License: ${pkg.license ?? "(missing)"}`);
20
+
21
+ if (pkg.fileNames.length === 0) {
22
+ push("- (no LICENSE/NOTICE/COPYING files)");
23
+ push("");
24
+ push("_No LICENSE/NOTICE/COPYING file found in package directory._");
25
+ push("");
26
+ continue;
27
+ }
28
+
29
+ for (const n of pkg.fileNames) push(`- ${n}`);
30
+ push("");
31
+
32
+ for (const lic of pkg.licenseTexts) {
33
+ push(`### ${lic.name}`);
34
+ push("```text");
35
+ push(mdSafeText(lic.text).replace(/\s+$/, ""));
36
+ push("```");
37
+ push("");
38
+ }
39
+ }
40
+
41
+ return lines.join(os.EOL) + os.EOL;
42
+ }
43
+
44
+ // レビューファイルを描画する
45
+ export function renderReview(
46
+ packages,
47
+ opts,
48
+ missingFiles,
49
+ missingSource,
50
+ missingLicenseField
51
+ ) {
52
+ const lines = [];
53
+ const push = (s = "") => lines.push(s);
54
+ const mainBase = path.basename(opts.outFile);
55
+
56
+ push("# THIRD-PARTY-LICENSE-REVIEW");
57
+ push("");
58
+ push(`Generated from: ${opts.nodeModules}`);
59
+ push(`Main file: ${mainBase}`);
60
+ push("");
61
+
62
+ for (const it of [...packages].sort((a, b) => a.key.localeCompare(b.key))) {
63
+ push(`## ${it.key}`);
64
+ push(`- Main: ${mainBase}#${it.anchor}`);
65
+ push(`- Source: ${it.source ?? "(missing)"}`);
66
+ push(`- License: ${it.license ?? "(missing)"}`);
67
+
68
+ push("- Files:");
69
+ if (it.fileNames.length === 0) {
70
+ push(" - (none)");
71
+ } else {
72
+ for (const f of it.fileNames) push(` - ${f}`);
73
+ }
74
+
75
+ if (it.flags.length === 0) {
76
+ push("- Status: Check manually");
77
+ } else {
78
+ push(`- Status: ${it.flags.join(" / ")}`);
79
+ }
80
+
81
+ push("- Notes:");
82
+ push("");
83
+ }
84
+
85
+ push("---");
86
+ push("");
87
+ push("## Missing summary");
88
+ push("");
89
+
90
+ const writeList = (title, arr) => {
91
+ push(`### ${title}`);
92
+ push("");
93
+ const xs = uniqSorted(arr);
94
+ if (xs.length === 0) push("- (none)");
95
+ else for (const x of xs) push(`- ${x}`);
96
+ push("");
97
+ };
98
+
99
+ writeList("Missing Source", missingSource);
100
+ writeList("Missing package.json license field", missingLicenseField);
101
+ writeList("Missing LICENSE/NOTICE/COPYING files", missingFiles);
102
+
103
+ return lines.join(os.EOL) + os.EOL;
104
+ }
package/src/scan.js ADDED
@@ -0,0 +1,133 @@
1
+ import path from "node:path";
2
+ import {
3
+ getLicenseLikeFilesInFolderRoot,
4
+ makeAnchorId,
5
+ readPackageJson,
6
+ readTextFileSmart,
7
+ uniqSorted,
8
+ walkForPackageJson,
9
+ } from "./fs-utils.js";
10
+ import { getRepositoryUrl } from "./url.js";
11
+
12
+ // node_modules を走査してパッケージ情報を集約する
13
+ export async function gatherPackages(opts) {
14
+ const missingFiles = [];
15
+ const missingSource = [];
16
+ const missingLicenseField = [];
17
+ const packages = [];
18
+ const seen = new Set();
19
+
20
+ for await (const pj of walkForPackageJson(opts.nodeModules)) {
21
+ const pkgDir = path.dirname(pj);
22
+ const pkg = await readPackageJson(pj);
23
+ if (!pkg) continue;
24
+
25
+ const name =
26
+ typeof pkg.name === "string" && pkg.name.trim().length > 0
27
+ ? pkg.name.trim()
28
+ : "";
29
+ const version =
30
+ typeof pkg.version === "string" && pkg.version.trim().length > 0
31
+ ? pkg.version.trim()
32
+ : "";
33
+ if (!name || !version) continue;
34
+
35
+ const key = `${name}@${version}`;
36
+ if (seen.has(key)) continue;
37
+ seen.add(key);
38
+
39
+ const anchor = makeAnchorId(key);
40
+ const source = getRepositoryUrl(pkg);
41
+ const license = formatLicense(pkg.license); // 文字列/オブジェクト/配列すべてを受け付ける
42
+
43
+ const flags = [];
44
+ if (!source) {
45
+ missingSource.push(key);
46
+ flags.push("Missing Source");
47
+ opts.warn(`Unknown source: ${key}`);
48
+ }
49
+ if (!license) {
50
+ missingLicenseField.push(key);
51
+ flags.push("Missing package.json license");
52
+ opts.warn(`Missing license in package.json: ${key}`);
53
+ }
54
+
55
+ const licFiles = await getLicenseLikeFilesInFolderRoot(pkgDir);
56
+ const fileNames = licFiles.map((f) => path.basename(f));
57
+
58
+ if (licFiles.length === 0) {
59
+ missingFiles.push(key);
60
+ flags.push("Missing LICENSE/NOTICE/COPYING files");
61
+ opts.warn(`Missing LICENSE/NOTICE/COPYING in ${pkgDir} (${key})`);
62
+ }
63
+
64
+ const licenseTexts =
65
+ licFiles.length > 0
66
+ ? await Promise.all(
67
+ licFiles.map(async (filePath) => ({
68
+ name: path.basename(filePath),
69
+ text: await readTextFileSmart(filePath),
70
+ }))
71
+ )
72
+ : [];
73
+
74
+ packages.push({
75
+ key,
76
+ anchor,
77
+ source,
78
+ license,
79
+ fileNames,
80
+ flags,
81
+ licenseTexts,
82
+ });
83
+ }
84
+
85
+ return {
86
+ packages,
87
+ missingFiles: uniqSorted(missingFiles),
88
+ missingSource: uniqSorted(missingSource),
89
+ missingLicenseField: uniqSorted(missingLicenseField),
90
+ seenCount: seen.size,
91
+ };
92
+ }
93
+
94
+ // license フィールドを人間可読にまとめる(文字列/オブジェクト/配列に対応)
95
+ function formatLicense(raw) {
96
+ const parts = [];
97
+
98
+ const pushMaybe = (v) => {
99
+ if (typeof v === "string" && v.trim()) parts.push(v.trim());
100
+ };
101
+
102
+ const handleObj = (licObj) => {
103
+ if (!licObj || typeof licObj !== "object") return;
104
+ const type =
105
+ typeof licObj.type === "string" && licObj.type.trim()
106
+ ? licObj.type.trim()
107
+ : "";
108
+ const url =
109
+ typeof licObj.url === "string" && licObj.url.trim()
110
+ ? licObj.url.trim()
111
+ : "";
112
+ if (type && url) {
113
+ parts.push(`${type} (${url})`);
114
+ } else {
115
+ pushMaybe(type);
116
+ pushMaybe(url);
117
+ }
118
+ };
119
+
120
+ if (typeof raw === "string") {
121
+ pushMaybe(raw);
122
+ } else if (Array.isArray(raw)) {
123
+ for (const lic of raw) {
124
+ if (typeof lic === "string") pushMaybe(lic);
125
+ else handleObj(lic);
126
+ }
127
+ } else {
128
+ handleObj(raw);
129
+ }
130
+
131
+ if (parts.length === 0) return null;
132
+ return [...new Set(parts)].join(" | ");
133
+ }
package/src/url.js ADDED
@@ -0,0 +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
+ }