From 44314450c1da2832b906396a42428e1466c193fa Mon Sep 17 00:00:00 2001 From: Dawid Malecki Date: Thu, 30 Jul 2026 11:34:49 +0200 Subject: [PATCH 1/2] download artifacts from Mirror first and introduce existence caching --- .../__tests__/download-spm-artifacts-test.js | 287 +++++++++++++++--- .../scripts/spm/download-spm-artifacts.js | 146 ++++++--- 2 files changed, 365 insertions(+), 68 deletions(-) diff --git a/packages/react-native/scripts/spm/__tests__/download-spm-artifacts-test.js b/packages/react-native/scripts/spm/__tests__/download-spm-artifacts-test.js index 9f9090e388e9..73832c1ea87c 100644 --- a/packages/react-native/scripts/spm/__tests__/download-spm-artifacts-test.js +++ b/packages/react-native/scripts/spm/__tests__/download-spm-artifacts-test.js @@ -11,12 +11,16 @@ 'use strict'; const { + clearRequestCache, exists, extractXCFramework, findFirst, + firstExistingUrl, formatBytes, formatSpeed, - hermesReleaseUrl, + hermesReleaseUrls, + mavenRepositoryUrls, + reactNativeMavenMirrorEnabled, resolveCacheSlotVersion, resolveHermesArtifact, resolveLatestV1Version, @@ -24,8 +28,8 @@ const { resolveRNCoreArtifact, resolveRNDepsArtifact, resolveSnapshotUrl, - rnCoreReleaseUrl, - rnDepsReleaseUrl, + rnCoreReleaseUrls, + rnDepsReleaseUrls, validateArtifactsCache, } = require('../download-spm-artifacts'); const {execSync} = require('node:child_process'); @@ -33,6 +37,36 @@ const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); +// Repository-selection env vars are read at call time; clear them per test so +// results don't depend on the host machine, and restore afterwards. The +// probe memo cache is module-global, so it is also reset per test — without +// this, a fetch mock from one test would leak its results into the next. +const REPO_ENV_KEYS = [ + 'ENTERPRISE_REPOSITORY', + 'RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED', +]; +let savedRepoEnv = {}; +beforeEach(() => { + savedRepoEnv = {}; + for (const key of REPO_ENV_KEYS) { + savedRepoEnv[key] = process.env[key]; + delete process.env[key]; + } + clearRequestCache(); +}); +afterEach(() => { + for (const key of REPO_ENV_KEYS) { + if (savedRepoEnv[key] !== undefined) { + process.env[key] = savedRepoEnv[key]; + } else { + delete process.env[key]; + } + } +}); + +const MIRROR = 'https://repo.reactnative.dev/maven2'; +const CENTRAL = 'https://repo1.maven.org/maven2'; + // Shared fetch router used by the URL-resolution tests below. Each key is a // URL substring; the matched value describes the response. Anything unmatched // returns 404 (the "release not found, fall back to snapshot" path). @@ -193,49 +227,125 @@ describe('resolveHermesArtifact', () => { }); // --------------------------------------------------------------------------- -// Maven URL builders — pure string composition. +// Repository selection — RN Maven mirror first, Maven Central as fallback; +// ENTERPRISE_REPOSITORY replaces both. +// --------------------------------------------------------------------------- + +describe('mavenRepositoryUrls', () => { + it('prefers the RN Maven mirror and keeps Maven Central as fallback by default', () => { + expect(reactNativeMavenMirrorEnabled()).toBe(true); + expect(mavenRepositoryUrls()).toEqual([MIRROR, CENTRAL]); + }); + + it('stays enabled when RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED=true', () => { + process.env.RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED = 'true'; + expect(reactNativeMavenMirrorEnabled()).toBe(true); + expect(mavenRepositoryUrls()).toEqual([MIRROR, CENTRAL]); + }); + + it.each(['false', 'FALSE', '0'])( + 'drops the mirror when RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED=%s', + value => { + process.env.RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED = value; + expect(reactNativeMavenMirrorEnabled()).toBe(false); + expect(mavenRepositoryUrls()).toEqual([CENTRAL]); + }, + ); + + it('ENTERPRISE_REPOSITORY replaces mirror AND Central, stripping trailing slashes', () => { + process.env.ENTERPRISE_REPOSITORY = 'https://maven.internal.example//'; + expect(mavenRepositoryUrls()).toEqual(['https://maven.internal.example']); + }); +}); + +// --------------------------------------------------------------------------- +// Maven URL builders — one candidate per repository, mirror first. // --------------------------------------------------------------------------- describe('release URL builders', () => { - it('rnCoreReleaseUrl points at the reactnative-core classifier on Maven Central', () => { - const url = rnCoreReleaseUrl('0.85.0', 'debug'); - expect(url).toBe( - 'https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/0.85.0/' + - 'react-native-artifacts-0.85.0-reactnative-core-debug.tar.gz', + it('rnCoreReleaseUrls builds a candidate per repository for the reactnative-core classifier', () => { + const suffix = + '/com/facebook/react/react-native-artifacts/0.85.0/' + + 'react-native-artifacts-0.85.0-reactnative-core-debug.tar.gz'; + expect(rnCoreReleaseUrls('0.85.0', 'debug')).toEqual([ + MIRROR + suffix, + CENTRAL + suffix, + ]); + }); + + it('rnDepsReleaseUrls points at the reactnative-dependencies classifier', () => { + const urls = rnDepsReleaseUrls('0.85.0', 'release'); + expect(urls).toHaveLength(2); + for (const url of urls) { + expect(url).toContain('react-native-artifacts/0.85.0/'); + expect(url).toContain('reactnative-dependencies-release.tar.gz'); + } + }); + + it('hermesReleaseUrls points at the hermes-ios coordinate', () => { + const suffix = + '/com/facebook/hermes/hermes-ios/0.13.0/' + + 'hermes-ios-0.13.0-hermes-ios-debug.tar.gz'; + expect(hermesReleaseUrls('0.13.0', 'debug')).toEqual([ + MIRROR + suffix, + CENTRAL + suffix, + ]); + }); + + it('honors ENTERPRISE_REPOSITORY as the only release base URL', () => { + process.env.ENTERPRISE_REPOSITORY = 'https://maven.internal.example'; + const urls = rnCoreReleaseUrls('0.85.0', 'debug'); + expect(urls).toHaveLength(1); + expect(urls[0]).toContain( + 'https://maven.internal.example/com/facebook/react/', ); }); +}); + +// --------------------------------------------------------------------------- +// firstExistingUrl — ordered candidate probing. +// --------------------------------------------------------------------------- + +describe('firstExistingUrl', () => { + let origFetch; + beforeEach(() => { + origFetch = globalThis.fetch; + }); + afterEach(() => { + globalThis.fetch = origFetch; + }); - it('rnDepsReleaseUrl points at the reactnative-dependencies classifier', () => { - const url = rnDepsReleaseUrl('0.85.0', 'release'); - expect(url).toContain('react-native-artifacts/0.85.0/'); - expect(url).toContain('reactnative-dependencies-release.tar.gz'); + it('returns the first candidate when it exists, without probing the rest', async () => { + globalThis.fetch = jest.fn(async () => ({status: 200})); + const url = await firstExistingUrl(['https://a/x', 'https://b/x']); + expect(url).toBe('https://a/x'); + expect(globalThis.fetch).toHaveBeenCalledTimes(1); }); - it('hermesReleaseUrl points at the hermes-ios coordinate', () => { - const url = hermesReleaseUrl('0.13.0', 'debug'); - expect(url).toBe( - 'https://repo1.maven.org/maven2/com/facebook/hermes/hermes-ios/0.13.0/' + - 'hermes-ios-0.13.0-hermes-ios-debug.tar.gz', + it('falls through to the next candidate on a miss', async () => { + globalThis.fetch = jest.fn(async u => + String(u).startsWith('https://b/') ? {status: 200} : {status: 404}, + ); + expect(await firstExistingUrl(['https://a/x', 'https://b/x'])).toBe( + 'https://b/x', ); }); - it('honors ENTERPRISE_REPOSITORY for the release base URL', () => { - jest.isolateModules(() => { - const prev = process.env.ENTERPRISE_REPOSITORY; - process.env.ENTERPRISE_REPOSITORY = 'https://maven.internal.example'; - try { - const mod = require('../download-spm-artifacts'); - expect(mod.rnCoreReleaseUrl('0.85.0', 'debug')).toContain( - 'https://maven.internal.example/com/facebook/react/', - ); - } finally { - if (prev !== undefined) { - process.env.ENTERPRISE_REPOSITORY = prev; - } else { - delete process.env.ENTERPRISE_REPOSITORY; - } + it('falls through to the next candidate when the probe errors (outage)', async () => { + globalThis.fetch = jest.fn(async u => { + if (String(u).startsWith('https://a/')) { + throw new Error('mirror down'); } + return {status: 200}; }); + expect(await firstExistingUrl(['https://a/x', 'https://b/x'])).toBe( + 'https://b/x', + ); + }); + + it('returns null when no candidate exists', async () => { + globalThis.fetch = jest.fn(async () => ({status: 404})); + expect(await firstExistingUrl(['https://a/x', 'https://b/x'])).toBeNull(); }); }); @@ -299,6 +409,70 @@ describe('exists', () => { }); }); +// --------------------------------------------------------------------------- +// Probe memoization — only the existence probes against the release +// repositories (RN Maven mirror / Maven Central) are cached per process; +// version lookups always go to the network. +// --------------------------------------------------------------------------- + +describe('probe memoization', () => { + let origFetch; + beforeEach(() => { + origFetch = globalThis.fetch; + }); + afterEach(() => { + globalThis.fetch = origFetch; + }); + + it('exists() probes each URL at most once until the cache is cleared', async () => { + globalThis.fetch = jest.fn(async () => ({status: 200})); + expect(await exists('https://example/x.tar.gz')).toBe(true); + expect(await exists('https://example/x.tar.gz')).toBe(true); + expect(globalThis.fetch).toHaveBeenCalledTimes(1); + + expect(await exists('https://example/y.tar.gz')).toBe(true); + expect(globalThis.fetch).toHaveBeenCalledTimes(2); + + clearRequestCache(); + expect(await exists('https://example/x.tar.gz')).toBe(true); + expect(globalThis.fetch).toHaveBeenCalledTimes(3); + }); + + it('caches negative exists() probes too', async () => { + globalThis.fetch = jest.fn(async () => ({status: 404})); + expect(await exists('https://example/missing.tar.gz')).toBe(false); + expect(await exists('https://example/missing.tar.gz')).toBe(false); + expect(globalThis.fetch).toHaveBeenCalledTimes(1); + }); + + it('does not memoize npm dist-tag lookups', async () => { + globalThis.fetch = routerFetch({ + 'react-native/nightly': {json: {version: '0.86.0-nightly-xyz'}}, + }); + expect(await resolveNightlyVersion('react-native')).toBe( + '0.86.0-nightly-xyz', + ); + expect(await resolveNightlyVersion('react-native')).toBe( + '0.86.0-nightly-xyz', + ); + expect(globalThis.fetch).toHaveBeenCalledTimes(2); + }); + + it('does not memoize snapshot-metadata fetches', async () => { + globalThis.fetch = routerFetch({ + 'maven-metadata.xml': { + text: + '20260101.000000' + + '1', + }, + }); + const args = ['0.85.0', 'react', 'react-native-artifacts', 'x.tar.gz']; + await resolveSnapshotUrl(...args); + await resolveSnapshotUrl(...args); + expect(globalThis.fetch).toHaveBeenCalledTimes(2); + }); +}); + // --------------------------------------------------------------------------- // resolveSnapshotUrl — parses maven-metadata.xml into a timestamped URL. // --------------------------------------------------------------------------- @@ -425,16 +599,61 @@ describe('resolveRNCoreArtifact', () => { fs.rmSync(tempDir, {recursive: true, force: true}); }); - it('uses the stable release URL when it exists', async () => { + it('prefers the RN Maven mirror release when it exists', async () => { globalThis.fetch = routerFetch({ 'reactnative-core-debug.tar.gz': {status: 200}, }); const result = await resolveRNCoreArtifact('0.85.0', 'debug', null); expect(result.version).toBe('0.85.0'); + expect(result.url).toContain('repo.reactnative.dev'); + expect(result.url).toContain('reactnative-core-debug.tar.gz'); + // Maven Central is never probed when the mirror has the artifact. + expect(globalThis.fetch).toHaveBeenCalledTimes(1); + }); + + it('falls back to Maven Central when the mirror is missing the artifact', async () => { + globalThis.fetch = routerFetch({ + // Only Central serves the artifact; the mirror probe hits the 404 + // default. + 'repo1.maven.org': {status: 200}, + }); + const result = await resolveRNCoreArtifact('0.85.0', 'debug', null); + expect(result.version).toBe('0.85.0'); expect(result.url).toContain('repo1.maven.org'); expect(result.url).toContain('reactnative-core-debug.tar.gz'); }); + it('falls back to Maven Central when the mirror errors (outage)', async () => { + globalThis.fetch = jest.fn(async url => { + if (String(url).includes('repo.reactnative.dev')) { + throw new Error('mirror unreachable'); + } + return {status: 200}; + }); + const result = await resolveRNCoreArtifact('0.85.0', 'debug', null); + expect(result.url).toContain('repo1.maven.org'); + }); + + it('goes straight to Maven Central when the mirror is disabled', async () => { + process.env.RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED = 'false'; + globalThis.fetch = routerFetch({ + 'reactnative-core-debug.tar.gz': {status: 200}, + }); + const result = await resolveRNCoreArtifact('0.85.0', 'debug', null); + expect(result.url).toContain('repo1.maven.org'); + expect(globalThis.fetch).toHaveBeenCalledTimes(1); + }); + + it('only consults ENTERPRISE_REPOSITORY when it is set', async () => { + process.env.ENTERPRISE_REPOSITORY = 'https://maven.internal.example'; + globalThis.fetch = routerFetch({ + 'reactnative-core-debug.tar.gz': {status: 200}, + }); + const result = await resolveRNCoreArtifact('0.85.0', 'debug', null); + expect(result.url).toContain('maven.internal.example'); + expect(globalThis.fetch).toHaveBeenCalledTimes(1); + }); + it('falls back to the snapshot URL when the release is missing', async () => { globalThis.fetch = jest.fn(async (url, opts) => { // HEAD probe of the release URL → 404. diff --git a/packages/react-native/scripts/spm/download-spm-artifacts.js b/packages/react-native/scripts/spm/download-spm-artifacts.js index 4faa4331154c..409cd82e38b6 100644 --- a/packages/react-native/scripts/spm/download-spm-artifacts.js +++ b/packages/react-native/scripts/spm/download-spm-artifacts.js @@ -42,7 +42,17 @@ * Per-artifact version overrides (mirrors existing env vars): * HERMES_VERSION= * RN_DEP_VERSION= - * ENTERPRISE_REPOSITORY= Custom Maven mirror (must match Maven structure) + * + * Repository selection: + * Release tarballs are looked up on the React Native Maven mirror first, + * with Maven Central as the fallback when the mirror doesn't have the + * artifact (or errors). Nightlies/snapshots always come from Sonatype. + * ENTERPRISE_REPOSITORY= Custom Maven repository (must match the + * Maven structure). When set it is the ONLY + * repository consulted (no mirror/Central). + * RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED=false|0 + * Skip the mirror and go straight to Maven + * Central. * * Output: * /React.xcframework/ @@ -128,36 +138,69 @@ function parseArgs(argv /*: Array */) /*: DownloadArgs */ { }; } -const MAVEN_RELEASE = - process.env.ENTERPRISE_REPOSITORY ?? 'https://repo1.maven.org/maven2'; +const MAVEN_CENTRAL_REPOSITORY = 'https://repo1.maven.org/maven2'; +const REACT_NATIVE_MAVEN_MIRROR_REPOSITORY = + 'https://repo.reactnative.dev/maven2'; const MAVEN_SNAPSHOT = 'https://central.sonatype.com/repository/maven-snapshots'; -function rnCoreReleaseUrl( +/** + * The mirror is ON unless RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED is + * explicitly "false"/"0". + */ +function reactNativeMavenMirrorEnabled() /*: boolean */ { + const value = process.env.RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED; + if (value == null || value === '') { + return true; + } + return value.toLowerCase() !== 'false' && value !== '0'; +} + +/** + * Release repositories to try, in order. + * When ENTERPRISE_REPOSITORY is set it is the ONLY repository consulted. + * + * Otherwise the React Native Maven mirror is kept before Maven Central so + * cached artifacts are tried first and Maven Central remains the fallback. + */ +function mavenRepositoryUrls() /*: Array */ { + const enterpriseRepository = process.env.ENTERPRISE_REPOSITORY; + if (enterpriseRepository != null && enterpriseRepository !== '') { + return [enterpriseRepository.replace(/\/+$/, '')]; + } + return reactNativeMavenMirrorEnabled() + ? [REACT_NATIVE_MAVEN_MIRROR_REPOSITORY, MAVEN_CENTRAL_REPOSITORY] + : [MAVEN_CENTRAL_REPOSITORY]; +} + +function rnCoreReleaseUrls( version /*: string */, flavor /*: string */, -) /*: string */ { - return ( - `${MAVEN_RELEASE}/com/facebook/react/react-native-artifacts/${version}/` + - `react-native-artifacts-${version}-reactnative-core-${flavor}.tar.gz` +) /*: Array */ { + return mavenRepositoryUrls().map( + repository => + `${repository}/com/facebook/react/react-native-artifacts/${version}/` + + `react-native-artifacts-${version}-reactnative-core-${flavor}.tar.gz`, ); } -function rnDepsReleaseUrl( +function rnDepsReleaseUrls( version /*: string */, flavor /*: string */, -) /*: string */ { - return ( - `${MAVEN_RELEASE}/com/facebook/react/react-native-artifacts/${version}/` + - `react-native-artifacts-${version}-reactnative-dependencies-${flavor}.tar.gz` +) /*: Array */ { + return mavenRepositoryUrls().map( + repository => + `${repository}/com/facebook/react/react-native-artifacts/${version}/` + + `react-native-artifacts-${version}-reactnative-dependencies-${flavor}.tar.gz`, ); } -function hermesReleaseUrl( +function hermesReleaseUrls( version /*: string */, flavor /*: string */, -) /*: string */ { - return ( - `${MAVEN_RELEASE}/com/facebook/hermes/hermes-ios/${version}/` + - `hermes-ios-${version}-hermes-ios-${flavor}.tar.gz` +) /*: Array */ { + return mavenRepositoryUrls().map( + repository => + `${repository}/com/facebook/hermes/hermes-ios/${version}/` + + `hermes-ios-${version}-hermes-ios-${flavor}.tar.gz`, ); } @@ -309,21 +352,52 @@ async function resolveLatestV1Version() /*: Promise */ { return ver; } +// Cache for memorizing the existence of a URL in the release repositories. +const existsCache /*: Map */ = new Map(); + +/** Test hook: forget every memoized probe result. */ +function clearRequestCache() /*: void */ { + existsCache.clear(); +} + async function exists(url /*: string */) /*: Promise */ { + const cached = existsCache.get(url); + if (cached != null) { + return cached; + } + let result = false; try { // $FlowFixMe[incompatible-call] global fetch not in Flow stubs const res = await fetch(url, {method: 'HEAD'}); - return res.status === 200; + result = res.status === 200; } catch { - return false; + // network error — treat the artifact as unavailable on this repository } + existsCache.set(url, result); + return result; +} + +/** + * Probes candidate URLs in order and returns the first one that exists + * (HTTP 200 on HEAD), or null when none do. + */ +async function firstExistingUrl( + candidates /*: Array */, +) /*: Promise */ { + for (const url of candidates) { + if (await exists(url)) { + return url; + } + } + return null; } /** * Returns {url, version} for the React Native core xcframework tarball. * Resolution order: - * 1. Stable release on Maven Central - * 2. Snapshot build on Sonatype + * 1. Stable release on the RN Maven mirror + * 2. Stable release on Maven Central + * 3. Snapshot build on Sonatype */ async function resolveRNCoreArtifact( version /*: string */, @@ -344,12 +418,12 @@ async function resolveRNCoreArtifact( log(` Using LOCAL core tarball: ${localTarball}`); return {url: localTarball, version: `${version}-local`}; } - const releaseUrl = rnCoreReleaseUrl(version, flavor); - if (await exists(releaseUrl)) { + const releaseUrl = await firstExistingUrl(rnCoreReleaseUrls(version, flavor)); + if (releaseUrl != null) { log(` Using stable release: ${releaseUrl}`); return {url: releaseUrl, version}; } - log(` Release not found, trying snapshot...`); + log(` Release not found in any Maven repository, trying snapshot...`); const snapshotUrl = await rnCoreSnapshotUrl(version, flavor); return {url: snapshotUrl, version}; } @@ -383,12 +457,12 @@ async function resolveRNDepsArtifact( version = await resolveNightlyVersion('react-native'); } - const releaseUrl = rnDepsReleaseUrl(version, flavor); - if (await exists(releaseUrl)) { + const releaseUrl = await firstExistingUrl(rnDepsReleaseUrls(version, flavor)); + if (releaseUrl != null) { log(` Using stable release: ${releaseUrl}`); return {url: releaseUrl, version}; } - log(` Release not found, trying snapshot...`); + log(` Release not found in any Maven repository, trying snapshot...`); const snapshotUrl = await rnDepsSnapshotUrl(version, flavor); return {url: snapshotUrl, version}; } @@ -422,12 +496,12 @@ async function resolveHermesArtifact( version = await resolveLatestV1Version(); } - const releaseUrl = hermesReleaseUrl(version, flavor); - if (await exists(releaseUrl)) { + const releaseUrl = await firstExistingUrl(hermesReleaseUrls(version, flavor)); + if (releaseUrl != null) { log(` Using stable release: ${releaseUrl}`); return {url: releaseUrl, version}; } - log(` Release not found, trying snapshot...`); + log(` Release not found in any Maven repository, trying snapshot...`); const snapshotUrl = await hermesSnapshotUrl(version, flavor); return {url: snapshotUrl, version}; } @@ -1393,9 +1467,13 @@ module.exports = { REQUIRED_ARTIFACTS, validateArtifactsCache, // Exposed for unit tests (pure / fetch-stubbable helpers). - rnCoreReleaseUrl, - rnDepsReleaseUrl, - hermesReleaseUrl, + mavenRepositoryUrls, + reactNativeMavenMirrorEnabled, + rnCoreReleaseUrls, + rnDepsReleaseUrls, + hermesReleaseUrls, + firstExistingUrl, + clearRequestCache, resolveSnapshotUrl, resolveNightlyVersion, resolveLatestV1Version, From 9c260b688250789c57cbfc5896b021f8023038e9 Mon Sep 17 00:00:00 2001 From: Dawid Malecki Date: Thu, 30 Jul 2026 12:02:07 +0200 Subject: [PATCH 2/2] revert cache --- .../__tests__/download-spm-artifacts-test.js | 70 +------------------ .../scripts/spm/download-spm-artifacts.js | 20 +----- 2 files changed, 3 insertions(+), 87 deletions(-) diff --git a/packages/react-native/scripts/spm/__tests__/download-spm-artifacts-test.js b/packages/react-native/scripts/spm/__tests__/download-spm-artifacts-test.js index 73832c1ea87c..0896c130ff5a 100644 --- a/packages/react-native/scripts/spm/__tests__/download-spm-artifacts-test.js +++ b/packages/react-native/scripts/spm/__tests__/download-spm-artifacts-test.js @@ -11,7 +11,6 @@ 'use strict'; const { - clearRequestCache, exists, extractXCFramework, findFirst, @@ -38,9 +37,7 @@ const os = require('node:os'); const path = require('node:path'); // Repository-selection env vars are read at call time; clear them per test so -// results don't depend on the host machine, and restore afterwards. The -// probe memo cache is module-global, so it is also reset per test — without -// this, a fetch mock from one test would leak its results into the next. +// results don't depend on the host machine, and restore afterwards. const REPO_ENV_KEYS = [ 'ENTERPRISE_REPOSITORY', 'RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED', @@ -52,7 +49,6 @@ beforeEach(() => { savedRepoEnv[key] = process.env[key]; delete process.env[key]; } - clearRequestCache(); }); afterEach(() => { for (const key of REPO_ENV_KEYS) { @@ -409,70 +405,6 @@ describe('exists', () => { }); }); -// --------------------------------------------------------------------------- -// Probe memoization — only the existence probes against the release -// repositories (RN Maven mirror / Maven Central) are cached per process; -// version lookups always go to the network. -// --------------------------------------------------------------------------- - -describe('probe memoization', () => { - let origFetch; - beforeEach(() => { - origFetch = globalThis.fetch; - }); - afterEach(() => { - globalThis.fetch = origFetch; - }); - - it('exists() probes each URL at most once until the cache is cleared', async () => { - globalThis.fetch = jest.fn(async () => ({status: 200})); - expect(await exists('https://example/x.tar.gz')).toBe(true); - expect(await exists('https://example/x.tar.gz')).toBe(true); - expect(globalThis.fetch).toHaveBeenCalledTimes(1); - - expect(await exists('https://example/y.tar.gz')).toBe(true); - expect(globalThis.fetch).toHaveBeenCalledTimes(2); - - clearRequestCache(); - expect(await exists('https://example/x.tar.gz')).toBe(true); - expect(globalThis.fetch).toHaveBeenCalledTimes(3); - }); - - it('caches negative exists() probes too', async () => { - globalThis.fetch = jest.fn(async () => ({status: 404})); - expect(await exists('https://example/missing.tar.gz')).toBe(false); - expect(await exists('https://example/missing.tar.gz')).toBe(false); - expect(globalThis.fetch).toHaveBeenCalledTimes(1); - }); - - it('does not memoize npm dist-tag lookups', async () => { - globalThis.fetch = routerFetch({ - 'react-native/nightly': {json: {version: '0.86.0-nightly-xyz'}}, - }); - expect(await resolveNightlyVersion('react-native')).toBe( - '0.86.0-nightly-xyz', - ); - expect(await resolveNightlyVersion('react-native')).toBe( - '0.86.0-nightly-xyz', - ); - expect(globalThis.fetch).toHaveBeenCalledTimes(2); - }); - - it('does not memoize snapshot-metadata fetches', async () => { - globalThis.fetch = routerFetch({ - 'maven-metadata.xml': { - text: - '20260101.000000' + - '1', - }, - }); - const args = ['0.85.0', 'react', 'react-native-artifacts', 'x.tar.gz']; - await resolveSnapshotUrl(...args); - await resolveSnapshotUrl(...args); - expect(globalThis.fetch).toHaveBeenCalledTimes(2); - }); -}); - // --------------------------------------------------------------------------- // resolveSnapshotUrl — parses maven-metadata.xml into a timestamped URL. // --------------------------------------------------------------------------- diff --git a/packages/react-native/scripts/spm/download-spm-artifacts.js b/packages/react-native/scripts/spm/download-spm-artifacts.js index 409cd82e38b6..62eaae782373 100644 --- a/packages/react-native/scripts/spm/download-spm-artifacts.js +++ b/packages/react-native/scripts/spm/download-spm-artifacts.js @@ -352,29 +352,14 @@ async function resolveLatestV1Version() /*: Promise */ { return ver; } -// Cache for memorizing the existence of a URL in the release repositories. -const existsCache /*: Map */ = new Map(); - -/** Test hook: forget every memoized probe result. */ -function clearRequestCache() /*: void */ { - existsCache.clear(); -} - async function exists(url /*: string */) /*: Promise */ { - const cached = existsCache.get(url); - if (cached != null) { - return cached; - } - let result = false; try { // $FlowFixMe[incompatible-call] global fetch not in Flow stubs const res = await fetch(url, {method: 'HEAD'}); - result = res.status === 200; + return res.status === 200; } catch { - // network error — treat the artifact as unavailable on this repository + return false; } - existsCache.set(url, result); - return result; } /** @@ -1473,7 +1458,6 @@ module.exports = { rnDepsReleaseUrls, hermesReleaseUrls, firstExistingUrl, - clearRequestCache, resolveSnapshotUrl, resolveNightlyVersion, resolveLatestV1Version,