@callstack/licenses 0.3.3 → 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.
Files changed (61) hide show
  1. package/README.md +4 -4
  2. package/build/constants/licenses.d.ts +4 -0
  3. package/build/constants/licenses.js +15 -15
  4. package/build/licenses/OrPolicy.d.ts +21 -0
  5. package/build/licenses/OrPolicy.js +16 -0
  6. package/build/licenses/index.d.ts +3 -0
  7. package/build/licenses/index.js +3 -0
  8. package/build/licenses/licenseAnalysis.d.ts +7 -8
  9. package/build/licenses/licenseAnalysis.js +15 -29
  10. package/build/licenses/licenseClassification.d.ts +52 -0
  11. package/build/licenses/licenseClassification.js +136 -0
  12. package/build/licenses/licenseExpression.d.ts +37 -0
  13. package/build/licenses/licenseExpression.js +166 -0
  14. package/build/node/common.d.ts +23 -2
  15. package/build/node/common.js +157 -54
  16. package/build/node/index.d.ts +1 -0
  17. package/build/node/index.js +1 -0
  18. package/build/node/mergeLicensesMappings.d.ts +8 -0
  19. package/build/node/mergeLicensesMappings.js +28 -0
  20. package/build/node/utils/aboutLibrariesLicenses.d.ts +11 -0
  21. package/build/node/utils/aboutLibrariesLicenses.js +58 -0
  22. package/build/node/utils/licenseFiles.d.ts +8 -0
  23. package/build/node/utils/licenseFiles.js +45 -0
  24. package/build/node/utils/licensePlistBody.d.ts +6 -0
  25. package/build/node/utils/licensePlistBody.js +20 -0
  26. package/build/node/utils/packageUtils.d.ts +14 -5
  27. package/build/node/utils/packageUtils.js +35 -23
  28. package/build/types/AboutLibrariesLikePackageInfo.d.ts +1 -1
  29. package/build/types/License.d.ts +14 -6
  30. package/build/types/LicenseAnalysisResult.d.ts +7 -0
  31. package/build/types/LicenseExpression.d.ts +39 -0
  32. package/build/types/LicenseExpression.js +2 -0
  33. package/build/types/LicenseFile.d.ts +8 -0
  34. package/build/types/LicenseFile.js +2 -0
  35. package/build/types/LicenseStats.d.ts +1 -1
  36. package/build/types/ScanDependenciesOptions.d.ts +12 -0
  37. package/build/types/ScanDependenciesOptions.js +2 -0
  38. package/build/types/index.d.ts +3 -0
  39. package/build/types/index.js +3 -0
  40. package/package.json +6 -2
  41. package/src/constants/licenses.ts +15 -15
  42. package/src/licenses/OrPolicy.ts +23 -0
  43. package/src/licenses/index.ts +3 -0
  44. package/src/licenses/licenseAnalysis.ts +19 -33
  45. package/src/licenses/licenseClassification.ts +173 -0
  46. package/src/licenses/licenseExpression.ts +200 -0
  47. package/src/node/common.ts +216 -65
  48. package/src/node/index.ts +1 -0
  49. package/src/node/mergeLicensesMappings.ts +32 -0
  50. package/src/node/utils/aboutLibrariesLicenses.ts +69 -0
  51. package/src/node/utils/licenseFiles.ts +50 -0
  52. package/src/node/utils/licensePlistBody.ts +18 -0
  53. package/src/node/utils/packageUtils.ts +37 -26
  54. package/src/types/AboutLibrariesLikePackageInfo.ts +1 -1
  55. package/src/types/License.ts +16 -7
  56. package/src/types/LicenseAnalysisResult.ts +8 -0
  57. package/src/types/LicenseExpression.ts +22 -0
  58. package/src/types/LicenseFile.ts +10 -0
  59. package/src/types/LicenseStats.ts +1 -1
  60. package/src/types/ScanDependenciesOptions.ts +12 -0
  61. package/src/types/index.ts +3 -0
