UNPKG

@callstack/react-native-legal-shared

Version:
344 lines (343 loc) 14.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.arrayIncludesObject = arrayIncludesObject; exports.scanDependencies = scanDependencies; exports.generateLicensePlistNPMOutput = generateLicensePlistNPMOutput; exports.generateAboutLibrariesNPMOutput = generateAboutLibrariesNPMOutput; const crypto_1 = __importDefault(require("crypto")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const glob_1 = __importDefault(require("glob")); function compareObjects(a, b) { if (a == null || b == null || typeof a !== 'object' || typeof b !== 'object') { return a === b; } const entriesA = Object.entries(a); const entriesB = Object.entries(b); return (entriesA.length === entriesB.length && entriesA .map(([keyA, valueA]) => { const entry = entriesB.find(([keyB]) => keyA === keyB); if (!entry) { return valueA === entry; } const [, valueB] = entry; return compareObjects(valueA, valueB); }) .reduce((acc, curr) => acc && curr, true)); } /** * Makes a deep-check between array items and provided object, returns true if array has provided object. */ function arrayIncludesObject(array, object) { return array?.map((item) => compareObjects(item, object)).reduce((acc, curr) => acc || curr, false); } /** * Scans a single package and its dependencies for license information */ function scanPackage(packageName, version, processedPackages, result) { const packageKey = `${packageName}@${version}`; // Skip if already processed to avoid circular dependencies if (processedPackages.has(packageKey)) { return; } processedPackages.add(packageKey); try { const localPackageJsonPath = getPackageJsonPath(packageName); if (!localPackageJsonPath) { console.warn(`[react-native-legal] skipping ${packageName} 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__}/**', }); result[packageName] = { author: 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: parseLicenseField(localPackageJson), url: parseRepositoryFieldToUrl(localPackageJson), version: localPackageJson.version, }; } const dependencies = localPackageJson.dependencies; const isWorkspacePackage = version.startsWith('workspace:'); if (!isWorkspacePackage) return; if (dependencies) { Object.entries(dependencies).forEach(([depName, depVersion]) => { scanPackage(depName, depVersion, processedPackages, result); }); } } 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. */ function scanDependencies(appPackageJsonPath) { const appPackageJson = require(path_1.default.resolve(appPackageJsonPath)); const dependencies = appPackageJson.dependencies; const result = {}; const processedPackages = new Set(); if (dependencies) { Object.entries(dependencies).forEach(([packageName, version]) => { scanPackage(packageName, version, processedPackages, result); }); } return result; } function needsQuoting(value) { return (value === '' || // empty string /^[#:>|-]/.test(value) || // starts with special char /^['"{}[\],&*#?|<>=!%@`]/.test(value) || // starts with indicator chars /^[\s]|[\s]$/.test(value) || // has leading/trailing whitespace /^[\d.+-]/.test(value) || // looks like a number/bool/null /[\n"'\\\s]/.test(value) || // contains newlines, quotes, backslash, or spaces /^(true|false|yes|no|null|on|off)$/i.test(value) // is a YAML keyword ); } function formatYamlKey(key) { return /[@/_.]/.test(key) ? `"${key}"` : key; } function formatYamlValue(value, indent) { if (value.includes('\n')) { const indentedValue = value .split('\n') .map((line) => `${' '.repeat(indent)}${line}`) .join('\n'); // Return the block indicator on the same line as the content return `|${indentedValue ? '\n' + indentedValue : ''}`; } if (needsQuoting(value)) { if (value.includes("'") && !value.includes('"')) { return `"${value.replace(/["\\]/g, '\\$&')}"`; } return `'${value.replace(/'/g, "''")}'`; } return value; } function toYaml(obj, indent = 0) { const spaces = ' '.repeat(indent); if (obj == null) return ''; if (Array.isArray(obj)) { return obj.map((item) => `${spaces}- ${toYaml(item, indent + 2).trimStart()}`).join('\n'); } if (typeof obj === 'object') { return Object.entries(obj) .filter(([, v]) => v != null) .map(([key, value]) => { const formattedKey = formatYamlKey(key); const formattedValue = toYaml(value, indent + 2); if (Array.isArray(value)) { return `${spaces}${formattedKey}:\n${formattedValue}`; } if (typeof value === 'object' && value !== null) { return `${spaces}${formattedKey}:\n${formattedValue}`; } if (typeof value === 'string' && value.includes('\n')) { return `${spaces}${formattedKey}: ${formattedValue}`; } return `${spaces}${formattedKey}: ${formattedValue}`; }) .join('\n'); } return typeof obj === 'string' ? formatYamlValue(obj, indent) : String(obj); } /** * Generates LicensePlist-compatible metadata for NPM dependencies * * 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 */ function generateLicensePlistNPMOutput(licenses, iosProjectPath) { const renames = {}; const licenseEntries = Object.entries(licenses).map(([dependency, licenseObj]) => { const normalizedName = normalizePackageName(dependency); if (dependency !== normalizedName) { renames[normalizedName] = dependency; } const relativeLicenseFile = licenseObj.file ? path_1.default.relative(iosProjectPath, licenseObj.file) : undefined; return { name: normalizedName, 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', toYaml(yamlDoc), '# END Generated NPM license entries', ].join('\n'); fs_1.default.writeFileSync(path_1.default.join(iosProjectPath, 'license_plist.yml'), yamlContent, { encoding: 'utf-8' }); } /** * 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 */ function generateAboutLibrariesNPMOutput(licenses, androidProjectPath) { 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); } Object.entries(licenses) .map(([dependency, licenseObj]) => { return { artifactVersion: licenseObj.version, content: licenseObj.content ?? '', description: licenseObj.description ?? '', developers: [{ name: licenseObj.author ?? '', organisationUrl: '' }], licenses: [prepareAboutLibrariesLicenseField(licenseObj)], name: dependency, tag: '', type: licenseObj.type, uniqueId: normalizePackageName(dependency), }; }) .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, }; const licenseJsonPayload = { content: jsonPayload.content, hash: jsonPayload.licenses[0], name: jsonPayload.type ?? '', url: '', }; const libraryJsonFilePath = path_1.default.join(aboutLibrariesConfigLibrariesDirPath, `${normalizePackageName(jsonPayload.name)}.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)); } }); } function prepareAboutLibrariesLicenseField(license) { if (!license.type) { return ''; } return `${license.type}_${sha512(license.content ?? license.type)}`; } function sha512(text) { return crypto_1.default.createHash('sha512').update(text).digest('hex'); } function parseAuthorField(json) { if (typeof json.author === 'object' && typeof json.author.name === 'string') { return json.author.name; } if (typeof json.author === 'string') { return json.author; } } function parseLicenseField(json) { if (typeof json.license === 'object' && typeof json.license.type === 'string') { return json.license.type; } if (typeof json.license === 'string') { return json.license; } } function parseRepositoryFieldToUrl(json) { if (typeof json.repository === 'object' && typeof json.repository.url === 'string') { return normalizeRepositoryUrl(json.repository.url); } if (typeof json.repository === 'string') { return normalizeRepositoryUrl(json.repository); } } function normalizeRepositoryUrl(url) { return url .replace('git+ssh://git@', 'git://') .replace('.git', '') .replace('git+https://github.com', 'https://github.com') .replace('.git', '') .replace('git://github.com', 'https://github.com') .replace('.git', '') .replace('git@github.com:', 'https://github.com/') .replace('.git', '') .replace('github:', 'https://github.com/') .replace('.git', ''); } function getPackageJsonPath(dependency, root = process.cwd()) { try { return require.resolve(`${dependency}/package.json`, { paths: [root] }); } catch (error) { const pkgJsonInNodeModules = path_1.default.join(root, 'node_modules', dependency, 'package.json'); return fs_1.default.existsSync(pkgJsonInNodeModules) ? pkgJsonInNodeModules : resolvePackageJsonFromEntry(dependency); } } function resolvePackageJsonFromEntry(dependency) { try { const entryPath = require.resolve(dependency); const packageDir = findPackageRoot(entryPath); if (!packageDir) return null; const packageJsonPath = path_1.default.join(packageDir, 'package.json'); return fs_1.default.existsSync(packageJsonPath) ? packageJsonPath : null; } catch { return null; } } function findPackageRoot(entryPath) { let currentDir = path_1.default.dirname(entryPath); while (currentDir !== path_1.default.dirname(currentDir)) { if (fs_1.default.existsSync(path_1.default.join(currentDir, 'package.json'))) return currentDir; currentDir = path_1.default.dirname(currentDir); } } function normalizePackageName(packageName) { return packageName.replace('/', '_'); }