@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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import spdxExceptionIds from 'spdx-exceptions/index.json';
|
|
2
|
+
import parse from 'spdx-expression-parse';
|
|
3
|
+
import deprecatedSpdxLicenseIds from 'spdx-license-ids/deprecated.json';
|
|
4
|
+
import spdxLicenseIds from 'spdx-license-ids/index.json';
|
|
5
|
+
|
|
6
|
+
import type { LicenseExpression } from '../types';
|
|
7
|
+
|
|
8
|
+
const SPDX_LICENSE_IDS = new Set(spdxLicenseIds);
|
|
9
|
+
const DEPRECATED_SPDX_LICENSE_IDS = new Set(deprecatedSpdxLicenseIds);
|
|
10
|
+
|
|
11
|
+
const CANONICAL_ID_BY_LOWERCASE = new Map(
|
|
12
|
+
[...spdxLicenseIds, ...deprecatedSpdxLicenseIds, ...spdxExceptionIds].map((id) => [id.toLowerCase(), id]),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const SPDX_OPERATORS = new Set(['and', 'or', 'with']);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Unambiguous spellings that are not valid SPDX License Expressions, keyed by the lowercase, trimmed value.
|
|
19
|
+
* Ambiguous values (`BSD`, `GPL`, `LGPL`) are deliberately absent: guessing a variant or version changes the legal meaning.
|
|
20
|
+
*/
|
|
21
|
+
const LICENSE_ALIASES = new Map([
|
|
22
|
+
['apache 2.0', 'Apache-2.0'],
|
|
23
|
+
['apache-2', 'Apache-2.0'],
|
|
24
|
+
['apache license 2.0', 'Apache-2.0'],
|
|
25
|
+
['mit/x11', 'MIT'],
|
|
26
|
+
['mit license', 'MIT'],
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parses a Raw License into a License Expression, following the SPDX License Expression syntax.
|
|
31
|
+
*
|
|
32
|
+
* Never throws: a missing or unparseable value becomes an Unknown License that keeps the original value.
|
|
33
|
+
*
|
|
34
|
+
* @param rawLicense the license declaration as written in package.json, or `null` if absent
|
|
35
|
+
* @returns the parsed License Expression
|
|
36
|
+
* @example
|
|
37
|
+
* parseLicenseExpression('MIT OR Apache-2.0');
|
|
38
|
+
* // { kind: 'or', left: { kind: 'license', id: 'MIT', declaredId: 'MIT' }, right: { kind: 'license', id: 'Apache-2.0', declaredId: 'Apache-2.0' } }
|
|
39
|
+
*/
|
|
40
|
+
export function parseLicenseExpression(rawLicense: string | null): LicenseExpression {
|
|
41
|
+
if (rawLicense === null) {
|
|
42
|
+
return { kind: 'unknown', raw: rawLicense };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const parsed =
|
|
46
|
+
tryParse(rawLicense) ??
|
|
47
|
+
tryParse(LICENSE_ALIASES.get(rawLicense.trim().toLowerCase())) ??
|
|
48
|
+
tryParse(fixIdentifierCase(rawLicense));
|
|
49
|
+
|
|
50
|
+
return parsed ?? { kind: 'unknown', raw: rawLicense };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function tryParse(rawLicense: string | undefined): LicenseExpression | undefined {
|
|
54
|
+
if (rawLicense === undefined) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
return mapSpdxParsedValueToLicenseExpression(parse(rawLicense));
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Rewrites every identifier and operator to its canonical spelling (SPDX Annex B.2: identifiers are case-insensitive).
|
|
67
|
+
* Tokens that match no known identifier are left as written, so the re-parse rejects them.
|
|
68
|
+
*/
|
|
69
|
+
function fixIdentifierCase(rawLicense: string): string {
|
|
70
|
+
return rawLicense.replace(/[^\s()]+/g, (token) => {
|
|
71
|
+
if (SPDX_OPERATORS.has(token.toLowerCase())) {
|
|
72
|
+
return token.toUpperCase();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const plus = token.endsWith('+') ? '+' : '';
|
|
76
|
+
const id = plus ? token.slice(0, -1) : token;
|
|
77
|
+
const canonicalId =
|
|
78
|
+
CANONICAL_ID_BY_LOWERCASE.get(id.toLowerCase()) ??
|
|
79
|
+
(/^licenseref-/i.test(id) ? `LicenseRef-${id.slice('LicenseRef-'.length)}` : id);
|
|
80
|
+
|
|
81
|
+
return `${canonicalId}${plus}`;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function mapSpdxParsedValueToLicenseExpression(spdxParsedValue: parse.Info): LicenseExpression {
|
|
86
|
+
if ('conjunction' in spdxParsedValue) {
|
|
87
|
+
return {
|
|
88
|
+
kind: spdxParsedValue.conjunction,
|
|
89
|
+
left: mapSpdxParsedValueToLicenseExpression(spdxParsedValue.left),
|
|
90
|
+
right: mapSpdxParsedValueToLicenseExpression(spdxParsedValue.right),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { license: declaredId, plus, exception } = spdxParsedValue;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
kind: 'license',
|
|
98
|
+
id: normalizeLicenseId(declaredId, plus === true),
|
|
99
|
+
declaredId,
|
|
100
|
+
...(plus && { plus: true }),
|
|
101
|
+
...(exception && { exception }),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Upgrades a declared License Identifier to its canonical form:
|
|
107
|
+
* - with `+`, to the `-or-later` identifier when one exists;
|
|
108
|
+
* - a deprecated identifier, to its `-only` identifier when one exists.
|
|
109
|
+
*/
|
|
110
|
+
function normalizeLicenseId(declaredId: string, plus: boolean): string {
|
|
111
|
+
if (plus && SPDX_LICENSE_IDS.has(`${declaredId}-or-later`)) {
|
|
112
|
+
return `${declaredId}-or-later`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!plus && DEPRECATED_SPDX_LICENSE_IDS.has(declaredId) && SPDX_LICENSE_IDS.has(`${declaredId}-only`)) {
|
|
116
|
+
return `${declaredId}-only`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return declaredId;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Collects the License Identifiers mentioned in a License Expression, regardless of how they are combined.
|
|
124
|
+
*
|
|
125
|
+
* @param expression the License Expression to flatten
|
|
126
|
+
* @returns the License Identifiers in order of first appearance, without duplicates; empty for an Unknown License
|
|
127
|
+
* @example
|
|
128
|
+
* collectLicenseIds(parseLicenseExpression('(MIT OR ISC) AND (MIT OR Apache-2.0)'));
|
|
129
|
+
* // ['MIT', 'ISC', 'Apache-2.0']
|
|
130
|
+
*/
|
|
131
|
+
export function collectLicenseIds(expression: LicenseExpression): string[] {
|
|
132
|
+
switch (expression.kind) {
|
|
133
|
+
case 'license':
|
|
134
|
+
return [expression.id];
|
|
135
|
+
case 'and':
|
|
136
|
+
case 'or':
|
|
137
|
+
return [...new Set([...collectLicenseIds(expression.left), ...collectLicenseIds(expression.right)])];
|
|
138
|
+
case 'unknown':
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Renders a License Expression to a string, following the SPDX License Expression syntax.
|
|
145
|
+
*
|
|
146
|
+
* Adds only the parentheses required by SPDX precedence and by the right-nesting of operator chains,
|
|
147
|
+
* so that parsing the result gives back the same tree. An Unknown License renders as its original value
|
|
148
|
+
* (or `unknown` when absent) and is not expected to parse back to the same tree.
|
|
149
|
+
*
|
|
150
|
+
* @param expression the License Expression to render
|
|
151
|
+
* @returns the rendered License Expression
|
|
152
|
+
* @example
|
|
153
|
+
* renderLicenseExpression(parseLicenseExpression('(MIT OR Apache-2.0) AND ISC'));
|
|
154
|
+
* // '(MIT OR Apache-2.0) AND ISC'
|
|
155
|
+
*/
|
|
156
|
+
export function renderLicenseExpression(expression: LicenseExpression): string {
|
|
157
|
+
switch (expression.kind) {
|
|
158
|
+
case 'unknown':
|
|
159
|
+
return expression.raw ?? 'unknown';
|
|
160
|
+
case 'or':
|
|
161
|
+
case 'and': {
|
|
162
|
+
const left = renderOperand(expression.left, expression.kind, 'left');
|
|
163
|
+
const right = renderOperand(expression.right, expression.kind, 'right');
|
|
164
|
+
|
|
165
|
+
return `${left} ${expression.kind.toUpperCase()} ${right}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
case 'license':
|
|
169
|
+
return renderLicenseLeaf(expression);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function renderLicenseLeaf(leaf: Extract<LicenseExpression, { kind: 'license' }>): string {
|
|
174
|
+
// `+` is already absorbed into an `-or-later` identifier
|
|
175
|
+
const plus = leaf.plus && !leaf.id.endsWith('-or-later') ? '+' : '';
|
|
176
|
+
const exception = leaf.exception ? ` WITH ${leaf.exception}` : '';
|
|
177
|
+
|
|
178
|
+
return `${leaf.id}${plus}${exception}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function renderOperand(
|
|
182
|
+
operand: LicenseExpression,
|
|
183
|
+
parentKind: 'or' | 'and',
|
|
184
|
+
operandPosition: 'left' | 'right',
|
|
185
|
+
): string {
|
|
186
|
+
const rendered = renderLicenseExpression(operand);
|
|
187
|
+
|
|
188
|
+
return shouldAddParenthesis(operand, parentKind, operandPosition) ? `(${rendered})` : rendered;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function shouldAddParenthesis(
|
|
192
|
+
childExpression: LicenseExpression,
|
|
193
|
+
parentKind: 'or' | 'and',
|
|
194
|
+
childPosition: 'left' | 'right',
|
|
195
|
+
): boolean {
|
|
196
|
+
return (
|
|
197
|
+
(parentKind === 'and' && childExpression.kind === 'or') ||
|
|
198
|
+
(childPosition === 'left' && parentKind === childExpression.kind)
|
|
199
|
+
);
|
|
200
|
+
}
|
package/src/node/common.ts
CHANGED
|
@@ -1,25 +1,150 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import glob from 'glob';
|
|
4
|
+
import { glob } from 'glob';
|
|
5
5
|
|
|
6
|
+
import { collectLicenseIds, parseLicenseExpression } from '../licenses';
|
|
6
7
|
import type {
|
|
7
8
|
AboutLibrariesLibraryJsonPayload,
|
|
8
9
|
AboutLibrariesLicenseJsonPayload,
|
|
9
10
|
AboutLibrariesLikePackageInfo,
|
|
10
11
|
AggregatedLicensesMapping,
|
|
11
12
|
DependencyType,
|
|
13
|
+
License,
|
|
12
14
|
LicensePlistPayload,
|
|
13
15
|
ParentPackageInfo,
|
|
16
|
+
ScanDependenciesOptions,
|
|
14
17
|
ScanPackageCallContext,
|
|
15
18
|
ScanPackageOptionsFactory,
|
|
16
19
|
} from '../types';
|
|
17
20
|
import { YamlUtils } from '../utils';
|
|
18
21
|
|
|
22
|
+
import { mergeLicensesMappings } from './mergeLicensesMappings';
|
|
19
23
|
import { PackageUtils } from './utils';
|
|
20
24
|
|
|
21
25
|
type InternalScanGroupSpecifier = { packages: [depName: string, depVersion: string][]; dependencyType: DependencyType };
|
|
22
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Finds the root directory of the package that a directory belongs to - the closest directory (itself or a parent)
|
|
29
|
+
* containing a `package.json` with a `name` field. Nested `package.json` files without a name
|
|
30
|
+
* (e.g. `lib/commonjs/package.json` containing only `{ "type": "commonjs" }`) are skipped.
|
|
31
|
+
*
|
|
32
|
+
* Works for packages installed in `node_modules` as well as for workspace / linked packages,
|
|
33
|
+
* which are referenced by their real path (outside `node_modules`)
|
|
34
|
+
*
|
|
35
|
+
* @param startDir Absolute path to the directory to start the search from, e.g. the directory of a module file
|
|
36
|
+
* @returns Path to the package root directory or `null` if the directory does not belong to any named package
|
|
37
|
+
*/
|
|
38
|
+
export function findPackageRoot(startDir: string): string | null {
|
|
39
|
+
let dir = startDir;
|
|
40
|
+
|
|
41
|
+
// `path.dirname` of the filesystem root returns the root itself, which ends the loop
|
|
42
|
+
while (path.dirname(dir) !== dir) {
|
|
43
|
+
if (hasNamedPackageJson(dir)) {
|
|
44
|
+
return dir;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
dir = path.dirname(dir);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return hasNamedPackageJson(dir) ? dir : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Checks whether a directory contains a valid `package.json` with a `name` field
|
|
55
|
+
*
|
|
56
|
+
* @param dir Path to the directory to check
|
|
57
|
+
*/
|
|
58
|
+
function hasNamedPackageJson(dir: string): boolean {
|
|
59
|
+
const packageJsonPath = path.join(dir, 'package.json');
|
|
60
|
+
|
|
61
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, { encoding: 'utf-8' }));
|
|
67
|
+
|
|
68
|
+
return Boolean(packageJson.name);
|
|
69
|
+
} catch {
|
|
70
|
+
// invalid package.json - treat it as not a package root
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Collects license information for a given list of package directories, e.g. the packages found in the Metro dependency graph.
|
|
77
|
+
* Unlike {@link scanDependencies}, it does not scan dependencies of the packages - the list is expected to be complete.
|
|
78
|
+
*
|
|
79
|
+
* @param packageRoots Paths to the root directories of the packages (directories containing `package.json`)
|
|
80
|
+
* @returns Aggregated licenses object containing the given packages and their license information
|
|
81
|
+
*/
|
|
82
|
+
export function scanPackageRoots(packageRoots: string[]): AggregatedLicensesMapping {
|
|
83
|
+
const result: AggregatedLicensesMapping = {};
|
|
84
|
+
|
|
85
|
+
for (const packageRoot of packageRoots) {
|
|
86
|
+
const packageJsonPath = path.join(packageRoot, 'package.json');
|
|
87
|
+
|
|
88
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
89
|
+
console.warn(`[react-native-legal] skipping ${packageRoot} could not find package.json`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const packageJson = require(path.resolve(packageJsonPath));
|
|
95
|
+
|
|
96
|
+
if (!packageJson.name || packageJson.private === true) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const licenseInfo = readPackageLicenseInfo(packageJsonPath);
|
|
101
|
+
|
|
102
|
+
result[`${licenseInfo.name}@${licenseInfo.version}`] = {
|
|
103
|
+
...licenseInfo,
|
|
104
|
+
dependencyType: 'dependency',
|
|
105
|
+
requiredVersion: licenseInfo.version,
|
|
106
|
+
parentPackages: [],
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.warn(`[react-native-legal] could not process package.json in ${packageRoot}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Reads license information of a single package, based only on its own files
|
|
118
|
+
* (package.json and LICENSE file), without any context of how the package was found
|
|
119
|
+
*
|
|
120
|
+
* @param packageJsonPath Path to the package.json file of the package
|
|
121
|
+
*/
|
|
122
|
+
function readPackageLicenseInfo(
|
|
123
|
+
packageJsonPath: string,
|
|
124
|
+
): Pick<
|
|
125
|
+
License,
|
|
126
|
+
'name' | 'author' | 'description' | 'rawLicense' | 'license' | 'licenseIds' | 'licenseFiles' | 'url' | 'version'
|
|
127
|
+
> {
|
|
128
|
+
const packageJson = require(path.resolve(packageJsonPath));
|
|
129
|
+
|
|
130
|
+
const rawLicense = PackageUtils.parseLicenseField(packageJson) ?? null;
|
|
131
|
+
const license = parseLicenseExpression(rawLicense);
|
|
132
|
+
const licenseIds = collectLicenseIds(license);
|
|
133
|
+
const licenseFiles = PackageUtils.readLicenseFiles(path.dirname(packageJsonPath), licenseIds);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
name: packageJson.name,
|
|
137
|
+
author: PackageUtils.parseAuthorField(packageJson),
|
|
138
|
+
description: packageJson.description,
|
|
139
|
+
rawLicense,
|
|
140
|
+
license,
|
|
141
|
+
licenseIds,
|
|
142
|
+
licenseFiles,
|
|
143
|
+
url: PackageUtils.parseRepositoryFieldToUrl(packageJson),
|
|
144
|
+
version: packageJson.version,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
23
148
|
/**
|
|
24
149
|
* Scans a single package and its dependencies for license information
|
|
25
150
|
*
|
|
@@ -43,7 +168,8 @@ function scanPackage(
|
|
|
43
168
|
dependencyType,
|
|
44
169
|
parentPackageRequiredVersion,
|
|
45
170
|
parentPackageResolvedVersion,
|
|
46
|
-
|
|
171
|
+
projectRoot,
|
|
172
|
+
}: InternalScanPackageCallContext,
|
|
47
173
|
) {
|
|
48
174
|
const requiredVersionPackageKey = `${packageName}@${requiredVersion}`;
|
|
49
175
|
|
|
@@ -62,7 +188,7 @@ function scanPackage(
|
|
|
62
188
|
processedPackages.add(requiredVersionPackageKey);
|
|
63
189
|
|
|
64
190
|
try {
|
|
65
|
-
const localPackageJsonPath = PackageUtils.getPackageJsonPath(packageName, parentPackageRoot);
|
|
191
|
+
const localPackageJsonPath = PackageUtils.getPackageJsonPath(packageName, parentPackageRoot, projectRoot);
|
|
66
192
|
|
|
67
193
|
if (!localPackageJsonPath) {
|
|
68
194
|
// do not warn if the package is an optional dependency, it's normal it may not be installed
|
|
@@ -76,14 +202,6 @@ function scanPackage(
|
|
|
76
202
|
const localPackageJson = require(path.resolve(localPackageJsonPath));
|
|
77
203
|
|
|
78
204
|
if (localPackageJson.private !== true) {
|
|
79
|
-
const licenseFiles = glob.sync('LICEN{S,C}E{.md,}', {
|
|
80
|
-
cwd: path.dirname(localPackageJsonPath),
|
|
81
|
-
absolute: true,
|
|
82
|
-
nocase: true,
|
|
83
|
-
nodir: true,
|
|
84
|
-
ignore: '**/{__tests__,__fixtures__,__mocks__}/**',
|
|
85
|
-
});
|
|
86
|
-
|
|
87
205
|
const resolvedVersionPackageKey = `${packageName}@${localPackageJson.version}`;
|
|
88
206
|
|
|
89
207
|
let parentPackageInfo: ParentPackageInfo | undefined;
|
|
@@ -102,14 +220,8 @@ function scanPackage(
|
|
|
102
220
|
];
|
|
103
221
|
} else {
|
|
104
222
|
result[resolvedVersionPackageKey] = {
|
|
223
|
+
...readPackageLicenseInfo(localPackageJsonPath),
|
|
105
224
|
name: packageName,
|
|
106
|
-
author: PackageUtils.parseAuthorField(localPackageJson),
|
|
107
|
-
content: licenseFiles?.[0] ? fs.readFileSync(licenseFiles[0], { encoding: 'utf-8' }) : undefined,
|
|
108
|
-
file: licenseFiles?.[0] ? licenseFiles[0] : undefined,
|
|
109
|
-
description: localPackageJson.description,
|
|
110
|
-
type: PackageUtils.parseLicenseField(localPackageJson),
|
|
111
|
-
url: PackageUtils.parseRepositoryFieldToUrl(localPackageJson),
|
|
112
|
-
version: localPackageJson.version,
|
|
113
225
|
requiredVersion,
|
|
114
226
|
parentPackages: parentPackageInfo ? [parentPackageInfo] : [],
|
|
115
227
|
parentPackageRequiredVersion,
|
|
@@ -159,6 +271,7 @@ function scanPackage(
|
|
|
159
271
|
parentPackageName: packageName,
|
|
160
272
|
parentPackageRequiredVersion: requiredVersion,
|
|
161
273
|
parentPackageResolvedVersion: localPackageJson.version,
|
|
274
|
+
projectRoot,
|
|
162
275
|
});
|
|
163
276
|
}
|
|
164
277
|
}
|
|
@@ -169,22 +282,63 @@ function scanPackage(
|
|
|
169
282
|
|
|
170
283
|
type DependencyMapping = Record<string, string>;
|
|
171
284
|
type MaybeDependencyMapping = DependencyMapping | undefined;
|
|
285
|
+
type InternalScanPackageCallContext = ScanPackageCallContext & { projectRoot: string };
|
|
172
286
|
|
|
173
287
|
/**
|
|
174
288
|
* Scans `package.json` and searches for all packages under `dependencies` field. Supports monorepo projects.
|
|
175
289
|
*
|
|
176
290
|
* @param appPackageJsonPath Path to the `package.json` file of the application
|
|
177
291
|
* @param scanOptionsFactory Factory function to create scan options for dependencies; defaults to {@link PackageUtils.legacyDefaultScanPackageOptionsFactory}
|
|
292
|
+
* @param options Additional options, see {@link ScanDependenciesOptions}
|
|
178
293
|
* @returns Aggregated licenses object containing all scanned dependencies and their license information
|
|
179
294
|
*/
|
|
180
295
|
export function scanDependencies(
|
|
181
296
|
appPackageJsonPath: string,
|
|
182
297
|
scanOptionsFactory: ScanPackageOptionsFactory = PackageUtils.legacyDefaultScanPackageOptionsFactory,
|
|
298
|
+
options: ScanDependenciesOptions = {},
|
|
299
|
+
): AggregatedLicensesMapping {
|
|
300
|
+
const { additionalProjectRoots = [] } = options;
|
|
301
|
+
|
|
302
|
+
if (
|
|
303
|
+
!Array.isArray(additionalProjectRoots) ||
|
|
304
|
+
additionalProjectRoots.some(
|
|
305
|
+
(additionalProjectRoot) => typeof additionalProjectRoot !== 'string' || !additionalProjectRoot,
|
|
306
|
+
)
|
|
307
|
+
) {
|
|
308
|
+
throw new TypeError('[react-native-legal] additionalProjectRoots must be an array of non-empty paths');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const appRoot = fs.realpathSync(path.dirname(path.resolve(appPackageJsonPath)));
|
|
312
|
+
const additionalPackageJsonPaths = additionalProjectRoots.map((additionalProjectRoot) => {
|
|
313
|
+
const packageJsonPath = path.join(path.resolve(appRoot, additionalProjectRoot), 'package.json');
|
|
314
|
+
|
|
315
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`[react-native-legal] additional project root "${additionalProjectRoot}" is not a directory with a package.json (${packageJsonPath} not found)`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return packageJsonPath;
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
return mergeLicensesMappings(
|
|
325
|
+
scanProjectDependencies(appPackageJsonPath, scanOptionsFactory),
|
|
326
|
+
...additionalPackageJsonPaths.map((packageJsonPath) =>
|
|
327
|
+
scanProjectDependencies(packageJsonPath, scanOptionsFactory),
|
|
328
|
+
),
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function scanProjectDependencies(
|
|
333
|
+
projectPackageJsonPath: string,
|
|
334
|
+
scanOptionsFactory: ScanPackageOptionsFactory,
|
|
183
335
|
): AggregatedLicensesMapping {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const
|
|
336
|
+
const projectPackageJson = require(path.resolve(projectPackageJsonPath));
|
|
337
|
+
// real path, so that `node_modules` directories of the project's parent directories are found even if the project is reached through a symlink
|
|
338
|
+
const projectRoot = fs.realpathSync(path.dirname(path.resolve(projectPackageJsonPath)));
|
|
339
|
+
const dependencies: MaybeDependencyMapping = projectPackageJson.dependencies;
|
|
340
|
+
const devDependencies: MaybeDependencyMapping = projectPackageJson.devDependencies;
|
|
341
|
+
const optionalDependencies: MaybeDependencyMapping = projectPackageJson.optionalDependencies;
|
|
188
342
|
const result: AggregatedLicensesMapping = {};
|
|
189
343
|
const processedPackages = new Set<string>();
|
|
190
344
|
|
|
@@ -208,6 +362,7 @@ export function scanDependencies(
|
|
|
208
362
|
for (const [depName, depVersion] of packages) {
|
|
209
363
|
scanPackage(depName, depVersion as string, processedPackages, result, scanOptionsFactory, {
|
|
210
364
|
dependencyType,
|
|
365
|
+
projectRoot,
|
|
211
366
|
});
|
|
212
367
|
}
|
|
213
368
|
}
|
|
@@ -236,13 +391,17 @@ export function generateLicensePlistNPMOutput(licenses: AggregatedLicensesMappin
|
|
|
236
391
|
renames[normalizedPackageNameWithVersion] = license.name;
|
|
237
392
|
}
|
|
238
393
|
|
|
239
|
-
|
|
394
|
+
// LicensePlist holds one `file`/`body` per package: a single license file is referenced directly,
|
|
395
|
+
// otherwise every license text is concatenated into the body under a per-license heading
|
|
396
|
+
const singleLicenseFile = license.licenseFiles.length === 1 ? license.licenseFiles[0].file : undefined;
|
|
240
397
|
|
|
241
398
|
return {
|
|
242
399
|
name: normalizedPackageNameWithVersion,
|
|
243
400
|
version: license.version,
|
|
244
401
|
...(license.url && { source: license.url }),
|
|
245
|
-
...(
|
|
402
|
+
...(singleLicenseFile
|
|
403
|
+
? { file: path.relative(iosProjectPath, singleLicenseFile) }
|
|
404
|
+
: { body: PackageUtils.buildLicensePlistBody(license) }),
|
|
246
405
|
} as LicensePlistPayload;
|
|
247
406
|
});
|
|
248
407
|
|
|
@@ -341,45 +500,34 @@ export function writeLicensePlistNPMOutput(
|
|
|
341
500
|
* @see {@link writeAboutLibrariesNPMOutput}
|
|
342
501
|
*/
|
|
343
502
|
export function generateAboutLibrariesNPMOutput(licenses: AggregatedLicensesMapping): AboutLibrariesLikePackageInfo[] {
|
|
344
|
-
return Object.entries(licenses)
|
|
345
|
-
.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
name: jsonPayload.name,
|
|
366
|
-
tag: jsonPayload.tag,
|
|
367
|
-
uniqueId: jsonPayload.uniqueId,
|
|
368
|
-
website: jsonPayload.website,
|
|
369
|
-
};
|
|
370
|
-
const licenseJsonPayload: AboutLibrariesLicenseJsonPayload = {
|
|
371
|
-
content: jsonPayload.content,
|
|
372
|
-
hash: jsonPayload.licenses[0],
|
|
373
|
-
name: jsonPayload.type ?? '',
|
|
374
|
-
url: '',
|
|
375
|
-
};
|
|
503
|
+
return Object.entries(licenses).map(([packageKey, license]) => {
|
|
504
|
+
const uniqueId = PackageUtils.normalizePackageName(packageKey);
|
|
505
|
+
const licenseJsonPayloads: AboutLibrariesLicenseJsonPayload[] = PackageUtils.prepareAboutLibrariesLicenses(
|
|
506
|
+
license,
|
|
507
|
+
).map(({ name, content }) => ({
|
|
508
|
+
content,
|
|
509
|
+
hash: PackageUtils.prepareAboutLibrariesLicenseField(name, content),
|
|
510
|
+
name,
|
|
511
|
+
url: '',
|
|
512
|
+
}));
|
|
513
|
+
|
|
514
|
+
const libraryJsonPayload: AboutLibrariesLibraryJsonPayload = {
|
|
515
|
+
artifactVersion: license.version,
|
|
516
|
+
description: license.description ?? '',
|
|
517
|
+
developers: [{ name: license.author ?? '', organisationUrl: '' }],
|
|
518
|
+
licenses: licenseJsonPayloads.map(({ hash }) => hash),
|
|
519
|
+
name: license.name,
|
|
520
|
+
tag: '',
|
|
521
|
+
uniqueId,
|
|
522
|
+
website: license.url,
|
|
523
|
+
};
|
|
376
524
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
525
|
+
return {
|
|
526
|
+
normalizedPackageNameWithVersion: uniqueId,
|
|
527
|
+
libraryJsonPayload,
|
|
528
|
+
licenseJsonPayloads,
|
|
529
|
+
};
|
|
530
|
+
});
|
|
383
531
|
}
|
|
384
532
|
|
|
385
533
|
/**
|
|
@@ -427,17 +575,20 @@ export function writeAboutLibrariesNPMOutput(
|
|
|
427
575
|
aboutLibrariesLikeOutput = generateAboutLibrariesNPMOutput(licenses);
|
|
428
576
|
}
|
|
429
577
|
|
|
430
|
-
aboutLibrariesLikeOutput.forEach(({ normalizedPackageNameWithVersion, libraryJsonPayload,
|
|
578
|
+
aboutLibrariesLikeOutput.forEach(({ normalizedPackageNameWithVersion, libraryJsonPayload, licenseJsonPayloads }) => {
|
|
431
579
|
const libraryJsonFilePath = path.join(
|
|
432
580
|
aboutLibrariesConfigLibrariesDirPath,
|
|
433
581
|
`${normalizedPackageNameWithVersion}.json`,
|
|
434
582
|
);
|
|
435
|
-
const licenseJsonFilePath = path.join(aboutLibrariesConfigLicensesDirPath, `${licenseJsonPayload.hash}.json`);
|
|
436
583
|
|
|
437
584
|
fs.writeFileSync(libraryJsonFilePath, JSON.stringify(libraryJsonPayload));
|
|
438
585
|
|
|
439
|
-
|
|
440
|
-
|
|
586
|
+
for (const licenseJsonPayload of licenseJsonPayloads) {
|
|
587
|
+
const licenseJsonFilePath = path.join(aboutLibrariesConfigLicensesDirPath, `${licenseJsonPayload.hash}.json`);
|
|
588
|
+
|
|
589
|
+
if (!fs.existsSync(licenseJsonFilePath)) {
|
|
590
|
+
fs.writeFileSync(licenseJsonFilePath, JSON.stringify(licenseJsonPayload));
|
|
591
|
+
}
|
|
441
592
|
}
|
|
442
593
|
});
|
|
443
594
|
}
|
package/src/node/index.ts
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AggregatedLicensesMapping } from '../types';
|
|
2
|
+
|
|
3
|
+
import { MiscUtils } from './utils';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Combines licenses mappings, e.g. from scans of several project roots, into one.
|
|
7
|
+
*
|
|
8
|
+
* @param mappings Licenses mappings to combine; a package version present in several of them keeps the entry from the first one, with the parent packages of all of them
|
|
9
|
+
* @returns A new licenses mapping that shares no objects with the mappings passed in
|
|
10
|
+
*/
|
|
11
|
+
export function mergeLicensesMappings(...mappings: AggregatedLicensesMapping[]): AggregatedLicensesMapping {
|
|
12
|
+
const result: AggregatedLicensesMapping = {};
|
|
13
|
+
|
|
14
|
+
for (const mapping of mappings) {
|
|
15
|
+
for (const [packageKey, license] of Object.entries(mapping)) {
|
|
16
|
+
const existingLicense = result[packageKey];
|
|
17
|
+
|
|
18
|
+
if (!existingLicense) {
|
|
19
|
+
result[packageKey] = structuredClone({ ...license, parentPackages: license.parentPackages ?? [] });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const parentPackage of license.parentPackages ?? []) {
|
|
24
|
+
if (!MiscUtils.arrayIncludesObject(existingLicense.parentPackages, parentPackage)) {
|
|
25
|
+
existingLicense.parentPackages.push(structuredClone(parentPackage));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return result;
|
|
32
|
+
}
|