license-cop 1.8.1 → 2.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.
Files changed (105) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +25 -5
  3. package/dist/bin.js +545 -0
  4. package/dist/bin.js.map +1 -0
  5. package/package.json +22 -15
  6. package/src/bin/license-cop +0 -4
  7. package/src/index.d.ts +0 -3
  8. package/src/index.js +0 -10
  9. package/src/index.js.map +0 -1
  10. package/src/lib/cli/commands/init.d.ts +0 -5
  11. package/src/lib/cli/commands/init.js +0 -39
  12. package/src/lib/cli/commands/init.js.map +0 -1
  13. package/src/lib/cli/commands/main.d.ts +0 -7
  14. package/src/lib/cli/commands/main.js +0 -61
  15. package/src/lib/cli/commands/main.js.map +0 -1
  16. package/src/lib/cli/commands/version.d.ts +0 -4
  17. package/src/lib/cli/commands/version.js +0 -21
  18. package/src/lib/cli/commands/version.js.map +0 -1
  19. package/src/lib/cli/create-command.d.ts +0 -5
  20. package/src/lib/cli/create-command.js +0 -17
  21. package/src/lib/cli/create-command.js.map +0 -1
  22. package/src/lib/cli/index.d.ts +0 -1
  23. package/src/lib/cli/index.js +0 -18
  24. package/src/lib/cli/index.js.map +0 -1
  25. package/src/lib/cli/report-failure.d.ts +0 -2
  26. package/src/lib/cli/report-failure.js +0 -36
  27. package/src/lib/cli/report-failure.js.map +0 -1
  28. package/src/lib/cli/report-success.d.ts +0 -2
  29. package/src/lib/cli/report-success.js +0 -22
  30. package/src/lib/cli/report-success.js.map +0 -1
  31. package/src/lib/config/config-error.d.ts +0 -4
  32. package/src/lib/config/config-error.js +0 -27
  33. package/src/lib/config/config-error.js.map +0 -1
  34. package/src/lib/config/config.d.ts +0 -39
  35. package/src/lib/config/config.js +0 -35
  36. package/src/lib/config/config.js.map +0 -1
  37. package/src/lib/config/find-config.d.ts +0 -1
  38. package/src/lib/config/find-config.js +0 -67
  39. package/src/lib/config/find-config.js.map +0 -1
  40. package/src/lib/config/load-config.d.ts +0 -7
  41. package/src/lib/config/load-config.js +0 -26
  42. package/src/lib/config/load-config.js.map +0 -1
  43. package/src/lib/config/load-parent-config.d.ts +0 -1
  44. package/src/lib/config/load-parent-config.js +0 -30
  45. package/src/lib/config/load-parent-config.js.map +0 -1
  46. package/src/lib/config/parent-resolutions/github.d.ts +0 -1
  47. package/src/lib/config/parent-resolutions/github.js +0 -28
  48. package/src/lib/config/parent-resolutions/github.js.map +0 -1
  49. package/src/lib/config/parent-resolutions/http.d.ts +0 -1
  50. package/src/lib/config/parent-resolutions/http.js +0 -20
  51. package/src/lib/config/parent-resolutions/http.js.map +0 -1
  52. package/src/lib/config/parent-resolutions/npm.d.ts +0 -2
  53. package/src/lib/config/parent-resolutions/npm.js +0 -32
  54. package/src/lib/config/parent-resolutions/npm.js.map +0 -1
  55. package/src/lib/config/parsers/json5.d.ts +0 -1
  56. package/src/lib/config/parsers/json5.js +0 -11
  57. package/src/lib/config/parsers/json5.js.map +0 -1
  58. package/src/lib/dependency/get-package-manager.d.ts +0 -2
  59. package/src/lib/dependency/get-package-manager.js +0 -52
  60. package/src/lib/dependency/get-package-manager.js.map +0 -1
  61. package/src/lib/dependency/package-json.d.ts +0 -54
  62. package/src/lib/dependency/package-json.js +0 -75
  63. package/src/lib/dependency/package-json.js.map +0 -1
  64. package/src/lib/dependency/package-rules.d.ts +0 -1
  65. package/src/lib/dependency/package-rules.js +0 -24
  66. package/src/lib/dependency/package-rules.js.map +0 -1
  67. package/src/lib/dependency-scanning/npm.d.ts +0 -3
  68. package/src/lib/dependency-scanning/npm.js +0 -100
  69. package/src/lib/dependency-scanning/npm.js.map +0 -1
  70. package/src/lib/dependency-scanning/options.d.ts +0 -2
  71. package/src/lib/dependency-scanning/options.js +0 -3
  72. package/src/lib/dependency-scanning/options.js.map +0 -1
  73. package/src/lib/dependency-scanning/pnpm.d.ts +0 -3
  74. package/src/lib/dependency-scanning/pnpm.js +0 -103
  75. package/src/lib/dependency-scanning/pnpm.js.map +0 -1
  76. package/src/lib/license-cop.d.ts +0 -9
  77. package/src/lib/license-cop.js +0 -52
  78. package/src/lib/license-cop.js.map +0 -1
  79. package/src/lib/logger.d.ts +0 -8
  80. package/src/lib/logger.js +0 -41
  81. package/src/lib/logger.js.map +0 -1
  82. package/src/lib/result.d.ts +0 -27
  83. package/src/lib/result.js +0 -3
  84. package/src/lib/result.js.map +0 -1
  85. package/src/lib/spdx/calculate-issues.d.ts +0 -2
  86. package/src/lib/spdx/calculate-issues.js +0 -45
  87. package/src/lib/spdx/calculate-issues.js.map +0 -1
  88. package/src/lib/spdx/get-tokens.d.ts +0 -2
  89. package/src/lib/spdx/get-tokens.js +0 -54
  90. package/src/lib/spdx/get-tokens.js.map +0 -1
  91. package/src/lib/spdx/parse-license-expression.d.ts +0 -2
  92. package/src/lib/spdx/parse-license-expression.js +0 -20
  93. package/src/lib/spdx/parse-license-expression.js.map +0 -1
  94. package/src/lib/spdx/parse-tokens.d.ts +0 -3
  95. package/src/lib/spdx/parse-tokens.js +0 -122
  96. package/src/lib/spdx/parse-tokens.js.map +0 -1
  97. package/src/lib/spdx/types/spdx-expression.d.ts +0 -20
  98. package/src/lib/spdx/types/spdx-expression.js +0 -3
  99. package/src/lib/spdx/types/spdx-expression.js.map +0 -1
  100. package/src/lib/spdx/types/token.d.ts +0 -23
  101. package/src/lib/spdx/types/token.js +0 -29
  102. package/src/lib/spdx/types/token.js.map +0 -1
  103. package/src/lib/utils/join-string-array.d.ts +0 -1
  104. package/src/lib/utils/join-string-array.js +0 -28
  105. package/src/lib/utils/join-string-array.js.map +0 -1
