Skip to content

Commit a0c7f92

Browse files
chrfalchclaude
andcommitted
fix(spm): drop the plist npm dependency from flavored-frameworks
flavored-frameworks.js required the `plist` module, which is not a dependency of the react-native package — it only resolves inside this monorepo (and in repos like Expo's) via hoisting. In a fresh consumer app `spm add` failed with "Cannot find module 'plist'". Parse xcframework Info.plists via `plutil -convert json` instead (the same pattern the pre-B1 swap script used), with a portable plutil mock in the test suites for Linux CI and a regression test that loads the module with `plist` unresolvable. 450 spm tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d6c0a9e commit a0c7f92

3 files changed

Lines changed: 135 additions & 6 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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+
// Regression test for the fresh-consumer-app failure: `plist` is NOT a
14+
// dependency of the react-native package, so it only resolves inside this
15+
// monorepo via hoisting. A generated app running `spm add` gets
16+
// "Cannot find module 'plist'". flavored-frameworks.js must therefore never
17+
// require it — Info.plist parsing goes through plutil instead.
18+
jest.mock('plist', () => {
19+
throw new Error(
20+
"Cannot find module 'plist' — flavored-frameworks.js must not depend on it",
21+
);
22+
});
23+
24+
// plutil is macOS-only; stand in with a portable plist-parse so the suite is
25+
// hermetic on Linux CI. Module-level mock because flavored-frameworks.js
26+
// destructures execFileSync at require time (same pattern the old
27+
// swap-flavor-test used).
28+
jest.mock('child_process', () => {
29+
const actual = jest.requireActual('child_process');
30+
return {
31+
...actual,
32+
execFileSync: (cmd, args, opts) => {
33+
if (cmd === 'plutil') {
34+
const fs = require('fs');
35+
const plist = jest.requireActual('plist');
36+
const file = args[args.length - 1];
37+
return Buffer.from(
38+
JSON.stringify(plist.parse(fs.readFileSync(file, 'utf8'))),
39+
);
40+
}
41+
return actual.execFileSync(cmd, args, opts);
42+
},
43+
};
44+
});
45+
46+
const fs = require('fs');
47+
const os = require('os');
48+
const path = require('path');
49+
const realPlist = jest.requireActual('plist');
50+
51+
function makeXcframework(root /*: string */) /*: string */ {
52+
const xcframework = path.join(root, 'React.xcframework');
53+
const sliceId = 'ios-arm64';
54+
fs.mkdirSync(path.join(xcframework, sliceId, 'React.framework'), {
55+
recursive: true,
56+
});
57+
fs.writeFileSync(
58+
path.join(xcframework, sliceId, 'React.framework', 'React'),
59+
'not-a-real-mach-o',
60+
);
61+
fs.writeFileSync(
62+
path.join(xcframework, 'Info.plist'),
63+
realPlist.build({
64+
AvailableLibraries: [
65+
{
66+
LibraryIdentifier: sliceId,
67+
LibraryPath: 'React.framework',
68+
SupportedPlatform: 'ios',
69+
SupportedArchitectures: ['arm64'],
70+
},
71+
],
72+
CFBundlePackageType: 'XFWK',
73+
XCFrameworkFormatVersion: '1.0',
74+
}),
75+
);
76+
return xcframework;
77+
}
78+
79+
describe('flavored-frameworks without the plist module', () => {
80+
let tmp;
81+
82+
beforeEach(() => {
83+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ff-plist-dep-'));
84+
});
85+
86+
afterEach(() => {
87+
fs.rmSync(tmp, {recursive: true, force: true});
88+
});
89+
90+
it('loads without requiring plist', () => {
91+
expect(() => require('../flavored-frameworks')).not.toThrow();
92+
});
93+
94+
it('parses an xcframework Info.plist via plutil', () => {
95+
const {parseXcframework} = require('../flavored-frameworks');
96+
const parsed = parseXcframework(makeXcframework(tmp));
97+
expect(parsed.slices).toHaveLength(1);
98+
expect(parsed.slices[0].libraryIdentifier).toBe('ios-arm64');
99+
expect(parsed.slices[0].platform).toBe('ios');
100+
});
101+
});

packages/react-native/scripts/spm/__tests__/flavored-frameworks-test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,28 @@
1010

1111
'use strict';
1212

13+
// flavored-frameworks.js shells out to `plutil` to read xcframework
14+
// Info.plists as JSON — plutil is macOS-only, so stand in with a portable
15+
// plist-parse for Linux CI. Module-level mock because the module destructures
16+
// `execFileSync` at require time (same pattern as the old swap-flavor-test).
17+
jest.mock('child_process', () => {
18+
const actual = jest.requireActual('child_process');
19+
return {
20+
...actual,
21+
execFileSync: (cmd, args, opts) => {
22+
if (cmd === 'plutil') {
23+
const fsActual = require('fs');
24+
const plistActual = jest.requireActual('plist');
25+
const file = args[args.length - 1];
26+
return Buffer.from(
27+
JSON.stringify(plistActual.parse(fsActual.readFileSync(file, 'utf8'))),
28+
);
29+
}
30+
return actual.execFileSync(cmd, args, opts);
31+
},
32+
};
33+
});
34+
1335
const {
1436
PLUGIN_FRAMEWORKS_MANIFEST,
1537
finalizeArtifactPublication,

packages/react-native/scripts/spm/flavored-frameworks.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,16 @@ const {execFileSync} = require('child_process');
1414
const crypto = require('crypto');
1515
const fs = require('fs');
1616
const path = require('path');
17-
// $FlowFixMe[untyped-import] plist does not publish Flow declarations.
18-
const plist = require('plist');
17+
18+
// Info.plist parsing goes through plutil (as the pre-B1 swap script did): the
19+
// `plist` npm module is NOT a react-native dependency, so it only resolves
20+
// inside this monorepo via hoisting — requiring it breaks `spm add` in a
21+
// fresh consumer app with "Cannot find module 'plist'".
22+
function plistJson(p /*: string */) /*: $FlowFixMe */ {
23+
return JSON.parse(
24+
execFileSync('plutil', ['-convert', 'json', '-o', '-', p]).toString(),
25+
);
26+
}
1927

2028
/*:: import type {
2129
FlavoredFrameworkManifestEntry,
@@ -162,7 +170,7 @@ function parseXcframework(
162170
if (!fs.existsSync(infoPath)) {
163171
throw new Error(`XCFramework Info.plist missing at ${infoPath}`);
164172
}
165-
const info = plist.parse(fs.readFileSync(infoPath, 'utf8'));
173+
const info = plistJson(infoPath);
166174
const libraries = info.AvailableLibraries;
167175
if (!Array.isArray(libraries) || libraries.length === 0) {
168176
throw new Error(
@@ -303,9 +311,7 @@ function frameworkHeaderHashes(
303311
function invariantHeadersHashes(
304312
xcframeworkPath /*: string */,
305313
) /*: Array<string> */ {
306-
const info = plist.parse(
307-
fs.readFileSync(path.join(xcframeworkPath, 'Info.plist'), 'utf8'),
308-
);
314+
const info = plistJson(path.join(xcframeworkPath, 'Info.plist'));
309315
if (!Array.isArray(info.AvailableLibraries)) {
310316
throw new Error(`XCFramework has no libraries: ${xcframeworkPath}`);
311317
}

0 commit comments

Comments
 (0)