@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
@@ -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
+ }
@@ -56,6 +56,15 @@ export function isExpoProject(projectRoot: string): boolean {
56
56
  return hasExpoConfig && dependsOnExpo;
57
57
  }
58
58
 
59
+ export function getExpoSdkMajor(projectRoot: string): number | null {
60
+ const rawExpoVersion = getExpoConfigIfIsExpo(projectRoot)?.exp.sdkVersion;
61
+ if (!rawExpoVersion) {
62
+ return null;
63
+ }
64
+ const expoSdkMajor = parseInt(rawExpoVersion.split('.')[0], 10);
65
+ return Number.isFinite(expoSdkMajor) ? expoSdkMajor : null;
66
+ }
67
+
59
68
  /**
60
69
  * Fills the RNC CLI project config from the Expo config by mutating the passed in `options.projectConfig` object in place
61
70
  */
@@ -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
+ }
@@ -0,0 +1,67 @@
1
+ import { getReactNativeVersion, versionCompare } from '@rock-js/tools';
2
+
3
+ import { getExpoSdkMajor, isExpoProject } from './project.js';
4
+
5
+ /** Minimum RN version that can opt in to prebuilts via `--use-prebuilt-rn-core`. */
6
+ export const MIN_REACT_NATIVE_VERSION_FOR_OPT_IN_PREBUILT_RN_CORE = '0.81.0';
7
+ /** Minimum RN version where Brownfield enables prebuilts by default (vanilla projects). */
8
+ export const MIN_REACT_NATIVE_VERSION_FOR_PREBUILT_RN_CORE_BY_DEFAULT =
9
+ '0.84.0';
10
+ export const MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_RN_CORE_BY_DEFAULT = 55;
11
+
12
+ export type PrebuiltRNCoreSupportResult =
13
+ | { supported: true; enabledByDefault: boolean; reason?: never }
14
+ | { supported: false; enabledByDefault?: never; reason: string };
15
+
16
+ export function supportsPrebuiltRNCore({
17
+ projectRoot,
18
+ }: {
19
+ projectRoot: string;
20
+ }): PrebuiltRNCoreSupportResult {
21
+ const reactNativeVersion = getReactNativeVersion(projectRoot);
22
+
23
+ if (reactNativeVersion === 'unknown') {
24
+ return {
25
+ supported: false,
26
+ reason:
27
+ 'Cannot use --use-prebuilt-rn-core: unable to resolve the installed react-native version.',
28
+ };
29
+ }
30
+
31
+ if (
32
+ versionCompare(
33
+ reactNativeVersion,
34
+ MIN_REACT_NATIVE_VERSION_FOR_OPT_IN_PREBUILT_RN_CORE
35
+ ) < 0
36
+ ) {
37
+ return {
38
+ supported: false,
39
+ reason: `--use-prebuilt-rn-core requires React Native ${MIN_REACT_NATIVE_VERSION_FOR_OPT_IN_PREBUILT_RN_CORE} or newer (found ${reactNativeVersion}).`,
40
+ };
41
+ }
42
+
43
+ if (isExpoProject(projectRoot)) {
44
+ const expoSdkMajor = getExpoSdkMajor(projectRoot);
45
+
46
+ if (
47
+ expoSdkMajor === null ||
48
+ expoSdkMajor < MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_RN_CORE_BY_DEFAULT
49
+ ) {
50
+ const sdkLabel = expoSdkMajor === null ? 'unknown' : String(expoSdkMajor);
51
+ return {
52
+ supported: false,
53
+ reason: `--use-prebuilt-rn-core is unsupported in Expo SDK ${sdkLabel}: packaging brownfield with prebuilts requires Expo SDK ${MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_RN_CORE_BY_DEFAULT} or newer.`,
54
+ };
55
+ }
56
+
57
+ return { supported: true, enabledByDefault: true };
58
+ }
59
+
60
+ const enabledByDefault =
61
+ versionCompare(
62
+ reactNativeVersion,
63
+ MIN_REACT_NATIVE_VERSION_FOR_PREBUILT_RN_CORE_BY_DEFAULT
64
+ ) >= 0;
65
+
66
+ return { supported: true, enabledByDefault };
67
+ }