@@ -1,12 +1,33 @@
1
- import type { AboutLibrariesLikePackageInfo, AggregatedLicensesMapping, ScanPackageOptionsFactory } from '../types';
1
+ import type { AboutLibrariesLikePackageInfo, AggregatedLicensesMapping, ScanDependenciesOptions, ScanPackageOptionsFactory } from '../types';
2
+ /**
3
+ * Finds the root directory of the package that a directory belongs to - the closest directory (itself or a parent)
4
+ * containing a `package.json` with a `name` field. Nested `package.json` files without a name
5
+ * (e.g. `lib/commonjs/package.json` containing only `{ "type": "commonjs" }`) are skipped.
6
+ *
7
+ * Works for packages installed in `node_modules` as well as for workspace / linked packages,
8
+ * which are referenced by their real path (outside `node_modules`)
9
+ *
10
+ * @param startDir Absolute path to the directory to start the search from, e.g. the directory of a module file
11
+ * @returns Path to the package root directory or `null` if the directory does not belong to any named package
12
+ */
13
+ export declare function findPackageRoot(startDir: string): string | null;
14
+ /**
15
+ * Collects license information for a given list of package directories, e.g. the packages found in the Metro dependency graph.
16
+ * Unlike {@link scanDependencies}, it does not scan dependencies of the packages - the list is expected to be complete.
17
+ *
18
+ * @param packageRoots Paths to the root directories of the packages (directories containing `package.json`)
19
+ * @returns Aggregated licenses object containing the given packages and their license information
20
+ */
21
+ export declare function scanPackageRoots(packageRoots: string[]): AggregatedLicensesMapping;
2
22
  /**
3
23
  * Scans `package.json` and searches for all packages under `dependencies` field. Supports monorepo projects.
4
24
  *
5
25
  * @param appPackageJsonPath Path to the `package.json` file of the application
6
26
  * @param scanOptionsFactory Factory function to create scan options for dependencies; defaults to {@link PackageUtils.legacyDefaultScanPackageOptionsFactory}
27
+ * @param options Additional options, see {@link ScanDependenciesOptions}
7
28
  * @returns Aggregated licenses object containing all scanned dependencies and their license information
8
29
  */
