@callstack/brownfield-cli 1.0.1 → 1.0.3

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 ADDED
@@ -0,0 +1,19 @@
1
+ # @callstack/brownfield-cli
2
+
3
+ ## 1.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#213](https://github.com/callstack/react-native-brownfield/pull/213) [`2347775`](https://github.com/callstack/react-native-brownfield/commit/23477753b16ee189b82c1aee3eac98a56c79f52a) Thanks [@thymikee](https://github.com/thymikee)! - feat: create brownfield package as CLI proxy
8
+
9
+ ## 1.0.2
10
+
11
+ ### Patch Changes
12
+
13
+ - [`2a8563f`](https://github.com/callstack/react-native-brownfield/commit/2a8563f65ed152054ad1290caf963791a368ee9a) Thanks [@okwasniewski](https://github.com/okwasniewski)! - feat: strip framework binaries to avoid duplicate symbol errors
14
+
15
+ ## 1.0.1
16
+
17
+ ### Patch Changes
18
+
19
+ - [#198](https://github.com/callstack/react-native-brownfield/pull/198) [`c8c903d`](https://github.com/callstack/react-native-brownfield/commit/c8c903d0d2b78a8c06a41213dfbe781a2daf3d25) Thanks [@artus9033](https://github.com/artus9033)! - docs: added README files to all packages
@@ -1 +1 @@
1
- {"version":3,"file":"packageIos.d.ts","sourceRoot":"","sources":["../../../src/brownfield/commands/packageIos.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,OAAO,EAGL,YAAY,EACb,MAAM,uBAAuB,CAAC;AAE/B,eAAO,MAAM,iBAAiB,SAkF7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,cAG7B,CAAC"}
1
+ {"version":3,"file":"packageIos.d.ts","sourceRoot":"","sources":["../../../src/brownfield/commands/packageIos.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,OAAO,EAGL,YAAY,EACb,MAAM,uBAAuB,CAAC;AAE/B,eAAO,MAAM,iBAAiB,SAuF7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,cAG7B,CAAC"}
@@ -5,7 +5,7 @@ import { colorLink, getReactNativeVersion, logger, relativeToCwd, } from '@rock-
5
5
  import { Command } from 'commander';
6
6
  import { isBrownieInstalled } from '../../brownie/config.js';
7
7
  import { runCodegen } from '../../brownie/commands/codegen.js';
8
- import { getProjectInfo } from '../utils/index.js';
8
+ import { getProjectInfo, stripFrameworkBinary } from '../utils/index.js';
9
9
  import { actionRunner, curryOptions, ExampleUsage, } from '../../shared/index.js';
10
10
  export const packageIosCommand = curryOptions(new Command('package:ios').description('Build iOS XCFramework'), getBuildOptions({ platformName: 'ios' }).map((option) => option.name.startsWith('--build-folder')
11
11
  ? {
@@ -51,6 +51,10 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
51
51
  ],
52
52
  outputPath: brownieOutputPath,
53
53
  });
54
+ // Strip the binary from Brownie.xcframework to make it interface-only.
55
+ // This avoids duplicate symbols when consumer apps embed both BrownfieldLib
56
+ // (which contains Brownie symbols) and Brownie.xcframework.
57
+ await stripFrameworkBinary(brownieOutputPath);
54
58
  logger.success(`Brownie.xcframework created at ${colorLink(relativeToCwd(brownieOutputPath))}`);
55
59
  }
56
60
  }));
@@ -1,3 +1,4 @@
1
1
  export * from './paths.js';
2
2
  export * from './rn-cli.js';
3
+ export * from './stripFrameworkBinary.js';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,2BAA2B,CAAC"}
@@ -1,2 +1,3 @@
1
1
  export * from './paths.js';
2
2
  export * from './rn-cli.js';
