@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 +8 -0
- package/dist/brownfield/commands/packageIos.d.ts.map +1 -1
- package/dist/brownfield/commands/packageIos.js +54 -2
- package/dist/brownfield/utils/copyDebugBundleToSimulatorSlice.d.ts +8 -0
- package/dist/brownfield/utils/copyDebugBundleToSimulatorSlice.d.ts.map +1 -0
- package/dist/brownfield/utils/copyDebugBundleToSimulatorSlice.js +21 -0
- package/dist/brownfield/utils/createLocalSpmPackage.d.ts +10 -0
- package/dist/brownfield/utils/createLocalSpmPackage.d.ts.map +1 -0
- package/dist/brownfield/utils/createLocalSpmPackage.js +138 -0
- package/dist/brownfield/utils/prepareLocalSpmArtifacts.d.ts +8 -0
- package/dist/brownfield/utils/prepareLocalSpmArtifacts.d.ts.map +1 -0
- package/dist/brownfield/utils/prepareLocalSpmArtifacts.js +77 -0
- package/dist/brownfield/utils/resolvePackagedFrameworkName.d.ts +14 -0
- package/dist/brownfield/utils/resolvePackagedFrameworkName.d.ts.map +1 -0
- package/dist/brownfield/utils/resolvePackagedFrameworkName.js +61 -0
- package/package.json +1 -1
- package/src/brownfield/commands/packageIos.ts +92 -10
- package/src/brownfield/utils/copyDebugBundleToSimulatorSlice.ts +58 -0
- package/src/brownfield/utils/createLocalSpmPackage.ts +208 -0
- package/src/brownfield/utils/prepareLocalSpmArtifacts.ts +117 -0
- package/src/brownfield/utils/resolvePackagedFrameworkName.ts +98 -0
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;
|
|
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
|
@@ -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
|
+
}
|