Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/react-native/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ let reactFeatureFlags = RNTarget(
let reactPerfLogger = RNTarget(
name: .reactPerfLogger,
path: "ReactCommon/reactperflogger",
excludedPaths: ["fusebox"]
excludedPaths: ["fusebox"],
dependencies: [.reactDebug, .reactNativeDependencies]
)

/// React-logger.podspec
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ target_link_libraries(jserrorhandler
folly_runtime
${mapbufferjni}
react_cxxstableapi
react_debug
react_featureflags
)
target_compile_reactnative_options(jserrorhandler PRIVATE)
3 changes: 2 additions & 1 deletion packages/react-native/ReactCommon/jsitooling/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ target_link_libraries(jsitooling
folly_runtime
glog
jsi
react_cxxstableapi)
react_cxxstableapi
react_timing)

target_compile_reactnative_options(jsitooling PRIVATE)
target_compile_options(jsitooling PRIVATE -Wpedantic)
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

#include <react/cxxstableapi/UmbrellaGuard.h>

#include <react/debug/flags.h>
#include <React/Debug.h>
#include <chrono>
#include <cmath>
#include <functional>
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/scripts/cocoapods/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ def self.update_search_paths(installer)
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-featureflags", "React_featureflags", []))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-renderercss", "React_renderercss", []))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-cxxstableapi", "React_cxxstableapi", []))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-debug", "React_debug", []))
.each{ |search_path|
header_search_paths = self.add_search_path_if_not_included(header_search_paths, search_path)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ scripts under `scripts/ios-prebuild/`:
| Script | Role |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `headers-inventory.js` | **Discover + classify** every shipped header (the facts) |
| `headers-spec.js` | **The rules** (R1–R11) — turns the inventory into a layout plan + module maps |
| `headers-spec.js` | **The rules** (R1–R12) — turns the inventory into a layout plan + module maps |
| `headers-compose.js` | **Emit** — projects the plan into `React.xcframework` and `ReactNativeHeaders.xcframework` |
| `headers-verify.js` | **Gate** — generator-time verification: include-health ratchet, structural byte-compare, consumer-shaped compile smokes (runs in the prebuild CI compose job) |

Expand All @@ -35,7 +35,7 @@ everything else. No overlay, no include rewriting, no consumer flags.
podspecs ──► headers-inventory.js ──► inventory (facts per header)
headers-spec.js (rules R1–R10)
headers-spec.js (rules R1–R12)
│ plan: what goes where + module maps
headers-compose.js (emission)
Expand Down Expand Up @@ -233,6 +233,28 @@ emitted as a one-line redirect shim (`#import <owner>`). Shims that are
namespace-module members are fine: they import the owning module, so
declarations stay single-owned.

**R12 — a namespace module's own umbrella stays in ReactNativeHeaders.** The C++
stable API ships one umbrella per module, physically nested inside it
(`ReactCommon/react/debug/React/Debug.h`), so its natural path is
`React/Debug.h` and R1 would hoist it into the framework. It must not be:
**ReactNativeHeaders is the lower layer** — `React.framework` imports it
(`RCTCallInvoker.h` → `<ReactCommon/CallInvoker.h>`). A framework-owned umbrella
makes every `#include <React/X.h>` inside a lowercase-namespace header an import
of module `React`, closing a cycle:

```text
React -> ReactNativeHeaders_react -> React
```

Found empirically: `react/timing/primitives.h` -> `<React/Debug.h>`. This is
the same two-module-ownership failure as `UMBRELLA_CXX_GUARDED_EXCLUSIONS`
(`RCTFrameTimingsObserver.h`, which reaches the same `primitives.h`), in the
opposite direction. They are `objc-blocked` by construction (they re-export
their module's C++ surface), so they were never R4 umbrella or R5 module
members; `planFromInventory` fails closed if one ever becomes a modular
candidate, since its R5 module would be named `React` and alias the framework
module.

## Stage 3 — Emission (headers-compose.js)

`computeSpecPlan(rnRoot)` = inventory → plan, throwing on R8 collisions. Then:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,55 @@ describe('R11 redirect shims for dual-identity headers', () => {
});
});

describe('R12 namespace module umbrellas stay in ReactNativeHeaders', () => {
test('a module-nested React/ umbrella is not hoisted into the framework', () => {
const m = validManifest();
m.headers.push(
entry(
'React/Debug.h',
'objc-blocked',
'ReactCommon/react/debug/React/Debug.h',
),
);
const plan = planFromInventoryForTest(m);
// Lower layer: including it from a react/-namespace header cannot create a
// React module edge (React -> ReactNativeHeaders_react -> React).
expect(
plan.reactNativeHeaders.find(e => e.naturalPath === 'React/Debug.h')
?.relPath,
).toBe('React/Debug.h');
expect(
plan.react.find(e => e.naturalPath === 'React/Debug.h'),
).toBeUndefined();
expect(plan.umbrella).not.toContain('React/Debug.h');
expect(Object.keys(plan.namespaceModules)).not.toContain('React');
});

test('genuine React.framework headers are still hoisted (R1)', () => {
const m = validManifest();
m.headers.push(
entry('React/RCTMessageThread.h', 'cxx', 'React/Base/RCTMessageThread.h'),
);
const plan = planFromInventoryForTest(m);
expect(
plan.react.find(e => e.naturalPath === 'React/RCTMessageThread.h')
?.relPath,
).toBe('RCTMessageThread.h');
});

test('fails closed if an R12 umbrella becomes a modular candidate', () => {
const m = validManifest();
m.headers.push(
entry(
'React/Debug.h',
'objc-modular-candidate',
'ReactCommon/react/debug/React/Debug.h',
),
);
expect(() => planFromInventoryForTest(m)).toThrow(/R12/);
});
});