3
+ export * from './stripFrameworkBinary.js';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Strips the binary from an xcframework, keeping only Swift module interfaces.
3
+ * This creates an "interface-only" framework where consumers can import the module
4
+ * but the actual symbols must come from another framework (e.g., BrownfieldLib).
5
+ *
6
+ * @param xcframeworkPath - Path to the .xcframework directory
7
+ */
8
+ export declare function stripFrameworkBinary(xcframeworkPath: string): void;
9
+ //# sourceMappingURL=stripFrameworkBinary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stripFrameworkBinary.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/stripFrameworkBinary.ts"],"names":[],"mappings":"AAuFA;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAsDlE"}
@@ -0,0 +1,107 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { execSync } from 'node:child_process';
5
+ import { logger } from '@rock-js/tools';
6
+ const SLICE_CONFIGS = {
7
+ 'ios-arm64': {
8
+ target: 'arm64-apple-ios15.0',
9
+ },
10
+ 'ios-arm64_x86_64-simulator': {
11
+ target: 'arm64-apple-ios15.0-simulator',
12
+ additionalTargets: ['x86_64-apple-ios15.0-simulator'],
13
+ },
14
+ };
15
+ /**
16
+ * Creates an empty static library for the given target.
17
+ */
18
+ function createEmptyStaticLib(target) {
19
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'framework-strip-'));
20
+ const tempObj = path.join(tempDir, 'empty.o');
21
+ const tempLib = path.join(tempDir, 'empty.a');
22
+ try {
23
+ execSync(`echo "" | xcrun clang -x c -c - -o "${tempObj}" -target ${target}`, {
24
+ stdio: 'pipe',
25
+ });
26
+ execSync(`xcrun ar rcs "${tempLib}" "${tempObj}"`, {
27
+ stdio: 'pipe',
28
+ });
29
+ }
30
+ catch (error) {
31
+ fs.rmSync(tempDir, { recursive: true });
32
+ throw new Error(`Failed to create empty static library for target ${target}: ${error instanceof Error ? error.message : error}`);
33
+ }
34
+ fs.unlinkSync(tempObj);
35
+ return tempLib;
36
+ }
37
+ /**
38
+ * Creates a fat static library combining multiple architectures.
39
+ */
40
+ function createFatStaticLib(targets) {
41
+ const libs = targets.map((target) => createEmptyStaticLib(target));
42
+ const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'framework-strip-fat-'));
43
+ const outputLib = path.join(outputDir, 'fat.a');
44
+ try {
45
+ execSync(`xcrun lipo -create ${libs.map((l) => `"${l}"`).join(' ')} -output "${outputLib}"`, {
46
+ stdio: 'pipe',
47
+ });
48
+ }
49
+ catch (error) {
50
+ libs.forEach((lib) => fs.rmSync(path.dirname(lib), { recursive: true }));
51
+ fs.rmSync(outputDir, { recursive: true });
52
+ throw new Error(`Failed to create fat static library: ${error instanceof Error ? error.message : error}`);
53
+ }
54
+ libs.forEach((lib) => {
55
+ fs.unlinkSync(lib);
56
+ fs.rmSync(path.dirname(lib), { recursive: true });
57
+ });
58
+ return outputLib;
59
+ }
60
+ /**
61
+ * Strips the binary from an xcframework, keeping only Swift module interfaces.
62
+ * This creates an "interface-only" framework where consumers can import the module
63
+ * but the actual symbols must come from another framework (e.g., BrownfieldLib).
64
+ *
65
+ * @param xcframeworkPath - Path to the .xcframework directory
66
+ */
67
+ export function stripFrameworkBinary(xcframeworkPath) {
68
+ if (!fs.existsSync(xcframeworkPath)) {
69
+ throw new Error(`XCFramework not found at: ${xcframeworkPath}`);
70
+ }
71
+ const frameworkName = path.basename(xcframeworkPath, '.xcframework');
72
+ logger.info(`Stripping binary from ${frameworkName}.xcframework (interface-only)...`);
73
+ const slices = fs.readdirSync(xcframeworkPath).filter((entry) => {
74
+ const fullPath = path.join(xcframeworkPath, entry);
75
+ return fs.statSync(fullPath).isDirectory() && entry.startsWith('ios-');
76
+ });
77
+ for (const sliceName of slices) {
78
+ const frameworkDir = path.join(xcframeworkPath, sliceName, `${frameworkName}.framework`);
79
+ const binaryPath = path.join(frameworkDir, frameworkName);
80
+ if (!fs.existsSync(binaryPath)) {
81
+ logger.warn(`No binary found at ${binaryPath}, skipping`);
82
+ continue;
83
+ }
84
+ const config = SLICE_CONFIGS[sliceName];
85
+ if (!config) {
86
+ logger.warn(`Unknown slice type: ${sliceName}, skipping`);
87
+ continue;
88
+ }
89
+ let emptyLib;
90
+ if (config.additionalTargets) {
91
+ // Create fat library for multiple architectures
92
+ emptyLib = createFatStaticLib([
93
+ config.target,
94
+ ...config.additionalTargets,
95
+ ]);
96
+ }
97
+ else {
98
+ // Create single-arch library
99
+ emptyLib = createEmptyStaticLib(config.target);
100
+ }
101
+ // Replace original binary with empty stub
102
+ fs.copyFileSync(emptyLib, binaryPath);
103
+ fs.unlinkSync(emptyLib);
104
+ fs.rmSync(path.dirname(emptyLib), { recursive: true });
105
+ }
106
+ logger.success(`${frameworkName}.xcframework is now interface-only`);
107
+ }
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import brownfieldCommands, { groupName as brownfieldCommandsGroupName, } from '.
6
6
  import brownieCommands, { groupName as brownieCommandsGroupName, } from './brownie/index.js';
