Skip to content

v7.0.0 - #3022

Merged
tanem merged 17 commits into
masterfrom
v7
Jul 31, 2026
Merged

v7.0.0#3022
tanem merged 17 commits into
masterfrom
v7

Conversation

@tanem

@tanem tanem commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

v7.0.0. Fifteen commits, each with the detail in its own message; the consumer-facing changes are all in MIGRATION.md.

Breaking

  • Drops the withNProgress HOC. React's docs treat higher-order components as a legacy pattern, and it was the only consumer of hoist-non-react-statics. The replacement is one line of consumer code.
  • Drops the UMD builds. React 19 ships no UMD build of its own, so the script-tag path already only worked with React 18 and earlier.
  • Replaces the tsc → Rollup + Babel pipeline with a single tsdown build, and adds an exports map. Output filenames change to dist/react-nprogress.cjs / .mjs with matching .d.cts / .d.mts. Node ESM consumers were resolving to the CommonJS build because Node ignores module; they now get the ES module build. main, module and types stay for webpack 4 and TypeScript node10 resolution.
  • Drops the separate development and production CommonJS builds. They differed only by minification, which bundlers apply themselves, so process.env.NODE_ENV is no longer read at require time.
  • Drops @babel/runtime. The package now has no runtime dependencies, only peers.

Fixes

  • Fixes two StrictMode bugs. Mounted under StrictMode, useNProgress() with isAnimating: false animated straight to progress: 1, and useNProgress({ isAnimating: true }) reported { isFinished: true, progress: 0 } instead of starting. Both came from useUpdateEffect, whose first-mount ref is flipped during render, so StrictMode's double-invoked render made the committed render look like an update. The hook is now a { phase, progress } reducer over four phases, gated on the phase rather than on whether this is the first render. That removes the sideEffect-in-state bridge, three hooks vendored from react-use, createQueue and four eslint suppressions. Observable pacing is unchanged: every pre-existing spec passes unedited.
  • Fixes two cases where changing an option mid-animation misbehaved, both predating v7. Changing minimum rewound the bar, because start re-ran against an animation already in flight; on the published 6.0.4 the same change reset progress to 0 outright. Changing animationDuration, which the animating phase never reads, cancelled the pending trickle. start is now guarded on the phase the same way complete is, and the timer effect is split per phase so each depends only on the options its own phase reads. Three specs cover it.
  • Adds "use client" to the published bundles. Every export is or wraps a hook and the timers run on window.requestAnimationFrame, so importing from a server component now fails at the module boundary rather than with a hooks error at render time.
  • Fixes the README Usage snippets, which mounted with render from react-dom, removed in React 19, so the first thing a new user copied threw at runtime.
  • Fixes the react-router example, which passed one unattached nodeRef to every CSSTransition, so the bar completed 323ms after a click instead of running for the configured 1200ms. Predates v7: reproduced against the published 6.0.4.
  • Makes the app-router example's slow pages render per request. await new Promise(setTimeout) carries no dynamic signal, so Next ran it at build time and both routes shipped static; only next dev behaved as intended.

Additions

  • Exports NProgressOptions and NProgressState. Both shapes previously reached the declarations as unnamed local interfaces, so wrapping either entry point meant Parameters<typeof useNProgress>[0]. Type-only, so the runtime bundles are byte-identical.
  • Gates the published package shape with publint and arethetypeswrong, as a postbuild hook.
  • Asserts what the build emits in test/bundles.spec.ts, run from its own Jest config after the build. Keeping it out of config.src.js leaves npm run test:src and the React matrix runnable on a clean checkout, and stops the bundle assertions repeating once per React version.
  • Gates bundle size with size-limit, gzip to match the README badge. Actuals: ESM 1003 B, CJS 1063 B.
  • Adds React 19.2 to the test matrix as the current 19 latest minor.
  • Runs CI on v* branches. Both filters were limited to master, so v7 ran no CI at all.

