react-native 0.88.0-rc.0 → 0.88.0-rc.2

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 (61) hide show
  1. package/Libraries/Core/ReactNativeVersion.js +1 -1
  2. package/Libraries/Renderer/implementations/ReactFabric-dev.js +3056 -1531
  3. package/Libraries/Renderer/implementations/ReactFabric-prod.js +2239 -1011
  4. package/Libraries/Renderer/implementations/ReactFabric-profiling.js +2451 -1124
  5. package/React/Base/RCTVersion.m +1 -1
  6. package/React/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h +3 -4
  7. package/React/FBReactNativeSpec/FBReactNativeSpecJSI.h +8 -0
  8. package/ReactAndroid/gradle.properties +1 -1
  9. package/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +7 -1
  10. package/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +11 -1
  11. package/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +3 -1
  12. package/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +3 -1
  13. package/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +12 -1
  14. package/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsOverrides_RNOSS_Canary_Android.kt +3 -1
  15. package/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +3 -1
  16. package/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.kt +1 -1
  17. package/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +15 -1
  18. package/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +4 -1
  19. package/ReactCommon/cxxreact/ReactNativeVersion.h +1 -1
  20. package/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +5 -1
  21. package/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +6 -1
  22. package/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +74 -56
  23. package/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +4 -2
  24. package/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +5 -1
  25. package/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +10 -1
  26. package/ReactCommon/react/featureflags/ReactNativeFeatureFlagsOverridesOSSCanary.h +5 -1
  27. package/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +2 -1
  28. package/ReactCommon/react/nativemodule/core/iostests/RCTTurboModuleTests.mm +18 -65
  29. package/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.h +1 -2
  30. package/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.mm +1 -2
  31. package/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h +2 -9
  32. package/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm +33 -71
  33. package/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +6 -1
  34. package/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +3 -1
  35. package/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm +8 -19
  36. package/package.json +16 -13
  37. package/scripts/codegen/generate-artifacts-executor/generateRCTThirdPartyComponents.js +18 -4
  38. package/scripts/setup-apple-spm.js +82 -4
  39. package/scripts/spm/autolinking-plugins.js +8 -5
  40. package/scripts/spm/expand-spm-dependencies.js +217 -156
  41. package/scripts/spm/generate-spm-autolinking.js +114 -88
  42. package/scripts/spm/generate-spm-xcodeproj.js +67 -22
  43. package/scripts/spm/ios-deployment-target.js +378 -0
  44. package/scripts/spm/read-podspec.js +156 -8
  45. package/scripts/spm/scaffold-package-swift.js +272 -105
  46. package/scripts/spm/spm-pbxproj.js +36 -21
  47. package/scripts/spm/spm-types.js +25 -8
  48. package/scripts/spm/spm-utils.js +26 -0
  49. package/scripts/spm/swiftpm-config.js +245 -0
  50. package/scripts/spm/sync-spm-autolinking.js +11 -2
  51. package/sdks/hermes-engine/version.properties +1 -1
  52. package/src/private/featureflags/ReactNativeFeatureFlags.js +10 -5
  53. package/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +2 -1
  54. package/src/private/webapis/dom/nodes/ReactNativeElement.js +6 -4
  55. package/src/private/webapis/dom/nodes/ReadOnlyText.js +6 -4
  56. package/src/react-private-interface.js +4 -0
  57. package/src/react-private-interface.js.flow +2 -0
  58. package/types_generated/Libraries/ReactNative/FabricUIManager.d.ts +47 -0
  59. package/types_generated/src/react-private-interface.d.ts +4 -1
  60. package/React/Base/RCTArrayBuffer.h +0 -64
  61. package/React/Base/RCTArrayBuffer.mm +0 -127
