Skip to content

Commit 7c63971

Browse files
committed
feat(spm): commit xcodeproj as canonical app project; drop unused app Package.swift
The developer-editable <appRoot>/Package.swift was never referenced by the generated <App>-SPM.xcodeproj — the xcodeproj's XCLocalSwiftPackageReference entries point only at the three sub-packages under build/. Edits to it had no effect on Xcode builds, making it dead weight and a source of confusion. In its place: treat <App>-SPM.xcodeproj as the canonical, committed app project (analogous to the legacy <App>.xcodeproj). Because the xcodeproj's sub-package references under build/ are stable across dep changes (autolinking churn happens inside the sub-packages, which are gitignored), regenerating it after the initial scaffold would only clobber Xcode-side edits (signing, capabilities, Build Phases). So generation flips to create-if-missing. Changes: - generate-spm-package.js: remove generateInitialPackageSwift, scanSourceFiles, --init flag, ScanResult / GeneratePackageOpts types. - setup-apple-spm.js: drop --init plumbing, warnForMissingPackageSwift, warnForMissingVfsOverlayFlags. generateXcodeProject is create-if-missing; new --force-xcodeproj for opt-in regeneration; new findExistingSpmXcodeproj helper. gatherCleanTargets no longer enumerates Package.swift; the --project clean scope now goes through the same confirmation prompt as --derived-data/--cache (bypass with --yes) to avoid silently deleting a committed xcodeproj. - Header docs updated to explain the committed-xcodeproj policy. - 4 new tests for findExistingSpmXcodeproj; 5 obsolete tests removed.
1 parent ef87a2f commit 7c63971

5 files changed

Lines changed: 143 additions & 470 deletions

File tree

packages/react-native/scripts/setup-apple-spm.js

