Skip to content

Commit cdb180c

Browse files
chrfalchcipolleschi
authored andcommitted
Derive the generated SPM manifests from shared name constants (#58035)
Summary: After community feedback from this one powersync-ja/powersync-js#1076 we've decided to fix the issue of clashing name constants - to avoid the SwiftPM pipeline scaffolding packages that collides with built-in packages. This PR is the first step fixing this - refactoring the names of internal packages into constants. ## Changelog: [IOS] [FIXED] - Created constants for internal SwiftPM packages so that we can create guards to avoid collisions Pull Request resolved: #58035 Test Plan: ✅ Unit tests Reviewed By: Abbondanzo Differential Revision: D116936194 Pulled By: cipolleschi fbshipit-source-id: 2882e512489cc4784a09aeb03769e97d64d286f7 (cherry picked from commit d36aa49)
1 parent f4cdbc4 commit cdb180c

8 files changed

Lines changed: 310 additions & 71 deletions

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,59 @@ const path = require('path');
2323
// generateXCFrameworksPackageSwift
2424
// ---------------------------------------------------------------------------
2525

26+
// Adding a product must be one edit to REACT_NATIVE_PRODUCTS (plus the static
27+
// codegen template): the manifest's products AND targets both derive from it.
28+
// ReactHeaders is the Clang umbrella target; every other product is served by
29+
// an xcframework of the same name.
30+
describe('generateXCFrameworksPackageSwift derives from the shared name constants', () => {
31+
// jest.doMock registers in the module registry beyond the isolateModules
32+
// scope, so the mocked constants must be dropped before the next test.
33+
afterEach(() => {
34+
jest.dontMock('../spm-utils');
35+
jest.resetModules();
36+
});
37+
38+
it('emits a product and a binary target for a newly reserved product', () => {
39+
jest.isolateModules(() => {
40+
// Declared by KIND, the way a real edit adds one — spm-utils derives the
41+
// flat list from the kind lists, so the mock mirrors that derivation.
42+
jest.doMock('../spm-utils', () => {
43+
const actual = jest.requireActual('../spm-utils');
44+
const xcframeworkProducts = Object.freeze([
45+
...actual.REACT_NATIVE_XCFRAMEWORK_PRODUCTS,
46+
'ReactBrandNewHeaders',
47+
]);
48+
return {
49+
...actual,
50+
REACT_NATIVE_XCFRAMEWORK_PRODUCTS: xcframeworkProducts,
51+
REACT_NATIVE_PRODUCTS: Object.freeze([
52+
actual.REACT_NATIVE_UMBRELLA_PRODUCT,
53+
...xcframeworkProducts,
54+
]),
55+
};
56+
});
57+
const {
58+
generateXCFrameworksPackageSwift: generate,
59+
} = require('../generate-spm-package');
60+
const out = generate();
61+
expect(out).toContain(
62+
'.library(name: "ReactBrandNewHeaders", targets: ["ReactBrandNewHeaders"])',
63+
);
64+
expect(out).toContain('path: "ReactBrandNewHeaders.xcframework"');
65+
});
66+
});
67+
68+
it('emits one library product per REACT_NATIVE_PRODUCTS entry, in order', () => {
69+
const {REACT_NATIVE_PRODUCTS} = require('../spm-utils');
70+
const libraries = [
71+
...generateXCFrameworksPackageSwift().matchAll(
72+
/\.library\(name: "([^"]+)"/g,
73+
),
74+
].map(m => m[1]);
75+
expect(libraries).toEqual([...REACT_NATIVE_PRODUCTS]);
76+
});
77+
});
78+
2679
describe('generateXCFrameworksPackageSwift', () => {
2780
it('exposes only invariant compile-time products', () => {
2881
const result = generateXCFrameworksPackageSwift();

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,55 @@ const FRAMEWORK = {
6161
],
6262
};
6363

64+
// The pbxproj product references must follow the shared name constants: a
65+
// product added there has to reach the app target, or the app links against a
66+
// package product Xcode never references.
67+
describe('SPM product references derive from the shared name constants', () => {
68+
// jest.doMock registers in the module registry beyond the isolateModules
69+
// scope, so the mocked constants must be dropped before the next test.
70+
afterEach(() => {
71+
jest.dontMock('../spm-utils');
72+
jest.resetModules();
73+
});
74+
75+
it('includes a newly reserved React Native product', () => {
76+
jest.isolateModules(() => {
77+
jest.doMock('../spm-utils', () => {
78+
const actual = jest.requireActual('../spm-utils');
79+
return {
80+
...actual,
81+
REACT_NATIVE_PRODUCTS: Object.freeze([
82+
...actual.REACT_NATIVE_PRODUCTS,
83+
'ReactBrandNewHeaders',
84+
]),
85+
};
86+
});
87+
const {buildSpmDependencyGraph} = require('../generate-spm-xcodeproj');
88+
const graph = buildSpmDependencyGraph(
89+
(section, id) => `${section}:${id}`,
90+
);
91+
expect(graph.products.map(p => p.product)).toContain(
92+
'ReactBrandNewHeaders',
93+
);
94+
});
95+
});
96+
97+
it('references every React Native, aggregator and codegen product exactly once', () => {
98+
const {
99+
AUTOLINKED_PACKAGE_NAME,
100+
REACT_CODEGEN_APP_PRODUCTS,
101+
REACT_NATIVE_PRODUCTS,
102+
} = require('../spm-utils');
103+
const {buildSpmDependencyGraph} = require('../generate-spm-xcodeproj');
104+
const graph = buildSpmDependencyGraph((section, id) => `${section}:${id}`);
105+
expect(graph.products.map(p => p.product)).toEqual([
106+
...REACT_NATIVE_PRODUCTS,
107+
AUTOLINKED_PACKAGE_NAME,
108+
...REACT_CODEGEN_APP_PRODUCTS,
109+
]);
110+
});
111+
});
112+
64113
describe('scheme pre-action', () => {
65114
it('contains the sync script and target-scoped build environment', () => {
66115
const result = generateXcscheme(

‎packages/react-native/scripts/spm/__tests__/spm-utils-test.js‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@
1111
'use strict';
1212

1313
const {
14+
AUTOLINKED_PACKAGE_NAME,
15+
REACT_CODEGEN_APP_PRODUCTS,
16+
REACT_CODEGEN_PACKAGE_NAME,
17+
REACT_CODEGEN_PRODUCTS,
18+
REACT_HEADERS_TARGET_DIR,
19+
REACT_NATIVE_HEADERS_PRODUCT,
20+
REACT_NATIVE_PACKAGE_NAME,
21+
REACT_NATIVE_PRODUCTS,
22+
REACT_NATIVE_UMBRELLA_PRODUCT,
23+
REACT_NATIVE_XCFRAMEWORK_PRODUCTS,
1424
RemoteVersionError,
1525
buildPerAppHeaderTree,
1626
defaultCacheDir,
@@ -46,6 +56,59 @@ describe('toSwiftName', () => {
4656
});
4757
});
4858

59+
// ---------------------------------------------------------------------------
60+
// Name constants — the single list every generated manifest derives its
61+
// package and product names from
62+
// ---------------------------------------------------------------------------
63+
64+
describe('name constants', () => {
65+
it('names the React Native package and the per-app codegen package', () => {
66+
expect(REACT_NATIVE_PACKAGE_NAME).toBe('ReactNative');
67+
expect(REACT_CODEGEN_PACKAGE_NAME).toBe('React-GeneratedCode');
68+
});
69+
70+
it('pins each product list to its literal names', () => {
71+
expect(REACT_NATIVE_PRODUCTS).toEqual([
72+
'ReactHeaders',
73+
'ReactNativeHeaders',
74+
'ReactNativeDependenciesHeaders',
75+
]);
76+
expect(REACT_CODEGEN_PRODUCTS).toEqual(['ReactAppHeaders']);
77+
expect(REACT_CODEGEN_APP_PRODUCTS).toEqual([
78+
'ReactCodegen',
79+
'ReactAppDependencyProvider',
80+
]);
81+
});
82+
83+
it('tags each React Native product by kind, so no consumer has to infer it from position', () => {
84+
expect(REACT_NATIVE_UMBRELLA_PRODUCT).toBe('ReactHeaders');
85+
expect(REACT_NATIVE_HEADERS_PRODUCT).toBe('ReactNativeHeaders');
86+
expect(REACT_NATIVE_XCFRAMEWORK_PRODUCTS).toEqual([
87+
'ReactNativeHeaders',
88+
'ReactNativeDependenciesHeaders',
89+
]);
90+
});
91+
92+
it('names the autolinking aggregator package (which shares its name with its product)', () => {
93+
expect(AUTOLINKED_PACKAGE_NAME).toBe('Autolinked');
94+
});
95+
96+
it('names the invariant React headers target directory', () => {
97+
expect(REACT_HEADERS_TARGET_DIR).toBe('ReactHeadersTarget');
98+
});
99+
100+
it('freezes the lists so no caller can mutate the shared source of truth', () => {
101+
for (const list of [
102+
REACT_NATIVE_PRODUCTS,
103+
REACT_CODEGEN_PRODUCTS,
104+
REACT_CODEGEN_APP_PRODUCTS,
105+
]) {
106+
expect(Array.isArray(list)).toBe(true);
107+
expect(Object.isFrozen(list)).toBe(true);
108+
}
109+
});
110+
});
111+
49112
// ---------------------------------------------------------------------------
50113
// defaultCacheDir
51114
// ---------------------------------------------------------------------------

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

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ const {
6565
} = require('./expand-spm-dependencies');
6666
const {readPodspec} = require('./read-podspec');
6767
const {
68+
AUTOLINKED_PACKAGE_NAME,
69+
REACT_CODEGEN_PACKAGE_NAME,
70+
REACT_CODEGEN_PRODUCTS,
71+
REACT_NATIVE_PACKAGE_NAME,
72+
REACT_NATIVE_PRODUCTS,
6873
RemoteVersionError,
6974
findProjectRoot,
7075
makeLogger,
@@ -90,7 +95,7 @@ const {log, warn} = makeLogger('generate-spm-autolinking');
9095
let remoteCfg /*: ?{url: string, version: string, identity: string} */ = null;
9196

9297
function reactNativePackageLabel() /*: string */ {
93-
return remoteCfg != null ? remoteCfg.identity : 'ReactNative';
98+
return remoteCfg != null ? remoteCfg.identity : REACT_NATIVE_PACKAGE_NAME;
9499
}
95100
function reactNativePackageDecl(localDecl /*: string */) /*: string */ {
96101
return remoteCfg != null
@@ -105,10 +110,11 @@ function reactNativePackageDecl(localDecl /*: string */) /*: string */ {
105110
function reactProducts() /*: Array<{name: string, package: string}> */ {
106111
const rn = reactNativePackageLabel();
107112
return [
108-
{name: 'ReactHeaders', package: rn},
109-
{name: 'ReactNativeHeaders', package: rn},
110-
{name: 'ReactNativeDependenciesHeaders', package: rn},
111-
{name: 'ReactAppHeaders', package: 'React-GeneratedCode'},
113+
...REACT_NATIVE_PRODUCTS.map(name => ({name, package: rn})),
114+
...REACT_CODEGEN_PRODUCTS.map(name => ({
115+
name,
116+
package: REACT_CODEGEN_PACKAGE_NAME,
117+
})),
112118
];
113119
}
114120
function reactProductDeps() /*: string */ {
@@ -147,7 +153,7 @@ function reactDescriptor(
147153
};
148154
} else if (absXcframeworks != null) {
149155
packageRef = {
150-
name: 'ReactNative',
156+
name: REACT_NATIVE_PACKAGE_NAME,
151157
path: toPosix(absXcframeworks),
152158
relPath:
153159
xcframeworksRelPath != null ? toPosix(xcframeworksRelPath) : undefined,
@@ -156,7 +162,7 @@ function reactDescriptor(
156162
return null;
157163
}
158164
const products = reactProducts().filter(
159-
p => p.package !== 'React-GeneratedCode' || codegenPackageExists,
165+
p => p.package !== REACT_CODEGEN_PACKAGE_NAME || codegenPackageExists,
160166
);
161167
return {packageRef, products};
162168
}
@@ -900,12 +906,14 @@ function generateAutolinkedPackageSwift(
900906
) {
901907
packageDeps.push(
902908
reactNativePackageDecl(
903-
`.package(name: "ReactNative", path: "${xcframeworksRelPath}")`,
909+
`.package(name: "${REACT_NATIVE_PACKAGE_NAME}", path: "${xcframeworksRelPath}")`,
904910
),
905911
);
906912
// Per-app generated headers come from the ReactAppHeaders product in
907913
// the codegen package (sibling of the autolinking dir).
908-
packageDeps.push(`.package(name: "React-GeneratedCode", path: "../ios")`);
914+
packageDeps.push(
915+
`.package(name: "${REACT_CODEGEN_PACKAGE_NAME}", path: "../ios")`,
916+
);
909917
}
910918

911919
// AutolinkedAggregate's target dependencies: .product(...) for npm sub-package
@@ -1008,10 +1016,10 @@ import PackageDescription
10081016
import Foundation
10091017
10101018
${guardBlock}let package = Package(
1011-
name: "Autolinked",
1019+
name: "${AUTOLINKED_PACKAGE_NAME}",
10121020
platforms: [.iOS(.v15)],
10131021
products: [
1014-
.library(name: "Autolinked", targets: ["AutolinkedAggregate"]),
1022+
.library(name: "${AUTOLINKED_PACKAGE_NAME}", targets: ["AutolinkedAggregate"]),
10151023
],
10161024
${packageDepsBlock} targets: [
10171025
.target(
@@ -1075,13 +1083,13 @@ function generateSynthPackageSwift(spec /*: SynthPackageSpec */) /*: string */ {
10751083
spec.codegenPackagePath ?? '../../../ios';
10761084
packageDeps.push(
10771085
reactNativePackageDecl(
1078-
`.package(name: "ReactNative", path: "${reactNativePackagePath}")`,
1086+
`.package(name: "${REACT_NATIVE_PACKAGE_NAME}", path: "${reactNativePackagePath}")`,
10791087
),
10801088
);
10811089
// Per-app generated headers come from the ReactAppHeaders product in
10821090
// the codegen package.
10831091
packageDeps.push(
1084-
`.package(name: "React-GeneratedCode", path: "${codegenPackagePath}")`,
1092+
`.package(name: "${REACT_CODEGEN_PACKAGE_NAME}", path: "${codegenPackagePath}")`,
10851093
);
10861094
}
10871095
for (const dep of spmDependencies) {

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

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@
3737

3838
const {prepareFlavoredFrameworks} = require('./flavored-frameworks');
3939
const {
40+
REACT_HEADERS_TARGET_DIR,
41+
REACT_NATIVE_HEADERS_PRODUCT,
42+
REACT_NATIVE_PACKAGE_NAME,
43+
REACT_NATIVE_PRODUCTS,
44+
REACT_NATIVE_UMBRELLA_PRODUCT,
45+
REACT_NATIVE_XCFRAMEWORK_PRODUCTS,
4046
deriveAppName,
4147
displayPath,
4248
findProjectRoot,
@@ -166,32 +172,38 @@ function findSourcePath(
166172
* Package.swift also imports it as a named package dependency.
167173
*/
168174
function generateXCFrameworksPackageSwift() /*: string */ {
175+
// Each product's target follows from its KIND, not from its position in the
176+
// list: the umbrella is a Clang target over the staged headers, and every
177+
// xcframework-backed product gets a binaryTarget of the same name.
178+
const products = REACT_NATIVE_PRODUCTS.map(
179+
product => ` .library(name: "${product}", targets: ["${product}"]),`,
180+
);
181+
const targets = [
182+
` .target(
183+
name: "${REACT_NATIVE_UMBRELLA_PRODUCT}",
184+
dependencies: ["${REACT_NATIVE_HEADERS_PRODUCT}"],
185+
path: "${REACT_HEADERS_TARGET_DIR}",
186+
publicHeadersPath: "include"
187+
),`,
188+
...REACT_NATIVE_XCFRAMEWORK_PRODUCTS.map(
189+
product => ` .binaryTarget(
190+
name: "${product}",
191+
path: "${product}.xcframework"
192+
),`,
193+
),
194+
];
195+
169196
return `// swift-tools-version: 6.0
170197
// AUTO-GENERATED by scripts/generate-spm-package.js – do not edit manually.
171198
import PackageDescription
172199
173200
let package = Package(
174-
name: "ReactNative",
201+
name: "${REACT_NATIVE_PACKAGE_NAME}",
175202
products: [
176-
.library(name: "ReactHeaders", targets: ["ReactHeaders"]),
177-
.library(name: "ReactNativeHeaders", targets: ["ReactNativeHeaders"]),
178-
.library(name: "ReactNativeDependenciesHeaders", targets: ["ReactNativeDependenciesHeaders"]),
203+
${products.join('\n')}
179204
],
180205
targets: [
181-
.target(
182-
name: "ReactHeaders",
183-
dependencies: ["ReactNativeHeaders"],
184-
path: "ReactHeadersTarget",
185-
publicHeadersPath: "include"
186-
),
187-
.binaryTarget(
188-
name: "ReactNativeHeaders",
189-
path: "ReactNativeHeaders.xcframework"
190-
),
191-
.binaryTarget(
192-
name: "ReactNativeDependenciesHeaders",
193-
path: "ReactNativeDependenciesHeaders.xcframework"
194-
),
206+
${targets.join('\n')}
195207
]
196208
)
197209
`;

0 commit comments

Comments
 (0)