Skip to content
Open
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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const {
scaffoldPackageSwiftForDep,
translatePodspecToSpmTarget,
} = require('../scaffold-package-swift');
const {RemoteVersionError} = require('../spm-utils');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
Expand Down Expand Up @@ -1017,6 +1018,130 @@ describe('scaffoldAll', () => {
'skipped-no-podspec',
);
});

function writeAutolinkingJson(dependencies) {
const autolinkingDir = path.join(appRoot, 'build/generated/autolinking');
fs.mkdirSync(autolinkingDir, {recursive: true});
fs.writeFileSync(
path.join(autolinkingDir, 'autolinking.json'),
JSON.stringify({dependencies}),
);
}

it('propagates a Swift name collision instead of scaffolding anyway, plugin or not', () => {
// 'react-headers' derives the reserved 'ReactHeaders', with no scope to
// borrow. Degrading to the direct deps would scaffold manifests SPM rejects
// later, and a plugin buys no exemption — `spm scaffold` has no plugin code.
const depRoot = path.join(appRoot, 'node_modules', 'react-headers');
fs.mkdirSync(depRoot, {recursive: true});
fs.writeFileSync(
path.join(depRoot, 'react-native.config.js'),
"module.exports = {spm: {autolinkingPlugin: './spm-plugin.js'}};\n",
);
writeAutolinkingJson({
'react-headers': {root: depRoot, platforms: {ios: {}}},
});
expect(() =>
scaffoldAll({appRoot, projectRoot: appRoot, reactNativeRoot: appRoot}),
).toThrow(/React Native reserves/);
});

it('still falls back to the direct deps when a transitive dep cannot be resolved', () => {
const depRoot = path.join(appRoot, 'node_modules', 'react-native-a');
fs.mkdirSync(depRoot, {recursive: true});
fs.writeFileSync(
path.join(depRoot, 'react-native.config.js'),
"module.exports = {spm: {dependencies: ['ghost-dep-that-is-not-installed']}};\n",
);
writeAutolinkingJson({
'react-native-a': {root: depRoot, platforms: {ios: {}}},
});
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
try {
const results = scaffoldAll({
appRoot,
projectRoot: appRoot,
reactNativeRoot: appRoot,
});
expect(results.map(r => r.depName)).toEqual(['react-native-a']);
expect(logSpy.mock.calls.map(call => call.join(' ')).join('\n')).toMatch(
/Transitive spm\.dependencies expansion failed/,
);
} finally {
logSpy.mockRestore();
}
});

it('emits the remote package reference for every dep it scaffolds', () => {
const depRoot = path.join(appRoot, 'node_modules', 'react-native-foo');
fs.mkdirSync(path.join(depRoot, 'ios'), {recursive: true});
fs.writeFileSync(path.join(depRoot, 'ios', 'Foo.mm'), '// native\n');
fs.writeFileSync(
path.join(depRoot, 'react-native-foo.podspec'),
'Pod::Spec.new do |s|\n' +
' s.name = "react-native-foo"\n' +
' s.version = "1.0"\n' +
' s.source_files = "ios/**/*.{h,m,mm}"\n' +
' s.dependency "React-Core"\n' +
'end\n',
);
writeAutolinkingJson({
'react-native-foo': {root: depRoot, platforms: {ios: {}}},
});
const prevUrl = process.env.RN_SPM_REMOTE_URL;
const prevVersion = process.env.RN_SPM_REMOTE_VERSION;
process.env.RN_SPM_REMOTE_URL = 'https://example.com/rn.git';
process.env.RN_SPM_REMOTE_VERSION = '9.9.9';
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
try {
const results = scaffoldAll({
appRoot,
projectRoot: appRoot,
reactNativeRoot: appRoot,
});
expect(results.map(r => r.status)).toEqual(['written']);
const manifest = fs.readFileSync(
path.join(depRoot, 'Package.swift'),
'utf8',
);
expect(manifest).toContain(
'.package(url: "https://example.com/rn.git", exact: "9.9.9")',
);
expect(manifest).not.toContain('.package(name: "ReactNative"');
} finally {
logSpy.mockRestore();
if (prevUrl == null) delete process.env.RN_SPM_REMOTE_URL;
else process.env.RN_SPM_REMOTE_URL = prevUrl;
if (prevVersion == null) delete process.env.RN_SPM_REMOTE_VERSION;
else process.env.RN_SPM_REMOTE_VERSION = prevVersion;
}
});

it('propagates a RemoteVersionError from the remote package config', () => {
writeAutolinkingJson({
'react-native-a': {root: '/no/such/a', platforms: {ios: {}}},
});
const prevUrl = process.env.RN_SPM_REMOTE_URL;
const prevVersion = process.env.RN_SPM_REMOTE_VERSION;
process.env.RN_SPM_REMOTE_URL = 'https://example.com/react-native-spm.git';
delete process.env.RN_SPM_REMOTE_VERSION;
try {
// No react-native under the temp appRoot, so no version resolves — the
// author must see that, not have it degraded into "expansion failed".
expect(() =>
scaffoldAll({appRoot, projectRoot: appRoot, reactNativeRoot: appRoot}),
).toThrow(RemoteVersionError);
} finally {
if (prevUrl == null) {
delete process.env.RN_SPM_REMOTE_URL;
} else {
process.env.RN_SPM_REMOTE_URL = prevUrl;
}
if (prevVersion != null) {
process.env.RN_SPM_REMOTE_VERSION = prevVersion;
}
}
});
});

// ---------------------------------------------------------------------------
Expand Down
37 changes: 34 additions & 3 deletions packages/react-native/scripts/spm/__tests__/spm-utils-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const {
REACT_NATIVE_UMBRELLA_PRODUCT,
REACT_NATIVE_XCFRAMEWORK_PRODUCTS,
RemoteVersionError,
RESERVED_SWIFT_NAMES,
buildPerAppHeaderTree,
defaultCacheDir,
displayPath,
Expand Down Expand Up @@ -57,11 +58,10 @@ describe('toSwiftName', () => {
});

// ---------------------------------------------------------------------------
// Name constants — the single list every generated manifest derives its
// package and product names from
// Reserved Swift names — the one list the manifests and the guard both use
// ---------------------------------------------------------------------------

describe('name constants', () => {
describe('reserved Swift names', () => {
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');
Expand Down Expand Up @@ -97,11 +97,42 @@ describe('name constants', () => {
expect(REACT_HEADERS_TARGET_DIR).toBe('ReactHeadersTarget');
});

// The one test that pins the literal strings: every other check compares
// constants to constants, so this is what would catch a rename.
it('reserves exactly the names React Native puts in a manifest', () => {
expect([...RESERVED_SWIFT_NAMES].sort()).toEqual([
'Autolinked',
'React-GeneratedCode',
'ReactAppDependencyProvider',
'ReactAppHeaders',
'ReactCodegen',
'ReactHeaders',
'ReactNative',
'ReactNativeDependenciesHeaders',
'ReactNativeHeaders',
]);
});

it('holds names that real generated manifests actually use', () => {
const {
generateXCFrameworksPackageSwift,
} = require('../generate-spm-package');
const manifest = generateXCFrameworksPackageSwift();
for (const name of [REACT_NATIVE_PACKAGE_NAME, ...REACT_NATIVE_PRODUCTS]) {
expect(manifest).toContain(`"${name}"`);
}
});

it('does NOT reserve the headers target dir — target names only have to be unique within their own package', () => {
expect(RESERVED_SWIFT_NAMES).not.toContain(REACT_HEADERS_TARGET_DIR);
});

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,
RESERVED_SWIFT_NAMES,
]) {
expect(Array.isArray(list)).toBe(true);
expect(Object.isFrozen(list)).toBe(true);
Expand Down
Loading
Loading