@gitlon/version 0.1.0

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 Long
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,244 @@
1
+ # @gitlon/version
2
+
3
+ Vite 构建版本插件。开发态提供版本接口,构建态输出 `version.json`,并可把同一份版本信息注入 HTML 和 Vite `define`。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pnpm add -D @gitlon/version
9
+ ```
10
+
11
+ 要求 Node.js >= 18、Vite >= 5。
12
+
13
+ ## Vite
14
+
15
+ ```ts
16
+ import { defineConfig } from 'vite'
17
+ import buildVersionPlugin from '@gitlon/version'
18
+
19
+ export default defineConfig({
20
+ plugins: [buildVersionPlugin()],
21
+ })
22
+ ```
23
+
24
+ 默认行为:
25
+
26
+ - 开发态通过 `/version.json` 返回版本信息;
27
+ - 构建态生成 `dist/version.json`;
28
+ - 向 HTML 注入 `window["__VERSION__"]`;
29
+ - 通过 Vite `define` 注入 `__VERSION__`;
30
+ - 读取项目最近的 `package.json`,写入 `pkgName`、`pkgVersion`;
31
+ - 开发态 `time` 为 `0`;
32
+ - 构建态 `time` 为 `Asia/Shanghai` 时区的 `YYYYMMDDHHmmss`。
33
+
34
+ ```json
35
+ {
36
+ "pkgName": "your-app",
37
+ "pkgVersion": "1.0.0",
38
+ "time": "20260917153045",
39
+ "env": "production"
40
+ }
41
+ ```
42
+
43
+ ## VitePress
44
+
45
+ 必须复用同一个插件实例:
46
+
47
+ ```ts
48
+ import { defineConfig } from 'vitepress'
49
+ import buildVersionPlugin from '@gitlon/version'
50
+
51
+ const buildVersion = buildVersionPlugin()
52
+
53
+ export default defineConfig({
54
+ vite: {
55
+ plugins: [buildVersion],
56
+ },
57
+ transformHtml: buildVersion.transformHtml,
58
+ })
59
+ ```
60
+
61
+ ## Nuxt
62
+
63
+ Nuxt HTML 不经过 Vite `transformIndexHtml`,需把 `headScript()` 接入 `render:html`:
64
+
65
+ ```ts
66
+ import buildVersionPlugin from '@gitlon/version'
67
+
68
+ const buildVersion = buildVersionPlugin()
69
+
70
+ export default defineNuxtConfig({
71
+ vite: {
72
+ plugins: [buildVersion],
73
+ },
74
+ hooks: {
75
+ 'render:html'(html) {
76
+ html.head.push(buildVersion.headScript())
77
+ },
78
+ },
79
+ })
80
+ ```
81
+
82
+ 插件自动识别 Nuxt 构建资产目录。默认静态文件路径通常为 `/_nuxt/version.json`。
83
+
84
+ ## 配置
85
+
86
+ ```ts
87
+ interface BuildVersionContext {
88
+ command: 'serve' | 'build'
89
+ mode: string
90
+ }
91
+
92
+ interface BuildVersionPluginOptions {
93
+ filename?: string
94
+ globalName?: string
95
+ defineName?: string | false
96
+ injectToHtml?: boolean
97
+ timeZone?: string
98
+ version?: string | number | ((context: BuildVersionContext) => string | number)
99
+ data?: Record<string, any>
100
+ log?: (content: any) => string
101
+ payload?: (json: any) => any
102
+ }
103
+ ```
104
+
105
+ ### `filename`
106
+
107
+ 版本文件路径,默认 `version.json`。自动跟随 Vite `base`:
108
+
109
+ ```ts
110
+ buildVersionPlugin({ filename: 'meta/version.json' })
111
+ ```
112
+
113
+ ### `globalName`
114
+
115
+ HTML 注入的 `window` 属性名,默认 `__VERSION__`:
116
+
117
+ ```ts
118
+ buildVersionPlugin({ globalName: '__APP_VERSION__' })
119
+ ```
120
+
121
+ ### `defineName`
122
+
123
+ Vite 编译期常量名,默认 `__VERSION__`。传 `false` 关闭:
124
+
125
+ ```ts
126
+ buildVersionPlugin({ defineName: false })
127
+ ```
128
+
129
+ ### `injectToHtml`
130
+
131
+ 是否注入 HTML,默认 `true`。关闭后仍提供和生成版本 JSON。
132
+
133
+ ### `timeZone`
134
+
135
+ 默认版本时间所用时区,默认 `Asia/Shanghai`。
136
+
137
+ ### `version`
138
+
139
+ 固定版本值或版本生成函数:
140
+
141
+ ```ts
142
+ buildVersionPlugin({
143
+ version: ({ command, mode }) => (command === 'serve' ? 0 : `${mode}-20260917`),
144
+ })
145
+ ```
146
+
147
+ ### `data`
148
+
149
+ 追加字段。覆盖顺序为 `data`、项目包信息、`time/env`、`payload`:
150
+
151
+ ```ts
152
+ buildVersionPlugin({
153
+ data: {
154
+ appName: 'admin',
155
+ commitSha: 'abc1234',
156
+ },
157
+ })
158
+ ```
159
+
160
+ ### `payload`
161
+
162
+ 最终改写 payload:
163
+
164
+ ```ts
165
+ buildVersionPlugin({
166
+ payload: (json) => ({
167
+ version: json.time,
168
+ environment: json.env,
169
+ }),
170
+ })
171
+ ```
172
+
173
+ ### `log`
174
+
175
+ 自定义构建完成日志:
176
+
177
+ ```ts
178
+ buildVersionPlugin({
179
+ log: (payload) => `build version: ${payload.time}`,
180
+ })
181
+ ```
182
+
183
+ ## 读取版本
184
+
185
+ 运行时:
186
+
187
+ ```ts
188
+ console.log(window.__VERSION__)
189
+ ```
190
+
191
+ 编译期:
192
+
193
+ ```ts
194
+ console.log(__VERSION__)
195
+ ```
196
+
197
+ 远程检查:
198
+
199
+ ```ts
200
+ const latest = await fetch('/version.json', { cache: 'no-store' }).then((response) =>
201
+ response.json(),
202
+ )
203
+
204
+ if (latest.time !== window.__VERSION__?.time) {
205
+ window.location.reload()
206
+ }
207
+ ```
208
+
209
+ ## 刷新项目缓存
210
+
211
+ ```ts
212
+ import { clearUrlCache } from '@gitlon/version/client'
213
+
214
+ await clearUrlCache({
215
+ urls: ['/vue-app/', '/vue-app/index.html'],
216
+ redirectUrl: '/vue-app/',
217
+ time: 5,
218
+ })
219
+ ```
220
+
221
+ `time` 单位为秒,默认 `5`。调用时显示全屏倒计时;依次使用无缓存请求、`reload` 请求和隐藏 iframe 刷新各 URL,完成或超时后携带随机参数跳转。
222
+
223
+ 无参数时处理当前页面路径及其 `index.html`:
224
+
225
+ ```ts
226
+ await clearUrlCache()
227
+ ```
228
+
229
+ hash 路由会保留,随机参数写在 `#` 前。该方法仅支持同源 URL;浏览器无法直接删除全部 HTTP 缓存,此方法只尽可能刷新目标项目缓存。
230
+
231
+ ## 类型导出
232
+
233
+ 主入口导出:
234
+
235
+ - `buildVersionPlugin`
236
+ - `BuildVersionContext`
237
+ - `BuildVersionPayload`
238
+ - `BuildVersionPluginOptions`
239
+ - `BuildVersionPlugin`
240
+
241
+ 客户端入口导出:
242
+
243
+ - `clearUrlCache`
244
+ - `ClearUrlCacheOptions`
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=`_`,t=`gitlon-version-cache-iframe`,n=`gitlon-version-loading`,r=5e3,i=5;function a(e){document.getElementById(n)?.remove();let t=document.createElement(`div`),r=document.createElement(`div`),i=document.createElement(`div`),a=Date.now()+e*1e3;t.id=n,t.style.cssText=`position:fixed;inset:0;z-index:2147483647;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(0,0,0,.65);color:#fff;font:14px/1.5 sans-serif;`,r.style.cssText=`width:36px;height:36px;margin-bottom:16px;border:3px solid rgba(255,255,255,.35);border-top-color:#fff;border-radius:50%;animation:gitlon-version-loading .8s linear infinite;`,i.textContent=`请稍等,正在检测更新...(${Math.ceil(e)}秒)`;let o=document.createElement(`style`);o.textContent=`@keyframes gitlon-version-loading{to{transform:rotate(360deg)}}`,t.append(o,r,i),document.documentElement.appendChild(t);let s=window.setInterval(()=>{let e=Math.max(0,Math.ceil((a-Date.now())/1e3));i.textContent=`请稍等,正在检测更新...(${e}秒)`},1e3);return()=>{window.clearInterval(s),t.remove()}}function o(t){let n=new URL(t,window.location.href),r=n.hash;return n.hash=``,n.searchParams.set(e,`${Date.now()}-${Math.random().toString(36).slice(2)}`),n.hash=r,n.href}function s(e){let t=new URL(e);return t.pathname.endsWith(`/`)||(t.pathname+=`/`),t.pathname+=`index.html`,t.href}function c(e){return new Promise(n=>{document.getElementById(t)?.remove();let i=document.createElement(`iframe`),a=!1,o=()=>{a||(a=!0,window.clearTimeout(s),n())},s=window.setTimeout(o,r);i.hidden=!0,i.id=t,i.addEventListener(`load`,o,{once:!0}),i.addEventListener(`error`,o,{once:!0}),i.src=e,document.documentElement.appendChild(i)})}async function l(e){let t=new URL(e,window.location.href).href;console.info(`[@gitlon/version] Clear cache for ${t}`),c(o(t)),await Promise.allSettled([fetch(o(t),{cache:`no-store`}).then(e=>e.arrayBuffer()),fetch(t,{cache:`reload`}).then(e=>e.arrayBuffer())])}async function u(e={}){let t=window.location.origin+window.location.pathname,n=e.urls??[t,s(t)],r=e.time,o=typeof r==`number`&&Number.isFinite(r)&&r>0?r:i,c=new URL(e.redirectUrl??n[0]??t,window.location.href).href,u=a(o),d=0;try{await Promise.race([(async()=>{for(let e of n)try{await l(e)}catch{}})(),new Promise(e=>{d=window.setTimeout(e,o*1e3)})])}finally{window.clearTimeout(d),u(),window.location.replace(c)}}exports.clearUrlCache=u;
2
+ //# sourceMappingURL=client.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.cjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["const CACHE_BUST_PARAM = '_'\nconst IFRAME_ID = 'gitlon-version-cache-iframe'\nconst LOADING_ID = 'gitlon-version-loading'\nconst IFRAME_TIMEOUT = 5_000\nconst DEFAULT_REFRESH_TIME = 5\n\nexport interface ClearUrlCacheOptions {\n urls?: string[]\n redirectUrl?: string\n time?: number\n}\n\nfunction showLoading(time: number): () => void {\n document.getElementById(LOADING_ID)?.remove()\n\n const loading = document.createElement('div')\n const spinner = document.createElement('div')\n const text = document.createElement('div')\n const deadline = Date.now() + time * 1_000\n\n loading.id = LOADING_ID\n loading.style.cssText =\n 'position:fixed;inset:0;z-index:2147483647;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(0,0,0,.65);color:#fff;font:14px/1.5 sans-serif;'\n spinner.style.cssText =\n 'width:36px;height:36px;margin-bottom:16px;border:3px solid rgba(255,255,255,.35);border-top-color:#fff;border-radius:50%;animation:gitlon-version-loading .8s linear infinite;'\n text.textContent = `请稍等,正在检测更新...(${Math.ceil(time)}秒)`\n\n const style = document.createElement('style')\n style.textContent = '@keyframes gitlon-version-loading{to{transform:rotate(360deg)}}'\n loading.append(style, spinner, text)\n document.documentElement.appendChild(loading)\n\n const countdown = window.setInterval(() => {\n const seconds = Math.max(0, Math.ceil((deadline - Date.now()) / 1_000))\n text.textContent = `请稍等,正在检测更新...(${seconds}秒)`\n }, 1_000)\n\n return () => {\n window.clearInterval(countdown)\n loading.remove()\n }\n}\n\nfunction cacheBustedUrl(url: string): string {\n const target = new URL(url, window.location.href)\n const hash = target.hash\n\n target.hash = ''\n target.searchParams.set(\n CACHE_BUST_PARAM,\n `${Date.now()}-${Math.random().toString(36).slice(2)}`,\n )\n target.hash = hash\n\n return target.href\n}\n\nfunction indexUrl(url: string): string {\n const target = new URL(url)\n if (!target.pathname.endsWith('/')) target.pathname += '/'\n target.pathname += 'index.html'\n return target.href\n}\n\nfunction loadIframe(url: string): Promise<void> {\n return new Promise((resolve) => {\n document.getElementById(IFRAME_ID)?.remove()\n\n const iframe = document.createElement('iframe')\n let completed = false\n\n const complete = () => {\n if (completed) return\n completed = true\n window.clearTimeout(timeout)\n resolve()\n }\n\n const timeout = window.setTimeout(complete, IFRAME_TIMEOUT)\n iframe.hidden = true\n iframe.id = IFRAME_ID\n iframe.addEventListener('load', complete, { once: true })\n iframe.addEventListener('error', complete, { once: true })\n iframe.src = url\n document.documentElement.appendChild(iframe)\n })\n}\n\nasync function refreshUrlCache(url: string): Promise<void> {\n const target = new URL(url, window.location.href).href\n console.info(`[@gitlon/version] Clear cache for ${target}`)\n\n loadIframe(cacheBustedUrl(target))\n await Promise.allSettled([\n fetch(cacheBustedUrl(target), { cache: 'no-store' }).then((response) =>\n response.arrayBuffer(),\n ),\n fetch(target, { cache: 'reload' }).then((response) => response.arrayBuffer()),\n ])\n\n}\n\nexport async function clearUrlCache(options: ClearUrlCacheOptions = {}): Promise<void> {\n const defaultUrl = window.location.origin + window.location.pathname\n const urls = options.urls ?? [defaultUrl, indexUrl(defaultUrl)]\n const configuredTime = options.time\n const time =\n typeof configuredTime === 'number' &&\n Number.isFinite(configuredTime) &&\n configuredTime > 0\n ? configuredTime\n : DEFAULT_REFRESH_TIME\n const redirectUrl = new URL(\n options.redirectUrl ?? urls[0] ?? defaultUrl,\n window.location.href,\n ).href\n const hideLoading = showLoading(time)\n let refreshTimeout = 0\n\n try {\n await Promise.race([\n (async () => {\n for (const url of urls) {\n try {\n await refreshUrlCache(url)\n } catch {\n // 单个 URL 失败不阻断后续 URL。\n }\n }\n })(),\n new Promise<void>((resolve) => {\n refreshTimeout = window.setTimeout(resolve, time * 1_000)\n }),\n ])\n } finally {\n window.clearTimeout(refreshTimeout)\n hideLoading()\n window.location.replace(redirectUrl)\n }\n}\n"],"mappings":"mEAAA,IAAM,EAAmB,IACnB,EAAY,8BACZ,EAAa,yBACb,EAAiB,IACjB,EAAuB,EAQ7B,SAAS,EAAY,EAA0B,CAC7C,SAAS,eAAe,CAAU,CAAC,EAAE,OAAO,EAE5C,IAAM,EAAU,SAAS,cAAc,KAAK,EACtC,EAAU,SAAS,cAAc,KAAK,EACtC,EAAO,SAAS,cAAc,KAAK,EACnC,EAAW,KAAK,IAAI,EAAI,EAAO,IAErC,EAAQ,GAAK,EACb,EAAQ,MAAM,QACZ,yLACF,EAAQ,MAAM,QACZ,iLACF,EAAK,YAAc,iBAAiB,KAAK,KAAK,CAAI,EAAE,IAEpD,IAAM,EAAQ,SAAS,cAAc,OAAO,EAC5C,EAAM,YAAc,kEACpB,EAAQ,OAAO,EAAO,EAAS,CAAI,EACnC,SAAS,gBAAgB,YAAY,CAAO,EAE5C,IAAM,EAAY,OAAO,gBAAkB,CACzC,IAAM,EAAU,KAAK,IAAI,EAAG,KAAK,MAAM,EAAW,KAAK,IAAI,GAAK,GAAK,CAAC,EACtE,EAAK,YAAc,iBAAiB,EAAQ,GAC9C,EAAG,GAAK,EAER,UAAa,CACX,OAAO,cAAc,CAAS,EAC9B,EAAQ,OAAO,CACjB,CACF,CAEA,SAAS,EAAe,EAAqB,CAC3C,IAAM,EAAS,IAAI,IAAI,EAAK,OAAO,SAAS,IAAI,EAC1C,EAAO,EAAO,KASpB,MAPA,GAAO,KAAO,GACd,EAAO,aAAa,IAClB,EACA,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,GACrD,EACA,EAAO,KAAO,EAEP,EAAO,IAChB,CAEA,SAAS,EAAS,EAAqB,CACrC,IAAM,EAAS,IAAI,IAAI,CAAG,EAG1B,OAFK,EAAO,SAAS,SAAS,GAAG,IAAG,EAAO,UAAY,KACvD,EAAO,UAAY,aACZ,EAAO,IAChB,CAEA,SAAS,EAAW,EAA4B,CAC9C,OAAO,IAAI,QAAS,GAAY,CAC9B,SAAS,eAAe,CAAS,CAAC,EAAE,OAAO,EAE3C,IAAM,EAAS,SAAS,cAAc,QAAQ,EAC1C,EAAY,GAEV,MAAiB,CACjB,IACJ,EAAY,GACZ,OAAO,aAAa,CAAO,EAC3B,EAAQ,EACV,EAEM,EAAU,OAAO,WAAW,EAAU,CAAc,EAC1D,EAAO,OAAS,GAChB,EAAO,GAAK,EACZ,EAAO,iBAAiB,OAAQ,EAAU,CAAE,KAAM,EAAK,CAAC,EACxD,EAAO,iBAAiB,QAAS,EAAU,CAAE,KAAM,EAAK,CAAC,EACzD,EAAO,IAAM,EACb,SAAS,gBAAgB,YAAY,CAAM,CAC7C,CAAC,CACH,CAEA,eAAe,EAAgB,EAA4B,CACzD,IAAM,EAAS,IAAI,IAAI,EAAK,OAAO,SAAS,IAAI,CAAC,CAAC,KAClD,QAAQ,KAAK,qCAAqC,GAAQ,EAE1D,EAAW,EAAe,CAAM,CAAC,EACjC,MAAM,QAAQ,WAAW,CACvB,MAAM,EAAe,CAAM,EAAG,CAAE,MAAO,UAAW,CAAC,CAAC,CAAC,KAAM,GACzD,EAAS,YAAY,CACvB,EACA,MAAM,EAAQ,CAAE,MAAO,QAAS,CAAC,CAAC,CAAC,KAAM,GAAa,EAAS,YAAY,CAAC,CAC9E,CAAC,CAEH,CAEA,eAAsB,EAAc,EAAgC,CAAC,EAAkB,CACrF,IAAM,EAAa,OAAO,SAAS,OAAS,OAAO,SAAS,SACtD,EAAO,EAAQ,MAAQ,CAAC,EAAY,EAAS,CAAU,CAAC,EACxD,EAAiB,EAAQ,KACzB,EACJ,OAAO,GAAmB,UAC1B,OAAO,SAAS,CAAc,GAC9B,EAAiB,EACb,EACA,EACA,EAAc,IAAI,IACtB,EAAQ,aAAe,EAAK,IAAM,EAClC,OAAO,SAAS,IAClB,CAAC,CAAC,KACI,EAAc,EAAY,CAAI,EAChC,EAAiB,EAErB,GAAI,CACF,MAAM,QAAQ,KAAK,EAChB,SAAY,CACX,IAAK,IAAM,KAAO,EAChB,GAAI,CACF,MAAM,EAAgB,CAAG,CAC3B,MAAQ,CAER,CAEJ,EAAA,CAAG,EACH,IAAI,QAAe,GAAY,CAC7B,EAAiB,OAAO,WAAW,EAAS,EAAO,GAAK,CAC1D,CAAC,CACH,CAAC,CACH,QAAU,CACR,OAAO,aAAa,CAAc,EAClC,EAAY,EACZ,OAAO,SAAS,QAAQ,CAAW,CACrC,CACF"}
@@ -0,0 +1,9 @@
1
+ export declare function clearUrlCache(options?: ClearUrlCacheOptions): Promise<void>;
2
+
3
+ export declare interface ClearUrlCacheOptions {
4
+ urls?: string[];
5
+ redirectUrl?: string;
6
+ time?: number;
7
+ }
8
+
9
+ export { }
package/dist/client.js ADDED
@@ -0,0 +1,55 @@
1
+ //#region src/client.ts
2
+ var e = "gitlon-version-cache-iframe", t = "gitlon-version-loading";
3
+ function n(e) {
4
+ document.getElementById(t)?.remove();
5
+ let n = document.createElement("div"), r = document.createElement("div"), i = document.createElement("div"), a = Date.now() + e * 1e3;
6
+ n.id = t, n.style.cssText = "position:fixed;inset:0;z-index:2147483647;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(0,0,0,.65);color:#fff;font:14px/1.5 sans-serif;", r.style.cssText = "width:36px;height:36px;margin-bottom:16px;border:3px solid rgba(255,255,255,.35);border-top-color:#fff;border-radius:50%;animation:gitlon-version-loading .8s linear infinite;", i.textContent = `请稍等,正在检测更新...(${Math.ceil(e)}秒)`;
7
+ let o = document.createElement("style");
8
+ o.textContent = "@keyframes gitlon-version-loading{to{transform:rotate(360deg)}}", n.append(o, r, i), document.documentElement.appendChild(n);
9
+ let s = window.setInterval(() => {
10
+ let e = Math.max(0, Math.ceil((a - Date.now()) / 1e3));
11
+ i.textContent = `请稍等,正在检测更新...(${e}秒)`;
12
+ }, 1e3);
13
+ return () => {
14
+ window.clearInterval(s), n.remove();
15
+ };
16
+ }
17
+ function r(e) {
18
+ let t = new URL(e, window.location.href), n = t.hash;
19
+ return t.hash = "", t.searchParams.set("_", `${Date.now()}-${Math.random().toString(36).slice(2)}`), t.hash = n, t.href;
20
+ }
21
+ function i(e) {
22
+ let t = new URL(e);
23
+ return t.pathname.endsWith("/") || (t.pathname += "/"), t.pathname += "index.html", t.href;
24
+ }
25
+ function a(t) {
26
+ return new Promise((n) => {
27
+ document.getElementById(e)?.remove();
28
+ let r = document.createElement("iframe"), i = !1, a = () => {
29
+ i || (i = !0, window.clearTimeout(o), n());
30
+ }, o = window.setTimeout(a, 5e3);
31
+ r.hidden = !0, r.id = e, r.addEventListener("load", a, { once: !0 }), r.addEventListener("error", a, { once: !0 }), r.src = t, document.documentElement.appendChild(r);
32
+ });
33
+ }
34
+ async function o(e) {
35
+ let t = new URL(e, window.location.href).href;
36
+ console.info(`[@gitlon/version] Clear cache for ${t}`), a(r(t)), await Promise.allSettled([fetch(r(t), { cache: "no-store" }).then((e) => e.arrayBuffer()), fetch(t, { cache: "reload" }).then((e) => e.arrayBuffer())]);
37
+ }
38
+ async function s(e = {}) {
39
+ let t = window.location.origin + window.location.pathname, r = e.urls ?? [t, i(t)], a = e.time, s = typeof a == "number" && Number.isFinite(a) && a > 0 ? a : 5, c = new URL(e.redirectUrl ?? r[0] ?? t, window.location.href).href, l = n(s), u = 0;
40
+ try {
41
+ await Promise.race([(async () => {
42
+ for (let e of r) try {
43
+ await o(e);
44
+ } catch {}
45
+ })(), new Promise((e) => {
46
+ u = window.setTimeout(e, s * 1e3);
47
+ })]);
48
+ } finally {
49
+ window.clearTimeout(u), l(), window.location.replace(c);
50
+ }
51
+ }
52
+ //#endregion
53
+ export { s as clearUrlCache };
54
+
55
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["const CACHE_BUST_PARAM = '_'\nconst IFRAME_ID = 'gitlon-version-cache-iframe'\nconst LOADING_ID = 'gitlon-version-loading'\nconst IFRAME_TIMEOUT = 5_000\nconst DEFAULT_REFRESH_TIME = 5\n\nexport interface ClearUrlCacheOptions {\n urls?: string[]\n redirectUrl?: string\n time?: number\n}\n\nfunction showLoading(time: number): () => void {\n document.getElementById(LOADING_ID)?.remove()\n\n const loading = document.createElement('div')\n const spinner = document.createElement('div')\n const text = document.createElement('div')\n const deadline = Date.now() + time * 1_000\n\n loading.id = LOADING_ID\n loading.style.cssText =\n 'position:fixed;inset:0;z-index:2147483647;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(0,0,0,.65);color:#fff;font:14px/1.5 sans-serif;'\n spinner.style.cssText =\n 'width:36px;height:36px;margin-bottom:16px;border:3px solid rgba(255,255,255,.35);border-top-color:#fff;border-radius:50%;animation:gitlon-version-loading .8s linear infinite;'\n text.textContent = `请稍等,正在检测更新...(${Math.ceil(time)}秒)`\n\n const style = document.createElement('style')\n style.textContent = '@keyframes gitlon-version-loading{to{transform:rotate(360deg)}}'\n loading.append(style, spinner, text)\n document.documentElement.appendChild(loading)\n\n const countdown = window.setInterval(() => {\n const seconds = Math.max(0, Math.ceil((deadline - Date.now()) / 1_000))\n text.textContent = `请稍等,正在检测更新...(${seconds}秒)`\n }, 1_000)\n\n return () => {\n window.clearInterval(countdown)\n loading.remove()\n }\n}\n\nfunction cacheBustedUrl(url: string): string {\n const target = new URL(url, window.location.href)\n const hash = target.hash\n\n target.hash = ''\n target.searchParams.set(\n CACHE_BUST_PARAM,\n `${Date.now()}-${Math.random().toString(36).slice(2)}`,\n )\n target.hash = hash\n\n return target.href\n}\n\nfunction indexUrl(url: string): string {\n const target = new URL(url)\n if (!target.pathname.endsWith('/')) target.pathname += '/'\n target.pathname += 'index.html'\n return target.href\n}\n\nfunction loadIframe(url: string): Promise<void> {\n return new Promise((resolve) => {\n document.getElementById(IFRAME_ID)?.remove()\n\n const iframe = document.createElement('iframe')\n let completed = false\n\n const complete = () => {\n if (completed) return\n completed = true\n window.clearTimeout(timeout)\n resolve()\n }\n\n const timeout = window.setTimeout(complete, IFRAME_TIMEOUT)\n iframe.hidden = true\n iframe.id = IFRAME_ID\n iframe.addEventListener('load', complete, { once: true })\n iframe.addEventListener('error', complete, { once: true })\n iframe.src = url\n document.documentElement.appendChild(iframe)\n })\n}\n\nasync function refreshUrlCache(url: string): Promise<void> {\n const target = new URL(url, window.location.href).href\n console.info(`[@gitlon/version] Clear cache for ${target}`)\n\n loadIframe(cacheBustedUrl(target))\n await Promise.allSettled([\n fetch(cacheBustedUrl(target), { cache: 'no-store' }).then((response) =>\n response.arrayBuffer(),\n ),\n fetch(target, { cache: 'reload' }).then((response) => response.arrayBuffer()),\n ])\n\n}\n\nexport async function clearUrlCache(options: ClearUrlCacheOptions = {}): Promise<void> {\n const defaultUrl = window.location.origin + window.location.pathname\n const urls = options.urls ?? [defaultUrl, indexUrl(defaultUrl)]\n const configuredTime = options.time\n const time =\n typeof configuredTime === 'number' &&\n Number.isFinite(configuredTime) &&\n configuredTime > 0\n ? configuredTime\n : DEFAULT_REFRESH_TIME\n const redirectUrl = new URL(\n options.redirectUrl ?? urls[0] ?? defaultUrl,\n window.location.href,\n ).href\n const hideLoading = showLoading(time)\n let refreshTimeout = 0\n\n try {\n await Promise.race([\n (async () => {\n for (const url of urls) {\n try {\n await refreshUrlCache(url)\n } catch {\n // 单个 URL 失败不阻断后续 URL。\n }\n }\n })(),\n new Promise<void>((resolve) => {\n refreshTimeout = window.setTimeout(resolve, time * 1_000)\n }),\n ])\n } finally {\n window.clearTimeout(refreshTimeout)\n hideLoading()\n window.location.replace(redirectUrl)\n }\n}\n"],"mappings":"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"}
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});let e=require("node:fs"),t=require("node:path");var n=`@gitlon/version`,r=`version.json`,i=`__VERSION__`,a=`__VERSION__`,o=`Asia/Shanghai`;function s(e){let t=e.replace(/\\/g,`/`).replace(/^\/+/,``);if(!t||t.endsWith(`/`))throw Error(`[${n}] "filename" must point to a file path.`);return t}function c(e,t){if(!e.trim())throw Error(`[${n}] "${t}" cannot be empty.`);return e}function l(e){let t=e.trim();if(!t)throw Error(`[${n}] "timeZone" cannot be empty.`);try{new Intl.DateTimeFormat(`en-US`,{timeZone:t}).format(new Date)}catch{throw Error(`[${n}] "timeZone" is invalid.`)}return t}function u(e){if(e===void 0)return{};if(!e||Array.isArray(e)||typeof e!=`object`)throw Error(`[${n}] "data" must be a plain object.`);return e}function d(e,t){if(e===`serve`)return 0;let n=new Intl.DateTimeFormat(`en-US`,{timeZone:t,year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,second:`2-digit`,hour12:!1,hourCycle:`h23`}).formatToParts(new Date),r=e=>n.find(t=>t.type===e)?.value??``;return[r(`year`),r(`month`),r(`day`),r(`hour`),r(`minute`),r(`second`)].join(``)}function f(n){let r=n;for(;;){let n=(0,t.join)(r,`package.json`);if((0,e.existsSync)(n))return n;let i=(0,t.dirname)(r);if(i===r)return;r=i}}function p(t){let n=f(t);if(!n)return{};try{let t=JSON.parse((0,e.readFileSync)(n,`utf8`)),r={};return t.name!==void 0&&(r.pkgName=t.name),t.version!==void 0&&(r.pkgVersion=t.version),r}catch{return{}}}function m(e){return e?(0,t.resolve)(process.cwd(),e):process.cwd()}function h(e,t,n){return typeof e.version==`function`?e.version(t):e.version===void 0?d(t.command,n):e.version}function g(e,t,n,r,i){let a={...n,...r,time:h(e,t,i),env:t.mode};return e.payload&&(a=e.payload(a)),a}function _(e){return`${JSON.stringify(e,null,2)}\n`}function v(e,t){return`window[${JSON.stringify(e)}] = ${JSON.stringify(t)};`}function y(e,t){return`<script>${v(e,t)}<\/script>`}function b(e,t){if(e.includes(t))return e;let n=`<script>${t}<\/script>`;return e.includes(`</head>`)?e.replace(`</head>`,`${n}</head>`):`${n}${e}`}function x(e){return e?new URL(e,`http://localhost`).pathname:`/`}function S(e,t){let n=e===`./`?`/`:e||`/`,r=n.startsWith(`/`)?n:`/${n}`;return`${r.endsWith(`/`)?r.slice(0,-1):r}/${t}`.replace(/\/{2,}/g,`/`)}function C(e,t){if(e.appType!==`custom`||e.build.copyPublicDir!==!1)return t;let n=s(e.build.assetsDir);return n===`.`||t.startsWith(`${n}/`)?t:`${n}/${t}`}function w(e={}){let t=s(e.filename??r),d=c(e.globalName??i,`globalName`),f=e.defineName!==!1&&c(e.defineName??a,`defineName`),h=e.injectToHtml??!0,w=l(e.timeZone??o),T=u(e.data),E,D=t,O=!1,k=(t,n)=>{E=g(e,t,T,p(n),w)},A=e=>{if(h&&E)return b(e,v(d,E))},j=()=>!h||!E?``:y(d,E),M={name:n,config(e,t){if(E||k({command:t.command,mode:t.mode},m(e.root)),f&&E)return{define:{[f]:JSON.stringify(E)}}},configResolved(e){O=!!e.build.ssr&&!e.build.ssrEmitAssets,D=C(e,t),E||k({command:e.command,mode:e.mode},e.root)},configureServer(e){let n=S(e.config.base,t);e.middlewares.use((e,t,r)=>{if(e.method!==`GET`&&e.method!==`HEAD`){r();return}if(x(e.url)!==n||!E){r();return}t.statusCode=200,t.setHeader(`Content-Type`,`application/json; charset=utf-8`),t.setHeader(`Cache-Control`,`no-cache`),t.end(e.method===`HEAD`?void 0:_(E))})},transformIndexHtml(){if(h&&E)return[{tag:`script`,injectTo:`head`,children:v(d,E)}]},generateBundle(){E&&!O&&(this.emitFile({type:`asset`,fileName:D,source:_(E)}),e.log?console.log(e.log(E)):(console.log(`[${n}][build finished]`),console.log(_(E))))}};return Object.defineProperty(M,"transformHtml",{value:A}),Object.defineProperty(M,"headScript",{value:j}),M}exports.buildVersionPlugin=w,exports.default=w;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\nimport type { HtmlTagDescriptor, Plugin, UserConfig } from 'vite'\n\nexport interface BuildVersionContext {\n command: 'serve' | 'build'\n mode: string\n}\n\nexport interface BuildVersionPayload {\n time: string | number\n env: string\n [key: string]: any\n}\n\nexport interface BuildVersionPluginOptions {\n filename?: string\n globalName?: string\n defineName?: string | false\n injectToHtml?: boolean\n timeZone?: string\n version?: string | number | ((context: BuildVersionContext) => string | number)\n data?: Record<string, any>\n log?: (content: any) => string\n payload?: (json: any) => any\n}\n\nexport type BuildVersionPlugin = Plugin & {\n transformHtml: (code: string, ...args: unknown[]) => string | undefined\n headScript: () => string\n}\n\nconst PLUGIN_NAME = '@gitlon/version'\nconst DEFAULT_FILENAME = 'version.json'\nconst DEFAULT_GLOBAL_NAME = '__VERSION__'\nconst DEFAULT_DEFINE_NAME = '__VERSION__'\nconst DEFAULT_TIME_ZONE = 'Asia/Shanghai'\n\nfunction normalizeFilename(filename: string): string {\n const value = filename.replace(/\\\\/g, '/').replace(/^\\/+/, '')\n\n if (!value || value.endsWith('/')) {\n throw new Error(`[${PLUGIN_NAME}] \"filename\" must point to a file path.`)\n }\n\n return value\n}\n\nfunction normalizeRequiredName(value: string, option: 'globalName' | 'defineName'): string {\n if (!value.trim()) {\n throw new Error(`[${PLUGIN_NAME}] \"${option}\" cannot be empty.`)\n }\n\n return value\n}\n\nfunction normalizeTimeZone(timeZone: string): string {\n const value = timeZone.trim()\n\n if (!value) {\n throw new Error(`[${PLUGIN_NAME}] \"timeZone\" cannot be empty.`)\n }\n\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: value }).format(new Date())\n } catch {\n throw new Error(`[${PLUGIN_NAME}] \"timeZone\" is invalid.`)\n }\n\n return value\n}\n\nfunction normalizeData(data: BuildVersionPluginOptions['data']): Record<string, any> {\n if (data === undefined) return {}\n\n if (!data || Array.isArray(data) || typeof data !== 'object') {\n throw new Error(`[${PLUGIN_NAME}] \"data\" must be a plain object.`)\n }\n\n return data\n}\n\nfunction createDefaultVersion(command: BuildVersionContext['command'], timeZone: string) {\n if (command === 'serve') return 0\n\n const parts = new Intl.DateTimeFormat('en-US', {\n timeZone,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false,\n hourCycle: 'h23',\n }).formatToParts(new Date())\n const part = (type: Intl.DateTimeFormatPartTypes) =>\n parts.find((item) => item.type === type)?.value ?? ''\n\n return [\n part('year'),\n part('month'),\n part('day'),\n part('hour'),\n part('minute'),\n part('second'),\n ].join('')\n}\n\nfunction findPackageJson(root: string): string | undefined {\n let directory = root\n\n while (true) {\n const packageJson = join(directory, 'package.json')\n if (existsSync(packageJson)) return packageJson\n\n const parent = dirname(directory)\n if (parent === directory) return undefined\n directory = parent\n }\n}\n\nfunction readPackageData(root: string): Record<string, any> {\n const filename = findPackageJson(root)\n if (!filename) return {}\n\n try {\n const packageJson = JSON.parse(readFileSync(filename, 'utf8'))\n const result: Record<string, any> = {}\n\n if (packageJson.name !== undefined) result.pkgName = packageJson.name\n if (packageJson.version !== undefined) result.pkgVersion = packageJson.version\n\n return result\n } catch {\n return {}\n }\n}\n\nfunction projectRoot(root: string | undefined): string {\n return root ? resolve(process.cwd(), root) : process.cwd()\n}\n\nfunction versionFor(\n options: BuildVersionPluginOptions,\n context: BuildVersionContext,\n timeZone: string,\n): string | number {\n if (typeof options.version === 'function') return options.version(context)\n if (options.version !== undefined) return options.version\n return createDefaultVersion(context.command, timeZone)\n}\n\nfunction makePayload(\n options: BuildVersionPluginOptions,\n context: BuildVersionContext,\n data: Record<string, any>,\n packageData: Record<string, any>,\n timeZone: string,\n): BuildVersionPayload {\n let payload: BuildVersionPayload = {\n ...data,\n ...packageData,\n time: versionFor(options, context, timeZone),\n env: context.mode,\n }\n\n if (options.payload) payload = options.payload(payload)\n return payload\n}\n\nfunction serialize(payload: BuildVersionPayload): string {\n return `${JSON.stringify(payload, null, 2)}\\n`\n}\n\nfunction assignment(globalName: string, payload: BuildVersionPayload): string {\n return `window[${JSON.stringify(globalName)}] = ${JSON.stringify(payload)};`\n}\n\nfunction scriptTag(globalName: string, payload: BuildVersionPayload): string {\n return `<script>${assignment(globalName, payload)}</script>`\n}\n\nfunction injectIntoHead(html: string, script: string): string {\n if (html.includes(script)) return html\n\n const tag = `<script>${script}</script>`\n return html.includes('</head>') ? html.replace('</head>', `${tag}</head>`) : `${tag}${html}`\n}\n\nfunction pathname(url: string | undefined): string {\n return url ? new URL(url, 'http://localhost').pathname : '/'\n}\n\nfunction requestPath(base: string, filename: string): string {\n const rawBase = base === './' ? '/' : base || '/'\n const leadingBase = rawBase.startsWith('/') ? rawBase : `/${rawBase}`\n const cleanBase = leadingBase.endsWith('/') ? leadingBase.slice(0, -1) : leadingBase\n return `${cleanBase}/${filename}`.replace(/\\/{2,}/g, '/')\n}\n\nfunction nuxtFilename(\n config: {\n appType: string\n build: { assetsDir: string; copyPublicDir: boolean }\n },\n filename: string,\n): string {\n if (config.appType !== 'custom' || config.build.copyPublicDir !== false) return filename\n\n const assetsDir = normalizeFilename(config.build.assetsDir)\n return assetsDir === '.' || filename.startsWith(`${assetsDir}/`)\n ? filename\n : `${assetsDir}/${filename}`\n}\n\nexport function buildVersionPlugin(\n options: BuildVersionPluginOptions = {},\n): BuildVersionPlugin {\n const filename = normalizeFilename(options.filename ?? DEFAULT_FILENAME)\n const globalName = normalizeRequiredName(\n options.globalName ?? DEFAULT_GLOBAL_NAME,\n 'globalName',\n )\n const defineName =\n options.defineName === false\n ? false\n : normalizeRequiredName(options.defineName ?? DEFAULT_DEFINE_NAME, 'defineName')\n const injectToHtml = options.injectToHtml ?? true\n const timeZone = normalizeTimeZone(options.timeZone ?? DEFAULT_TIME_ZONE)\n const data = normalizeData(options.data)\n let payload: BuildVersionPayload | undefined\n let emittedFilename = filename\n let skipEmit = false\n\n const createPayload = (context: BuildVersionContext, root: string) => {\n payload = makePayload(options, context, data, readPackageData(root), timeZone)\n }\n\n const transformHtml = (html: string): string | undefined => {\n if (!injectToHtml || !payload) return undefined\n return injectIntoHead(html, assignment(globalName, payload))\n }\n\n const headScript = (): string => {\n if (!injectToHtml || !payload) return ''\n return scriptTag(globalName, payload)\n }\n\n const plugin: Plugin = {\n name: PLUGIN_NAME,\n\n config(userConfig, environment) {\n if (!payload) {\n createPayload(\n {\n command: environment.command as BuildVersionContext['command'],\n mode: environment.mode,\n },\n projectRoot(userConfig.root),\n )\n }\n\n if (!defineName || !payload) return undefined\n\n return {\n define: {\n [defineName]: JSON.stringify(payload),\n },\n } satisfies UserConfig\n },\n\n configResolved(config) {\n skipEmit = Boolean(config.build.ssr) && !config.build.ssrEmitAssets\n emittedFilename = nuxtFilename(config, filename)\n\n if (!payload) {\n createPayload(\n {\n command: config.command as BuildVersionContext['command'],\n mode: config.mode,\n },\n config.root,\n )\n }\n },\n\n configureServer(server) {\n const targetPath = requestPath(server.config.base, filename)\n\n server.middlewares.use((request, response, next) => {\n if (request.method !== 'GET' && request.method !== 'HEAD') {\n next()\n return\n }\n\n if (pathname(request.url) !== targetPath || !payload) {\n next()\n return\n }\n\n response.statusCode = 200\n response.setHeader('Content-Type', 'application/json; charset=utf-8')\n response.setHeader('Cache-Control', 'no-cache')\n response.end(request.method === 'HEAD' ? undefined : serialize(payload))\n })\n },\n\n transformIndexHtml() {\n if (!injectToHtml || !payload) return undefined\n\n const tags: HtmlTagDescriptor[] = [\n {\n tag: 'script',\n injectTo: 'head',\n children: assignment(globalName, payload),\n },\n ]\n return tags\n },\n\n generateBundle() {\n if (!payload || skipEmit) return\n\n this.emitFile({\n type: 'asset',\n fileName: emittedFilename,\n source: serialize(payload),\n })\n\n if (options.log) {\n console.log(options.log(payload))\n } else {\n console.log(`[${PLUGIN_NAME}][build finished]`)\n console.log(serialize(payload))\n }\n },\n }\n\n Object.defineProperty(plugin, 'transformHtml', { value: transformHtml })\n Object.defineProperty(plugin, 'headScript', { value: headScript })\n\n return plugin as BuildVersionPlugin\n}\n\nexport default buildVersionPlugin\n"],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import { Plugin as Plugin_2 } from 'vite';
2
+
3
+ export declare interface BuildVersionContext {
4
+ command: 'serve' | 'build';
5
+ mode: string;
6
+ }
7
+
8
+ export declare interface BuildVersionPayload {
9
+ time: string | number;
10
+ env: string;
11
+ [key: string]: any;
12
+ }
13
+
14
+ export declare type BuildVersionPlugin = Plugin_2 & {
15
+ transformHtml: (code: string, ...args: unknown[]) => string | undefined;
16
+ headScript: () => string;
17
+ };
18
+
19
+ declare function buildVersionPlugin(options?: BuildVersionPluginOptions): BuildVersionPlugin;
20
+ export { buildVersionPlugin }
21
+ export default buildVersionPlugin;
22
+
23
+ export declare interface BuildVersionPluginOptions {
24
+ filename?: string;
25
+ globalName?: string;
26
+ defineName?: string | false;
27
+ injectToHtml?: boolean;
28
+ timeZone?: string;
29
+ version?: string | number | ((context: BuildVersionContext) => string | number);
30
+ data?: Record<string, any>;
31
+ log?: (content: any) => string;
32
+ payload?: (json: any) => any;
33
+ }
34
+
35
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,165 @@
1
+ import { existsSync as e, readFileSync as t } from "node:fs";
2
+ import { dirname as n, join as r, resolve as i } from "node:path";
3
+ //#region src/index.ts
4
+ var a = "@gitlon/version", o = "version.json", s = "__VERSION__", c = "__VERSION__", l = "Asia/Shanghai";
5
+ function u(e) {
6
+ let t = e.replace(/\\/g, "/").replace(/^\/+/, "");
7
+ if (!t || t.endsWith("/")) throw Error(`[${a}] "filename" must point to a file path.`);
8
+ return t;
9
+ }
10
+ function d(e, t) {
11
+ if (!e.trim()) throw Error(`[${a}] "${t}" cannot be empty.`);
12
+ return e;
13
+ }
14
+ function f(e) {
15
+ let t = e.trim();
16
+ if (!t) throw Error(`[${a}] "timeZone" cannot be empty.`);
17
+ try {
18
+ new Intl.DateTimeFormat("en-US", { timeZone: t }).format(/* @__PURE__ */ new Date());
19
+ } catch {
20
+ throw Error(`[${a}] "timeZone" is invalid.`);
21
+ }
22
+ return t;
23
+ }
24
+ function p(e) {
25
+ if (e === void 0) return {};
26
+ if (!e || Array.isArray(e) || typeof e != "object") throw Error(`[${a}] "data" must be a plain object.`);
27
+ return e;
28
+ }
29
+ function m(e, t) {
30
+ if (e === "serve") return 0;
31
+ let n = new Intl.DateTimeFormat("en-US", {
32
+ timeZone: t,
33
+ year: "numeric",
34
+ month: "2-digit",
35
+ day: "2-digit",
36
+ hour: "2-digit",
37
+ minute: "2-digit",
38
+ second: "2-digit",
39
+ hour12: !1,
40
+ hourCycle: "h23"
41
+ }).formatToParts(/* @__PURE__ */ new Date()), r = (e) => n.find((t) => t.type === e)?.value ?? "";
42
+ return [
43
+ r("year"),
44
+ r("month"),
45
+ r("day"),
46
+ r("hour"),
47
+ r("minute"),
48
+ r("second")
49
+ ].join("");
50
+ }
51
+ function h(t) {
52
+ let i = t;
53
+ for (;;) {
54
+ let t = r(i, "package.json");
55
+ if (e(t)) return t;
56
+ let a = n(i);
57
+ if (a === i) return;
58
+ i = a;
59
+ }
60
+ }
61
+ function g(e) {
62
+ let n = h(e);
63
+ if (!n) return {};
64
+ try {
65
+ let e = JSON.parse(t(n, "utf8")), r = {};
66
+ return e.name !== void 0 && (r.pkgName = e.name), e.version !== void 0 && (r.pkgVersion = e.version), r;
67
+ } catch {
68
+ return {};
69
+ }
70
+ }
71
+ function _(e) {
72
+ return e ? i(process.cwd(), e) : process.cwd();
73
+ }
74
+ function v(e, t, n) {
75
+ return typeof e.version == "function" ? e.version(t) : e.version === void 0 ? m(t.command, n) : e.version;
76
+ }
77
+ function y(e, t, n, r, i) {
78
+ let a = {
79
+ ...n,
80
+ ...r,
81
+ time: v(e, t, i),
82
+ env: t.mode
83
+ };
84
+ return e.payload && (a = e.payload(a)), a;
85
+ }
86
+ function b(e) {
87
+ return `${JSON.stringify(e, null, 2)}\n`;
88
+ }
89
+ function x(e, t) {
90
+ return `window[${JSON.stringify(e)}] = ${JSON.stringify(t)};`;
91
+ }
92
+ function S(e, t) {
93
+ return `<script>${x(e, t)}<\/script>`;
94
+ }
95
+ function C(e, t) {
96
+ if (e.includes(t)) return e;
97
+ let n = `<script>${t}<\/script>`;
98
+ return e.includes("</head>") ? e.replace("</head>", `${n}</head>`) : `${n}${e}`;
99
+ }
100
+ function w(e) {
101
+ return e ? new URL(e, "http://localhost").pathname : "/";
102
+ }
103
+ function T(e, t) {
104
+ let n = e === "./" ? "/" : e || "/", r = n.startsWith("/") ? n : `/${n}`;
105
+ return `${r.endsWith("/") ? r.slice(0, -1) : r}/${t}`.replace(/\/{2,}/g, "/");
106
+ }
107
+ function E(e, t) {
108
+ if (e.appType !== "custom" || e.build.copyPublicDir !== !1) return t;
109
+ let n = u(e.build.assetsDir);
110
+ return n === "." || t.startsWith(`${n}/`) ? t : `${n}/${t}`;
111
+ }
112
+ function D(e = {}) {
113
+ let t = u(e.filename ?? o), n = d(e.globalName ?? s, "globalName"), r = e.defineName !== !1 && d(e.defineName ?? c, "defineName"), i = e.injectToHtml ?? !0, m = f(e.timeZone ?? l), h = p(e.data), v, D = t, O = !1, k = (t, n) => {
114
+ v = y(e, t, h, g(n), m);
115
+ }, A = (e) => {
116
+ if (i && v) return C(e, x(n, v));
117
+ }, j = () => !i || !v ? "" : S(n, v), M = {
118
+ name: a,
119
+ config(e, t) {
120
+ if (v || k({
121
+ command: t.command,
122
+ mode: t.mode
123
+ }, _(e.root)), r && v) return { define: { [r]: JSON.stringify(v) } };
124
+ },
125
+ configResolved(e) {
126
+ O = !!e.build.ssr && !e.build.ssrEmitAssets, D = E(e, t), v || k({
127
+ command: e.command,
128
+ mode: e.mode
129
+ }, e.root);
130
+ },
131
+ configureServer(e) {
132
+ let n = T(e.config.base, t);
133
+ e.middlewares.use((e, t, r) => {
134
+ if (e.method !== "GET" && e.method !== "HEAD") {
135
+ r();
136
+ return;
137
+ }
138
+ if (w(e.url) !== n || !v) {
139
+ r();
140
+ return;
141
+ }
142
+ t.statusCode = 200, t.setHeader("Content-Type", "application/json; charset=utf-8"), t.setHeader("Cache-Control", "no-cache"), t.end(e.method === "HEAD" ? void 0 : b(v));
143
+ });
144
+ },
145
+ transformIndexHtml() {
146
+ if (i && v) return [{
147
+ tag: "script",
148
+ injectTo: "head",
149
+ children: x(n, v)
150
+ }];
151
+ },
152
+ generateBundle() {
153
+ v && !O && (this.emitFile({
154
+ type: "asset",
155
+ fileName: D,
156
+ source: b(v)
157
+ }), e.log ? console.log(e.log(v)) : (console.log(`[${a}][build finished]`), console.log(b(v))));
158
+ }
159
+ };
160
+ return Object.defineProperty(M, "transformHtml", { value: A }), Object.defineProperty(M, "headScript", { value: j }), M;
161
+ }
162
+ //#endregion
163
+ export { D as buildVersionPlugin, D as default };
164
+
165
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\nimport type { HtmlTagDescriptor, Plugin, UserConfig } from 'vite'\n\nexport interface BuildVersionContext {\n command: 'serve' | 'build'\n mode: string\n}\n\nexport interface BuildVersionPayload {\n time: string | number\n env: string\n [key: string]: any\n}\n\nexport interface BuildVersionPluginOptions {\n filename?: string\n globalName?: string\n defineName?: string | false\n injectToHtml?: boolean\n timeZone?: string\n version?: string | number | ((context: BuildVersionContext) => string | number)\n data?: Record<string, any>\n log?: (content: any) => string\n payload?: (json: any) => any\n}\n\nexport type BuildVersionPlugin = Plugin & {\n transformHtml: (code: string, ...args: unknown[]) => string | undefined\n headScript: () => string\n}\n\nconst PLUGIN_NAME = '@gitlon/version'\nconst DEFAULT_FILENAME = 'version.json'\nconst DEFAULT_GLOBAL_NAME = '__VERSION__'\nconst DEFAULT_DEFINE_NAME = '__VERSION__'\nconst DEFAULT_TIME_ZONE = 'Asia/Shanghai'\n\nfunction normalizeFilename(filename: string): string {\n const value = filename.replace(/\\\\/g, '/').replace(/^\\/+/, '')\n\n if (!value || value.endsWith('/')) {\n throw new Error(`[${PLUGIN_NAME}] \"filename\" must point to a file path.`)\n }\n\n return value\n}\n\nfunction normalizeRequiredName(value: string, option: 'globalName' | 'defineName'): string {\n if (!value.trim()) {\n throw new Error(`[${PLUGIN_NAME}] \"${option}\" cannot be empty.`)\n }\n\n return value\n}\n\nfunction normalizeTimeZone(timeZone: string): string {\n const value = timeZone.trim()\n\n if (!value) {\n throw new Error(`[${PLUGIN_NAME}] \"timeZone\" cannot be empty.`)\n }\n\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: value }).format(new Date())\n } catch {\n throw new Error(`[${PLUGIN_NAME}] \"timeZone\" is invalid.`)\n }\n\n return value\n}\n\nfunction normalizeData(data: BuildVersionPluginOptions['data']): Record<string, any> {\n if (data === undefined) return {}\n\n if (!data || Array.isArray(data) || typeof data !== 'object') {\n throw new Error(`[${PLUGIN_NAME}] \"data\" must be a plain object.`)\n }\n\n return data\n}\n\nfunction createDefaultVersion(command: BuildVersionContext['command'], timeZone: string) {\n if (command === 'serve') return 0\n\n const parts = new Intl.DateTimeFormat('en-US', {\n timeZone,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false,\n hourCycle: 'h23',\n }).formatToParts(new Date())\n const part = (type: Intl.DateTimeFormatPartTypes) =>\n parts.find((item) => item.type === type)?.value ?? ''\n\n return [\n part('year'),\n part('month'),\n part('day'),\n part('hour'),\n part('minute'),\n part('second'),\n ].join('')\n}\n\nfunction findPackageJson(root: string): string | undefined {\n let directory = root\n\n while (true) {\n const packageJson = join(directory, 'package.json')\n if (existsSync(packageJson)) return packageJson\n\n const parent = dirname(directory)\n if (parent === directory) return undefined\n directory = parent\n }\n}\n\nfunction readPackageData(root: string): Record<string, any> {\n const filename = findPackageJson(root)\n if (!filename) return {}\n\n try {\n const packageJson = JSON.parse(readFileSync(filename, 'utf8'))\n const result: Record<string, any> = {}\n\n if (packageJson.name !== undefined) result.pkgName = packageJson.name\n if (packageJson.version !== undefined) result.pkgVersion = packageJson.version\n\n return result\n } catch {\n return {}\n }\n}\n\nfunction projectRoot(root: string | undefined): string {\n return root ? resolve(process.cwd(), root) : process.cwd()\n}\n\nfunction versionFor(\n options: BuildVersionPluginOptions,\n context: BuildVersionContext,\n timeZone: string,\n): string | number {\n if (typeof options.version === 'function') return options.version(context)\n if (options.version !== undefined) return options.version\n return createDefaultVersion(context.command, timeZone)\n}\n\nfunction makePayload(\n options: BuildVersionPluginOptions,\n context: BuildVersionContext,\n data: Record<string, any>,\n packageData: Record<string, any>,\n timeZone: string,\n): BuildVersionPayload {\n let payload: BuildVersionPayload = {\n ...data,\n ...packageData,\n time: versionFor(options, context, timeZone),\n env: context.mode,\n }\n\n if (options.payload) payload = options.payload(payload)\n return payload\n}\n\nfunction serialize(payload: BuildVersionPayload): string {\n return `${JSON.stringify(payload, null, 2)}\\n`\n}\n\nfunction assignment(globalName: string, payload: BuildVersionPayload): string {\n return `window[${JSON.stringify(globalName)}] = ${JSON.stringify(payload)};`\n}\n\nfunction scriptTag(globalName: string, payload: BuildVersionPayload): string {\n return `<script>${assignment(globalName, payload)}</script>`\n}\n\nfunction injectIntoHead(html: string, script: string): string {\n if (html.includes(script)) return html\n\n const tag = `<script>${script}</script>`\n return html.includes('</head>') ? html.replace('</head>', `${tag}</head>`) : `${tag}${html}`\n}\n\nfunction pathname(url: string | undefined): string {\n return url ? new URL(url, 'http://localhost').pathname : '/'\n}\n\nfunction requestPath(base: string, filename: string): string {\n const rawBase = base === './' ? '/' : base || '/'\n const leadingBase = rawBase.startsWith('/') ? rawBase : `/${rawBase}`\n const cleanBase = leadingBase.endsWith('/') ? leadingBase.slice(0, -1) : leadingBase\n return `${cleanBase}/${filename}`.replace(/\\/{2,}/g, '/')\n}\n\nfunction nuxtFilename(\n config: {\n appType: string\n build: { assetsDir: string; copyPublicDir: boolean }\n },\n filename: string,\n): string {\n if (config.appType !== 'custom' || config.build.copyPublicDir !== false) return filename\n\n const assetsDir = normalizeFilename(config.build.assetsDir)\n return assetsDir === '.' || filename.startsWith(`${assetsDir}/`)\n ? filename\n : `${assetsDir}/${filename}`\n}\n\nexport function buildVersionPlugin(\n options: BuildVersionPluginOptions = {},\n): BuildVersionPlugin {\n const filename = normalizeFilename(options.filename ?? DEFAULT_FILENAME)\n const globalName = normalizeRequiredName(\n options.globalName ?? DEFAULT_GLOBAL_NAME,\n 'globalName',\n )\n const defineName =\n options.defineName === false\n ? false\n : normalizeRequiredName(options.defineName ?? DEFAULT_DEFINE_NAME, 'defineName')\n const injectToHtml = options.injectToHtml ?? true\n const timeZone = normalizeTimeZone(options.timeZone ?? DEFAULT_TIME_ZONE)\n const data = normalizeData(options.data)\n let payload: BuildVersionPayload | undefined\n let emittedFilename = filename\n let skipEmit = false\n\n const createPayload = (context: BuildVersionContext, root: string) => {\n payload = makePayload(options, context, data, readPackageData(root), timeZone)\n }\n\n const transformHtml = (html: string): string | undefined => {\n if (!injectToHtml || !payload) return undefined\n return injectIntoHead(html, assignment(globalName, payload))\n }\n\n const headScript = (): string => {\n if (!injectToHtml || !payload) return ''\n return scriptTag(globalName, payload)\n }\n\n const plugin: Plugin = {\n name: PLUGIN_NAME,\n\n config(userConfig, environment) {\n if (!payload) {\n createPayload(\n {\n command: environment.command as BuildVersionContext['command'],\n mode: environment.mode,\n },\n projectRoot(userConfig.root),\n )\n }\n\n if (!defineName || !payload) return undefined\n\n return {\n define: {\n [defineName]: JSON.stringify(payload),\n },\n } satisfies UserConfig\n },\n\n configResolved(config) {\n skipEmit = Boolean(config.build.ssr) && !config.build.ssrEmitAssets\n emittedFilename = nuxtFilename(config, filename)\n\n if (!payload) {\n createPayload(\n {\n command: config.command as BuildVersionContext['command'],\n mode: config.mode,\n },\n config.root,\n )\n }\n },\n\n configureServer(server) {\n const targetPath = requestPath(server.config.base, filename)\n\n server.middlewares.use((request, response, next) => {\n if (request.method !== 'GET' && request.method !== 'HEAD') {\n next()\n return\n }\n\n if (pathname(request.url) !== targetPath || !payload) {\n next()\n return\n }\n\n response.statusCode = 200\n response.setHeader('Content-Type', 'application/json; charset=utf-8')\n response.setHeader('Cache-Control', 'no-cache')\n response.end(request.method === 'HEAD' ? undefined : serialize(payload))\n })\n },\n\n transformIndexHtml() {\n if (!injectToHtml || !payload) return undefined\n\n const tags: HtmlTagDescriptor[] = [\n {\n tag: 'script',\n injectTo: 'head',\n children: assignment(globalName, payload),\n },\n ]\n return tags\n },\n\n generateBundle() {\n if (!payload || skipEmit) return\n\n this.emitFile({\n type: 'asset',\n fileName: emittedFilename,\n source: serialize(payload),\n })\n\n if (options.log) {\n console.log(options.log(payload))\n } else {\n console.log(`[${PLUGIN_NAME}][build finished]`)\n console.log(serialize(payload))\n }\n },\n }\n\n Object.defineProperty(plugin, 'transformHtml', { value: transformHtml })\n Object.defineProperty(plugin, 'headScript', { value: headScript })\n\n return plugin as BuildVersionPlugin\n}\n\nexport default buildVersionPlugin\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@gitlon/version",
3
+ "version": "0.1.0",
4
+ "description": "Vite 构建版本插件:生成 version.json、注入版本信息并提供客户端缓存刷新能力",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./client": {
16
+ "types": "./dist/client.d.ts",
17
+ "import": "./dist/client.js",
18
+ "require": "./dist/client.cjs"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "keywords": [
26
+ "vite",
27
+ "vite-plugin",
28
+ "build-version",
29
+ "version-json",
30
+ "cache-refresh"
31
+ ],
32
+ "license": "MIT",
33
+ "author": "Long",
34
+ "peerDependencies": {
35
+ "vite": ">=5"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.0.0"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "scripts": {
44
+ "build": "vite build",
45
+ "dev": "vite build --watch",
46
+ "typecheck": "tsc --noEmit",
47
+ "clean": "rm -rf dist",
48
+ "test": "vitest run --passWithNoTests"
49
+ }
50
+ }