9
- export declare function scanDependencies(appPackageJsonPath: string, scanOptionsFactory?: ScanPackageOptionsFactory): AggregatedLicensesMapping;
30
+ export declare function scanDependencies(appPackageJsonPath: string, scanOptionsFactory?: ScanPackageOptionsFactory, options?: ScanDependenciesOptions): AggregatedLicensesMapping;
10
31
  /**
11
32
  * Generates LicensePlist-compatible metadata for NPM dependencies as a YAML string.
12
33
  *
@@ -3,6 +3,8 @@ 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.findPackageRoot = findPackageRoot;
7
+ exports.scanPackageRoots = scanPackageRoots;
6
8
  exports.scanDependencies = scanDependencies;
7
9
  exports.generateLicensePlistNPMOutput = generateLicensePlistNPMOutput;
8
10
  exports.writeLicensePlistNPMOutput = writeLicensePlistNPMOutput;
@@ -11,8 +13,109 @@ exports.writeAboutLibrariesNPMOutput = writeAboutLibrariesNPMOutput;
11
13
  const node_fs_1 = __importDefault(require("node:fs"));
12
14
  const node_path_1 = __importDefault(require("node:path"));
13
15
  const glob_1 = require("glob");
16
+ const licenses_1 = require("../licenses");
14
17
  const utils_1 = require("../utils");
18
+ const mergeLicensesMappings_1 = require("./mergeLicensesMappings");
15
19
  const utils_2 = require("./utils");
20
+ /**
21
+ * Finds the root directory of the package that a directory belongs to - the closest directory (itself or a parent)
22
+ * containing a `package.json` with a `name` field. Nested `package.json` files without a name
23
+ * (e.g. `lib/commonjs/package.json` containing only `{ "type": "commonjs" }`) are skipped.
24
+ *
25
+ * Works for packages installed in `node_modules` as well as for workspace / linked packages,
26
+ * which are referenced by their real path (outside `node_modules`)
27
+ *
28
+ * @param startDir Absolute path to the directory to start the search from, e.g. the directory of a module file
29
+ * @returns Path to the package root directory or `null` if the directory does not belong to any named package
30
+ */
31
+ function findPackageRoot(startDir) {
32
+ let dir = startDir;
33
+ // `path.dirname` of the filesystem root returns the root itself, which ends the loop
34
+ while (node_path_1.default.dirname(dir) !== dir) {
35
+ if (hasNamedPackageJson(dir)) {
36
+ return dir;
37
+ }
38
+ dir = node_path_1.default.dirname(dir);
39
+ }
40
+ return hasNamedPackageJson(dir) ? dir : null;
41
+ }
42
+ /**
43
+ * Checks whether a directory contains a valid `package.json` with a `name` field
44
+ *
45
+ * @param dir Path to the directory to check
46
+ */
47
+ function hasNamedPackageJson(dir) {
48
+ const packageJsonPath = node_path_1.default.join(dir, 'package.json');
49
+ if (!node_fs_1.default.existsSync(packageJsonPath)) {
50
+ return false;
51
+ }
52
+ try {
53
+ const packageJson = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, { encoding: 'utf-8' }));
54
+ return Boolean(packageJson.name);
55
+ }
56
+ catch {
57
+ // invalid package.json - treat it as not a package root
58
+ return false;
59
+ }
60
+ }
61
+ /**
62
+ * Collects license information for a given list of package directories, e.g. the packages found in the Metro dependency graph.
63
+ * Unlike {@link scanDependencies}, it does not scan dependencies of the packages - the list is expected to be complete.
64
+ *
65
+ * @param packageRoots Paths to the root directories of the packages (directories containing `package.json`)
66
+ * @returns Aggregated licenses object containing the given packages and their license information
67
+ */
68
+ function scanPackageRoots(packageRoots) {
69
+ const result = {};
70
+ for (const packageRoot of packageRoots) {
71
+ const packageJsonPath = node_path_1.default.join(packageRoot, 'package.json');
72
+ if (!node_fs_1.default.existsSync(packageJsonPath)) {
73
+ console.warn(`[react-native-legal] skipping ${packageRoot} could not find package.json`);
74
+ continue;
75
+ }
76
+ try {
77
+ const packageJson = require(node_path_1.default.resolve(packageJsonPath));
78
+ if (!packageJson.name || packageJson.private === true) {
79
+ continue;
80
+ }
81
+ const licenseInfo = readPackageLicenseInfo(packageJsonPath);
82
+ result[`${licenseInfo.name}@${licenseInfo.version}`] = {
83
+ ...licenseInfo,
84
+ dependencyType: 'dependency',
85
+ requiredVersion: licenseInfo.version,
86
+ parentPackages: [],
87
+ };
88
+ }
89
+ catch (error) {
90
+ console.warn(`[react-native-legal] could not process package.json in ${packageRoot}`);
91
+ }
92
+ }
93
+ return result;
94
+ }
95
+ /**
96
+ * Reads license information of a single package, based only on its own files
97
+ * (package.json and LICENSE file), without any context of how the package was found
98
+ *
99
+ * @param packageJsonPath Path to the package.json file of the package
100
+ */
101
+ function readPackageLicenseInfo(packageJsonPath) {
102
+ const packageJson = require(node_path_1.default.resolve(packageJsonPath));
103
+ const rawLicense = utils_2.PackageUtils.parseLicenseField(packageJson) ?? null;
104
+ const license = (0, licenses_1.parseLicenseExpression)(rawLicense);
105
+ const licenseIds = (0, licenses_1.collectLicenseIds)(license);
106
+ const licenseFiles = utils_2.PackageUtils.readLicenseFiles(node_path_1.default.dirname(packageJsonPath), licenseIds);
107
+ return {
108
+ name: packageJson.name,
109
+ author: utils_2.PackageUtils.parseAuthorField(packageJson),
110
+ description: packageJson.description,
111
+ rawLicense,
112
+ license,
113
+ licenseIds,
114
+ licenseFiles,
115
+ url: utils_2.PackageUtils.parseRepositoryFieldToUrl(packageJson),
116
+ version: packageJson.version,
117
+ };
118
+ }
16
119
  /**
17
120
  * Scans a single package and its dependencies for license information
18
121
  *
@@ -24,7 +127,7 @@ const utils_2 = require("./utils");
24
127
  * @param isOptionalDependency Whether the package is an optional dependency, in which case a warning will not be logged if the corresponding package.json is not found; defaults to `false`
25
128
  * @param parentPackageRoot Optional path to the parent package root, has priority over default root to lock for dependencies in; used to discover different versions of the same package installed in nested node_modules, e.g. suppose `X@1`, `Y@1` where `Y@1` -> `X@2`; then, node_modules would have `X@1`, `Y@1` and `X@2` would be installed to `node_modules/Y/node_modules/X@2`
26
129
  */