package/LICENSE.md CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2023-2025, [Toby Smith](https://tobysmith.uk), contact@tobythe.dev
1
+ Copyright (c) 2023-2026, [Toby Smith](https://tobysmith.uk), contact@tobythe.dev
2
2
 
3
3
  Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
4
4
 
package/README.md CHANGED
@@ -36,9 +36,29 @@ npx license-cop
36
36
 
37
37
  The `license-cop` command will use an exit code of 0 if all your dependencies conform to the settings in your config file.
38
38
 
39
+ ## Command line options
40
+
41
+ ```bash
42
+ license-cop [options] [command]
43
+ ```
44
+
45
+ | Option | Description |
46
+ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
47
+ | `--dev-dependencies <include\|only>` | `include` checks your dev-dependencies as well as your production dependencies, `only` checks just your dev-dependencies. |
48
+ | `-d, --directory <dir>` | The directory of the project. Defaults to the current working directory. |
49
+ | `--verbose` | Enable verbose logging. |
50
+ | `-v, --version` | Print the installed version of license-cop. |
51
+ | `-h, --help` | Display the help text. |
52
+
53
+ | Command | Description |
54
+ | ------- | -------------------------------------------------------------------------------------------------------------------------------------- |
55
+ | `init` | Create a new license-cop config file. It won't overwrite one: it fails if the project already has a config in any of the places below. |
56
+
57
+ When `--dev-dependencies` is given it replaces the [`includeDevDependencies`](#includedevdependencies) and [`devDependenciesOnly`](#devdependenciesonly) config options; when it's absent those config options apply.
58
+
39
59
  ## Config File
40
60
 
41
- By default the `--init` flag will make a `.licenses.json` file, however you can use many different variations of file name and file type including:
61
+ By default the `init` command will make a `.licenses.json` file, however you can use many different variations of file name and file type including:
42
62
 
43
63
  <!--- cspell:disable-next-line --->
44
64
 
@@ -101,12 +121,12 @@ If you extend a remote file, and that in-turn extends an npm package, then you'r
101
121
  #### `includeDevDependencies`
102
122
 
103
123
  `false` by default.
104
- Set to `true` to make license-cop also check your dev-dependencies.
124
+ Set to `true` to make license-cop also check your dev-dependencies. The `--dev-dependencies` command line option takes precedence over this.
105
125
 
106
126
  #### `devDependenciesOnly`
107
127
 
108
128
  `false` by default.
109
- Set to `true` to make license-cop only check your dev-dependencies.
129
+ Set to `true` to make license-cop only check your dev-dependencies. The `--dev-dependencies` command line option takes precedence over this.
110
130
 
111
131
  ## CI/CD Example (GitHub Actions)
112
132
 
@@ -129,10 +149,10 @@ jobs:
129
149
  runs-on: ubuntu-latest
130
150
 
131
151
  steps:
132
- - uses: actions/checkout@v3
152
+ - uses: actions/checkout@v7
133
153
 
134
154
  - name: Setup Node.js
135
- uses: actions/setup-node@v3
155
+ uses: actions/setup-node@v7
136
156
  with:
137
157
  cache: npm
138
158
 
package/dist/bin.js ADDED
@@ -0,0 +1,545 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { z } from "zod";
4
+ import { LicenseCopError, PackageJsonError, checkLicenses } from "@license-cop/core";
5
+ import { join } from "node:path";
6
+ import { readFile, stat, writeFile } from "node:fs/promises";
7
+ import JSON5 from "json5";
8
+ import deepMerge from "deepmerge";
9
+ import { cosmiconfig } from "cosmiconfig";
10
+ import { stat as stat$1 } from "fs/promises";
11
+ import { join as join$1 } from "path";
12
+ import { fileURLToPath } from "node:url";
13
+ //#region src/io.ts
14
+ const defaultIo = {
15
+ stdout: (line) => console.log(line),
16
+ stderr: (line) => console.error(line)
17
+ };
18
+ const createVerboseLogger = (io, enabled) => {
19
+ if (!enabled) return () => {};
20
+ const verbose = (message) => io.stdout(message);
21
+ verbose("Verbose logging enabled");
22
+ return verbose;
23
+ };
24
+ //#endregion
25
+ //#region src/errors.ts
26
+ var UsageError = class extends Error {
27
+ constructor(message) {
28
+ super(message);
29
+ this.name = "UsageError";
30
+ }
31
+ };
32
+ //#endregion
33
+ //#region src/args/schema.ts
34
+ const devDependenciesModeSchema = z.enum(["include", "only"], { error: (issue) => `--dev-dependencies must be 'include' or 'only', but got '${String(issue.input)}'` });
35
+ /** What the command line asks for. Each `error` is what the user sees, so it reads as a sentence. */
36
+ const cliInvocationSchema = z.discriminatedUnion("kind", [
37
+ z.object({
38
+ kind: z.literal("check"),
39
+ directory: z.string(),
40
+ verbose: z.boolean(),
41
+ devDependencies: devDependenciesModeSchema.optional()
42
+ }),
43
+ z.object({
44
+ kind: z.literal("init"),
45
+ directory: z.string(),
46
+ verbose: z.boolean(),
47
+ devDependencies: z.undefined({ error: "'init' does not accept --dev-dependencies" }).optional()
48
+ }),
49
+ z.object({ kind: z.literal("version") }),
50
+ z.object({ kind: z.literal("help") })
51
+ ]);
52
+ //#endregion
53
+ //#region src/args/parse.ts
54
+ const removedFlagHints = {
55
+ "--init": "the --init flag has been removed, use 'license-cop init' instead",
56
+ "-D": "the -D flag has been removed, use '--dev-dependencies include' instead",
57
+ "--include-dev": "the --include-dev flag has been removed, use '--dev-dependencies include' instead",
58
+ "--dev-only": "the --dev-only flag has been removed, use '--dev-dependencies only' instead"
59
+ };
60
+ const cliOptions = {
61
+ directory: {
62
+ type: "string",
63
+ short: "d"
64
+ },
65
+ verbose: { type: "boolean" },
66
+ "dev-dependencies": { type: "string" },
67
+ version: {
68
+ type: "boolean",
69
+ short: "v"
70
+ },
71
+ help: {
72
+ type: "boolean",
73
+ short: "h"
74
+ }
75
+ };
76
+ const parseCliArgs = (args, defaultDirectory) => {
77
+ throwIfRemovedFlag(args);
78
+ const tokens = tokenize(args);
79
+ const candidate = toCandidate(tokens, defaultDirectory);
80
+ const result = cliInvocationSchema.safeParse(candidate);
81
+ if (!result.success) throw new UsageError(firstMessage(result.error));
82
+ return result.data;
83
+ };
84
+ const throwIfRemovedFlag = (args) => {
85
+ for (const arg of args) {
86
+ const hint = removedFlagHints[arg];
87
+ if (hint) throw new UsageError(hint);
88
+ }
89
+ };
90
+ const tokenize = (args) => {
91
+ try {
92
+ const { values, positionals } = parseArgs({
93
+ args,
94
+ allowPositionals: true,
95
+ strict: true,
96
+ options: cliOptions
97
+ });
98
+ return {
99
+ values,
100
+ positionals
101
+ };
102
+ } catch (error) {
103
+ throw new UsageError(error instanceof Error ? error.message : String(error));
104
+ }
105
+ };
106
+ /**
107
+ * Decides which command was asked for, once, and lays the flags out for the schema to check. Help
108
+ * and the version win over everything else on the command line.
109
+ */
110
+ const toCandidate = ({ values, positionals }, defaultDirectory) => {
111
+ if (values.help) return { kind: "help" };
112
+ if (values.version) return { kind: "version" };
113
+ const isInit = positionals[0] === "init";
114
+ const unexpected = isInit ? positionals[1] : positionals[0];
115
+ if (unexpected !== void 0) throw new UsageError(`unexpected argument '${unexpected}'`);
116
+ return {
117
+ kind: isInit ? "init" : "check",
118
+ directory: values.directory ?? defaultDirectory,
119
+ verbose: values.verbose ?? false,
120
+ devDependencies: values["dev-dependencies"]
121
+ };
122
+ };
123
+ const firstMessage = (error) => error.issues[0]?.message ?? error.message;
124
+ //#endregion
125
+ //#region src/config/parsers/json5.ts
126
+ const json5Parse = (content) => JSON5.parse(content);
127
+ //#endregion
128
+ //#region src/read-project-name.ts
129
+ const projectPackageJsonSchema = z.object({ name: z.string() });
130
+ /** The name in a project's package.json, for saying what's being scanned. */
131
+ const readProjectName = async (pathToPackageJson, onVerbose) => {
132
+ const contents = await readFile(pathToPackageJson, "utf8").catch(() => {
133
+ onVerbose(`Cannot find the package.json: '${pathToPackageJson}'`);
134
+ throw new PackageJsonError(`Cannot find the file: '${pathToPackageJson}'`);
135
+ });
136
+ let parsed;
137
+ try {
138
+ parsed = json5Parse(contents);
139
+ } catch (error) {
140
+ const reason = error instanceof Error ? error.message : String(error);
141
+ throw new PackageJsonError(`Unable to parse package.json: ${pathToPackageJson}: ${reason}`);
142
+ }
143
+ const result = projectPackageJsonSchema.safeParse(parsed);
144
+ if (!result.success) throw new PackageJsonError(`Unable to parse package.json: ${pathToPackageJson}: ${result.error.message}`);
145
+ return result.data.name;
146
+ };
147
+ //#endregion
148
+ //#region src/config/dev-dependencies.ts
149
+ /**
150
+ * The `--dev-dependencies` flag, when given, decides on its own and the config file's keys are
151
+ * ignored; only when it's absent do the config keys apply. This means the two options are never
152
+ * both true when they come from the flag.
153
+ */
154
+ const resolveDevDependencyOptions = (flag, config) => {
155
+ switch (flag) {
156
+ case void 0: return {
157
+ includeDevDependencies: config.includeDevDependencies,
158
+ devDependenciesOnly: config.devDependenciesOnly
159
+ };
160
+ case "include": return {
161
+ includeDevDependencies: true,
162
+ devDependenciesOnly: false
163
+ };
164
+ case "only": return {
165
+ includeDevDependencies: false,
166
+ devDependenciesOnly: true
167
+ };
168
+ default: throw new Error(`Unknown dev dependencies mode: ${flag}`);
169
+ }
170
+ };
171
+ //#endregion
172
+ //#region src/config/config-error.ts
173
+ var ConfigError = class ConfigError extends LicenseCopError {
174
+ constructor(message) {
175
+ super(`Config error: ${message}`);
176
+ }
177
+ static fromUnknown(e) {
178
+ const message = resolveMessage(e);
179
+ return new ConfigError(message);
180
+ }
181
+ };
182
+ const resolveMessage = (e) => {
183
+ if (e instanceof Error) return e.message;
184
+ if (typeof e === "string") return e;
185
+ return "Unknown error";
186
+ };
187
+ //#endregion
188
+ //#region src/config/config.ts
189
+ const rawConfigParser = z.object({
190
+ extends: z.string().optional(),
191
+ licenses: z.array(z.string()).optional(),
192
+ packages: z.array(z.string()).optional(),
193
+ includeDevDependencies: z.boolean().optional(),
194
+ devDependenciesOnly: z.boolean().optional(),
195
+ directory: z.string().optional()
196
+ });
197
+ /**
198
+ * Validates a config without applying any defaults. Configs must be merged in this form,
199
+ * otherwise a default on the child would override a value that was explicitly set on its parent.
200
+ */
201
+ const parseRawConfig = (config) => {
202
+ const parseResult = rawConfigParser.safeParse(config);
203
+ if (!parseResult.success) throw new ConfigError(parseResult.error.message);
204
+ return parseResult.data;
205
+ };
206
+ const applyConfigDefaults = (config) => ({
207
+ extends: config.extends,
208
+ licenses: config.licenses ?? [],
209
+ packages: config.packages ?? [],
210
+ includeDevDependencies: config.includeDevDependencies ?? false,
211
+ devDependenciesOnly: config.devDependenciesOnly ?? false
212
+ });
213
+ //#endregion
214
+ //#region src/config/find-config.ts
215
+ const moduleNames = [
216
+ "licenses",
217
+ "licences",
218
+ "licensesrc",
219
+ "licencesrc"
220
+ ];
221
+ const generateSearchPlaces = (moduleName) => [
222
+ `.${moduleName}`,
223
+ `.${moduleName}.json`,
224
+ `.${moduleName}.jsonc`,
225
+ `.${moduleName}.json5`,
226
+ `.${moduleName}.yaml`,
227
+ `.${moduleName}.yml`,
228
+ `.${moduleName}.js`,
229
+ `.${moduleName}.cjs`,
230
+ `.config/${moduleName}`,
231
+ `.config/${moduleName}.json`,
232
+ `.config/${moduleName}.jsonc`,
233
+ `.config/${moduleName}.json5`,
234
+ `.config/${moduleName}.yaml`,
235
+ `.config/${moduleName}.yml`,
236
+ `.config/${moduleName}.js`,
237
+ `.config/${moduleName}.cjs`,
238
+ `${moduleName}.config.js`,
239
+ `${moduleName}.config.cjs`
240
+ ];
241
+ const searchPlaces = ["package.json"];
242
+ for (const moduleName of moduleNames) searchPlaces.push(...generateSearchPlaces(moduleName));
243
+ const json5Loader = (_filepath, content) => Promise.resolve(json5Parse(content));
244
+ /**
245
+ * Looks for a config in the directory, loading it the way a normal run would. Resolves to `null`
246
+ * when there isn't one, and throws a `ConfigError` when there is one that can't be loaded. Empty
247
+ * files are skipped, so they don't count as a config.
248
+ */
249
+ const searchConfig = async (rootDir) => {
250
+ const explorer = cosmiconfig("licensecop", {
251
+ stopDir: rootDir,
252
+ searchPlaces,
253
+ loaders: {
254
+ ".json": json5Loader,
255
+ ".jsonc": json5Loader,
256
+ ".json5": json5Loader
257
+ }
258
+ });
259
+ try {
260
+ return await explorer.search(rootDir);
261
+ } catch (e) {
262
+ throw ConfigError.fromUnknown(e);
263
+ }
264
+ };
265
+ const findConfig = async (rootDir) => {
266
+ const result = await searchConfig(rootDir);
267
+ if (!result?.config) throw ConfigError.fromUnknown("No config file found");
268
+ return result.config;
269
+ };
270
+ //#endregion
271
+ //#region src/config/parent-resolutions/github.ts
272
+ const githubResolution = async (repoId, onVerbose) => {
273
+ onVerbose(`Resolving config from GitHub repo: ${repoId}`);
274
+ const parts = repoId.split("/");
275
+ const [owner, repoName] = parts;
276
+ if (parts.length !== 2 || owner === void 0 || repoName === void 0 || owner.length === 0 || repoName.length === 0) throw new ConfigError(`Invalid GitHub repo ID: ${repoId}`);
277
+ try {
278
+ const url = `https://api.github.com/repos/${owner}/${repoName}/contents/.licenses.json`;
279
+ const response = await fetch(url, { headers: {
280
+ accept: "application/vnd.github.raw+json",
281
+ "user-agent": "license-cop"
282
+ } });
283
+ if (!response.ok) throw new Error(`Request failed with status code ${response.status}`);
284
+ const fileContent = await response.text();
285
+ return json5Parse(fileContent);
286
+ } catch (e) {
287
+ throw new ConfigError(`Could not resolve config from GitHub repo: ${repoId}, error: ${e instanceof Error ? e.message : String(e)}`);
288
+ }
289
+ };
290
+ //#endregion
291
+ //#region src/config/parent-resolutions/http.ts
292
+ const httpResolution = async (url, onVerbose) => {
293
+ onVerbose(`Resolving http config: ${url}`);
294
+ const response = await fetch(url);
295
+ if (!response.ok) throw new Error(`Request failed with status code ${response.status}`);
296
+ const body = await response.text();
297
+ return json5Parse(body);
298
+ };
299
+ //#endregion
300
+ //#region src/config/parent-resolutions/npm.ts
301
+ const npmResolution = async (packageName, rootDir, onVerbose) => {
302
+ onVerbose(`Resolving npm package: ${packageName}`);
303
+ const packagePath = getPackagePath(rootDir, packageName);
304
+ return await findConfig(packagePath);
305
+ };
306
+ const nodeModuleExists = async (packageName, rootDir) => {
307
+ const packagePath = getPackagePath(rootDir, packageName);
308
+ try {
309
+ return (await stat$1(packagePath)).isDirectory();
310
+ } catch {
311
+ return false;
312
+ }
313
+ };
314
+ const getPackagePath = (rootDir, packageName) => join$1(rootDir, "node_modules", packageName);
315
+ //#endregion
316
+ //#region src/config/load-parent-config.ts
317
+ const loadParentConfig = async (parentConfigPath, rootDir, onVerbose) => {
318
+ if (parentConfigPath.startsWith("npm:")) {
319
+ const packageName = parentConfigPath.substring(4);
320
+ return await npmResolution(packageName, rootDir, onVerbose);
321
+ }
322
+ if (parentConfigPath.startsWith("github:")) {
323
+ const repo = parentConfigPath.substring(7);
324
+ return await githubResolution(repo, onVerbose);
325
+ }
326
+ if (parentConfigPath.startsWith("http")) return await httpResolution(parentConfigPath, onVerbose);
327
+ if (await nodeModuleExists(parentConfigPath, rootDir)) return await npmResolution(parentConfigPath, rootDir, onVerbose);
328
+ throw new ConfigError(`Invalid parent config location: ${parentConfigPath}`);
329
+ };
330
+ //#endregion
331
+ //#region src/config/load-config.ts
332
+ const loadConfig = async (rootDir, onVerbose) => {
333
+ const foundConfig = await findConfig(rootDir);
334
+ let config = parseRawConfig(foundConfig);
335
+ const visitedParents = /* @__PURE__ */ new Set();
336
+ while (config.extends) {
337
+ const parentLocation = config.extends;
338
+ if (visitedParents.has(parentLocation)) throw new ConfigError(`Circular config extension: ${parentLocation}`);
339
+ visitedParents.add(parentLocation);
340
+ onVerbose(`Extending config with ${parentLocation}`);
341
+ const loadedParentConfig = await loadParentConfig(parentLocation, rootDir, onVerbose);
342
+ const parsedParentConfig = parseRawConfig(loadedParentConfig);
343
+ config = deepMerge(parsedParentConfig, config);
344
+ config.extends = parsedParentConfig.extends;
345
+ }
346
+ return applyConfigDefaults(config);
347
+ };
348
+ //#endregion
349
+ //#region src/config/resolve-check-options.ts
350
+ /**
351
+ * Everything needed to check a project before scanning it: reads the project's name and its config
352
+ * file (with anything it extends) and combines them with what the command line asked for into the
353
+ * options for `checkLicenses`. Nothing is scanned, so it's quick, and it fails on a bad config
354
+ * before a slow scan.
355
+ */
356
+ const resolveCheckOptions = async (directory, input) => {
357
+ const { devDependencies, onVerbose } = input;
358
+ const packageJsonPath = join(directory, "package.json");
359
+ const productName = await readProjectName(packageJsonPath, onVerbose);
360
+ const config = await loadConfig(directory, onVerbose);
361
+ return {
362
+ productName,
363
+ options: {
364
+ allowedLicenses: config.licenses,
365
+ allowedPackages: config.packages,
366
+ workingDirectory: directory,
367
+ ...resolveDevDependencyOptions(devDependencies, config),
368
+ onVerbose
369
+ }
370
+ };
371
+ };
372
+ //#endregion
373
+ //#region src/report-failure.ts
374
+ const reportFailure = (result, io) => {
375
+ const { noLicenses, forbiddenLicenses } = result;
376
+ io.stderr("Found the following issues...\n");
377
+ if (noLicenses.size > 0) logNoLicenseIssues(noLicenses, io);
378
+ if (forbiddenLicenses.size > 0) logForbiddenLicenseIssues(forbiddenLicenses, io);
379
+ };
380
+ const logNoLicenseIssues = (noLicenseResults, io) => {
381
+ io.stderr("Packages with no license:");
382
+ for (const result of noLicenseResults.values()) {
383
+ const { name, version } = result;
384
+ io.stderr(`${name}@${version}`);
385
+ }
386
+ io.stderr("");
387
+ };
388
+ const logForbiddenLicenseIssues = (forbiddenLicensesResults, io) => {
389
+ io.stderr("Packages with forbidden licenses:");
390
+ for (const result of forbiddenLicensesResults.values()) {
391
+ const { name, version, licenseIdentifiers, spdxExpression } = result;
392
+ const licenseDetails = licenseIdentifiers === spdxExpression ? licenseIdentifiers : `${licenseIdentifiers} from ${spdxExpression}`;
393
+ io.stderr(`${name}@${version} - ${licenseDetails}`);
394
+ }
395
+ };
396
+ //#endregion
397
+ //#region src/report-success.ts
398
+ const reportSuccess = (result, io) => {
399
+ const { allowedPackages, allowedLicenses } = result;
400
+ io.stdout("\nDone! No issues found");
401
+ if (allowedPackages.size !== 0) io.stdout(`Found ${allowedPackages.size} allowed packages`);
402
+ if (allowedLicenses.size !== 0) io.stdout(`Found ${allowedLicenses.size} packages with allowed licenses`);
403
+ };
404
+ //#endregion
405
+ //#region src/commands/check.ts
406
+ const runCheck = async (options, io) => {
407
+ const { directory, verbose: verboseEnabled, devDependencies } = options;
408
+ const verbose = createVerboseLogger(io, verboseEnabled);
409
+ verbose(`Using directory: ${directory}`);
410
+ const resolved = await resolveCheckOptions(directory, {
411
+ devDependencies,
412
+ onVerbose: verbose
413
+ });
414
+ io.stdout(`Scanning dependencies of: ${resolved.productName}`);
415
+ const result = await checkLicenses(resolved.options);
416
+ const exitCode = result.noLicenses.size > 0 || result.forbiddenLicenses.size > 0 ? 1 : 0;
417
+ if (exitCode === 0) reportSuccess(result, io);
418
+ else reportFailure(result, io);
419
+ verbose(`Exiting with code ${exitCode}`);
420
+ return exitCode;
421
+ };
422
+ //#endregion
423
+ //#region src/commands/init.ts
424
+ const defaultConfig = `{
425
+ "$schema": "https://license-cop.js.org/schema.json",
426
+ "licenses": ["MIT", "ISC", "Apache-2.0"],
427
+ "packages": []
428
+ }
429
+ `;
430
+ const runInit = async (options, io) => {
431
+ const { directory, verbose: verboseEnabled } = options;
432
+ const verbose = createVerboseLogger(io, verboseEnabled);
433
+ if (!await stat(directory).then((stats) => stats.isDirectory(), () => false)) {
434
+ io.stderr(`Cannot set up a config file: ${directory} isn't a directory that exists`);
435
+ return 1;
436
+ }
437
+ const existingConfig = await searchConfig(directory);
438
+ if (existingConfig) throw new ConfigError(`this project already has a config file: ${existingConfig.filepath}`);
439
+ io.stdout("Setting up a new license-cop config file...");
440
+ const configPath = join(directory, ".licenses.json");
441
+ verbose(`Writing config file to: ${configPath}`);
442
+ await writeFile(configPath, defaultConfig);
443
+ io.stdout("Done!");
444
+ io.stdout("Run `npx license-cop` to check your dependencies");
445
+ return 0;
446
+ };
447
+ //#endregion
448
+ //#region src/help.ts
449
+ const helpText = () => `Usage: license-cop [options] [command]
450
+
451
+ Yet another license checker tool for your dependencies; focused on simplicity
452
+
453
+ Options:
454
+ --dev-dependencies <mode> Which dev dependencies to check: 'include' checks them as well as
455
+ production dependencies, 'only' checks only them. Defaults to production
456
+ dependencies only.
457
+ --verbose Enable verbose logging
458
+ -d, --directory <dir> The directory of the project. Defaults to the current working directory.
459
+ -v, --version Print the installed version of license-cop
460
+ -h, --help Display this help
461
+
462
+ Commands:
463
+ init Create a new license-cop configuration file`;
464
+ const versionText = async () => {
465
+ const packageJsonLocation = fileURLToPath(new URL("../package.json", import.meta.url));
466
+ const contents = await readFile(packageJsonLocation, "utf8");
467
+ const { version } = ownPackageJsonSchema.parse(JSON.parse(contents));
468
+ return `v${version}`;
469
+ };
470
+ const ownPackageJsonSchema = z.object({ version: z.string() });
471
+ //#endregion
472
+ //#region src/report-error.ts
473
+ /**
474
+ * Turns anything a command threw into a message and an exit code, so that `run` never throws. What
475
+ * the user did wrong reads as a sentence; only what nobody expected shows a stack, and only when
476
+ * asked to, since a stack trace is no help to someone who just mistyped a directory.
477
+ */
478
+ const reportError = (error, io, verbose) => {
479
+ if (error instanceof UsageError) {
480
+ io.stderr(`error: ${error.message}`);
481
+ io.stderr("Run 'license-cop --help' for usage");
482
+ return 1;
483
+ }
484
+ if (error instanceof LicenseCopError) {
485
+ io.stderr(error.message);
486
+ return 1;
487
+ }
488
+ io.stderr(`error: ${describeUnexpectedError(error)}`);
489
+ if (!verbose) {
490
+ io.stderr("Run again with --verbose to see the full stack trace");
491
+ return 1;
492
+ }
493
+ if (error instanceof Error && error.stack) io.stderr(error.stack);
494
+ return 1;
495
+ };
496
+ const fileSystemProblems = {
497
+ ENOENT: "no such file or directory",
498
+ EACCES: "permission denied",
499
+ EPERM: "operation not permitted",
500
+ EISDIR: "is a directory",
501
+ EROFS: "read-only file system"
502
+ };
503
+ const describeUnexpectedError = (error) => {
504
+ if (!(error instanceof Error)) return String(error);
505
+ const { code, path } = error;
506
+ const problem = typeof code === "string" ? fileSystemProblems[code] : void 0;
507
+ if (problem && typeof path === "string") return `${problem}: ${path}`;
508
+ return error.message;
509
+ };
510
+ //#endregion
511
+ //#region src/main.ts
512
+ const run = async (args, io = defaultIo, cwd = process.cwd()) => {
513
+ let verbose = false;
514
+ try {
515
+ const invocation = parseCliArgs(args, cwd);
516
+ verbose = "verbose" in invocation && invocation.verbose;
517
+ return await execute(invocation, io);
518
+ } catch (error) {
519
+ return reportError(error, io, verbose);
520
+ }
521
+ };
522
+ const execute = async (invocation, io) => {
523
+ switch (invocation.kind) {
524
+ case "check": return await runCheck(invocation, io);
525
+ case "init": return await runInit(invocation, io);
526
+ case "version": {
527
+ const text = await versionText();
528
+ io.stdout(text);
529
+ return 0;
530
+ }
531
+ case "help":
532
+ io.stdout(helpText());
533
+ return 0;
534
+ default: throw new Error(`Unknown invocation: ${JSON.stringify(invocation)}`);
535
+ }
536
+ };
537
+ //#endregion
538
+ //#region src/bin.ts
539
+ run(process.argv.slice(2), defaultIo).then((exitCode) => {
540
+ process.exitCode = exitCode;
541
+ });
542
+ //#endregion
543
+ export {};
544
+
545
+ //# sourceMappingURL=bin.js.map