@callstack/brownfield-cli 3.10.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
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
+
3
11
  ## 3.10.0
4
12
 
5
13
  ### Minor Changes
@@ -1 +1 @@
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;AAY/B,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,MAAM,GAAG,OAAO,GACtB,OAAO,CAcT;AAOD,eAAO,MAAM,iBAAiB,SA6L3B,CAAC;AAEJ,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"}
@@ -10,7 +10,10 @@ import { supportsPrebuiltRNCore } from '../utils/supportsPrebuiltRNCore.js';
10
10
  import { actionRunner, curryOptions, ExampleUsage, } from '../../shared/index.js';
11
11
  import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
12
12
  import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js';
13
+ import { copyDebugBundleToSimulatorSlice } from '../utils/copyDebugBundleToSimulatorSlice.js';
14
+ import { resolvePackagedFrameworkName } from '../utils/resolvePackagedFrameworkName.js';
13
15
  import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js';
16
+ import { createLocalSpmPackage } from '../utils/createLocalSpmPackage.js';
14
17
  /** Help text for `--use-prebuilt-rn-core` (keep in sync with docs/docs/docs/getting-started/ios.mdx, "React Native Prebuilts" section). */
15
18
  const USE_PREBUILT_RN_CORE_HELP = 'Whether the Xcode build for packaging should use React Native Apple prebuilt binaries (via CocoaPods). ' +
16
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. ' +
@@ -29,6 +32,11 @@ export function parseUsePrebuiltRnCoreArgument(value) {
29
32
  }
30
33
  throw new RockError(`Invalid value for --use-prebuilt-rn-core: expected true or false, received "${value}"`);
