@callstack/brownfield-cli 3.9.0 → 3.11.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 (30) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/brownfield/commands/packageIos.d.ts +1 -0
  3. package/dist/brownfield/commands/packageIos.d.ts.map +1 -1
  4. package/dist/brownfield/commands/packageIos.js +97 -6
  5. package/dist/brownfield/utils/copyDebugBundleToSimulatorSlice.d.ts +8 -0
  6. package/dist/brownfield/utils/copyDebugBundleToSimulatorSlice.d.ts.map +1 -0
  7. package/dist/brownfield/utils/copyDebugBundleToSimulatorSlice.js +21 -0
  8. package/dist/brownfield/utils/createLocalSpmPackage.d.ts +10 -0
  9. package/dist/brownfield/utils/createLocalSpmPackage.d.ts.map +1 -0
  10. package/dist/brownfield/utils/createLocalSpmPackage.js +138 -0
  11. package/dist/brownfield/utils/prepareLocalSpmArtifacts.d.ts +8 -0
  12. package/dist/brownfield/utils/prepareLocalSpmArtifacts.d.ts.map +1 -0
  13. package/dist/brownfield/utils/prepareLocalSpmArtifacts.js +77 -0
  14. package/dist/brownfield/utils/project.d.ts +1 -0
  15. package/dist/brownfield/utils/project.d.ts.map +1 -1
  16. package/dist/brownfield/utils/project.js +8 -0
  17. package/dist/brownfield/utils/resolvePackagedFrameworkName.d.ts +14 -0
  18. package/dist/brownfield/utils/resolvePackagedFrameworkName.d.ts.map +1 -0
  19. package/dist/brownfield/utils/resolvePackagedFrameworkName.js +61 -0
  20. package/dist/brownfield/utils/supportsPrebuiltRNCore.d.ts +18 -0
  21. package/dist/brownfield/utils/supportsPrebuiltRNCore.d.ts.map +1 -0
  22. package/dist/brownfield/utils/supportsPrebuiltRNCore.js +36 -0
  23. package/package.json +6 -6
  24. package/src/brownfield/commands/packageIos.ts +288 -129
  25. package/src/brownfield/utils/copyDebugBundleToSimulatorSlice.ts +58 -0
  26. package/src/brownfield/utils/createLocalSpmPackage.ts +208 -0
  27. package/src/brownfield/utils/prepareLocalSpmArtifacts.ts +117 -0
  28. package/src/brownfield/utils/project.ts +9 -0
  29. package/src/brownfield/utils/resolvePackagedFrameworkName.ts +98 -0
  30. package/src/brownfield/utils/supportsPrebuiltRNCore.ts +67 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @callstack/brownfield-cli
2
2
 
