@callstack/brownfield-cli 6.0.0 → 6.1.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 +22 -0
- package/dist/brownfield/commands/packageIos.d.ts +2 -0
- package/dist/brownfield/commands/packageIos.d.ts.map +1 -1
- package/dist/brownfield/commands/packageIos.js +102 -18
- package/dist/brownfield/utils/copyDebugBundleToSimulatorSlice.d.ts +4 -1
- package/dist/brownfield/utils/copyDebugBundleToSimulatorSlice.d.ts.map +1 -1
- package/dist/brownfield/utils/copyDebugBundleToSimulatorSlice.js +6 -1
- package/dist/brownfield/utils/destinationSdks.d.ts +36 -0
- package/dist/brownfield/utils/destinationSdks.d.ts.map +1 -0
- package/dist/brownfield/utils/destinationSdks.js +70 -0
- package/dist/brownfield/utils/emitExpoSupportXcframeworks.d.ts +9 -1
- package/dist/brownfield/utils/emitExpoSupportXcframeworks.d.ts.map +1 -1
- package/dist/brownfield/utils/emitExpoSupportXcframeworks.js +27 -5
- package/dist/brownfield/utils/expo.d.ts +7 -0
- package/dist/brownfield/utils/expo.d.ts.map +1 -1
- package/dist/brownfield/utils/expo.js +28 -1
- package/dist/brownfield/utils/project.d.ts +14 -0
- package/dist/brownfield/utils/project.d.ts.map +1 -1
- package/dist/brownfield/utils/project.js +47 -0
- package/dist/brownfield/utils/resolvePackagedFrameworkName.d.ts +4 -1
- package/dist/brownfield/utils/resolvePackagedFrameworkName.d.ts.map +1 -1
- package/dist/brownfield/utils/resolvePackagedFrameworkName.js +7 -3
- package/dist/brownfield/utils/supportsPrebuiltExpo.d.ts +20 -0
- package/dist/brownfield/utils/supportsPrebuiltExpo.d.ts.map +1 -1
- package/dist/brownfield/utils/supportsPrebuiltExpo.js +68 -1
- package/dist/expoPluginConfig.d.ts +2 -0
- package/dist/expoPluginConfig.d.ts.map +1 -1
- package/dist/expoPluginConfig.js +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/schema.json +7 -0
- package/src/brownfield/commands/packageIos.ts +143 -49
- package/src/brownfield/utils/copyDebugBundleToSimulatorSlice.ts +10 -0
- package/src/brownfield/utils/destinationSdks.ts +122 -0
- package/src/brownfield/utils/emitExpoSupportXcframeworks.ts +48 -4
- package/src/brownfield/utils/expo.ts +38 -1
- package/src/brownfield/utils/project.ts +65 -0
- package/src/brownfield/utils/resolvePackagedFrameworkName.ts +16 -5
- package/src/brownfield/utils/supportsPrebuiltExpo.ts +98 -2
- package/src/expoPluginConfig.ts +4 -0
- package/src/index.ts +12 -0
- package/src/types.ts +8 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { RockError } from '@rock-js/tools';
|
|
5
|
+
|
|
6
|
+
/** Xcode SDK a build product directory belongs to (`Release-iphoneos`, `Release-iphonesimulator`). */
|
|
7
|
+
export type AppleSdk = 'iphoneos' | 'iphonesimulator';
|
|
8
|
+
|
|
9
|
+
/** Both slices, in the order Rock's `packageIosAction` builds them. */
|
|
10
|
+
export const ALL_APPLE_SDKS: AppleSdk[] = ['iphoneos', 'iphonesimulator'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Map a single `--destination` value onto the SDK(s) its build products land in.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors `resolveDestination` in `@rock-js/platform-apple-helpers`: the aliases
|
|
16
|
+
* `device`/`simulator` plus any raw `xcodebuild -destination` string. A value we
|
|
17
|
+
* cannot classify (`id=<udid>`, `name=iPhone 17`) may resolve to either SDK, so it
|
|
18
|
+
* widens to both rather than silently dropping a slice.
|
|
19
|
+
*/
|
|
20
|
+
function resolveSdksForDestination(destination: string): AppleSdk[] {
|
|
21
|
+
const normalized = destination.trim().toLowerCase();
|
|
22
|
+
|
|
23
|
+
if (normalized === 'device') {
|
|
24
|
+
return ['iphoneos'];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (normalized === 'simulator' || normalized.includes('simulator')) {
|
|
28
|
+
return ['iphonesimulator'];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (normalized.includes('platform=ios')) {
|
|
32
|
+
return ['iphoneos'];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return ALL_APPLE_SDKS;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* SDK slices a `package:ios` run produces for the given `--destination` values.
|
|
40
|
+
*
|
|
41
|
+
* Omitting `--destination` builds both slices, matching Rock's default.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveDestinationSdks(
|
|
44
|
+
destination?: string[] | null
|
|
45
|
+
): AppleSdk[] {
|
|
46
|
+
if (!destination || destination.length === 0) {
|
|
47
|
+
return ALL_APPLE_SDKS;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const sdks = new Set(destination.flatMap(resolveSdksForDestination));
|
|
51
|
+
|
|
52
|
+
// keep a stable order regardless of the order the flags were passed in
|
|
53
|
+
return ALL_APPLE_SDKS.filter((sdk) => sdks.has(sdk));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether `directoryPath` holds a build product `mergeFrameworks` can consume:
|
|
58
|
+
* either a real `.framework` bundle or the static library it synthesizes one from.
|
|
59
|
+
*/
|
|
60
|
+
export function hasFrameworkBuildProduct(
|
|
61
|
+
directoryPath: string,
|
|
62
|
+
frameworkName: string
|
|
63
|
+
) {
|
|
64
|
+
return (
|
|
65
|
+
fs.existsSync(path.join(directoryPath, `${frameworkName}.framework`)) ||
|
|
66
|
+
fs.existsSync(path.join(directoryPath, `lib${frameworkName}.a`))
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface CollectFrameworkPathsOptions {
|
|
71
|
+
productsPath: string;
|
|
72
|
+
configuration: string;
|
|
73
|
+
sdks: AppleSdk[];
|
|
74
|
+
frameworkName: string;
|
|
75
|
+
/** Sub-directory of the configuration products dir, e.g. `Brownie` for a Pod target. */
|
|
76
|
+
productSubDir?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* `.framework` paths to merge into an XCFramework, limited to the slices that were
|
|
81
|
+
* actually built. Slices with no build product on disk are dropped, so a
|
|
82
|
+
* `--destination simulator` run merges the simulator slice alone instead of failing
|
|
83
|
+
* on a missing `-iphoneos` directory.
|
|
84
|
+
*
|
|
85
|
+
* Throws when no slice has a build product: passing an empty list to
|
|
86
|
+
* `mergeFrameworks` would surface as xcodebuild's opaque
|
|
87
|
+
* "at least one framework or library must be specified".
|
|
88
|
+
*/
|
|
89
|
+
export function collectFrameworkPaths({
|
|
90
|
+
productsPath,
|
|
91
|
+
configuration,
|
|
92
|
+
sdks,
|
|
93
|
+
frameworkName,
|
|
94
|
+
productSubDir,
|
|
95
|
+
}: CollectFrameworkPathsOptions): string[] {
|
|
96
|
+
const searchedDirectories = sdks.map((sdk) =>
|
|
97
|
+
path.join(
|
|
98
|
+
productsPath,
|
|
99
|
+
`${configuration}-${sdk}`,
|
|
100
|
+
...(productSubDir ? [productSubDir] : [])
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const frameworkPaths = searchedDirectories
|
|
105
|
+
.filter((directoryPath) =>
|
|
106
|
+
hasFrameworkBuildProduct(directoryPath, frameworkName)
|
|
107
|
+
)
|
|
108
|
+
.map((directoryPath) =>
|
|
109
|
+
path.join(directoryPath, `${frameworkName}.framework`)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (frameworkPaths.length === 0) {
|
|
113
|
+
throw new RockError(
|
|
114
|
+
`Could not find a build product for ${frameworkName} in the ${configuration} configuration. ` +
|
|
115
|
+
`Looked for ${frameworkName}.framework or lib${frameworkName}.a in:\n` +
|
|
116
|
+
searchedDirectories.map((dir) => ` - ${dir}`).join('\n') +
|
|
117
|
+
`\nIf the build produced an .app instead of a framework, the wrong scheme was built; pass --scheme with your brownfield framework scheme.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return frameworkPaths;
|
|
122
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import { RockError } from '@rock-js/tools';
|
|
4
|
+
import { logger, RockError } from '@rock-js/tools';
|
|
5
5
|
|
|
6
6
|
import { normalizeCopiedXcframework } from './normalizeCopiedXcframework.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
getExpoIosUsePrecompiledModules,
|
|
9
|
+
getExpoSdkMajor,
|
|
10
|
+
isExpoProject,
|
|
11
|
+
} from './project.js';
|
|
8
12
|
|
|
9
13
|
import { MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO } from './supportsPrebuiltExpo.js';
|
|
10
14
|
|
|
@@ -87,14 +91,32 @@ function resolveExpoFrameworkSourcePath(
|
|
|
87
91
|
throw new RockError(`Unsupported Expo XCFramework: ${frameworkName}`);
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
function getPrebuiltExpoRemedy(projectRoot: string) {
|
|
95
|
+
const cause =
|
|
96
|
+
getExpoIosUsePrecompiledModules(projectRoot) === false
|
|
97
|
+
? 'expo-build-properties sets ios.usePrecompiledModules to false, so prebuilt Expo XCFrameworks are never produced. '
|
|
98
|
+
: '';
|
|
99
|
+
return `${cause}Either re-run with --use-prebuilt-expo false to build Expo modules from source, or run \`pod install\` (with ios.usePrecompiledModules enabled) to materialize the prebuilts.`;
|
|
100
|
+
}
|
|
101
|
+
|
|
90
102
|
export function emitExpoSupportXcframeworks({
|
|
91
103
|
projectRoot,
|
|
92
104
|
packageDir,
|
|
93
105
|
usePrebuiltExpo = true,
|
|
106
|
+
usePrebuiltExpoExplicit = true,
|
|
107
|
+
onDegradeToSource,
|
|
94
108
|
}: {
|
|
95
109
|
projectRoot: string;
|
|
96
110
|
packageDir: string;
|
|
97
111
|
usePrebuiltExpo?: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Whether `usePrebuiltExpo` was chosen by the user (CLI flag or brownfield.config.json)
|
|
114
|
+
* rather than inferred from the Expo SDK version. When inferred, missing prebuilts
|
|
115
|
+
* degrade to building Expo modules from source instead of throwing.
|
|
116
|
+
*/
|
|
117
|
+
usePrebuiltExpoExplicit?: boolean;
|
|
118
|
+
/** Called when an inferred `usePrebuiltExpo` was degraded to `false` because prebuilts are absent */
|
|
119
|
+
onDegradeToSource?: () => void;
|
|
98
120
|
}) {
|
|
99
121
|
if (!isExpoProject(projectRoot)) {
|
|
100
122
|
return false;
|
|
@@ -108,6 +130,22 @@ export function emitExpoSupportXcframeworks({
|
|
|
108
130
|
return false;
|
|
109
131
|
}
|
|
110
132
|
|
|
133
|
+
if (usePrebuiltExpo && !usePrebuiltExpoExplicit) {
|
|
134
|
+
const missingPrebuilts = ALL_EXPO_SUPPORT_XCFRAMEWORK_NAMES.filter(
|
|
135
|
+
(frameworkName) =>
|
|
136
|
+
!fs.existsSync(resolveExpoFrameworkSourcePath(projectRoot, frameworkName))
|
|
137
|
+
);
|
|
138
|
+
// ExpoModulesJSI ships in node_modules and is not a Pods prebuilt; only degrade
|
|
139
|
+
// when Pods-provided prebuilts are absent.
|
|
140
|
+
if (missingPrebuilts.some((name) => name !== 'ExpoModulesJSI')) {
|
|
141
|
+
logger.warn(
|
|
142
|
+
`Prebuilt Expo XCFrameworks were not found (${missingPrebuilts.join(', ')}). ${getPrebuiltExpoRemedy(projectRoot)} Continuing without prebuilt Expo (--use-prebuilt-expo false); pass --use-prebuilt-expo true to make this an error.`
|
|
143
|
+
);
|
|
144
|
+
usePrebuiltExpo = false;
|
|
145
|
+
onDegradeToSource?.();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
111
149
|
for (const frameworkName of getExpoSupportXcframeworkNames(usePrebuiltExpo)) {
|
|
112
150
|
const sourcePath = resolveExpoFrameworkSourcePath(
|
|
113
151
|
projectRoot,
|
|
@@ -115,7 +153,7 @@ export function emitExpoSupportXcframeworks({
|
|
|
115
153
|
);
|
|
116
154
|
if (!fs.existsSync(sourcePath)) {
|
|
117
155
|
throw new RockError(
|
|
118
|
-
`Expected Expo SDK ${MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO}+ XCFramework not found: ${frameworkName}.xcframework at ${path.relative(projectRoot, sourcePath)}`
|
|
156
|
+
`Expected Expo SDK ${MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO}+ XCFramework not found: ${frameworkName}.xcframework at ${path.relative(projectRoot, sourcePath)}. ${getPrebuiltExpoRemedy(projectRoot)}`
|
|
119
157
|
);
|
|
120
158
|
}
|
|
121
159
|
|
|
@@ -124,7 +162,13 @@ export function emitExpoSupportXcframeworks({
|
|
|
124
162
|
`${frameworkName}.xcframework`
|
|
125
163
|
);
|
|
126
164
|
fs.rmSync(destinationPath, { recursive: true, force: true });
|
|
127
|
-
|
|
165
|
+
// With precompiled Expo, CocoaPods symlinks expo-image's dependencies into
|
|
166
|
+
// node_modules. Copy what the link points to, so the package is portable
|
|
167
|
+
// and normalizing it doesn't modify node_modules.
|
|
168
|
+
fs.cpSync(fs.realpathSync(sourcePath), destinationPath, {
|
|
169
|
+
recursive: true,
|
|
170
|
+
verbatimSymlinks: true,
|
|
171
|
+
});
|
|
128
172
|
normalizeCopiedXcframework(destinationPath);
|
|
129
173
|
}
|
|
130
174
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
2
4
|
|
|
3
5
|
import { logger } from '@rock-js/tools';
|
|
4
6
|
|
|
@@ -41,6 +43,34 @@ function getNpxCommand() {
|
|
|
41
43
|
return process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
function isGitIgnored(targetPath: string): boolean {
|
|
47
|
+
// `git check-ignore` exits 0 when ignored, 1 when not, 128 outside a repo.
|
|
48
|
+
const result = spawnSync('git', ['check-ignore', '-q', targetPath], {
|
|
49
|
+
cwd: path.dirname(targetPath),
|
|
50
|
+
stdio: 'ignore',
|
|
51
|
+
});
|
|
52
|
+
if (result.status !== 0 && result.status !== 1) {
|
|
53
|
+
logger.warn(
|
|
54
|
+
`Couldn't check with git whether ${path.basename(targetPath)}/ is gitignored, so it's treated as committed and expo prebuild is skipped. If this project uses Continuous Native Generation, run \`npx expo prebuild\` before packaging.`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return result.status === 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether the platform's native project is generated by `expo prebuild` (Continuous Native
|
|
62
|
+
* Generation) rather than maintained by hand. A bare React Native app that installed Expo modules
|
|
63
|
+
* commits its native directories; running prebuild there would wipe them (including the
|
|
64
|
+
* brownfield library module), so prebuild only runs when the directory is missing or gitignored.
|
|
65
|
+
*/
|
|
66
|
+
export function usesContinuousNativeGeneration(
|
|
67
|
+
projectRoot: string,
|
|
68
|
+
platform: ExpoPlatform
|
|
69
|
+
): boolean {
|
|
70
|
+
const nativeDir = path.join(projectRoot, platform);
|
|
71
|
+
return !fs.existsSync(nativeDir) || isGitIgnored(nativeDir);
|
|
72
|
+
}
|
|
73
|
+
|
|
44
74
|
export async function runExpoPrebuildIfNeeded({
|
|
45
75
|
projectRoot,
|
|
46
76
|
platform,
|
|
@@ -52,6 +82,13 @@ export async function runExpoPrebuildIfNeeded({
|
|
|
52
82
|
return false;
|
|
53
83
|
}
|
|
54
84
|
|
|
85
|
+
if (!usesContinuousNativeGeneration(projectRoot, platform)) {
|
|
86
|
+
logger.info(
|
|
87
|
+
`Expo modules detected in a project with a committed ${platform} directory. Skipping expo prebuild.`
|
|
88
|
+
);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
55
92
|
logger.info(
|
|
56
93
|
`Expo project detected. Running expo prebuild for ${platform}...`
|
|
57
94
|
);
|
|
@@ -81,6 +81,71 @@ export function isExpoProject(projectRoot: string): boolean {
|
|
|
81
81
|
return hasExpoAppConfig(projectRoot) && projectDependsOnExpo(projectRoot);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
type ExpoPluginEntry = string | [string, ...unknown[]] | unknown;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Reads `ios.usePrecompiledModules` from the `expo-build-properties` plugin entry.
|
|
88
|
+
*
|
|
89
|
+
* `getExpoConfigIfIsExpo` uses `skipPlugins: true`, which makes `@expo/config`
|
|
90
|
+
* delete `exp.plugins`, so the plugins array is read from `rootConfig` (the raw
|
|
91
|
+
* static app.json / app.config.json) instead. `rootConfig` is the raw *static*
|
|
92
|
+
* config only, so a dynamic config (app.config.js/ts) exposes no plugins that
|
|
93
|
+
* way; for those we retry once with plugins enabled, which keeps the plugins
|
|
94
|
+
* array. That retry can throw for plugins that are not loadable as Node modules
|
|
95
|
+
* (the reason `skipPlugins` is set in the first place), so it is best-effort.
|
|
96
|
+
*
|
|
97
|
+
* @returns the explicit boolean value, or `undefined` if it is not set or not resolvable
|
|
98
|
+
*/
|
|
99
|
+
export function getExpoIosUsePrecompiledModules(
|
|
100
|
+
projectRoot: string
|
|
101
|
+
): boolean | undefined {
|
|
102
|
+
const config = getExpoConfigIfIsExpo(projectRoot);
|
|
103
|
+
if (!config) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const rootConfig = config.rootConfig as
|
|
108
|
+
| { plugins?: ExpoPluginEntry[]; expo?: { plugins?: ExpoPluginEntry[] } }
|
|
109
|
+
| undefined;
|
|
110
|
+
let plugins =
|
|
111
|
+
(config.exp as { plugins?: ExpoPluginEntry[] }).plugins ??
|
|
112
|
+
rootConfig?.expo?.plugins ??
|
|
113
|
+
rootConfig?.plugins;
|
|
114
|
+
|
|
115
|
+
if (!Array.isArray(plugins)) {
|
|
116
|
+
// Dynamic config: no static plugins array exists. Retry with plugins
|
|
117
|
+
// enabled, which preserves `exp.plugins`. Best-effort — plugin resolution
|
|
118
|
+
// can throw, and an unreadable config is treated the same as an unset value.
|
|
119
|
+
try {
|
|
120
|
+
plugins = (
|
|
121
|
+
getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp as {
|
|
122
|
+
plugins?: ExpoPluginEntry[];
|
|
123
|
+
}
|
|
124
|
+
).plugins;
|
|
125
|
+
} catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!Array.isArray(plugins)) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const plugin of plugins) {
|
|
135
|
+
if (!Array.isArray(plugin) || plugin[0] !== 'expo-build-properties') {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const value = (
|
|
139
|
+
plugin[1] as { ios?: { usePrecompiledModules?: unknown } } | undefined
|
|
140
|
+
)?.ios?.usePrecompiledModules;
|
|
141
|
+
if (typeof value === 'boolean') {
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
84
149
|
export function getExpoSdkMajor(projectRoot: string): number | null {
|
|
85
150
|
const rawExpoVersion = getExpoConfigIfIsExpo(projectRoot)?.exp.sdkVersion;
|
|
86
151
|
if (!rawExpoVersion) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
import { ALL_APPLE_SDKS, type AppleSdk } from './destinationSdks.js';
|
|
5
|
+
|
|
4
6
|
type Resolution = 'explicit' | 'detected' | 'not_found' | 'ambiguous';
|
|
5
7
|
|
|
6
8
|
export interface ResolvePackagedFrameworkNameResult {
|
|
@@ -13,6 +15,8 @@ interface ResolvePackagedFrameworkNameOptions {
|
|
|
13
15
|
explicitScheme?: string;
|
|
14
16
|
productsPath: string;
|
|
15
17
|
configuration: string;
|
|
18
|
+
/** Slices the build produced; defaults to both. */
|
|
19
|
+
sdks?: AppleSdk[];
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
function collectFrameworkCandidates(configurationProductsPath: string): string[] {
|
|
@@ -61,6 +65,7 @@ export function resolvePackagedFrameworkName({
|
|
|
61
65
|
explicitScheme,
|
|
62
66
|
productsPath,
|
|
63
67
|
configuration,
|
|
68
|
+
sdks = ALL_APPLE_SDKS,
|
|
64
69
|
}: ResolvePackagedFrameworkNameOptions): ResolvePackagedFrameworkNameResult {
|
|
65
70
|
if (explicitScheme) {
|
|
66
71
|
return {
|
|
@@ -69,11 +74,17 @@ export function resolvePackagedFrameworkName({
|
|
|
69
74
|
};
|
|
70
75
|
}
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
// the JS bundle is emitted per slice, so scan every slice that was built rather
|
|
78
|
+
// than assuming the device one exists (`--destination simulator` skips it)
|
|
79
|
+
const candidates = [
|
|
80
|
+
...new Set(
|
|
81
|
+
sdks.flatMap((sdk) =>
|
|
82
|
+
collectFrameworkCandidates(
|
|
83
|
+
path.join(productsPath, `${configuration}-${sdk}`)
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
),
|
|
87
|
+
].sort();
|
|
77
88
|
|
|
78
89
|
if (candidates.length === 1) {
|
|
79
90
|
return {
|
|
@@ -1,11 +1,69 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { getReactNativeVersion } from '@rock-js/tools';
|
|
6
|
+
|
|
7
|
+
import { getExpoIosUsePrecompiledModules, getExpoSdkMajor, isExpoProject } from './project.js';
|
|
2
8
|
|
|
3
9
|
export const MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO = 56;
|
|
4
10
|
|
|
5
11
|
export type PrebuiltExpoSupportResult =
|
|
6
|
-
| {
|
|
12
|
+
| {
|
|
13
|
+
supported: true;
|
|
14
|
+
enabledByDefault: boolean;
|
|
15
|
+
/** Why prebuilts are off by default even though they are supported. */
|
|
16
|
+
defaultDisabledReason?: string;
|
|
17
|
+
reason?: never;
|
|
18
|
+
}
|
|
7
19
|
| { supported: false; enabledByDefault?: never; reason: string };
|
|
8
20
|
|
|
21
|
+
function majorMinor(version: string): string {
|
|
22
|
+
return version.split('.').slice(0, 2).join('.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The React Native version the installed Expo SDK pins. Expo ships its
|
|
27
|
+
* precompiled modules built against this version.
|
|
28
|
+
*/
|
|
29
|
+
export function getExpoReactNativeVersion(projectRoot: string): string | null {
|
|
30
|
+
try {
|
|
31
|
+
const require = createRequire(path.join(projectRoot, 'package.json'));
|
|
32
|
+
const expoPackageJson = require.resolve('expo/package.json');
|
|
33
|
+
const bundledNativeModules = JSON.parse(
|
|
34
|
+
fs.readFileSync(
|
|
35
|
+
path.join(path.dirname(expoPackageJson), 'bundledNativeModules.json'),
|
|
36
|
+
'utf8'
|
|
37
|
+
)
|
|
38
|
+
) as Record<string, string>;
|
|
39
|
+
return bundledNativeModules['react-native'] ?? null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Describes a React Native version the Expo prebuilts were not built for, or
|
|
47
|
+
* returns null when they match. Prebuilt Expo modules link against React
|
|
48
|
+
* Native's C++ API, which changes between minor versions, so a mismatch
|
|
49
|
+
* crashes at runtime.
|
|
50
|
+
*/
|
|
51
|
+
export function getPrebuiltExpoReactNativeMismatch(
|
|
52
|
+
projectRoot: string
|
|
53
|
+
): string | null {
|
|
54
|
+
const expected = getExpoReactNativeVersion(projectRoot);
|
|
55
|
+
const found = getReactNativeVersion(projectRoot);
|
|
56
|
+
if (!expected || found === 'unknown') {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (majorMinor(expected) === majorMinor(found)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return `Expo's precompiled modules are built for React Native ${majorMinor(expected)}, but this project uses ${found}.`;
|
|
65
|
+
}
|
|
66
|
+
|
|
9
67
|
export function supportsPrebuiltExpo({
|
|
10
68
|
projectRoot,
|
|
11
69
|
}: {
|
|
@@ -27,8 +85,46 @@ export function supportsPrebuiltExpo({
|
|
|
27
85
|
};
|
|
28
86
|
}
|
|
29
87
|
|
|
88
|
+
const reactNativeMismatch = getPrebuiltExpoReactNativeMismatch(projectRoot);
|
|
89
|
+
if (reactNativeMismatch) {
|
|
90
|
+
return {
|
|
91
|
+
supported: true,
|
|
92
|
+
enabledByDefault: false,
|
|
93
|
+
defaultDisabledReason: reactNativeMismatch,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (getExpoIosUsePrecompiledModules(projectRoot) === false) {
|
|
98
|
+
return {
|
|
99
|
+
supported: false,
|
|
100
|
+
reason:
|
|
101
|
+
"--use-prebuilt-expo is unsupported: expo-build-properties sets ios.usePrecompiledModules to false in your Expo config, so prebuilt Expo XCFrameworks are never produced. Set it to true (or remove it) and run 'pod install', or pass --use-prebuilt-expo false to build Expo modules from source.",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
30
105
|
return {
|
|
31
106
|
supported: true,
|
|
32
107
|
enabledByDefault: true,
|
|
33
108
|
};
|
|
34
109
|
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Whether the project's Podfile turns Expo's precompiled modules off. Expo
|
|
113
|
+
* Podfiles force `EXPO_USE_PRECOMPILED_MODULES` to '0' when
|
|
114
|
+
* `ios/Podfile.properties.json` sets it to "false", whatever the environment says.
|
|
115
|
+
*/
|
|
116
|
+
export function isPrecompiledExpoDisabledByPodfile(
|
|
117
|
+
projectRoot: string
|
|
118
|
+
): boolean {
|
|
119
|
+
try {
|
|
120
|
+
const properties = JSON.parse(
|
|
121
|
+
fs.readFileSync(
|
|
122
|
+
path.join(projectRoot, 'ios', 'Podfile.properties.json'),
|
|
123
|
+
'utf8'
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
return properties.EXPO_USE_PRECOMPILED_MODULES === 'false';
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
package/src/expoPluginConfig.ts
CHANGED
|
@@ -28,6 +28,7 @@ export type BrownfieldPluginProps = {
|
|
|
28
28
|
extraProguardRules?: string[];
|
|
29
29
|
useLocalMaven?: boolean;
|
|
30
30
|
missingDimensionStrategies?: string[];
|
|
31
|
+
excludeDependencies?: string[];
|
|
31
32
|
};
|
|
32
33
|
};
|
|
33
34
|
|
|
@@ -45,6 +46,7 @@ export type ResolvedBrownfieldPluginAndroidConfig = {
|
|
|
45
46
|
extraProguardRules: string[];
|
|
46
47
|
useLocalMaven: boolean;
|
|
47
48
|
missingDimensionStrategies: string[];
|
|
49
|
+
excludeDependencies: string[];
|
|
48
50
|
};
|
|
49
51
|
|
|
50
52
|
export type ResolvedBrownfieldPluginIosConfig = {
|
|
@@ -214,6 +216,8 @@ export function resolveBrownfieldPluginConfig(
|
|
|
214
216
|
useLocalMaven: effectiveProps.android?.useLocalMaven ?? false,
|
|
215
217
|
missingDimensionStrategies:
|
|
216
218
|
effectiveProps.android?.missingDimensionStrategies ?? [],
|
|
219
|
+
excludeDependencies:
|
|
220
|
+
effectiveProps.android?.excludeDependencies ?? [],
|
|
217
221
|
...(effectiveProps.android?.targetSdkVersion != null
|
|
218
222
|
? { targetSdkVersion: effectiveProps.android.targetSdkVersion }
|
|
219
223
|
: {}),
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,18 @@ import navigationCommands, {
|
|
|
15
15
|
groupName as navigationCommandsGroupName,
|
|
16
16
|
} from './navigation/index.js';
|
|
17
17
|
|
|
18
|
+
// CocoaPods (spawned by Rock) raises Encoding::CompatibilityError under a non-UTF-8
|
|
19
|
+
// locale. Ruby resolves the character encoding as LC_ALL > LC_CTYPE > LANG, so check
|
|
20
|
+
// them in that precedence. Default to UTF-8 only when the effective value is missing
|
|
21
|
+
// or non-UTF-8.
|
|
22
|
+
const inheritedCharset =
|
|
23
|
+
process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG || '';
|
|
24
|
+
if (!/utf-?8/i.test(inheritedCharset)) {
|
|
25
|
+
process.env.LANG = 'en_US.UTF-8';
|
|
26
|
+
process.env.LC_CTYPE = 'en_US.UTF-8';
|
|
27
|
+
process.env.LC_ALL = 'en_US.UTF-8';
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
const program = new Command();
|
|
19
31
|
|
|
20
32
|
program
|
package/src/types.ts
CHANGED
|
@@ -113,10 +113,17 @@ export type BrownfieldExpoAndroidConfig = {
|
|
|
113
113
|
*/
|
|
114
114
|
useLocalMaven?: boolean;
|
|
115
115
|
|
|
116
|
-
/**
|
|
116
|
+
/**
|
|
117
117
|
* Missing dimension strategies as dimension + flavor(s), e.g. "type, alpha".
|
|
118
118
|
*/
|
|
119
119
|
missingDimensionStrategies?: string[];
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Dependencies to keep out of the published AAR's POM and module.json, as
|
|
123
|
+
* "group:module" or "group" for a whole group. Written as Gradle excludes
|
|
124
|
+
* into the generated module, e.g. "com.caverock:androidsvg".
|
|
125
|
+
*/
|
|
126
|
+
excludeDependencies?: string[];
|
|
120
127
|
};
|
|
121
128
|
|
|
122
129
|
/**
|