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 numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,59 @@ const path = require('node:path');
// generateXCFrameworksPackageSwift
// ---------------------------------------------------------------------------

// Adding a product must be one edit to REACT_NATIVE_PRODUCTS (plus the static
// codegen template): the manifest's products AND targets both derive from it.
// ReactHeaders is the Clang umbrella target; every other product is served by
// an xcframework of the same name.
describe('generateXCFrameworksPackageSwift derives from the shared name constants', () => {
// jest.doMock registers in the module registry beyond the isolateModules
// scope, so the mocked constants must be dropped before the next test.
afterEach(() => {
jest.dontMock('../spm-utils');
jest.resetModules();
});

it('emits a product and a binary target for a newly reserved product', () => {
jest.isolateModules(() => {
// Declared by KIND, the way a real edit adds one — spm-utils derives the
// flat list from the kind lists, so the mock mirrors that derivation.
jest.doMock('../spm-utils', () => {
const actual = jest.requireActual('../spm-utils');
const xcframeworkProducts = Object.freeze([
...actual.REACT_NATIVE_XCFRAMEWORK_PRODUCTS,
'ReactBrandNewHeaders',
]);
return {
...actual,
REACT_NATIVE_XCFRAMEWORK_PRODUCTS: xcframeworkProducts,
REACT_NATIVE_PRODUCTS: Object.freeze([
actual.REACT_NATIVE_UMBRELLA_PRODUCT,
...xcframeworkProducts,
]),
};
});
const {
generateXCFrameworksPackageSwift: generate,
} = require('../generate-spm-package');
const out = generate();
expect(out).toContain(
'.library(name: "ReactBrandNewHeaders", targets: ["ReactBrandNewHeaders"])',
);
expect(out).toContain('path: "ReactBrandNewHeaders.xcframework"');
});
});

it('emits one library product per REACT_NATIVE_PRODUCTS entry, in order', () => {
const {REACT_NATIVE_PRODUCTS} = require('../spm-utils');
const libraries = [
...generateXCFrameworksPackageSwift().matchAll(
/\.library\(name: "([^"]+)"/g,
),
].map(m => m[1]);
expect(libraries).toEqual([...REACT_NATIVE_PRODUCTS]);
});
});