7
7
  const program = new Command();
8
8
  program
9
- .name(styleText('magenta', 'brownie'))
9
+ .name(styleText('magenta', 'brownfield'))
10
10
  .usage(styleText('yellow', '[options] [command]'))
11
11
  .description(styleText('magentaBright', 'React Native Brownfield CLI - ') +
12
12
  styleText(['magenta', 'bold', 'underline'], 'Brownie'))
package/dist/main.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@callstack/brownfield-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "license": "MIT",
5
5
  "author": "Artur Morys-Magiera <artus9033@gmail.com>",
6
6
  "bin": {
@@ -99,4 +99,4 @@
99
99
  "engines": {
100
100
  "node": ">=20"
101
101
  }
102
- }
102
+ }
@@ -17,7 +17,7 @@ import { Command } from 'commander';
17
17
 
18
18
  import { isBrownieInstalled } from '../../brownie/config.js';
19
19
  import { runCodegen } from '../../brownie/commands/codegen.js';
20
- import { getProjectInfo } from '../utils/index.js';
20
+ import { getProjectInfo, stripFrameworkBinary } from '../utils/index.js';
21
21
  import {
22
22
  actionRunner,
23
23
  curryOptions,
@@ -101,6 +101,11 @@ export const packageIosCommand = curryOptions(
101
101
  outputPath: brownieOutputPath,
102
102
  });
103
103
 
104
+ // Strip the binary from Brownie.xcframework to make it interface-only.
105
+ // This avoids duplicate symbols when consumer apps embed both BrownfieldLib
106
+ // (which contains Brownie symbols) and Brownie.xcframework.
107
+ await stripFrameworkBinary(brownieOutputPath);
108
+
104
109
  logger.success(
105
110
  `Brownie.xcframework created at ${colorLink(relativeToCwd(brownieOutputPath))}`
106
111
  );
@@ -1,2 +1,3 @@
1
1
  export * from './paths.js';
2
2
  export * from './rn-cli.js';
