Skip to content

Commit 19dc7a9

Browse files
chrfalchclaude
andcommitted
[iOS][SPM] Refresh transitive scaffolded manifests too
The floor refresh visited only the direct autolinking.json entries, while the scaffolder covers the expanded set including transitive SwiftPM dependencies, so a previously scaffolded transitive dep kept its old floor after `spm update`. Both now share one dependency-collection helper. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 386fcfb commit 19dc7a9

2 files changed

Lines changed: 133 additions & 71 deletions

File tree

packages/react-native/scripts/spm/__tests__/scaffold-package-swift-test.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1773,6 +1773,45 @@ describe('refreshScaffoldedPlatformFloors', () => {
17731773
expect(read('react-native-autogen')).toBe(autogen);
17741774
});
17751775

1776+
it('refreshes a transitive spm.dependency that autolinking.json never lists', () => {
1777+
writeApp({'react-native-a': manifest('platforms: [.iOS("15.1")],')});
1778+
fs.writeFileSync(
1779+
path.join(appRoot, 'node_modules/react-native-a/package.json'),
1780+
JSON.stringify({
1781+
name: 'react-native-a',
1782+
swiftpmConfig: {dependencies: ['react-native-transitive']},
1783+
}),
1784+
);
1785+
const transitiveRoot = path.join(
1786+
appRoot,
1787+
'node_modules',
1788+
'react-native-transitive',
1789+
);
1790+
fs.mkdirSync(transitiveRoot, {recursive: true});
1791+
fs.writeFileSync(
1792+
path.join(transitiveRoot, 'package.json'),
1793+
JSON.stringify({name: 'react-native-transitive', version: '1.0.0'}),
1794+
);
1795+
fs.writeFileSync(
1796+
path.join(transitiveRoot, 'react-native.config.js'),
1797+
'module.exports = {dependency: {platforms: {ios: {}}}};\n',
1798+
);
1799+
fs.writeFileSync(
1800+
path.join(transitiveRoot, 'Package.swift'),
1801+
manifest('platforms: [.iOS("15.1")],'),
1802+
'utf8',
1803+
);
1804+
1805+
expect(
1806+
refresh()
1807+
.map(entry => entry.depName)
1808+
.sort(),
1809+
).toEqual(['react-native-a', 'react-native-transitive']);
1810+
expect(read('react-native-transitive')).toContain(
1811+
'platforms: [.iOS("16.4")]',
1812+
);
1813+
});
1814+
17761815
it('returns nothing when there is no autolinking.json', () => {
17771816
expect(refresh()).toEqual([]);
17781817
});

packages/react-native/scripts/spm/scaffold-package-swift.js

Lines changed: 94 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,77 @@ function scaffoldPackageSwiftForDep(
11311131
};
11321132
}
11331133