Docs

  • Restructures the README API section into a table of options with one heading per option, adds a "When to Use This" section, and documents what the old bullets left out: minimum is clamped against the first increment, and isAnimating going false is what drives completion.
  • Moves .github/copilot-instructions.md to a root AGENTS.md, read by Claude Code, Copilot, Cursor and Codex. Copilot code review gained root AGENTS.md support in June 2026, so the vendor-specific file is deleted rather than kept as a pointer.

Test plan

  • npm test passes: format and type checks, lint, build, publint, arethetypeswrong, size gate, then src, CJS, ESM, bundle and React-matrix specs
  • Coverage of src stays at 100%
  • CI green on every commit in the branch
  • arethetypeswrong reports no problems under node10, node16 (from CJS and ESM) and bundler resolution
  • All eight examples clean-installed against a npm pack tarball of this branch and driven in a headless browser: each starts, advances and completes with no console errors
  • Both Next examples additionally checked under next build && next start, confirming the CJS entry and the "use client" directive hold on the server
    Labelled breaking, so the Monday release cron derives a major bump.

🤖 Generated with Claude Code

Both the push and pull_request filters were limited to master, so commits
pushed to a version branch such as v7 ran no CI. Using [master, 'v*'] covers
future majors without a further edit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tanem tanem added the breaking label Jul 31, 2026
tanem and others added 14 commits August 1, 2026 07:44
Every export is or wraps a hook and the timers run on
window.requestAnimationFrame, so the package is client-only. Marking the
published bundles means importing it from a React server component fails
at the module boundary rather than with a hooks error at render time.

Rollup strips file-level directives while bundling, so the directive is
added via output.banner on the CJS and ES configs. Terser classifies
'use client' as a non-standard directive and drops it from the minified
CJS bundle unless compress.directives is disabled. The dist/index.js
dev/prod shim carries it in source, above 'use strict'.

UMD bundles are left alone: they are script-tag targets that never enter
a server/client module graph.

test/bundles.spec.ts asserts the directive on every published module,
derived from the contents of dist rather than a hard-coded bundle list,
so it holds across a change of build tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nothing checked what npm actually publishes. publint audits the
package.json/exports shape; arethetypeswrong verifies TypeScript consumers
resolve types under node10, node16 (from CJS and ESM) and bundler
resolution. Both run as a postbuild hook rather than alongside check:*,
because check:* runs before build in the test script order and these need
dist/ to exist.

attw's --pack flag cannot be used directly here: it shells out to npm pack,
which runs prepare, which runs build, whose postbuild hook runs attw again.
scripts/attw.js packs with --ignore-scripts into a temp directory and hands
attw the tarball, which breaks the loop. publint needs no equivalent
workaround, as it builds its own file list without running lifecycle
scripts.

Current state passes: attw reports no problems in all four modes, and
publint reports no errors. publint's warning about react-nprogress.esm.js
being interpreted as CJS, and its suggestions for a type field and an
exports map, are left standing for now; the exports map work resolves them.
Warnings do not fail publint, so the gate is green today.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
React 19 ships no UMD build of its own, so the script-tag path already
only worked with React 18 and earlier. Script-tag users can pin
@tanem/react-nprogress@^6.

Removes the two UMD rollup configs, their jest configs and test scripts,
and the umd-dev/umd-prod examples. With UMD gone every remaining bundle
externalises peers and dependencies alike, so getExternal collapses to a
single predicate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
React's docs treat higher-order components as a legacy pattern, and the
HOC was the only consumer of hoist-non-react-statics, one of the two
runtime dependencies. The replacement is a line of consumer code:

  const Enhanced = (props) => <Inner {...props} {...useNProgress(props)} />

