noticg 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/notice-gen +426 -0
- package/package.json +26 -0
package/notice-gen
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import fscb from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
|
|
8
|
+
const LICENSE_FILE_NAMES = [
|
|
9
|
+
"LICENSE",
|
|
10
|
+
"LICENSE.md",
|
|
11
|
+
"LICENSE.txt",
|
|
12
|
+
"LICENCE",
|
|
13
|
+
"LICENCE.md",
|
|
14
|
+
"LICENCE.txt",
|
|
15
|
+
"COPYING",
|
|
16
|
+
"COPYING.txt",
|
|
17
|
+
"NOTICE",
|
|
18
|
+
"NOTICE.txt",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const DEFAULT_OUT = "NOTICE.md";
|
|
22
|
+
|
|
23
|
+
function parseArgs(argv) {
|
|
24
|
+
const args = {
|
|
25
|
+
out: DEFAULT_OUT,
|
|
26
|
+
format: "md",
|
|
27
|
+
root: process.cwd(),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
for (let i = 2; i < argv.length; i++) {
|
|
31
|
+
const arg = argv[i];
|
|
32
|
+
const next = argv[i + 1];
|
|
33
|
+
|
|
34
|
+
if (arg === "--out" && next) {
|
|
35
|
+
args.out = next;
|
|
36
|
+
i++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (arg.startsWith("--out=")) {
|
|
41
|
+
args.out = arg.slice("--out=".length);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (arg === "--format" && next) {
|
|
46
|
+
args.format = next;
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (arg.startsWith("--format=")) {
|
|
52
|
+
args.format = arg.slice("--format=".length);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (arg === "--root" && next) {
|
|
57
|
+
args.root = path.resolve(next);
|
|
58
|
+
i++;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (arg.startsWith("--root=")) {
|
|
63
|
+
args.root = path.resolve(arg.slice("--root=".length));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return args;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function fileExists(filePath) {
|
|
72
|
+
try {
|
|
73
|
+
await fs.access(filePath);
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function readJson(filePath) {
|
|
81
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
82
|
+
return JSON.parse(raw);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeLicenseField(license) {
|
|
86
|
+
if (!license) return "UNKNOWN";
|
|
87
|
+
if (typeof license === "string") return license.trim() || "UNKNOWN";
|
|
88
|
+
if (Array.isArray(license)) {
|
|
89
|
+
return license
|
|
90
|
+
.map((item) => {
|
|
91
|
+
if (typeof item === "string") return item;
|
|
92
|
+
if (item && typeof item === "object") return item.type || item.name || "";
|
|
93
|
+
return "";
|
|
94
|
+
})
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.join(" OR ") || "UNKNOWN";
|
|
97
|
+
}
|
|
98
|
+
if (typeof license === "object") {
|
|
99
|
+
return license.type || license.name || "UNKNOWN";
|
|
100
|
+
}
|
|
101
|
+
return "UNKNOWN";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function escapeMarkdown(text) {
|
|
105
|
+
return String(text)
|
|
106
|
+
.replaceAll("\\", "\\\\")
|
|
107
|
+
.replaceAll("`", "\\`")
|
|
108
|
+
.replaceAll("*", "\\*")
|
|
109
|
+
.replaceAll("_", "\\_")
|
|
110
|
+
.replaceAll("{", "\\{")
|
|
111
|
+
.replaceAll("}", "\\}")
|
|
112
|
+
.replaceAll("[", "\\[")
|
|
113
|
+
.replaceAll("]", "\\]")
|
|
114
|
+
.replaceAll("(", "\\(")
|
|
115
|
+
.replaceAll(")", "\\)")
|
|
116
|
+
.replaceAll("#", "\\#")
|
|
117
|
+
.replaceAll("+", "\\+")
|
|
118
|
+
.replaceAll("-", "\\-")
|
|
119
|
+
.replaceAll("!", "\\!");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function findLicenseFile(pkgDir) {
|
|
123
|
+
for (const name of LICENSE_FILE_NAMES) {
|
|
124
|
+
const candidate = path.join(pkgDir, name);
|
|
125
|
+
if (await fileExists(candidate)) {
|
|
126
|
+
return candidate;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Fallback: scan for a file that starts with LICENSE / COPYING / NOTICE
|
|
131
|
+
try {
|
|
132
|
+
const entries = await fs.readdir(pkgDir, { withFileTypes: true });
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (!entry.isFile()) continue;
|
|
135
|
+
const upper = entry.name.toUpperCase();
|
|
136
|
+
if (
|
|
137
|
+
upper.startsWith("LICENSE") ||
|
|
138
|
+
upper.startsWith("LICENCE") ||
|
|
139
|
+
upper.startsWith("COPYING") ||
|
|
140
|
+
upper.startsWith("NOTICE")
|
|
141
|
+
) {
|
|
142
|
+
return path.join(pkgDir, entry.name);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// ignore
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function readLicenseText(pkgDir, pkgJson) {
|
|
153
|
+
const licenseFile = await findLicenseFile(pkgDir);
|
|
154
|
+
if (licenseFile) {
|
|
155
|
+
try {
|
|
156
|
+
const text = await fs.readFile(licenseFile, "utf8");
|
|
157
|
+
return {
|
|
158
|
+
source: path.basename(licenseFile),
|
|
159
|
+
text: text.trim(),
|
|
160
|
+
};
|
|
161
|
+
} catch {
|
|
162
|
+
// ignore and fallback below
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const license = normalizeLicenseField(pkgJson.license || pkgJson.licenses);
|
|
167
|
+
return {
|
|
168
|
+
source: "package.json",
|
|
169
|
+
text: `License information not found in a license file.\nDeclared license: ${license}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isSkippablePackage(meta = {}) {
|
|
174
|
+
return Boolean(
|
|
175
|
+
meta.dev ||
|
|
176
|
+
meta.optional ||
|
|
177
|
+
meta.peer ||
|
|
178
|
+
meta.extraneous ||
|
|
179
|
+
meta.link ||
|
|
180
|
+
meta.inBundle
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function collectFromPackageLock(root) {
|
|
185
|
+
const lockPath = path.join(root, "package-lock.json");
|
|
186
|
+
if (!(await fileExists(lockPath))) return null;
|
|
187
|
+
|
|
188
|
+
const lock = await readJson(lockPath);
|
|
189
|
+
|
|
190
|
+
// npm lockfile v2/v3
|
|
191
|
+
if (lock && lock.packages && typeof lock.packages === "object") {
|
|
192
|
+
const items = [];
|
|
193
|
+
const seen = new Set();
|
|
194
|
+
|
|
195
|
+
for (const [relPath, meta] of Object.entries(lock.packages)) {
|
|
196
|
+
if (!relPath) continue; // root package
|
|
197
|
+
|
|
198
|
+
if (isSkippablePackage(meta)) continue;
|
|
199
|
+
|
|
200
|
+
const pkgDir = path.join(root, relPath);
|
|
201
|
+
const pkgJsonPath = path.join(pkgDir, "package.json");
|
|
202
|
+
if (!(await fileExists(pkgJsonPath))) continue;
|
|
203
|
+
|
|
204
|
+
const pkgJson = await readJson(pkgJsonPath);
|
|
205
|
+
const name = pkgJson.name || meta.name || path.basename(pkgDir);
|
|
206
|
+
const version = pkgJson.version || meta.version || "0.0.0";
|
|
207
|
+
const key = `${name}@${version}`;
|
|
208
|
+
|
|
209
|
+
if (seen.has(key)) continue;
|
|
210
|
+
seen.add(key);
|
|
211
|
+
|
|
212
|
+
const licenseText = await readLicenseText(pkgDir, pkgJson);
|
|
213
|
+
|
|
214
|
+
items.push({
|
|
215
|
+
name,
|
|
216
|
+
version,
|
|
217
|
+
license: normalizeLicenseField(pkgJson.license || pkgJson.licenses || meta.license),
|
|
218
|
+
homepage: pkgJson.homepage || meta.homepage || "",
|
|
219
|
+
repository:
|
|
220
|
+
typeof pkgJson.repository === "string"
|
|
221
|
+
? pkgJson.repository
|
|
222
|
+
: pkgJson.repository?.url || "",
|
|
223
|
+
pkgDir,
|
|
224
|
+
licenseText,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
items.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
229
|
+
return items;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// npm lockfile v1 fallback
|
|
233
|
+
if (lock && lock.dependencies && typeof lock.dependencies === "object") {
|
|
234
|
+
const items = [];
|
|
235
|
+
const seen = new Set();
|
|
236
|
+
|
|
237
|
+
const walk = async (deps) => {
|
|
238
|
+
for (const [name, meta] of Object.entries(deps)) {
|
|
239
|
+
if (!meta || isSkippablePackage(meta)) continue;
|
|
240
|
+
|
|
241
|
+
const version = meta.version || "0.0.0";
|
|
242
|
+
const key = `${name}@${version}`;
|
|
243
|
+
if (!seen.has(key)) {
|
|
244
|
+
const pkgDir = path.join(root, "node_modules", name);
|
|
245
|
+
const pkgJsonPath = path.join(pkgDir, "package.json");
|
|
246
|
+
|
|
247
|
+
if (await fileExists(pkgJsonPath)) {
|
|
248
|
+
const pkgJson = await readJson(pkgJsonPath);
|
|
249
|
+
seen.add(key);
|
|
250
|
+
|
|
251
|
+
items.push({
|
|
252
|
+
name: pkgJson.name || name,
|
|
253
|
+
version: pkgJson.version || version,
|
|
254
|
+
license: normalizeLicenseField(pkgJson.license || pkgJson.licenses || meta.license),
|
|
255
|
+
homepage: pkgJson.homepage || "",
|
|
256
|
+
repository:
|
|
257
|
+
typeof pkgJson.repository === "string"
|
|
258
|
+
? pkgJson.repository
|
|
259
|
+
: pkgJson.repository?.url || "",
|
|
260
|
+
pkgDir,
|
|
261
|
+
licenseText: await readLicenseText(pkgDir, pkgJson),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (meta.dependencies && typeof meta.dependencies === "object") {
|
|
267
|
+
await walk(meta.dependencies);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
await walk(lock.dependencies);
|
|
273
|
+
items.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
274
|
+
return items;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function collectFromNpmLs(root) {
|
|
281
|
+
try {
|
|
282
|
+
const out = execFileSync(
|
|
283
|
+
"npm",
|
|
284
|
+
["ls", "--json", "--all", "--long", "--omit=dev"],
|
|
285
|
+
{
|
|
286
|
+
cwd: root,
|
|
287
|
+
encoding: "utf8",
|
|
288
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
289
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
290
|
+
}
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const tree = JSON.parse(out);
|
|
294
|
+
const items = [];
|
|
295
|
+
const seen = new Set();
|
|
296
|
+
|
|
297
|
+
const walk = async (deps) => {
|
|
298
|
+
if (!deps || typeof deps !== "object") return;
|
|
299
|
+
|
|
300
|
+
for (const [name, node] of Object.entries(deps)) {
|
|
301
|
+
if (!node || isSkippablePackage(node)) continue;
|
|
302
|
+
|
|
303
|
+
const version = node.version || "0.0.0";
|
|
304
|
+
const key = `${name}@${version}`;
|
|
305
|
+
let pkgDir = node.path || "";
|
|
306
|
+
|
|
307
|
+
if (!pkgDir && node._location) {
|
|
308
|
+
pkgDir = path.join(root, node._location);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!pkgDir) {
|
|
312
|
+
pkgDir = path.join(root, "node_modules", name);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const pkgJsonPath = path.join(pkgDir, "package.json");
|
|
316
|
+
if (seen.has(key)) {
|
|
317
|
+
if (node.dependencies) await walk(node.dependencies);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!(await fileExists(pkgJsonPath))) {
|
|
322
|
+
if (node.dependencies) await walk(node.dependencies);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const pkgJson = await readJson(pkgJsonPath);
|
|
327
|
+
seen.add(key);
|
|
328
|
+
|
|
329
|
+
items.push({
|
|
330
|
+
name: pkgJson.name || name,
|
|
331
|
+
version: pkgJson.version || version,
|
|
332
|
+
license: normalizeLicenseField(pkgJson.license || pkgJson.licenses || node.license),
|
|
333
|
+
homepage: pkgJson.homepage || "",
|
|
334
|
+
repository:
|
|
335
|
+
typeof pkgJson.repository === "string"
|
|
336
|
+
? pkgJson.repository
|
|
337
|
+
: pkgJson.repository?.url || "",
|
|
338
|
+
pkgDir,
|
|
339
|
+
licenseText: await readLicenseText(pkgDir, pkgJson),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
if (node.dependencies) await walk(node.dependencies);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
await walk(tree.dependencies);
|
|
347
|
+
items.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
348
|
+
return items;
|
|
349
|
+
} catch {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function formatNoticeMarkdown(items, rootPackageName) {
|
|
355
|
+
const header = [
|
|
356
|
+
"# NOTICE",
|
|
357
|
+
"",
|
|
358
|
+
`This file lists third-party software used by **${rootPackageName}**.`,
|
|
359
|
+
"",
|
|
360
|
+
"---",
|
|
361
|
+
"",
|
|
362
|
+
].join("\n");
|
|
363
|
+
|
|
364
|
+
const sections = items.map((item, index) => {
|
|
365
|
+
const metaLines = [
|
|
366
|
+
`## ${index + 1}. ${escapeMarkdown(item.name)}`,
|
|
367
|
+
"",
|
|
368
|
+
`- Version: \`${escapeMarkdown(item.version)}\``,
|
|
369
|
+
`- License: \`${escapeMarkdown(item.license)}\``,
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
if (item.homepage) metaLines.push(`- Homepage: ${escapeMarkdown(item.homepage)}`);
|
|
373
|
+
if (item.repository) metaLines.push(`- Repository: ${escapeMarkdown(item.repository)}`);
|
|
374
|
+
|
|
375
|
+
metaLines.push("");
|
|
376
|
+
metaLines.push(`### License text (${escapeMarkdown(item.licenseText.source)})`);
|
|
377
|
+
metaLines.push("");
|
|
378
|
+
metaLines.push("```text");
|
|
379
|
+
metaLines.push(item.licenseText.text || "License text not available.");
|
|
380
|
+
metaLines.push("```");
|
|
381
|
+
metaLines.push("");
|
|
382
|
+
metaLines.push("---");
|
|
383
|
+
metaLines.push("");
|
|
384
|
+
|
|
385
|
+
return metaLines.join("\n");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
return [header, ...sections].join("\n");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function main() {
|
|
392
|
+
const { out, root } = parseArgs(process.argv);
|
|
393
|
+
|
|
394
|
+
const rootPkgPath = path.join(root, "package.json");
|
|
395
|
+
if (!(await fileExists(rootPkgPath))) {
|
|
396
|
+
console.error(`package.json not found in: ${root}`);
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const rootPkg = await readJson(rootPkgPath);
|
|
401
|
+
const rootPackageName = rootPkg.name || path.basename(root);
|
|
402
|
+
|
|
403
|
+
let items = await collectFromPackageLock(root);
|
|
404
|
+
if (!items) {
|
|
405
|
+
items = await collectFromNpmLs(root);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (!items || items.length === 0) {
|
|
409
|
+
console.error("No third-party dependencies found, or dependency tree could not be read.");
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const notice = formatNoticeMarkdown(items, rootPackageName);
|
|
414
|
+
const outPath = path.isAbsolute(out) ? out : path.join(root, out);
|
|
415
|
+
|
|
416
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
417
|
+
await fs.writeFile(outPath, notice, "utf8");
|
|
418
|
+
|
|
419
|
+
console.log(`NOTICE written to: ${outPath}`);
|
|
420
|
+
console.log(`Included packages: ${items.length}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
main().catch((err) => {
|
|
424
|
+
console.error(err instanceof Error ? err.stack || err.message : String(err));
|
|
425
|
+
process.exit(1);
|
|
426
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "noticg",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "NOTICG - A Notice License Generator",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"notice-gen",
|
|
7
|
+
"notice-file",
|
|
8
|
+
"license",
|
|
9
|
+
"licensor"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/KhairyK/zetro#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/KhairyK/zetro/issues"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/KhairyK/zetro.git"
|
|
18
|
+
},
|
|
19
|
+
"license": "Apache-2.0",
|
|
20
|
+
"author": "Sholehuddin Khairy (KhairyK)",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "notice-gen",
|
|
23
|
+
"bin": {
|
|
24
|
+
"noticg": "./notice-gen"
|
|
25
|
+
}
|
|
26
|
+
}
|