1134+
/**
1135+
* The dependency set the autolinker considers: the iOS entries of
1136+
* autolinking.json plus their transitive SwiftPM dependencies. Null when the
1137+
* file declares none; throws when it cannot be read or parsed. `onSkipped`
1138+
* reports the entries dropped for having no iOS platform.
1139+
*/
1140+
function collectAutolinkedDeps(
1141+
opts /*: {
1142+
autolinkingJsonPath: string,
1143+
remote: ?{url: string, version: string, identity: string},
1144+
onSkipped?: (name: string) => void,
1145+
} */,
1146+
) /*: ?Array<AutolinkedDep> */ {
1147+
const {autolinkingJsonPath, remote, onSkipped} = opts;
1148+
/*:: type AutolinkingJson = {dependencies?: ?{[string]: {root?: string, platforms?: {ios?: ?{...}, ...}, ...}}, ...}; */
1149+
// $FlowFixMe[incompatible-type] JSON.parse returns any
1150+
const data /*: AutolinkingJson */ = JSON.parse(
1151+
fs.readFileSync(autolinkingJsonPath, 'utf8'),
1152+
);
1153+
const deps = data.dependencies;
1154+
if (deps == null) {
1155+
return null;
1156+
}
1157+
1158+
const directDeps /*: Array<AutolinkedDep> */ = [];
1159+
for (const name of Object.keys(deps)) {
1160+
const raw = deps[name];
1161+
if (raw == null) continue;
1162+
const root = raw.root;
1163+
const ios = raw.platforms?.ios;
1164+
if (typeof root !== 'string' || ios == null) {
1165+
onSkipped?.(name);
1166+
continue;
1167+
}
1168+
// $FlowFixMe[incompatible-type] `ios` shape is runtime-validated above
1169+
const iosPlatform /*: AutolinkingIosPlatform */ = ios;
1170+
directDeps.push({name, root, platforms: {ios: iosPlatform}});
1171+
}
1172+
1173+
try {
1174+
return expandSpmDependencies(directDeps, {
1175+
readConfig: defaultReadConfig,
1176+
resolveDep: defaultResolveDep,
1177+
readPodspec: defaultReadPodspec,
1178+
extraReservedNames: remote != null ? [remote.identity] : undefined,
1179+
});
1180+
} catch (e) {
1181+
if (e instanceof SpmNameCollisionError) {
1182+
throw e;
1183+
}
1184+
// A transitive-resolution failure shouldn't abort the whole pass; fall back
1185+
// to the direct deps so at least those are covered. They are named the way
1186+
// the autolinker names them — a manifest written under any other name
1187+
// outlives this error on disk and then fails to resolve.
1188+
log(`Transitive dependency expansion failed: ${e.message}`);
1189+
return directDeps.map(dep => {
1190+
const resolved = resolveSwiftName(
1191+
dep.name,
1192+
readSwiftpmConfig(dep.root, defaultReadConfig(dep.root)),
1193+
defaultReadPodspec(dep.root, dep.platforms.ios.podspecPath),
1194+
);
1195+
return {
1196+
...dep,
1197+
swiftName: resolved.name,
1198+
swiftNameSource: resolved.source,
1199+
swiftNamePodspecKey: resolved.podspecKey,
1200+
};
1201+
});
1202+
}
1203+
}
1204+
11341205
// The `.iOS(...)` element of the emitted platforms array — the `.v15` enum form
11351206
// pre-v20 scaffolds carry included, and without the array's closing bracket so
11361207
// a user-extended array (`[.iOS(…), .macOS(…)]`) is still matched.
@@ -1152,24 +1223,23 @@ function refreshScaffoldedPlatformFloors(
11521223
path.join(appRoot, 'build', 'generated', 'autolinking', 'autolinking.json');
11531224
const refreshed = [];
11541225

1155-
let deps;
1226+
// Best-effort: an unreadable autolinking.json, a name collision or a
1227+
// malformed remote config is the autolinker's error to report a moment
1228+
// later, not this pass's.
1229+
let deps /*: ?Array<AutolinkedDep> */ = null;
11561230
try {
1157-
// $FlowFixMe[incompatible-type] JSON.parse returns any
1158-
deps = JSON.parse(
1159-
fs.readFileSync(autolinkingJsonPath, 'utf8'),
1160-
).dependencies;
1231+
deps = collectAutolinkedDeps({
1232+
autolinkingJsonPath,
1233+
remote: remotePackageConfig(appRoot),
1234+
});
11611235
} catch {
11621236
return refreshed;
11631237
}
11641238
if (deps == null) {
11651239
return refreshed;
11661240
}
11671241

1168-
for (const depName of Object.keys(deps)) {
1169-
const root = deps[depName]?.root;
1170-
if (typeof root !== 'string') {
1171-
continue;
1172-
}
1242+
for (const {name: depName, root} of deps) {
11731243
const manifestPath = path.join(root, 'Package.swift');
11741244
let content;
11751245
try {
@@ -1247,76 +1317,29 @@ function scaffoldAll(
12471317
return [];
12481318
}
12491319

1250-
/*:: type AutolinkingJson = {dependencies?: ?{[string]: {root?: string, platforms?: {ios?: ?{...}, ...}, ...}}, ...}; */
1251-
// $FlowFixMe[incompatible-type] JSON.parse returns any
1252-
const data /*: AutolinkingJson */ = JSON.parse(
1253-
fs.readFileSync(autolinkingJsonPath, 'utf8'),
1254-
);
1255-
const deps = data.dependencies;
1256-
if (deps == null) {
1257-
return [];
1258-
}
1259-
1260-
// Narrow the direct autolinking.json entries with an iOS platform, then
1261-
// expand transitive `spm.dependencies` so the scaffolder covers EXACTLY the
1262-
// set the autolinker considers. Without this, a transitive native dep that
1320+
// The scaffolder covers EXACTLY the set the autolinker considers, transitive
1321+
// `spm.dependencies` included. Without them, a transitive native dep that
12631322
// ships no Package.swift would be flagged by the autolinker but never
12641323
// scaffolded here — leaving `react-native spm scaffold` unable to clear the
12651324
// autolinker's missing-manifest error.
12661325
const results /*: Array<ScaffoldResult> */ = [];
1267-
const directDeps /*: Array<AutolinkedDep> */ = [];
1268-
for (const name of Object.keys(deps)) {
1269-
const raw = deps[name];
1270-
if (raw == null) continue;
1271-
const root = raw.root;
1272-
const ios = raw.platforms?.ios;
1273-
if (typeof root !== 'string' || ios == null) {
1326+
// Outside collectAutolinkedDeps' own try: a malformed remote config
1327+
// (RemoteVersionError) is a misconfiguration to surface, not an expansion
1328+
// failure to degrade past.
1329+
const remote = remotePackageConfig(appRoot);
1330+
const allDeps = collectAutolinkedDeps({
1331+
autolinkingJsonPath,
1332+
remote,
1333+
onSkipped: name => {
12741334
results.push({
12751335
depName: name,
12761336
status: 'skipped-no-ios',
12771337
reason: 'no iOS platform in autolinking.json',
12781338
});
1279-
continue;
1280-
}
1281-
// $FlowFixMe[incompatible-type] `ios` shape is runtime-validated above
1282-
const iosPlatform /*: AutolinkingIosPlatform */ = ios;
1283-
directDeps.push({name, root, platforms: {ios: iosPlatform}});
1284-
}
1285-
1286-
// Outside the try: a malformed remote config (RemoteVersionError) is a
1287-
// misconfiguration to surface, not an expansion failure to degrade past.
1288-
const remote = remotePackageConfig(appRoot);
1289-
1290-
let allDeps /*: Array<AutolinkedDep> */ = [];
1291-
try {
1292-
allDeps = expandSpmDependencies(directDeps, {
1293-
readConfig: defaultReadConfig,
1294-
resolveDep: defaultResolveDep,
1295-
readPodspec: defaultReadPodspec,
1296-
extraReservedNames: remote != null ? [remote.identity] : undefined,
1297-
});
1298-
} catch (e) {
1299-
if (e instanceof SpmNameCollisionError) {
1300-
throw e;
1301-
}
1302-
// A transitive-resolution failure shouldn't abort the whole scaffold pass;
1303-
// fall back to the direct deps so at least those get manifests. They are
1304-
// named the same way the autolinker names them — a manifest written under
1305-
// any other name outlives this error on disk and then fails to resolve.
1306-
log(`Transitive dependency expansion failed: ${e.message}`);
1307-
allDeps = directDeps.map(dep => {
1308-
const resolved = resolveSwiftName(
1309-
dep.name,
1310-
readSwiftpmConfig(dep.root, defaultReadConfig(dep.root)),
1311-
defaultReadPodspec(dep.root, dep.platforms.ios.podspecPath),
1312-
);
1313-
return {
1314-
...dep,
1315-
swiftName: resolved.name,
1316-
swiftNameSource: resolved.source,
1317-
swiftNamePodspecKey: resolved.podspecKey,
1318-
};
1319-
});
1339+
},
1340+
});
1341+
if (allDeps == null) {
1342+
return results;
13201343
}
13211344

13221345
// Index every autolinked dep's podspec name → its npm name, so a dep that

0 commit comments

Comments
 (0)