3
+ ## 3.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#358](https://github.com/callstack/react-native-brownfield/pull/358) [`3715ac7`](https://github.com/callstack/react-native-brownfield/commit/3715ac7783000756ef1c66ecfe24a85c5e43abab) Thanks [@adamTrz](https://github.com/adamTrz)! - Add `--add-spm-package` to `brownfield package:ios` so packaging can also generate a local Swift Package Manager wrapper around the produced XCFrameworks, including a generated `Package.swift`, `README.md`, and Xcode integration instructions. Fail fast when Debug packaging cannot resolve the app framework name while local SPM output is requested.
8
+
9
+ - [#364](https://github.com/callstack/react-native-brownfield/pull/364) [`05c557d`](https://github.com/callstack/react-native-brownfield/commit/05c557da9e2b6fca02e6e1a0b0fe71a909bab15f) Thanks [@hurali97](https://github.com/hurali97)! - keep only one reference of xcframeworks when spm enabled
10
+
11
+ ## 3.10.0
12
+
13
+ ### Minor Changes
14
+
15
+ - [#323](https://github.com/callstack/react-native-brownfield/pull/323) [`3456d3a`](https://github.com/callstack/react-native-brownfield/commit/3456d3aded18002475def4b79889979e76e1db5e) Thanks [@artus9033](https://github.com/artus9033)! - Support RN prebuilts in Brownfield, by default enabled in RN >= 0.84, opt-in in RN 0.83; or in Expo 55+ (Expo 54 is not supported).
16
+
17
+ Add `--use-prebuilt-rn-core` to `brownfield package:ios` so callers can opt into or out of React Native Apple prebuilt binaries; omitting the flag defers to version-aware defaults handled by Rock. The CLI rejects `--use-prebuilt-rn-core` when React Native is older than 0.81 or when the project is Expo SDK older than 55.
18
+
19
+ Fix brownfield framework dylib install names to use @rpath instead of hardcoded paths.
20
+
3
21
  ## 3.9.0
4
22
 
5
23
  ### Minor Changes
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import { ExampleUsage } from '../../shared/index.js';
3
+ export declare function parseUsePrebuiltRnCoreArgument(value: string | boolean): boolean;
3
4
  export declare const packageIosCommand: Command;
4
5
  export declare const packageIosExample: ExampleUsage;
5
6
  //# sourceMappingURL=packageIos.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"packageIos.d.ts","sourceRoot":"","sources":["../../../src/brownfield/commands/packageIos.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,OAAO,EAGL,YAAY,EACb,MAAM,uBAAuB,CAAC;AAK/B,eAAO,MAAM,iBAAiB,SAgJ7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,cAG7B,CAAC"}
1
+ {"version":3,"file":"packageIos.d.ts","sourceRoot":"","sources":["../../../src/brownfield/commands/packageIos.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAK5C,OAAO,EAGL,YAAY,EACb,MAAM,uBAAuB,CAAC;AAe/B,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,MAAM,GAAG,OAAO,GACtB,OAAO,CAcT;AAqBD,eAAO,MAAM,iBAAiB,SA8P3B,CAAC;AAEJ,eAAO,MAAM,iBAAiB,cAG7B,CAAC"}
@@ -2,22 +2,69 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { getBuildOptions, mergeFrameworks, } from '@rock-js/platform-apple-helpers';
4
4
  import { packageIosAction } from '@rock-js/plugin-brownfield-ios';
5
- import { colorLink, getReactNativeVersion, logger, relativeToCwd, } from '@rock-js/tools';
6
- import { Command } from 'commander';
5
+ import { RockError, colorLink, getReactNativeVersion, logger, relativeToCwd, } from '@rock-js/tools';
6
+ import { Command, Option } from 'commander';
7
7
  import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
8
8
  import { getProjectInfo } from '../utils/project.js';
9
+ import { supportsPrebuiltRNCore } from '../utils/supportsPrebuiltRNCore.js';
9
10
  import { actionRunner, curryOptions, ExampleUsage, } from '../../shared/index.js';
10
11
  import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
11
12
  import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js';
13
+ import { copyDebugBundleToSimulatorSlice } from '../utils/copyDebugBundleToSimulatorSlice.js';
14
+ import { resolvePackagedFrameworkName } from '../utils/resolvePackagedFrameworkName.js';
12
15
  import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js';
16
+ import { createLocalSpmPackage } from '../utils/createLocalSpmPackage.js';
17
+ /** Help text for `--use-prebuilt-rn-core` (keep in sync with docs/docs/docs/getting-started/ios.mdx, "React Native Prebuilts" section). */
18
+ const USE_PREBUILT_RN_CORE_HELP = 'Whether the Xcode build for packaging should use React Native Apple prebuilt binaries (via CocoaPods). ' +
19
+ 'If you omit this flag, Brownfield follows version-aware defaults: for React Native 0.84 and newer, prebuilts are enabled by default; for RN 0.83, they are disabled unless you opt in. ' +
20
+ 'Pass true or false to force either behavior. Use the flag without a value as shorthand for true (same as `--use-prebuilt-rn-core true`). ' +
21
+ 'See the Brownfield iOS guide for details: https://oss.callstack.com/react-native-brownfield/docs/getting-started/ios#react-native-prebuilts';
22
+ export function parseUsePrebuiltRnCoreArgument(value) {
23
+ if (typeof value === 'boolean') {
24
+ return value;
25
+ }
26
+ const normalized = value.trim().toLowerCase();
27
+ if (normalized === 'true' || normalized === '1') {
28
+ return true;
29
+ }
30
+ if (normalized === 'false' || normalized === '0') {
31
+ return false;
32
+ }
33
+ throw new RockError(`Invalid value for --use-prebuilt-rn-core: expected true or false, received "${value}"`);
34
+ }
35
+ function getPackagedFrameworkResolutionFailureMessage({ resolution, candidates, }) {
36
+ return resolution === 'ambiguous'
37
+ ? `found multiple bundled framework candidates (${candidates?.join(', ') ?? 'none'}); pass --scheme explicitly`
38
+ : 'could not resolve the packaged framework output automatically; pass --scheme explicitly';
39
+ }
13
40
  export const packageIosCommand = curryOptions(new Command('package:ios').description('Build iOS XCFramework'), getBuildOptions({ platformName: 'ios' }).map((option) => option.name.startsWith('--build-folder')
14
41
  ? {
15
42
  ...option,
16
43
  description: option.description +
17
44
  " By default, the '.brownfield/build' path will be used.",
18
45
  }
19
- : option)).action(actionRunner(async (options) => {
46
+ : option))
47
+ .addOption(new Option('--use-prebuilt-rn-core [bool]', USE_PREBUILT_RN_CORE_HELP)
48
+ .preset(true)
49
+ .argParser(parseUsePrebuiltRnCoreArgument))
50
+ .addOption(new Option('--add-spm-package', 'Generate a local Swift Package Manager manifest next to the packaged XCFramework outputs'))
51
+ .action(actionRunner(async (options) => {
20
52
  const { projectRoot, platformConfig, userConfig } = getProjectInfo('ios');
53
+ const prebuiltRNCoreSupport = supportsPrebuiltRNCore({ projectRoot });
54
+ // version-aware default when the flag is omitted (see ios.mdx "React Native Prebuilts")
55
+ options.usePrebuiltRnCore ??= prebuiltRNCoreSupport.supported
56
+ ? prebuiltRNCoreSupport.enabledByDefault
57
+ : false;
58
+ if (prebuiltRNCoreSupport) {
59
+ logger.info(`${options.usePrebuiltRnCore ? 'Using' : 'Not using'} prebuilt RN core`);
60
+ if (!prebuiltRNCoreSupport.enabledByDefault &&
61
+ !options.usePrebuiltRnCore) {
62
+ logger.info('Your environment supports prebuilt RN Core as an opt-in feature, but it is disabled by default. Pass --use-prebuilt-rn-core to enable it.');
63
+ }
64
+ }
65
+ if (options.usePrebuiltRnCore && !prebuiltRNCoreSupport.supported) {
66
+ throw new RockError(prebuiltRNCoreSupport.reason);
67
+ }
21
68
  await runExpoPrebuildIfNeeded({ projectRoot, platform: 'ios' });
22
69
  if (!userConfig.project.ios) {
23
70
  throw new Error('iOS project not found.');
@@ -35,6 +82,7 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
35
82
  dotBrownfieldDir = path.join(projectRoot, dotBrownfieldDir);
36
83
  }
37
84
  options.buildFolder ??= path.join(dotBrownfieldDir, 'build');
85
+ options.verbose ??= logger.isVerbose();
38
86
  // The new_architecture.rb script scans Info.plist and fails on binary plist files,
39
87
  // which is the case for our XCFrameworks.
40
88
  // We're reusing the "build" directory which is excluded from the scan.
@@ -50,10 +98,46 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
50
98
  // e.g. '0.82' (note the missing patch component),
51
99
  // therefore we resolve it manually from RN's package.json using Rock's utils
52
100
  reactNativeVersion: getReactNativeVersion(projectRoot),
53
- usePrebuiltRNCore: false, // for brownfield, it is required to build RN from source
54
101
  packageDir, // the output directory for artifacts
55
102
  skipCache: true, // cache is dependent on existence of Rock config file
103
+ usePrebuiltRNCore: options.usePrebuiltRnCore,
56
104
  }, platformConfig);
105
+ const productsPath = path.join(options.buildFolder, 'Build', 'Products');
106
+ const { frameworkName, resolution, candidates } = resolvePackagedFrameworkName({
107
+ explicitScheme: options.scheme,
108
+ productsPath,
109
+ configuration,
110
+ });
111
+ if (!frameworkName && options.addSpmPackage) {
112
+ throw new RockError(`Cannot generate local SPM package: ${getPackagedFrameworkResolutionFailureMessage({
113
+ resolution,
114
+ candidates,
115
+ })}`);
116
+ }
117
+ if (frameworkName) {
118
+ copyDebugBundleToSimulatorSlice({
119
+ productsPath,
120
+ configuration,
121
+ frameworkName,
122
+ });
123
+ if (configuration.includes('Debug')) {
124
+ // Re-merge only Debug frameworks so the simulator slice includes main.jsbundle.
125
+ await mergeFrameworks({
126
+ sourceDir: userConfig.project.ios.sourceDir,
127
+ frameworkPaths: [
128
+ path.join(productsPath, `${configuration}-iphoneos`, `${frameworkName}.framework`),
129
+ path.join(productsPath, `${configuration}-iphonesimulator`, `${frameworkName}.framework`),
130
+ ],
131
+ outputPath: path.join(packageDir, `${frameworkName}.xcframework`),
132
+ });
133
+ }
134
+ }
135
+ else if (configuration.includes('Debug')) {
136
+ logger.warn(`Skipping Debug simulator JS bundle copy: ${getPackagedFrameworkResolutionFailureMessage({
137
+ resolution,
138
+ candidates,
139
+ })}`);
140
+ }
57
141
  const reactBrownfieldXcframeworkPath = path.join(packageDir, 'ReactBrownfield.xcframework');
58
142
  if (fs.existsSync(reactBrownfieldXcframeworkPath)) {
59
143
  // Strip the binary from ReactBrownfield.xcframework to make it interface-only.
@@ -62,7 +146,6 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
62
146
  stripFrameworkBinary(reactBrownfieldXcframeworkPath);
63
147
  }
64
148
  if (hasBrownie) {
65
- const productsPath = path.join(options.buildFolder, 'Build', 'Products');
66
149
  const brownieOutputPath = path.join(packageDir, 'Brownie.xcframework');
67
150
  await mergeFrameworks({
68
151
  sourceDir: userConfig.project.ios.sourceDir,
@@ -79,7 +162,6 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
79
162
  logger.success(`Brownie.xcframework created at ${colorLink(relativeToCwd(brownieOutputPath))}`);
80
163
  }
81
164
  if (hasNavigation) {
82
- const productsPath = path.join(options.buildFolder, 'Build', 'Products');
83
165
  const brownfieldNavigationOutputPath = path.join(packageDir, 'BrownfieldNavigation.xcframework');
84
166
  await mergeFrameworks({
85
167
  sourceDir: userConfig.project.ios.sourceDir,
@@ -92,5 +174,14 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
92
174
  stripFrameworkBinary(brownfieldNavigationOutputPath);
93
175
  logger.success(`BrownfieldNavigation.xcframework created at ${colorLink(relativeToCwd(brownfieldNavigationOutputPath))}`);
94
176
  }
177
+ if (options.addSpmPackage) {
178
+ const { packageManifestPath } = createLocalSpmPackage({
179
+ packageDir,
180
+ frameworkName: frameworkName ?? undefined,
181
+ });
182
+ logger.success(`Local SPM package manifest created at ${colorLink(relativeToCwd(packageManifestPath))}`);
183
+ logger.info(`Add the local package folder in Xcode: ${colorLink(relativeToCwd(packageDir))}`);
184
+ logger.info("In Xcode, choose File > Add Package Dependencies..., click Add Local..., and select that folder.");
185
+ }
95
186
  }));
96
187
  export const packageIosExample = new ExampleUsage('package:ios --scheme BrownfieldLib --configuration Release', "Build iOS XCFramework for 'BrownfieldLib' scheme in Release configuration");
@@ -0,0 +1,8 @@
1
+ interface CopyDebugBundleToSimulatorSliceOptions {
2
+ productsPath: string;
3
+ configuration: string;
4
+ frameworkName: string;
5
+ }
6
+ export declare function copyDebugBundleToSimulatorSlice({ productsPath, configuration, frameworkName, }: CopyDebugBundleToSimulatorSliceOptions): void;
7
+ export {};
8
+ //# sourceMappingURL=copyDebugBundleToSimulatorSlice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copyDebugBundleToSimulatorSlice.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/copyDebugBundleToSimulatorSlice.ts"],"names":[],"mappings":"AAKA,UAAU,sCAAsC;IAC9C,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,+BAA+B,CAAC,EAC9C,YAAY,EACZ,aAAa,EACb,aAAa,GACd,EAAE,sCAAsC,QA0CxC"}
@@ -0,0 +1,21 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { colorLink, logger, relativeToCwd } from '@rock-js/tools';
4
+ export function copyDebugBundleToSimulatorSlice({ productsPath, configuration, frameworkName, }) {
5
+ if (!configuration.includes('Debug')) {
6
+ return;
7
+ }
8
+ const deviceBundlePath = path.join(productsPath, `${configuration}-iphoneos`, `${frameworkName}.framework`, 'main.jsbundle');
9
+ const simulatorFrameworkPath = path.join(productsPath, `${configuration}-iphonesimulator`, `${frameworkName}.framework`);
10
+ const simulatorBundlePath = path.join(simulatorFrameworkPath, 'main.jsbundle');
11
+ if (!fs.existsSync(deviceBundlePath)) {
12
+ logger.warn(`Skipping simulator JS bundle copy: missing ${relativeToCwd(deviceBundlePath)}`);
13
+ return;
14
+ }
15
+ if (!fs.existsSync(simulatorFrameworkPath)) {
16
+ logger.warn(`Skipping simulator JS bundle copy: missing ${relativeToCwd(simulatorFrameworkPath)}`);
17
+ return;
18
+ }
19
+ fs.copyFileSync(deviceBundlePath, simulatorBundlePath);
20
+ logger.success(`Copied Debug JS bundle to simulator slice at ${colorLink(relativeToCwd(simulatorBundlePath))}`);
21
+ }
@@ -0,0 +1,10 @@
1
+ type CreateLocalSpmPackageOptions = {
2
+ packageDir: string;
3
+ frameworkName?: string;
4
+ };
5
+ type CreateLocalSpmPackageResult = {
6
+ packageManifestPath: string;
7
+ };
8
+ export declare function createLocalSpmPackage({ packageDir, frameworkName, }: CreateLocalSpmPackageOptions): CreateLocalSpmPackageResult;
9
+ export {};
10
+ //# sourceMappingURL=createLocalSpmPackage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createLocalSpmPackage.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/createLocalSpmPackage.ts"],"names":[],"mappings":"AAQA,KAAK,4BAA4B,GAAG;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,KAAK,2BAA2B,GAAG;IACjC,mBAAmB,EAAE,MAAM,CAAC;CAC7B,CAAC;AAuJF,wBAAgB,qBAAqB,CAAC,EACpC,UAAU,EACV,aAAa,GACd,EAAE,4BAA4B,GAAG,2BAA2B,CAsC5D"}
@@ -0,0 +1,138 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { prepareLocalSpmArtifacts, SPM_ARTIFACTS_DIR_NAME, } from './prepareLocalSpmArtifacts.js';
4
+ const RESERVED_FRAMEWORK_NAMES = new Set([
5
+ 'hermes',
6
+ 'hermesvm',
7
+ 'ReactBrownfield',
8
+ 'Brownie',
9
+ 'BrownfieldNavigation',
10
+ 'React',
11
+ 'ReactNativeDependencies',
12
+ ]);
13
+ function requireXcframework(packageDir, name) {
14
+ const xcframeworkPath = path.join(packageDir, `${name}.xcframework`);
15
+ if (!fs.existsSync(xcframeworkPath)) {
16
+ throw new Error(`Missing required XCFramework: ${name}.xcframework`);
17
+ }
18
+ return name;
19
+ }
20
+ function optionalXcframework(packageDir, name) {
21
+ return fs.existsSync(path.join(packageDir, `${name}.xcframework`))
22
+ ? name
23
+ : null;
24
+ }
25
+ function requireHermesXcframework(packageDir) {
26
+ return (optionalXcframework(packageDir, 'hermesvm') ??
27
+ requireXcframework(packageDir, 'hermes'));
28
+ }
29
+ function resolveAppFrameworkName(packageDir, explicitFrameworkName) {
30
+ if (explicitFrameworkName) {
31
+ return requireXcframework(packageDir, explicitFrameworkName);
32
+ }
33
+ const candidates = fs
34
+ .readdirSync(packageDir, { withFileTypes: true })
35
+ .filter((entry) => entry.isDirectory() &&
36
+ entry.name.endsWith('.xcframework') &&
37
+ !RESERVED_FRAMEWORK_NAMES.has(path.basename(entry.name, '.xcframework')))
38
+ .map((entry) => path.basename(entry.name, '.xcframework'))
39
+ .sort();
40
+ if (candidates.length === 1 && candidates[0]) {
41
+ return candidates[0];
42
+ }
43
+ if (candidates.length === 0) {
44
+ throw new Error('Could not resolve the packaged app XCFramework automatically. Pass --scheme explicitly when packaging.');
45
+ }
46
+ throw new Error(`Found multiple packaged app XCFramework candidates (${candidates.join(', ')}). Pass --scheme explicitly when packaging.`);
47
+ }
48
+ function renderPackageSwift({ packageName, libraryName, targetNames, }) {
49
+ const binaryTargets = targetNames
50
+ .map((targetName) => ` .binaryTarget(name: "${targetName}", path: "./${SPM_ARTIFACTS_DIR_NAME}/${targetName}.xcframework")`)
51
+ .join(',\n');
52
+ const targetDependencies = targetNames
53
+ .map((targetName) => `"${targetName}"`)
54
+ .join(', ');
55
+ return `// swift-tools-version: 5.9
56
+
57
+ import PackageDescription
58
+
59
+ let package = Package(
60
+ name: "${packageName}",
61
+ platforms: [
62
+ .iOS(.v14),
63
+ ],
64
+ products: [
65
+ .library(name: "${libraryName}", targets: [${targetDependencies}]),
66
+ ],
67
+ targets: [
68
+ ${binaryTargets}
69
+ ]
70
+ )
71
+ `;
72
+ }
73
+ function renderReadme({ packageName, libraryName, targetNames, }) {
74
+ const frameworks = targetNames.map((targetName) => `- \`${targetName}\``).join('\n');
75
+ return `# ${packageName}
76
+
77
+ This is a generated local Swift Package Manager package for the packaged React Native brownfield artifacts.
78
+
79
+ ## Product
80
+
81
+ - Library product: \`${libraryName}\`
82
+
83
+ ## Included Binary Targets
84
+
85
+ ${frameworks}
86
+
87
+ ## How To Use
88
+
89
+ 1. In Xcode, choose **File > Add Package Dependencies...**
90
+ 2. Click **Add Local...**
91
+ 3. Select this folder, the one containing \`Package.swift\` and the \`spm-artifacts\` directory
92
+ 4. Add the \`${libraryName}\` library product to your app target
93
+
94
+ ## Troubleshooting
95
+
96
+ If Xcode builds your host app but the simulator installation fails, check the host app target first:
97
+
98
+ - Make sure the target still includes its app entry point source files, such as \`App.swift\`, \`SceneDelegate\`, \`AppDelegate\`, or equivalent startup files
99
+ - 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
100
+ - 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
101
+ - If you are migrating an existing target from direct \`*.xcframework\` linking to local SPM, remove the old direct XCFramework references before building again
102
+
103
+ 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.
104
+ `;
105
+ }
106
+ export function createLocalSpmPackage({ packageDir, frameworkName, }) {
107
+ const resolvedFrameworkName = resolveAppFrameworkName(packageDir, frameworkName);
108
+ const targetNames = [
109
+ resolvedFrameworkName,
110
+ requireHermesXcframework(packageDir),
111
+ requireXcframework(packageDir, 'ReactBrownfield'),
112
+ optionalXcframework(packageDir, 'Brownie'),
113
+ optionalXcframework(packageDir, 'BrownfieldNavigation'),
114
+ optionalXcframework(packageDir, 'React'),
115
+ optionalXcframework(packageDir, 'ReactNativeDependencies'),
116
+ ].filter((targetName) => targetName !== null);
117
+ const packageManifestPath = path.join(packageDir, 'Package.swift');
118
+ const readmePath = path.join(packageDir, 'README.md');
119
+ prepareLocalSpmArtifacts({
120
+ packageDir,
121
+ targetNames,
122
+ });
123
+ const manifest = renderPackageSwift({
124
+ packageName: `${resolvedFrameworkName}Package`,
125
+ libraryName: resolvedFrameworkName,
126
+ targetNames,
127
+ });
128
+ const readme = renderReadme({
129
+ packageName: `${resolvedFrameworkName}Package`,
130
+ libraryName: resolvedFrameworkName,
131
+ targetNames,
132
+ });
133
+ fs.writeFileSync(packageManifestPath, manifest, 'utf8');
134
+ fs.writeFileSync(readmePath, readme, 'utf8');
135
+ return {
136
+ packageManifestPath,
137
+ };
138
+ }
@@ -0,0 +1,8 @@
1
+ type PrepareLocalSpmArtifactsOptions = {
2
+ packageDir: string;
3
+ targetNames: string[];
4
+ };
5
+ export declare const SPM_ARTIFACTS_DIR_NAME = "spm-artifacts";
6
+ export declare function prepareLocalSpmArtifacts({ packageDir, targetNames, }: PrepareLocalSpmArtifactsOptions): string;
7
+ export {};
8
+ //# sourceMappingURL=prepareLocalSpmArtifacts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prepareLocalSpmArtifacts.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/prepareLocalSpmArtifacts.ts"],"names":[],"mappings":"AAIA,KAAK,+BAA+B,GAAG;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB,CAAC;AAEF,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AAyFtD,wBAAgB,wBAAwB,CAAC,EACvC,UAAU,EACV,WAAW,GACZ,EAAE,+BAA+B,UAejC"}
@@ -0,0 +1,77 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+ export const SPM_ARTIFACTS_DIR_NAME = 'spm-artifacts';
5
+ function removeCodeSignatureArtifacts(dirPath) {
6
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
7
+ const entryPath = path.join(dirPath, entry.name);
8
+ if (entry.isDirectory()) {
9
+ if (entry.name === '_CodeSignature') {
10
+ fs.rmSync(entryPath, { recursive: true, force: true });
11
+ continue;
12
+ }
13
+ removeCodeSignatureArtifacts(entryPath);
14
+ continue;
15
+ }
16
+ if (entry.name === 'CodeResources') {
17
+ fs.rmSync(entryPath, { force: true });
18
+ }
19
+ }
20
+ }
21
+ function resolveFrameworkExecutablePath(frameworkDir, frameworkName) {
22
+ const directExecutablePath = path.join(frameworkDir, frameworkName);
23
+ if (fs.existsSync(directExecutablePath)) {
24
+ return directExecutablePath;
25
+ }
26
+ const versionedExecutablePath = path.join(frameworkDir, 'Versions', 'Current', frameworkName);
27
+ if (fs.existsSync(versionedExecutablePath)) {
28
+ return versionedExecutablePath;
29
+ }
30
+ return null;
31
+ }
32
+ function removeExecutableSignature(executablePath) {
33
+ try {
34
+ execFileSync('codesign', ['--remove-signature', executablePath], {
35
+ stdio: 'pipe',
36
+ });
37
+ }
38
+ catch (error) {
39
+ if (error instanceof Error &&
40
+ error.message.includes('code object is not signed at all')) {
41
+ return;
42
+ }
43
+ throw error;
44
+ }
45
+ }
46
+ function normalizeCopiedXcframeworkSignature(xcframeworkPath) {
47
+ removeCodeSignatureArtifacts(xcframeworkPath);
48
+ for (const sliceName of fs.readdirSync(xcframeworkPath)) {
49
+ const slicePath = path.join(xcframeworkPath, sliceName);
50
+ if (!fs.statSync(slicePath).isDirectory()) {
51
+ continue;
52
+ }
53
+ for (const entry of fs.readdirSync(slicePath)) {
54
+ if (!entry.endsWith('.framework')) {
55
+ continue;
56
+ }
57
+ const frameworkName = path.basename(entry, '.framework');
58
+ const frameworkDir = path.join(slicePath, entry);
59
+ const executablePath = resolveFrameworkExecutablePath(frameworkDir, frameworkName);
60
+ if (executablePath) {
61
+ removeExecutableSignature(executablePath);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ export function prepareLocalSpmArtifacts({ packageDir, targetNames, }) {
67
+ const spmArtifactsDir = path.join(packageDir, SPM_ARTIFACTS_DIR_NAME);
68
+ fs.rmSync(spmArtifactsDir, { recursive: true, force: true });
69
+ fs.mkdirSync(spmArtifactsDir, { recursive: true });
70
+ for (const targetName of targetNames) {
71
+ const sourcePath = path.join(packageDir, `${targetName}.xcframework`);
72
+ const destinationPath = path.join(spmArtifactsDir, `${targetName}.xcframework`);
73
+ fs.renameSync(sourcePath, destinationPath);
74
+ normalizeCopiedXcframeworkSignature(destinationPath);
75
+ }
76
+ return spmArtifactsDir;
77
+ }
@@ -14,6 +14,7 @@ export declare function getExpoConfigIfIsExpo(projectRoot: string): ExpoProjectC
14
14
  * @returns Whether the project is an Expo project
15
15
  */
16
16
  export declare function isExpoProject(projectRoot: string): boolean;
17
+ export declare function getExpoSdkMajor(projectRoot: string): number | null;
17
18
  /**
18
19
  * Fills the RNC CLI project config from the Expo config by mutating the passed in `options.projectConfig` object in place
19
20
  */
@@ -1 +1 @@
1
- {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/project.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,oBAAoB,EACpB,aAAa,EACb,UAAU,EACX,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAKjE,OAAO,EAEL,KAAK,aAAa,IAAI,iBAAiB,EACxC,MAAM,cAAc,CAAC;AAQtB;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,4BAMxD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAc1D;AAED;;GAEG;AACH,wBAAgB,+BAA+B,CAAC,EAC9C,aAAa,EACb,UAAU,EAAE,EAAE,GAAG,EAAE,EACnB,WAAW,GACZ,EAAE;IACD,8CAA8C;IAC9C,aAAa,EAAE,aAAa,CAAC;IAE7B,8BAA8B;IAC9B,UAAU,EAAE,iBAAiB,CAAC;IAE9B,4BAA4B;IAC5B,WAAW,EAAE,MAAM,CAAC;CACrB,QAuBA;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,SAAS,KAAK,GAAG,SAAS,EAC/D,QAAQ,EAAE,QAAQ,GACjB;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;CACzC,CAeA;AAED,wBAAgB,aAAa,CAAC,EAC5B,WAAW,EACX,QAAQ,GACT,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,KAAK,GAAG,SAAS,CAAC;CAC7B,GAAG,UAAU,CAuBb;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,eAAe,EACrB,aAAa,EAAE,oBAAoB;;;EAQpC"}
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/project.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,oBAAoB,EACpB,aAAa,EACb,UAAU,EACX,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAKjE,OAAO,EAEL,KAAK,aAAa,IAAI,iBAAiB,EACxC,MAAM,cAAc,CAAC;AAQtB;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,4BAMxD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAc1D;AAED,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOlE;AAED;;GAEG;AACH,wBAAgB,+BAA+B,CAAC,EAC9C,aAAa,EACb,UAAU,EAAE,EAAE,GAAG,EAAE,EACnB,WAAW,GACZ,EAAE;IACD,8CAA8C;IAC9C,aAAa,EAAE,aAAa,CAAC;IAE7B,8BAA8B;IAC9B,UAAU,EAAE,iBAAiB,CAAC;IAE9B,4BAA4B;IAC5B,WAAW,EAAE,MAAM,CAAC;CACrB,QAuBA;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,SAAS,KAAK,GAAG,SAAS,EAC/D,QAAQ,EAAE,QAAQ,GACjB;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;CACzC,CAeA;AAED,wBAAgB,aAAa,CAAC,EAC5B,WAAW,EACX,QAAQ,GACT,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,KAAK,GAAG,SAAS,CAAC;CAC7B,GAAG,UAAU,CAuBb;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,eAAe,EACrB,aAAa,EAAE,oBAAoB;;;EAQpC"}
@@ -36,6 +36,14 @@ export function isExpoProject(projectRoot) {
36
36
  ['dependencies', 'peerDependencies', 'devDependencies'].some((key) => JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))[key]?.expo);
37
37
  return hasExpoConfig && dependsOnExpo;
38
38
  }
39
+ export function getExpoSdkMajor(projectRoot) {
40
+ const rawExpoVersion = getExpoConfigIfIsExpo(projectRoot)?.exp.sdkVersion;
41
+ if (!rawExpoVersion) {
42
+ return null;
43
+ }
44
+ const expoSdkMajor = parseInt(rawExpoVersion.split('.')[0], 10);
45
+ return Number.isFinite(expoSdkMajor) ? expoSdkMajor : null;
46
+ }
39
47
  /**
40
48
  * Fills the RNC CLI project config from the Expo config by mutating the passed in `options.projectConfig` object in place
41
49
  */
@@ -0,0 +1,14 @@
1
+ type Resolution = 'explicit' | 'detected' | 'not_found' | 'ambiguous';
2
+ export interface ResolvePackagedFrameworkNameResult {
3
+ frameworkName: string | null;
4
+ resolution: Resolution;
5
+ candidates?: string[];
6
+ }
7
+ interface ResolvePackagedFrameworkNameOptions {
8
+ explicitScheme?: string;
9
+ productsPath: string;
10
+ configuration: string;
11
+ }
12
+ export declare function resolvePackagedFrameworkName({ explicitScheme, productsPath, configuration, }: ResolvePackagedFrameworkNameOptions): ResolvePackagedFrameworkNameResult;
13
+ export {};
14
+ //# sourceMappingURL=resolvePackagedFrameworkName.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolvePackagedFrameworkName.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/resolvePackagedFrameworkName.ts"],"names":[],"mappings":"AAGA,KAAK,UAAU,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CAAC;AAEtE,MAAM,WAAW,kCAAkC;IACjD,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE,UAAU,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,UAAU,mCAAmC;IAC3C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB;AA4CD,wBAAgB,4BAA4B,CAAC,EAC3C,cAAc,EACd,YAAY,EACZ,aAAa,GACd,EAAE,mCAAmC,GAAG,kCAAkC,CAkC1E"}
@@ -0,0 +1,61 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ function collectFrameworkCandidates(configurationProductsPath) {
4
+ if (!fs.existsSync(configurationProductsPath)) {
5
+ return [];
6
+ }
7
+ const discoveredFrameworks = new Set();
8
+ for (const entry of fs.readdirSync(configurationProductsPath, { withFileTypes: true })) {
9
+ const entryPath = path.join(configurationProductsPath, entry.name);
10
+ if (entry.isDirectory() && entry.name.endsWith('.framework')) {
11
+ const frameworkName = path.basename(entry.name, '.framework');
12
+ const bundlePath = path.join(entryPath, 'main.jsbundle');
13
+ if (fs.existsSync(bundlePath)) {
14
+ discoveredFrameworks.add(frameworkName);
15
+ }
16
+ continue;
17
+ }
18
+ if (!entry.isDirectory()) {
19
+ continue;
20
+ }
21
+ for (const nestedEntry of fs.readdirSync(entryPath, { withFileTypes: true })) {
22
+ if (!nestedEntry.isDirectory() || !nestedEntry.name.endsWith('.framework')) {
23
+ continue;
24
+ }
25
+ const frameworkName = path.basename(nestedEntry.name, '.framework');
26
+ const bundlePath = path.join(entryPath, nestedEntry.name, 'main.jsbundle');
27
+ if (fs.existsSync(bundlePath)) {
28
+ discoveredFrameworks.add(frameworkName);
29
+ }
30
+ }
31
+ }
32
+ return [...discoveredFrameworks].sort();
33
+ }
34
+ export function resolvePackagedFrameworkName({ explicitScheme, productsPath, configuration, }) {
35
+ if (explicitScheme) {
36
+ return {
37
+ frameworkName: explicitScheme,
38
+ resolution: 'explicit',
39
+ };
40
+ }
41
+ const configurationProductsPath = path.join(productsPath, `${configuration}-iphoneos`);
42
+ const candidates = collectFrameworkCandidates(configurationProductsPath);
43
+ if (candidates.length === 1) {
44
+ return {
45
+ frameworkName: candidates[0] ?? null,
46
+ resolution: 'detected',
47
+ };
48
+ }
49
+ if (candidates.length === 0) {
50
+ return {
51
+ frameworkName: null,
52
+ resolution: 'not_found',
53
+ candidates,
54
+ };
55
+ }
56
+ return {
57
+ frameworkName: null,
58
+ resolution: 'ambiguous',
59
+ candidates,
60
+ };
61
+ }
@@ -0,0 +1,18 @@
1
+ /** Minimum RN version that can opt in to prebuilts via `--use-prebuilt-rn-core`. */
2
+ export declare const MIN_REACT_NATIVE_VERSION_FOR_OPT_IN_PREBUILT_RN_CORE = "0.81.0";
3
+ /** Minimum RN version where Brownfield enables prebuilts by default (vanilla projects). */
4
+ export declare const MIN_REACT_NATIVE_VERSION_FOR_PREBUILT_RN_CORE_BY_DEFAULT = "0.84.0";
5
+ export declare const MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_RN_CORE_BY_DEFAULT = 55;
6
+ export type PrebuiltRNCoreSupportResult = {
7
+ supported: true;
8
+ enabledByDefault: boolean;
9
+ reason?: never;
10
+ } | {
11
+ supported: false;
12
+ enabledByDefault?: never;
13
+ reason: string;
14
+ };
15
+ export declare function supportsPrebuiltRNCore({ projectRoot, }: {
16
+ projectRoot: string;
17
+ }): PrebuiltRNCoreSupportResult;
18
+ //# sourceMappingURL=supportsPrebuiltRNCore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supportsPrebuiltRNCore.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/supportsPrebuiltRNCore.ts"],"names":[],"mappings":"AAIA,oFAAoF;AACpF,eAAO,MAAM,oDAAoD,WAAW,CAAC;AAC7E,2FAA2F;AAC3F,eAAO,MAAM,wDAAwD,WAC3D,CAAC;AACX,eAAO,MAAM,kDAAkD,KAAK,CAAC;AAErE,MAAM,MAAM,2BAA2B,GACnC;IAAE,SAAS,EAAE,IAAI,CAAC;IAAC,gBAAgB,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,KAAK,CAAA;CAAE,GAC9D;IAAE,SAAS,EAAE,KAAK,CAAC;IAAC,gBAAgB,CAAC,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnE,wBAAgB,sBAAsB,CAAC,EACrC,WAAW,GACZ,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,2BAA2B,CA+C9B"}