27
- function scanPackage(packageName, requiredVersion, processedPackages, result, scanOptionsFactory = utils_2.PackageUtils.legacyDefaultScanPackageOptionsFactory, { parentPackageRoot, parentPackageName, dependencyType, parentPackageRequiredVersion, parentPackageResolvedVersion, }) {
130
+ function scanPackage(packageName, requiredVersion, processedPackages, result, scanOptionsFactory = utils_2.PackageUtils.legacyDefaultScanPackageOptionsFactory, { parentPackageRoot, parentPackageName, dependencyType, parentPackageRequiredVersion, parentPackageResolvedVersion, projectRoot, }) {
28
131
  const requiredVersionPackageKey = `${packageName}@${requiredVersion}`;
29
132
  // Skip if already processed to avoid circular dependencies
30
133
  if (processedPackages.has(requiredVersionPackageKey)) {
@@ -36,7 +139,7 @@ function scanPackage(packageName, requiredVersion, processedPackages, result, sc
36
139
  }
37
140
  processedPackages.add(requiredVersionPackageKey);
38
141
  try {
39
- const localPackageJsonPath = utils_2.PackageUtils.getPackageJsonPath(packageName, parentPackageRoot);
142
+ const localPackageJsonPath = utils_2.PackageUtils.getPackageJsonPath(packageName, parentPackageRoot, projectRoot);
40
143
  if (!localPackageJsonPath) {
41
144
  // do not warn if the package is an optional dependency, it's normal it may not be installed
42
145
  if (!dependencyType.toLowerCase().includes('optional')) {
@@ -46,13 +149,6 @@ function scanPackage(packageName, requiredVersion, processedPackages, result, sc
46
149
  }
47
150
  const localPackageJson = require(node_path_1.default.resolve(localPackageJsonPath));
48
151
  if (localPackageJson.private !== true) {
49
- const licenseFiles = glob_1.glob.sync('LICEN{S,C}E{.md,}', {
50
- cwd: node_path_1.default.dirname(localPackageJsonPath),
51
- absolute: true,
52
- nocase: true,
53
- nodir: true,
54
- ignore: '**/{__tests__,__fixtures__,__mocks__}/**',
55
- });
56
152
  const resolvedVersionPackageKey = `${packageName}@${localPackageJson.version}`;
57
153
  let parentPackageInfo;
58
154
  if (parentPackageName && parentPackageRequiredVersion && parentPackageResolvedVersion) {
@@ -70,14 +166,8 @@ function scanPackage(packageName, requiredVersion, processedPackages, result, sc
70
166
  }
71
167
  else {
72
168
  result[resolvedVersionPackageKey] = {
169
+ ...readPackageLicenseInfo(localPackageJsonPath),
73
170
  name: packageName,
74
- author: utils_2.PackageUtils.parseAuthorField(localPackageJson),
75
- content: licenseFiles?.[0] ? node_fs_1.default.readFileSync(licenseFiles[0], { encoding: 'utf-8' }) : undefined,
76
- file: licenseFiles?.[0] ? licenseFiles[0] : undefined,
77
- description: localPackageJson.description,
78
- type: utils_2.PackageUtils.parseLicenseField(localPackageJson),
79
- url: utils_2.PackageUtils.parseRepositoryFieldToUrl(localPackageJson),
80
- version: localPackageJson.version,
81
171
  requiredVersion,
82
172
  parentPackages: parentPackageInfo ? [parentPackageInfo] : [],
83
173
  parentPackageRequiredVersion,
@@ -121,6 +211,7 @@ function scanPackage(packageName, requiredVersion, processedPackages, result, sc
121
211
  parentPackageName: packageName,
122
212
  parentPackageRequiredVersion: requiredVersion,
123
213
  parentPackageResolvedVersion: localPackageJson.version,
214
+ projectRoot,
124
215
  });
125
216
  }
126
217
  }
@@ -134,13 +225,32 @@ function scanPackage(packageName, requiredVersion, processedPackages, result, sc
134
225
  *
135
226
  * @param appPackageJsonPath Path to the `package.json` file of the application
136
227
  * @param scanOptionsFactory Factory function to create scan options for dependencies; defaults to {@link PackageUtils.legacyDefaultScanPackageOptionsFactory}
228
+ * @param options Additional options, see {@link ScanDependenciesOptions}
137
229
  * @returns Aggregated licenses object containing all scanned dependencies and their license information
138
230
  */
139
- function scanDependencies(appPackageJsonPath, scanOptionsFactory = utils_2.PackageUtils.legacyDefaultScanPackageOptionsFactory) {
140
- const appPackageJson = require(node_path_1.default.resolve(appPackageJsonPath));
141
- const dependencies = appPackageJson.dependencies;
142
- const devDependencies = appPackageJson.devDependencies;
143
- const optionalDependencies = appPackageJson.optionalDependencies;
231
+ function scanDependencies(appPackageJsonPath, scanOptionsFactory = utils_2.PackageUtils.legacyDefaultScanPackageOptionsFactory, options = {}) {
232
+ const { additionalProjectRoots = [] } = options;
233
+ if (!Array.isArray(additionalProjectRoots) ||
234
+ additionalProjectRoots.some((additionalProjectRoot) => typeof additionalProjectRoot !== 'string' || !additionalProjectRoot)) {
235
+ throw new TypeError('[react-native-legal] additionalProjectRoots must be an array of non-empty paths');
236
+ }
237
+ const appRoot = node_fs_1.default.realpathSync(node_path_1.default.dirname(node_path_1.default.resolve(appPackageJsonPath)));
238
+ const additionalPackageJsonPaths = additionalProjectRoots.map((additionalProjectRoot) => {
239
+ const packageJsonPath = node_path_1.default.join(node_path_1.default.resolve(appRoot, additionalProjectRoot), 'package.json');
240
+ if (!node_fs_1.default.existsSync(packageJsonPath)) {
241
+ throw new Error(`[react-native-legal] additional project root "${additionalProjectRoot}" is not a directory with a package.json (${packageJsonPath} not found)`);
242
+ }
243
+ return packageJsonPath;
244
+ });
245
+ return (0, mergeLicensesMappings_1.mergeLicensesMappings)(scanProjectDependencies(appPackageJsonPath, scanOptionsFactory), ...additionalPackageJsonPaths.map((packageJsonPath) => scanProjectDependencies(packageJsonPath, scanOptionsFactory)));
246
+ }
247
+ function scanProjectDependencies(projectPackageJsonPath, scanOptionsFactory) {
248
+ const projectPackageJson = require(node_path_1.default.resolve(projectPackageJsonPath));
249
+ // real path, so that `node_modules` directories of the project's parent directories are found even if the project is reached through a symlink
250
+ const projectRoot = node_fs_1.default.realpathSync(node_path_1.default.dirname(node_path_1.default.resolve(projectPackageJsonPath)));
251
+ const dependencies = projectPackageJson.dependencies;
252
+ const devDependencies = projectPackageJson.devDependencies;
253
+ const optionalDependencies = projectPackageJson.optionalDependencies;
144
254
  const result = {};
145
255
  const processedPackages = new Set();
146
256
  const rootScanOptions = scanOptionsFactory({ isRoot: true, isWorkspacePackage: false });
@@ -161,6 +271,7 @@ function scanDependencies(appPackageJsonPath, scanOptionsFactory = utils_2.Packa
161
271
  for (const [depName, depVersion] of packages) {
162
272
  scanPackage(depName, depVersion, processedPackages, result, scanOptionsFactory, {
163
273
  dependencyType,
274
+ projectRoot,
164
275
  });
165
276
  }
166
277
  }
@@ -183,12 +294,16 @@ function generateLicensePlistNPMOutput(licenses, iosProjectPath) {
183
294
  if (license.name !== normalizedPackageNameWithVersion) {
184
295
  renames[normalizedPackageNameWithVersion] = license.name;
185
296
  }
186
- const relativeLicenseFile = license.file ? node_path_1.default.relative(iosProjectPath, license.file) : undefined;
297
+ // LicensePlist holds one `file`/`body` per package: a single license file is referenced directly,
298
+ // otherwise every license text is concatenated into the body under a per-license heading
299
+ const singleLicenseFile = license.licenseFiles.length === 1 ? license.licenseFiles[0].file : undefined;
187
300
  return {
188
301
  name: normalizedPackageNameWithVersion,
189
302
  version: license.version,
190
303
  ...(license.url && { source: license.url }),
191
- ...(license.file ? { file: relativeLicenseFile } : { body: license.content ?? license.type ?? 'UNKNOWN' }),
304
+ ...(singleLicenseFile
305
+ ? { file: node_path_1.default.relative(iosProjectPath, singleLicenseFile) }
306
+ : { body: utils_2.PackageUtils.buildLicensePlistBody(license) }),
192
307
  };
193
308
  });
194
309
  const excludedEntries = Object.values(licenses).reduce((acc, license) => {
@@ -270,42 +385,28 @@ function writeLicensePlistNPMOutput(licenses, iosProjectPath, plistLikeOutput) {
270
385
  * @see {@link writeAboutLibrariesNPMOutput}
271
386
  */
272
387
  function generateAboutLibrariesNPMOutput(licenses) {
273
- return Object.entries(licenses)
274
- .map(([packageKey, license]) => {
275
- return {
388
+ return Object.entries(licenses).map(([packageKey, license]) => {
389
+ const uniqueId = utils_2.PackageUtils.normalizePackageName(packageKey);
390
+ const licenseJsonPayloads = utils_2.PackageUtils.prepareAboutLibrariesLicenses(license).map(({ name, content }) => ({
391
+ content,
392
+ hash: utils_2.PackageUtils.prepareAboutLibrariesLicenseField(name, content),
393
+ name,
394
+ url: '',
395
+ }));
396
+ const libraryJsonPayload = {
276
397
  artifactVersion: license.version,
277
- content: license.content ?? '',
278
398
  description: license.description ?? '',
279
399
  developers: [{ name: license.author ?? '', organisationUrl: '' }],
280
- licenses: [utils_2.PackageUtils.prepareAboutLibrariesLicenseField(license)],
400
+ licenses: licenseJsonPayloads.map(({ hash }) => hash),
281
401
  name: license.name,
282
402
  tag: '',
283
- type: license.type,
284
- uniqueId: utils_2.PackageUtils.normalizePackageName(packageKey),
403
+ uniqueId,
285
404
  website: license.url,
286
405
  };
287
- })
288
- .map((jsonPayload) => {
289
- const libraryJsonPayload = {
290
- artifactVersion: jsonPayload.artifactVersion,
291
- description: jsonPayload.description,
292
- developers: jsonPayload.developers,
293
- licenses: jsonPayload.licenses,
294
- name: jsonPayload.name,
295
- tag: jsonPayload.tag,
296
- uniqueId: jsonPayload.uniqueId,
297
- website: jsonPayload.website,
298
- };
299
- const licenseJsonPayload = {
300
- content: jsonPayload.content,
301
- hash: jsonPayload.licenses[0],
302
- name: jsonPayload.type ?? '',
303
- url: '',
304
- };
305
406
  return {
306
- normalizedPackageNameWithVersion: jsonPayload.uniqueId,
407
+ normalizedPackageNameWithVersion: uniqueId,
307
408
  libraryJsonPayload,
308
- licenseJsonPayload,
409
+ licenseJsonPayloads,
309
410
  };
310
411
  });
311
412
  }
@@ -345,12 +446,14 @@ function writeAboutLibrariesNPMOutput(licenses, androidProjectPath, aboutLibrari
345
446
  if (!aboutLibrariesLikeOutput) {
346
447
  aboutLibrariesLikeOutput = generateAboutLibrariesNPMOutput(licenses);
347
448
  }
348
- aboutLibrariesLikeOutput.forEach(({ normalizedPackageNameWithVersion, libraryJsonPayload, licenseJsonPayload }) => {
449
+ aboutLibrariesLikeOutput.forEach(({ normalizedPackageNameWithVersion, libraryJsonPayload, licenseJsonPayloads }) => {
349
450
  const libraryJsonFilePath = node_path_1.default.join(aboutLibrariesConfigLibrariesDirPath, `${normalizedPackageNameWithVersion}.json`);
350
- const licenseJsonFilePath = node_path_1.default.join(aboutLibrariesConfigLicensesDirPath, `${licenseJsonPayload.hash}.json`);
351
451
  node_fs_1.default.writeFileSync(libraryJsonFilePath, JSON.stringify(libraryJsonPayload));
352
- if (!node_fs_1.default.existsSync(licenseJsonFilePath)) {
353
- node_fs_1.default.writeFileSync(licenseJsonFilePath, JSON.stringify(licenseJsonPayload));
452
+ for (const licenseJsonPayload of licenseJsonPayloads) {
453
+ const licenseJsonFilePath = node_path_1.default.join(aboutLibrariesConfigLicensesDirPath, `${licenseJsonPayload.hash}.json`);
454
+ if (!node_fs_1.default.existsSync(licenseJsonFilePath)) {
455
+ node_fs_1.default.writeFileSync(licenseJsonFilePath, JSON.stringify(licenseJsonPayload));
456
+ }
354
457
  }
355
458
  });
356
459
  }
@@ -1,3 +1,4 @@
1
1
  export * from './common';
2
+ export * from './mergeLicensesMappings';
2
3
  export * from './utils';
3
4
  export * from '../';
@@ -15,5 +15,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./common"), exports);
18
+ __exportStar(require("./mergeLicensesMappings"), exports);
18
19
  __exportStar(require("./utils"), exports);
19
20
  __exportStar(require("../"), exports);
@@ -0,0 +1,8 @@
1
+ import type { AggregatedLicensesMapping } from '../types';
2
+ /**
3
+ * Combines licenses mappings, e.g. from scans of several project roots, into one.
4
+ *
5
+ * @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
6
+ * @returns A new licenses mapping that shares no objects with the mappings passed in
7
+ */
8
+ export declare function mergeLicensesMappings(...mappings: AggregatedLicensesMapping[]): AggregatedLicensesMapping;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeLicensesMappings = mergeLicensesMappings;
4
+ const utils_1 = require("./utils");
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
+ function mergeLicensesMappings(...mappings) {
12
+ const result = {};
13
+ for (const mapping of mappings) {
14
+ for (const [packageKey, license] of Object.entries(mapping)) {
15
+ const existingLicense = result[packageKey];
16
+ if (!existingLicense) {
17
+ result[packageKey] = structuredClone({ ...license, parentPackages: license.parentPackages ?? [] });
18
+ continue;
19
+ }
20
+ for (const parentPackage of license.parentPackages ?? []) {
21
+ if (!utils_1.MiscUtils.arrayIncludesObject(existingLicense.parentPackages, parentPackage)) {
22
+ existingLicense.parentPackages.push(structuredClone(parentPackage));
23
+ }
24
+ }
25
+ }
26
+ }
27
+ return result;
28
+ }
@@ -0,0 +1,11 @@
1
+ import type { License } from '../../types';
2
+ /**
3
+ * One AboutLibraries license per License Identifier (or the Raw License when it is Unknown). Its name keeps
4
+ * a retained `+` and the License Exception; its content is the linked license file, else every unlinked text
5
+ * when the package has one license or exactly one unlinked text.
6
+ */
7
+ export declare function prepareAboutLibrariesLicenses(license: License): {
8
+ name: string;
9
+ content: string;
10
+ }[];
11
+ export declare function prepareAboutLibrariesLicenseField(name: string, content?: string): string;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prepareAboutLibrariesLicenses = prepareAboutLibrariesLicenses;
4
+ exports.prepareAboutLibrariesLicenseField = prepareAboutLibrariesLicenseField;
5
+ const licenses_1 = require("../../licenses");
6
+ const miscUtils_1 = require("./miscUtils");
7
+ function collectLicenseLeaves(expression) {
8
+ switch (expression.kind) {
9
+ case 'license':
10
+ return [expression];
11
+ case 'and':
12
+ case 'or':
13
+ return [...collectLicenseLeaves(expression.left), ...collectLicenseLeaves(expression.right)];
14
+ case 'unknown':
15
+ return [];
16
+ }
17
+ }
18
+ function getAboutLibrariesEntries(license) {
19
+ const leavesById = new Map(collectLicenseLeaves(license.license).map((leaf) => [leaf.id, leaf]));
20
+ if (leavesById.size > 0) {
21
+ return [...leavesById.values()].map((leaf) => ({ id: leaf.id, name: (0, licenses_1.renderLicenseExpression)(leaf) }));
22
+ }
23
+ // Unknown License: the Raw License is the best name we have
24
+ if (license.rawLicense) {
25
+ return [{ name: license.rawLicense }];
26
+ }
27
+ // nothing declared, but the package ships a license text, so keep it
28
+ if (license.licenseFiles.length > 0) {
29
+ return [{ name: (0, licenses_1.renderLicenseExpression)(license.license) }];
30
+ }
31
+ return [];
32
+ }
33
+ /**
34
+ * One AboutLibraries license per License Identifier (or the Raw License when it is Unknown). Its name keeps
35
+ * a retained `+` and the License Exception; its content is the linked license file, else every unlinked text
36
+ * when the package has one license or exactly one unlinked text.
37
+ */
38
+ function prepareAboutLibrariesLicenses(license) {
39
+ const entries = getAboutLibrariesEntries(license);
40
+ const unlinkedTexts = license.licenseFiles.filter((file) => !file.licenseId).map((file) => file.content);
41
+ // unlinked texts go to every license when there is only one text (it may cover all of them),
42
+ // or when there is only one license (it owns every text)
43
+ const sharedContent = entries.length === 1 || unlinkedTexts.length === 1 ? unlinkedTexts.join('\n\n') : '';
44
+ return entries.map(({ id, name }) => ({
45
+ name,
46
+ content: (id ? license.licenseFiles.find((file) => file.licenseId === id)?.content : undefined) ?? sharedContent,
47
+ }));
48
+ }
49
+ function prepareAboutLibrariesLicenseField(name, content) {
50
+ // The returned value is used as a filename under `android/config/licenses/`.
51
+ // Legacy/compound SPDX expressions like `MIT/X11` or `(MIT OR Apache-2.0)` would
52
+ // otherwise produce paths containing `/`, `(` or spaces, which causes the writer
53
+ // to either fail with ENOENT (when a `/` is interpreted as a subdirectory) or
54
+ // to produce names that are invalid on some filesystems.
55
+ const sanitizedName = name.replace(/[^A-Za-z0-9._-]/g, '_');
56
+ // hash the unsanitized name when there is no text, so that names differing only in sanitized characters stay distinct
57
+ return `${sanitizedName}_${(0, miscUtils_1.sha512)(content || name)}`;
58
+ }
@@ -0,0 +1,8 @@
1
+ import type { LicenseFile } from '../../types';
2
+ /**
3
+ * Reads every license file (`LICEN{S,C}E*`, `COPYING*`, case-insensitive) from the package root.
4
+ *
5
+ * A file is linked to a License Identifier by its name suffix (e.g. `LICENSE-APACHE` → `Apache-2.0`),
6
+ * but only when exactly one of `licenseIds` matches; otherwise it is listed without an identifier.
7
+ */
8
+ export declare function readLicenseFiles(packageDir: string, licenseIds: string[]): LicenseFile[];
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readLicenseFiles = readLicenseFiles;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const glob_1 = require("glob");
10
+ // `LICEN{S,C}E*` also matches code and data files such as `license.js` or `licenses.json`
11
+ const NON_LICENSE_EXTENSIONS = new Set(['.js', '.cjs', '.mjs', '.ts', '.cts', '.mts', '.tsx', '.jsx', '.json', '.map']);
12
+ /**
13
+ * Reads every license file (`LICEN{S,C}E*`, `COPYING*`, case-insensitive) from the package root.
14
+ *
15
+ * A file is linked to a License Identifier by its name suffix (e.g. `LICENSE-APACHE` → `Apache-2.0`),
16
+ * but only when exactly one of `licenseIds` matches; otherwise it is listed without an identifier.
17
+ */
18
+ function readLicenseFiles(packageDir, licenseIds) {
19
+ return glob_1.glob
20
+ .sync('{LICEN{S,C}E*,COPYING*}', { cwd: packageDir, absolute: true, nocase: true, nodir: true })
21
+ .sort()
22
+ .filter((file) => !NON_LICENSE_EXTENSIONS.has(node_path_1.default.extname(file).toLowerCase()))
23
+ .map((file) => {
24
+ const licenseId = linkLicenseId(node_path_1.default.basename(file), licenseIds);
25
+ return {
26
+ file,
27
+ content: node_fs_1.default.readFileSync(file, { encoding: 'utf-8' }),
28
+ ...(licenseId && { licenseId }),
29
+ };
30
+ });
31
+ }
32
+ function linkLicenseId(fileName, licenseIds) {
33
+ const suffix = fileName
34
+ .replace(/\.(txt|md)$/i, '')
35
+ .replace(/^(licen[sc]e|copying)[-_. ]*/i, '')
36
+ .toLowerCase();
37
+ if (!suffix) {
38
+ return undefined;
39
+ }
40
+ const matching = licenseIds.filter((id) => {
41
+ const lowerId = id.toLowerCase();
42
+ return lowerId === suffix || lowerId.startsWith(`${suffix}-`);
43
+ });
44
+ return matching.length === 1 ? matching[0] : undefined;
45
+ }
@@ -0,0 +1,6 @@
1
+ import type { License } from '../../types';
2
+ /**
3
+ * LicensePlist body: every license text under a per-license heading (plain text, as LicensePlist shows
4
+ * bodies verbatim); the rendered License Expression when there is no text.
5
+ */
6
+ export declare function buildLicensePlistBody(license: License): string;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildLicensePlistBody = buildLicensePlistBody;
7
+ const path_1 = __importDefault(require("path"));
8
+ const licenses_1 = require("../../licenses");
9
+ /**
10
+ * LicensePlist body: every license text under a per-license heading (plain text, as LicensePlist shows
11
+ * bodies verbatim); the rendered License Expression when there is no text.
12
+ */
13
+ function buildLicensePlistBody(license) {
14
+ if (license.licenseFiles.length === 0) {
15
+ return (0, licenses_1.renderLicenseExpression)(license.license);
16
+ }
17
+ return license.licenseFiles
18
+ .map(({ file, content, licenseId }) => `=== ${licenseId ?? path_1.default.basename(file)} ===\n\n${content.trim()}`)
19
+ .join('\n\n');
20
+ }
@@ -1,18 +1,27 @@
1
- import type { License, ScanPackageOptionsFactory } from '../../types';
2
- export declare function getPackageJsonPath(dependency: string, root?: string): string | null;
3
- export declare function resolvePackageJsonFromEntry(dependency: string): string | null;
1
+ import type { ScanPackageOptionsFactory } from '../../types';
2
+ export { prepareAboutLibrariesLicenseField, prepareAboutLibrariesLicenses } from './aboutLibrariesLicenses';
3
+ export { buildLicensePlistBody } from './licensePlistBody';
4
+ export { readLicenseFiles } from './licenseFiles';
5
+ export declare function getPackageJsonPath(dependency: string, root?: string, projectRoot?: string): string | null;
6
+ export declare function resolvePackageJsonFromEntry(dependency: string, paths?: string[]): string | null;
4
7
  export declare function findPackageRoot(entryPath: string): string | undefined;
5
8
  export declare function normalizePackageName(packageName: string): string;
6
- export declare function prepareAboutLibrariesLicenseField(license: License): string;
7
9
  export declare function parseAuthorField(json: {
8
10
  author: string | {
9
11
  name: string;
10
12
  };
11
13
  }): string | undefined;
14
+ /**
15
+ * Reads the Raw License from package.json: the `license` string, the legacy `license: { type }` object,
16
+ * or the legacy `licenses: [{ type }, …]` array, whose types are joined with ` OR `.
17
+ */
12
18
  export declare function parseLicenseField(json: {
13
- license: string | {
19
+ license?: string | {
14
20
  type: string;
15
21
  };
22
+ licenses?: Array<{
23
+ type?: string;
24
+ }>;
16
25
  }): string | undefined;
17
26
  export declare function parseRepositoryFieldToUrl(json: {
18
27
  repository: string | {