Removes the source, its test, the hoc example and the README sections,
along with the higher-order-component and hoc keywords. The NProgress
render-props component stays: it is zero-dependency and the docs and
examples lean on it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the tsc -> compiled/ -> Rollup + Babel pipeline with a single
tsdown build. Node ESM consumers were resolving to the CommonJS build
because Node ignores pkg.module, so the package now ships an exports map
with matching .d.mts/.d.cts pairs. main/module/types stay for webpack 4
and TypeScript node10 resolution.

Output still targets es2019 so webpack 4 can parse it. @babel/runtime is
gone, leaving the package with no runtime dependencies at all. The
separate development and production CommonJS builds go too: they differed
only by minification, which bundlers apply themselves, so
process.env.NODE_ENV is no longer read at require time.

The ES module bundle is now a .mjs file, which TypeScript always emits as
ESM, so test:es runs Jest in ESM mode. src is published so the
declaration maps resolve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adding StrictMode coverage surfaced two bugs. Mounted under
StrictMode, `useNProgress()` with `isAnimating: false` animated
straight to `progress: 1`, and `useNProgress({ isAnimating: true })`
reported `{ isFinished: true, progress: 0 }` instead of starting.
Both came from `useUpdateEffect`, whose first-mount ref is flipped
during render: StrictMode's double-invoked render makes the committed
render look like an update, so the completion effect fired on mount.

The hook is now a `{ phase, progress }` reducer over four phases,
driven by one effect that dispatches `start`/`complete` from the
`isAnimating` prop and one effect keyed on the phase that owns the
timeout. Transitions are gated on the phase rather than on whether
this is the first render, so a double-mount is a no-op.

That removes the `sideEffect`-in-state bridge, the three hooks
vendored from `react-use`, `createQueue` and four eslint
suppressions. Observable pacing is unchanged: every pre-existing spec
passes unedited and coverage stays at 100%.

Also adds React 19.2 to the test matrix as the current 19 latest
minor, alongside the existing 19.0 boundary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nothing prevented a size regression from landing, despite the README
advertising a minzip badge. `npm run size` now runs inside `npm test`,
after `build`, so local and CI runs agree.

Uses `@size-limit/file` only, so the gate measures our own output rather
than a resolved dependency tree, and budgets are gzip to match the badge
metric. Actuals at this commit: ESM 997 B, CJS 1056 B.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Usage snippets mounted with `render` from `react-dom`, which React 19
removed, so the first thing a new user copied threw at runtime. They now
show the component only, dropping the mount boilerplate.

Adds a "When to Use This" section covering when a drop-in router-integrated
bar is the better fit and when this package is, with a nav entry alongside
the existing sections.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Move the contents of .github/copilot-instructions.md to a root AGENTS.md,
the cross-tool location read by Claude Code, Copilot, Cursor and Codex.
Copilot code review, the last surface tied to the old path, gained root
AGENTS.md support in June 2026, so the vendor-specific file is deleted
rather than kept as a pointer.

Update the rules the v7 work invalidated: the build is tsdown with a
postbuild package check, coverage is collected by test:src alone and the
bundle tests run against dist, and size-limit gates the bundle sizes.

CLAUDE.md imports AGENTS.md. An import rather than a symlink, since
symlinks need admin rights on Windows checkouts.

Add a Contributing section to the README pointing at AGENTS.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The four options were bullets mixing name, type, default and prose, with
no anchors to link at, and the API section repeated the Container/Bar/
Spinner boilerplate the Usage section had already shown. The options are
now a summary table over one heading per option, so each has an anchor,
and the API snippets show only the two entry points.

Documents what the bullets left out: `minimum` is clamped against the
first increment, which starts from 0.1, so the bar appears at
max(0.1, minimum) and the option only bites above 0.1; `isAnimating`
going false is what drives completion, `animationDuration` later. The
return shape of `useNProgress` was previously only inferable from the
snippets and now has a table of its own.

