react-build-reload 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Satyam shah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # react-build-reload
2
+
3
+ React library for detecting when a new frontend build is available and helping the app refresh safely.
4
+
5
+ ## Website
6
+
7
+ Homepage: [react-refresh-website.satyamshah.workers.dev](https://react-refresh-website.satyamshah.workers.dev)
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install react-build-reload
13
+ ```
14
+
15
+ React is a peer dependency. The consuming app must provide React and React DOM.
16
+
17
+ ## Quick Start
18
+
19
+ Generate the version file before building your app:
20
+
21
+ ```bash
22
+ npx react-build-reload generate
23
+ ```
24
+
25
+ This creates `public/build-version.json`.
26
+
27
+ Add the watcher near the root of your app:
28
+
29
+ ```tsx
30
+ import { BuildReloadWatcher } from "react-build-reload";
31
+
32
+ export function App() {
33
+ return (
34
+ <>
35
+ <BuildReloadWatcher
36
+ versionUrl="/build-version.json"
37
+ checkInterval={60_000}
38
+ reloadMode="prompt"
39
+ />
40
+ <MainApp />
41
+ </>
42
+ );
43
+ }
44
+ ```
45
+
46
+ The generated version file looks like this:
47
+
48
+ ```json
49
+ {
50
+ "buildId": "abc123-20260530123000",
51
+ "version": "1.0.0",
52
+ "builtAt": "2026-05-30T12:30:00.000Z",
53
+ "environment": "production"
54
+ }
55
+ ```
56
+
57
+ When `buildId` changes, the library can show a prompt, reload automatically, or call your callback.
58
+
59
+ ## Defaults
60
+
61
+ ```tsx
62
+ <BuildReloadWatcher
63
+ versionUrl="/build-version.json"
64
+ checkInterval={60000}
65
+ reloadMode="prompt"
66
+ reloadOnChunkError={true}
67
+ />
68
+ ```
69
+
70
+ ## Hook Usage
71
+
72
+ ```tsx
73
+ import { useBuildReload } from "react-build-reload";
74
+
75
+ function CustomReloadNotice() {
76
+ const { isNewBuildAvailable, reloadApp } = useBuildReload({
77
+ reloadMode: "manual"
78
+ });
79
+
80
+ if (!isNewBuildAvailable) return null;
81
+
82
+ return <button onClick={reloadApp}>Refresh now</button>;
83
+ }
84
+ ```
85
+
86
+ ## Documentation
87
+
88
+ - [Getting started](docs/getting-started.md)
89
+ - [API reference](docs/api-reference.md)
90
+ - [Version file](docs/version-file.md)
91
+ - [Usage examples](docs/usage-examples.md)
92
+ - [Roadmap](docs/roadmap.md)
93
+
94
+ ## Scope
95
+
96
+ This package handles runtime build checks in React apps and includes a small CLI to create `build-version.json`. It does not provide Vite/Webpack/Next.js plugins or manage service workers.
97
+
98
+ ## Author
99
+
100
+ Created by [Satyam Shah](https://github.com/satyam-shah-gt).
101
+
102
+ ## Free to Use
103
+
104
+ `react-build-reload` is free to use under the MIT license.
105
+
106
+ ## No Warranty
107
+
108
+ This package is provided as-is, without any warranty. The author is not liable for any damages, data loss, downtime, or other issues caused by using this package. Review and test it in your own environment before using it in production.
109
+
110
+ ## Contributing
111
+
112
+ Contributions are welcome. Please open an issue or pull request on the repository:
113
+
114
+ [github.com/satyam-shah-gt/react-build-reload](https://github.com/satyam-shah-gt/react-build-reload)
115
+
116
+ Before submitting changes, run:
117
+
118
+ ```bash
119
+ npm run typecheck
120
+ npm test
121
+ npm run build
122
+ ```
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ realpathSync,
8
+ writeFileSync
9
+ } from "node:fs";
10
+ import { dirname, resolve } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const DEFAULT_OUT = "public/build-version.json";
14
+
15
+ function printHelp() {
16
+ console.log(`react-build-reload
17
+
18
+ Usage:
19
+ react-build-reload generate [options]
20
+
21
+ Options:
22
+ --out <path> Output file path. Default: ${DEFAULT_OUT}
23
+ --build-id <id> Build ID to write. Defaults to env/git/timestamp data.
24
+ --version <version> App version. Defaults to package.json version when found.
25
+ --environment <name> Environment name. Defaults to NODE_ENV or production.
26
+ --no-git Skip git metadata lookup.
27
+ --help Show help.
28
+
29
+ Example:
30
+ react-build-reload generate --out public/build-version.json
31
+ `);
32
+ }
33
+
34
+ function parseArgs(args) {
35
+ const options = {
36
+ command: "generate",
37
+ out: DEFAULT_OUT,
38
+ buildId: undefined,
39
+ version: undefined,
40
+ environment: undefined,
41
+ includeGit: true,
42
+ help: false
43
+ };
44
+
45
+ const pending = [...args];
46
+
47
+ if (pending[0] && !pending[0].startsWith("-")) {
48
+ options.command = pending.shift();
49
+ }
50
+
51
+ for (let index = 0; index < pending.length; index += 1) {
52
+ const arg = pending[index];
53
+
54
+ switch (arg) {
55
+ case "--out":
56
+ options.out = readRequiredValue(pending, ++index, "--out");
57
+ break;
58
+ case "--build-id":
59
+ options.buildId = readRequiredValue(pending, ++index, "--build-id");
60
+ break;
61
+ case "--version":
62
+ options.version = readRequiredValue(pending, ++index, "--version");
63
+ break;
64
+ case "--environment":
65
+ options.environment = readRequiredValue(pending, ++index, "--environment");
66
+ break;
67
+ case "--no-git":
68
+ options.includeGit = false;
69
+ break;
70
+ case "--help":
71
+ case "-h":
72
+ options.help = true;
73
+ break;
74
+ default:
75
+ throw new Error(`Unknown option: ${arg}`);
76
+ }
77
+ }
78
+
79
+ return options;
80
+ }
81
+
82
+ function readRequiredValue(args, index, optionName) {
83
+ const value = args[index];
84
+
85
+ if (!value || value.startsWith("-")) {
86
+ throw new Error(`${optionName} requires a value.`);
87
+ }
88
+
89
+ return value;
90
+ }
91
+
92
+ function readPackageJson(cwd) {
93
+ const packagePath = resolve(cwd, "package.json");
94
+
95
+ if (!existsSync(packagePath)) {
96
+ return {};
97
+ }
98
+
99
+ try {
100
+ return JSON.parse(readFileSync(packagePath, "utf8"));
101
+ } catch {
102
+ throw new Error(`Could not parse ${packagePath}.`);
103
+ }
104
+ }
105
+
106
+ function readGitValue(cwd, args) {
107
+ try {
108
+ return execFileSync("git", args, {
109
+ cwd,
110
+ encoding: "utf8",
111
+ stdio: ["ignore", "pipe", "ignore"]
112
+ }).trim();
113
+ } catch {
114
+ return undefined;
115
+ }
116
+ }
117
+
118
+ function readGitInfo(cwd) {
119
+ const commit = readGitValue(cwd, ["rev-parse", "HEAD"]);
120
+ const shortCommit = readGitValue(cwd, ["rev-parse", "--short", "HEAD"]);
121
+ const branch = readGitValue(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
122
+ const status = readGitValue(cwd, ["status", "--porcelain"]);
123
+
124
+ if (!commit && !shortCommit && !branch) {
125
+ return undefined;
126
+ }
127
+
128
+ return {
129
+ commit,
130
+ shortCommit,
131
+ branch,
132
+ dirty: Boolean(status)
133
+ };
134
+ }
135
+
136
+ function readEnvBuildId(env) {
137
+ return (
138
+ env.BUILD_ID ||
139
+ env.VITE_BUILD_ID ||
140
+ env.REACT_APP_BUILD_ID ||
141
+ env.GITHUB_SHA ||
142
+ env.VERCEL_GIT_COMMIT_SHA ||
143
+ env.CF_PAGES_COMMIT_SHA ||
144
+ env.COMMIT_SHA
145
+ );
146
+ }
147
+
148
+ function createBuildId({ explicitBuildId, env, gitInfo, builtAt }) {
149
+ if (explicitBuildId?.trim()) {
150
+ return explicitBuildId.trim();
151
+ }
152
+
153
+ const envBuildId = readEnvBuildId(env);
154
+
155
+ if (envBuildId?.trim()) {
156
+ return envBuildId.trim();
157
+ }
158
+
159
+ const timestamp = builtAt.replace(/\D/g, "").slice(0, 14);
160
+
161
+ if (gitInfo?.shortCommit) {
162
+ return `${gitInfo.shortCommit}-${timestamp}`;
163
+ }
164
+
165
+ return `build-${timestamp}`;
166
+ }
167
+
168
+ function createBuildInfo(options, cwd = process.cwd(), env = process.env) {
169
+ const packageJson = readPackageJson(cwd);
170
+ const builtAt = new Date().toISOString();
171
+ const git = options.includeGit ? readGitInfo(cwd) : undefined;
172
+ const buildId = createBuildId({
173
+ explicitBuildId: options.buildId,
174
+ env,
175
+ gitInfo: git,
176
+ builtAt
177
+ });
178
+
179
+ if (!buildId.trim()) {
180
+ throw new Error("Generated buildId is empty.");
181
+ }
182
+
183
+ const buildInfo = {
184
+ buildId,
185
+ version: options.version || packageJson.version || "0.0.0",
186
+ builtAt,
187
+ environment: options.environment || env.NODE_ENV || "production"
188
+ };
189
+
190
+ if (packageJson.name) {
191
+ buildInfo.name = packageJson.name;
192
+ }
193
+
194
+ if (git) {
195
+ buildInfo.git = git;
196
+ }
197
+
198
+ return buildInfo;
199
+ }
200
+
201
+ function writeBuildInfo(buildInfo, outPath, cwd = process.cwd()) {
202
+ const absoluteOutPath = resolve(cwd, outPath);
203
+
204
+ mkdirSync(dirname(absoluteOutPath), { recursive: true });
205
+ writeFileSync(absoluteOutPath, `${JSON.stringify(buildInfo, null, 2)}\n`, "utf8");
206
+
207
+ return absoluteOutPath;
208
+ }
209
+
210
+ function run(args = process.argv.slice(2), cwd = process.cwd(), env = process.env) {
211
+ const options = parseArgs(args);
212
+
213
+ if (options.help) {
214
+ printHelp();
215
+ return 0;
216
+ }
217
+
218
+ if (options.command !== "generate") {
219
+ throw new Error(`Unknown command: ${options.command}`);
220
+ }
221
+
222
+ const buildInfo = createBuildInfo(options, cwd, env);
223
+ const outPath = writeBuildInfo(buildInfo, options.out, cwd);
224
+
225
+ console.log(`Created ${outPath}`);
226
+ console.log(`buildId: ${buildInfo.buildId}`);
227
+
228
+ return 0;
229
+ }
230
+
231
+ function isDirectCliInvocation(argvPath, currentFilePath) {
232
+ if (!argvPath) {
233
+ return false;
234
+ }
235
+
236
+ try {
237
+ return realpathSync(argvPath) === currentFilePath;
238
+ } catch {
239
+ return resolve(argvPath) === currentFilePath;
240
+ }
241
+ }
242
+
243
+ const currentFilePath = fileURLToPath(import.meta.url);
244
+
245
+ if (isDirectCliInvocation(process.argv[1], currentFilePath)) {
246
+ try {
247
+ process.exitCode = run();
248
+ } catch (error) {
249
+ console.error(error instanceof Error ? error.message : String(error));
250
+ process.exitCode = 1;
251
+ }
252
+ }
253
+
254
+ export {
255
+ createBuildId,
256
+ createBuildInfo,
257
+ parseArgs,
258
+ readGitInfo,
259
+ run,
260
+ writeBuildInfo,
261
+ isDirectCliInvocation
262
+ };
@@ -0,0 +1,3 @@
1
+ import type { BuildReloadWatcherProps } from "../types";
2
+ export declare function BuildReloadWatcher({ promptMessage, refreshButtonLabel, dismissButtonLabel, promptPosition, showDismissButton, reloadOnChunkError, ...options }: BuildReloadWatcherProps): import("react/jsx-runtime").JSX.Element | null;
3
+ //# sourceMappingURL=BuildReloadWatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BuildReloadWatcher.d.ts","sourceRoot":"","sources":["../../src/components/BuildReloadWatcher.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAoDxD,wBAAgB,kBAAkB,CAAC,EACjC,aAAgE,EAChE,kBAA8B,EAC9B,kBAA8B,EAC9B,cAAyB,EACzB,iBAAwB,EACxB,kBAAyB,EACzB,GAAG,OAAO,EACX,EAAE,uBAAuB,kDA6CzB"}
@@ -0,0 +1,3 @@
1
+ import type { UseBuildReloadOptions, UseBuildReloadResult } from "../types";
2
+ export declare function useBuildReload(options?: UseBuildReloadOptions): UseBuildReloadResult;
3
+ //# sourceMappingURL=useBuildReload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useBuildReload.d.ts","sourceRoot":"","sources":["../../src/hooks/useBuildReload.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAIV,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,UAAU,CAAC;AAMlB,wBAAgB,cAAc,CAC5B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAiJtB"}
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("react"),t=require("react/jsx-runtime");var n=[/loading chunk \d* failed/i,/chunkloaderror/i,/failed to fetch dynamically imported module/i,/importing a module script failed/i,/error loading dynamically imported module/i,/unable to preload css/i],r=`react-build-reload:chunk-error-reloaded`;function i(e){if(typeof e==`string`)return e;if(e instanceof Error)return`${e.name} ${e.message}`;if(typeof e==`object`&&e){let t=e;return[t.message,t.reason,t.type].filter(Boolean).map(String).join(` `)}return``}function a(e){let t=i(e);return n.some(e=>e.test(t))}function o(e,t){if(typeof window>`u`)return()=>void 0;let n=n=>{if(!a(n))return;let o=n instanceof Error?n:Error(i(n)||`Chunk loading failed.`);t?.(o),sessionStorage.getItem(r)!==`true`&&(sessionStorage.setItem(r,`true`),e())},o=e=>{n(e.error??e.message)},s=e=>{n(e.reason)};return window.addEventListener(`error`,o),window.addEventListener(`unhandledrejection`,s),()=>{window.removeEventListener(`error`,o),window.removeEventListener(`unhandledrejection`,s)}}var s=class extends Error{constructor(e){super(e),this.name=`BuildReloadError`}},c=`/build-version.json`;function l(e,t=Date.now()){let n=/^[a-z][a-z\d+\-.]*:/i.test(e),r=typeof window>`u`?`http://localhost`:window.location.href,i=new URL(e,r);return i.searchParams.set(`t`,String(t)),n?i.toString():`${i.pathname}${i.search}${i.hash}`}function u(e){return typeof e==`object`&&!!e&&`buildId`in e&&typeof e.buildId==`string`&&e.buildId.trim().length>0}async function d(e=c,t){let n;try{n=await fetch(l(e),{cache:`no-store`,signal:t})}catch(e){throw e instanceof Error?e:new s(`Failed to fetch build information.`)}if(!n.ok)throw new s(`Failed to fetch build information: ${n.status} ${n.statusText}`.trim());let r;try{r=await n.json()}catch{throw new s(`Build information response is not valid JSON.`)}if(!u(r))throw new s(`Build information must include a non-empty buildId.`);return r}function f(e,t){return!e||!t?!1:e!==t}function p(){typeof window>`u`||window.location.reload()}var m=6e4,h=`prompt`,g=0;function _(t={}){let{versionUrl:n=c,currentBuildId:r,checkInterval:i=m,reloadMode:a=h,reloadDelay:o=g,enabled:s=!0,onNewBuild:l,onError:u}=t,[_,v]=(0,e.useState)(r??null),[y,b]=(0,e.useState)(null),[x,S]=(0,e.useState)(!1),[C,w]=(0,e.useState)(null),T=(0,e.useRef)(r??null),E=(0,e.useRef)(null),D=(0,e.useRef)(null),O=(0,e.useRef)(null),k=(0,e.useRef)(l),A=(0,e.useRef)(u);(0,e.useEffect)(()=>{k.current=l},[l]),(0,e.useEffect)(()=>{A.current=u},[u]),(0,e.useEffect)(()=>{r!==void 0&&(T.current=r,v(r))},[r]);let j=(0,e.useCallback)(()=>{p()},[]),M=(0,e.useCallback)(e=>{E.current!==e.latestBuildId&&(E.current=e.latestBuildId,S(!0),k.current?.(e),a===`auto`&&(D.current=setTimeout(j,Math.max(0,o))))},[j,o,a]),N=(0,e.useCallback)(async()=>{if(!s)return;O.current?.abort();let e=new AbortController;O.current=e;try{let t=await d(n,e.signal);b(t),w(null);let r=T.current;if(!r){T.current=t.buildId,v(t.buildId);return}f(r,t.buildId)&&M({currentBuildId:r,latestBuildId:t.buildId,latestBuildInfo:t})}catch(e){if(e instanceof DOMException&&e.name===`AbortError`)return;let t=e instanceof Error?e:Error(`Build reload check failed.`);w(t),A.current?.(t)}},[s,M,n]);(0,e.useEffect)(()=>{if(!s)return;N();let e=window.setInterval(()=>{N()},Math.max(1e3,i));return()=>{window.clearInterval(e),O.current?.abort(),D.current&&clearTimeout(D.current)}},[i,N,s]);let P=(0,e.useCallback)(()=>{S(!1)},[]);return{isNewBuildAvailable:x,currentBuildId:_,latestBuildId:y?.buildId??null,latestBuildInfo:y,error:C,reloadApp:j,checkNow:N,dismissPrompt:P}}var v={position:`fixed`,left:`16px`,right:`16px`,zIndex:2147483647,display:`flex`,alignItems:`center`,justifyContent:`space-between`,gap:`12px`,padding:`12px 14px`,borderRadius:`8px`,background:`#111827`,color:`#ffffff`,boxShadow:`0 12px 28px rgba(17, 24, 39, 0.24)`,fontFamily:`Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`,fontSize:`14px`,lineHeight:1.4},y={display:`flex`,alignItems:`center`,gap:`8px`,flexShrink:0},b={border:0,borderRadius:`6px`,padding:`7px 10px`,cursor:`pointer`,font:`inherit`,fontWeight:600},x={...b,background:`#ffffff`,color:`#111827`},S={...b,background:`transparent`,color:`#d1d5db`};function C({promptMessage:n=`A new version is available. Refresh to update.`,refreshButtonLabel:r=`Refresh`,dismissButtonLabel:i=`Dismiss`,promptPosition:a=`bottom`,showDismissButton:s=!0,reloadOnChunkError:c=!0,...l}){let{isNewBuildAvailable:u,reloadApp:d,dismissPrompt:f}=_(l);if((0,e.useEffect)(()=>{if(c)return o(d,l.onError)},[d,c,l.onError]),l.reloadMode===`manual`||!u)return null;let p=a===`top`?{top:`16px`}:{bottom:`16px`};return(0,t.jsxs)(`div`,{role:`status`,"aria-live":`polite`,style:{...v,...p},children:[(0,t.jsx)(`span`,{children:n}),(0,t.jsxs)(`div`,{style:y,children:[s?(0,t.jsx)(`button`,{type:`button`,style:S,onClick:f,children:i}):null,(0,t.jsx)(`button`,{type:`button`,style:x,onClick:d,children:r})]})]})}exports.BuildReloadWatcher=C,exports.DEFAULT_VERSION_URL=c,exports.createCacheSafeUrl=l,exports.fetchBuildInfo=d,exports.hasBuildChanged=f,exports.installChunkErrorReload=o,exports.isChunkLoadError=a,exports.useBuildReload=_;
@@ -0,0 +1,7 @@
1
+ export { BuildReloadWatcher } from "./components/BuildReloadWatcher";
2
+ export { useBuildReload } from "./hooks/useBuildReload";
3
+ export { createCacheSafeUrl, fetchBuildInfo, DEFAULT_VERSION_URL } from "./utils/fetchBuildInfo";
4
+ export { hasBuildChanged } from "./utils/compareBuildId";
5
+ export { isChunkLoadError, installChunkErrorReload } from "./utils/chunkErrors";
6
+ export type { BuildInfo, BuildReloadConfig, BuildReloadWatcherProps, NewBuildPayload, PromptPosition, ReloadMode, UseBuildReloadOptions, UseBuildReloadResult } from "./types";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACpB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAChF,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,uBAAuB,EACvB,eAAe,EACf,cAAc,EACd,UAAU,EACV,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,238 @@
1
+ import { useCallback as e, useEffect as t, useRef as n, useState as r } from "react";
2
+ import { jsx as i, jsxs as a } from "react/jsx-runtime";
3
+ //#region src/utils/chunkErrors.ts
4
+ var o = [
5
+ /loading chunk \d* failed/i,
6
+ /chunkloaderror/i,
7
+ /failed to fetch dynamically imported module/i,
8
+ /importing a module script failed/i,
9
+ /error loading dynamically imported module/i,
10
+ /unable to preload css/i
11
+ ], s = "react-build-reload:chunk-error-reloaded";
12
+ function c(e) {
13
+ if (typeof e == "string") return e;
14
+ if (e instanceof Error) return `${e.name} ${e.message}`;
15
+ if (typeof e == "object" && e) {
16
+ let t = e;
17
+ return [
18
+ t.message,
19
+ t.reason,
20
+ t.type
21
+ ].filter(Boolean).map(String).join(" ");
22
+ }
23
+ return "";
24
+ }
25
+ function l(e) {
26
+ let t = c(e);
27
+ return o.some((e) => e.test(t));
28
+ }
29
+ function u(e, t) {
30
+ if (typeof window > "u") return () => void 0;
31
+ let n = (n) => {
32
+ if (!l(n)) return;
33
+ let r = n instanceof Error ? n : Error(c(n) || "Chunk loading failed.");
34
+ t?.(r), sessionStorage.getItem(s) !== "true" && (sessionStorage.setItem(s, "true"), e());
35
+ }, r = (e) => {
36
+ n(e.error ?? e.message);
37
+ }, i = (e) => {
38
+ n(e.reason);
39
+ };
40
+ return window.addEventListener("error", r), window.addEventListener("unhandledrejection", i), () => {
41
+ window.removeEventListener("error", r), window.removeEventListener("unhandledrejection", i);
42
+ };
43
+ }
44
+ //#endregion
45
+ //#region src/types/index.ts
46
+ var d = class extends Error {
47
+ constructor(e) {
48
+ super(e), this.name = "BuildReloadError";
49
+ }
50
+ }, f = "/build-version.json";
51
+ function p(e, t = Date.now()) {
52
+ let n = /^[a-z][a-z\d+\-.]*:/i.test(e), r = typeof window > "u" ? "http://localhost" : window.location.href, i = new URL(e, r);
53
+ return i.searchParams.set("t", String(t)), n ? i.toString() : `${i.pathname}${i.search}${i.hash}`;
54
+ }
55
+ function m(e) {
56
+ return typeof e == "object" && !!e && "buildId" in e && typeof e.buildId == "string" && e.buildId.trim().length > 0;
57
+ }
58
+ async function h(e = f, t) {
59
+ let n;
60
+ try {
61
+ n = await fetch(p(e), {
62
+ cache: "no-store",
63
+ signal: t
64
+ });
65
+ } catch (e) {
66
+ throw e instanceof Error ? e : new d("Failed to fetch build information.");
67
+ }
68
+ if (!n.ok) throw new d(`Failed to fetch build information: ${n.status} ${n.statusText}`.trim());
69
+ let r;
70
+ try {
71
+ r = await n.json();
72
+ } catch {
73
+ throw new d("Build information response is not valid JSON.");
74
+ }
75
+ if (!m(r)) throw new d("Build information must include a non-empty buildId.");
76
+ return r;
77
+ }
78
+ //#endregion
79
+ //#region src/utils/compareBuildId.ts
80
+ function g(e, t) {
81
+ return !e || !t ? !1 : e !== t;
82
+ }
83
+ //#endregion
84
+ //#region src/utils/reloadPage.ts
85
+ function _() {
86
+ typeof window > "u" || window.location.reload();
87
+ }
88
+ //#endregion
89
+ //#region src/hooks/useBuildReload.ts
90
+ var v = 6e4, y = "prompt", b = 0;
91
+ function x(i = {}) {
92
+ let { versionUrl: a = f, currentBuildId: o, checkInterval: s = v, reloadMode: c = y, reloadDelay: l = b, enabled: u = !0, onNewBuild: d, onError: p } = i, [m, x] = r(o ?? null), [S, C] = r(null), [w, T] = r(!1), [E, D] = r(null), O = n(o ?? null), k = n(null), A = n(null), j = n(null), M = n(d), N = n(p);
93
+ t(() => {
94
+ M.current = d;
95
+ }, [d]), t(() => {
96
+ N.current = p;
97
+ }, [p]), t(() => {
98
+ o !== void 0 && (O.current = o, x(o));
99
+ }, [o]);
100
+ let P = e(() => {
101
+ _();
102
+ }, []), F = e((e) => {
103
+ k.current !== e.latestBuildId && (k.current = e.latestBuildId, T(!0), M.current?.(e), c === "auto" && (A.current = setTimeout(P, Math.max(0, l))));
104
+ }, [
105
+ P,
106
+ l,
107
+ c
108
+ ]), I = e(async () => {
109
+ if (!u) return;
110
+ j.current?.abort();
111
+ let e = new AbortController();
112
+ j.current = e;
113
+ try {
114
+ let t = await h(a, e.signal);
115
+ C(t), D(null);
116
+ let n = O.current;
117
+ if (!n) {
118
+ O.current = t.buildId, x(t.buildId);
119
+ return;
120
+ }
121
+ g(n, t.buildId) && F({
122
+ currentBuildId: n,
123
+ latestBuildId: t.buildId,
124
+ latestBuildInfo: t
125
+ });
126
+ } catch (e) {
127
+ if (e instanceof DOMException && e.name === "AbortError") return;
128
+ let t = e instanceof Error ? e : /* @__PURE__ */ Error("Build reload check failed.");
129
+ D(t), N.current?.(t);
130
+ }
131
+ }, [
132
+ u,
133
+ F,
134
+ a
135
+ ]);
136
+ t(() => {
137
+ if (!u) return;
138
+ I();
139
+ let e = window.setInterval(() => {
140
+ I();
141
+ }, Math.max(1e3, s));
142
+ return () => {
143
+ window.clearInterval(e), j.current?.abort(), A.current && clearTimeout(A.current);
144
+ };
145
+ }, [
146
+ s,
147
+ I,
148
+ u
149
+ ]);
150
+ let L = e(() => {
151
+ T(!1);
152
+ }, []);
153
+ return {
154
+ isNewBuildAvailable: w,
155
+ currentBuildId: m,
156
+ latestBuildId: S?.buildId ?? null,
157
+ latestBuildInfo: S,
158
+ error: E,
159
+ reloadApp: P,
160
+ checkNow: I,
161
+ dismissPrompt: L
162
+ };
163
+ }
164
+ //#endregion
165
+ //#region src/components/BuildReloadWatcher.tsx
166
+ var S = {
167
+ position: "fixed",
168
+ left: "16px",
169
+ right: "16px",
170
+ zIndex: 2147483647,
171
+ display: "flex",
172
+ alignItems: "center",
173
+ justifyContent: "space-between",
174
+ gap: "12px",
175
+ padding: "12px 14px",
176
+ borderRadius: "8px",
177
+ background: "#111827",
178
+ color: "#ffffff",
179
+ boxShadow: "0 12px 28px rgba(17, 24, 39, 0.24)",
180
+ fontFamily: "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif",
181
+ fontSize: "14px",
182
+ lineHeight: 1.4
183
+ }, C = {
184
+ display: "flex",
185
+ alignItems: "center",
186
+ gap: "8px",
187
+ flexShrink: 0
188
+ }, w = {
189
+ border: 0,
190
+ borderRadius: "6px",
191
+ padding: "7px 10px",
192
+ cursor: "pointer",
193
+ font: "inherit",
194
+ fontWeight: 600
195
+ }, T = {
196
+ ...w,
197
+ background: "#ffffff",
198
+ color: "#111827"
199
+ }, E = {
200
+ ...w,
201
+ background: "transparent",
202
+ color: "#d1d5db"
203
+ };
204
+ function D({ promptMessage: e = "A new version is available. Refresh to update.", refreshButtonLabel: n = "Refresh", dismissButtonLabel: r = "Dismiss", promptPosition: o = "bottom", showDismissButton: s = !0, reloadOnChunkError: c = !0, ...l }) {
205
+ let { isNewBuildAvailable: d, reloadApp: f, dismissPrompt: p } = x(l);
206
+ if (t(() => {
207
+ if (c) return u(f, l.onError);
208
+ }, [
209
+ f,
210
+ c,
211
+ l.onError
212
+ ]), l.reloadMode === "manual" || !d) return null;
213
+ let m = o === "top" ? { top: "16px" } : { bottom: "16px" };
214
+ return /* @__PURE__ */ a("div", {
215
+ role: "status",
216
+ "aria-live": "polite",
217
+ style: {
218
+ ...S,
219
+ ...m
220
+ },
221
+ children: [/* @__PURE__ */ i("span", { children: e }), /* @__PURE__ */ a("div", {
222
+ style: C,
223
+ children: [s ? /* @__PURE__ */ i("button", {
224
+ type: "button",
225
+ style: E,
226
+ onClick: p,
227
+ children: r
228
+ }) : null, /* @__PURE__ */ i("button", {
229
+ type: "button",
230
+ style: T,
231
+ onClick: f,
232
+ children: n
233
+ })]
234
+ })]
235
+ });
236
+ }
237
+ //#endregion
238
+ export { D as BuildReloadWatcher, f as DEFAULT_VERSION_URL, p as createCacheSafeUrl, h as fetchBuildInfo, g as hasBuildChanged, u as installChunkErrorReload, l as isChunkLoadError, x as useBuildReload };
@@ -0,0 +1,49 @@
1
+ import type { ReactNode } from "react";
2
+ export type ReloadMode = "prompt" | "auto" | "manual";
3
+ export type PromptPosition = "top" | "bottom";
4
+ export interface BuildInfo {
5
+ buildId: string;
6
+ version?: string;
7
+ builtAt?: string;
8
+ environment?: string;
9
+ [key: string]: unknown;
10
+ }
11
+ export interface NewBuildPayload {
12
+ currentBuildId: string;
13
+ latestBuildId: string;
14
+ latestBuildInfo: BuildInfo;
15
+ }
16
+ export interface BuildReloadConfig {
17
+ versionUrl?: string;
18
+ currentBuildId?: string;
19
+ checkInterval?: number;
20
+ reloadMode?: ReloadMode;
21
+ reloadDelay?: number;
22
+ enabled?: boolean;
23
+ reloadOnChunkError?: boolean;
24
+ onNewBuild?: (payload: NewBuildPayload) => void;
25
+ onError?: (error: Error) => void;
26
+ }
27
+ export interface UseBuildReloadOptions extends BuildReloadConfig {
28
+ }
29
+ export interface UseBuildReloadResult {
30
+ isNewBuildAvailable: boolean;
31
+ currentBuildId: string | null;
32
+ latestBuildId: string | null;
33
+ latestBuildInfo: BuildInfo | null;
34
+ error: Error | null;
35
+ reloadApp: () => void;
36
+ checkNow: () => Promise<void>;
37
+ dismissPrompt: () => void;
38
+ }
39
+ export interface BuildReloadWatcherProps extends BuildReloadConfig {
40
+ promptMessage?: ReactNode;
41
+ refreshButtonLabel?: string;
42
+ dismissButtonLabel?: string;
43
+ promptPosition?: PromptPosition;
44
+ showDismissButton?: boolean;
45
+ }
46
+ export declare class BuildReloadError extends Error {
47
+ constructor(message: string);
48
+ }
49
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEtD,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,QAAQ,CAAC;AAE9C,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAC;IAChD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,qBAAsB,SAAQ,iBAAiB;CAAG;AAEnE,MAAM,WAAW,oBAAoB;IACnC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,EAAE,SAAS,GAAG,IAAI,CAAC;IAClC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,aAAa,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAwB,SAAQ,iBAAiB;IAChE,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B"}
@@ -0,0 +1,4 @@
1
+ export declare function isChunkLoadError(error: unknown): boolean;
2
+ export declare function clearChunkReloadMarker(): void;
3
+ export declare function installChunkErrorReload(reloadApp: () => void, onError?: (error: Error) => void): () => void;
4
+ //# sourceMappingURL=chunkErrors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunkErrors.d.ts","sourceRoot":"","sources":["../../src/utils/chunkErrors.ts"],"names":[],"mappings":"AA+BA,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAGxD;AAED,wBAAgB,sBAAsB,IAAI,IAAI,CAM7C;AAED,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,IAAI,EACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAC/B,MAAM,IAAI,CAyCZ"}
@@ -0,0 +1,2 @@
1
+ export declare function hasBuildChanged(currentBuildId: string | null | undefined, latestBuildId: string | null | undefined): boolean;
2
+ //# sourceMappingURL=compareBuildId.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compareBuildId.d.ts","sourceRoot":"","sources":["../../src/utils/compareBuildId.ts"],"names":[],"mappings":"AAAA,wBAAgB,eAAe,CAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACzC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACvC,OAAO,CAMT"}
@@ -0,0 +1,6 @@
1
+ import { type BuildInfo } from "../types";
2
+ export declare const DEFAULT_VERSION_URL = "/build-version.json";
3
+ export declare const CACHE_BUST_PARAM = "t";
4
+ export declare function createCacheSafeUrl(versionUrl: string, now?: number): string;
5
+ export declare function fetchBuildInfo(versionUrl?: string, signal?: AbortSignal): Promise<BuildInfo>;
6
+ //# sourceMappingURL=fetchBuildInfo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetchBuildInfo.d.ts","sourceRoot":"","sources":["../../src/utils/fetchBuildInfo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AAE5D,eAAO,MAAM,mBAAmB,wBAAwB,CAAC;AACzD,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,MAAM,CAa/E;AAYD,wBAAsB,cAAc,CAClC,UAAU,SAAsB,EAChC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,SAAS,CAAC,CAmCpB"}
@@ -0,0 +1,2 @@
1
+ export declare function reloadPage(): void;
2
+ //# sourceMappingURL=reloadPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reloadPage.d.ts","sourceRoot":"","sources":["../../src/utils/reloadPage.ts"],"names":[],"mappings":"AAAA,wBAAgB,UAAU,IAAI,IAAI,CAMjC"}
@@ -0,0 +1,87 @@
1
+ # API Reference
2
+
3
+ ## `BuildReloadWatcher`
4
+
5
+ ```tsx
6
+ <BuildReloadWatcher />
7
+ ```
8
+
9
+ Component that starts polling, handles chunk load errors, and renders the default prompt UI when needed.
10
+
11
+ ### Props
12
+
13
+ | Prop | Type | Default | Description |
14
+ | --- | --- | --- | --- |
15
+ | `versionUrl` | `string` | `"/build-version.json"` | URL that returns the latest build info. |
16
+ | `currentBuildId` | `string` | first fetched `buildId` | Current running app build ID. |
17
+ | `checkInterval` | `number` | `60000` | Poll interval in milliseconds. Minimum runtime interval is clamped to 1000 ms. |
18
+ | `reloadMode` | `"prompt" \| "auto" \| "manual"` | `"prompt"` | What happens when a new build is detected. |
19
+ | `reloadDelay` | `number` | `0` | Delay before automatic reload in `auto` mode. |
20
+ | `enabled` | `boolean` | `true` | Enables or disables polling. |
21
+ | `reloadOnChunkError` | `boolean` | `true` | Reload once when common dynamic import/chunk failures occur. |
22
+ | `onNewBuild` | `(payload) => void` | `undefined` | Called once when a new build is detected. |
23
+ | `onError` | `(error) => void` | `undefined` | Called when fetching or parsing build info fails. |
24
+ | `promptMessage` | `ReactNode` | default message | Prompt text or node. |
25
+ | `refreshButtonLabel` | `string` | `"Refresh"` | Refresh button label. |
26
+ | `dismissButtonLabel` | `string` | `"Dismiss"` | Dismiss button label. |
27
+ | `promptPosition` | `"top" \| "bottom"` | `"bottom"` | Default prompt position. |
28
+ | `showDismissButton` | `boolean` | `true` | Whether the prompt includes a dismiss button. |
29
+
30
+ ## `useBuildReload`
31
+
32
+ ```tsx
33
+ const state = useBuildReload(options);
34
+ ```
35
+
36
+ Hook for custom UI or manual integration.
37
+
38
+ ### Return Value
39
+
40
+ | Field | Type | Description |
41
+ | --- | --- | --- |
42
+ | `isNewBuildAvailable` | `boolean` | True after a different latest `buildId` is detected. |
43
+ | `currentBuildId` | `string \| null` | Running app build ID. |
44
+ | `latestBuildId` | `string \| null` | Latest fetched build ID. |
45
+ | `latestBuildInfo` | `BuildInfo \| null` | Full latest response object. |
46
+ | `error` | `Error \| null` | Last non-abort error from version checking. |
47
+ | `reloadApp` | `() => void` | Reloads the current page. |
48
+ | `checkNow` | `() => Promise<void>` | Runs an immediate version check. |
49
+ | `dismissPrompt` | `() => void` | Clears the prompt state locally. |
50
+
51
+ ## Types
52
+
53
+ ```ts
54
+ type ReloadMode = "prompt" | "auto" | "manual";
55
+
56
+ interface BuildInfo {
57
+ buildId: string;
58
+ version?: string;
59
+ builtAt?: string;
60
+ environment?: string;
61
+ [key: string]: unknown;
62
+ }
63
+
64
+ interface NewBuildPayload {
65
+ currentBuildId: string;
66
+ latestBuildId: string;
67
+ latestBuildInfo: BuildInfo;
68
+ }
69
+ ```
70
+
71
+ ## CLI
72
+
73
+ ```bash
74
+ react-build-reload generate [options]
75
+ ```
76
+
77
+ Creates a JSON build metadata file for the runtime watcher.
78
+
79
+ | Option | Default | Description |
80
+ | --- | --- | --- |
81
+ | `--out <path>` | `public/build-version.json` | Output file path. Parent folders are created automatically. |
82
+ | `--build-id <id>` | env/git/timestamp | Build ID to write. |
83
+ | `--version <version>` | app `package.json` version or `0.0.0` | Version value to write. |
84
+ | `--environment <name>` | `NODE_ENV` or `production` | Environment value to write. |
85
+ | `--no-git` | disabled | Skips git metadata lookup. |
86
+
87
+ Generated files include `buildId`, `version`, `builtAt`, `environment`, and `name` when available. Git metadata is included when git is available and `--no-git` is not used.
@@ -0,0 +1,97 @@
1
+ # Getting Started
2
+
3
+ `react-build-reload` watches a build version endpoint and detects when the running app is older than the deployed app.
4
+
5
+ ## 1. Install
6
+
7
+ ```bash
8
+ npm install react-build-reload
9
+ ```
10
+
11
+ ## 2. Generate a Version File
12
+
13
+ Run this in your app before building:
14
+
15
+ ```bash
16
+ npx react-build-reload generate
17
+ ```
18
+
19
+ This creates:
20
+
21
+ ```txt
22
+ public/build-version.json
23
+ ```
24
+
25
+ Example output:
26
+
27
+ ```json
28
+ {
29
+ "buildId": "abc123-20260530123000",
30
+ "version": "1.0.0",
31
+ "builtAt": "2026-05-30T12:30:00.000Z",
32
+ "environment": "production",
33
+ "name": "my-app"
34
+ }
35
+ ```
36
+
37
+ The default URL is:
38
+
39
+ ```txt
40
+ /build-version.json
41
+ ```
42
+
43
+ The generator uses an explicit build ID, common CI environment variables, git metadata, or a timestamp fallback. The runtime library only requires `buildId`.
44
+
45
+ Add it to your app build scripts:
46
+
47
+ ```json
48
+ {
49
+ "scripts": {
50
+ "prebuild": "react-build-reload generate",
51
+ "build": "vite build"
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## 3. Add the Watcher
57
+
58
+ ```tsx
59
+ import { BuildReloadWatcher } from "react-build-reload";
60
+
61
+ function App() {
62
+ return (
63
+ <>
64
+ <BuildReloadWatcher />
65
+ <MainApp />
66
+ </>
67
+ );
68
+ }
69
+ ```
70
+
71
+ By default, the watcher checks every 60 seconds and shows a refresh prompt when a new build is detected.
72
+
73
+ ## 4. Use Your Own Endpoint
74
+
75
+ ```tsx
76
+ <BuildReloadWatcher versionUrl="/api/version" />
77
+ ```
78
+
79
+ The endpoint should return JSON with a non-empty `buildId`.
80
+
81
+ ## 5. Disable in Development
82
+
83
+ ```tsx
84
+ <BuildReloadWatcher enabled={import.meta.env.PROD} />
85
+ ```
86
+
87
+ This keeps local development from repeatedly checking a file that may not exist.
88
+
89
+ ## 6. Generator Options
90
+
91
+ ```bash
92
+ react-build-reload generate --out public/build-version.json
93
+ react-build-reload generate --build-id "$GITHUB_SHA"
94
+ react-build-reload generate --environment staging
95
+ react-build-reload generate --version 2.0.0
96
+ react-build-reload generate --no-git
97
+ ```
@@ -0,0 +1,26 @@
1
+ # Roadmap
2
+
3
+ These items are intentionally outside the MVP.
4
+
5
+ ## Future Features
6
+
7
+ - Multi-tab synchronization with `BroadcastChannel` or storage events.
8
+ - Custom prompt render prop or component slot.
9
+ - Toast notification integrations.
10
+ - Next.js-specific guidance and compatibility helpers.
11
+ - Service worker update coordination.
12
+ - Vite or Webpack plugin wrappers around the existing generator.
13
+ - Deployment metadata display.
14
+ - Environment-specific behavior.
15
+ - Pause checks while users are typing.
16
+ - Check immediately when a tab becomes active again.
17
+
18
+ ## Non-MVP Boundaries
19
+
20
+ The MVP remains focused on React runtime behavior:
21
+
22
+ - Poll a version URL.
23
+ - Compare build IDs.
24
+ - Prompt, reload, or call a callback.
25
+ - Recover once from chunk load failures.
26
+ - Avoid breaking the app when version checks fail.
@@ -0,0 +1,85 @@
1
+ # Usage Examples
2
+
3
+ ## Generate Before Vite Build
4
+
5
+ ```json
6
+ {
7
+ "scripts": {
8
+ "prebuild": "react-build-reload generate",
9
+ "build": "vite build"
10
+ }
11
+ }
12
+ ```
13
+
14
+ Vite copies `public/build-version.json` to `/build-version.json` during build.
15
+
16
+ ## Generate with CI Build ID
17
+
18
+ ```bash
19
+ react-build-reload generate --build-id "$GITHUB_SHA" --environment production
20
+ ```
21
+
22
+ ## Prompt Mode
23
+
24
+ ```tsx
25
+ <BuildReloadWatcher reloadMode="prompt" />
26
+ ```
27
+
28
+ Shows a small refresh prompt when a new build is detected. This is the default and safest mode for user-facing apps.
29
+
30
+ ## Auto Reload
31
+
32
+ ```tsx
33
+ <BuildReloadWatcher reloadMode="auto" reloadDelay={3000} />
34
+ ```
35
+
36
+ Reloads after a new build is detected. Use this for dashboards, internal tools, or monitoring screens where showing the latest version is more important than preserving unsaved input.
37
+
38
+ ## Manual Callback
39
+
40
+ ```tsx
41
+ <BuildReloadWatcher
42
+ reloadMode="manual"
43
+ onNewBuild={({ latestBuildId }) => {
44
+ showToast(`New build available: ${latestBuildId}`);
45
+ }}
46
+ />
47
+ ```
48
+
49
+ Use this when the application already has its own modal, toast, or unsaved-work flow.
50
+
51
+ ## Custom Version URL
52
+
53
+ ```tsx
54
+ <BuildReloadWatcher versionUrl="/api/version" />
55
+ ```
56
+
57
+ The endpoint must return JSON with `buildId`.
58
+
59
+ ## Disabled in Development
60
+
61
+ ```tsx
62
+ <BuildReloadWatcher enabled={import.meta.env.PROD} />
63
+ ```
64
+
65
+ This avoids noisy local errors when `/build-version.json` is not available during development.
66
+
67
+ ## Hook with Custom UI
68
+
69
+ ```tsx
70
+ function ReloadToast() {
71
+ const { isNewBuildAvailable, reloadApp, dismissPrompt } = useBuildReload({
72
+ reloadMode: "manual"
73
+ });
74
+
75
+ if (!isNewBuildAvailable) return null;
76
+
77
+ return (
78
+ <div>
79
+ <span>A new version is available.</span>
80
+ <button onClick={dismissPrompt}>Later</button>
81
+ <button onClick={reloadApp}>Refresh</button>
82
+ </div>
83
+ );
84
+ }
85
+ ```
@@ -0,0 +1,63 @@
1
+ # Version File
2
+
3
+ The library needs a URL that returns the latest deployed build ID. You can create this file with the included CLI.
4
+
5
+ ## Generate the File
6
+
7
+ ```bash
8
+ react-build-reload generate
9
+ ```
10
+
11
+ Default output:
12
+
13
+ ```txt
14
+ public/build-version.json
15
+ ```
16
+
17
+ Recommended app setup:
18
+
19
+ ```json
20
+ {
21
+ "scripts": {
22
+ "prebuild": "react-build-reload generate",
23
+ "build": "vite build"
24
+ }
25
+ }
26
+ ```
27
+
28
+ ## Required Shape
29
+
30
+ ```json
31
+ {
32
+ "buildId": "abc123"
33
+ }
34
+ ```
35
+
36
+ `buildId` must be a non-empty string. It should change on every frontend deployment.
37
+
38
+ ## Optional Metadata
39
+
40
+ ```json
41
+ {
42
+ "buildId": "abc123",
43
+ "version": "1.0.0",
44
+ "builtAt": "2026-05-30T12:30:00Z",
45
+ "environment": "production"
46
+ }
47
+ ```
48
+
49
+ Optional fields are preserved in `latestBuildInfo` and passed to `onNewBuild`.
50
+
51
+ ## Cache Safety
52
+
53
+ Every version check adds a timestamp query param:
54
+
55
+ ```txt
56
+ /build-version.json?t=123456789
57
+ ```
58
+
59
+ The request also uses `cache: "no-store"`. This reduces the chance of comparing against stale browser-cached metadata.
60
+
61
+ ## Deployment Responsibility
62
+
63
+ The consuming app owns when the file is generated and deployed. The included CLI writes the file, but your app build or CI pipeline must run it before publishing assets.
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "react-build-reload",
3
+ "version": "0.1.1",
4
+ "description": "React library for detecting new frontend builds and reloading safely.",
5
+ "type": "module",
6
+ "author": {
7
+ "name": "Satyam Shah",
8
+ "url": "https://github.com/satyam-shah-gt"
9
+ },
10
+ "homepage": "https://react-refresh-website.satyamshah.workers.dev",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/satyam-shah-gt/react-build-reload"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/satyam-shah-gt/react-build-reload/issues"
17
+ },
18
+ "main": "./dist/index.cjs",
19
+ "module": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "bin": {
22
+ "react-build-reload": "./bin/react-build-reload.js"
23
+ },
24
+ "files": [
25
+ "bin",
26
+ "dist",
27
+ "README.md",
28
+ "docs/getting-started.md",
29
+ "docs/api-reference.md",
30
+ "docs/version-file.md",
31
+ "docs/usage-examples.md",
32
+ "docs/roadmap.md"
33
+ ],
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js",
38
+ "require": "./dist/index.cjs"
39
+ }
40
+ },
41
+ "sideEffects": false,
42
+ "scripts": {
43
+ "build": "vite build && tsc -p tsconfig.build.json",
44
+ "generate:version": "node ./bin/react-build-reload.js generate",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest",
47
+ "typecheck": "tsc --noEmit"
48
+ },
49
+ "keywords": [
50
+ "react",
51
+ "reload",
52
+ "deployment",
53
+ "build-version",
54
+ "frontend"
55
+ ],
56
+ "license": "MIT",
57
+ "peerDependencies": {
58
+ "react": ">=18.0.0",
59
+ "react-dom": ">=18.0.0"
60
+ },
61
+ "devDependencies": {
62
+ "@testing-library/jest-dom": "^6.6.3",
63
+ "@testing-library/react": "^16.3.0",
64
+ "@testing-library/user-event": "^14.6.1",
65
+ "@types/node": "^24.12.3",
66
+ "@types/react": "^19.2.14",
67
+ "@types/react-dom": "^19.2.3",
68
+ "@vitejs/plugin-react": "^6.0.1",
69
+ "jsdom": "^26.1.0",
70
+ "typescript": "~6.0.2",
71
+ "vite": "^8.0.12",
72
+ "vitest": "^4.1.7"
73
+ }
74
+ }