Skip to content

Commit 1a228e2

Browse files
chrfalchclaude
andcommitted
test(spm): regression-test the autolinking plugin-host exemption
Drive generate-spm-autolinking's main() over a minimal app fixture whose only autolinked iOS dep is a plugin host (expo: native sources, no Package.swift) to pin down the exemption added with the plugin system: a dep that declares an `spm.autolinkingPlugin` owns its native contribution and must NOT also be source-built through the community-lib path. A regression pair, so the guard is meaningful rather than incidental: - With the plugin declared: main() does not throw, the plugin's ExpoModulesCore package + product contribution is merged into the aggregator, and expo is not source-built (no `packages/<Name>` wrapper ref, no `__rnAutolinkedLibs` guard). - Negative control: the SAME fixture with the plugin declaration removed throws MissingManifestError — proving the exemption is load-bearing. This exercises the full main() path (dep walk, plugin discovery/invoke, manifest write) rather than a pure helper, which is why it was missing; the surrounding pieces (discoverPlugins / invokePlugins / the manifest merge) were already covered. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c2f7a04 commit 1a228e2

1 file changed

Lines changed: 105 additions & 0 deletions

File tree

packages/react-native/scripts/spm/__tests__/generate-spm-autolinking-test.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const {
2121
hasMixedLanguageSources,
2222
hasPodspec,
2323
linkHeaderTree,
24+
main,
2425
reactDescriptor,
2526
reportMissingManifests,
2627
} = require('../generate-spm-autolinking');
@@ -998,3 +999,107 @@ describe('hasMixedLanguageSources', () => {
998999
expect(hasMixedLanguageSources(root)).toBe(false);
9991000
});
10001001
});
1002+
1003+
// ---------------------------------------------------------------------------
1004+
// main() — autolinking plugin host exemption
1005+
//
1006+
// A dep that declares an autolinking plugin OWNS its native contribution (the
1007+
// plugin returns its package/product deps). RN must NOT also try to
1008+
// source-build that dep through the community-lib path: a plugin host like
1009+
// Expo typically ships no Package.swift and is mixed Swift/ObjC, so the
1010+
// community-lib path would raise MissingManifestError before the plugin ever
1011+
// runs. The regression pair below pins that exemption down — the negative
1012+
// control proves it is load-bearing (remove the plugin and the SAME dep throws).
1013+
// ---------------------------------------------------------------------------
1014+
1015+
describe('main() — autolinking plugin host exemption', () => {
1016+
let created = [];
1017+
let spies = [];
1018+
1019+
beforeEach(() => {
1020+
// Silence the [generate-spm-autolinking] logger (console.log/warn/error).
1021+
for (const m of ['log', 'warn', 'error']) {
1022+
spies.push(jest.spyOn(console, m).mockImplementation(() => {}));
1023+
}
1024+
});
1025+
1026+
afterEach(() => {
1027+
for (const s of spies) s.mockRestore();
1028+
spies = [];
1029+
for (const d of created) fs.rmSync(d, {recursive: true, force: true});
1030+
created = [];
1031+
});
1032+
1033+
// Builds a minimal app fixture whose ONLY autolinked iOS dep is `expo`, which
1034+
// ships NO Package.swift. When `withPlugin` is set, expo declares an
1035+
// autolinking plugin in its own react-native.config.js (transitive opt-in).
1036+
function buildFixture({withPlugin}) {
1037+
const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'spm-plugin-host-'));
1038+
created.push(appRoot);
1039+
// rnRoot only needs to exist (main() existence-checks it, then passes it
1040+
// through as context.reactNativeRoot).
1041+
const rnRoot = path.join(appRoot, 'rn');
1042+
fs.mkdirSync(rnRoot, {recursive: true});
1043+
// package.json so findProjectRoot() resolves to appRoot.
1044+
fs.writeFileSync(
1045+
path.join(appRoot, 'package.json'),
1046+
JSON.stringify({name: 'app'}),
1047+
);
1048+
// The plugin-host dep: native sources present, but NO Package.swift.
1049+
const expoDir = path.join(appRoot, 'node_modules', 'expo');
1050+
fs.mkdirSync(path.join(expoDir, 'ios'), {recursive: true});
1051+
fs.writeFileSync(path.join(expoDir, 'ios', 'Expo.mm'), '// native source\n');
1052+
if (withPlugin) {
1053+
fs.writeFileSync(
1054+
path.join(expoDir, 'react-native.config.js'),
1055+
"module.exports = { spm: { autolinkingPlugin: './spm-plugin.js' } };\n",
1056+
);
1057+
fs.writeFileSync(
1058+
path.join(expoDir, 'spm-plugin.js'),
1059+
'module.exports = function () {\n' +
1060+
' return {\n' +
1061+
" packageDependencies: [{name: 'ExpoModulesCore', path: '../../../../node_modules/expo/ios'}],\n" +
1062+
" productDependencies: [{name: 'ExpoModulesCore', package: 'ExpoModulesCore'}],\n" +
1063+
' };\n' +
1064+
'};\n',
1065+
);
1066+
}
1067+
const autolinkDir = path.join(appRoot, 'build', 'generated', 'autolinking');
1068+
fs.mkdirSync(autolinkDir, {recursive: true});
1069+
fs.writeFileSync(
1070+
path.join(autolinkDir, 'autolinking.json'),
1071+
JSON.stringify({
1072+
dependencies: {expo: {root: expoDir, platforms: {ios: {}}}},
1073+
}),
1074+
);
1075+
return {appRoot, rnRoot};
1076+
}
1077+
1078+
it('exempts a plugin-host dep from source-building (no MissingManifestError; plugin contribution merged)', () => {
1079+
const {appRoot, rnRoot} = buildFixture({withPlugin: true});
1080+
expect(() =>
1081+
main(['--app-root', appRoot, '--react-native-root', rnRoot]),
1082+
).not.toThrow();
1083+
1084+
const pkg = fs.readFileSync(
1085+
path.join(appRoot, 'build/generated/autolinking/Package.swift'),
1086+
'utf8',
1087+
);
1088+
// The plugin's contribution is present …
1089+
expect(pkg).toContain('.package(name: "ExpoModulesCore"');
1090+
expect(pkg).toContain(
1091+
'.product(name: "ExpoModulesCore", package: "ExpoModulesCore")',
1092+
);
1093+
// … and expo is NOT source-built as a community lib: no wrapper package
1094+
// reference and no eval-time missing-manifest guard.
1095+
expect(pkg).not.toContain('path: "packages/');
1096+
expect(pkg).not.toContain('__rnAutolinkedLibs');
1097+
});
1098+
1099+
it('negative control: without the plugin declaration the SAME dep throws MissingManifestError (exemption is load-bearing)', () => {
1100+
const {appRoot, rnRoot} = buildFixture({withPlugin: false});
1101+
expect(() =>
1102+
main(['--app-root', appRoot, '--react-native-root', rnRoot]),
1103+
).toThrow(MissingManifestError);
1104+
});
1105+
});

0 commit comments

Comments
 (0)