@@ -0,0 +1,378 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow strict-local
8
+ * @format
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ /**
14
+ * The iOS platform floor of the SwiftPM manifests React Native generates.
15
+ * SwiftPM refuses to link a product whose minimum is above the depending
16
+ * target's, so the floor tracks the app's own deployment target rather than a
17
+ * hardcoded value a dependency (every Expo package: iOS 16.4) can exceed.
18
+ */
19
+
20
+ const {targetBuildConfigUuids} = require('./generate-spm-xcodeproj');
21
+ const {
22
+ findApplicationTargets,
23
+ findField,
24
+ findObjectByUuid,
25
+ findProjectObject,
26
+ forEachObjectInSection,
27
+ uuidsInArray,
28
+ } = require('./spm-pbxproj');
29
+ const fs = require('node:fs');
30
+ const path = require('node:path');
31
+
32
+ // Keep in sync with `min_ios_version_supported` in scripts/cocoapods/helpers.rb.
33
+ const MIN_IOS_VERSION_SUPPORTED /*: string */ = '15.1';
34
+
35
+ const IOS_VERSION_RE = /^\d+(\.\d+){0,2}$/;
36
+
37
+ const DEPLOYMENT_TARGET_KEY = 'IPHONEOS_DEPLOYMENT_TARGET';
38
+
39
+ // An xcconfig may `#include` another; cap the chain instead of trusting it.
40
+ const MAX_XCCONFIG_DEPTH = 16;
41
+
42
+ /*::
43
+ // Everything the reader needs to follow a configuration's xcconfig. Without a
44
+ // srcRoot the xcconfig step is skipped (a caller with only pbxproj text).
45
+ type XcconfigContext = {srcRoot: ?string, readFile: (absPath: string) => ?string};
46
+ */
47
+
48
+ function normalizeIosVersion(version /*: string */) /*: string */ {
49
+ return version.includes('.') ? version : `${version}.0`;
50
+ }
51
+
52
+ function segment(
53
+ parts /*: Array<string> */,
54
+ index /*: number */,
55
+ ) /*: number */ {
56
+ return index < parts.length ? Number(parts[index]) : 0;
57
+ }
58
+
59
+ /** Numeric, not lexicographic: `9.0` < `10.0` and `16.4` < `16.10`. */
60
+ function compareIosVersions(a /*: string */, b /*: string */) /*: number */ {
61
+ const left = a.split('.');
62
+ const right = b.split('.');
63
+ for (let i = 0; i < Math.max(left.length, right.length); i++) {
64
+ const delta = segment(left, i) - segment(right, i);
65
+ if (delta !== 0) {
66
+ return delta < 0 ? -1 : 1;
67
+ }
68
+ }
69
+ return 0;
70
+ }
71
+
72
+ function unquote(value /*: string */) /*: string */ {
73
+ return value.replace(/^"|"$/g, '');
74
+ }
75
+
76
+ /**
77
+ * A version reduced to something safe to interpolate into a manifest:
78
+ * normalized, and never below React Native's own minimum.
79
+ */
80
+ function sanitizeIosDeploymentTarget(raw /*: ?string */) /*: string */ {
81
+ if (raw == null || !IOS_VERSION_RE.test(raw)) {
82
+ return MIN_IOS_VERSION_SUPPORTED;
83
+ }
84
+ const version = normalizeIosVersion(raw);
85
+ return compareIosVersions(version, MIN_IOS_VERSION_SUPPORTED) > 0
86
+ ? version
87
+ : MIN_IOS_VERSION_SUPPORTED;
88
+ }
89
+
90
+ function defaultReadFile(absPath /*: string */) /*: ?string */ {
91
+ try {
92
+ return fs.readFileSync(absPath, 'utf8');
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * `IPHONEOS_DEPLOYMENT_TARGET` as an xcconfig defines it, following `#include`
100
+ * lines in place so a later assignment in the includer wins. Null when the file
101
+ * is unreadable, sets no unconditional value, or sets one that is not a plain
102
+ * version (`$(inherited)`).
103
+ */
104
+ function parseXcconfigSetting(
105
+ absPath /*: string */,
106
+ content /*: string */,
107
+ readFile /*: (absPath: string) => ?string */,
108
+ visited /*: Set<string> */ = new Set(),
109
+ depth /*: number */ = 0,
110
+ ) /*: ?string */ {
111
+ if (visited.has(absPath)) {
112
+ return null;
113
+ }
114
+ visited.add(absPath);
115
+ const assignment = new RegExp(`^${DEPLOYMENT_TARGET_KEY}\\s*=\\s*([^;]*);?$`);
116
+ let value /*: ?string */ = null;
117
+ for (const rawLine of content.split('\n')) {
118
+ const line = rawLine.replace(/\/\/.*$/, '').trim();
119
+ const include = line.match(/^#include\??\s+"([^"]+)"/);
120
+ if (include != null) {
121
+ const included = readXcconfigSetting(
122
+ path.resolve(path.dirname(absPath), include[1]),
123
+ readFile,
124
+ visited,
125
+ depth + 1,
126
+ );
127
+ if (included != null) {
128
+ value = included;
129
+ }
130
+ continue;
131
+ }
132
+ const match = line.match(assignment);
133
+ if (match != null) {
134
+ const raw = unquote(match[1].trim());
135
+ value = IOS_VERSION_RE.test(raw) ? raw : null;
136
+ }
137
+ }
138
+ return value;
139
+ }
140
+
141
+ /** parseXcconfigSetting for a path not read yet — the `#include` entry point. */
142
+ function readXcconfigSetting(
143
+ absPath /*: string */,
144
+ readFile /*: (absPath: string) => ?string */,
145
+ visited /*: Set<string> */,
146
+ depth /*: number */,
147
+ ) /*: ?string */ {
148
+ if (depth > MAX_XCCONFIG_DEPTH) {
149
+ return null;
150
+ }
151
+ const content = readFile(absPath);
152
+ return content != null
153
+ ? parseXcconfigSetting(absPath, content, readFile, visited, depth)
154
+ : null;
155
+ }
156
+
157
+ /**
158
+ * Directory components of the PBXGroup chain holding `uuid`, outermost first —
159
+ * how a `"<group>"` file reference's path is anchored to the project dir.
160
+ */
161
+ function groupPathPrefix(
162
+ text /*: string */,
163
+ uuid /*: string */,
164
+ ) /*: Array<string> */ {
165
+ const groups /*: Array<{uuid: string, path: ?string, children: Set<string>}> */ =
166
+ [];
167
+ forEachObjectInSection(text, 'PBXGroup', ({uuid: groupUuid, ...body}) => {
168
+ const children = findField(text, body, 'children');
169
+ const groupPath = findField(text, body, 'path');
170
+ groups.push({
171
+ uuid: groupUuid,
172
+ path: groupPath != null ? unquote(groupPath.value) : null,
173
+ children: children != null ? uuidsInArray(children.value) : new Set(),
174
+ });
175
+ });
176
+
177
+ const parts /*: Array<string> */ = [];
178
+ let current = uuid;
179
+ for (let i = 0; i < groups.length; i++) {
180
+ const parent = groups.find(group => group.children.has(current));
181
+ if (parent == null) {
182
+ break;
183
+ }
184
+ if (parent.path != null) {
185
+ parts.unshift(parent.path);
186
+ }
187
+ current = parent.uuid;
188
+ }
189
+ return parts;
190
+ }
191
+
192
+ /**
193
+ * Absolute paths to try for the xcconfig a configuration is based on, in order:
194
+ * the reference's own anchoring first, then plain `<srcRoot>/<path>`.
195
+ */
196
+ function xcconfigCandidates(
197
+ text /*: string */,
198
+ configObj /*: {bodyOpen: number, bodyClose: number, ...} */,
199
+ srcRoot /*: string */,
200
+ ) /*: Array<string> */ {
201
+ const base = findField(text, configObj, 'baseConfigurationReference');
202
+ const refUuid = base?.value.match(/[0-9A-Fa-f]{24}/)?.[0];
203
+ if (refUuid == null) {
204
+ return [];
205
+ }
206
+ const ref = findObjectByUuid(text, refUuid);
207
+ if (ref == null) {
208
+ return [];
209
+ }
210
+ const pathField = findField(text, ref, 'path');
211
+ if (pathField == null) {
212
+ return [];
213
+ }
214
+ const refPath = unquote(pathField.value);
215
+ if (path.isAbsolute(refPath)) {
216
+ return [refPath];
217
+ }
218
+ const sourceTreeField = findField(text, ref, 'sourceTree');
219
+ const sourceTree =
220
+ sourceTreeField != null ? unquote(sourceTreeField.value) : '';
221
+ const fallback = path.join(srcRoot, refPath);
222
+ if (sourceTree !== '<group>') {
223
+ return [fallback];
224
+ }
225
+ const anchored = path.join(
226
+ srcRoot,
227
+ ...groupPathPrefix(text, refUuid),
228
+ refPath,
229
+ );
230
+ return anchored === fallback ? [fallback] : [anchored, fallback];
231
+ }
232
+
233
+ function configName(
234
+ text /*: string */,
235
+ configObj /*: {bodyOpen: number, bodyClose: number, ...} */,
236
+ ) /*: ?string */ {
237
+ const field = findField(text, configObj, 'name');
238
+ return field != null ? unquote(field.value) : null;
239
+ }
240
+
241
+ /** A plain-version `IPHONEOS_DEPLOYMENT_TARGET` literal in a configuration. */
242
+ function configLiteral(
243
+ text /*: string */,
244
+ configObj /*: {bodyOpen: number, bodyClose: number, ...} */,
245
+ ) /*: ?string */ {
246
+ const settings = findField(text, configObj, 'buildSettings');
247
+ if (settings == null) {
248
+ return null;
249
+ }
250
+ const field = findField(
251
+ text,
252
+ {bodyOpen: settings.valueStart, bodyClose: settings.tokenEnd - 1},
253
+ DEPLOYMENT_TARGET_KEY,
254
+ );
255
+ if (field == null) {
256
+ return null;
257
+ }
258
+ const raw = unquote(field.value);
259
+ return IOS_VERSION_RE.test(raw) ? raw : null;
260
+ }
261
+
262
+ /**
263
+ * What one configuration declares: its own literal, else the xcconfig it is
264
+ * based on (Xcode's own precedence).
265
+ */
266
+ function configDeploymentTarget(
267
+ text /*: string */,
268
+ configObj /*: {bodyOpen: number, bodyClose: number, ...} */,
269
+ ctx /*: XcconfigContext */,
270
+ ) /*: ?string */ {
271
+ const literal = configLiteral(text, configObj);
272
+ if (literal != null || ctx.srcRoot == null) {
273
+ return literal;
274
+ }
275
+ for (const candidate of xcconfigCandidates(text, configObj, ctx.srcRoot)) {
276
+ const content = ctx.readFile(candidate);
277
+ if (content != null) {
278
+ return parseXcconfigSetting(candidate, content, ctx.readFile);
279
+ }
280
+ }
281
+ return null;
282
+ }
283
+
284
+ /**
285
+ * The app's declared iOS deployment target, or null when nothing declares one.
286
+ * Target selection: the marker's `targetUuid`, else the app target named
287
+ * `targetName` (`--product-name`), else every app target. Each configuration
288
+ * falls back to the project-level one of the same name (each level accepting a
289
+ * literal or the xcconfig it is based on), and the lowest wins — a single
290
+ * manifest floor has to hold for every configuration.
291
+ */
292
+ function readIosDeploymentTargetFromPbxproj(
293
+ text /*: string */,
294
+ opts /*:: ?: {
295
+ targetUuid?: ?string,
296
+ targetName?: ?string,
297
+ srcRoot?: ?string,
298
+ readFile?: (absPath: string) => ?string,
299
+ } */,
300
+ ) /*: ?string */ {
301
+ const ctx /*: XcconfigContext */ = {
302
+ srcRoot: opts?.srcRoot,
303
+ readFile: opts?.readFile ?? defaultReadFile,
304
+ };
305
+ const targetUuid = opts?.targetUuid;
306
+ const marked = targetUuid != null ? findObjectByUuid(text, targetUuid) : null;
307
+ const apps = findApplicationTargets(text);
308
+ const named =
309
+ opts?.targetName != null
310
+ ? apps.find(app => app.name === opts.targetName)
311
+ : null;
312
+ const targets = marked != null ? [marked] : named != null ? [named] : apps;
313
+
314
+ const projectDefaults /*: Map<string, string> */ = new Map();
315
+ const project = findProjectObject(text);
316
+ if (project != null) {
317
+ for (const uuid of targetBuildConfigUuids(text, project)) {
318
+ const config = findObjectByUuid(text, uuid);
319
+ const name = config != null ? configName(text, config) : null;
320
+ const value =
321
+ config != null ? configDeploymentTarget(text, config, ctx) : null;
322
+ if (name != null && value != null) {
323
+ projectDefaults.set(name, value);
324
+ }
325
+ }
326
+ }
327
+
328
+ let floor /*: ?string */ = null;
329
+ for (const target of targets) {
330
+ for (const uuid of targetBuildConfigUuids(text, target)) {
331
+ const config = findObjectByUuid(text, uuid);
332
+ if (config == null) {
333
+ continue;
334
+ }
335
+ const name = configName(text, config);
336
+ const value =
337
+ configDeploymentTarget(text, config, ctx) ??
338
+ (name != null ? projectDefaults.get(name) : null) ??
339
+ null;
340
+ if (
341
+ value != null &&
342
+ (floor == null || compareIosVersions(value, floor) < 0)
343
+ ) {
344
+ floor = value;
345
+ }
346
+ }
347
+ }
348
+ return floor;
349
+ }
350
+
351
+ /**
352
+ * The sanitized floor read from `<xcodeprojPath>/project.pbxproj`, or null when
353
+ * there is no project, it cannot be read, or it declares nothing usable.
354
+ */
355
+ function resolveIosDeploymentTarget(
356
+ opts /*: {xcodeprojPath: ?string, targetUuid?: ?string, targetName?: ?string} */,
357
+ ) /*: ?string */ {
358
+ const {xcodeprojPath, targetUuid, targetName} = opts;
359
+ if (xcodeprojPath == null) {
360
+ return null;
361
+ }
362
+ try {
363
+ const found = readIosDeploymentTargetFromPbxproj(
364
+ fs.readFileSync(path.join(xcodeprojPath, 'project.pbxproj'), 'utf8'),
365
+ {targetUuid, targetName, srcRoot: path.dirname(xcodeprojPath)},
366
+ );
367
+ return found != null ? sanitizeIosDeploymentTarget(found) : null;
368
+ } catch {
369
+ return null;
370
+ }
371
+ }
372
+
373
+ module.exports = {
374
+ MIN_IOS_VERSION_SUPPORTED,
375
+ readIosDeploymentTargetFromPbxproj,
376
+ resolveIosDeploymentTarget,
377
+ sanitizeIosDeploymentTarget,
378
+ };
@@ -170,6 +170,27 @@ function cleanupPatchedPodspec(patchedPath /*: ?string */) /*: void */ {
170
170
  // Regex fallback
171
171
  // ---------------------------------------------------------------------------
172
172
 
173
+ // Comment-only lines are dropped so a commented-out `# s.header_dir` cannot win
174
+ // over the live one. A trailing `#` and a `#` inside a string survive: only the
175
+ // first non-whitespace character counts.
176
+ function readPodspecSource(podspecPath /*: string */) /*: string */ {
177
+ return fs
178
+ .readFileSync(podspecPath, 'utf8')
179
+ .split('\n')
180
+ .filter(line => !/^\s*#/.test(line))
181
+ .join('\n');
182
+ }
183
+
184
+ // A subspec that re-binds a receiver the identity regexes trust: inside
185
+ // `s.subspec "common" do |s|`, `s` is the CHILD, and no regex over flat text
186
+ // can see that. Any other block variable (`do |ss|`) is unambiguous.
187
+ const SHADOWING_SUBSPEC_RE =
188
+ /\.subspec\s+["'][^"']*["']\s+do\s*\|\s*(?:s|spec)\s*\|/;
189
+
190
+ function hasShadowingSubspec(source /*: string */) /*: boolean */ {
191
+ return SHADOWING_SUBSPEC_RE.test(source);
192
+ }
193
+
173
194
  /**
174
195
  * Best-effort Ruby podspec parser. Extracts the literal-string and
175
196
  * literal-array fields most RN libs use. Skips subspec blocks, Ruby helper
@@ -178,16 +199,39 @@ function cleanupPatchedPodspec(patchedPath /*: ?string */) /*: void */ {
178
199
  * user. Always returns a RawSpec; pure-JS, no Ruby dep required.
179
200
  */
180
201
  function regexPodspec(podspecPath /*: string */) /*: RawSpec */ {
181
- const content = fs.readFileSync(podspecPath, 'utf8');
202
+ const content = readPodspecSource(podspecPath);
182
203
  const warnings /*: Array<string> */ = [];
183
204
 
184
- // Matches: s.<field> = "value" or s.<field> = 'value'
205
+ // Matches: s.<field> = "value" or s.<field> = 'value' — at ANY scope,
206
+ // so a subspec's value counts. Right for the fields flattenSubspecs merges
207
+ // (source globs, header mappings); wrong for the fields that identify the
208
+ // spec, which use getSpecStringField.
185
209
  function getStringField(name /*: string */) /*: string | null */ {
186
210
  const re = new RegExp(`(?:s|spec)\\.${name}\\s*=\\s*["']([^"']+)["']`);
187
211
  const m = content.match(re);
188
212
  return m ? m[1] : null;
189
213
  }
190
214
 
215
+ // The same, but only where the receiver starts the statement — so a subspec's
216
+ // block variable (`ss.header_dir`, the shape react-native-screens and
217
+ // react-native-svg ship) cannot answer for the spec itself.
218
+ function getSpecStringField(name /*: string */) /*: string | null */ {
219
+ const re = new RegExp(
220
+ `(?:^|[;|])\\s*(?:s|spec)\\.${name}\\s*=\\s*["']([^"']+)["']`,
221
+ 'm',
222
+ );
223
+ const m = content.match(re);
224
+ return m ? m[1] : null;
225
+ }
226
+
227
+ // The fields that identify the spec, and only when the receiver spelling
228
+ // still says which scope declared them: a subspec re-binding `s` puts its own
229
+ // `header_dir` exactly where the library's would be.
230
+ const shadowedIdentity = hasShadowingSubspec(content);
231
+ function getIdentityField(name /*: string */) /*: string | null */ {
232
+ return shadowedIdentity ? null : getSpecStringField(name);
233
+ }
234
+
191
235
  // Matches:
192
236
  // s.<field> = ["a", "b"] (array)
193
237
  // s.<field> = "single" (single value treated as 1-element array)
@@ -289,6 +333,11 @@ function regexPodspec(podspecPath /*: string */) /*: RawSpec */ {
289
333
  }
290
334
 
291
335
  // Surface known unparseable constructs so the caller can warn the user.
336
+ if (shadowedIdentity) {
337
+ warnings.push(
338
+ "A subspec rebinds the spec variable (`do |s|`), so its `name`, `module_name` and `header_dir` cannot be told from the library's own — those are left unread. Install CocoaPods (`gem install cocoapods`) to enable full `pod ipc spec` parsing.",
339
+ );
340
+ }
292
341
  if (/(?:s|spec)\.subspec\s+["']/.test(content)) {
293
342
  warnings.push(
294
343
  'Subspecs detected — regex parser only extracts top-level fields. Install CocoaPods (`gem install cocoapods`) to enable full `pod ipc spec` parsing.',
@@ -306,14 +355,15 @@ function regexPodspec(podspecPath /*: string */) /*: RawSpec */ {
306
355
  }
307
356
 
308
357
  return {
309
- name: getStringField('name'),
310
- version: getStringField('version'),
358
+ name: getIdentityField('name'),
359
+ module_name: getIdentityField('module_name'),
360
+ version: getSpecStringField('version'),
311
361
  source_files: getArrayField('source_files'),
312
362
  public_header_files: getArrayField('public_header_files'),
313
363
  private_header_files: getArrayField('private_header_files'),
314
364
  exclude_files: getArrayField('exclude_files'),
315
365
  header_mappings_dir: getStringField('header_mappings_dir'),
316
- header_dir: getStringField('header_dir'),
366
+ header_dir: getIdentityField('header_dir'),
317
367
  frameworks: getFrameworks(false),
318
368
  weak_frameworks: getFrameworks(true),
319
369
  libraries: getArrayField('libraries'),
@@ -432,6 +482,14 @@ function flattenSubspecs(rawSpec /*: RawSpec */) /*: PodspecModel */ {
432
482
  return Array.from(new Set(out));
433
483
  }
434
484
 
485
+ // Only the spec's own value: a subspec's `header_dir` names that subspec's
486
+ // headers, and several subspecs can disagree.
487
+ function specStringField(key /*: string */) /*: string | null */ {
488
+ // $FlowFixMe[incompatible-use] dynamic shape
489
+ const value = rawSpec[key];
490
+ return typeof value === 'string' && value.length > 0 ? value : null;
491
+ }
492
+
435
493
  function mergeStringField(key /*: string */) /*: string | null */ {
436
494
  for (const layer of layers) {
437
495
  // $FlowFixMe[incompatible-use] layer narrowed from `mixed`; runtime-validated below
@@ -620,8 +678,8 @@ function flattenSubspecs(rawSpec /*: RawSpec */) /*: PodspecModel */ {
620
678
  : true; // RN ecosystem default
621
679
 
622
680
  return {
623
- name: mergeStringField('name') ?? '',
624
- version: mergeStringField('version') ?? '',
681
+ name: specStringField('name') ?? '',
682
+ version: specStringField('version') ?? '',
625
683
  sourceFiles: mergeArrayField('source_files'),
626
684
  publicHeaderFiles: mergeArrayField('public_header_files'),
627
685
  privateHeaderFiles: mergeArrayField('private_header_files'),
@@ -634,7 +692,8 @@ function flattenSubspecs(rawSpec /*: RawSpec */) /*: PodspecModel */ {
634
692
  // resolve from the physical tree — CocoaPods does this via the
635
693
  // header_mappings_dir copy step, which SPM has no equivalent for.
636
694
  headerMappingsDirs: mergeArrayField('header_mappings_dir'),
637
- headerDir: mergeStringField('header_dir'),
695
+ headerDir: specStringField('header_dir'),
696
+ moduleName: specStringField('module_name'),
638
697
  frameworks: mergeArrayField('frameworks'),
639
698
  weakFrameworks: mergeArrayField('weak_frameworks'),
640
699
  libraries: mergeArrayField('libraries'),
@@ -687,8 +746,97 @@ function readPodspec(podspecPath /*: string */) /*: PodspecModel */ {
687
746
  return model;
688
747
  }
689
748
 
749
+ const podspecModels /*: Map<string, PodspecModel> */ = new Map();
750
+
751
+ /**
752
+ * readPodspec memoized on the resolved path. `pod ipc spec` costs a process
753
+ * spawn, and one autolinking run reads the same podspec from several sites
754
+ * (name resolution, header search paths, scaffolding). The model is shared, so
755
+ * callers must treat it as read-only.
756
+ */
757
+ function readPodspecCached(podspecPath /*: string */) /*: PodspecModel */ {
758
+ const key = path.resolve(podspecPath);
759
+ const cached = podspecModels.get(key);
760
+ if (cached != null) {
761
+ return cached;
762
+ }
763
+ const model = readPodspec(podspecPath);
764
+ podspecModels.set(key, model);
765
+ return model;
766
+ }
767
+
768
+ /**
769
+ * The three fields that name a library, read WITHOUT `pod ipc spec`'s process
770
+ * spawn — name resolution runs for every dep, including self-managed ones that
771
+ * need nothing else from the podspec.
772
+ *
773
+ * Null when the answer cannot be trusted without Ruby: an interpolated value,
774
+ * or a spec-level `header_dir`/`module_name` the regex could not read. Both
775
+ * outrank the pod name, so a literal name alone is not enough — resolving
776
+ * without them would pick the wrong prefix. A value only a subspec declares is
777
+ * not the library's, so it neither answers nor defers — unless that subspec
778
+ * re-binds `s` (or `spec`), which makes every field's scope unreadable.
779
+ */
780
+ function readPodspecNames(
781
+ podspecPath /*: string */,
782
+ ) /*: ?{name: ?string, moduleName: ?string, headerDir: ?string} */ {
783
+ const raw = regexPodspec(podspecPath);
784
+ const field = (key /*: string */) /*: ?string */ => {
785
+ // $FlowFixMe[prop-missing] RawSpec is dynamically shaped
786
+ const value = raw[key];
787
+ return typeof value === 'string' &&
788
+ value.length > 0 &&
789
+ !value.includes('#{')
790
+ ? value
791
+ : null;
792
+ };
793
+ const source = readPodspecSource(podspecPath);
794
+ if (hasShadowingSubspec(source)) {
795
+ return null;
796
+ }
797
+ const declaredUnread = (
798
+ key /*: string */,
799
+ value /*: ?string */,
800
+ ) /*: boolean */ =>
801
+ value == null &&
802
+ new RegExp(`(?:^|[;|])\\s*(?:s|spec)\\.${key}\\s*=`, 'm').test(source);
803
+
804
+ const name = field('name');
805
+ const moduleName = field('module_name');
806
+ const headerDir = field('header_dir');
807
+ if (
808
+ declaredUnread('header_dir', headerDir) ||
809
+ declaredUnread('module_name', moduleName)
810
+ ) {
811
+ return null;
812
+ }
813
+ return name == null && moduleName == null && headerDir == null
814
+ ? null
815
+ : {name, moduleName, headerDir};
816
+ }
817
+
818
+ /**
819
+ * Every podspec in `dir`, sorted so two of them name a target the same way on
820
+ * every machine (readdir order is unspecified). Skips a crashed run's leftover
821
+ * `.spm-scaffold-<pid>-<name>.podspec` copy.
822
+ */
823
+ function findPodspecs(dir /*: string */) /*: Array<string> */ {
824
+ try {
825
+ return fs
826
+ .readdirSync(dir)
827
+ .filter(e => e.endsWith('.podspec') && !e.startsWith('.spm-scaffold-'))
828
+ .sort()
829
+ .map(e => path.join(dir, e));
830
+ } catch {
831
+ return [];
832
+ }
833
+ }
834
+
690
835
  module.exports = {
836
+ findPodspecs,
691
837
  readPodspec,
838
+ readPodspecCached,
839
+ readPodspecNames,
692
840
  runPodIpcSpec,
693
841
  regexPodspec,
694
842
  flattenSubspecs,