From d5c8a3c5be67e7db9bf8466204d9ba5c75ece0af Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Mon, 6 Jul 2026 19:29:00 +0100 Subject: [PATCH 1/4] feat: warn about legacy experiments.cache config under rspack 2 Rspack 2 moved persistent cache configuration from `experiments.cache` to the top-level `cache` option and silently ignores the legacy key - users migrating a Rspack 1 config would lose persistent caching without any signal. Emit a one-time actionable warning from the rspack start/bundle commands when running under Rspack 2 with a legacy `experiments.cache` value, pointing at the top-level `cache` option. The config is left untouched: migrating it is the user's move, and mutating it would make Re.Pack behave differently from bare Rspack given the same config. --- .changeset/rspack-2-cache-warning.md | 5 +++ packages/repack/src/commands/common/index.ts | 1 + .../common/warnLegacyRspackCacheConfig.ts | 32 +++++++++++++++++++ packages/repack/src/commands/rspack/bundle.ts | 9 +++++- packages/repack/src/commands/rspack/start.ts | 9 +++++- 5 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 .changeset/rspack-2-cache-warning.md create mode 100644 packages/repack/src/commands/common/warnLegacyRspackCacheConfig.ts diff --git a/.changeset/rspack-2-cache-warning.md b/.changeset/rspack-2-cache-warning.md new file mode 100644 index 000000000..5fa740cc3 --- /dev/null +++ b/.changeset/rspack-2-cache-warning.md @@ -0,0 +1,5 @@ +--- +"@callstack/repack": patch +--- + +Show a one-time warning when a legacy `experiments.cache` value is detected under Rspack 2. Rspack 2 moved persistent cache configuration to the top-level `cache` option and silently ignores the legacy key, so persistent caching would be lost without any signal. The config is left untouched - migrate the value to the top-level `cache` option in your Rspack config. diff --git a/packages/repack/src/commands/common/index.ts b/packages/repack/src/commands/common/index.ts index ce61289b7..bdae6df17 100644 --- a/packages/repack/src/commands/common/index.ts +++ b/packages/repack/src/commands/common/index.ts @@ -8,5 +8,6 @@ export * from './runAdbReverse.js'; export * from './setupEnvironment.js'; export * from './setupInteractions.js'; export * from './setupStatsWriter.js'; +export * from './warnLegacyRspackCacheConfig.js'; export * from './config/makeCompilerConfig.js'; diff --git a/packages/repack/src/commands/common/warnLegacyRspackCacheConfig.ts b/packages/repack/src/commands/common/warnLegacyRspackCacheConfig.ts new file mode 100644 index 000000000..050307598 --- /dev/null +++ b/packages/repack/src/commands/common/warnLegacyRspackCacheConfig.ts @@ -0,0 +1,32 @@ +import * as colorette from 'colorette'; +import type { RspackConfigurationWithLegacyCache } from './resetPersistentCache.js'; + +let warningDisplayed = false; + +/** + * Rspack 2 moved the persistent cache configuration from `experiments.cache` + * to the top-level `cache` option and silently ignores the legacy key + * (validation is loose) - users migrating a Rspack 1 config would lose + * persistent caching without any signal. + * + * Warn (once) when running Rspack 2 with a legacy `experiments.cache` value, + * but leave the config untouched - migrating it is the user's move, and + * mutating it here would make Re.Pack behave differently from bare Rspack + * given the same config. + */ +export function warnLegacyRspackCacheConfig( + configs: RspackConfigurationWithLegacyCache[] +) { + if (warningDisplayed) return; + if (configs.every((config) => config.experiments?.cache === undefined)) { + return; + } + warningDisplayed = true; + console.warn( + colorette.yellow( + "Rspack 2 ignores the legacy 'experiments.cache' option, so persistent " + + "caching is NOT enabled. Move the value to the top-level 'cache' " + + 'option in your Rspack config.\n' + ) + ); +} diff --git a/packages/repack/src/commands/rspack/bundle.ts b/packages/repack/src/commands/rspack/bundle.ts index 810888178..2292885f0 100644 --- a/packages/repack/src/commands/rspack/bundle.ts +++ b/packages/repack/src/commands/rspack/bundle.ts @@ -1,6 +1,6 @@ import { type Configuration, rspack } from '@rspack/core'; import type { Stats } from '@rspack/core'; -import { CLIError } from '../../helpers/index.js'; +import { CLIError, isRspack2 } from '../../helpers/index.js'; import { makeCompilerConfig } from '../common/config/makeCompilerConfig.js'; import { getMaxWorkers, @@ -9,6 +9,7 @@ import { resetPersistentCache, setupEnvironment, setupRspackEnvironment, + warnLegacyRspackCacheConfig, writeStats, } from '../common/index.js'; import type { BundleArguments, CliConfig } from '../types.js'; @@ -36,6 +37,12 @@ export async function bundle( reactNativePath: cliConfig.reactNativePath, }); + // Rspack 2 silently ignores the legacy `experiments.cache` option - + // warn the user so they migrate it to the top-level `cache` option + if (isRspack2(cliConfig.root)) { + warnLegacyRspackCacheConfig([config]); + } + // expose selected args as environment variables setupEnvironment(args); diff --git a/packages/repack/src/commands/rspack/start.ts b/packages/repack/src/commands/rspack/start.ts index c2117b695..936faff48 100644 --- a/packages/repack/src/commands/rspack/start.ts +++ b/packages/repack/src/commands/rspack/start.ts @@ -1,7 +1,7 @@ import type { Configuration, MultiRspackOptions } from '@rspack/core'; import packageJson from '../../../package.json'; import { VERBOSE_ENV_KEY } from '../../env.js'; -import { CLIError, isTruthyEnv } from '../../helpers/index.js'; +import { CLIError, isRspack2, isTruthyEnv } from '../../helpers/index.js'; import { ConsoleReporter, FileReporter, @@ -22,6 +22,7 @@ import { setupEnvironment, setupInteractions, setupRspackEnvironment, + warnLegacyRspackCacheConfig, } from '../common/index.js'; import logo from '../common/logo.js'; import type { CliConfig, StartArguments } from '../types.js'; @@ -58,6 +59,12 @@ export async function start( reactNativePath: cliConfig.reactNativePath, }); + // Rspack 2 silently ignores the legacy `experiments.cache` option - + // warn the user so they migrate it to the top-level `cache` option + if (isRspack2(cliConfig.root)) { + warnLegacyRspackCacheConfig(configs); + } + // expose selected args as environment variables setupEnvironment(args); From c90a6773d4f99537ec48385a54e1496f4a5dfc66 Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Mon, 6 Jul 2026 20:27:53 +0100 Subject: [PATCH 2/4] fix: only warn about legacy cache config when persistent caching is off The warning claimed persistent caching is NOT enabled whenever 'experiments.cache' was present, which is false for a migrated config that sets top-level 'cache: { type: 'persistent' }' and only leaves the inert legacy key behind. Gate the warning on the top-level persistent cache being absent in the same config, and cover the helper with a test matrix (legacy-only, typical Rspack 1 shape with 'cache: true', memory-only top-level cache, migrated leftover key, multi-config, and the once-only latch). --- .../warnLegacyRspackCacheConfig.test.ts | 112 ++++++++++++++++++ .../common/warnLegacyRspackCacheConfig.ts | 23 ++-- 2 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 packages/repack/src/commands/common/__tests__/warnLegacyRspackCacheConfig.test.ts diff --git a/packages/repack/src/commands/common/__tests__/warnLegacyRspackCacheConfig.test.ts b/packages/repack/src/commands/common/__tests__/warnLegacyRspackCacheConfig.test.ts new file mode 100644 index 000000000..97c1f065d --- /dev/null +++ b/packages/repack/src/commands/common/__tests__/warnLegacyRspackCacheConfig.test.ts @@ -0,0 +1,112 @@ +describe('warnLegacyRspackCacheConfig', () => { + let warnSpy: jest.SpyInstance; + + beforeEach(() => { + // the helper keeps module-level once-only state - re-evaluate it per test + jest.resetModules(); + warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + warnSpy.mockRestore(); + }); + + const loadHelper = () => { + // require instead of import so jest.resetModules() re-evaluates the module + const module: typeof import( + '../warnLegacyRspackCacheConfig.js' + ) = require('../warnLegacyRspackCacheConfig.js'); + return module.warnLegacyRspackCacheConfig; + }; + + it('does not warn when no config uses the legacy experiments.cache key', () => { + const warnLegacyRspackCacheConfig = loadHelper(); + + warnLegacyRspackCacheConfig([ + {}, + { cache: true }, + { cache: { type: 'persistent' } }, + { experiments: {} }, + ]); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('warns when a config has experiments.cache and no top-level cache', () => { + const warnLegacyRspackCacheConfig = loadHelper(); + + warnLegacyRspackCacheConfig([ + { experiments: { cache: { type: 'persistent' } } }, + ]); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy.mock.calls[0][0]).toMatch(/experiments\.cache/); + expect(warnSpy.mock.calls[0][0]).toMatch(/top-level 'cache'/); + }); + + it('warns for the typical Rspack 1 shape (cache: true + experiments.cache)', () => { + const warnLegacyRspackCacheConfig = loadHelper(); + + // under Rspack 2 `cache: true` enables the memory cache only, + // so persistent caching is still not in effect + warnLegacyRspackCacheConfig([ + { cache: true, experiments: { cache: { type: 'persistent' } } }, + ]); + + expect(warnSpy).toHaveBeenCalledTimes(1); + }); + + it('warns when top-level cache is memory-only next to the legacy key', () => { + const warnLegacyRspackCacheConfig = loadHelper(); + + warnLegacyRspackCacheConfig([ + { + cache: { type: 'memory' }, + experiments: { cache: { type: 'persistent' } }, + }, + ]); + + expect(warnSpy).toHaveBeenCalledTimes(1); + }); + + it('does not warn for a migrated config that only left the inert legacy key behind', () => { + const warnLegacyRspackCacheConfig = loadHelper(); + + // persistent caching IS enabled here - warning that it is not would be false + warnLegacyRspackCacheConfig([ + { + cache: { type: 'persistent' }, + experiments: { cache: { type: 'persistent' } }, + }, + ]); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('warns when any config of a multi-config array is misconfigured', () => { + const warnLegacyRspackCacheConfig = loadHelper(); + + warnLegacyRspackCacheConfig([ + { + cache: { type: 'persistent' }, + experiments: { cache: { type: 'persistent' } }, + }, + { experiments: { cache: { type: 'persistent' } } }, + ]); + + expect(warnSpy).toHaveBeenCalledTimes(1); + }); + + it('warns only once across repeated calls', () => { + const warnLegacyRspackCacheConfig = loadHelper(); + + warnLegacyRspackCacheConfig([ + { experiments: { cache: { type: 'persistent' } } }, + ]); + warnLegacyRspackCacheConfig([ + { experiments: { cache: { type: 'persistent' } } }, + ]); + + expect(warnSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/repack/src/commands/common/warnLegacyRspackCacheConfig.ts b/packages/repack/src/commands/common/warnLegacyRspackCacheConfig.ts index 050307598..165e4989f 100644 --- a/packages/repack/src/commands/common/warnLegacyRspackCacheConfig.ts +++ b/packages/repack/src/commands/common/warnLegacyRspackCacheConfig.ts @@ -3,24 +3,33 @@ import type { RspackConfigurationWithLegacyCache } from './resetPersistentCache. let warningDisplayed = false; +function hasPersistentCacheEnabled(config: RspackConfigurationWithLegacyCache) { + return typeof config.cache === 'object' && config.cache.type === 'persistent'; +} + /** * Rspack 2 moved the persistent cache configuration from `experiments.cache` * to the top-level `cache` option and silently ignores the legacy key * (validation is loose) - users migrating a Rspack 1 config would lose * persistent caching without any signal. * - * Warn (once) when running Rspack 2 with a legacy `experiments.cache` value, - * but leave the config untouched - migrating it is the user's move, and - * mutating it here would make Re.Pack behave differently from bare Rspack - * given the same config. + * Warn (once) when running Rspack 2 with a legacy `experiments.cache` value + * and no top-level persistent `cache` option (a migrated config that only + * left the inert legacy key behind has persistent caching enabled, so the + * warning would be false there). The config is left untouched - migrating it + * is the user's move, and mutating it here would make Re.Pack behave + * differently from bare Rspack given the same config. */ export function warnLegacyRspackCacheConfig( configs: RspackConfigurationWithLegacyCache[] ) { if (warningDisplayed) return; - if (configs.every((config) => config.experiments?.cache === undefined)) { - return; - } + const misconfigured = configs.some( + (config) => + config.experiments?.cache !== undefined && + !hasPersistentCacheEnabled(config) + ); + if (!misconfigured) return; warningDisplayed = true; console.warn( colorette.yellow( From 614bdfe1e8bea36c37560bbc17930b018a5661b6 Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Tue, 7 Jul 2026 23:11:58 +0100 Subject: [PATCH 3/4] fix: warn when ignored experiments.cache options are not migrated to top-level cache --- .../warnLegacyRspackCacheConfig.test.ts | 83 ++++++++++++++++- .../common/warnLegacyRspackCacheConfig.ts | 90 +++++++++++++++---- 2 files changed, 156 insertions(+), 17 deletions(-) diff --git a/packages/repack/src/commands/common/__tests__/warnLegacyRspackCacheConfig.test.ts b/packages/repack/src/commands/common/__tests__/warnLegacyRspackCacheConfig.test.ts index 97c1f065d..41a92931a 100644 --- a/packages/repack/src/commands/common/__tests__/warnLegacyRspackCacheConfig.test.ts +++ b/packages/repack/src/commands/common/__tests__/warnLegacyRspackCacheConfig.test.ts @@ -72,10 +72,11 @@ describe('warnLegacyRspackCacheConfig', () => { it('does not warn for a migrated config that only left the inert legacy key behind', () => { const warnLegacyRspackCacheConfig = loadHelper(); - // persistent caching IS enabled here - warning that it is not would be false + // persistent caching IS enabled and the legacy key carries no options + // beyond `type` - there is nothing Rspack 2 could drop warnLegacyRspackCacheConfig([ { - cache: { type: 'persistent' }, + cache: { type: 'persistent', storage: { directory: '/custom' } }, experiments: { cache: { type: 'persistent' } }, }, ]); @@ -83,6 +84,84 @@ describe('warnLegacyRspackCacheConfig', () => { expect(warnSpy).not.toHaveBeenCalled(); }); + it('does not warn when the legacy key is deep-equal to the top-level cache', () => { + const warnLegacyRspackCacheConfig = loadHelper(); + + // every option under the legacy key is mirrored at the top level, + // so ignoring the legacy key changes nothing + warnLegacyRspackCacheConfig([ + { + cache: { + type: 'persistent', + storage: { directory: '/custom' }, + buildDependencies: ['/project/rspack.config.mjs'], + }, + experiments: { + cache: { + type: 'persistent', + storage: { directory: '/custom' }, + buildDependencies: ['/project/rspack.config.mjs'], + }, + }, + }, + ]); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('warns softly for a partial migration that leaves options under the legacy key', () => { + const warnLegacyRspackCacheConfig = loadHelper(); + + // persistent caching IS enabled, but the storage directory only lives + // under the legacy key - Rspack 2 drops it and uses the default location + warnLegacyRspackCacheConfig([ + { + cache: { type: 'persistent' }, + experiments: { + cache: { type: 'persistent', storage: { directory: '/custom' } }, + }, + }, + ]); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy.mock.calls[0][0]).toMatch(/experiments\.cache/); + expect(warnSpy.mock.calls[0][0]).toMatch(/not applied/); + // caching IS enabled - the soft warning must not claim otherwise + expect(warnSpy.mock.calls[0][0]).not.toMatch(/NOT enabled/); + }); + + it('warns strongly when legacy options come without a top-level persistent cache', () => { + const warnLegacyRspackCacheConfig = loadHelper(); + + warnLegacyRspackCacheConfig([ + { + experiments: { + cache: { type: 'persistent', storage: { directory: '/custom' } }, + }, + }, + ]); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy.mock.calls[0][0]).toMatch(/NOT enabled/); + }); + + it('prefers the strong warning when configs trigger both tiers', () => { + const warnLegacyRspackCacheConfig = loadHelper(); + + warnLegacyRspackCacheConfig([ + { + cache: { type: 'persistent' }, + experiments: { + cache: { type: 'persistent', storage: { directory: '/custom' } }, + }, + }, + { experiments: { cache: { type: 'persistent' } } }, + ]); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy.mock.calls[0][0]).toMatch(/NOT enabled/); + }); + it('warns when any config of a multi-config array is misconfigured', () => { const warnLegacyRspackCacheConfig = loadHelper(); diff --git a/packages/repack/src/commands/common/warnLegacyRspackCacheConfig.ts b/packages/repack/src/commands/common/warnLegacyRspackCacheConfig.ts index 165e4989f..ce5d9df6f 100644 --- a/packages/repack/src/commands/common/warnLegacyRspackCacheConfig.ts +++ b/packages/repack/src/commands/common/warnLegacyRspackCacheConfig.ts @@ -7,35 +7,95 @@ function hasPersistentCacheEnabled(config: RspackConfigurationWithLegacyCache) { return typeof config.cache === 'object' && config.cache.type === 'persistent'; } +function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function isDeepEqual(a: unknown, b: unknown): boolean { + if (a === b) return true; + if (Array.isArray(a) && Array.isArray(b)) { + return ( + a.length === b.length && a.every((item, i) => isDeepEqual(item, b[i])) + ); + } + if (isPlainObject(a) && isPlainObject(b)) { + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); + return ( + aKeys.length === bKeys.length && + aKeys.every((key) => key in b && isDeepEqual(a[key], b[key])) + ); + } + return false; +} + +/** + * A legacy `experiments.cache` value carries extra information when it is an + * object with options beyond `type` that are not mirrored by the top-level + * `cache` value - Rspack 2 drops those options and runs with defaults. + * Booleans, a bare `{ type: 'persistent' }` and a value deep-equal to the + * top-level `cache` are inert leftovers of a completed migration. + */ +function legacyCacheCarriesExtraOptions( + config: RspackConfigurationWithLegacyCache +) { + const legacyCache = config.experiments?.cache; + if (!isPlainObject(legacyCache)) return false; + const extraKeys = Object.keys(legacyCache).filter((key) => key !== 'type'); + if (extraKeys.length === 0) return false; + return !isDeepEqual(legacyCache, config.cache); +} + /** * Rspack 2 moved the persistent cache configuration from `experiments.cache` * to the top-level `cache` option and silently ignores the legacy key * (validation is loose) - users migrating a Rspack 1 config would lose * persistent caching without any signal. * - * Warn (once) when running Rspack 2 with a legacy `experiments.cache` value - * and no top-level persistent `cache` option (a migrated config that only - * left the inert legacy key behind has persistent caching enabled, so the - * warning would be false there). The config is left untouched - migrating it - * is the user's move, and mutating it here would make Re.Pack behave - * differently from bare Rspack given the same config. + * Warn (once) when running Rspack 2 with a legacy `experiments.cache` value: + * - without a top-level persistent `cache` option, persistent caching is NOT + * in effect - warn that it is disabled. + * - with a top-level persistent `cache` option, caching IS enabled, but any + * options that only live under the legacy key are silently dropped - warn + * (more softly) that they are not applied. A truly inert leftover (a + * boolean, a bare `{ type: 'persistent' }` or a value deep-equal to the + * top-level `cache`) stays silent. + * + * The config is left untouched - migrating it is the user's move, and + * mutating it here would make Re.Pack behave differently from bare Rspack + * given the same config. */ export function warnLegacyRspackCacheConfig( configs: RspackConfigurationWithLegacyCache[] ) { if (warningDisplayed) return; - const misconfigured = configs.some( + const cachingDisabled = configs.some( (config) => config.experiments?.cache !== undefined && !hasPersistentCacheEnabled(config) ); - if (!misconfigured) return; - warningDisplayed = true; - console.warn( - colorette.yellow( - "Rspack 2 ignores the legacy 'experiments.cache' option, so persistent " + - "caching is NOT enabled. Move the value to the top-level 'cache' " + - 'option in your Rspack config.\n' - ) + const optionsDropped = configs.some( + (config) => + hasPersistentCacheEnabled(config) && + legacyCacheCarriesExtraOptions(config) ); + if (!cachingDisabled && !optionsDropped) return; + warningDisplayed = true; + if (cachingDisabled) { + console.warn( + colorette.yellow( + "Rspack 2 ignores the legacy 'experiments.cache' option, so persistent " + + "caching is NOT enabled. Move the value to the top-level 'cache' " + + 'option in your Rspack config.\n' + ) + ); + } else { + console.warn( + colorette.yellow( + "Rspack 2 ignores the legacy 'experiments.cache' option - options set " + + "there are not applied. Move them to the top-level 'cache' option " + + 'in your Rspack config or remove the key.\n' + ) + ); + } } From 625c6964351317813ae2e674f592689854c0dacd Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Tue, 7 Jul 2026 23:17:19 +0100 Subject: [PATCH 4/4] fix: satisfy CacheStorageOptions in cache warning tests --- .../warnLegacyRspackCacheConfig.test.ts | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/repack/src/commands/common/__tests__/warnLegacyRspackCacheConfig.test.ts b/packages/repack/src/commands/common/__tests__/warnLegacyRspackCacheConfig.test.ts index 41a92931a..ada707008 100644 --- a/packages/repack/src/commands/common/__tests__/warnLegacyRspackCacheConfig.test.ts +++ b/packages/repack/src/commands/common/__tests__/warnLegacyRspackCacheConfig.test.ts @@ -76,7 +76,10 @@ describe('warnLegacyRspackCacheConfig', () => { // beyond `type` - there is nothing Rspack 2 could drop warnLegacyRspackCacheConfig([ { - cache: { type: 'persistent', storage: { directory: '/custom' } }, + cache: { + type: 'persistent', + storage: { type: 'filesystem', directory: '/custom' }, + }, experiments: { cache: { type: 'persistent' } }, }, ]); @@ -93,13 +96,13 @@ describe('warnLegacyRspackCacheConfig', () => { { cache: { type: 'persistent', - storage: { directory: '/custom' }, + storage: { type: 'filesystem', directory: '/custom' }, buildDependencies: ['/project/rspack.config.mjs'], }, experiments: { cache: { type: 'persistent', - storage: { directory: '/custom' }, + storage: { type: 'filesystem', directory: '/custom' }, buildDependencies: ['/project/rspack.config.mjs'], }, }, @@ -118,7 +121,10 @@ describe('warnLegacyRspackCacheConfig', () => { { cache: { type: 'persistent' }, experiments: { - cache: { type: 'persistent', storage: { directory: '/custom' } }, + cache: { + type: 'persistent', + storage: { type: 'filesystem', directory: '/custom' }, + }, }, }, ]); @@ -136,7 +142,10 @@ describe('warnLegacyRspackCacheConfig', () => { warnLegacyRspackCacheConfig([ { experiments: { - cache: { type: 'persistent', storage: { directory: '/custom' } }, + cache: { + type: 'persistent', + storage: { type: 'filesystem', directory: '/custom' }, + }, }, }, ]); @@ -152,7 +161,10 @@ describe('warnLegacyRspackCacheConfig', () => { { cache: { type: 'persistent' }, experiments: { - cache: { type: 'persistent', storage: { directory: '/custom' } }, + cache: { + type: 'persistent', + storage: { type: 'filesystem', directory: '/custom' }, + }, }, }, { experiments: { cache: { type: 'persistent' } } },