31
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
+ }
32
40
  export const packageIosCommand = curryOptions(new Command('package:ios').description('Build iOS XCFramework'), getBuildOptions({ platformName: 'ios' }).map((option) => option.name.startsWith('--build-folder')
33
41
  ? {
34
42
  ...option,
@@ -39,6 +47,7 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
39
47
  .addOption(new Option('--use-prebuilt-rn-core [bool]', USE_PREBUILT_RN_CORE_HELP)
40
48
  .preset(true)
41
49
  .argParser(parseUsePrebuiltRnCoreArgument))
50
+ .addOption(new Option('--add-spm-package', 'Generate a local Swift Package Manager manifest next to the packaged XCFramework outputs'))
42
51
  .action(actionRunner(async (options) => {
43
52
  const { projectRoot, platformConfig, userConfig } = getProjectInfo('ios');
44
53
  const prebuiltRNCoreSupport = supportsPrebuiltRNCore({ projectRoot });
@@ -93,6 +102,42 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
93
102
  skipCache: true, // cache is dependent on existence of Rock config file
94
103
  usePrebuiltRNCore: options.usePrebuiltRnCore,
95
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
+ }
96
141
  const reactBrownfieldXcframeworkPath = path.join(packageDir, 'ReactBrownfield.xcframework');
97
142
  if (fs.existsSync(reactBrownfieldXcframeworkPath)) {
98
143
  // Strip the binary from ReactBrownfield.xcframework to make it interface-only.
@@ -101,7 +146,6 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
101
146
  stripFrameworkBinary(reactBrownfieldXcframeworkPath);
102
147
  }
103
148
  if (hasBrownie) {
104
- const productsPath = path.join(options.buildFolder, 'Build', 'Products');
105
149
  const brownieOutputPath = path.join(packageDir, 'Brownie.xcframework');
106
150
  await mergeFrameworks({
107
151
  sourceDir: userConfig.project.ios.sourceDir,
@@ -118,7 +162,6 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
118
162
  logger.success(`Brownie.xcframework created at ${colorLink(relativeToCwd(brownieOutputPath))}`);
119
163
  }
120
164
  if (hasNavigation) {
121
- const productsPath = path.join(options.buildFolder, 'Build', 'Products');
122
165
  const brownfieldNavigationOutputPath = path.join(packageDir, 'BrownfieldNavigation.xcframework');
123
166
  await mergeFrameworks({
124
167
  sourceDir: userConfig.project.ios.sourceDir,
@@ -131,5 +174,14 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
131
174
  stripFrameworkBinary(brownfieldNavigationOutputPath);
132
175
  logger.success(`BrownfieldNavigation.xcframework created at ${colorLink(relativeToCwd(brownfieldNavigationOutputPath))}`);
133
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
+ }
134
186
  }));
135
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
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@callstack/brownfield-cli",
3
- "version": "3.10.0",
3
+ "version": "3.11.0",
4
4
  "license": "MIT",
5
5
  "author": "Artur Morys-Magiera <artus9033@gmail.com>",
6
6
  "bin": {
@@ -27,7 +27,10 @@ import {
27
27
  } from '../../shared/index.js';
28
28
  import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
29
29
  import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js';
30
+ import { copyDebugBundleToSimulatorSlice } from '../utils/copyDebugBundleToSimulatorSlice.js';
31
+ import { resolvePackagedFrameworkName } from '../utils/resolvePackagedFrameworkName.js';
30
32
  import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js';
33
+ import { createLocalSpmPackage } from '../utils/createLocalSpmPackage.js';
31
34
 
32
35
  /** Help text for `--use-prebuilt-rn-core` (keep in sync with docs/docs/docs/getting-started/ios.mdx, "React Native Prebuilts" section). */
33
36
  const USE_PREBUILT_RN_CORE_HELP =
@@ -54,9 +57,23 @@ export function parseUsePrebuiltRnCoreArgument(
54
57
  );
55
58
  }
56
59
 
60
+ function getPackagedFrameworkResolutionFailureMessage({
61
+ resolution,
62
+ candidates,
63
+ }: {
64
+ resolution: string | null | undefined;
65
+ candidates?: string[];
66
+ }) {
67
+ return resolution === 'ambiguous'
68
+ ? `found multiple bundled framework candidates (${candidates?.join(', ') ?? 'none'}); pass --scheme explicitly`
69
+ : 'could not resolve the packaged framework output automatically; pass --scheme explicitly';
70
+ }
71
+
57
72
  type PackageIosCliFlags = AppleBuildFlags & {
58
73
  /** Set when `--use-prebuilt-rn-core` is passed; omitted when the flag is absent (Rock applies RN version defaults). */
59
74
  usePrebuiltRnCore?: boolean;
75
+ /** When set, generate a local Swift Package Manager manifest next to the packaged XCFramework outputs. */
76
+ addSpmPackage?: boolean;
60
77
  };
61
78
 
62
79
  export const packageIosCommand = curryOptions(
@@ -77,6 +94,12 @@ export const packageIosCommand = curryOptions(
77
94
  .preset(true)
78
95
  .argParser(parseUsePrebuiltRnCoreArgument)
79
96
  )
97
+ .addOption(
98
+ new Option(
99
+ '--add-spm-package',
100
+ 'Generate a local Swift Package Manager manifest next to the packaged XCFramework outputs'
101
+ )
102
+ )
80
103
  .action(
81
104
  actionRunner(async (options: PackageIosCliFlags) => {
82
105
  const { projectRoot, platformConfig, userConfig } = getProjectInfo('ios');
@@ -163,6 +186,58 @@ export const packageIosCommand = curryOptions(
163
186
  platformConfig
164
187
  );
165
188
 
189
+ const productsPath = path.join(options.buildFolder, 'Build', 'Products');
190
+ const { frameworkName, resolution, candidates } =
191
+ resolvePackagedFrameworkName({
192
+ explicitScheme: options.scheme,
193
+ productsPath,
194
+ configuration,
195
+ });
196
+
197
+ if (!frameworkName && options.addSpmPackage) {
198
+ throw new RockError(
199
+ `Cannot generate local SPM package: ${getPackagedFrameworkResolutionFailureMessage({
200
+ resolution,
201
+ candidates,
202
+ })}`
203
+ );
204
+ }
205
+
206
+ if (frameworkName) {
207
+ copyDebugBundleToSimulatorSlice({
208
+ productsPath,
209
+ configuration,
210
+ frameworkName,
211
+ });
212
+
213
+ if (configuration.includes('Debug')) {
214
+ // Re-merge only Debug frameworks so the simulator slice includes main.jsbundle.
215
+ await mergeFrameworks({
216
+ sourceDir: userConfig.project.ios.sourceDir,
217
+ frameworkPaths: [
218
+ path.join(
219
+ productsPath,
220
+ `${configuration}-iphoneos`,
221
+ `${frameworkName}.framework`
222
+ ),
223
+ path.join(
224
+ productsPath,
225
+ `${configuration}-iphonesimulator`,
226
+ `${frameworkName}.framework`
227
+ ),
228
+ ],
229
+ outputPath: path.join(packageDir, `${frameworkName}.xcframework`),
230
+ });
231
+ }
232
+ } else if (configuration.includes('Debug')) {
233
+ logger.warn(
234
+ `Skipping Debug simulator JS bundle copy: ${getPackagedFrameworkResolutionFailureMessage({
235
+ resolution,
236
+ candidates,
237
+ })}`
238
+ );
239
+ }
240
+
166
241
  const reactBrownfieldXcframeworkPath = path.join(
167
242
  packageDir,
168
243
  'ReactBrownfield.xcframework'
@@ -175,11 +250,6 @@ export const packageIosCommand = curryOptions(
175
250
  }
176
251
 
177
252
  if (hasBrownie) {
178
- const productsPath = path.join(
179
- options.buildFolder,
180
- 'Build',
181
- 'Products'
182
- );
183
253
  const brownieOutputPath = path.join(packageDir, 'Brownie.xcframework');
184
254
 
185
255
  await mergeFrameworks({
@@ -212,11 +282,6 @@ export const packageIosCommand = curryOptions(
212
282
  }
213
283
 
214
284
  if (hasNavigation) {
215
- const productsPath = path.join(
216
- options.buildFolder,
217
- 'Build',
218
- 'Products'
219
- );
220
285
  const brownfieldNavigationOutputPath = path.join(
221
286
  packageDir,
222
287
  'BrownfieldNavigation.xcframework'
@@ -247,6 +312,23 @@ export const packageIosCommand = curryOptions(
247
312
  `BrownfieldNavigation.xcframework created at ${colorLink(relativeToCwd(brownfieldNavigationOutputPath))}`
248
313
  );
249
314
  }
315
+
316
+ if (options.addSpmPackage) {
317
+ const { packageManifestPath } = createLocalSpmPackage({
318
+ packageDir,
319
+ frameworkName: frameworkName ?? undefined,
320
+ });
321
+
322
+ logger.success(
323
+ `Local SPM package manifest created at ${colorLink(relativeToCwd(packageManifestPath))}`
324
+ );
325
+ logger.info(
326
+ `Add the local package folder in Xcode: ${colorLink(relativeToCwd(packageDir))}`
327
+ );
328
+ logger.info(
329
+ "In Xcode, choose File > Add Package Dependencies..., click Add Local..., and select that folder."
330
+ );
331
+ }
250
332
  })
251
333
  );
252
334
 
@@ -0,0 +1,58 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { colorLink, logger, relativeToCwd } from '@rock-js/tools';
5
+
6
+ interface CopyDebugBundleToSimulatorSliceOptions {
7
+ productsPath: string;
8
+ configuration: string;
9
+ frameworkName: string;
10
+ }
11
+
12
+ export function copyDebugBundleToSimulatorSlice({
13
+ productsPath,
14
+ configuration,
15
+ frameworkName,
16
+ }: CopyDebugBundleToSimulatorSliceOptions) {
17
+ if (!configuration.includes('Debug')) {
18
+ return;
19
+ }
20
+
21
+ const deviceBundlePath = path.join(
22
+ productsPath,
23
+ `${configuration}-iphoneos`,
24
+ `${frameworkName}.framework`,
25
+ 'main.jsbundle'
26
+ );
27
+
28
+ const simulatorFrameworkPath = path.join(
29
+ productsPath,
30
+ `${configuration}-iphonesimulator`,
31
+ `${frameworkName}.framework`
32
+ );
33
+
34
+ const simulatorBundlePath = path.join(
35
+ simulatorFrameworkPath,
36
+ 'main.jsbundle'
37
+ );
38
+
39
+ if (!fs.existsSync(deviceBundlePath)) {
40
+ logger.warn(
41
+ `Skipping simulator JS bundle copy: missing ${relativeToCwd(deviceBundlePath)}`
42
+ );
43
+ return;
44
+ }
45
+
46
+ if (!fs.existsSync(simulatorFrameworkPath)) {
47
+ logger.warn(
48
+ `Skipping simulator JS bundle copy: missing ${relativeToCwd(simulatorFrameworkPath)}`
49
+ );
50
+ return;
51
+ }
52
+
53
+ fs.copyFileSync(deviceBundlePath, simulatorBundlePath);
54
+
55
+ logger.success(
56
+ `Copied Debug JS bundle to simulator slice at ${colorLink(relativeToCwd(simulatorBundlePath))}`
57
+ );
58
+ }
@@ -0,0 +1,208 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import {
5
+ prepareLocalSpmArtifacts,
6
+ SPM_ARTIFACTS_DIR_NAME,
7
+ } from './prepareLocalSpmArtifacts.js';
8
+
9
+ type CreateLocalSpmPackageOptions = {
10
+ packageDir: string;
11
+ frameworkName?: string;
12
+ };
13
+
14
+ type CreateLocalSpmPackageResult = {
15
+ packageManifestPath: string;
16
+ };
17
+
18
+ const RESERVED_FRAMEWORK_NAMES = new Set([
19
+ 'hermes',
20
+ 'hermesvm',
21
+ 'ReactBrownfield',
22
+ 'Brownie',
23
+ 'BrownfieldNavigation',
24
+ 'React',
25
+ 'ReactNativeDependencies',
26
+ ]);
27
+
28
+ function requireXcframework(packageDir: string, name: string) {
29
+ const xcframeworkPath = path.join(packageDir, `${name}.xcframework`);
30
+
31
+ if (!fs.existsSync(xcframeworkPath)) {
32
+ throw new Error(`Missing required XCFramework: ${name}.xcframework`);
33
+ }
34
+
35
+ return name;
36
+ }
37
+
38
+ function optionalXcframework(packageDir: string, name: string) {
39
+ return fs.existsSync(path.join(packageDir, `${name}.xcframework`))
40
+ ? name
41
+ : null;
42
+ }
43
+
44
+ function requireHermesXcframework(packageDir: string) {
45
+ return (
46
+ optionalXcframework(packageDir, 'hermesvm') ??
47
+ requireXcframework(packageDir, 'hermes')
48
+ );
49
+ }
50
+
51
+ function resolveAppFrameworkName(
52
+ packageDir: string,
53
+ explicitFrameworkName?: string
54
+ ) {
55
+ if (explicitFrameworkName) {
56
+ return requireXcframework(packageDir, explicitFrameworkName);
57
+ }
58
+
59
+ const candidates = fs
60
+ .readdirSync(packageDir, { withFileTypes: true })
61
+ .filter(
62
+ (entry) =>
63
+ entry.isDirectory() &&
64
+ entry.name.endsWith('.xcframework') &&
65
+ !RESERVED_FRAMEWORK_NAMES.has(path.basename(entry.name, '.xcframework'))
66
+ )
67
+ .map((entry) => path.basename(entry.name, '.xcframework'))
68
+ .sort();
69
+
70
+ if (candidates.length === 1 && candidates[0]) {
71
+ return candidates[0];
72
+ }
73
+
74
+ if (candidates.length === 0) {
75
+ throw new Error(
76
+ 'Could not resolve the packaged app XCFramework automatically. Pass --scheme explicitly when packaging.'
77
+ );
78
+ }
79
+
80
+ throw new Error(
81
+ `Found multiple packaged app XCFramework candidates (${candidates.join(', ')}). Pass --scheme explicitly when packaging.`
82
+ );
83
+ }
84
+
85
+ function renderPackageSwift({
86
+ packageName,
87
+ libraryName,
88
+ targetNames,
89
+ }: {
90
+ packageName: string;
91
+ libraryName: string;
92
+ targetNames: string[];
93
+ }) {
94
+ const binaryTargets = targetNames
95
+ .map(
96
+ (targetName) =>
97
+ ` .binaryTarget(name: "${targetName}", path: "./${SPM_ARTIFACTS_DIR_NAME}/${targetName}.xcframework")`
98
+ )
99
+ .join(',\n');
100
+
101
+ const targetDependencies = targetNames
102
+ .map((targetName) => `"${targetName}"`)
103
+ .join(', ');
104
+
105
+ return `// swift-tools-version: 5.9
106
+
107
+ import PackageDescription
108
+
109
+ let package = Package(
110
+ name: "${packageName}",
111
+ platforms: [
112
+ .iOS(.v14),
113
+ ],
114
+ products: [
115
+ .library(name: "${libraryName}", targets: [${targetDependencies}]),
116
+ ],
117
+ targets: [
118
+ ${binaryTargets}
119
+ ]
120
+ )
121
+ `;
122
+ }
123
+
124
+ function renderReadme({
125
+ packageName,
126
+ libraryName,
127
+ targetNames,
128
+ }: {
129
+ packageName: string;
130
+ libraryName: string;
131
+ targetNames: string[];
132
+ }) {
133
+ const frameworks = targetNames.map((targetName) => `- \`${targetName}\``).join('\n');
134
+
135
+ return `# ${packageName}
136
+
137
+ This is a generated local Swift Package Manager package for the packaged React Native brownfield artifacts.
138
+
139
+ ## Product
140
+
141
+ - Library product: \`${libraryName}\`
142
+
143
+ ## Included Binary Targets
144
+
145
+ ${frameworks}
146
+
147
+ ## How To Use
148
+
149
+ 1. In Xcode, choose **File > Add Package Dependencies...**
150
+ 2. Click **Add Local...**
151
+ 3. Select this folder, the one containing \`Package.swift\` and the \`spm-artifacts\` directory
152
+ 4. Add the \`${libraryName}\` library product to your app target
153
+
154
+ ## Troubleshooting
155
+
156
+ If Xcode builds your host app but the simulator installation fails, check the host app target first:
157
+
158
+ - Make sure the target still includes its app entry point source files, such as \`App.swift\`, \`SceneDelegate\`, \`AppDelegate\`, or equivalent startup files
159
+ - 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
160
+ - 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
161
+ - If you are migrating an existing target from direct \`*.xcframework\` linking to local SPM, remove the old direct XCFramework references before building again
162
+
163
+ 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.
164
+ `;
165
+ }
166
+
167
+ export function createLocalSpmPackage({
168
+ packageDir,
169
+ frameworkName,
170
+ }: CreateLocalSpmPackageOptions): CreateLocalSpmPackageResult {
171
+ const resolvedFrameworkName = resolveAppFrameworkName(
172
+ packageDir,
173
+ frameworkName
174
+ );
175
+ const targetNames = [
176
+ resolvedFrameworkName,
177
+ requireHermesXcframework(packageDir),
178
+ requireXcframework(packageDir, 'ReactBrownfield'),
179
+ optionalXcframework(packageDir, 'Brownie'),
180
+ optionalXcframework(packageDir, 'BrownfieldNavigation'),
181
+ optionalXcframework(packageDir, 'React'),
182
+ optionalXcframework(packageDir, 'ReactNativeDependencies'),
183
+ ].filter((targetName): targetName is string => targetName !== null);
184
+
185
+ const packageManifestPath = path.join(packageDir, 'Package.swift');
186
+ const readmePath = path.join(packageDir, 'README.md');
187
+ prepareLocalSpmArtifacts({
188
+ packageDir,
189
+ targetNames,
190
+ });
191
+ const manifest = renderPackageSwift({
192
+ packageName: `${resolvedFrameworkName}Package`,
193
+ libraryName: resolvedFrameworkName,
194
+ targetNames,
195
+ });
196
+ const readme = renderReadme({
197
+ packageName: `${resolvedFrameworkName}Package`,
198
+ libraryName: resolvedFrameworkName,
199
+ targetNames,
200
+ });
201
+
202
+ fs.writeFileSync(packageManifestPath, manifest, 'utf8');
203
+ fs.writeFileSync(readmePath, readme, 'utf8');
204
+
205
+ return {
206
+ packageManifestPath,
207
+ };
208
+ }
@@ -0,0 +1,117 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+
5
+ type PrepareLocalSpmArtifactsOptions = {
6
+ packageDir: string;
7
+ targetNames: string[];
8
+ };
9
+
10
+ export const SPM_ARTIFACTS_DIR_NAME = 'spm-artifacts';
11
+
12
+ function removeCodeSignatureArtifacts(dirPath: string) {
13
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
14
+ const entryPath = path.join(dirPath, entry.name);
15
+
16
+ if (entry.isDirectory()) {
17
+ if (entry.name === '_CodeSignature') {
18
+ fs.rmSync(entryPath, { recursive: true, force: true });
19
+ continue;
20
+ }
21
+
22
+ removeCodeSignatureArtifacts(entryPath);
23
+ continue;
24
+ }
25
+
26
+ if (entry.name === 'CodeResources') {
27
+ fs.rmSync(entryPath, { force: true });
28
+ }
29
+ }
30
+ }
31
+
32
+ function resolveFrameworkExecutablePath(
33
+ frameworkDir: string,
34
+ frameworkName: string
35
+ ) {
36
+ const directExecutablePath = path.join(frameworkDir, frameworkName);
37
+ if (fs.existsSync(directExecutablePath)) {
38
+ return directExecutablePath;
39
+ }
40
+
41
+ const versionedExecutablePath = path.join(
42
+ frameworkDir,
43
+ 'Versions',
44
+ 'Current',
45
+ frameworkName
46
+ );
47
+ if (fs.existsSync(versionedExecutablePath)) {
48
+ return versionedExecutablePath;
49
+ }
50
+
51
+ return null;
52
+ }
53
+
54
+ function removeExecutableSignature(executablePath: string) {
55
+ try {
56
+ execFileSync('codesign', ['--remove-signature', executablePath], {
57
+ stdio: 'pipe',
58
+ });
59
+ } catch (error) {
60
+ if (
61
+ error instanceof Error &&
62
+ error.message.includes('code object is not signed at all')
63
+ ) {
64
+ return;
65
+ }
66
+
67
+ throw error;
68
+ }
69
+ }
70
+
71
+ function normalizeCopiedXcframeworkSignature(xcframeworkPath: string) {
72
+ removeCodeSignatureArtifacts(xcframeworkPath);
73
+
74
+ for (const sliceName of fs.readdirSync(xcframeworkPath)) {
75
+ const slicePath = path.join(xcframeworkPath, sliceName);
76
+ if (!fs.statSync(slicePath).isDirectory()) {
77
+ continue;
78
+ }
79
+
80
+ for (const entry of fs.readdirSync(slicePath)) {
81
+ if (!entry.endsWith('.framework')) {
82
+ continue;
83
+ }
84
+
85
+ const frameworkName = path.basename(entry, '.framework');
86
+ const frameworkDir = path.join(slicePath, entry);
87
+ const executablePath = resolveFrameworkExecutablePath(
88
+ frameworkDir,
89
+ frameworkName
90
+ );
91
+
92
+ if (executablePath) {
93
+ removeExecutableSignature(executablePath);
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ export function prepareLocalSpmArtifacts({
100
+ packageDir,
101
+ targetNames,
102
+ }: PrepareLocalSpmArtifactsOptions) {
103
+ const spmArtifactsDir = path.join(packageDir, SPM_ARTIFACTS_DIR_NAME);
104
+
105
+ fs.rmSync(spmArtifactsDir, { recursive: true, force: true });
106
+ fs.mkdirSync(spmArtifactsDir, { recursive: true });
107
+
108
+ for (const targetName of targetNames) {
109
+ const sourcePath = path.join(packageDir, `${targetName}.xcframework`);
110
+ const destinationPath = path.join(spmArtifactsDir, `${targetName}.xcframework`);
111
+
112
+ fs.renameSync(sourcePath, destinationPath);
113
+ normalizeCopiedXcframeworkSignature(destinationPath);
114
+ }
115
+
116
+ return spmArtifactsDir;
117
+ }
@@ -0,0 +1,98 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ type Resolution = 'explicit' | 'detected' | 'not_found' | 'ambiguous';
5
+
6
+ export interface ResolvePackagedFrameworkNameResult {
7
+ frameworkName: string | null;
8
+ resolution: Resolution;
9
+ candidates?: string[];
10
+ }
11
+
12
+ interface ResolvePackagedFrameworkNameOptions {
13
+ explicitScheme?: string;
14
+ productsPath: string;
15
+ configuration: string;
16
+ }
17
+
18
+ function collectFrameworkCandidates(configurationProductsPath: string): string[] {
19
+ if (!fs.existsSync(configurationProductsPath)) {
20
+ return [];
21
+ }
22
+
23
+ const discoveredFrameworks = new Set<string>();
24
+
25
+ for (const entry of fs.readdirSync(configurationProductsPath, { withFileTypes: true })) {
26
+ const entryPath = path.join(configurationProductsPath, entry.name);
27
+
28
+ if (entry.isDirectory() && entry.name.endsWith('.framework')) {
29
+ const frameworkName = path.basename(entry.name, '.framework');
30
+ const bundlePath = path.join(entryPath, 'main.jsbundle');
31
+
32
+ if (fs.existsSync(bundlePath)) {
33
+ discoveredFrameworks.add(frameworkName);
34
+ }
35
+
36
+ continue;
37
+ }
38
+
39
+ if (!entry.isDirectory()) {
40
+ continue;
41
+ }
42
+
43
+ for (const nestedEntry of fs.readdirSync(entryPath, { withFileTypes: true })) {
44
+ if (!nestedEntry.isDirectory() || !nestedEntry.name.endsWith('.framework')) {
45
+ continue;
46
+ }
47
+
48
+ const frameworkName = path.basename(nestedEntry.name, '.framework');
49
+ const bundlePath = path.join(entryPath, nestedEntry.name, 'main.jsbundle');
50
+
51
+ if (fs.existsSync(bundlePath)) {
52
+ discoveredFrameworks.add(frameworkName);
53
+ }
54
+ }
55
+ }
56
+
57
+ return [...discoveredFrameworks].sort();
58
+ }
59
+
60
+ export function resolvePackagedFrameworkName({
61
+ explicitScheme,
62
+ productsPath,
63
+ configuration,
64
+ }: ResolvePackagedFrameworkNameOptions): ResolvePackagedFrameworkNameResult {
65
+ if (explicitScheme) {
66
+ return {
67
+ frameworkName: explicitScheme,
68
+ resolution: 'explicit',
69
+ };
70
+ }
71
+
72
+ const configurationProductsPath = path.join(
73
+ productsPath,
74
+ `${configuration}-iphoneos`
75
+ );
76
+ const candidates = collectFrameworkCandidates(configurationProductsPath);
77
+
78
+ if (candidates.length === 1) {
79
+ return {
80
+ frameworkName: candidates[0] ?? null,
81
+ resolution: 'detected',
82
+ };
83
+ }
84
+
85
+ if (candidates.length === 0) {
86
+ return {
87
+ frameworkName: null,
88
+ resolution: 'not_found',
89
+ candidates,
90
+ };
91
+ }
92
+
93
+ return {
94
+ frameworkName: null,
95
+ resolution: 'ambiguous',
96
+ candidates,
97
+ };
98
+ }