Live Examples moves below the API as a two-column table instead of a
wall of pipe-separated bullets. The nav line keeps every fragment it
linked before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The examples pin `"@tanem/react-nprogress": "latest"` and nothing in CI
touches them, so v7's packaging would first meet them the moment 7.0.0
publishes. Ran `npm run build && npm pack`, clean-installed the tarball into
all eight examples in place of the registry pin, and drove each progress bar
in a headless browser. Both Next examples were additionally checked under
`next build && next start`. Every example starts, advances and completes with
no console errors, and the CJS entry plus the `"use client"` directive hold
up on the Next server. The `"latest"` pins are restored: CodeSandbox resolves
the registry, not the tarball.

Three things needed fixing along the way.

The react-router example passed one `nodeRef`, held by `Home`, to every
CSSTransition, and it was never attached to a DOM node, since `<Routes>`
cannot take a ref. react-transition-group ends the transition on the next
tick when the ref does not resolve, so the fade classes never applied and
`onEntered` fired straight after `onEnter`: the bar completed 323ms after a
click instead of running for the 1200ms the example configures. A `Fade`
wrapper now owns a ref per `key` and renders the element it points at.
Reproduced against the published 6.0.4 too, so it predates v7.

Next rewrites `next-env.d.ts` on every run, with different contents for
`next dev` and `next build`, and in a format prettier rejects. It is now
gitignored and prettier-ignored in both Next examples, as create-next-app
has it.

`next build` also reconfigures `jsx` to `react-jsx`, which Next 16 requires,
so both example tsconfigs now carry that instead of `preserve`.

AGENTS.md records the tarball smoke-test procedure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`src/index.tsx` re-exported only `NProgress` and `useNProgress`, so the
option shape and the hook's return shape reached the emitted declarations
as local interfaces with no exported names. Anyone typing a wrapper around
either entry point had to reach for `Parameters<typeof useNProgress>[0]`
and `ReturnType<typeof useNProgress>`.

`Options` is renamed to `NProgressOptions`, since `Options` is too generic
an identifier to put in a consumer's import namespace. The return shape,
previously an inline object type on `useNProgress` and spelled
`ReturnType<typeof useNProgress>` in the render-prop signature, is now
`NProgressState`. No MIGRATION.md entry: neither name was reachable from
the package entry point before this commit.

Both are type-only exports, so the runtime bundles are byte-identical:
ESM 997 B, CJS 1.06 kB gzipped, unchanged.

`test/bundles.spec.ts` already asserts facts about what the build emits,
so the public export surface is checked there, over the declaration files
it finds in `dist` rather than a hard-coded list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`app/about/page.tsx` and `app/forever/page.tsx` simulate a delayed API
request with a bare `await new Promise(setTimeout)`. Since Next 15 an
uncached `fetch()` opts a route out of static prerendering, so a real
app's slow data fetch produces a slow navigation on every request. A
`setTimeout` carries no such signal: Next treats it as slow work during
prerendering, runs it once at build time and bakes the HTML in. Under
`next build && next start` both routes built as `○ (Static)` and
navigation to /forever settled in 75ms, so the progress bar jumped
straight to 100% and faded instead of animating. Only `next dev` behaved
as intended, which is where CodeSandbox runs the example, so this went
unnoticed.

`await connection()` immediately before the delay supplies the missing
signal. Both routes now build as `ƒ (Dynamic)`, and navigation to
/forever takes 3061ms in production with the bar visible for 3.4s.
`connection()` is per-render and sits next to the simulated fetch, so it
reads as part of the fake. The alternative, `export const dynamic =
'force-dynamic'`, is a route-level declaration a real app usually never
writes: its data access opts the route in by itself.

