Skip to content

Commit e940c8e

Browse files
committed
feat(spm): auto-redirect non-destructive actions into ios/; fix Flow parse errors
UX: when invoked from the JS root of a standard RN app (cwd === projectRoot, ios/ subdir exists), automatically run the action inside <projectRoot>/ios with a yellow banner instead of erroring. Saves the user a read-error / cd ios / retype-command cycle for the routine path. `clean` keeps refusing because its destructive scopes (--project, --derived-data, --cache) shouldn't silently retarget a different directory. - New `detectStandardRnLayoutRedirect(appRoot, projectRoot)` helper: pure detection, returns the ios/ subdir or null. - `describeRnRootMismatch` refactored to delegate to it (public contract preserved; still used by the clean refuse path). - main() flips `appRoot` to the redirect target before any sub-script invocation when the action is non-destructive. - 4 new unit tests covering the detection helper. Flow parse-error cleanup (pre-existing, unblocked while in the area): - spm/download-spm-artifacts.js:637 — type cast moved to a separate binding so it doesn't sit between `)` and `.member`. - spm/download-spm-artifacts.js:740/:743 — same pattern for two inline- returned object literals. - spm/spm-pbxproj.js:113 — dropped the dangling `/*: any */` cast that appeared after the trailing `;`; consolidated FlowFixMe to the call line. - Once the parse errors cleared, the cascading untyped-import on setup-apple-spm.js:76 resolved automatically.
1 parent 7c63971 commit e940c8e

4 files changed

Lines changed: 98 additions & 22 deletions

File tree

