@callstack/react-native-legal-shared
Version:
Shared code for all packages
309 lines (308 loc) • 15.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.scanDependencies = scanDependencies;
exports.generateLicensePlistNPMOutput = generateLicensePlistNPMOutput;
exports.writeLicensePlistNPMOutput = writeLicensePlistNPMOutput;
exports.generateAboutLibrariesNPMOutput = generateAboutLibrariesNPMOutput;
exports.writeAboutLibrariesNPMOutput = writeAboutLibrariesNPMOutput;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const glob_1 = __importDefault(require("glob"));
const utils_1 = require("./utils");
/**
* Scans a single package and its dependencies for license information
*
* @param packageName Name of the package to scan
* @param requiredVersion Version of the package to scan; this is the version specifier from package.json, e.g. `^1.0.0`, `~2.3.4`, etc.
* @param processedPackages Set of already processed packages (avoids cycles)
* @param result Aggregated licenses object to store the results; the keys will be in the format of `packageName@version` where version is the resolved version of the package
* @param scanOptionsFactory Factory function to create scan options for dependencies; defaults to {@link PackageUtils.legacyDefaultScanPackageOptionsFactory}
* @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`
* @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`
*/
function scanPackage(packageName, requiredVersion, processedPackages, result, scanOptionsFactory = utils_1.PackageUtils.legacyDefaultScanPackageOptionsFactory, { parentPackageRoot, parentPackageName, dependencyType, parentPackageRequiredVersion, parentPackageResolvedVersion, }) {
const requiredVersionPackageKey = `${packageName}@${requiredVersion}`;
// Skip if already processed to avoid circular dependencies
if (processedPackages.has(requiredVersionPackageKey)) {
return;
}
// If the package is a file: dependency, warn about lack of support
if (requiredVersion.startsWith('file:')) {
console.warn(`[react-native-legal] ${packageName} (${requiredVersion}) is 'file:' dependency. Such packages are not supported yet (see https://callstackincubator.github.io/react-native-legal/docs/programmatic-usage.html#known-limitations).`);
}
processedPackages.add(requiredVersionPackageKey);
try {
const localPackageJsonPath = utils_1.PackageUtils.getPackageJsonPath(packageName, parentPackageRoot);
if (!localPackageJsonPath) {
// do not warn if the package is an optional dependency, it's normal it may not be installed
if (!dependencyType.toLowerCase().includes('optional')) {
console.warn(`[react-native-legal] skipping ${requiredVersionPackageKey} could not find package.json`);
}
return;
}
const localPackageJson = require(path_1.default.resolve(localPackageJsonPath));
if (localPackageJson.private !== true) {
const licenseFiles = glob_1.default.sync('LICEN{S,C}E{.md,}', {
cwd: path_1.default.dirname(localPackageJsonPath),
absolute: true,
nocase: true,
nodir: true,
ignore: '**/{__tests__,__fixtures__,__mocks__}/**',
});
const resolvedVersionPackageKey = `${packageName}@${localPackageJson.version}`;
result[resolvedVersionPackageKey] = {
name: packageName,
author: utils_1.PackageUtils.parseAuthorField(localPackageJson),
content: licenseFiles?.[0] ? fs_1.default.readFileSync(licenseFiles[0], { encoding: 'utf-8' }) : undefined,
file: licenseFiles?.[0] ? licenseFiles[0] : undefined,
description: localPackageJson.description,
type: utils_1.PackageUtils.parseLicenseField(localPackageJson),
url: utils_1.PackageUtils.parseRepositoryFieldToUrl(localPackageJson),
version: localPackageJson.version,
requiredVersion,
parentPackageName,
parentPackageRequiredVersion,
parentPackageResolvedVersion,
dependencyType,
};
}
const dependencies = localPackageJson.dependencies;
const devDependencies = localPackageJson.devDependencies;
const optionalDependencies = localPackageJson.optionalDependencies;
const isWorkspacePackage = requiredVersion.startsWith('workspace:');
const scanOptions = scanOptionsFactory({
isRoot: false,
isWorkspacePackage,
});
// check if transitive dependencies should be scanned
if (!scanOptions.includeTransitiveDependencies) {
return;
}
// helper used for finding nested dependencies installed with different versions for a given package, see docstring of scanPackage
const currentPackageRoot = path_1.default.dirname(localPackageJsonPath);
for (const { dependencyType, packages } of [
{
dependencyType: 'transitiveDependency',
packages: dependencies ? Object.entries(dependencies) : [],
},
{
dependencyType: 'transitiveDevDependency',
packages: devDependencies && scanOptions.includeDevDependencies ? Object.entries(devDependencies) : [],
},
{
dependencyType: 'transitiveOptionalDependency',
packages: optionalDependencies && scanOptions.includeOptionalDependencies ? Object.entries(optionalDependencies) : [],
},
]) {
for (const [depName, depVersion] of packages) {
scanPackage(depName, depVersion, processedPackages, result, scanOptionsFactory, {
dependencyType,
parentPackageRoot: currentPackageRoot,
parentPackageName: packageName,
parentPackageRequiredVersion: requiredVersion,
parentPackageResolvedVersion: localPackageJson.version,
});
}
}
}
catch (error) {
console.warn(`[react-native-legal] could not process package.json for ${packageName}`);
}
}
/**
* Scans `package.json` and searches for all packages under `dependencies` field. Supports monorepo projects.
*
* @param appPackageJsonPath Path to the `package.json` file of the application
* @param scanOptionsFactory Factory function to create scan options for dependencies; defaults to {@link PackageUtils.legacyDefaultScanPackageOptionsFactory}
* @returns Aggregated licenses object containing all scanned dependencies and their license information
*/
function scanDependencies(appPackageJsonPath, scanOptionsFactory = utils_1.PackageUtils.legacyDefaultScanPackageOptionsFactory) {
const appPackageJson = require(path_1.default.resolve(appPackageJsonPath));
const dependencies = appPackageJson.dependencies;
const devDependencies = appPackageJson.devDependencies;
const optionalDependencies = appPackageJson.optionalDependencies;
const result = {};
const processedPackages = new Set();
const rootScanOptions = scanOptionsFactory({ isRoot: true, isWorkspacePackage: false });
for (const { dependencyType, packages } of [
{
dependencyType: 'dependency',
packages: dependencies ? Object.entries(dependencies) : [],
},
{
dependencyType: 'devDependency',
packages: devDependencies && rootScanOptions.includeDevDependencies ? Object.entries(devDependencies) : [],
},
{
dependencyType: 'optionalDependency',
packages: optionalDependencies && rootScanOptions.includeOptionalDependencies ? Object.entries(optionalDependencies) : [],
},
]) {
for (const [depName, depVersion] of packages) {
scanPackage(depName, depVersion, processedPackages, result, scanOptionsFactory, {
dependencyType,
});
}
}
return result;
}
/**
* Generates LicensePlist-compatible metadata for NPM dependencies as a YAML string.
*
* To write a file directly, use `writeLicensePlistNPMOutput` function.
*
* @param licenses Scanned NPM licenses
* @param iosProjectPath Path to the iOS project directory
* @see {@link writeLicensePlistNPMOutput}
*/
function generateLicensePlistNPMOutput(licenses, iosProjectPath) {
const renames = {};
const licenseEntries = Object.entries(licenses).map(([packageKey, licenseObj]) => {
const normalizedPackageNameWithVersion = utils_1.PackageUtils.normalizePackageName(packageKey);
if (licenseObj.name !== normalizedPackageNameWithVersion) {
renames[normalizedPackageNameWithVersion] = licenseObj.name;
}
const relativeLicenseFile = licenseObj.file ? path_1.default.relative(iosProjectPath, licenseObj.file) : undefined;
return {
name: normalizedPackageNameWithVersion,
version: licenseObj.version,
...(licenseObj.url && { source: licenseObj.url }),
...(licenseObj.file
? { file: relativeLicenseFile }
: { body: licenseObj.content ?? licenseObj.type ?? 'UNKNOWN' }),
};
});
const yamlDoc = {
...(Object.keys(renames).length > 0 && { rename: renames }),
manual: licenseEntries,
};
const yamlContent = [
'# BEGIN Generated NPM license entries',
utils_1.YamlUtils.toYaml(yamlDoc),
'# END Generated NPM license entries',
].join('\n');
return yamlContent;
}
/**
* Writes LicensePlist-compatible metadata for NPM dependencies to a file
*
* This will take scanned NPM licenses and produce following output inside iOS project's directory:
*
* ```
* | - ios
* | ---- myawesomeapp
* | ---- myawesomeapp.xcodeproj
* | ---- myawesomeapp.xcodeworkspace
* | ---- license_plist.yml <--- generated LicensePlist config with NPM dependencies
* | ---- Podfile
* | ---- Podfile.lock
* ```
*
* @param licenses Scanned NPM licenses
* @param iosProjectPath Path to the iOS project directory
* @param plistLikeOutput Optional pre-generated string output to use instead of generating it using `generateLicensePlistNPMOutput`
* @see {@link generateLicensePlistNPMOutput}
*/
function writeLicensePlistNPMOutput(licenses, iosProjectPath, plistLikeOutput) {
if (!plistLikeOutput) {
plistLikeOutput = generateLicensePlistNPMOutput(licenses, iosProjectPath);
}
fs_1.default.writeFileSync(path_1.default.join(iosProjectPath, 'license_plist.yml'), plistLikeOutput, { encoding: 'utf-8' });
}
/**
* Generates AboutLibraries-compatible metadata for NPM dependencies
*
* This will take scanned NPM licenses and produce output that can be modified and/or written to the Android project files.
*
* @param licenses Scanned NPM licenses
* @returns Array of AboutLibrariesLikePackage objects, each representing a NPM dependency
* @see {@link writeAboutLibrariesNPMOutput}
*/
function generateAboutLibrariesNPMOutput(licenses) {
return Object.entries(licenses)
.map(([packageKey, licenseObj]) => {
return {
artifactVersion: licenseObj.version,
content: licenseObj.content ?? '',
description: licenseObj.description ?? '',
developers: [{ name: licenseObj.author ?? '', organisationUrl: '' }],
licenses: [utils_1.PackageUtils.prepareAboutLibrariesLicenseField(licenseObj)],
name: licenseObj.name,
tag: '',
type: licenseObj.type,
uniqueId: utils_1.PackageUtils.normalizePackageName(packageKey),
website: licenseObj.url,
};
})
.map((jsonPayload) => {
const libraryJsonPayload = {
artifactVersion: jsonPayload.artifactVersion,
description: jsonPayload.description,
developers: jsonPayload.developers,
licenses: jsonPayload.licenses,
name: jsonPayload.name,
tag: jsonPayload.tag,
uniqueId: jsonPayload.uniqueId,
website: jsonPayload.website,
};
const licenseJsonPayload = {
content: jsonPayload.content,
hash: jsonPayload.licenses[0],
name: jsonPayload.type ?? '',
url: '',
};
return {
normalizedPackageNameWithVersion: jsonPayload.uniqueId,
libraryJsonPayload,
licenseJsonPayload,
};
});
}
/**
* Generates AboutLibraries-compatible metadata for NPM dependencies
*
* This will take scanned NPM licenses and produce following output inside android project's directory:
*
* ```
* | - android
* | ---- app
* | ---- config <--- generated AboutLibraries config directory
* | ------- libraries <--- generated directory with JSON files list of NPM dependencies
* | ------- licenses <--- generated directory with JSON files list of used licenses
* | ---- build.gradle
* | ---- settings.gradle
* ```
*
* @param licenses Scanned NPM licenses
* @param androidProjectPath Path to the Android project directory
* @param aboutLibrariesLikeOutput Optional pre-generated output to use instead of generating it using `generateAboutLibrariesNPMOutput`
* @see {@link generateAboutLibrariesNPMOutput}
*/
function writeAboutLibrariesNPMOutput(licenses, androidProjectPath, aboutLibrariesLikeOutput) {
const aboutLibrariesConfigDirPath = path_1.default.join(androidProjectPath, 'config');
const aboutLibrariesConfigLibrariesDirPath = path_1.default.join(aboutLibrariesConfigDirPath, 'libraries');
const aboutLibrariesConfigLicensesDirPath = path_1.default.join(aboutLibrariesConfigDirPath, 'licenses');
if (!fs_1.default.existsSync(aboutLibrariesConfigDirPath)) {
fs_1.default.mkdirSync(aboutLibrariesConfigDirPath);
}
if (!fs_1.default.existsSync(aboutLibrariesConfigLibrariesDirPath)) {
fs_1.default.mkdirSync(aboutLibrariesConfigLibrariesDirPath);
}
if (!fs_1.default.existsSync(aboutLibrariesConfigLicensesDirPath)) {
fs_1.default.mkdirSync(aboutLibrariesConfigLicensesDirPath);
}
if (!aboutLibrariesLikeOutput) {
aboutLibrariesLikeOutput = generateAboutLibrariesNPMOutput(licenses);
}
aboutLibrariesLikeOutput.forEach(({ normalizedPackageNameWithVersion, libraryJsonPayload, licenseJsonPayload }) => {
const libraryJsonFilePath = path_1.default.join(aboutLibrariesConfigLibrariesDirPath, `${normalizedPackageNameWithVersion}.json`);
const licenseJsonFilePath = path_1.default.join(aboutLibrariesConfigLicensesDirPath, `${licenseJsonPayload.hash}.json`);
fs_1.default.writeFileSync(libraryJsonFilePath, JSON.stringify(libraryJsonPayload));
if (!fs_1.default.existsSync(licenseJsonFilePath)) {
fs_1.default.writeFileSync(licenseJsonFilePath, JSON.stringify(licenseJsonPayload));
}
});
}