3
+ export * from './stripFrameworkBinary.js';
@@ -0,0 +1,149 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { execSync } from 'node:child_process';
5
+ import { logger } from '@rock-js/tools';
6
+
7
+ interface SliceConfig {
8
+ target: string;
9
+ /** Additional targets for fat binaries */
10
+ additionalTargets?: string[];
11
+ }
12
+
13
+ const SLICE_CONFIGS: Record<string, SliceConfig> = {
14
+ 'ios-arm64': {
15
+ target: 'arm64-apple-ios15.0',
16
+ },
17
+ 'ios-arm64_x86_64-simulator': {
18
+ target: 'arm64-apple-ios15.0-simulator',
19
+ additionalTargets: ['x86_64-apple-ios15.0-simulator'],
20
+ },
21
+ };
22
+
23
+ /**
24
+ * Creates an empty static library for the given target.
25
+ */
26
+ function createEmptyStaticLib(target: string): string {
27
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'framework-strip-'));
28
+ const tempObj = path.join(tempDir, 'empty.o');
29
+ const tempLib = path.join(tempDir, 'empty.a');
30
+
31
+ try {
32
+ execSync(
33
+ `echo "" | xcrun clang -x c -c - -o "${tempObj}" -target ${target}`,
34
+ {
35
+ stdio: 'pipe',
36
+ }
37
+ );
38
+
39
+ execSync(`xcrun ar rcs "${tempLib}" "${tempObj}"`, {
40
+ stdio: 'pipe',
41
+ });
42
+ } catch (error) {
43
+ fs.rmSync(tempDir, { recursive: true });
44
+ throw new Error(
45
+ `Failed to create empty static library for target ${target}: ${error instanceof Error ? error.message : error}`
46
+ );
47
+ }
48
+
49
+ fs.unlinkSync(tempObj);
50
+
51
+ return tempLib;
52
+ }
53
+
54
+ /**
55
+ * Creates a fat static library combining multiple architectures.
56
+ */
57
+ function createFatStaticLib(targets: string[]): string {
58
+ const libs = targets.map((target) => createEmptyStaticLib(target));
59
+
60
+ const outputDir = fs.mkdtempSync(
61
+ path.join(os.tmpdir(), 'framework-strip-fat-')
62
+ );
63
+ const outputLib = path.join(outputDir, 'fat.a');
64
+
65
+ try {
66
+ execSync(
67
+ `xcrun lipo -create ${libs.map((l) => `"${l}"`).join(' ')} -output "${outputLib}"`,
68
+ {
69
+ stdio: 'pipe',
70
+ }
71
+ );
72
+ } catch (error) {
73
+ libs.forEach((lib) => fs.rmSync(path.dirname(lib), { recursive: true }));
74
+ fs.rmSync(outputDir, { recursive: true });
75
+ throw new Error(
76
+ `Failed to create fat static library: ${error instanceof Error ? error.message : error}`
77
+ );
78
+ }
79
+
80
+ libs.forEach((lib) => {
81
+ fs.unlinkSync(lib);
82
+ fs.rmSync(path.dirname(lib), { recursive: true });
83
+ });
84
+
85
+ return outputLib;
86
+ }
87
+
88
+ /**
89
+ * Strips the binary from an xcframework, keeping only Swift module interfaces.
90
+ * This creates an "interface-only" framework where consumers can import the module
91
+ * but the actual symbols must come from another framework (e.g., BrownfieldLib).
92
+ *
93
+ * @param xcframeworkPath - Path to the .xcframework directory
94
+ */
95
+ export function stripFrameworkBinary(xcframeworkPath: string): void {
96
+ if (!fs.existsSync(xcframeworkPath)) {
97
+ throw new Error(`XCFramework not found at: ${xcframeworkPath}`);
98
+ }
99
+
100
+ const frameworkName = path.basename(xcframeworkPath, '.xcframework');
101
+
102
+ logger.info(
103
+ `Stripping binary from ${frameworkName}.xcframework (interface-only)...`
104
+ );
105
+
106
+ const slices = fs.readdirSync(xcframeworkPath).filter((entry) => {
107
+ const fullPath = path.join(xcframeworkPath, entry);
108
+ return fs.statSync(fullPath).isDirectory() && entry.startsWith('ios-');
109
+ });
110
+
111
+ for (const sliceName of slices) {
112
+ const frameworkDir = path.join(
113
+ xcframeworkPath,
114
+ sliceName,
115
+ `${frameworkName}.framework`
116
+ );
117
+ const binaryPath = path.join(frameworkDir, frameworkName);
118
+
119
+ if (!fs.existsSync(binaryPath)) {
120
+ logger.warn(`No binary found at ${binaryPath}, skipping`);
121
+ continue;
122
+ }
123
+
124
+ const config = SLICE_CONFIGS[sliceName];
125
+ if (!config) {
126
+ logger.warn(`Unknown slice type: ${sliceName}, skipping`);
127
+ continue;
128
+ }
129
+
130
+ let emptyLib: string;
131
+ if (config.additionalTargets) {
132
+ // Create fat library for multiple architectures
133
+ emptyLib = createFatStaticLib([
134
+ config.target,
135
+ ...config.additionalTargets,
136
+ ]);
137
+ } else {
138
+ // Create single-arch library
139
+ emptyLib = createEmptyStaticLib(config.target);
140
+ }
141
+
142
+ // Replace original binary with empty stub
143
+ fs.copyFileSync(emptyLib, binaryPath);
144
+ fs.unlinkSync(emptyLib);
145
+ fs.rmSync(path.dirname(emptyLib), { recursive: true });
146
+ }
147
+
148
+ logger.success(`${frameworkName}.xcframework is now interface-only`);
149
+ }
package/src/index.ts CHANGED
@@ -15,7 +15,7 @@ import brownieCommands, {
15
15
  const program = new Command();
16
16
 
17
17
  program
18
- .name(styleText('magenta', 'brownie'))
18
+ .name(styleText('magenta', 'brownfield'))
19
19
  .usage(styleText('yellow', '[options] [command]'))
20
20
  .description(
21
21
  styleText('magentaBright', 'React Native Brownfield CLI - ') +