@callstack/licenses 0.3.2 → 1.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/README.md +4 -4
- package/build/constants/licenses.d.ts +4 -0
- package/build/constants/licenses.js +15 -15
- package/build/licenses/OrPolicy.d.ts +21 -0
- package/build/licenses/OrPolicy.js +16 -0
- package/build/licenses/index.d.ts +3 -0
- package/build/licenses/index.js +3 -0
- package/build/licenses/licenseAnalysis.d.ts +7 -8
- package/build/licenses/licenseAnalysis.js +15 -29
- package/build/licenses/licenseClassification.d.ts +52 -0
- package/build/licenses/licenseClassification.js +136 -0
- package/build/licenses/licenseExpression.d.ts +37 -0
- package/build/licenses/licenseExpression.js +166 -0
- package/build/node/common.d.ts +23 -2
- package/build/node/common.js +159 -56
- package/build/node/index.d.ts +1 -0
- package/build/node/index.js +1 -0
- package/build/node/mergeLicensesMappings.d.ts +8 -0
- package/build/node/mergeLicensesMappings.js +28 -0
- package/build/node/utils/aboutLibrariesLicenses.d.ts +11 -0
- package/build/node/utils/aboutLibrariesLicenses.js +58 -0
- package/build/node/utils/licenseFiles.d.ts +8 -0
- package/build/node/utils/licenseFiles.js +45 -0
- package/build/node/utils/licensePlistBody.d.ts +6 -0
- package/build/node/utils/licensePlistBody.js +20 -0
- package/build/node/utils/packageUtils.d.ts +14 -5
- package/build/node/utils/packageUtils.js +35 -23
- package/build/types/AboutLibrariesLikePackageInfo.d.ts +1 -1
- package/build/types/License.d.ts +14 -6
- package/build/types/LicenseAnalysisResult.d.ts +7 -0
- package/build/types/LicenseExpression.d.ts +39 -0
- package/build/types/LicenseExpression.js +2 -0
- package/build/types/LicenseFile.d.ts +8 -0
- package/build/types/LicenseFile.js +2 -0
- package/build/types/LicenseStats.d.ts +1 -1
- package/build/types/ScanDependenciesOptions.d.ts +12 -0
- package/build/types/ScanDependenciesOptions.js +2 -0
- package/build/types/index.d.ts +3 -0
- package/build/types/index.js +3 -0
- package/package.json +9 -6
- package/src/constants/licenses.ts +15 -15
- package/src/licenses/OrPolicy.ts +23 -0
- package/src/licenses/index.ts +3 -0
- package/src/licenses/licenseAnalysis.ts +19 -33
- package/src/licenses/licenseClassification.ts +173 -0
- package/src/licenses/licenseExpression.ts +200 -0
- package/src/node/common.ts +217 -66
- package/src/node/index.ts +1 -0
- package/src/node/mergeLicensesMappings.ts +32 -0
- package/src/node/utils/aboutLibrariesLicenses.ts +69 -0
- package/src/node/utils/licenseFiles.ts +50 -0
- package/src/node/utils/licensePlistBody.ts +18 -0
- package/src/node/utils/packageUtils.ts +37 -26
- package/src/types/AboutLibrariesLikePackageInfo.ts +1 -1
- package/src/types/License.ts +16 -7
- package/src/types/LicenseAnalysisResult.ts +8 -0
- package/src/types/LicenseExpression.ts +22 -0
- package/src/types/LicenseFile.ts +10 -0
- package/src/types/LicenseStats.ts +1 -1
- package/src/types/ScanDependenciesOptions.ts +12 -0
- package/src/types/index.ts +3 -0
|
@@ -3,40 +3,56 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.legacyDefaultScanPackageOptionsFactory = void 0;
|
|
6
|
+
exports.legacyDefaultScanPackageOptionsFactory = exports.readLicenseFiles = exports.buildLicensePlistBody = exports.prepareAboutLibrariesLicenses = exports.prepareAboutLibrariesLicenseField = void 0;
|
|
7
7
|
exports.getPackageJsonPath = getPackageJsonPath;
|
|
8
8
|
exports.resolvePackageJsonFromEntry = resolvePackageJsonFromEntry;
|
|
9
9
|
exports.findPackageRoot = findPackageRoot;
|
|
10
10
|
exports.normalizePackageName = normalizePackageName;
|
|
11
|
-
exports.prepareAboutLibrariesLicenseField = prepareAboutLibrariesLicenseField;
|
|
12
11
|
exports.parseAuthorField = parseAuthorField;
|
|
13
12
|
exports.parseLicenseField = parseLicenseField;
|
|
14
13
|
exports.parseRepositoryFieldToUrl = parseRepositoryFieldToUrl;
|
|
15
14
|
const fs_1 = __importDefault(require("fs"));
|
|
16
15
|
const path_1 = __importDefault(require("path"));
|
|
17
16
|
const repositoryUtils_1 = require("../../utils/repositoryUtils");
|
|
18
|
-
|
|
19
|
-
function
|
|
17
|
+
var aboutLibrariesLicenses_1 = require("./aboutLibrariesLicenses");
|
|
18
|
+
Object.defineProperty(exports, "prepareAboutLibrariesLicenseField", { enumerable: true, get: function () { return aboutLibrariesLicenses_1.prepareAboutLibrariesLicenseField; } });
|
|
19
|
+
Object.defineProperty(exports, "prepareAboutLibrariesLicenses", { enumerable: true, get: function () { return aboutLibrariesLicenses_1.prepareAboutLibrariesLicenses; } });
|
|
20
|
+
var licensePlistBody_1 = require("./licensePlistBody");
|
|
21
|
+
Object.defineProperty(exports, "buildLicensePlistBody", { enumerable: true, get: function () { return licensePlistBody_1.buildLicensePlistBody; } });
|
|
22
|
+
var licenseFiles_1 = require("./licenseFiles");
|
|
23
|
+
Object.defineProperty(exports, "readLicenseFiles", { enumerable: true, get: function () { return licenseFiles_1.readLicenseFiles; } });
|
|
24
|
+
function getPackageJsonPath(dependency, root, projectRoot = process.cwd()) {
|
|
20
25
|
const rootsToSearch = [
|
|
21
26
|
...(root ? [root] : []), // provided for purpose of nested node_modules resolution inside a package dir, in a subdirectory inside the root node_modules dir
|
|
22
|
-
|
|
27
|
+
projectRoot, // fallback - root node_modules directory home
|
|
23
28
|
]; // in order of priority (left-to-right)
|
|
24
29
|
try {
|
|
25
30
|
return require.resolve(`${dependency}/package.json`, { paths: rootsToSearch });
|
|
26
31
|
}
|
|
27
32
|
catch (error) {
|
|
28
33
|
for (const root of rootsToSearch) {
|
|
29
|
-
const pkgJsonInNodeModules =
|
|
30
|
-
if (
|
|
34
|
+
const pkgJsonInNodeModules = findPackageJsonInNodeModules(dependency, root);
|
|
35
|
+
if (pkgJsonInNodeModules) {
|
|
31
36
|
return pkgJsonInNodeModules;
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
|
-
return resolvePackageJsonFromEntry(dependency); // final fallback
|
|
39
|
+
return resolvePackageJsonFromEntry(dependency, rootsToSearch); // final fallback
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
|
-
function
|
|
42
|
+
function findPackageJsonInNodeModules(dependency, startDir) {
|
|
43
|
+
for (let currentDir = startDir;; currentDir = path_1.default.dirname(currentDir)) {
|
|
44
|
+
if (path_1.default.basename(currentDir) !== 'node_modules') {
|
|
45
|
+
const pkgJsonInNodeModules = path_1.default.join(currentDir, 'node_modules', dependency, 'package.json');
|
|
46
|
+
if (fs_1.default.existsSync(pkgJsonInNodeModules))
|
|
47
|
+
return fs_1.default.realpathSync(pkgJsonInNodeModules);
|
|
48
|
+
}
|
|
49
|
+
if (currentDir === path_1.default.dirname(currentDir))
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function resolvePackageJsonFromEntry(dependency, paths) {
|
|
38
54
|
try {
|
|
39
|
-
const entryPath = require.resolve(dependency);
|
|
55
|
+
const entryPath = require.resolve(dependency, paths ? { paths } : undefined);
|
|
40
56
|
const packageDir = findPackageRoot(entryPath);
|
|
41
57
|
if (!packageDir)
|
|
42
58
|
return null;
|
|
@@ -58,18 +74,6 @@ function findPackageRoot(entryPath) {
|
|
|
58
74
|
function normalizePackageName(packageName) {
|
|
59
75
|
return packageName.replace('/', '_');
|
|
60
76
|
}
|
|
61
|
-
function prepareAboutLibrariesLicenseField(license) {
|
|
62
|
-
if (!license.type) {
|
|
63
|
-
return '';
|
|
64
|
-
}
|
|
65
|
-
// The returned value is used as a filename under `android/config/licenses/`.
|
|
66
|
-
// Legacy/compound SPDX expressions like `MIT/X11` or `(MIT OR Apache-2.0)` would
|
|
67
|
-
// otherwise produce paths containing `/`, `(` or spaces, which causes the writer
|
|
68
|
-
// to either fail with ENOENT (when a `/` is interpreted as a subdirectory) or
|
|
69
|
-
// to produce names that are invalid on some filesystems.
|
|
70
|
-
const sanitizedType = license.type.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
71
|
-
return `${sanitizedType}_${(0, miscUtils_1.sha512)(license.content ?? sanitizedType)}`;
|
|
72
|
-
}
|
|
73
77
|
function parseAuthorField(json) {
|
|
74
78
|
if (typeof json.author === 'object' && typeof json.author.name === 'string') {
|
|
75
79
|
return json.author.name;
|
|
@@ -78,13 +82,21 @@ function parseAuthorField(json) {
|
|
|
78
82
|
return json.author;
|
|
79
83
|
}
|
|
80
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Reads the Raw License from package.json: the `license` string, the legacy `license: { type }` object,
|
|
87
|
+
* or the legacy `licenses: [{ type }, …]` array, whose types are joined with ` OR `.
|
|
88
|
+
*/
|
|
81
89
|
function parseLicenseField(json) {
|
|
82
|
-
if (typeof json.license === 'object' && typeof json.license
|
|
90
|
+
if (typeof json.license === 'object' && typeof json.license?.type === 'string') {
|
|
83
91
|
return json.license.type;
|
|
84
92
|
}
|
|
85
93
|
if (typeof json.license === 'string') {
|
|
86
94
|
return json.license;
|
|
87
95
|
}
|
|
96
|
+
if (Array.isArray(json.licenses)) {
|
|
97
|
+
const types = json.licenses.flatMap((entry) => typeof entry?.type === 'string' && entry.type.trim() !== '' ? [entry.type] : []);
|
|
98
|
+
return types.length > 0 ? types.join(' OR ') : undefined;
|
|
99
|
+
}
|
|
88
100
|
}
|
|
89
101
|
function parseRepositoryFieldToUrl(json) {
|
|
90
102
|
if (typeof json.repository === 'object' && typeof json.repository.url === 'string') {
|
|
@@ -3,5 +3,5 @@ import type { AboutLibrariesLicenseJsonPayload } from './AboutLibrariesLicenseJs
|
|
|
3
3
|
export type AboutLibrariesLikePackageInfo = {
|
|
4
4
|
normalizedPackageNameWithVersion: string;
|
|
5
5
|
libraryJsonPayload: AboutLibrariesLibraryJsonPayload;
|
|
6
|
-
|
|
6
|
+
licenseJsonPayloads: AboutLibrariesLicenseJsonPayload[];
|
|
7
7
|
};
|
package/build/types/License.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { DependencyType } from './DependencyType';
|
|
2
|
+
import type { LicenseExpression } from './LicenseExpression';
|
|
3
|
+
import type { LicenseFile } from './LicenseFile';
|
|
2
4
|
import type { ParentPackageInfo } from './ParentPackageInfo';
|
|
3
5
|
import type { ScanPackageCallContext } from './ScanPackageCallContext';
|
|
4
6
|
export type License = {
|
|
@@ -6,14 +8,20 @@ export type License = {
|
|
|
6
8
|
name: string;
|
|
7
9
|
/** Package author */
|
|
8
10
|
author?: string;
|
|
9
|
-
/** Package license contents */
|
|
10
|
-
content?: string;
|
|
11
11
|
/** Package description */
|
|
12
12
|
description?: string;
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Raw License: the license declaration exactly as written in package.json;
|
|
15
|
+
* for the legacy `licenses: [{ type }]` array, the types joined with ` OR `;
|
|
16
|
+
* `null` if the package declares no license
|
|
17
|
+
*/
|
|
18
|
+
rawLicense: string | null;
|
|
19
|
+
/** Parsed License Expression of {@link License.rawLicense} */
|
|
20
|
+
license: LicenseExpression;
|
|
21
|
+
/** Canonical SPDX License Identifiers found in {@link License.license}; empty for an Unknown License */
|
|
22
|
+
licenseIds: string[];
|
|
23
|
+
/** Every license file found in the package root, each linked to its License Identifier where unambiguous */
|
|
24
|
+
licenseFiles: LicenseFile[];
|
|
17
25
|
/** Package repository URL */
|
|
18
26
|
url?: string;
|
|
19
27
|
/** The resolved version that was actually installed */
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { LicenseCategory } from '../licenses/LicenseCategory';
|
|
2
2
|
import type { LicenseStats } from './LicenseStats';
|
|
3
3
|
export interface LicenseAnalysisResult extends LicenseStats {
|
|
4
|
+
/**
|
|
5
|
+
* Mapping of package key (`<package-name>@<package-version>`) to the License Category of that package
|
|
6
|
+
*/
|
|
4
7
|
categorizedLicenses: Record<string, LicenseCategory>;
|
|
8
|
+
/**
|
|
9
|
+
* Mapping of rendered License Expression to its License Category; has the same keys as {@link LicenseStats.byLicense}
|
|
10
|
+
*/
|
|
11
|
+
categoryByLicense: Record<string, LicenseCategory>;
|
|
5
12
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured meaning of a package's Raw License, following the SPDX License Expression
|
|
3
|
+
* syntax (SPDX spec v3.0.1, Annex B).
|
|
4
|
+
*
|
|
5
|
+
* The tree is binary: `A OR B OR C` is represented as `A OR (B OR C)`.
|
|
6
|
+
*
|
|
7
|
+
* @see https://spdx.github.io/spdx-spec/v3.0.1/annexes/spdx-license-expressions/
|
|
8
|
+
*/
|
|
9
|
+
export type LicenseExpression =
|
|
10
|
+
/**
|
|
11
|
+
* A single license, identified by its SPDX License Identifier (e.g. `MIT`).
|
|
12
|
+
* - `id` is canonical: deprecated identifiers are upgraded and `+` is absorbed into `-or-later` when such an identifier exists
|
|
13
|
+
* - `declaredId` and `plus` record what was written
|
|
14
|
+
* - `exception` is the License Exception attached with `WITH`
|
|
15
|
+
*/
|
|
16
|
+
{
|
|
17
|
+
kind: 'license';
|
|
18
|
+
id: string;
|
|
19
|
+
declaredId: string;
|
|
20
|
+
plus?: boolean;
|
|
21
|
+
exception?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Dual License: the licensee may comply with either operand */
|
|
24
|
+
| {
|
|
25
|
+
kind: 'or';
|
|
26
|
+
left: LicenseExpression;
|
|
27
|
+
right: LicenseExpression;
|
|
28
|
+
}
|
|
29
|
+
/** Both operands apply and must be complied with simultaneously */
|
|
30
|
+
| {
|
|
31
|
+
kind: 'and';
|
|
32
|
+
left: LicenseExpression;
|
|
33
|
+
right: LicenseExpression;
|
|
34
|
+
}
|
|
35
|
+
/** Unknown License: missing, unparseable, or not a license grant; `raw` keeps the original value */
|
|
36
|
+
| {
|
|
37
|
+
kind: 'unknown';
|
|
38
|
+
raw: string | null;
|
|
39
|
+
};
|
|
@@ -9,7 +9,7 @@ export interface LicenseStats {
|
|
|
9
9
|
*/
|
|
10
10
|
byCategory: Record<LicenseCategory, number>;
|
|
11
11
|
/**
|
|
12
|
-
* Mapping of
|
|
12
|
+
* Mapping of rendered License Expression to count of packages with that expression
|
|
13
13
|
*/
|
|
14
14
|
byLicense: Record<string, number>;
|
|
15
15
|
/**
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Additional options for scanning dependencies of a project
|
|
3
|
+
*/
|
|
4
|
+
export type ScanDependenciesOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* Paths to other project directories whose dependencies are scanned too,
|
|
7
|
+
* e.g. monorepo workspaces the app does not depend on;
|
|
8
|
+
* relative paths are resolved against the real directory (with symlinks resolved) of the scanned `package.json`;
|
|
9
|
+
* each of them must be a directory with a `package.json`, otherwise an error is thrown
|
|
10
|
+
*/
|
|
11
|
+
additionalProjectRoots?: readonly string[];
|
|
12
|
+
};
|
package/build/types/index.d.ts
CHANGED
|
@@ -5,8 +5,11 @@ export * from './AboutLibrariesLicenseJsonPayload';
|
|
|
5
5
|
export * from './LicensePlistPayload';
|
|
6
6
|
export * from './AboutLibrariesLikePackageInfo';
|
|
7
7
|
export * from './ScanPackageOptions';
|
|
8
|
+
export * from './ScanDependenciesOptions';
|
|
8
9
|
export * from './ScanPackageCallContext';
|
|
9
10
|
export * from './DependencyType';
|
|
10
11
|
export * from './ParentPackageInfo';
|
|
11
12
|
export * from './LicenseAnalysisResult';
|
|
12
13
|
export * from './LicenseStats';
|
|
14
|
+
export * from './LicenseExpression';
|
|
15
|
+
export * from './LicenseFile';
|
package/build/types/index.js
CHANGED
|
@@ -21,8 +21,11 @@ __exportStar(require("./AboutLibrariesLicenseJsonPayload"), exports);
|
|
|
21
21
|
__exportStar(require("./LicensePlistPayload"), exports);
|
|
22
22
|
__exportStar(require("./AboutLibrariesLikePackageInfo"), exports);
|
|
23
23
|
__exportStar(require("./ScanPackageOptions"), exports);
|
|
24
|
+
__exportStar(require("./ScanDependenciesOptions"), exports);
|
|
24
25
|
__exportStar(require("./ScanPackageCallContext"), exports);
|
|
25
26
|
__exportStar(require("./DependencyType"), exports);
|
|
26
27
|
__exportStar(require("./ParentPackageInfo"), exports);
|
|
27
28
|
__exportStar(require("./LicenseAnalysisResult"), exports);
|
|
28
29
|
__exportStar(require("./LicenseStats"), exports);
|
|
30
|
+
__exportStar(require("./LicenseExpression"), exports);
|
|
31
|
+
__exportStar(require("./LicenseFile"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@callstack/licenses",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "API package that exposes functionalities for programmatic scanning of licenses in Node.js projects.",
|
|
5
5
|
"browser": "./build/index.js",
|
|
6
6
|
"main": "./build/node/index.js",
|
|
@@ -64,14 +64,17 @@
|
|
|
64
64
|
"registry": "https://registry.npmjs.org/"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"glob": "^
|
|
67
|
+
"glob": "^13.0.0",
|
|
68
|
+
"spdx-exceptions": "^2.1.0",
|
|
69
|
+
"spdx-expression-parse": "^5.0.0",
|
|
70
|
+
"spdx-license-ids": "^3.0.24"
|
|
68
71
|
},
|
|
69
72
|
"devDependencies": {
|
|
70
|
-
"@babel/core": "7.
|
|
71
|
-
"@babel/preset-env": "7.
|
|
72
|
-
"@babel/preset-typescript": "7.
|
|
73
|
-
"@types/glob": "^8.1.0",
|
|
73
|
+
"@babel/core": "^7.29.7",
|
|
74
|
+
"@babel/preset-env": "^7.29.7",
|
|
75
|
+
"@babel/preset-typescript": "^7.29.7",
|
|
74
76
|
"@types/jest": "^29.5.5",
|
|
77
|
+
"@types/spdx-expression-parse": "^4.0.0",
|
|
75
78
|
"babel-jest": "^29.7.0",
|
|
76
79
|
"jest": "^29.7.0",
|
|
77
80
|
"rimraf": "^6.0.1",
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* https://spdx.org/licenses
|
|
3
|
+
*
|
|
4
|
+
* Canonical License Identifiers, as produced by parsing a License Expression: deprecated identifiers are upgraded
|
|
5
|
+
* (`GPL-2.0` becomes `GPL-2.0-only`) and `+` is absorbed (`GPL-2.0+` becomes `GPL-2.0-or-later`), so neither form is listed.
|
|
6
|
+
* The exception are deprecated identifiers with a built-in License Exception, which have no `-only` form.
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
9
|
export const STRONG_COPYLEFT_LICENSES = new Set([
|
|
6
|
-
'GPL',
|
|
7
|
-
'GPL-1.0',
|
|
8
|
-
'GPL-1.0+',
|
|
9
|
-
'GPL-2.0',
|
|
10
|
-
'GPL-2.0+',
|
|
10
|
+
'GPL-1.0-only',
|
|
11
|
+
'GPL-1.0-or-later',
|
|
11
12
|
'GPL-2.0-only',
|
|
12
13
|
'GPL-2.0-or-later',
|
|
13
|
-
'GPL-
|
|
14
|
-
'GPL-
|
|
14
|
+
'GPL-2.0-with-autoconf-exception',
|
|
15
|
+
'GPL-2.0-with-bison-exception',
|
|
16
|
+
'GPL-2.0-with-classpath-exception',
|
|
17
|
+
'GPL-2.0-with-font-exception',
|
|
18
|
+
'GPL-2.0-with-GCC-exception',
|
|
19
|
+
'GPL-3.0-with-autoconf-exception',
|
|
20
|
+
'GPL-3.0-with-GCC-exception',
|
|
15
21
|
'GPL-3.0-only',
|
|
16
22
|
'GPL-3.0-or-later',
|
|
17
|
-
'AGPL-
|
|
23
|
+
'AGPL-1.0-only',
|
|
24
|
+
'AGPL-1.0-or-later',
|
|
18
25
|
'AGPL-3.0-only',
|
|
19
26
|
'AGPL-3.0-or-later',
|
|
20
27
|
'EUPL-1.0',
|
|
@@ -32,17 +39,10 @@ export const WEAK_COPYLEFT_LICENSES = new Set([
|
|
|
32
39
|
'CDDL-1.1',
|
|
33
40
|
'EPL-1.0',
|
|
34
41
|
'EPL-2.0',
|
|
35
|
-
'LGPL',
|
|
36
|
-
'LGPL-2.0',
|
|
37
|
-
'LGPL-2.0+',
|
|
38
42
|
'LGPL-2.0-only',
|
|
39
43
|
'LGPL-2.0-or-later',
|
|
40
|
-
'LGPL-2.1',
|
|
41
|
-
'LGPL-2.1+',
|
|
42
44
|
'LGPL-2.1-only',
|
|
43
45
|
'LGPL-2.1-or-later',
|
|
44
|
-
'LGPL-3.0',
|
|
45
|
-
'LGPL-3.0+',
|
|
46
46
|
'LGPL-3.0-only',
|
|
47
47
|
'LGPL-3.0-or-later',
|
|
48
48
|
'MPL-1.1',
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enumerates all valid OR Policy values
|
|
3
|
+
*
|
|
4
|
+
* @see {@link OrPolicy}
|
|
5
|
+
*/
|
|
6
|
+
export const validOrPolicies = ['most-restrictive', 'least-restrictive'] as const;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Rule for which operand of a Dual License (`A OR B`) determines its License Category:
|
|
10
|
+
* - 'most-restrictive' (the most restrictive operand)
|
|
11
|
+
* - 'least-restrictive' (the least restrictive operand; matches SPDX's "choice" meaning of OR)
|
|
12
|
+
*
|
|
13
|
+
* Never applies to AND, which always takes the most restrictive operand.
|
|
14
|
+
*/
|
|
15
|
+
export type OrPolicy = (typeof validOrPolicies)[number];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* OR Policy used when none is given: a Dual License takes its most restrictive operand,
|
|
19
|
+
* as a conservative default for compliance checks.
|
|
20
|
+
*
|
|
21
|
+
* @see {@link OrPolicy}
|
|
22
|
+
*/
|
|
23
|
+
export const DEFAULT_OR_POLICY: OrPolicy = 'most-restrictive';
|
package/src/licenses/index.ts
CHANGED
|
@@ -1,40 +1,27 @@
|
|
|
1
|
-
import { STRONG_COPYLEFT_LICENSES_LOWERCASE, WEAK_COPYLEFT_LICENSES_LOWERCASE } from '../constants/licenses';
|
|
2
1
|
import type { AggregatedLicensesMapping } from '../types';
|
|
3
2
|
import type { LicenseAnalysisResult } from '../types/LicenseAnalysisResult';
|
|
4
3
|
|
|
5
4
|
import { LicenseCategory } from './LicenseCategory';
|
|
5
|
+
import type { OrPolicy } from './OrPolicy';
|
|
6
|
+
import { DEFAULT_OR_POLICY } from './OrPolicy';
|
|
6
7
|
import { getGraphStateInfo } from './descriptions';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* Categorizes a license based on its copyleft characteristics.
|
|
10
|
-
* @param licenseType the license type
|
|
11
|
-
* @returns the license category
|
|
12
|
-
*/
|
|
13
|
-
export function categorizeLicense(licenseType?: string): LicenseCategory {
|
|
14
|
-
if (!licenseType || licenseType === 'unknown') {
|
|
15
|
-
return LicenseCategory.UNKNOWN;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// check for strong copyleft licenses
|
|
19
|
-
if (STRONG_COPYLEFT_LICENSES_LOWERCASE.has(licenseType.toLowerCase())) {
|
|
20
|
-
return LicenseCategory.STRONG_COPYLEFT;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// check for weak copyleft licenses
|
|
24
|
-
if (WEAK_COPYLEFT_LICENSES_LOWERCASE.has(licenseType.toLowerCase())) {
|
|
25
|
-
return LicenseCategory.WEAK_COPYLEFT;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// everything else is considered permissive
|
|
29
|
-
return LicenseCategory.PERMISSIVE;
|
|
30
|
-
}
|
|
8
|
+
import { classifyLicenseExpression } from './licenseClassification';
|
|
9
|
+
import { renderLicenseExpression } from './licenseExpression';
|
|
31
10
|
|
|
32
11
|
/**
|
|
33
12
|
* Analyzes license data and returns comprehensive statistics.
|
|
13
|
+
*
|
|
14
|
+
* Each package is classified by its License Expression and grouped by the rendered expression,
|
|
15
|
+
* so a Dual License such as `MIT OR Apache-2.0` is counted under one key.
|
|
16
|
+
*
|
|
34
17
|
* @param report the licenses report data
|
|
18
|
+
* @param orPolicy the OR Policy used to classify Dual Licenses; defaults to {@link DEFAULT_OR_POLICY}
|
|
35
19
|
* @returns the license analysis result
|
|
36
20
|
*/
|
|
37
|
-
export function analyzeLicenses(
|
|
21
|
+
export function analyzeLicenses(
|
|
22
|
+
report: AggregatedLicensesMapping,
|
|
23
|
+
orPolicy: OrPolicy = DEFAULT_OR_POLICY,
|
|
24
|
+
): LicenseAnalysisResult {
|
|
38
25
|
const byCategory: Record<LicenseCategory, number> = {
|
|
39
26
|
[LicenseCategory.STRONG_COPYLEFT]: 0,
|
|
40
27
|
[LicenseCategory.WEAK_COPYLEFT]: 0,
|
|
@@ -43,21 +30,19 @@ export function analyzeLicenses(report: AggregatedLicensesMapping): LicenseAnaly
|
|
|
43
30
|
};
|
|
44
31
|
|
|
45
32
|
const byLicense: Record<string, number> = {};
|
|
33
|
+
const categoryByLicense: Record<string, LicenseCategory> = {};
|
|
46
34
|
const categorizedLicenses: Record<string, LicenseCategory> = {};
|
|
47
35
|
|
|
48
36
|
Object.entries(report).forEach(([packageKey, license]) => {
|
|
49
|
-
const
|
|
50
|
-
const
|
|
37
|
+
const category = classifyLicenseExpression(license.license, orPolicy);
|
|
38
|
+
const renderedLicense = renderLicenseExpression(license.license);
|
|
51
39
|
|
|
52
40
|
// stats by category
|
|
53
41
|
byCategory[category]++;
|
|
54
42
|
|
|
55
43
|
// stats by specific license
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
byLicense[key] = (byLicense[key] ?? 0) + 1;
|
|
60
|
-
}
|
|
44
|
+
byLicense[renderedLicense] = (byLicense[renderedLicense] ?? 0) + 1;
|
|
45
|
+
categoryByLicense[renderedLicense] = category;
|
|
61
46
|
|
|
62
47
|
// memoization for lookup
|
|
63
48
|
categorizedLicenses[packageKey] = category;
|
|
@@ -71,6 +56,7 @@ export function analyzeLicenses(report: AggregatedLicensesMapping): LicenseAnaly
|
|
|
71
56
|
total,
|
|
72
57
|
byCategory,
|
|
73
58
|
byLicense,
|
|
59
|
+
categoryByLicense,
|
|
74
60
|
description,
|
|
75
61
|
categoriesPresence,
|
|
76
62
|
categorizedLicenses,
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { STRONG_COPYLEFT_LICENSES_LOWERCASE, WEAK_COPYLEFT_LICENSES_LOWERCASE } from '../constants';
|
|
2
|
+
import type { LicenseExpression } from '../types';
|
|
3
|
+
|
|
4
|
+
import { LicenseCategory } from './LicenseCategory';
|
|
5
|
+
import type { OrPolicy } from './OrPolicy';
|
|
6
|
+
import { DEFAULT_OR_POLICY } from './OrPolicy';
|
|
7
|
+
|
|
8
|
+
const CATEGORY_RANK: Record<LicenseCategory, number> = {
|
|
9
|
+
[LicenseCategory.PERMISSIVE]: 0,
|
|
10
|
+
[LicenseCategory.WEAK_COPYLEFT]: 1,
|
|
11
|
+
[LicenseCategory.STRONG_COPYLEFT]: 2,
|
|
12
|
+
[LicenseCategory.UNKNOWN]: 3,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const OR_COMPARISON_STRATEGY: Record<OrPolicy, (a: LicenseCategory, b: LicenseCategory) => LicenseCategory> = {
|
|
16
|
+
'most-restrictive': takeMoreRestrictive,
|
|
17
|
+
'least-restrictive': takeLessRestrictive,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Categorizes a canonical License Identifier from a parsed License Expression.
|
|
22
|
+
* Not for raw strings: an identifier outside the copyleft tables is assumed permissive,
|
|
23
|
+
* which is only safe because the parser rejects unrecognised identifiers.
|
|
24
|
+
*/
|
|
25
|
+
function categorizeLicenseId(licenseId: string): LicenseCategory {
|
|
26
|
+
if (STRONG_COPYLEFT_LICENSES_LOWERCASE.has(licenseId.toLowerCase())) {
|
|
27
|
+
return LicenseCategory.STRONG_COPYLEFT;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (WEAK_COPYLEFT_LICENSES_LOWERCASE.has(licenseId.toLowerCase())) {
|
|
31
|
+
return LicenseCategory.WEAK_COPYLEFT;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return LicenseCategory.PERMISSIVE;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Classifies a License Expression into a License Category.
|
|
39
|
+
*
|
|
40
|
+
* Categories are ordered permissive < weak copyleft < strong copyleft < unknown:
|
|
41
|
+
* - a license takes the category of its License Identifier;
|
|
42
|
+
* - an Unknown License is unknown;
|
|
43
|
+
* - AND always takes its most restrictive operand;
|
|
44
|
+
* - OR takes the operand chosen by the OR Policy.
|
|
45
|
+
*
|
|
46
|
+
* @param expression the License Expression to classify
|
|
47
|
+
* @param orPolicy the OR Policy applied to every OR in the expression; defaults to {@link DEFAULT_OR_POLICY}
|
|
48
|
+
* @returns the License Category of the whole expression
|
|
49
|
+
* @example
|
|
50
|
+
* classifyLicenseExpression(parseLicenseExpression('MIT OR GPL-3.0-only'), 'least-restrictive');
|
|
51
|
+
* // LicenseCategory.PERMISSIVE
|
|
52
|
+
*/
|
|
53
|
+
export function classifyLicenseExpression(
|
|
54
|
+
expression: LicenseExpression,
|
|
55
|
+
orPolicy: OrPolicy = DEFAULT_OR_POLICY,
|
|
56
|
+
): LicenseCategory {
|
|
57
|
+
switch (expression.kind) {
|
|
58
|
+
case 'license':
|
|
59
|
+
return isLicenseRef(expression.id) ? LicenseCategory.UNKNOWN : categorizeLicenseId(expression.id);
|
|
60
|
+
case 'unknown':
|
|
61
|
+
return LicenseCategory.UNKNOWN;
|
|
62
|
+
case 'and':
|
|
63
|
+
return takeMoreRestrictive(
|
|
64
|
+
classifyLicenseExpression(expression.left, orPolicy),
|
|
65
|
+
classifyLicenseExpression(expression.right, orPolicy),
|
|
66
|
+
);
|
|
67
|
+
case 'or':
|
|
68
|
+
return OR_COMPARISON_STRATEGY[orPolicy](
|
|
69
|
+
classifyLicenseExpression(expression.left, orPolicy),
|
|
70
|
+
classifyLicenseExpression(expression.right, orPolicy),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The copyleft that cannot be avoided for a License Expression, see {@link classifyUnavoidableCopyleft} */
|
|
76
|
+
export interface UnavoidableCopyleft {
|
|
77
|
+
/** The known copyleft that must be complied with, never unknown; permissive when no known operand requires copyleft */
|
|
78
|
+
category: LicenseCategory;
|
|
79
|
+
/** `true` when an Unknown operand could require more than `category`, so `category` is only a lower bound */
|
|
80
|
+
unidentified: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Decides which copyleft cannot be avoided for a License Expression under an OR Policy.
|
|
85
|
+
*
|
|
86
|
+
* Unknown operands are ignored, so a custom license next to a copyleft one cannot hide the copyleft:
|
|
87
|
+
* - AND requires the most restrictive known operand;
|
|
88
|
+
* - OR with an Unknown operand requires what the other operand requires;
|
|
89
|
+
* - OR with two known operands follows the OR Policy;
|
|
90
|
+
* - an expression with no known operand requires no copyleft.
|
|
91
|
+
*
|
|
92
|
+
* Ignoring an Unknown operand can also understate the result (`MIT AND LicenseRef-Custom`, or an expression
|
|
93
|
+
* that is wholly Unknown), so the result says when that happened: `unidentified` is `true` when an Unknown operand
|
|
94
|
+
* could require more than the category under the OR Policy and the category is not already strong copyleft,
|
|
95
|
+
* the most restrictive known category. `MIT OR LicenseRef-Custom` is only unidentified under the most restrictive OR Policy.
|
|
96
|
+
*
|
|
97
|
+
* @param expression the License Expression to check
|
|
98
|
+
* @param orPolicy the OR Policy applied to every OR in the expression; defaults to {@link DEFAULT_OR_POLICY}
|
|
99
|
+
* @returns the known copyleft category, and whether an Unknown operand leaves it unidentified
|
|
100
|
+
* @example
|
|
101
|
+
* classifyUnavoidableCopyleft(parseLicenseExpression('GPL-3.0-only AND LicenseRef-Custom'));
|
|
102
|
+
* // { category: LicenseCategory.STRONG_COPYLEFT, unidentified: false }
|
|
103
|
+
* @example
|
|
104
|
+
* classifyUnavoidableCopyleft(parseLicenseExpression('MIT AND LicenseRef-Custom'));
|
|
105
|
+
* // { category: LicenseCategory.PERMISSIVE, unidentified: true }
|
|
106
|
+
*/
|
|
107
|
+
export function classifyUnavoidableCopyleft(
|
|
108
|
+
expression: LicenseExpression,
|
|
109
|
+
orPolicy: OrPolicy = DEFAULT_OR_POLICY,
|
|
110
|
+
): UnavoidableCopyleft {
|
|
111
|
+
const category = findKnownCopyleft(expression, orPolicy) ?? LicenseCategory.PERMISSIVE;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
category,
|
|
115
|
+
unidentified: category !== LicenseCategory.STRONG_COPYLEFT && hasUnknownOperand(expression, orPolicy),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Whether an Unknown operand could require more than {@link findKnownCopyleft} found under the OR Policy.
|
|
121
|
+
*
|
|
122
|
+
* An OR with a known operand only has this risk under the most restrictive OR Policy: under the least restrictive
|
|
123
|
+
* one the known operand can always be chosen instead of the Unknown one.
|
|
124
|
+
*/
|
|
125
|
+
function hasUnknownOperand(expression: LicenseExpression, orPolicy: OrPolicy): boolean {
|
|
126
|
+
switch (expression.kind) {
|
|
127
|
+
case 'unknown':
|
|
128
|
+
return true;
|
|
129
|
+
case 'license':
|
|
130
|
+
return classifyLicenseExpression(expression, orPolicy) === LicenseCategory.UNKNOWN;
|
|
131
|
+
case 'and':
|
|
132
|
+
return hasUnknownOperand(expression.left, orPolicy) || hasUnknownOperand(expression.right, orPolicy);
|
|
133
|
+
case 'or':
|
|
134
|
+
return orPolicy === 'least-restrictive'
|
|
135
|
+
? hasUnknownOperand(expression.left, orPolicy) && hasUnknownOperand(expression.right, orPolicy)
|
|
136
|
+
: hasUnknownOperand(expression.left, orPolicy) || hasUnknownOperand(expression.right, orPolicy);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Like {@link classifyUnavoidableCopyleft}, but `null` when no operand has a known category */
|
|
141
|
+
function findKnownCopyleft(expression: LicenseExpression, orPolicy: OrPolicy): LicenseCategory | null {
|
|
142
|
+
if (expression.kind === 'unknown') {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (expression.kind === 'license') {
|
|
147
|
+
const category = classifyLicenseExpression(expression, orPolicy);
|
|
148
|
+
|
|
149
|
+
return category === LicenseCategory.UNKNOWN ? null : category;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const left = findKnownCopyleft(expression.left, orPolicy);
|
|
153
|
+
const right = findKnownCopyleft(expression.right, orPolicy);
|
|
154
|
+
|
|
155
|
+
if (left === null || right === null) {
|
|
156
|
+
return left ?? right;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return expression.kind === 'and' ? takeMoreRestrictive(left, right) : OR_COMPARISON_STRATEGY[orPolicy](left, right);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A custom license reference, optionally qualified by a document: `LicenseRef-X` or `DocumentRef-Y:LicenseRef-X` */
|
|
163
|
+
function isLicenseRef(id: string): boolean {
|
|
164
|
+
return /^(DocumentRef-[^:]+:)?LicenseRef-/.test(id);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function takeMoreRestrictive(a: LicenseCategory, b: LicenseCategory): LicenseCategory {
|
|
168
|
+
return CATEGORY_RANK[a] >= CATEGORY_RANK[b] ? a : b;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function takeLessRestrictive(a: LicenseCategory, b: LicenseCategory): LicenseCategory {
|
|
172
|
+
return CATEGORY_RANK[a] <= CATEGORY_RANK[b] ? a : b;
|
|
173
|
+
}
|