The pages-router example never had the problem, since
`getServerSideProps` is inherently per-request. It builds the same two
routes as `ƒ`, and both examples now trace the same progress curve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The React boundary verification snippet chained `cd test/react/<version>`
into the jest run, so jest resolved `--config
./scripts/jest/config.src.js` against the version directory, which holds
only `package.json` and `node_modules`. It failed with "Can't find a root
directory while resolving a config file path". `scripts/test-react.js`
scopes only the install to that directory and runs jest from the repo
root, which is what `config.src.js` needs: it sets `rootDir` to the
current working directory and `roots` to `<rootDir>/test`. The install
now runs in a subshell so the jest line keeps the repo root. Checked
against 16.14 and 17.0, 26 tests passing on each.

Three gaps alongside it.

Nothing recorded how a release triggers. `release.yml` runs on a Monday
cron against master with no content gate, and `tanem-scripts release`
throws on unlabelled and multi-labelled PRs while deriving the bump from
the labels it finds. An agent opening an unlabelled PR breaks the next
release with no local signal that it did.

MIGRATION.md appeared only in a list of docs to keep current, so the rule
that a breaking change earns an entry was left to inference. It now sits
under Versioning, next to the semver rule it follows from.

The coverage requirement read as machine-enforced. `config.src.js` sets
no `coverageThreshold` and `codecov.yml` turns patch status off, so a
drop below 100% fails nothing and has to be read off the report.

`roadmap/` stays undocumented here: it is excluded via
`.git/info/exclude` and is not present in a fresh clone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tanem

tanem commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

Code review

Found 1 issue:

  1. The minimum docs state the opposite of the implemented behaviour. The README says changing minimum mid-animation "restarts the bar from that first increment", but the start reducer case returns the existing state unchanged when state.phase === 'animating', so progress holds. The code comment ("a repeat dispatch against a running animation must not rewind the bar. That happens whenever minimum changes mid-animation") and the test keeps its place when minimum changes mid-animation both confirm the guard.

react-nprogress/README.md

Lines 134 to 137 in 63b1dcf

#### `minimum`
Lower bound for `progress`, between `0` and `1`. The first increment starts from `0.1` rather than from `0`, so the bar appears at `max(0.1, minimum)` and the option only shows through when it is set above `0.1`. Changing it while the bar is animating restarts the bar from that first increment.

//
// Guarded the same way as `complete`, and for the same reason: a repeat
// dispatch against a running animation must not rewind the bar. That
// happens whenever `minimum` changes mid-animation, as well as on a
// StrictMode double-mount.
return state.phase === 'animating'
? state
: {
phase: 'animating',
progress: clamp(increment(0), action.minimum, 1),
}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

The README said changing `minimum` while the bar is animating restarts
it from the first increment. The `start` reducer case returns the
existing state unchanged when the phase is already `animating`, so
progress holds instead. Describe what the code does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tanem

tanem commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

Fixed in 3a87f7d. The minimum section now describes the guard in the start reducer case rather than the opposite behaviour.

react-nprogress/README.md

Lines 134 to 137 in 3a87f7d

#### `minimum`
Lower bound for `progress`, between `0` and `1`. The first increment starts from `0.1` rather than from `0`, so the bar appears at `max(0.1, minimum)` and the option only shows through when it is set above `0.1`. Changing it while the bar is animating does not rewind the bar. Progress holds where it is, and the new bound applies from the next increment.

"and `animationDuration` later `isFinished` becomes `true`" was missing
its unit, so the clause did not read as a sentence. State the delay the
same way the `animationDuration` section does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tanem

tanem commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

Also fixed the isAnimating timing sentence in 40c1cfe, which was missing its unit. It now reads "isFinished becomes true animationDuration milliseconds later", matching how the animationDuration section states the same delay.

react-nprogress/README.md

Lines 130 to 133 in 40c1cfe

#### `isAnimating`
Whether the bar is running. Going `true` starts it, going `false` completes it. Completion is what drives the final state: `progress` is set to `1`, and `isFinished` becomes `true` `animationDuration` milliseconds later.

@tanem
tanem merged commit 79e8938 into master Jul 31, 2026
3 checks passed
@tanem
tanem deleted the v7 branch August 1, 2026 00:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant