Skip to content
Draft
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
54 changes: 51 additions & 3 deletions packages/react-native/scripts/setup-apple-spm.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,18 @@ const {
const {
generateAutolinkingConfig,
parseConfigCommandJson,
readEnvConfigCommand,
resolveEnvConfigCommand,
} = require('./spm/generate-spm-autolinking-config');
const {main: generatePackage} = require('./spm/generate-spm-package');
const {findSourcePath} = require('./spm/generate-spm-package');
const {
SPM_INJECTED_MARKER,
cleanupDanglingJavaScriptCoreRef,
cleanupLeftoverPodsGroup,
findInjectedXcodeproj,
injectSpmIntoExistingXcodeproj,
readPinnedConfigCommand,
removeSpmInjection,
} = require('./spm/generate-spm-xcodeproj');
const {scaffoldAll} = require('./spm/scaffold-package-swift');
Expand Down Expand Up @@ -192,7 +196,7 @@ function parseArgs(argv /*: Array<string> */) /*: SetupArgs */ {
.option('config-command', {
type: 'string',
describe:
'[advanced] JSON array of the argv used to generate autolinking.json, overriding the default @react-native-community/cli config command. Also settable via RCT_SPM_AUTOLINKING_CONFIG_COMMAND. Example: \'["npx","expo-modules-autolinking","react-native-config","--json","--platform","ios"]\'',
'[advanced] JSON array of the argv used to generate autolinking.json, overriding the default @react-native-community/cli config command. Also settable via RCT_SPM_AUTOLINKING_CONFIG_COMMAND. Either way `add`/`update` remembers the value in .spm-injected.json, so later runs and Xcode builds reuse it. Example: \'["npx","expo-modules-autolinking","react-native-config","--json","--platform","ios"]\'',
})
.usage(
'Usage: $0 [action] [options]\n\nSets up Swift Package Manager support in a React Native app.',
Expand Down Expand Up @@ -844,6 +848,7 @@ async function setupXcodeproj(
// (injectSpmIntoExistingXcodeproj preserves it — see
// generate-spm-xcodeproj.js).
artifactsVersionOverride: args.version ?? null,
configCommand: resolveConfigCommandToPin(args),
});
if (result.status !== 'injected') {
logError(`SPM injection failed: ${result.reason}`);
Expand Down Expand Up @@ -903,6 +908,45 @@ function logNextSteps(
log('To remove SPM later: `npx react-native spm deinit`');
}

// The autolinking config command for this run: an explicit `--config-command`
// first, then the value a previous `add`/`update` pinned into the injection
// marker. undefined means "no explicit command", which is what makes
// generateAutolinkingConfig fall back to RCT_SPM_AUTOLINKING_CONFIG_COMMAND and
// then to the built-in default — so the pin has to be WITHHELD while the env
// var is set, or a stale pin would outrank a developer's env override.
function resolveExplicitConfigCommand(
args /*: SetupArgs */,
appRoot /*: string */,
) /*: Array<string> | void */ {
if (args.configCommand != null) {
return args.configCommand;
}
if (readEnvConfigCommand() != null) {
return undefined;
}
const pinned = readPinnedConfigCommand(appRoot);
if (pinned == null) {
return undefined;
}
log(
`Autolinking config command (pinned in ${SPM_INJECTED_MARKER}): ` +
pinned.join(' '),
);
return pinned;
}

// The command to record in the injection marker. The env var is resolved here
// too, because the Xcode build phase inherits neither the flag nor the shell
// that set it — an env-only override that went unpinned would leave the build
// re-deriving autolinking.json with the default command. null pins nothing and
// preserves any earlier pin. An invalid env value throws, as the flag does,
// though `add` has already failed closed on it by this point.
function resolveConfigCommandToPin(
args /*: SetupArgs */,
) /*: ?Array<string> */ {
return args.configCommand ?? resolveEnvConfigCommand();
}

// Generate autolinking.json, failing closed on a config-command error.
//
// generateAutolinkingConfig throws ONLY when the config command itself fails —
Expand Down Expand Up @@ -937,7 +981,9 @@ function generateAutolinkingConfigOrFailClosed(
'RCT_SPM_AUTOLINKING_CONFIG_COMMAND (or pass --config-command) to a ' +
'JSON argv array whose command prints the React Native CLI config, ' +
'e.g. \'["npx","expo-modules-autolinking","react-native-config",' +
'"--json","--platform","ios"]\'.',
'"--json","--platform","ios"]\'. An earlier `add`/`update` may also ' +
`have pinned a command in ${SPM_INJECTED_MARKER}; re-run with ` +
'--config-command to replace a stale one.',
);
process.exitCode = 2;
return null;
Expand Down Expand Up @@ -1035,7 +1081,7 @@ async function main(argv /*:: ?: Array<string> */) /*: Promise<void> */ {
log('Generating autolinking.json (CLI config)...');
autolinkingConfigResult = generateAutolinkingConfigOrFailClosed({
projectRoot,
configCommand: args.configCommand ?? undefined,
configCommand: resolveExplicitConfigCommand(args, appRoot),
});
if (autolinkingConfigResult == null) {
// Fail closed: the config command errored and the helper already set
Expand Down Expand Up @@ -1214,6 +1260,8 @@ module.exports = {
generateAutolinkingConfigOrFailClosed,
parseArgs,
resolveAction,
resolveConfigCommandToPin,
resolveExplicitConfigCommand,
shouldAutoDeintegrate,
ensureBothArtifactFlavors,
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const {
SPM_INJECTED_MARKER,
injectSpmIntoExistingXcodeproj,
readArtifactsVersionOverride,
readPinnedConfigCommand,
removeSpmInjection,
} = require('../generate-spm-xcodeproj');
const fs = require('node:fs');
Expand Down Expand Up @@ -393,3 +394,148 @@ describe('readArtifactsVersionOverride', () => {
expect(readArtifactsVersionOverride(appRoot)).toBeNull();
});
});

// ---------------------------------------------------------------------------
// configCommand — the marker field persisting an explicit `spm add/update
// --config-command '<json argv>'`. Without the pin, the build-time `sync`
// re-derived autolinking.json with the default @react-native-community/cli
// command and failed the "Sync SPM Autolinking" build phase in apps (e.g. Expo
// apps) that replace it.
// ---------------------------------------------------------------------------
describe('configCommand marker field', () => {
const EXPO_COMMAND = [
'npx',
'expo-modules-autolinking',
'react-native-config',
'--json',
'--platform',
'ios',
];

it('records an explicit config command into the marker', () => {
const {appRoot, xcodeprojPath, rnRoot} = scaffoldApp();
injectSpmIntoExistingXcodeproj({
appRoot,
reactNativeRoot: rnRoot,
xcodeprojPath,
configCommand: EXPO_COMMAND,
});
expect(readMarker(xcodeprojPath).configCommand).toEqual(EXPO_COMMAND);
expect(readPinnedConfigCommand(appRoot)).toEqual(EXPO_COMMAND);
});

it('defaults to null when --config-command has never been given', () => {
const {appRoot, xcodeprojPath, rnRoot} = scaffoldApp();
injectSpmIntoExistingXcodeproj({
appRoot,
reactNativeRoot: rnRoot,
xcodeprojPath,
});
expect(readMarker(xcodeprojPath).configCommand).toBeNull();
expect(readPinnedConfigCommand(appRoot)).toBeNull();
});

it('preserves the pin on a later run without --config-command', () => {
const {appRoot, xcodeprojPath, rnRoot} = scaffoldApp();
injectSpmIntoExistingXcodeproj({
appRoot,
reactNativeRoot: rnRoot,
xcodeprojPath,
configCommand: EXPO_COMMAND,
});
injectSpmIntoExistingXcodeproj({
appRoot,
reactNativeRoot: rnRoot,
xcodeprojPath,
});
expect(readMarker(xcodeprojPath).configCommand).toEqual(EXPO_COMMAND);
expect(readPinnedConfigCommand(appRoot)).toEqual(EXPO_COMMAND);
});

it('a later explicit --config-command overwrites the previous pin', () => {
const {appRoot, xcodeprojPath, rnRoot} = scaffoldApp();
injectSpmIntoExistingXcodeproj({
appRoot,
reactNativeRoot: rnRoot,
xcodeprojPath,
configCommand: EXPO_COMMAND,
});
injectSpmIntoExistingXcodeproj({
appRoot,
reactNativeRoot: rnRoot,
xcodeprojPath,
configCommand: ['my-cli', 'config'],
});
expect(readMarker(xcodeprojPath).configCommand).toEqual([
'my-cli',
'config',
]);
expect(readPinnedConfigCommand(appRoot)).toEqual(['my-cli', 'config']);
});

it('deinit drops the pin along with the whole marker', () => {
const {appRoot, xcodeprojPath, rnRoot} = scaffoldApp();
injectSpmIntoExistingXcodeproj({
appRoot,
reactNativeRoot: rnRoot,
xcodeprojPath,
configCommand: EXPO_COMMAND,
});
removeSpmInjection({appRoot, xcodeprojPath});
expect(fs.existsSync(path.join(xcodeprojPath, SPM_INJECTED_MARKER))).toBe(
false,
);
expect(readPinnedConfigCommand(appRoot)).toBeNull();
});
});

// ---------------------------------------------------------------------------
// readPinnedConfigCommand — pure fs read, used by setup-apple-spm.js (including
// the build-time `sync`) to reuse the config command an earlier `add`/`update`
// pinned. A hand-edited or corrupt marker must degrade to the env/default path
// instead of injecting a bogus argv or throwing mid-build.
// ---------------------------------------------------------------------------
describe('readPinnedConfigCommand', () => {
it('returns null when no xcodeproj has been injected yet', () => {
const {appRoot} = scaffoldApp();
expect(readPinnedConfigCommand(appRoot)).toBeNull();
});

it('returns null (never throws) on a malformed marker', () => {
const {appRoot, xcodeprojPath, rnRoot} = scaffoldApp();
injectSpmIntoExistingXcodeproj({
appRoot,
reactNativeRoot: rnRoot,
xcodeprojPath,
configCommand: ['my-cli', 'config'],
});
fs.writeFileSync(
path.join(xcodeprojPath, SPM_INJECTED_MARKER),
'{ not valid json',
'utf8',
);
expect(() => readPinnedConfigCommand(appRoot)).not.toThrow();
expect(readPinnedConfigCommand(appRoot)).toBeNull();
});

it.each([
['a bare string', '"npx expo-modules-autolinking"'],
['an empty array', '[]'],
['a non-string member', '["npx", 7]'],
['an empty-string member', '["npx", ""]'],
['an object', '{"command": "npx"}'],
])('returns null for a pinned value that is %s', (_label, pinned) => {
const {appRoot, xcodeprojPath, rnRoot} = scaffoldApp();
injectSpmIntoExistingXcodeproj({
appRoot,
reactNativeRoot: rnRoot,
xcodeprojPath,
configCommand: ['my-cli', 'config'],
});
const markerPath = path.join(xcodeprojPath, SPM_INJECTED_MARKER);
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
marker.configCommand = JSON.parse(pinned);
fs.writeFileSync(markerPath, JSON.stringify(marker), 'utf8');
expect(readPinnedConfigCommand(appRoot)).toBeNull();
});
});
Loading
Loading