Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,63 @@ describe('main() — autolinking plugin host exemption', () => {
main(['--app-root', appRoot, '--react-native-root', rnRoot]),
).toThrow(MissingManifestError);
});

// Adds a dep that declares the plugin host in its own `spm.dependencies`.
// `selfManaged` gives it a Package.swift of its own — which is what decides
// whether React Native emits its package references or the dep does.
function addDependentOfExpo(appRoot, name, {selfManaged = false} = {}) {
const depDir = path.join(appRoot, 'node_modules', name);
fs.mkdirSync(path.join(depDir, 'ios'), {recursive: true});
fs.writeFileSync(path.join(depDir, 'ios', 'Dep.mm'), '// native source\n');
if (selfManaged) {
fs.writeFileSync(
path.join(depDir, 'Package.swift'),
'// swift-tools-version: 6.0\n',
);
}
fs.writeFileSync(
path.join(depDir, 'react-native.config.js'),
'module.exports = {dependency: {platforms: {ios: {}}}, ' +
"spm: {dependencies: ['expo']}};\n",
);
const jsonPath = path.join(
appRoot,
'build/generated/autolinking/autolinking.json',
);
const json = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
json.dependencies[name] = {root: depDir, platforms: {ios: {}}};
fs.writeFileSync(jsonPath, JSON.stringify(json));
}

it('fails when a dep whose manifest RN generates declares the plugin host in its spm.dependencies', () => {
const {appRoot, rnRoot} = buildFixture({withPlugin: true});
addDependentOfExpo(appRoot, 'react-native-y');
const run = () =>
main(['--app-root', appRoot, '--react-native-root', rnRoot]);
// Both packages, so the reader knows which config to edit and why.
expect(run).toThrow(/'react-native-y'/);
expect(run).toThrow(/'expo'/);
expect(run).toThrow(/autolinking plugin/);
expect(run).toThrow(/spm\.dependencies/);
});

it('leaves a self-managed dependent alone — its own Package.swift declares its package references, so RN emits none', () => {
const {appRoot, rnRoot} = buildFixture({withPlugin: true});
addDependentOfExpo(appRoot, 'react-native-y', {selfManaged: true});
expect(() =>
main(['--app-root', appRoot, '--react-native-root', rnRoot]),
).not.toThrow();
});

it('leaves the same pair alone when the host ships no plugin — a plain spm.dependency is not a plugin host', () => {
const {appRoot, rnRoot} = buildFixture({withPlugin: false});
addDependentOfExpo(appRoot, 'react-native-y');
// Both deps ship no manifest, so the missing-manifest error is the expected
// one — the plugin-host diagnosis must not fire for a plain dependency.
expect(() =>
main(['--app-root', appRoot, '--react-native-root', rnRoot]),
).toThrow(MissingManifestError);
});
});

// ---------------------------------------------------------------------------
Expand Down
31 changes: 31 additions & 0 deletions packages/react-native/scripts/spm/generate-spm-autolinking.js
Original file line number Diff line number Diff line change
Expand Up @@ -1296,8 +1296,39 @@ function main(argv /*:: ?: Array<string> */) /*: void */ {
discoveredPlugins.map(p => p.depName),
);

// Skipped means no sibling package is created for the host either, so a
// dep declaring it in `spm.dependencies` gets a package reference to a
// path this run never writes — SPM then reports only the missing path.
// Only manifests React Native emits can carry that reference: a dep
// shipping its own Package.swift declares its package references itself,
// and the classification loop below would treat it as self-managed.
const pluginHostDependents /*: Map<string, Array<string>> */ = new Map();
for (const dep of allDeps) {
const declaredHosts = (dep.spmDependencies ?? []).filter(name =>
pluginHostDeps.has(name),
);
if (declaredHosts.length === 0) {
continue;
}
const sourceDir = dep.platforms.ios.sourceDir ?? dep.root;
if (sourceDir == null || findSelfManagedPackageDir(sourceDir) != null) {
continue;
}
for (const host of declaredHosts) {
const dependents = pluginHostDependents.get(host) ?? [];
dependents.push(dep.name);
pluginHostDependents.set(host, dependents);
}
}

for (const dep of allDeps) {
if (pluginHostDeps.has(dep.name)) {
const dependents = pluginHostDependents.get(dep.name);
if (dependents != null) {
throw new Error(
`react-native autolinking: '${dep.name}' ships an SPM autolinking plugin, which owns its native contribution — so React Native does not build it as a sibling target for anything to depend on. It is declared in 'spm.dependencies' by ${dependents.map(name => `'${name}'`).join(', ')}. Remove it there; nothing is lost. Its plugin links its products into the app and resolves its own ecosystem's dependencies, so a library that builds against it does not declare it here.`,
);
}
log(
`Skipping ${dep.name} target generation — provided by its SPM autolinking plugin`,
);
Expand Down
Loading