packages/react-native/scripts/setup-apple-spm.js

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -647,10 +647,12 @@ function resolveAction(
647647
* silently skip every npm native dep. The build "succeeds" but anything
648648
* touching a native module crashes at runtime.
649649
*
650-
* Returns null when the cwd is fine, or a string describing the problem
651-
* when the user should `cd` into `ios/`.
650+
* Returns the absolute path to the redirected app root (`<projectRoot>/ios`)
651+
* when the redirect heuristic applies, else null. Pure: no side effects.
652+
* The caller decides whether to auto-redirect (non-destructive actions) or
653+
* refuse (destructive actions like `clean`).
652654
*/
653-
function describeRnRootMismatch(
655+
function detectStandardRnLayoutRedirect(
654656
appRoot /*: string */,
655657
projectRoot /*: string */,
656658
) /*: string | null */ {
@@ -663,13 +665,27 @@ function describeRnRootMismatch(
663665
// Standard RN layout has an `ios/` subdir holding the native project.
664666
// Without it (e.g. rn-tester's flat layout), no mismatch to flag.
665667
const iosSubdir = path.join(projectRoot, 'ios');
666-
let isDir = false;
667668
try {
668-
isDir = fs.statSync(iosSubdir).isDirectory();
669+
if (!fs.statSync(iosSubdir).isDirectory()) {
670+
return null;
671+
}
669672
} catch {
670673
return null;
671674
}
672-
if (!isDir) {
675+
return iosSubdir;
676+
}
677+
678+
/**
679+
* Formats the refuse-message for destructive actions (`clean`) — and for
680+
* tooling that wants the long explanation. Returns null when no mismatch
681+
* is detected (delegates the detection to detectStandardRnLayoutRedirect).
682+
*/
683+
function describeRnRootMismatch(
684+
appRoot /*: string */,
685+
projectRoot /*: string */,
686+
) /*: string | null */ {
687+
const iosSubdir = detectStandardRnLayoutRedirect(appRoot, projectRoot);
688+
if (iosSubdir == null) {
673689
return null;
674690
}
675691
return (
@@ -1168,23 +1184,34 @@ function logNextSteps(
11681184
}
11691185

11701186
async function main(argv /*:: ?: Array<string> */) /*: Promise<void> */ {
1171-
const appRoot = process.cwd();
1187+
let appRoot = process.cwd();
11721188
const projectRoot = findProjectRoot(appRoot);
11731189
const args = parseArgs(argv ?? process.argv.slice(2));
11741190
const action = resolveAction(args.action, appRoot);
11751191

1192+
// Standard-RN-layout redirect: if invoked from the JS root and there's an
1193+
// `ios/` subdir, route the run there. `clean` keeps refusing because its
1194+
// destructive scopes (--project, --derived-data, --cache) shouldn't
1195+
// silently retarget a different directory.
1196+
const redirectTo = detectStandardRnLayoutRedirect(appRoot, projectRoot);
1197+
if (redirectTo != null) {
1198+
if (action === 'clean') {
1199+
logError(describeRnRootMismatch(appRoot, projectRoot) ?? '');
1200+
process.exitCode = 1;
1201+
return;
1202+
}
1203+
log(
1204+
`\x1b[33mDetected standard RN layout — running ${action} in ${displayPath(redirectTo)} ` +
1205+
`instead of ${displayPath(appRoot)}.\x1b[0m`,
1206+
);
1207+
appRoot = redirectTo;
1208+
}
1209+
11761210
log(`Running SPM ${action} in: ${displayPath(appRoot)}`);
11771211
if (projectRoot !== appRoot) {
11781212
log(`Project root (package.json): ${displayPath(projectRoot)}`);
11791213
}
11801214

1181-
const mismatch = describeRnRootMismatch(appRoot, projectRoot);
1182-
if (mismatch != null) {
1183-
logError(mismatch);
1184-
process.exitCode = 1;
1185-
return;
1186-
}
1187-
11881215
if (action === 'clean') {
11891216
const wantProject = args.cleanProject || args.cleanAll;
11901217
const wantDerivedData = args.cleanDerivedData || args.cleanAll;
@@ -1376,6 +1403,7 @@ module.exports = {
13761403
cleanGeneratedState,
13771404
gatherCleanTargets,
13781405
describeRnRootMismatch,
1406+
detectStandardRnLayoutRedirect,
13791407
findExistingSpmXcodeproj,
13801408
findLegacyXcodeproj,
13811409
podfileNeedsPatch,

packages/react-native/scripts/spm/__tests__/setup-apple-spm-test.js

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

1313
const {
1414
describeRnRootMismatch,
15+
detectStandardRnLayoutRedirect,
1516
findExistingSpmXcodeproj,
1617
findLegacyXcodeproj,
1718
gatherCleanTargets,
@@ -196,6 +197,48 @@ describe('describeRnRootMismatch', () => {
196197
});
197198
});
198199

200+
// ---------------------------------------------------------------------------
201+
// detectStandardRnLayoutRedirect — pure detection helper used by main() to
202+
// decide whether to auto-redirect into the ios/ subdir. Non-destructive
203+
// actions (init/update/sync/codegen/download/scaffold) redirect with a
204+
// banner; clean keeps refusing via describeRnRootMismatch's full message.
205+
// ---------------------------------------------------------------------------
206+
207+
describe('detectStandardRnLayoutRedirect', () => {
208+
let tempDir;
209+
210+
beforeEach(() => {
211+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'spm-redirect-'));
212+
});
213+
214+
afterEach(() => {
215+
fs.rmSync(tempDir, {recursive: true, force: true});
216+
});
217+
218+
it('returns the ios/ subdir when cwd === projectRoot AND ios/ exists', () => {
219+
fs.mkdirSync(path.join(tempDir, 'ios'));
220+
expect(detectStandardRnLayoutRedirect(tempDir, tempDir)).toBe(
221+
path.join(tempDir, 'ios'),
222+
);
223+
});
224+
225+
it('returns null when running from a subdirectory (already cd-ed)', () => {
226+
fs.mkdirSync(path.join(tempDir, 'ios'));
227+
expect(
228+
detectStandardRnLayoutRedirect(path.join(tempDir, 'ios'), tempDir),
229+
).toBeNull();
230+
});
231+
232+
it('returns null for flat layouts (no ios/ subdir, e.g. rn-tester)', () => {
233+
expect(detectStandardRnLayoutRedirect(tempDir, tempDir)).toBeNull();
234+
});
235+
236+
it('returns null when `ios` is a file, not a directory', () => {
237+
fs.writeFileSync(path.join(tempDir, 'ios'), '');
238+
expect(detectStandardRnLayoutRedirect(tempDir, tempDir)).toBeNull();
239+
});
240+
});
241+
199242
// ---------------------------------------------------------------------------
200243
// findLegacyXcodeproj + podfileNeedsPatch — Podfile auto-patch helpers.
201244
// CocoaPods refuses to choose between sibling xcodeprojs when the Podfile

packages/react-native/scripts/spm/download-spm-artifacts.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -632,9 +632,10 @@ async function main(argv /*:: ?: Array<string> */) /*: Promise<void> */ {
632632
let rnVersion = args.version;
633633
if (rnVersion == null) {
634634
// $FlowFixMe[incompatible-type] JSON.parse returns any
635-
rnVersion = JSON.parse(
635+
const rnPkg /*: {version: string} */ = JSON.parse(
636636
fs.readFileSync(path.join(rnRoot, 'package.json'), 'utf8'),
637-
) /*: {version: string} */.version;
637+
);
638+
rnVersion = rnPkg.version;
638639
}
639640
if (rnVersion === '1000.0.0') {
640641
log('Detected dev version (1000.0.0), resolving as nightly...');
@@ -732,14 +733,19 @@ async function main(argv /*:: ?: Array<string> */) /*: Promise<void> */ {
732733
outputDir,
733734
makeCallback(index),
734735
);
735-
return {
736+
const ok /*: ArtifactResultEntry */ = {
736737
name: spec.name,
737738
error: undefined,
738739
...r,
739-
} /*: ArtifactResultEntry */;
740+
};
741+
return ok;
740742
} catch (e) {
741743
progress.update(index, ` ${spec.name}: FAILED - ${e.message}`);
742-
return {name: spec.name, error: e.message} /*: ArtifactResultEntry */;
744+
const failed /*: ArtifactResultEntry */ = {
745+
name: spec.name,
746+
error: e.message,
747+
};
748+
return failed;
743749
}
744750
}),
745751
);

packages/react-native/scripts/spm/spm-pbxproj.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,9 @@ function scanProjectFiles(sourceDir /*: string */) /*: ProjectFiles */ {
107107
if (!fs.existsSync(dir)) {
108108
return;
109109
}
110-
// $FlowFixMe[incompatible-type]
111110
const entries /*: Array<{name: string, isDirectory(): boolean}> */ =
112-
// $FlowFixMe[unclear-type]
113-
fs.readdirSync(dir, {withFileTypes: true}); /*: any */
111+
// $FlowFixMe[incompatible-type] Dirent typing
112+
fs.readdirSync(dir, {withFileTypes: true});
114113
for (const entry of entries) {
115114
if (entry.name.startsWith('.')) {
116115
continue;

0 commit comments

Comments
 (0)