UNPKG

react-native

Version:

A framework for building native apps using React

315 lines (291 loc) • 10.3 kB
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format */ 'use strict'; /*:: import type {GeneratePackageArgs} from './spm-types'; */ /** * generate-spm-package.js – Generates the xcframeworks sub-package for a * React Native app using prebuilt XCFrameworks via Swift Package Manager. * * Usage: * node generate-spm-package.js [options] * * Options: * --app-root <path> Path to the app directory (default: cwd) * --react-native-root <path> Path to react-native package root * --version <ver> RN version for Maven artifact URLs * --debug-artifacts-dir <path> Complete Debug artifact cache slot * --release-artifacts-dir <path> Complete Release artifact cache slot * --app-name <name> App/package name (default: from package.json) * --target-name <name> Main app target name (default: derived from app-name) * --source-path <path> Path to app source relative to app-root (default: auto-detected) * --ios-version <ver> Minimum iOS version (default: 15) * * Generates build/xcframeworks/Package.swift + symlinks. The xcodeproj * references this sub-package directly; no separate app-level Package.swift * is needed. */ const {prepareFlavoredFrameworks} = require('./flavored-frameworks'); const { REACT_HEADERS_TARGET_DIR, REACT_NATIVE_HEADERS_PRODUCT, REACT_NATIVE_PACKAGE_NAME, REACT_NATIVE_PRODUCTS, REACT_NATIVE_UMBRELLA_PRODUCT, REACT_NATIVE_XCFRAMEWORK_PRODUCTS, deriveAppName, displayPath, findProjectRoot, makeLogger, readPackageJson, resolveReactNativeRoot, toSwiftName, } = require('./spm-utils'); const fs = require('node:fs'); const path = require('node:path'); const yargs = require('yargs'); const {log} = makeLogger('generate-spm-package'); function parseArgs(argv /*: Array<string> */) /*: GeneratePackageArgs */ { const parsed = yargs(argv) .version(false) .option('app-root', { type: 'string', default: process.cwd(), describe: 'Path to the app directory', }) .option('react-native-root', { type: 'string', describe: 'Path to react-native package root', }) .option('version', { type: 'string', describe: 'RN version for Maven artifact URLs', }) .option('debug-artifacts-dir', { type: 'string', describe: 'Path to the complete Debug artifacts cache slot', }) .option('release-artifacts-dir', { type: 'string', describe: 'Path to the complete Release artifacts cache slot', }) .option('app-name', { type: 'string', describe: 'App/package name (default: from package.json)', }) .option('target-name', { type: 'string', describe: 'Main app target name (default: derived from app-name)', }) .option('source-path', { type: 'string', describe: 'Path to app source relative to app-root (default: auto-detected)', }) .option('ios-version', { type: 'string', default: '15', describe: 'Minimum iOS version', }) .usage( 'Usage: $0 [options]\n\nGenerates the xcframeworks sub-package for a React Native app using SPM.', ) .help() .parseSync(); return { appRoot: parsed['app-root'], reactNativeRoot: parsed['react-native-root'] ?? null, version: parsed.version ?? null, debugArtifactsDir: parsed['debug-artifacts-dir'] ?? null, releaseArtifactsDir: parsed['release-artifacts-dir'] ?? null, appName: parsed['app-name'] ?? null, targetName: parsed['target-name'] ?? null, sourcePath: parsed['source-path'] ?? null, iosVersion: parsed['ios-version'], }; } /** * Find the app's main Swift/ObjC source directory. * Looks for directories that contain native iOS source files. */ function findSourcePath( appRoot /*: string */, packageName /*: string */, ) /*: string */ { // Derive from package name (e.g. "@react-native/tester" -> "Tester") const derived = toSwiftName(packageName.replace(/^@[^/]+\//, '')); // Also check "RN" + derived (e.g. "Tester" -> "RNTester") and "RN" + whole name const rnPrefixed = 'RN' + derived; const candidates = [derived, rnPrefixed, 'ios', 'App', 'Sources', 'src']; for (const c of candidates) { if (fs.existsSync(path.join(appRoot, c))) { return c; } } // Scan for a directory that looks like an iOS source root // (contains .m, .mm, .swift, or .h files) try { const entries /*: Array<{name: string, isDirectory(): boolean}> */ = // $FlowFixMe[incompatible-type] Dirent.name is string|Buffer in Flow but always string here fs.readdirSync(appRoot, {withFileTypes: true}); for (const entry of entries) { if (!entry.isDirectory()) continue; if (entry.name.startsWith('.') || entry.name === 'node_modules') continue; const dirPath = path.join(appRoot, entry.name); const subEntries = fs.readdirSync(dirPath); const hasNativeSources = subEntries.some((f /*: string | Buffer */) => /\.(m|mm|swift|cpp|h|hpp)$/.test(String(f)), ); if (hasNativeSources) { return entry.name; } } } catch (_) { // ignore } return derived; } /** * Generates the Package.swift for the xcframeworks sub-package. * * When using local xcframeworks (from the cache), we put the binary targets in * a dedicated Package.swift at build/xcframeworks/. The generated .xcodeproj * references this sub-package via XCLocalSwiftPackageReference; the codegen * Package.swift also imports it as a named package dependency. */ function generateXCFrameworksPackageSwift() /*: string */ { // Each product's target follows from its KIND, not from its position in the // list: the umbrella is a Clang target over the staged headers, and every // xcframework-backed product gets a binaryTarget of the same name. const products = REACT_NATIVE_PRODUCTS.map( product => ` .library(name: "${product}", targets: ["${product}"]),`, ); const targets = [ ` .target( name: "${REACT_NATIVE_UMBRELLA_PRODUCT}", dependencies: ["${REACT_NATIVE_HEADERS_PRODUCT}"], path: "${REACT_HEADERS_TARGET_DIR}", publicHeadersPath: "include" ),`, ...REACT_NATIVE_XCFRAMEWORK_PRODUCTS.map( product => ` .binaryTarget( name: "${product}", path: "${product}.xcframework" ),`, ), ]; return `// swift-tools-version: 6.0 // AUTO-GENERATED by scripts/generate-spm-package.js – do not edit manually. import PackageDescription let package = Package( name: "${REACT_NATIVE_PACKAGE_NAME}", products: [ ${products.join('\n')} ], targets: [ ${targets.join('\n')} ] ) `; } function main( argv /*:: ?: Array<string> */, dependencies /*:: ?: {prepareFlavoredFrameworks?: typeof prepareFlavoredFrameworks} */, ) /*: void */ { const args = parseArgs(argv ?? process.argv.slice(2)); // Ensure appRoot is always absolute so path.join/path.resolve produce absolute paths // even when called with --app-root . or other relative paths. const appRoot = path.resolve(args.appRoot); // Read app package.json // package.json may be in a parent directory (e.g. when appRoot is ios/). const projectRoot = findProjectRoot(appRoot); const pkgJson = readPackageJson(projectRoot); if (!pkgJson) { throw new Error( `[generate-spm-package] No package.json found in ${appRoot} or parent directories`, ); } let rnRoot = args.reactNativeRoot != null ? path.resolve(args.reactNativeRoot) : resolveReactNativeRoot(appRoot, projectRoot); if (rnRoot == null) { throw new Error( '[generate-spm-package] Could not find react-native. Pass --react-native-root.', ); } let version = args.version; if (version == null) { const rnPkg = readPackageJson(rnRoot); version = rnPkg?.version ?? '0.0.0'; } const rawName = pkgJson.name ?? path.basename(appRoot); const sourcePath = args.sourcePath ?? findSourcePath(appRoot, rawName); const appName = args.appName ?? deriveAppName(rawName, sourcePath); const targetName = args.targetName ?? appName + 'App'; log(`App name: ${appName}`); log(`Target name: ${targetName}`); log(`Source path: ${sourcePath}`); log(`Version: ${version}`); const debugArtifactsDir = args.debugArtifactsDir; const releaseArtifactsDir = args.releaseArtifactsDir; if ((debugArtifactsDir == null) !== (releaseArtifactsDir == null)) { throw new Error( '[generate-spm-package] both --debug-artifacts-dir and --release-artifacts-dir are required', ); } if (debugArtifactsDir != null && releaseArtifactsDir != null) { const prepare = dependencies?.prepareFlavoredFrameworks ?? prepareFlavoredFrameworks; prepare({ appRoot, debugArtifactsDir, releaseArtifactsDir, }); const xcfwLinksDir = path.join(appRoot, 'build', 'xcframeworks'); const xcfwPkgContent = generateXCFrameworksPackageSwift(); const xcfwPkgPath = path.join(xcfwLinksDir, 'Package.swift'); let packageChanged = true; try { packageChanged = fs.readFileSync(xcfwPkgPath, 'utf8') !== xcfwPkgContent; } catch {} if (packageChanged) { fs.writeFileSync(xcfwPkgPath, xcfwPkgContent, 'utf8'); } log( `${packageChanged ? 'Generated' : 'Unchanged'}: ${path.relative( appRoot, xcfwPkgPath, )}`, ); log( `Artifact slots: debug=${displayPath(debugArtifactsDir)}, ` + `release=${displayPath(releaseArtifactsDir)}`, ); } else { // Auto-detect an already-generated invariant package. const xcfwLinksDir = path.join(appRoot, 'build', 'xcframeworks'); if (fs.existsSync(path.join(xcfwLinksDir, 'Package.swift'))) { log(`Auto-detected local xcframeworks: build/xcframeworks`); } } } if (require.main === module) { try { main(); } catch (e) { console.error(e.message); process.exitCode = 1; } } module.exports = { main, generateXCFrameworksPackageSwift, findSourcePath, };