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.
- package/LICENSE.md +1 -1
- package/README.md +25 -5
- package/dist/bin.js +545 -0
- package/dist/bin.js.map +1 -0
- package/package.json +22 -15
- package/src/bin/license-cop +0 -4
- package/src/index.d.ts +0 -3
- package/src/index.js +0 -10
- package/src/index.js.map +0 -1
- package/src/lib/cli/commands/init.d.ts +0 -5
- package/src/lib/cli/commands/init.js +0 -39
- package/src/lib/cli/commands/init.js.map +0 -1
- package/src/lib/cli/commands/main.d.ts +0 -7
- package/src/lib/cli/commands/main.js +0 -61
- package/src/lib/cli/commands/main.js.map +0 -1
- package/src/lib/cli/commands/version.d.ts +0 -4
- package/src/lib/cli/commands/version.js +0 -21
- package/src/lib/cli/commands/version.js.map +0 -1
- package/src/lib/cli/create-command.d.ts +0 -5
- package/src/lib/cli/create-command.js +0 -17
- package/src/lib/cli/create-command.js.map +0 -1
- package/src/lib/cli/index.d.ts +0 -1
- package/src/lib/cli/index.js +0 -18
- package/src/lib/cli/index.js.map +0 -1
- package/src/lib/cli/report-failure.d.ts +0 -2
- package/src/lib/cli/report-failure.js +0 -36
- package/src/lib/cli/report-failure.js.map +0 -1
- package/src/lib/cli/report-success.d.ts +0 -2
- package/src/lib/cli/report-success.js +0 -22
- package/src/lib/cli/report-success.js.map +0 -1
- package/src/lib/config/config-error.d.ts +0 -4
- package/src/lib/config/config-error.js +0 -27
- package/src/lib/config/config-error.js.map +0 -1
- package/src/lib/config/config.d.ts +0 -39
- package/src/lib/config/config.js +0 -35
- package/src/lib/config/config.js.map +0 -1
- package/src/lib/config/find-config.d.ts +0 -1
- package/src/lib/config/find-config.js +0 -67
- package/src/lib/config/find-config.js.map +0 -1
- package/src/lib/config/load-config.d.ts +0 -7
- package/src/lib/config/load-config.js +0 -26
- package/src/lib/config/load-config.js.map +0 -1
- package/src/lib/config/load-parent-config.d.ts +0 -1
- package/src/lib/config/load-parent-config.js +0 -30
- package/src/lib/config/load-parent-config.js.map +0 -1
- package/src/lib/config/parent-resolutions/github.d.ts +0 -1
- package/src/lib/config/parent-resolutions/github.js +0 -28
- package/src/lib/config/parent-resolutions/github.js.map +0 -1
- package/src/lib/config/parent-resolutions/http.d.ts +0 -1
- package/src/lib/config/parent-resolutions/http.js +0 -20
- package/src/lib/config/parent-resolutions/http.js.map +0 -1
- package/src/lib/config/parent-resolutions/npm.d.ts +0 -2
- package/src/lib/config/parent-resolutions/npm.js +0 -32
- package/src/lib/config/parent-resolutions/npm.js.map +0 -1
- package/src/lib/config/parsers/json5.d.ts +0 -1
- package/src/lib/config/parsers/json5.js +0 -11
- package/src/lib/config/parsers/json5.js.map +0 -1
- package/src/lib/dependency/get-package-manager.d.ts +0 -2
- package/src/lib/dependency/get-package-manager.js +0 -52
- package/src/lib/dependency/get-package-manager.js.map +0 -1
- package/src/lib/dependency/package-json.d.ts +0 -54
- package/src/lib/dependency/package-json.js +0 -75
- package/src/lib/dependency/package-json.js.map +0 -1
- package/src/lib/dependency/package-rules.d.ts +0 -1
- package/src/lib/dependency/package-rules.js +0 -24
- package/src/lib/dependency/package-rules.js.map +0 -1
- package/src/lib/dependency-scanning/npm.d.ts +0 -3
- package/src/lib/dependency-scanning/npm.js +0 -100
- package/src/lib/dependency-scanning/npm.js.map +0 -1
- package/src/lib/dependency-scanning/options.d.ts +0 -2
- package/src/lib/dependency-scanning/options.js +0 -3
- package/src/lib/dependency-scanning/options.js.map +0 -1
- package/src/lib/dependency-scanning/pnpm.d.ts +0 -3
- package/src/lib/dependency-scanning/pnpm.js +0 -103
- package/src/lib/dependency-scanning/pnpm.js.map +0 -1
- package/src/lib/license-cop.d.ts +0 -9
- package/src/lib/license-cop.js +0 -52
- package/src/lib/license-cop.js.map +0 -1
- package/src/lib/logger.d.ts +0 -8
- package/src/lib/logger.js +0 -41
- package/src/lib/logger.js.map +0 -1
- package/src/lib/result.d.ts +0 -27
- package/src/lib/result.js +0 -3
- package/src/lib/result.js.map +0 -1
- package/src/lib/spdx/calculate-issues.d.ts +0 -2
- package/src/lib/spdx/calculate-issues.js +0 -45
- package/src/lib/spdx/calculate-issues.js.map +0 -1
- package/src/lib/spdx/get-tokens.d.ts +0 -2
- package/src/lib/spdx/get-tokens.js +0 -54
- package/src/lib/spdx/get-tokens.js.map +0 -1
- package/src/lib/spdx/parse-license-expression.d.ts +0 -2
- package/src/lib/spdx/parse-license-expression.js +0 -20
- package/src/lib/spdx/parse-license-expression.js.map +0 -1
- package/src/lib/spdx/parse-tokens.d.ts +0 -3
- package/src/lib/spdx/parse-tokens.js +0 -122
- package/src/lib/spdx/parse-tokens.js.map +0 -1
- package/src/lib/spdx/types/spdx-expression.d.ts +0 -20
- package/src/lib/spdx/types/spdx-expression.js +0 -3
- package/src/lib/spdx/types/spdx-expression.js.map +0 -1
- package/src/lib/spdx/types/token.d.ts +0 -23
- package/src/lib/spdx/types/token.js +0 -29
- package/src/lib/spdx/types/token.js.map +0 -1
- package/src/lib/utils/join-string-array.d.ts +0 -1
- package/src/lib/utils/join-string-array.js +0 -28
- package/src/lib/utils/join-string-array.js.map +0 -1
package/LICENSE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copyright (c) 2023-
|
|
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
|
|
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@
|
|
152
|
+
- uses: actions/checkout@v7
|
|
133
153
|
|
|
134
154
|
- name: Setup Node.js
|
|
135
|
-
uses: actions/setup-node@
|
|
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
|