Lines changed: 73 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
* node node_modules/react-native/scripts/setup-apple-spm.js [action] [options]
2121
*
2222
* Actions:
23-
* init First-time setup: generate root Package.swift,
24-
* generated packages, artifacts, and .xcodeproj.
25-
* update Regenerate generated packages/artifacts/project
26-
* without overwriting root Package.swift.
23+
* init First-time setup: generate sub-packages,
24+
* artifacts, and .xcodeproj. Adds SPM entries
25+
* to .gitignore.
26+
* update Regenerate sub-packages/artifacts. Does NOT
27+
* touch <App>-SPM.xcodeproj (it's committed —
28+
* see below). Pass --force-xcodeproj to opt in.
2729
* sync Lightweight sync invoked by the Xcode auto-sync
2830
* build phase: regenerates autolinking and
2931
* xcframeworks sub-packages and writes the
@@ -51,11 +53,19 @@
5153
* 2. generate-spm-autolinking-config.js → build/generated/autolinking/autolinking.json
5254
* 3. generate-spm-autolinking.js → build/generated/autolinking/Package.swift
5355
* 4. download-spm-artifacts.js → <artifacts-dir>/ (skipped if already present)
54-
* 5. generate-spm-package.js → build/xcframeworks/Package.swift + symlinks (+ main Package.swift with init)
55-
* 6. generate-spm-xcodeproj.js → <AppName>-SPM.xcodeproj (skipped with --skip-xcodeproj)
56+
* 5. generate-spm-package.js → build/xcframeworks/Package.swift + symlinks
57+
* 6. generate-spm-xcodeproj.js → <AppName>-SPM.xcodeproj (create-if-missing;
58+
* skipped with --skip-xcodeproj; opt back
59+
* into overwrite with --force-xcodeproj)
5660
*
57-
* The main Package.swift is committed by the developer and NOT overwritten on
58-
* subsequent runs. Use init for first-time setup to generate an initial one.
61+
* Commit policy: <AppName>-SPM.xcodeproj is COMMITTED to your repo, like
62+
* the legacy <AppName>.xcodeproj. It holds your signing, capabilities,
63+
* Build Phases, schemes — edit it in Xcode the normal way. Its
64+
* XCLocalSwiftPackageReference entries point at three stable sub-package
65+
* paths under build/ (xcframeworks, generated/autolinking, generated/ios);
66+
* adding/removing community deps changes the contents of those sub-packages
67+
* (gitignored) and never requires regenerating the xcodeproj. No app-level
68+
* Package.swift is generated or required.
5969
*
6070
* After running: open <AppName>-SPM.xcodeproj in Xcode.
6171
*/
@@ -158,6 +168,12 @@ function parseArgs(argv /*: Array<string> */) /*: SetupArgs */ {
158168
default: false,
159169
describe: 'Skip .xcodeproj generation step',
160170
})
171+
.option('force-xcodeproj', {
172+
type: 'boolean',
173+
default: false,
174+
describe:
175+
'Regenerate <App>-SPM.xcodeproj even if it already exists. WARNING: overwrites in-place Xcode edits (signing, capabilities, build phases). The xcodeproj is committed to your repo; SPM references stable sub-package paths under build/, so regeneration is not normally needed.',
176+
})
161177
.option('bundle-identifier', {
162178
type: 'string',
163179
describe: 'Override CFBundleIdentifier in generated Info.plist',
@@ -175,7 +191,8 @@ function parseArgs(argv /*: Array<string> */) /*: SetupArgs */ {
175191
.option('project', {
176192
type: 'boolean',
177193
default: false,
178-
describe: '[clean] Also remove Package.swift and <App>-SPM.xcodeproj/',
194+
describe:
195+
'[clean] Also remove the committed <App>-SPM.xcodeproj/ (will prompt for confirmation; bypass with --yes)',
179196
})
180197
.option('derived-data', {
181198
type: 'boolean',
@@ -232,6 +249,7 @@ function parseArgs(argv /*: Array<string> */) /*: SetupArgs */ {
232249
skipDownload: parsed['skip-download'],
233250
forceDownload: parsed['force-download'],
234251
skipXcodeproj: parsed['skip-xcodeproj'],
252+
forceXcodeproj: parsed['force-xcodeproj'],
235253
bundleIdentifier: parsed['bundle-identifier'] ?? null,
236254
productName: parsed['product-name'] ?? null,
237255
entryFile: parsed['entry-file'] ?? null,
@@ -327,10 +345,6 @@ function gatherCleanTargets(
327345
}
328346

329347
if (opts.project === true) {
330-
targets.push({
331-
path: path.join(appRoot, 'Package.swift'),
332-
label: 'Package.swift',
333-
});
334348
for (const name of xcodeprojNames) {
335349
targets.push({path: path.join(appRoot, name), label: `${name}/`});
336350
}
@@ -1045,7 +1059,6 @@ function generateXcframeworksPackage(
10451059
reactNativeRoot /*: string */,
10461060
version /*: string */,
10471061
resolvedArtifactsDir /*: string | null */,
1048-
shouldInit /*: boolean */,
10491062
) {
10501063
log('Generating xcframeworks sub-package...');
10511064
const packageArgs = [
@@ -1062,50 +1075,9 @@ function generateXcframeworksPackage(
10621075
if (resolvedArtifactsDir != null) {
10631076
packageArgs.push('--artifacts-dir', resolvedArtifactsDir);
10641077
}
1065-
if (shouldInit) {
1066-
packageArgs.push('--init');
1067-
}
10681078
generatePackage(packageArgs);
10691079
}
10701080

1071-
function warnForMissingPackageSwift(appRoot /*: string */) {
1072-
const mainPackageSwift = path.join(appRoot, 'Package.swift');
1073-
if (fs.existsSync(mainPackageSwift)) {
1074-
return;
1075-
}
1076-
1077-
log('');
1078-
log(
1079-
'\x1b[33mWARNING: Package.swift not found.\x1b[0m Run init to generate an initial one:',
1080-
);
1081-
log(' react-native spm init');
1082-
log('');
1083-
}
1084-
1085-
function warnForMissingVfsOverlayFlags(appRoot /*: string */) {
1086-
const mainPackageSwift = path.join(appRoot, 'Package.swift');
1087-
if (!fs.existsSync(mainPackageSwift)) {
1088-
return;
1089-
}
1090-
1091-
const pkgContent = fs.readFileSync(mainPackageSwift, 'utf8');
1092-
if (!pkgContent.includes('ivfsoverlay')) {
1093-
log('');
1094-
log(
1095-
'\x1b[33mWARNING: Your Package.swift does not include -ivfsoverlay flags.\x1b[0m',
1096-
);
1097-
log('Add the following to your Package.swift for stable header identity:');
1098-
log('');
1099-
log(' let vfsOverlay = packageDir + "/build/xcframeworks/React-VFS.yaml"');
1100-
log('');
1101-
log(' // Add to cFlags:');
1102-
log(' "-ivfsoverlay", vfsOverlay');
1103-
log(' // Add to swiftFlags:');
1104-
log(' "-Xcc", "-ivfsoverlay", "-Xcc", vfsOverlay');
1105-
log('');
1106-
}
1107-
}
1108-
11091081
function generateXcodeProject(
11101082
args /*: SetupArgs */,
11111083
appRoot /*: string */,
@@ -1116,7 +1088,25 @@ function generateXcodeProject(
11161088
return;
11171089
}
11181090

1119-
log('Generating .xcodeproj...');
1091+
// The xcodeproj is committed; its XCLocalSwiftPackageReference entries
1092+
// point at three stable sub-package paths under build/, so adding/removing
1093+
// community deps never requires regenerating it. Regenerate only when
1094+
// missing (fresh clone / first init) or when the user explicitly opts in
1095+
// via --force-xcodeproj.
1096+
const existing = findExistingSpmXcodeproj(appRoot);
1097+
if (existing != null && !args.forceXcodeproj) {
1098+
log(
1099+
`Found existing ${path.basename(existing)}; skipping regeneration ` +
1100+
`(pass --force-xcodeproj to overwrite, e.g. after deleting it).`,
1101+
);
1102+
return;
1103+
}
1104+
1105+
log(
1106+
existing != null
1107+
? `Regenerating .xcodeproj (--force-xcodeproj will overwrite Xcode-side edits)...`
1108+
: 'Generating .xcodeproj...',
1109+
);
11201110
const xcodeprojArgs = [
11211111
'--app-root',
11221112
appRoot,
@@ -1135,6 +1125,26 @@ function generateXcodeProject(
11351125
generateXcodeproj(xcodeprojArgs);
11361126
}
11371127

1128+
/**
1129+
* Returns the absolute path to the first `*-SPM.xcodeproj/` directory found
1130+
* directly inside `appRoot`, or null if none exists.
1131+
*/
1132+
function findExistingSpmXcodeproj(appRoot /*: string */) /*: string | null */ {
1133+
try {
1134+
const entries /*: Array<{name: string, isDirectory(): boolean}> */ =
1135+
// $FlowFixMe[incompatible-type] Dirent typing
1136+
fs.readdirSync(appRoot, {withFileTypes: true});
1137+
for (const entry of entries) {
1138+
if (entry.isDirectory() && entry.name.endsWith('-SPM.xcodeproj')) {
1139+
return path.join(appRoot, entry.name);
1140+
}
1141+
}
1142+
} catch {
1143+
/* appRoot may not exist on init */
1144+
}
1145+
return null;
1146+
}
1147+
11381148
function logNextSteps(
11391149
projectRoot /*: string */,
11401150
appRoot /*: string */,
@@ -1202,9 +1212,12 @@ async function main(argv /*:: ?: Array<string> */) /*: Promise<void> */ {
12021212
}
12031213
}
12041214

1205-
// Destructive scopes (DerivedData / cache) touch state outside appRoot.
1206-
// Ask for confirmation unless --yes is passed or stdin isn't a TTY.
1207-
const isDestructive = wantDerivedData || wantCache;
1215+
// Destructive scopes ask for confirmation unless --yes is passed:
1216+
// --derived-data / --cache touch state outside appRoot
1217+
// --project removes the committed <App>-SPM.xcodeproj/
1218+
// The xcodeproj carries the user's signing, capabilities, build phases
1219+
// and is committed to the repo — deleting it loses Xcode-side edits.
1220+
const isDestructive = wantDerivedData || wantCache || wantProject;
12081221
if (isDestructive && !args.cleanYes) {
12091222
const targets = gatherCleanTargets(appRoot, cleanOpts).filter(t =>
12101223
fs.existsSync(t.path),
@@ -1317,7 +1330,6 @@ async function main(argv /*:: ?: Array<string> */) /*: Promise<void> */ {
13171330
reactNativeRoot,
13181331
version,
13191332
resolvedArtifactsDir,
1320-
action === 'init',
13211333
);
13221334
} catch (e) {
13231335
logError(`generate-spm-package.js failed: ${e.message}`);
@@ -1329,10 +1341,7 @@ async function main(argv /*:: ?: Array<string> */) /*: Promise<void> */ {
13291341

13301342
if (action === 'init') {
13311343
ensureGitignoreSpmEntries(appRoot);
1332-
} else {
1333-
warnForMissingPackageSwift(appRoot);
13341344
}
1335-
warnForMissingVfsOverlayFlags(appRoot);
13361345

13371346
try {
13381347
generateXcodeProject(args, appRoot, reactNativeRoot);
@@ -1367,6 +1376,7 @@ module.exports = {
13671376
cleanGeneratedState,
13681377
gatherCleanTargets,
13691378
describeRnRootMismatch,
1379+
findExistingSpmXcodeproj,
13701380
findLegacyXcodeproj,
13711381
podfileNeedsPatch,
13721382
};

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

Lines changed: 0 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
const {
1414
findSourcePath,
15-
generateInitialPackageSwift,
1615
generateXCFrameworksPackageSwift,
1716
} = require('../generate-spm-package');
1817
const fs = require('fs');
@@ -62,110 +61,6 @@ describe('generateXCFrameworksPackageSwift', () => {
6261
});
6362
});
6463

65-
// ---------------------------------------------------------------------------
66-
// generateInitialPackageSwift
67-
// ---------------------------------------------------------------------------
68-
69-
describe('generateInitialPackageSwift', () => {
70-
let tempDir;
71-
72-
beforeEach(() => {
73-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'spm-pkg-test-'));
74-
fs.mkdirSync(path.join(tempDir, 'MyApp'));
75-
});
76-
77-
afterEach(() => {
78-
fs.rmSync(tempDir, {recursive: true, force: true});
79-
});
80-
81-
it('generates single target for pure ObjC', () => {
82-
const result = generateInitialPackageSwift({
83-
appName: 'MyApp',
84-
targetName: 'MyAppApp',
85-
sourcePath: 'MyApp',
86-
iosVersion: '15',
87-
swiftFiles: [],
88-
hasObjC: true,
89-
appRoot: tempDir,
90-
});
91-
expect(result).toContain('name: "MyAppApp"');
92-
expect(result).toContain('path: "MyApp"');
93-
expect(result).toContain('.product(name: "ReactNative"');
94-
expect(result).not.toContain('MyAppAppSwift');
95-
});
96-
97-
it('generates single target for pure Swift', () => {
98-
const result = generateInitialPackageSwift({
99-
appName: 'MyApp',
100-
targetName: 'MyAppApp',
101-
sourcePath: 'MyApp',
102-
iosVersion: '15',
103-
swiftFiles: ['App.swift'],
104-
hasObjC: false,
105-
appRoot: tempDir,
106-
});
107-
expect(result).toContain('name: "MyAppApp"');
108-
expect(result).not.toContain('MyAppAppSwift');
109-
expect(result).toContain('swiftSettings:');
110-
});
111-
112-
it('generates split targets for mixed Swift+ObjC', () => {
113-
const result = generateInitialPackageSwift({
114-
appName: 'MyApp',
115-
targetName: 'MyAppApp',
116-
sourcePath: 'MyApp',
117-
iosVersion: '16',
118-
swiftFiles: ['App.swift', 'Bridge.swift'],
119-
hasObjC: true,
120-
appRoot: tempDir,
121-
});
122-
// ObjC target
123-
expect(result).toContain('name: "MyAppApp"');
124-
expect(result).toContain('publicHeadersPath: "."');
125-
// Swift target
126-
expect(result).toContain('name: "MyAppAppSwift"');
127-
expect(result).toContain('dependencies: ["MyAppApp"]');
128-
expect(result).toContain('sources: ["App.swift", "Bridge.swift"]');
129-
// Both in products
130-
expect(result).toContain(
131-
'.library(name: "MyAppApp", targets: ["MyAppApp"])',
132-
);
133-
expect(result).toContain(
134-
'.library(name: "MyAppAppSwift", targets: ["MyAppAppSwift"])',
135-
);
136-
});
137-
138-
it('sets iOS version from parameter', () => {
139-
const result = generateInitialPackageSwift({
140-
appName: 'MyApp',
141-
targetName: 'MyAppApp',
142-
sourcePath: 'MyApp',
143-
iosVersion: '17',
144-
swiftFiles: [],
145-
hasObjC: true,
146-
appRoot: tempDir,
147-
});
148-
expect(result).toContain('.iOS(.v17)');
149-
});
150-
151-
it('excludes main.m and Info.plist when present', () => {
152-
fs.writeFileSync(path.join(tempDir, 'MyApp', 'main.m'), '');
153-
fs.writeFileSync(path.join(tempDir, 'MyApp', 'Info.plist'), '');
154-
155-
const result = generateInitialPackageSwift({
156-
appName: 'MyApp',
157-
targetName: 'MyAppApp',
158-
sourcePath: 'MyApp',
159-
iosVersion: '15',
160-
swiftFiles: [],
161-
hasObjC: true,
162-
appRoot: tempDir,
163-
});
164-
expect(result).toContain('"main.m"');
165-
expect(result).toContain('"Info.plist"');
166-
});
167-
});
168-
16964
// ---------------------------------------------------------------------------
17065
// findSourcePath
17166
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)