vite-plugin-build-version-file 0.1.17 → 0.1.18

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/README.md CHANGED
@@ -27,6 +27,7 @@
27
27
  - Vite 项目
28
28
  - Vue 3 + Vite 项目
29
29
  - VitePress 项目
30
+ - Nuxt 项目(需自行接 `render:html`,见下)
30
31
  - `peerDependencies` 要求 `vite >= 5`
31
32
  - Node.js 要求 `>= 18`
32
33
 
@@ -81,6 +82,33 @@ export default defineConfig({
81
82
 
82
83
  VitePress 构建会包含 client bundle 和 SSR bundle。插件只在最终静态产物 bundle 输出 `version.json`,避免同一次 VitePress build 出现重复日志或重复 emit。
83
84
 
85
+ ### Nuxt 项目
86
+
87
+ Nuxt 的 HTML 由 Nitro 渲染,Vite 客户端构建的入口是 JS 模块而不是 `index.html`,所以插件的 `transformIndexHtml` 在 Nuxt 里不会被调用,`window["__VERSION__"]` 不会自动注入。
88
+
89
+ 同样复用同一个插件实例,把它的 `headScript()` 推进 Nuxt `render:html` 钩子的 `html.head` 数组:
90
+
91
+ ```ts
92
+ import buildVersionPlugin from 'vite-plugin-build-version-file'
93
+
94
+ const buildVersion = buildVersionPlugin()
95
+
96
+ export default defineNuxtConfig({
97
+ vite: {
98
+ plugins: [buildVersion]
99
+ },
100
+ hooks: {
101
+ 'render:html'(html) {
102
+ html.head.push(buildVersion.headScript())
103
+ }
104
+ }
105
+ })
106
+ ```
107
+
108
+ `headScript()` 返回完整的 `<script>...</script>` 片段;`injectToHtml: false` 时、或 payload 还没生成时返回空字符串(不抛错),所以直接 `push` 是安全的。
109
+
110
+ 注意 Nuxt 里 `version.json` 的落点和 Vite / VitePress 不同:插件通过 `generateBundle` emit 到 Vite 客户端构建目录的根,而 Nuxt 发布到静态目录的是 assetsDir 内的内容,根目录上的文件不会被带出去。需要线上能访问这个文件时,要么把 `filename` 指到 assetsDir 内(例如 `_nuxt/version.json`),要么在构建后自行复制到 public 目录。
111
+
84
112
  ## 默认行为
85
113
 
86
114
  默认配置下,插件会做这些事:
@@ -342,45 +370,97 @@ console.log(payload.env)
342
370
 
343
371
  ## 做版本检查
344
372
 
345
- 插件不内置版本检查 UI。推荐业务项目自行处理:
373
+ 插件不内置版本检查 UI。推荐业务项目自行处理。下面示例可以直接放到应用入口、布局组件或独立的版本检查模块中:
346
374
 
347
375
  ```js
348
376
  const CHECK_INTERVAL = 60 * 1000
377
+ const CANCEL_RETRY_DELAY = 5 * 60 * 1000
349
378
  const VERSION_URL = '/version.json'
350
379
 
380
+ // 使用 window 共享状态,避免多个组件或入口各自创建轮询和更新弹窗。
381
+ const versionCheckState = (window.__VERSION_CHECK_STATE__ ??= {
382
+ timer: 0,
383
+ checking: false,
384
+ promptPromise: null
385
+ })
386
+
387
+ function scheduleVersionCheck(delay) {
388
+ window.clearTimeout(versionCheckState.timer)
389
+ versionCheckState.timer = window.setTimeout(() => {
390
+ void checkVersion()
391
+ }, delay)
392
+ }
393
+
394
+ function showUpdatePrompt() {
395
+ if (versionCheckState.promptPromise) {
396
+ return versionCheckState.promptPromise
397
+ }
398
+
399
+ // 使用 Element Plus 等异步弹窗时,替换为返回 Promise<boolean> 的业务弹窗函数。
400
+ versionCheckState.promptPromise = Promise.resolve(
401
+ window.confirm('检测到新版本,是否立即刷新页面?')
402
+ ).finally(() => {
403
+ versionCheckState.promptPromise = null
404
+ })
405
+
406
+ return versionCheckState.promptPromise
407
+ }
408
+
351
409
  async function checkVersion() {
410
+ // 防止多个入口同时发起版本请求。
411
+ if (versionCheckState.checking) {
412
+ return
413
+ }
414
+
415
+ versionCheckState.checking = true
416
+ let nextDelay = CHECK_INTERVAL
417
+
352
418
  try {
353
419
  const response = await fetch(VERSION_URL, {
354
420
  cache: 'no-store'
355
421
  })
422
+
423
+ if (!response.ok) {
424
+ throw new Error(`version request failed: ${response.status}`)
425
+ }
426
+
356
427
  const latestPayload = await response.json()
357
428
  const currentPayload = window.__VERSION__
358
429
 
359
430
  if (currentPayload && latestPayload.time !== currentPayload.time) {
360
- const shouldReload = window.confirm('检测到新版本,是否刷新页面?')
431
+ const shouldReload = await showUpdatePrompt()
361
432
 
362
433
  if (shouldReload) {
363
434
  window.location.reload()
364
435
  return
365
436
  }
437
+
438
+ // 用户取消后,5 分钟后再检查,避免短时间内重复打扰。
439
+ nextDelay = CANCEL_RETRY_DELAY
366
440
  }
367
441
  } catch (error) {
368
442
  console.warn('check version failed', error)
443
+ } finally {
444
+ versionCheckState.checking = false
445
+ scheduleVersionCheck(nextDelay)
369
446
  }
370
-
371
- window.setTimeout(checkVersion, CHECK_INTERVAL)
372
447
  }
373
448
 
374
- checkVersion()
449
+ void checkVersion()
375
450
  ```
376
451
 
377
- 建议:
452
+ 这个示例有几个关键点:
378
453
 
379
454
  - 通常比较 `payload.time` 即可
380
455
  - 请求 `version.json` 时使用 `cache: 'no-store'`
381
- - 轮询间隔不要过短,通常 1 分钟或更长
382
- - 弹窗、通知、静默刷新等策略放在业务项目里
383
- - 请求失败后继续下一轮检查,避免一次失败中断流程
456
+ - `window.__VERSION_CHECK_STATE__` 是当前页面内的共享状态;多个组件重复调用 `checkVersion()` 时,只会保留一个检查流程
457
+ - `promptPromise` 让更新弹窗成为单例;同一时间已有弹窗时,不会再次弹出
458
+ - 用户确认后立即刷新页面;用户取消后固定等待 5 分钟,再开始下一次检查
459
+ - 没有检测到更新或请求失败时,按 `CHECK_INTERVAL` 继续检查
460
+ - `CHECK_INTERVAL` 通常设置为 1 分钟或更长,避免产生过多请求
461
+ - 如果项目配置了自定义 `globalName`,将示例中的 `window.__VERSION__` 换成对应的全局变量
462
+
463
+ 示例使用原生 `window.confirm`,因此不依赖 UI 框架。接入 Element Plus、Ant Design 等异步弹窗时,只需让 `showUpdatePrompt` 内部调用业务弹窗,并在确认/取消时分别返回 `true`/`false`;不要在多个组件中各自维护一份弹窗状态。
384
464
 
385
465
  ## 刷新指定项目缓存
386
466
 
@@ -537,6 +617,7 @@ Those behaviors should stay in your application code.
537
617
  - Vite projects
538
618
  - Vue 3 + Vite projects
539
619
  - VitePress projects
620
+ - Nuxt projects (wire up `render:html` yourself, see below)
540
621
  - `vite >= 5`
541
622
  - Node.js `>= 18`
542
623
 
@@ -591,6 +672,33 @@ Use the same `buildVersion` instance in both places. Calling `buildVersionPlugin
591
672
 
592
673
  VitePress build includes a client bundle and an SSR bundle. The plugin emits `version.json` only from the final static bundle, so one VitePress build does not print duplicate payload logs or emit the same asset twice.
593
674
 
675
+ ### Nuxt
676
+
677
+ Nuxt renders HTML through Nitro, and its Vite client build entry is a JS module rather than an `index.html`, so the plugin's `transformIndexHtml` is never called and `window["__VERSION__"]` is not injected automatically.
678
+
679
+ Reuse the same plugin instance and push its `headScript()` into the `html.head` array of the Nuxt `render:html` hook:
680
+
681
+ ```ts
682
+ import buildVersionPlugin from 'vite-plugin-build-version-file'
683
+
684
+ const buildVersion = buildVersionPlugin()
685
+
686
+ export default defineNuxtConfig({
687
+ vite: {
688
+ plugins: [buildVersion]
689
+ },
690
+ hooks: {
691
+ 'render:html'(html) {
692
+ html.head.push(buildVersion.headScript())
693
+ }
694
+ }
695
+ })
696
+ ```
697
+
698
+ `headScript()` returns a complete `<script>...</script>` snippet. It returns an empty string when `injectToHtml: false` or when the payload has not been created yet, so pushing it directly is safe.
699
+
700
+ Note that `version.json` lands in a different place in Nuxt: the plugin emits it through `generateBundle` at the root of the Vite client build directory, while Nuxt publishes the contents of the assetsDir into the static output. Files sitting at that build root are not carried over. If the file must be reachable in production, either point `filename` inside the assetsDir (for example `_nuxt/version.json`) or copy it into the public directory after the build.
701
+
594
702
  ## Default Behavior
595
703
 
596
704
  With default options, the plugin:
@@ -819,45 +927,97 @@ Use this when you need to compare the current page against the latest deployed v
819
927
 
820
928
  ## Checking for Updates
821
929
 
822
- The plugin does not include update-checking UI. Keep that logic in your app.
930
+ The plugin does not include update-checking UI. Keep that logic in your app. The following example can be placed in your app entry, layout component, or a dedicated version-check module:
823
931
 
824
932
  ```js
825
933
  const CHECK_INTERVAL = 60 * 1000
934
+ const CANCEL_RETRY_DELAY = 5 * 60 * 1000
826
935
  const VERSION_URL = '/version.json'
827
936
 
937
+ // 使用 window 共享状态,让多个组件或入口复用同一套检查和弹窗。
938
+ const versionCheckState = (window.__VERSION_CHECK_STATE__ ??= {
939
+ timer: 0,
940
+ checking: false,
941
+ promptPromise: null
942
+ })
943
+
944
+ function scheduleVersionCheck(delay) {
945
+ window.clearTimeout(versionCheckState.timer)
946
+ versionCheckState.timer = window.setTimeout(() => {
947
+ void checkVersion()
948
+ }, delay)
949
+ }
950
+
951
+ function showUpdatePrompt() {
952
+ if (versionCheckState.promptPromise) {
953
+ return versionCheckState.promptPromise
954
+ }
955
+
956
+ // 使用异步业务弹窗时,替换为返回 Promise<boolean> 的弹窗函数。
957
+ versionCheckState.promptPromise = Promise.resolve(
958
+ window.confirm('A new version is available. Reload now?')
959
+ ).finally(() => {
960
+ versionCheckState.promptPromise = null
961
+ })
962
+
963
+ return versionCheckState.promptPromise
964
+ }
965
+
828
966
  async function checkVersion() {
967
+ // 防止多个入口同时发起版本请求。
968
+ if (versionCheckState.checking) {
969
+ return
970
+ }
971
+
972
+ versionCheckState.checking = true
973
+ let nextDelay = CHECK_INTERVAL
974
+
829
975
  try {
830
976
  const response = await fetch(VERSION_URL, {
831
977
  cache: 'no-store'
832
978
  })
979
+
980
+ if (!response.ok) {
981
+ throw new Error(`version request failed: ${response.status}`)
982
+ }
983
+
833
984
  const latestPayload = await response.json()
834
985
  const currentPayload = window.__VERSION__
835
986
 
836
987
  if (currentPayload && latestPayload.time !== currentPayload.time) {
837
- const shouldReload = window.confirm('A new version is available. Reload now?')
988
+ const shouldReload = await showUpdatePrompt()
838
989
 
839
990
  if (shouldReload) {
840
991
  window.location.reload()
841
992
  return
842
993
  }
994
+
995
+ // 用户取消后,等待 5 分钟再开始下一次检查。
996
+ nextDelay = CANCEL_RETRY_DELAY
843
997
  }
844
998
  } catch (error) {
845
999
  console.warn('check version failed', error)
1000
+ } finally {
1001
+ versionCheckState.checking = false
1002
+ scheduleVersionCheck(nextDelay)
846
1003
  }
847
-
848
- window.setTimeout(checkVersion, CHECK_INTERVAL)
849
1004
  }
850
1005
 
851
- checkVersion()
1006
+ void checkVersion()
852
1007
  ```
853
1008
 
854
- Recommendations:
1009
+ Key points:
855
1010
 
856
1011
  - Compare `payload.time` in most cases.
857
1012
  - Fetch `version.json` with `cache: 'no-store'`.
858
- - Avoid very short polling intervals. One minute or longer is usually enough.
859
- - Keep prompts, notifications, silent reloads, and retry rules in application code.
860
- - Continue polling after failed requests.
1013
+ - `window.__VERSION_CHECK_STATE__` is shared by the current page. Repeated `checkVersion()` calls reuse one check flow.
1014
+ - `promptPromise` makes the update prompt a singleton, so an already-open prompt is not shown again.
1015
+ - Confirming reloads immediately. Canceling waits five minutes before the next check.
1016
+ - No update or a failed request uses `CHECK_INTERVAL` for the next check.
1017
+ - Set `CHECK_INTERVAL` to one minute or longer to avoid excessive requests.
1018
+ - If you configure a custom `globalName`, replace `window.__VERSION__` with that global variable.
1019
+
1020
+ The example uses native `window.confirm`, so it has no UI framework dependency. With Element Plus, Ant Design, or another async dialog, replace the body of `showUpdatePrompt` with your app dialog and resolve `true` on confirm or `false` on cancel. Keep the shared state in one global object instead of maintaining separate prompt state in each component.
861
1021
 
862
1022
  ## Refreshing a Project Cache
863
1023
 
package/dist/index.cjs CHANGED
@@ -142,6 +142,9 @@ function serializePayload(payload) {
142
142
  function createInjectScript(globalName, payload) {
143
143
  return `window[${JSON.stringify(globalName)}] = ${JSON.stringify(payload)};`;
144
144
  }
145
+ function createInjectScriptTag(globalName, payload) {
146
+ return `<script>${createInjectScript(globalName, payload)}</script>`;
147
+ }
145
148
  function injectScriptToHead(code, script) {
146
149
  const scriptTag = `<script>${script}</script>`;
147
150
  if (code.includes(script)) {
@@ -207,6 +210,12 @@ function buildVersionPlugin(options = {}) {
207
210
  }
208
211
  return injectScriptToHead(code, createInjectScript(globalName, payload));
209
212
  };
213
+ const headScript = () => {
214
+ if (!injectToHtml || !payload) {
215
+ return "";
216
+ }
217
+ return createInjectScriptTag(globalName, payload);
218
+ };
210
219
  const plugin = {
211
220
  name: PLUGIN_NAME,
212
221
  config(userConfig, env) {
@@ -290,6 +299,9 @@ function buildVersionPlugin(options = {}) {
290
299
  Object.defineProperty(plugin, "transformHtml", {
291
300
  value: transformHtml
292
301
  });
302
+ Object.defineProperty(plugin, "headScript", {
303
+ value: headScript
304
+ });
293
305
  return plugin;
294
306
  }
295
307
  var index_default = buildVersionPlugin;
package/dist/index.d.cts CHANGED
@@ -22,6 +22,7 @@ interface BuildVersionPluginOptions {
22
22
  }
23
23
  type BuildVersionPlugin = Plugin & {
24
24
  transformHtml: (code: string, ...args: unknown[]) => string | undefined;
25
+ headScript: () => string;
25
26
  };
26
27
  declare function buildVersionPlugin(options?: BuildVersionPluginOptions): BuildVersionPlugin;
27
28
 
package/dist/index.d.ts CHANGED
@@ -22,6 +22,7 @@ interface BuildVersionPluginOptions {
22
22
  }
23
23
  type BuildVersionPlugin = Plugin & {
24
24
  transformHtml: (code: string, ...args: unknown[]) => string | undefined;
25
+ headScript: () => string;
25
26
  };
26
27
  declare function buildVersionPlugin(options?: BuildVersionPluginOptions): BuildVersionPlugin;
27
28
 
package/dist/index.mjs CHANGED
@@ -117,6 +117,9 @@ function serializePayload(payload) {
117
117
  function createInjectScript(globalName, payload) {
118
118
  return `window[${JSON.stringify(globalName)}] = ${JSON.stringify(payload)};`;
119
119
  }
120
+ function createInjectScriptTag(globalName, payload) {
121
+ return `<script>${createInjectScript(globalName, payload)}</script>`;
122
+ }
120
123
  function injectScriptToHead(code, script) {
121
124
  const scriptTag = `<script>${script}</script>`;
122
125
  if (code.includes(script)) {
@@ -182,6 +185,12 @@ function buildVersionPlugin(options = {}) {
182
185
  }
183
186
  return injectScriptToHead(code, createInjectScript(globalName, payload));
184
187
  };
188
+ const headScript = () => {
189
+ if (!injectToHtml || !payload) {
190
+ return "";
191
+ }
192
+ return createInjectScriptTag(globalName, payload);
193
+ };
185
194
  const plugin = {
186
195
  name: PLUGIN_NAME,
187
196
  config(userConfig, env) {
@@ -265,6 +274,9 @@ function buildVersionPlugin(options = {}) {
265
274
  Object.defineProperty(plugin, "transformHtml", {
266
275
  value: transformHtml
267
276
  });
277
+ Object.defineProperty(plugin, "headScript", {
278
+ value: headScript
279
+ });
268
280
  return plugin;
269
281
  }
270
282
  var index_default = buildVersionPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-build-version-file",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "A Vite plugin that emits version.json and injects version metadata into HTML.",
5
5
  "license": "MIT",
6
6
  "type": "module",