describe('generateXCFrameworksPackageSwift', () => {
it('exposes only invariant compile-time products', () => {
const result = generateXCFrameworksPackageSwift();
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,55 @@ const FRAMEWORK = {
],
};

// The pbxproj product references must follow the shared name constants: a
// product added there has to reach the app target, or the app links against a
// package product Xcode never references.
describe('SPM product references derive from the shared name constants', () => {
// jest.doMock registers in the module registry beyond the isolateModules
// scope, so the mocked constants must be dropped before the next test.
afterEach(() => {
jest.dontMock('../spm-utils');
jest.resetModules();
});

it('includes a newly reserved React Native product', () => {
jest.isolateModules(() => {
jest.doMock('../spm-utils', () => {
const actual = jest.requireActual('../spm-utils');
return {
...actual,
REACT_NATIVE_PRODUCTS: Object.freeze([
...actual.REACT_NATIVE_PRODUCTS,
'ReactBrandNewHeaders',
]),
};
});
const {buildSpmDependencyGraph} = require('../generate-spm-xcodeproj');
const graph = buildSpmDependencyGraph(
(section, id) => `${section}:${id}`,
);
expect(graph.products.map(p => p.product)).toContain(
'ReactBrandNewHeaders',
);
});
});

it('references every React Native, aggregator and codegen product exactly once', () => {
const {
AUTOLINKED_PACKAGE_NAME,
REACT_CODEGEN_APP_PRODUCTS,
REACT_NATIVE_PRODUCTS,
} = require('../spm-utils');
const {buildSpmDependencyGraph} = require('../generate-spm-xcodeproj');
const graph = buildSpmDependencyGraph((section, id) => `${section}:${id}`);
expect(graph.products.map(p => p.product)).toEqual([
...REACT_NATIVE_PRODUCTS,
AUTOLINKED_PACKAGE_NAME,
...REACT_CODEGEN_APP_PRODUCTS,
]);
});
});

describe('scheme pre-action', () => {
it('contains the sync script and target-scoped build environment', () => {
const result = generateXcscheme(
Expand Down
63 changes: 63 additions & 0 deletions packages/react-native/scripts/spm/__tests__/spm-utils-test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,16 @@
'use strict';

const {
AUTOLINKED_PACKAGE_NAME,
REACT_CODEGEN_APP_PRODUCTS,
REACT_CODEGEN_PACKAGE_NAME,
REACT_CODEGEN_PRODUCTS,
REACT_HEADERS_TARGET_DIR,
REACT_NATIVE_HEADERS_PRODUCT,
REACT_NATIVE_PACKAGE_NAME,
REACT_NATIVE_PRODUCTS,
REACT_NATIVE_UMBRELLA_PRODUCT,
REACT_NATIVE_XCFRAMEWORK_PRODUCTS,
RemoteVersionError,
buildPerAppHeaderTree,
defaultCacheDir,
Expand DownExpand Up@@ -46,6 +56,59 @@ describe('toSwiftName', () => {
});
});

// ---------------------------------------------------------------------------
// Name constants — the single list every generated manifest derives its
// package and product names from
// ---------------------------------------------------------------------------

describe('name constants', () => {
it('names the React Native package and the per-app codegen package', () => {
expect(REACT_NATIVE_PACKAGE_NAME).toBe('ReactNative');
expect(REACT_CODEGEN_PACKAGE_NAME).toBe('React-GeneratedCode');
});

it('pins each product list to its literal names', () => {
expect(REACT_NATIVE_PRODUCTS).toEqual([
'ReactHeaders',
'ReactNativeHeaders',
'ReactNativeDependenciesHeaders',
]);
expect(REACT_CODEGEN_PRODUCTS).toEqual(['ReactAppHeaders']);
expect(REACT_CODEGEN_APP_PRODUCTS).toEqual([
'ReactCodegen',
'ReactAppDependencyProvider',
]);
});

it('tags each React Native product by kind, so no consumer has to infer it from position', () => {
expect(REACT_NATIVE_UMBRELLA_PRODUCT).toBe('ReactHeaders');
expect(REACT_NATIVE_HEADERS_PRODUCT).toBe('ReactNativeHeaders');
expect(REACT_NATIVE_XCFRAMEWORK_PRODUCTS).toEqual([
'ReactNativeHeaders',
'ReactNativeDependenciesHeaders',
]);
});

it('names the autolinking aggregator package (which shares its name with its product)', () => {
expect(AUTOLINKED_PACKAGE_NAME).toBe('Autolinked');
});

it('names the invariant React headers target directory', () => {
expect(REACT_HEADERS_TARGET_DIR).toBe('ReactHeadersTarget');
});

it('freezes the lists so no caller can mutate the shared source of truth', () => {
for (const list of [
REACT_NATIVE_PRODUCTS,
REACT_CODEGEN_PRODUCTS,
REACT_CODEGEN_APP_PRODUCTS,
]) {
expect(Array.isArray(list)).toBe(true);
expect(Object.isFrozen(list)).toBe(true);
}
});
});

// ---------------------------------------------------------------------------
// defaultCacheDir
// ---------------------------------------------------------------------------
Expand Down
34 changes: 21 additions & 13 deletions packages/react-native/scripts/spm/generate-spm-autolinking.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,6 +65,11 @@ const {
} = require('./expand-spm-dependencies');
const {readPodspec} = require('./read-podspec');
const {
AUTOLINKED_PACKAGE_NAME,
REACT_CODEGEN_PACKAGE_NAME,
REACT_CODEGEN_PRODUCTS,
REACT_NATIVE_PACKAGE_NAME,
REACT_NATIVE_PRODUCTS,
RemoteVersionError,
findProjectRoot,
makeLogger,
Expand All@@ -90,7 +95,7 @@ const {log, warn} = makeLogger('generate-spm-autolinking');
let remoteCfg /*: ?{url: string, version: string, identity: string} */ = null;

function reactNativePackageLabel() /*: string */ {
return remoteCfg != null ? remoteCfg.identity : 'ReactNative';
return remoteCfg != null ? remoteCfg.identity : REACT_NATIVE_PACKAGE_NAME;
}
function reactNativePackageDecl(localDecl /*: string */) /*: string */ {
return remoteCfg != null
Expand All@@ -105,10 +110,11 @@ function reactNativePackageDecl(localDecl /*: string */) /*: string */ {
function reactProducts() /*: Array<{name: string, package: string}> */ {
const rn = reactNativePackageLabel();
return [
{name: 'ReactHeaders', package: rn},
{name: 'ReactNativeHeaders', package: rn},
{name: 'ReactNativeDependenciesHeaders', package: rn},
{name: 'ReactAppHeaders', package: 'React-GeneratedCode'},
...REACT_NATIVE_PRODUCTS.map(name => ({name, package: rn})),
...REACT_CODEGEN_PRODUCTS.map(name => ({
name,
package: REACT_CODEGEN_PACKAGE_NAME,
})),
];
}
function reactProductDeps() /*: string */ {
Expand DownExpand Up@@ -147,7 +153,7 @@ function reactDescriptor(
};
} else if (absXcframeworks != null) {
packageRef = {
name: 'ReactNative',
name: REACT_NATIVE_PACKAGE_NAME,
path: toPosix(absXcframeworks),
relPath:
xcframeworksRelPath != null ? toPosix(xcframeworksRelPath) : undefined,
Expand All@@ -156,7 +162,7 @@ function reactDescriptor(
return null;
}
const products = reactProducts().filter(
p => p.package !== 'React-GeneratedCode' || codegenPackageExists,
p => p.package !== REACT_CODEGEN_PACKAGE_NAME || codegenPackageExists,
);
return {packageRef, products};
}
Expand DownExpand Up@@ -900,12 +906,14 @@ function generateAutolinkedPackageSwift(
) {
packageDeps.push(
reactNativePackageDecl(
`.package(name: "ReactNative", path: "${xcframeworksRelPath}")`,
`.package(name: "${REACT_NATIVE_PACKAGE_NAME}", path: "${xcframeworksRelPath}")`,
),
);
// Per-app generated headers come from the ReactAppHeaders product in
// the codegen package (sibling of the autolinking dir).
packageDeps.push(`.package(name: "React-GeneratedCode", path: "../ios")`);
packageDeps.push(
`.package(name: "${REACT_CODEGEN_PACKAGE_NAME}", path: "../ios")`,
);
}

// AutolinkedAggregate's target dependencies: .product(...) for npm sub-package
Expand DownExpand Up@@ -1008,10 +1016,10 @@ import PackageDescription
import Foundation

${guardBlock}let package = Package(
name: "Autolinked",
name: "${AUTOLINKED_PACKAGE_NAME}",
platforms: [.iOS(.v15)],
products: [
.library(name: "Autolinked", targets: ["AutolinkedAggregate"]),
.library(name: "${AUTOLINKED_PACKAGE_NAME}", targets: ["AutolinkedAggregate"]),
],
${packageDepsBlock} targets: [
.target(
Expand DownExpand Up@@ -1075,13 +1083,13 @@ function generateSynthPackageSwift(spec /*: SynthPackageSpec */) /*: string */ {
spec.codegenPackagePath ?? '../../../ios';
packageDeps.push(
reactNativePackageDecl(
`.package(name: "ReactNative", path: "${reactNativePackagePath}")`,
`.package(name: "${REACT_NATIVE_PACKAGE_NAME}", path: "${reactNativePackagePath}")`,
),
);
// Per-app generated headers come from the ReactAppHeaders product in
// the codegen package.
packageDeps.push(
`.package(name: "React-GeneratedCode", path: "${codegenPackagePath}")`,
`.package(name: "${REACT_CODEGEN_PACKAGE_NAME}", path: "${codegenPackagePath}")`,
);
}
for (const dep of spmDependencies) {
Expand Down
48 changes: 30 additions & 18 deletions packages/react-native/scripts/spm/generate-spm-package.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,12 @@

const {prepareFlavoredFrameworks} = require('./flavored-frameworks');
const {
REACT_HEADERS_TARGET_DIR,
REACT_NATIVE_HEADERS_PRODUCT,
REACT_NATIVE_PACKAGE_NAME,
REACT_NATIVE_PRODUCTS,
REACT_NATIVE_UMBRELLA_PRODUCT,
REACT_NATIVE_XCFRAMEWORK_PRODUCTS,
deriveAppName,
displayPath,
findProjectRoot,
Expand DownExpand Up@@ -166,32 +172,38 @@ function findSourcePath(
* Package.swift also imports it as a named package dependency.
*/
function generateXCFrameworksPackageSwift() /*: string */ {
// Each product's target follows from its KIND, not from its position in the
// list: the umbrella is a Clang target over the staged headers, and every
// xcframework-backed product gets a binaryTarget of the same name.
const products = REACT_NATIVE_PRODUCTS.map(
product => ` .library(name: "${product}", targets: ["${product}"]),`,
);
const targets = [
` .target(
name: "${REACT_NATIVE_UMBRELLA_PRODUCT}",
dependencies: ["${REACT_NATIVE_HEADERS_PRODUCT}"],
path: "${REACT_HEADERS_TARGET_DIR}",
publicHeadersPath: "include"
),`,
...REACT_NATIVE_XCFRAMEWORK_PRODUCTS.map(
product => ` .binaryTarget(
name: "${product}",
path: "${product}.xcframework"
),`,
),
];

return `// swift-tools-version: 6.0
// AUTO-GENERATED by scripts/generate-spm-package.js – do not edit manually.
import PackageDescription

let package = Package(
name: "ReactNative",
name: "${REACT_NATIVE_PACKAGE_NAME}",
products: [
.library(name: "ReactHeaders", targets: ["ReactHeaders"]),
.library(name: "ReactNativeHeaders", targets: ["ReactNativeHeaders"]),
.library(name: "ReactNativeDependenciesHeaders", targets: ["ReactNativeDependenciesHeaders"]),
${products.join('\n')}
],
targets: [
.target(
name: "ReactHeaders",
dependencies: ["ReactNativeHeaders"],
path: "ReactHeadersTarget",
publicHeadersPath: "include"
),
.binaryTarget(
name: "ReactNativeHeaders",
path: "ReactNativeHeaders.xcframework"
),
.binaryTarget(
name: "ReactNativeDependenciesHeaders",
path: "ReactNativeDependenciesHeaders.xcframework"
),
${targets.join('\n')}
]
)
`;
Expand Down
Loading
Loading