Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

Dev server progress goes backwards mid-compilation (non-monotonic percentage forwarded to reporters)

オープン
#1,459 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
3/5
見積もり時間
1〜2日
初心者へのやさしさ
70/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
活発
技術スタック
react-native, typescript, webpack

調査の方向性

Read packages/repack/src/commands/rspack/Compiler.ts and packages/repack/src/commands/webpack/Compiler.ts, then run the standalone repro.mjs described in the issue to observe the regression. Implement the per-platform progress behavior proposed in the issue, and verify that reported percentages never decrease during a compilation while a platform restart resets its own maximum.

索引モデルが issue の本文から書いたものです。

説明

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:

  1. 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)
  2. Nothing upstream clamps it. Math.floor(value * 100) is the whole transformation, and I found no monotonicity guard in main (@ 8fba59747fe1) 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
  1. mkdir repro && cd repro && npm init -y && npm i @rspack/core
  2. Save the script below as repro.mjs
  3. 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.

主要言語
TypeScript
スター
1.9k
フォーク
164
平均マージ
10日 13時間
マージ済み PR(30日)
10

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

callstack/repack のほかの issue

callstack/repack の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。