describe('DEPS_NAMESPACES (R2 — the deps sidecar namespace set)', () => {
test('includes SocketRocket: one physical home, in the sidecar', () => {
// Pre-sidecar, SocketRocket was excluded from relocation because a REAL
Expand Down
51 changes: 48 additions & 3 deletions packages/react-native/scripts/ios-prebuild/headers-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,20 @@
* one-line redirect shim (`#import <owner>`). Shims that are namespace-
* module members are fine: they import the owning module, so declarations
* stay single-owned.
* R12. A namespace module's OWN umbrella (ReactCommon/<ns...>/React/<Name>.h,
* natural path React/<Name>.h) ships in ReactNativeHeaders — it is NOT
* hoisted into React.framework by R1. ReactNativeHeaders is the LOWER
* layer (React.framework imports it: RCTCallInvoker.h pulls
* <ReactCommon/CallInvoker.h>), so a framework-owned umbrella turns every
* `#include <React/X.h>` inside a lowercase-namespace header into an import
* of module React and closes a cycle:
* React -> ReactNativeHeaders_react -> React
* Found empirically: react/timing/primitives.h -> <React/Debug.h>. Kept in
* ReactNativeHeaders the same spelling still resolves — framework lookup
* misses and clang falls back to the header search path — textually and
* within one artifact, so no module edge is created. These umbrellas are
* objc-blocked by construction (they re-export their module's C++
* surface), so they were never R4 umbrella or R5 module members anyway.
*/

const fs = require('node:fs');
Expand Down Expand Up @@ -280,6 +294,13 @@ function renderNamespaceUmbrella(
return `#ifdef __OBJC__\n#import <UIKit/UIKit.h>\n#endif\n\n${imports}\n`;
}

// R12: a namespace module's own umbrella — physically nested inside the module
// it re-exports (ReactCommon/react/debug/React/Debug.h), which is what tells it
// apart from the ~310 genuine React.framework headers that also carry a
// `React/` natural path but live under React/, Libraries/, ReactApple/, ...
const NS_MODULE_UMBRELLA_RE /*: RegExp */ =
/^ReactCommon\/.+\/React\/[^/]+\.h$/;

/**
* Computes the full layout plan from the header inventory manifest
* (build/header-inventory.json — regenerate with header-inventory.js).
Expand Down Expand Up @@ -307,7 +328,13 @@ function planFromInventory(
let bucketKey;
let entryList;
let relPath;
if (np.startsWith('React/')) {
if (np.startsWith('React/') && NS_MODULE_UMBRELLA_RE.test(source)) {
// R12: a namespace module's own umbrella stays in the LOWER layer, so
// including it from that namespace cannot create a React module edge.
relPath = np;
bucketKey = `ReactNativeHeaders/${relPath}`;
entryList = reactNativeHeaders;
} else if (np.startsWith('React/')) {
relPath = np.slice(6); // R1: hoist React/ to the framework Headers root
bucketKey = `React.framework/${relPath}`;
entryList = react;
Expand All @@ -332,8 +359,13 @@ function planFromInventory(
seen.set(bucketKey, source);
entryList.push({relPath, source, naturalPath: np});

// R4: React umbrella membership.
if (np.startsWith('React/') && isUmbrellaSafe(h, root)) {
// R4: React umbrella membership. Only headers the framework actually ships
// (R12 umbrellas carry a React/ natural path but live in ReactNativeHeaders).
if (
entryList === react &&
np.startsWith('React/') &&
isUmbrellaSafe(h, root)
) {
umbrella.push(np);
}
// R5: namespace modules (only for ReactNativeHeaders namespaces). Every
Expand All @@ -346,6 +378,19 @@ function planFromInventory(
if (entryList === reactNativeHeaders) {
const ns = np.split('/')[0];
if (isUmbrellaSafe(h, root)) {
// R12 assert: an R12-routed umbrella lands under the `React` namespace
// here, and renderNamespaceModuleMap only renames the lowercase `react`
// one — so its module would be named `React` and alias the framework
// module, reintroducing the cycle R12 exists to break. These umbrellas
// are objc-blocked today so they never reach this branch; fail closed
// if that ever changes.
if (ns === 'React') {
throw new Error(
`R12: '${np}' is a modular candidate in ReactNativeHeaders. Its ` +
`namespace module would be named 'React' and alias the React ` +
`framework module. Keep it out of the modular surface.`,
);
}
// R5 exemption assert: a namespace whose name is not a valid module
// identifier cannot get a module, so a modular-candidate header in it
// would be silently non-modular — consumers importing it from a
Expand Down
Loading