import fs from 'node:fs'; import path from 'node:path'; import { prepareLocalSpmArtifacts, SPM_ARTIFACTS_DIR_NAME, } from './prepareLocalSpmArtifacts.js'; type CreateLocalSpmPackageOptions = { packageDir: string; frameworkName?: string; }; type CreateLocalSpmPackageResult = { packageManifestPath: string; }; const RESERVED_FRAMEWORK_NAMES = new Set([ 'hermes', 'hermesvm', 'ReactBrownfield', 'Brownie', 'BrownfieldNavigation', 'React', 'ReactNativeDependencies', ]); function requireXcframework(packageDir: string, name: string) { const xcframeworkPath = path.join(packageDir, `${name}.xcframework`); if (!fs.existsSync(xcframeworkPath)) { throw new Error(`Missing required XCFramework: ${name}.xcframework`); } return name; } function optionalXcframework(packageDir: string, name: string) { return fs.existsSync(path.join(packageDir, `${name}.xcframework`)) ? name : null; } function requireHermesXcframework(packageDir: string) { return ( optionalXcframework(packageDir, 'hermesvm') ?? requireXcframework(packageDir, 'hermes') ); } function resolveAppFrameworkName( packageDir: string, explicitFrameworkName?: string ) { if (explicitFrameworkName) { return requireXcframework(packageDir, explicitFrameworkName); } const candidates = fs .readdirSync(packageDir, { withFileTypes: true }) .filter( (entry) => entry.isDirectory() && entry.name.endsWith('.xcframework') && !RESERVED_FRAMEWORK_NAMES.has(path.basename(entry.name, '.xcframework')) ) .map((entry) => path.basename(entry.name, '.xcframework')) .sort(); if (candidates.length === 1 && candidates[0]) { return candidates[0]; } if (candidates.length === 0) { throw new Error( 'Could not resolve the packaged app XCFramework automatically. Pass --scheme explicitly when packaging.' ); } throw new Error( `Found multiple packaged app XCFramework candidates (${candidates.join(', ')}). Pass --scheme explicitly when packaging.` ); } function renderPackageSwift({ packageName, libraryName, targetNames, }: { packageName: string; libraryName: string; targetNames: string[]; }) { const binaryTargets = targetNames .map( (targetName) => ` .binaryTarget(name: "${targetName}", path: "./${SPM_ARTIFACTS_DIR_NAME}/${targetName}.xcframework")` ) .join(',\n'); const targetDependencies = targetNames .map((targetName) => `"${targetName}"`) .join(', '); return `// swift-tools-version: 5.9 import PackageDescription let package = Package( name: "${packageName}", platforms: [ .iOS(.v14), ], products: [ .library(name: "${libraryName}", targets: [${targetDependencies}]), ], targets: [ ${binaryTargets} ] ) `; } function renderReadme({ packageName, libraryName, targetNames, }: { packageName: string; libraryName: string; targetNames: string[]; }) { const frameworks = targetNames.map((targetName) => `- \`${targetName}\``).join('\n'); return `# ${packageName} This is a generated local Swift Package Manager package for the packaged React Native brownfield artifacts. ## Product - Library product: \`${libraryName}\` ## Included Binary Targets ${frameworks} ## How To Use 1. In Xcode, choose **File > Add Package Dependencies...** 2. Click **Add Local...** 3. Select this folder, the one containing \`Package.swift\` and the \`spm-artifacts\` directory 4. Add the \`${libraryName}\` library product to your app target ## Troubleshooting If Xcode builds your host app but the simulator installation fails, check the host app target first: - Make sure the target still includes its app entry point source files, such as \`App.swift\`, \`SceneDelegate\`, \`AppDelegate\`, or equivalent startup files - Make sure the host target uses the same React Native module name that the packaged app registers for its brownfield surface. This repo's example apps use \`RNApp\` for both the plain React Native and Expo variants - If the install error says the app is "missing its bundle executable", the host app target produced an invalid \`.app\` bundle and the issue is not in this generated SPM package - If you are migrating an existing target from direct \`*.xcframework\` linking to local SPM, remove the old direct XCFramework references before building again This folder is generated by \`brownfield package:ios --add-spm-package\`. Re-run that command whenever the packaged XCFrameworks change so this package stays in sync. `; } export function createLocalSpmPackage({ packageDir, frameworkName, }: CreateLocalSpmPackageOptions): CreateLocalSpmPackageResult { const resolvedFrameworkName = resolveAppFrameworkName( packageDir, frameworkName ); const targetNames = [ resolvedFrameworkName, requireHermesXcframework(packageDir), requireXcframework(packageDir, 'ReactBrownfield'), optionalXcframework(packageDir, 'Brownie'), optionalXcframework(packageDir, 'BrownfieldNavigation'), optionalXcframework(packageDir, 'React'), optionalXcframework(packageDir, 'ReactNativeDependencies'), ].filter((targetName): targetName is string => targetName !== null); const packageManifestPath = path.join(packageDir, 'Package.swift'); const readmePath = path.join(packageDir, 'README.md'); prepareLocalSpmArtifacts({ packageDir, targetNames, }); const manifest = renderPackageSwift({ packageName: `${resolvedFrameworkName}Package`, libraryName: resolvedFrameworkName, targetNames, }); const readme = renderReadme({ packageName: `${resolvedFrameworkName}Package`, libraryName: resolvedFrameworkName, targetNames, }); fs.writeFileSync(packageManifestPath, manifest, 'utf8'); fs.writeFileSync(readmePath, readme, 'utf8'); return { packageManifestPath, }; }