Describe the bug
During development, the build progress percentage Re.Pack reports for a platform decreases in the middle of a single compilation. Re.Pack forwards the bundler's percentage straight through to both consumers, so the value shown to the developer visibly runs backwards.
What I expect: within one compilation, the reported progress never goes down.
What actually happens (single platform, single compilation, development build):
63% -> 63% -> 27% -> 64% -> 64% -> ... -> 100%
Two things make this worth reporting separately from any app:
- It is not caused by Re.Pack's own logic. The reproduction below drives rspack's
ProgressPlugin directly with one configuration - no React Native app, no second platform, no watch mode - and the percentage handed to the handler still decreases. Re.Pack then forwards that value unmodified on both code paths:
packages/repack/src/commands/rspack/Compiler.ts:43 - const percentage = Math.floor(value * 100); forwarded to every progressSenders[platform] and into the progress: { platform, value } message
packages/repack/src/commands/webpack/Compiler.ts:163 - const percentage = Math.floor(value.percentage * 100);, same pattern (I verified the rspack path empirically; for the webpack path I am reading the code, not reporting a measurement)
- Nothing upstream clamps it.
Math.floor(value * 100) is the whole transformation, and I found no monotonicity guard in main (@ 8fba597) or in the published 5.3.0 / 5.4.0-canary-20260913172854 builds.
The shape of the drop points at a modules-ratio whose total grows as modules are discovered. From a real run, with the plugin's own message next to each percentage, the total jumps while the percentage falls:
62% | build modules (183)
63% | build modules (219)
63% | build modules (255)
27% | build modules (289) <-- reported percentage falls
64% | build modules (919) <-- module total jumped ~3.2x
64% | build modules (955)
Impact for us: the React Native dev-loading view and the terminal both show a counter that goes backwards mid-build, which reads like a stuck or restarting build. Anything downstream that assumes monotonic input (progress bars, animations, code waiting for a percentage threshold) inherits the problem.
Proposal: clamp the forwarded value per platform - keep the maximum seen so far within a compilation, and reset per platform when that platform's compilation restarts (a reset that clears every platform would let another platform's restart un-clamp this one on a multi-platform dev server). I am happy to open a PR if you agree the guard belongs in Re.Pack rather than in each app.
System Info
The reproduction is standalone and does not involve a React Native app, so a react-native info dump would describe a different project than the one that reproduces the bug. This is the environment the output below was produced in:
macOS 26.5.2 (arm64)
node 26.7.0
npm 11.19.0
@rspack/core 2.2.4
@callstack/repack 5.3.0
Happy to post a full npx react-native info from the app where this is user-visible if that is more useful than the standalone case.
Re.Pack Version
5.3.0 in the app where the symptom is visible to developers.
Checked and also affected - the forwarding code is unchanged: main at 8fba59747fe1 and 5.4.0-canary-20260913172854 (no clamp anywhere; Math.floor(value * 100) is the only transformation). Note the reproduction below needs only @rspack/core, so the underlying report is independent of the Re.Pack version - Re.Pack's part is forwarding a value that can regress.
Reproduction
Inline below rather than a repository link: the whole reproduction is one dependency-free script (@rspack/core only), and a link would add a step without adding information. Say the word and I will publish it as a gist or a tiny repository.
mkdir repro && cd repro && npm init -y && npm i @rspack/core && node repro.mjs
Steps to reproduce
mkdir repro && cd repro && npm init -y && npm i @rspack/core
- Save the script below as
repro.mjs
node repro.mjs
It builds a synthetic module graph: a long serial chain (so progress climbs smoothly while modules are discovered one step at a time), with one wide subtree hanging off the middle of the chain so the module total jumps late in the build. A small loader adds a delay per module so the plugin's throttled handler samples the module-build phase repeatedly. No React Native, no Re.Pack, no watch mode, one configuration.
/**
* Minimal reproduction: a single rspack (React Native dev server uses one child
* compiler per platform) compilation reports a NON-MONOTONIC progress percentage.
*
* usage: node repro.mjs (needs @rspack/core resolvable)
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { rspack } from '@rspack/core';
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'progress-monotonic-'));
const src = path.join(dir, 'src');
fs.mkdirSync(src);
const write = (name, code) => fs.writeFileSync(path.join(src, name), code);
// A serialized dependency chain: modules are discovered step by step, so the
// completed/total ratio climbs steadily and the percentage rises to ~64%.
const DEPTH = 60;
for (let c = 0; c < DEPTH; c++) {
let code = '';
for (let k = 0; k < 8; k++) {
write(`l${c}_${k}.js`, `export const v = ${k};\n`);
code += `import { v } from './l${c}_${k}.js';\n`;
}
// Halfway through the chain, one module pulls in a wide subtree. The module
// count jumps from ~500 to ~1100 while only a handful of modules complete.
if (c === DEPTH / 2) code += `import { big } from './big.js';\n`;
if (c < DEPTH - 1) code += `import { next } from './c${c + 1}.js';\n`;
code += 'export const next = 1;\n';
write(`c${c}.js`, code);
}
write(
'big.js',
Array.from({ length: 600 }, (_, k) => {
write(`d${k}.js`, `export const d = ${k};\n`);
return `import { d } from './d${k}.js';`;
}).join('\n') + '\nexport const big = 1;\n'
);
write('entry.js', `import { next } from './c0.js';\nexport default next;\n`);
const samples = [];
const compiler = rspack({
name: 'ios',
mode: 'development',
entry: path.join(src, 'entry.js'),
output: { path: path.join(dir, 'dist'), filename: 'main.js' },
// Slows each module build so ProgressPlugin's 100ms throttle samples the
// module-build phase several times instead of twice.
module: {
rules: [
{
test: /[\\/]src[\\/]/,
use: [{ loader: path.join(dir, 'delay-loader.mjs') }],
},
],
},
plugins: [
new rspack.ProgressPlugin((percentage, msg) => {
samples.push({ pct: Math.floor(percentage * 100), msg });
}),
],
});
fs.writeFileSync(
path.join(dir, 'delay-loader.mjs'),
'export default function loader(source){const cb=this.async();setTimeout(()=>cb(null,source),25);return undefined;}\n'
);
compiler.run((error, stats) => {
if (error) throw error;
if (stats.hasErrors())
console.error(stats.toJson({ all: false, errors: true }).errors.slice(0, 2));
let prev = -1;
const decreases = [];
for (const sample of samples) {
if (sample.pct < prev) {
const i = samples.indexOf(sample);
decreases.push(`${samples[i - 1].pct}% -> ${sample.pct}% (${sample.msg})`);
}
prev = sample.pct;
}
console.log('module-build phase:', samples.map((s) => s.pct).join(' -> '));
console.log(
'NON-MONOTONIC DECREASES:',
decreases.length ? decreases.join(', ') : 'none'
);
compiler.close(() => {});
});
Observed today with @rspack/core 2.2.4, three consecutive runs of the unmodified script:
# 2.2.4, run 1
module-build phase: 8 -> 9 -> 10 -> 25 -> 55 -> 59 -> 61 -> 62 -> 62 -> 63 -> 63 -> 38 -> 64 -> 64 -> 64 -> 64 -> 64 -> 64 -> 64 -> 69 -> 70 -> 70 -> 71 -> 74 -> 75 -> 75 -> 77 -> 78 -> 80 -> 83 -> 87 -> 93 -> 93 -> 98 -> 100
NON-MONOTONIC DECREASES: 63% -> 38% (build modules (460))
# 2.2.4, run 2
module-build phase: 8 -> 9 -> 10 -> 25 -> 55 -> 59 -> 61 -> 62 -> 62 -> 63 -> 63 -> 27 -> 64 -> 64 -> 64 -> 64 -> 64 -> 64 -> 64 -> 69 -> 70 -> 70 -> 71 -> 74 -> 75 -> 75 -> 77 -> 78 -> 80 -> 83 -> 87 -> 93 -> 93 -> 98 -> 100
NON-MONOTONIC DECREASES: 63% -> 27% (build modules (283))
# 2.2.4, run 3
NON-MONOTONIC DECREASES: 63% -> 27% (build modules (292))
The same script reproduces on @rspack/core 2.1.3 (NON-MONOTONIC DECREASES: 63% -> 27% (build modules (283))), so this is not new in the latest release.
The dip magnitude varies per run because it depends on where the module total jumps relative to the handler's throttle window. Applying only a per-compilation maximum to the same script, changing nothing else, makes every sample monotonic:
NON-MONOTONIC DECREASES: none
In the app, the same value is what reaches the terminal reporter and the React Native dev-loading view, so this is what developers see. We currently carry a local patch that clamps the forwarded percentage per platform as a workaround, and would much rather delete it once the guard lives here.
Describe the bug
During development, the build progress percentage Re.Pack reports for a platform decreases in the middle of a single compilation. Re.Pack forwards the bundler's percentage straight through to both consumers, so the value shown to the developer visibly runs backwards.
What I expect: within one compilation, the reported progress never goes down.
What actually happens (single platform, single compilation, development build):
Two things make this worth reporting separately from any app:
ProgressPlugindirectly with one configuration - no React Native app, no second platform, no watch mode - and the percentage handed to the handler still decreases. Re.Pack then forwards that value unmodified on both code paths:packages/repack/src/commands/rspack/Compiler.ts:43-const percentage = Math.floor(value * 100);forwarded to everyprogressSenders[platform]and into theprogress: { platform, value }messagepackages/repack/src/commands/webpack/Compiler.ts:163-const percentage = Math.floor(value.percentage * 100);, same pattern (I verified the rspack path empirically; for the webpack path I am reading the code, not reporting a measurement)Math.floor(value * 100)is the whole transformation, and I found no monotonicity guard inmain(@ 8fba597) or in the published5.3.0/5.4.0-canary-20260913172854builds.The shape of the drop points at a modules-ratio whose total grows as modules are discovered. From a real run, with the plugin's own message next to each percentage, the total jumps while the percentage falls:
Impact for us: the React Native dev-loading view and the terminal both show a counter that goes backwards mid-build, which reads like a stuck or restarting build. Anything downstream that assumes monotonic input (progress bars, animations, code waiting for a percentage threshold) inherits the problem.
Proposal: clamp the forwarded value per platform - keep the maximum seen so far within a compilation, and reset per platform when that platform's compilation restarts (a reset that clears every platform would let another platform's restart un-clamp this one on a multi-platform dev server). I am happy to open a PR if you agree the guard belongs in Re.Pack rather than in each app.
System Info
The reproduction is standalone and does not involve a React Native app, so a
react-native infodump would describe a different project than the one that reproduces the bug. This is the environment the output below was produced in:Happy to post a full
npx react-native infofrom the app where this is user-visible if that is more useful than the standalone case.Re.Pack Version
5.3.0in the app where the symptom is visible to developers.Checked and also affected - the forwarding code is unchanged:
mainat8fba59747fe1and5.4.0-canary-20260913172854(no clamp anywhere;Math.floor(value * 100)is the only transformation). Note the reproduction below needs only@rspack/core, so the underlying report is independent of the Re.Pack version - Re.Pack's part is forwarding a value that can regress.Reproduction
Inline below rather than a repository link: the whole reproduction is one dependency-free script (
@rspack/coreonly), and a link would add a step without adding information. Say the word and I will publish it as a gist or a tiny repository.Steps to reproduce
mkdir repro && cd repro && npm init -y && npm i @rspack/corerepro.mjsnode repro.mjsIt builds a synthetic module graph: a long serial chain (so progress climbs smoothly while modules are discovered one step at a time), with one wide subtree hanging off the middle of the chain so the module total jumps late in the build. A small loader adds a delay per module so the plugin's throttled handler samples the module-build phase repeatedly. No React Native, no Re.Pack, no watch mode, one configuration.
Observed today with
@rspack/core2.2.4, three consecutive runs of the unmodified script:The same script reproduces on
@rspack/core2.1.3 (NON-MONOTONIC DECREASES: 63% -> 27% (build modules (283))), so this is not new in the latest release.The dip magnitude varies per run because it depends on where the module total jumps relative to the handler's throttle window. Applying only a per-compilation maximum to the same script, changing nothing else, makes every sample monotonic:
In the app, the same value is what reaches the terminal reporter and the React Native dev-loading view, so this is what developers see. We currently carry a local patch that clamps the forwarded percentage per platform as a workaround, and would much rather delete it once the guard lives here.