Skip to content

Commit 10db72d

Browse files
chrfalchclaude
andcommitted
fix(ios-prebuild): hash every compose-tooling sibling into the recompose freshness marker
composeToolingHash() covered only three of headers-compose.js's five local sibling modules, so a local edit to headers-xcframework.js (which ensureHeadersLayout delegates the ReactNativeHeaders compose and the deps sidecar build to) or framework-resources.js (privacy manifest + i18n bundle emission) served stale composed output. Extract the list to COMPOSE_TOOLING_FILES, add the two missing files, and add a guard test that parses this file's own requires so the list can't silently drift again — the same bug class as the CI cache-key hashFiles gap, now unrepresentable. Also rewrite the stale headers-rules.md paragraph that still described the removed DEPS_NAMESPACES_NOT_RELOCATED carve-out; SocketRocket lives in DEPS_NAMESPACES with a single physical home in the deps sidecar. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 2986154 commit 10db72d

3 files changed

Lines changed: 68 additions & 8 deletions

File tree

packages/react-native/scripts/ios-prebuild/__docs__/headers-rules.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -356,9 +356,14 @@ These classes previously failed late (consumer CI lane) or not at all; the gate
356356
Proof the gate earns its keep — its FIRST run found two real shipping defects:
357357
the dual-identity redefinitions that became R11, and an undeclared
358358
`SocketRocket` namespace in the deps artifact (caught by the set-equality guard
359-
the moment it was added). SocketRocket is deliberately NOT relocated
360-
(`DEPS_NAMESPACES_NOT_RELOCATED`): the real pod vends it, and textual copies
361-
collide with it under `use_frameworks` — the gate asserts its absence.
359+
the moment it was added). SocketRocket now lives in `DEPS_NAMESPACES` alongside
360+
the other third-party deps namespaces, with a single physical home in the
361+
ReactNativeDependenciesHeaders sidecar: relocating a second textual copy into
362+
ReactNativeHeaders collided with the real pod's own headers under
363+
`use_frameworks` (the duplicate-`@interface` / poisoned-module-graph Expo
364+
regression, 2026-07-03), so the set-equality gate asserts the declared
365+
namespace set — `DEPS_NAMESPACES` — matches the deps artifact's namespaces
366+
exactly, in both directions.
362367

363368
### D. Remaining silent gaps — allowlist maintenance (by design)
364369

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
const {
14+
COMPOSE_TOOLING_FILES,
15+
composeToolingHash,
16+
} = require('../headers-compose');
17+
const fs = require('fs');
18+
const path = require('path');
19+
20+
describe('COMPOSE_TOOLING_FILES stays in sync with headers-compose.js requires', () => {
21+
test('every local sibling require is covered by the hashed file list', () => {
22+
const source = fs.readFileSync(
23+
path.join(__dirname, '..', 'headers-compose.js'),
24+
'utf8',
25+
);
26+
const requireRe = /require\('\.\/([\w-]+)'\)/g;
27+
const required = new Set();
28+
let match;
29+
while ((match = requireRe.exec(source)) != null) {
30+
required.add(`${match[1]}.js`);
31+
}
32+
for (const name of required) {
33+
expect(COMPOSE_TOOLING_FILES).toContain(name);
34+
}
35+
});
36+
});
37+
38+
describe('composeToolingHash', () => {
39+
test('returns a 64-char hex sha256 digest', () => {
40+
const hash = composeToolingHash();
41+
expect(hash).toMatch(/^[0-9a-f]{64}$/);
42+
});
43+
});

packages/react-native/scripts/ios-prebuild/headers-compose.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,23 @@ const CP_FLAGS = process.platform === 'darwin' ? '-Rc' : '-R';
5353
// forces a recompose even when the cached source xcframework is untouched —
5454
// the Info.plist mtime alone can't catch that. Stable across fresh checkouts
5555
// (content-based, not mtime-based). sha256 to avoid weak-hash lint.
56+
//
57+
// This list must contain every local sibling module headers-compose.js
58+
// requires (i.e. every `require('./*.js')` above) — the guard test in
59+
// __tests__/headers-compose-test.js enforces that by parsing this file's
60+
// source and diffing it against this list, so an added require without a
61+
// matching entry here fails the test instead of silently going stale.
62+
const COMPOSE_TOOLING_FILES /*: Array<string> */ = [
63+
'framework-resources.js',
64+
'headers-inventory.js',
65+
'headers-spec.js',
66+
'headers-xcframework.js',
67+
'headers-compose.js',
68+
];
69+
5670
function composeToolingHash() /*: string */ {
5771
const hash = crypto.createHash('sha256');
58-
for (const name of [
59-
'headers-inventory.js',
60-
'headers-spec.js',
61-
'headers-compose.js',
62-
]) {
72+
for (const name of COMPOSE_TOOLING_FILES) {
6373
hash.update(fs.readFileSync(path.join(__dirname, name)));
6474
}
6575
return hash.digest('hex');
@@ -376,4 +386,6 @@ module.exports = {
376386
buildReactNativeHeadersXcframework,
377387
ensureHeadersLayout,
378388
DEPS_NAMESPACES,
389+
COMPOSE_TOOLING_FILES,
390+
composeToolingHash,
379391
};

0 commit comments

Comments
 (0)