Skip to content

Commit c2dac6a

Browse files
coadometa-codesync[bot]
authored andcommitted
Download artifacts from Mirror first on SwiftPM builds (#57763)
Summary: SwiftPM builds now download prebuilt artifacts from the React Native Maven mirror first and fall back to Maven Central. To disable the mirror the flag `RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED=false` can be set. ## Changelog: [IOS][CHANGED] - Download SwiftPM prebuilt artifacts from the React Native Maven mirror first, with Maven Central as fallback [Internal] Pull Request resolved: #57763 Test Plan: ``` yarn jest packages/react-native/scripts/spm ``` Reviewed By: cipolleschi Differential Revision: D114348469 Pulled By: coado fbshipit-source-id: f1068995ac69c604262d3fffe645a69ba3203189
1 parent 146aaac commit c2dac6a

2 files changed

Lines changed: 292 additions & 66 deletions

File tree

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

Lines changed: 198 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,55 @@ const {
1414
exists,
1515
extractXCFramework,
1616
findFirst,
17+
firstExistingUrl,
1718
formatBytes,
1819
formatSpeed,
19-
hermesReleaseUrl,
20+
hermesReleaseUrls,
21+
mavenRepositoryUrls,
22+
reactNativeMavenMirrorEnabled,
2023
resolveCacheSlotVersion,
2124
resolveHermesArtifact,
2225
resolveLatestV1Version,
2326
resolveNightlyVersion,
2427
resolveRNCoreArtifact,
2528
resolveRNDepsArtifact,
2629
resolveSnapshotUrl,
27-
rnCoreReleaseUrl,
28-
rnDepsReleaseUrl,
30+
rnCoreReleaseUrls,
31+
rnDepsReleaseUrls,
2932
validateArtifactsCache,
3033
} = require('../download-spm-artifacts');
3134
const {execSync} = require('node:child_process');
3235
const fs = require('node:fs');
3336
const os = require('node:os');
3437
const path = require('node:path');
3538

39+
// Repository-selection env vars are read at call time; clear them per test so
40+
// results don't depend on the host machine, and restore afterwards.
41+
const REPO_ENV_KEYS = [
42+
'ENTERPRISE_REPOSITORY',
43+
'RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED',
44+
];
45+
let savedRepoEnv = {};
46+
beforeEach(() => {
47+
savedRepoEnv = {};
48+
for (const key of REPO_ENV_KEYS) {
49+
savedRepoEnv[key] = process.env[key];
50+
delete process.env[key];
51+
}
52+
});
53+
afterEach(() => {
54+
for (const key of REPO_ENV_KEYS) {
55+
if (savedRepoEnv[key] !== undefined) {
56+
process.env[key] = savedRepoEnv[key];
57+
} else {
58+
delete process.env[key];
59+
}
60+
}
61+
});
62+
63+
const MIRROR = 'https://repo.reactnative.dev/maven2';
64+
const CENTRAL = 'https://repo1.maven.org/maven2';
65+
3666
// Shared fetch router used by the URL-resolution tests below. Each key is a
3767
// URL substring; the matched value describes the response. Anything unmatched
3868
// returns 404 (the "release not found, fall back to snapshot" path).
@@ -193,49 +223,138 @@ describe('resolveHermesArtifact', () => {
193223
});
194224

195225
// ---------------------------------------------------------------------------
196-
// Maven URL builders — pure string composition.
226+
// Repository selection — RN Maven mirror first, Maven Central as fallback;
227+
// ENTERPRISE_REPOSITORY replaces both.
228+
// ---------------------------------------------------------------------------
229+
230+
describe('mavenRepositoryUrls', () => {
231+
it('prefers the RN Maven mirror and keeps Maven Central as fallback by default', () => {
232+
expect(reactNativeMavenMirrorEnabled()).toBe(true);
233+
expect(mavenRepositoryUrls()).toEqual([MIRROR, CENTRAL]);
234+
});
235+
236+
it('stays enabled when RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED=true', () => {
237+
process.env.RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED = 'true';
238+
expect(reactNativeMavenMirrorEnabled()).toBe(true);
239+
expect(mavenRepositoryUrls()).toEqual([MIRROR, CENTRAL]);
240+
});
241+
242+
it.each(['false', 'FALSE', '0'])(
243+
'drops the mirror when RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED=%s',
244+
value => {
245+
process.env.RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED = value;
246+
expect(reactNativeMavenMirrorEnabled()).toBe(false);
247+
expect(mavenRepositoryUrls()).toEqual([CENTRAL]);
248+
},
249+
);
250+
251+
it('ENTERPRISE_REPOSITORY replaces mirror AND Central, stripping trailing slashes', () => {
252+
process.env.ENTERPRISE_REPOSITORY = 'https://maven.internal.example//';
253+
expect(mavenRepositoryUrls()).toEqual(['https://maven.internal.example']);
254+
});
255+
256+
it.each(['', ' ', '\t\n'])(
257+
'ignores a blank ENTERPRISE_REPOSITORY (%j) and keeps the defaults',
258+
value => {
259+
process.env.ENTERPRISE_REPOSITORY = value;
260+
expect(mavenRepositoryUrls()).toEqual([MIRROR, CENTRAL]);
261+
},
262+
);
263+
264+
it('trims surrounding whitespace from ENTERPRISE_REPOSITORY', () => {
265+
process.env.ENTERPRISE_REPOSITORY = ' https://maven.internal.example/ ';
266+
expect(mavenRepositoryUrls()).toEqual(['https://maven.internal.example']);
267+
});
268+
});
269+
270+
// ---------------------------------------------------------------------------
271+
// Maven URL builders — one candidate per repository, mirror first.
197272
// ---------------------------------------------------------------------------
198273

199274
describe('release URL builders', () => {
200-
it('rnCoreReleaseUrl points at the reactnative-core classifier on Maven Central', () => {
201-
const url = rnCoreReleaseUrl('0.85.0', 'debug');
202-
expect(url).toBe(
203-
'https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/0.85.0/' +
204-
'react-native-artifacts-0.85.0-reactnative-core-debug.tar.gz',
275+
it('rnCoreReleaseUrls builds a candidate per repository for the reactnative-core classifier', () => {
276+
const suffix =
277+
'/com/facebook/react/react-native-artifacts/0.85.0/' +
278+
'react-native-artifacts-0.85.0-reactnative-core-debug.tar.gz';
279+
expect(rnCoreReleaseUrls('0.85.0', 'debug')).toEqual([
280+
MIRROR + suffix,
281+
CENTRAL + suffix,
282+
]);
283+
});
284+
285+
it('rnDepsReleaseUrls points at the reactnative-dependencies classifier', () => {
286+
const urls = rnDepsReleaseUrls('0.85.0', 'release');
287+
expect(urls).toHaveLength(2);
288+
for (const url of urls) {
289+
expect(url).toContain('react-native-artifacts/0.85.0/');
290+
expect(url).toContain('reactnative-dependencies-release.tar.gz');
291+
}
292+
});
293+
294+
it('hermesReleaseUrls points at the hermes-ios coordinate', () => {
295+
const suffix =
296+
'/com/facebook/hermes/hermes-ios/0.13.0/' +
297+
'hermes-ios-0.13.0-hermes-ios-debug.tar.gz';
298+
expect(hermesReleaseUrls('0.13.0', 'debug')).toEqual([
299+
MIRROR + suffix,
300+
CENTRAL + suffix,
301+
]);
302+
});
303+
304+
it('honors ENTERPRISE_REPOSITORY as the only release base URL', () => {
305+
process.env.ENTERPRISE_REPOSITORY = 'https://maven.internal.example';
306+
const urls = rnCoreReleaseUrls('0.85.0', 'debug');
307+
expect(urls).toHaveLength(1);
308+
expect(urls[0]).toContain(
309+
'https://maven.internal.example/com/facebook/react/',
205310
);
206311
});
312+
});
313+
314+
// ---------------------------------------------------------------------------
315+
// firstExistingUrl — ordered candidate probing.
316+
// ---------------------------------------------------------------------------
317+
318+
describe('firstExistingUrl', () => {
319+
let origFetch;
320+
beforeEach(() => {
321+
origFetch = globalThis.fetch;
322+
});
323+
afterEach(() => {
324+
globalThis.fetch = origFetch;
325+
});
207326

208-
it('rnDepsReleaseUrl points at the reactnative-dependencies classifier', () => {
209-
const url = rnDepsReleaseUrl('0.85.0', 'release');
210-
expect(url).toContain('react-native-artifacts/0.85.0/');
211-
expect(url).toContain('reactnative-dependencies-release.tar.gz');
327+
it('returns the first candidate when it exists, without probing the rest', async () => {
328+
globalThis.fetch = jest.fn(async () => ({status: 200}));
329+
const url = await firstExistingUrl(['https://a/x', 'https://b/x']);
330+
expect(url).toBe('https://a/x');
331+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
212332
});
213333

214-
it('hermesReleaseUrl points at the hermes-ios coordinate', () => {
215-
const url = hermesReleaseUrl('0.13.0', 'debug');
216-
expect(url).toBe(
217-
'https://repo1.maven.org/maven2/com/facebook/hermes/hermes-ios/0.13.0/' +
218-
'hermes-ios-0.13.0-hermes-ios-debug.tar.gz',
334+
it('falls through to the next candidate on a miss', async () => {
335+
globalThis.fetch = jest.fn(async u =>
336+
String(u).startsWith('https://b/') ? {status: 200} : {status: 404},
337+
);
338+
expect(await firstExistingUrl(['https://a/x', 'https://b/x'])).toBe(
339+
'https://b/x',
219340
);
220341
});
221342

222-
it('honors ENTERPRISE_REPOSITORY for the release base URL', () => {
223-
jest.isolateModules(() => {
224-
const prev = process.env.ENTERPRISE_REPOSITORY;
225-
process.env.ENTERPRISE_REPOSITORY = 'https://maven.internal.example';
226-
try {
227-
const mod = require('../download-spm-artifacts');
228-
expect(mod.rnCoreReleaseUrl('0.85.0', 'debug')).toContain(
229-
'https://maven.internal.example/com/facebook/react/',
230-
);
231-
} finally {
232-
if (prev !== undefined) {
233-
process.env.ENTERPRISE_REPOSITORY = prev;
234-
} else {
235-
delete process.env.ENTERPRISE_REPOSITORY;
236-
}
343+
it('falls through to the next candidate when the probe errors (outage)', async () => {
344+
globalThis.fetch = jest.fn(async u => {
345+
if (String(u).startsWith('https://a/')) {
346+
throw new Error('mirror down');
237347
}
348+
return {status: 200};
238349
});
350+
expect(await firstExistingUrl(['https://a/x', 'https://b/x'])).toBe(
351+
'https://b/x',
352+
);
353+
});
354+
355+
it('returns null when no candidate exists', async () => {
356+
globalThis.fetch = jest.fn(async () => ({status: 404}));
357+
expect(await firstExistingUrl(['https://a/x', 'https://b/x'])).toBeNull();
239358
});
240359
});
241360

@@ -425,16 +544,61 @@ describe('resolveRNCoreArtifact', () => {
425544
fs.rmSync(tempDir, {recursive: true, force: true});
426545
});
427546

428-
it('uses the stable release URL when it exists', async () => {
547+
it('prefers the RN Maven mirror release when it exists', async () => {
429548
globalThis.fetch = routerFetch({
430549
'reactnative-core-debug.tar.gz': {status: 200},
431550
});
432551
const result = await resolveRNCoreArtifact('0.85.0', 'debug', null);
433552
expect(result.version).toBe('0.85.0');
553+
expect(result.url).toContain('repo.reactnative.dev');
554+
expect(result.url).toContain('reactnative-core-debug.tar.gz');
555+
// Maven Central is never probed when the mirror has the artifact.
556+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
557+
});
558+
559+
it('falls back to Maven Central when the mirror is missing the artifact', async () => {
560+
globalThis.fetch = routerFetch({
561+
// Only Central serves the artifact; the mirror probe hits the 404
562+
// default.
563+
'repo1.maven.org': {status: 200},
564+
});
565+
const result = await resolveRNCoreArtifact('0.85.0', 'debug', null);
566+
expect(result.version).toBe('0.85.0');
434567
expect(result.url).toContain('repo1.maven.org');
435568
expect(result.url).toContain('reactnative-core-debug.tar.gz');
436569
});
437570

571+
it('falls back to Maven Central when the mirror errors (outage)', async () => {
572+
globalThis.fetch = jest.fn(async url => {
573+
if (String(url).includes('repo.reactnative.dev')) {
574+
throw new Error('mirror unreachable');
575+
}
576+
return {status: 200};
577+
});
578+
const result = await resolveRNCoreArtifact('0.85.0', 'debug', null);
579+
expect(result.url).toContain('repo1.maven.org');
580+
});
581+
582+
it('goes straight to Maven Central when the mirror is disabled', async () => {
583+
process.env.RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED = 'false';
584+
globalThis.fetch = routerFetch({
585+
'reactnative-core-debug.tar.gz': {status: 200},
586+
});
587+
const result = await resolveRNCoreArtifact('0.85.0', 'debug', null);
588+
expect(result.url).toContain('repo1.maven.org');
589+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
590+
});
591+
592+
it('only consults ENTERPRISE_REPOSITORY when it is set', async () => {
593+
process.env.ENTERPRISE_REPOSITORY = 'https://maven.internal.example';
594+
globalThis.fetch = routerFetch({
595+
'reactnative-core-debug.tar.gz': {status: 200},
596+
});
597+
const result = await resolveRNCoreArtifact('0.85.0', 'debug', null);
598+
expect(result.url).toContain('maven.internal.example');
599+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
600+
});
601+
438602
it('falls back to the snapshot URL when the release is missing', async () => {
439603
globalThis.fetch = jest.fn(async (url, opts) => {
440604
// HEAD probe of the release URL → 404.

0 commit comments

Comments
 (0)