Skip to content

Commit 55f897f

Browse files
committed
fix(spm) add support for rewriting package.swift files
Added support for markers with versions to detecetd outdated/new versions of generated package.swift files.
1 parent 6f0d393 commit 55f897f

6 files changed

Lines changed: 269 additions & 27 deletions

File tree

packages/react-native/scripts/codegen/templates/Package.swift.spm-template

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
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-
1+
// swift-tools-version: 6.0
82
/*
93
* Copyright (c) Meta Platforms, Inc. and affiliates.
104
*
@@ -15,7 +9,6 @@
159
// AUTO-GENERATED by scripts/setup-apple-spm.js – do not edit manually.
1610
// This SPM-specific template replaces the CocoaPods codegen template
1711
// with xcframework configuration built in.
18-
// swift-tools-version: 6.0
1912

2013
import PackageDescription
2114
import Foundation

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

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

1313
const {
1414
SCAFFOLDER_MARKER,
15+
SCAFFOLDER_VERSION,
1516
emitScaffoldedPackageSwift,
1617
scaffoldAll,
1718
scaffoldPackageSwiftForDep,
@@ -256,9 +257,12 @@ describe('emitScaffoldedPackageSwift', () => {
256257
};
257258
}
258259

259-
it('starts with the SCAFFOLDER marker (and NOT the autolinker AUTOGEN marker)', () => {
260+
it('contains the SCAFFOLDER marker (after the line-1 swift-tools-version directive) and NOT the autolinker AUTOGEN marker', () => {
260261
const out = emitScaffoldedPackageSwift(baseSpec());
261-
expect(out.startsWith(SCAFFOLDER_MARKER)).toBe(true);
262+
// Line 1 is reserved for the swift-tools-version directive — SPM ignores
263+
// it elsewhere. The scaffolder marker lives on a subsequent line.
264+
expect(out.split('\n', 1)[0]).toMatch(/^\/\/ swift-tools-version: /);
265+
expect(out).toContain(SCAFFOLDER_MARKER);
262266
// The autolinker's marker — must be absent so isSelfManagedPackage
263267
// treats this file as self-managed.
264268
expect(out).not.toContain(
@@ -415,7 +419,11 @@ end
415419
path.join(depRoot, 'Package.swift'),
416420
'utf8',
417421
);
418-
expect(content.startsWith(SCAFFOLDER_MARKER)).toBe(true);
422+
// Line 1 is the swift-tools-version directive; the scaffolder marker
423+
// appears immediately after (still detectable by `isScaffolded` checks
424+
// that scan the whole file).
425+
expect(content.split('\n', 1)[0]).toMatch(/^\/\/ swift-tools-version: /);
426+
expect(content).toContain(SCAFFOLDER_MARKER);
419427
});
420428

421429
it('reports previouslyExisted=false for first-time scaffolds (so the CLI can prompt)', () => {
@@ -502,9 +510,12 @@ end
502510

503511
it('skips re-scaffolding when the existing file carries the scaffolder marker AND the same cache slot', () => {
504512
makePodspec();
505-
// Pre-existing scaffold from same slot
513+
// Pre-existing scaffold from same slot AND current generator version
514+
// — otherwise the version-bump skip-bypass kicks in.
506515
const prior =
507-
SCAFFOLDER_MARKER + '\n// Cache slot: 0.87.0-X/debug\n// rest unchanged';
516+
SCAFFOLDER_MARKER +
517+
`\n// AUTO-SCAFFOLDED-VERSION: ${SCAFFOLDER_VERSION}` +
518+
'\n// Cache slot: 0.87.0-X/debug\n// rest unchanged';
508519
fs.writeFileSync(path.join(depRoot, 'Package.swift'), prior);
509520
const result = scaffoldPackageSwiftForDep(
510521
makeDep(),
@@ -629,3 +640,119 @@ describe('scaffoldAll', () => {
629640
);
630641
});
631642
});
643+
644+
// ---------------------------------------------------------------------------
645+
// SCAFFOLDER_VERSION — auto-regen when the emitter's output format changes
646+
//
647+
// Without versioning, a Package.swift scaffolded by an older generator stays
648+
// on disk indefinitely (skip-on-marker), even when our template has since
649+
// been fixed. Bumping SCAFFOLDER_VERSION triggers a one-time regeneration
650+
// on next scaffold. Edits are persisted via patch-package per the marker
651+
// comment, so destructive regen here aligns with the documented workflow.
652+
// ---------------------------------------------------------------------------
653+
654+
describe('SCAFFOLDER_VERSION', () => {
655+
it('is a positive integer', () => {
656+
expect(Number.isInteger(SCAFFOLDER_VERSION)).toBe(true);
657+
expect(SCAFFOLDER_VERSION).toBeGreaterThanOrEqual(1);
658+
});
659+
660+
it('emitter writes the current version to the file', () => {
661+
const out = emitScaffoldedPackageSwift({
662+
swiftName: 'foo',
663+
sources: [],
664+
headerSearchPaths: [],
665+
coreReactNative: false,
666+
siblingNames: [],
667+
extraFrameworks: [],
668+
weakFrameworks: [],
669+
compilerFlags: [],
670+
publicHeadersPath: null,
671+
resources: [],
672+
warnings: [],
673+
});
674+
expect(out).toMatch(
675+
new RegExp(`^// AUTO-SCAFFOLDED-VERSION: ${SCAFFOLDER_VERSION}$`, 'm'),
676+
);
677+
});
678+
});
679+
680+
describe('scaffoldPackageSwiftForDep — version-based regen', () => {
681+
let tempDir;
682+
let depRoot;
683+
684+
beforeEach(() => {
685+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'spm-scaffold-version-'));
686+
depRoot = path.join(tempDir, 'node_modules', 'react-native-foo');
687+
fs.mkdirSync(depRoot, {recursive: true});
688+
fs.writeFileSync(
689+
path.join(depRoot, 'package.json'),
690+
JSON.stringify({name: 'react-native-foo', version: '1.0.0'}),
691+
);
692+
fs.writeFileSync(
693+
path.join(depRoot, 'react-native-foo.podspec'),
694+
"Pod::Spec.new do |s|\n s.name = 'react-native-foo'\n s.version = '1.0'\n s.source_files = 'ios/**/*.{h,m,mm}'\nend\n",
695+
);
696+
});
697+
698+
afterEach(() => {
699+
fs.rmSync(tempDir, {recursive: true, force: true});
700+
});
701+
702+
function makeDep() {
703+
return {
704+
name: 'react-native-foo',
705+
root: depRoot,
706+
platforms: {ios: {}},
707+
};
708+
}
709+
710+
function makeCtx(overrides = {}) {
711+
return {
712+
appRoot: tempDir,
713+
reactNativeRoot: depRoot,
714+
force: false,
715+
dryRun: false,
716+
cacheSlotLabel: 'SLOT-A/debug',
717+
skipDeps: new Set(),
718+
...overrides,
719+
};
720+
}
721+
722+
it('regenerates a file scaffolded under an older version, even without --force', () => {
723+
const olderVersion = Math.max(1, SCAFFOLDER_VERSION - 1);
724+
fs.writeFileSync(
725+
path.join(depRoot, 'Package.swift'),
726+
`${SCAFFOLDER_MARKER}\n// AUTO-SCAFFOLDED-VERSION: ${olderVersion}\n// Cache slot: SLOT-A/debug\n`,
727+
);
728+
const result = scaffoldPackageSwiftForDep(makeDep(), makeCtx());
729+
expect(result.status).toBe('written');
730+
const after = fs.readFileSync(path.join(depRoot, 'Package.swift'), 'utf8');
731+
expect(after).toContain(
732+
`// AUTO-SCAFFOLDED-VERSION: ${SCAFFOLDER_VERSION}`,
733+
);
734+
});
735+
736+
it('regenerates a marker-tagged file with NO version line (treats as v1)', () => {
737+
fs.writeFileSync(
738+
path.join(depRoot, 'Package.swift'),
739+
`${SCAFFOLDER_MARKER}\n// Cache slot: SLOT-A/debug\n// pre-versioning scaffold\n`,
740+
);
741+
const result = scaffoldPackageSwiftForDep(makeDep(), makeCtx());
742+
expect(result.status).toBe('written');
743+
const after = fs.readFileSync(path.join(depRoot, 'Package.swift'), 'utf8');
744+
expect(after).not.toContain('pre-versioning scaffold');
745+
expect(after).toContain(
746+
`// AUTO-SCAFFOLDED-VERSION: ${SCAFFOLDER_VERSION}`,
747+
);
748+
});
749+
750+
it('skips when the existing file is already at the current version and slot', () => {
751+
fs.writeFileSync(
752+
path.join(depRoot, 'Package.swift'),
753+
`${SCAFFOLDER_MARKER}\n// AUTO-SCAFFOLDED-VERSION: ${SCAFFOLDER_VERSION}\n// Cache slot: SLOT-A/debug\n`,
754+
);
755+
const result = scaffoldPackageSwiftForDep(makeDep(), makeCtx());
756+
expect(result.status).toBe('skipped-scaffolder-marker');
757+
});
758+
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
* @format
8+
* @noflow
9+
*/
10+
11+
'use strict';
12+
13+
// ---------------------------------------------------------------------------
14+
// SPM requires `// swift-tools-version: X.Y` on the FIRST LINE of Package.swift.
15+
// When the directive isn't on line 1, SPM silently treats the manifest as
16+
// tools-version 3.1.0 and recent Xcode rejects it outright:
17+
//
18+
// error: package 'package.swift' is using Swift tools version 3.1.0 which
19+
// is no longer supported; consider using '// swift-tools-version: 6.3'
20+
//
21+
// This regression test pins every Package.swift our generators emit to that
22+
// rule, and asserts the same for the static codegen template.
23+
// ---------------------------------------------------------------------------
24+
25+
const {
26+
generateAutolinkedPackageSwift,
27+
generateSynthPackageSwift,
28+
} = require('../generate-spm-autolinking');
29+
const {generateXCFrameworksPackageSwift} = require('../generate-spm-package');
30+
const {emitScaffoldedPackageSwift} = require('../scaffold-package-swift');
31+
const fs = require('fs');
32+
const path = require('path');
33+
34+
const TOOLS_VERSION_RE = /^\/\/ swift-tools-version: \d+\.\d+/;
35+
36+
function firstLineOf(s) {
37+
return s.split('\n', 1)[0];
38+
}
39+
40+
describe('swift-tools-version directive must be on line 1', () => {
41+
it('generateXCFrameworksPackageSwift (xcframeworks sub-package)', () => {
42+
const out = generateXCFrameworksPackageSwift(
43+
['React', 'ReactNativeDependencies', 'hermes-engine'],
44+
'/tmp/cache/0.85.3/debug',
45+
);
46+
expect(firstLineOf(out)).toMatch(TOOLS_VERSION_RE);
47+
});
48+
49+
it('generateAutolinkedPackageSwift (autolinker aggregator)', () => {
50+
const out = generateAutolinkedPackageSwift({});
51+
expect(firstLineOf(out)).toMatch(TOOLS_VERSION_RE);
52+
});
53+
54+
it('generateSynthPackageSwift (per-dep synth wrapper)', () => {
55+
const out = generateSynthPackageSwift({
56+
swiftName: 'MyDep',
57+
exclude: [],
58+
publicHeadersPath: '.',
59+
spmDependencies: [],
60+
hasReactDep: false,
61+
hasXcfwHeaders: false,
62+
hasDepsHeaders: false,
63+
codegenHeadersIncluded: false,
64+
});
65+
expect(firstLineOf(out)).toMatch(TOOLS_VERSION_RE);
66+
});
67+
68+
it('emitScaffoldedPackageSwift (community-lib scaffold)', () => {
69+
const out = emitScaffoldedPackageSwift({
70+
swiftName: 'foo',
71+
sources: [],
72+
headerSearchPaths: [],
73+
coreReactNative: false,
74+
siblingNames: [],
75+
extraFrameworks: [],
76+
weakFrameworks: [],
77+
compilerFlags: [],
78+
publicHeadersPath: null,
79+
resources: [],
80+
warnings: [],
81+
});
82+
expect(firstLineOf(out)).toMatch(TOOLS_VERSION_RE);
83+
});
84+
85+
it('codegen Package.swift.spm-template (static file)', () => {
86+
const templatePath = path.resolve(
87+
__dirname,
88+
'..',
89+
'..',
90+
'codegen',
91+
'templates',
92+
'Package.swift.spm-template',
93+
);
94+
const out = fs.readFileSync(templatePath, 'utf8');
95+
expect(firstLineOf(out)).toMatch(TOOLS_VERSION_RE);
96+
});
97+
});

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -722,12 +722,12 @@ function generateAutolinkedPackageSwift(
722722
const inlineDeclsBlock =
723723
inlineDecls.length > 0 ? `,\n${inlineDecls.join(',\n')}` : '';
724724

725-
return `// AUTO-GENERATED by scripts/generate-spm-autolinking.js – do not edit manually.
725+
return `// swift-tools-version: 6.0
726+
// AUTO-GENERATED by scripts/generate-spm-autolinking.js – do not edit manually.
726727
// Top-level Autolinked package. Every autolinked dep (npm or spmModule) is
727728
// referenced as .package(path: <dep-source-dir>) — each has its own synth
728729
// Package.swift written in-place. AutolinkedAggregate depends on every dep's
729730
// product so the app build pulls them all in.
730-
// swift-tools-version: 6.0
731731
732732
import PackageDescription
733733
import Foundation
@@ -948,9 +948,9 @@ function generateSynthPackageSwift(spec /*: SynthPackageSpec */) /*: string */ {
948948
: `let packageDir = URL(fileURLWithPath: #filePath).deletingLastPathComponent().path
949949
let appRoot = packageDir + "${spec.appRootRelativeToPackage ?? '/../../..'}"`;
950950

951-
return `// AUTO-GENERATED by scripts/generate-spm-autolinking.js – do not edit manually.
951+
return `// swift-tools-version: 6.0
952+
// AUTO-GENERATED by scripts/generate-spm-autolinking.js – do not edit manually.
952953
// Synth Package.swift for autolinked dep "${swiftName}".
953-
// swift-tools-version: 6.0
954954
955955
import PackageDescription
956956
import Foundation

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,9 @@ function generateXCFrameworksPackageSwift(
208208
slotComment = `// Cache slot: ${version}/${flavor}\n`;
209209
}
210210

211-
return `// AUTO-GENERATED by scripts/generate-spm-package.js – do not edit manually.
212-
${slotComment}// swift-tools-version: 6.0
213-
import PackageDescription
211+
return `// swift-tools-version: 6.0
212+
// AUTO-GENERATED by scripts/generate-spm-package.js – do not edit manually.
213+
${slotComment}import PackageDescription
214214
215215
let package = Package(
216216
name: "ReactNative",

0 commit comments

Comments
 (0)