unplugin-version-injector 2.2.0 → 2.3.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/README.md +119 -15
- package/README.zh-CN.md +120 -15
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +68 -29
- package/dist/index.mjs +68 -29
- package/dist/rolldown.d.mts +1 -1
- package/dist/rolldown.d.ts +1 -1
- package/dist/rolldown.js +68 -29
- package/dist/rolldown.mjs +68 -29
- package/dist/rollup.d.mts +1 -1
- package/dist/rollup.d.ts +1 -1
- package/dist/rollup.js +68 -29
- package/dist/rollup.mjs +68 -29
- package/dist/rspack.d.mts +1 -1
- package/dist/rspack.d.ts +1 -1
- package/dist/rspack.js +68 -29
- package/dist/rspack.mjs +68 -29
- package/dist/{types-Cc-nzIS0.d.mts → types-CLRw9rBI.d.mts} +5 -0
- package/dist/{types-Cc-nzIS0.d.ts → types-CLRw9rBI.d.ts} +5 -0
- package/dist/vite.d.mts +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +68 -29
- package/dist/vite.mjs +68 -29
- package/dist/webpack.d.mts +1 -1
- package/dist/webpack.d.ts +1 -1
- package/dist/webpack.js +68 -29
- package/dist/webpack.mjs +68 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
✅ Supports Vite, Webpack 4/5, Rspack, Rollup, Rolldown
|
|
18
18
|
✅ Fully compatible with Multi-Page Applications (MPA)
|
|
19
19
|
✅ Customizable version, project name, date format, and theme-based console styling
|
|
20
|
+
✅ Date format supports dayjs-style patterns (`YYYY-MM-DD HH:mm:ss`) with zero extra dependencies
|
|
21
|
+
✅ Optional: attach a version header (`X-Client-Version`) to `fetch` / `XMLHttpRequest` requests to identify clients in backend logs
|
|
20
22
|
|
|
21
23
|
---
|
|
22
24
|
|
|
@@ -125,47 +127,149 @@ In your final HTML output:
|
|
|
125
127
|
| `version` | `string` | Custom version number | Read from package.json |
|
|
126
128
|
| `name` | `string` | Custom project name | Read from package.json |
|
|
127
129
|
| `log` | `boolean` | Whether to inject the console log script | `true` |
|
|
128
|
-
| `formatDate` | `(date: Date) => string`
|
|
130
|
+
| `formatDate` | `string \| ((date: Date) => string)` | Custom build time format: a dayjs-style pattern (e.g. `'YYYY-MM-DD HH:mm:ss'`) or a function | ISO 8601 format |
|
|
129
131
|
| `requestHeaders` | `boolean \| RequestHeadersOptions` | Attach version/build-time headers to outgoing requests | `false` |
|
|
132
|
+
| `nonce` | `string` | CSP nonce added to the injected inline `<script>` tags | — |
|
|
130
133
|
|
|
131
134
|
> `version` and `name` can be provided independently — whichever is missing falls back to the nearest `package.json`.
|
|
132
135
|
|
|
133
136
|
---
|
|
134
137
|
|
|
138
|
+
## 📅 Formatting build time (`formatDate`)
|
|
139
|
+
|
|
140
|
+
`formatDate` accepts two forms, and applies to both the **console banner** and the **`X-Client-Build-Time` header**:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
// 1) dayjs-style pattern (built-in lightweight impl, no dayjs needed)
|
|
144
|
+
versionInjector({ formatDate: 'YYYY-MM-DD HH:mm:ss' }); // 2024-04-01 12:30:45
|
|
145
|
+
|
|
146
|
+
// 2) custom function
|
|
147
|
+
versionInjector({ formatDate: (date) => date.getTime().toString() }); // timestamp
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Supported tokens: `YYYY YY MMMM MMM MM M DD D dddd ddd dd d HH H hh h mm m ss s SSS SS S A a`.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
135
154
|
## 📡 Request Headers (identify clients in backend logs)
|
|
136
155
|
|
|
137
|
-
Enable `requestHeaders` to patch `window.fetch` and `XMLHttpRequest` so
|
|
156
|
+
Enable `requestHeaders` to patch `window.fetch` and `XMLHttpRequest` so requests carry the client version and build time — making it trivial to tell which client build produced a request in backend/API logs. Two headers are injected by default:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
X-Client-Version: my-app/1.2.3
|
|
160
|
+
X-Client-Build-Time: 2024-04-01 12:00:00
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
> Because it patches `fetch` / `XMLHttpRequest`, **axios and most HTTP clients work automatically**. `navigator.sendBeacon` and WebSocket connections cannot carry custom headers and are not covered.
|
|
164
|
+
|
|
165
|
+
### `RequestHeadersOptions`
|
|
166
|
+
|
|
167
|
+
| Option | Type | Description | Default |
|
|
168
|
+
|---|---|---|---|
|
|
169
|
+
| `versionHeaderName` | `string` | Version header name; value is `${name}/${version}` | `'X-Client-Version'` |
|
|
170
|
+
| `buildTimeHeaderName` | `string` | Build-time header name; value is the `formatDate` output | `'X-Client-Build-Time'` |
|
|
171
|
+
| `include` | `(string \| RegExp)[]` | Extra **cross-origin** allowlist: string = URL prefix, RegExp = full-URL test. **Same-origin requests are always injected** | `[]` |
|
|
172
|
+
|
|
173
|
+
### Scenario 1: same-origin (simplest)
|
|
174
|
+
|
|
175
|
+
Page and API share an origin, or you use a dev-server proxy (requests hit `/api`, which the browser treats as same-origin):
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
versionInjector({ requestHeaders: true }); // true = defaults, same-origin only
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Scenario 2: custom header names
|
|
138
182
|
|
|
139
183
|
```ts
|
|
140
184
|
versionInjector({
|
|
141
|
-
requestHeaders:
|
|
185
|
+
requestHeaders: {
|
|
186
|
+
versionHeaderName: 'X-App-Version',
|
|
187
|
+
buildTimeHeaderName: 'X-App-Build',
|
|
188
|
+
},
|
|
142
189
|
});
|
|
143
190
|
```
|
|
144
191
|
|
|
145
|
-
|
|
192
|
+
### Scenario 3: a single cross-origin API (most common in prod)
|
|
146
193
|
|
|
194
|
+
Front end and API are on different origins — add the API origin to `include` (string = URL prefix):
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
versionInjector({
|
|
198
|
+
requestHeaders: { include: ['https://api.example.com'] },
|
|
199
|
+
});
|
|
147
200
|
```
|
|
148
|
-
|
|
149
|
-
|
|
201
|
+
|
|
202
|
+
### Scenario 4: multiple cross-origin APIs / RegExp
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
versionInjector({
|
|
206
|
+
requestHeaders: {
|
|
207
|
+
include: [
|
|
208
|
+
'https://api.example.com',
|
|
209
|
+
'https://auth.example.com',
|
|
210
|
+
/^https:\/\/[^/]*\.example\.com\//, // any *.example.com subdomain
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
});
|
|
150
214
|
```
|
|
151
215
|
|
|
152
|
-
|
|
216
|
+
### Scenario 5: Monorepo + all cross-origin (the trickiest) ⭐
|
|
217
|
+
|
|
218
|
+
Many packages share one build config, and each app talks to cross-origin APIs that differ per environment (dev / sandbox / prod). Two keys:
|
|
219
|
+
|
|
220
|
+
**1. Write `include` once in the shared root config** — every package inherits it (put the plugin in the shared `configureWebpack` / `vite` config).
|
|
221
|
+
|
|
222
|
+
**2. Don't hardcode origins — build them from env vars.** Each app's `.env.*` usually already defines its API origins:
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
// shared root build config (webpack example)
|
|
226
|
+
const versionInjector = require('unplugin-version-injector/webpack');
|
|
227
|
+
|
|
228
|
+
// provided by each app / each environment's .env
|
|
229
|
+
const apiOrigins = [
|
|
230
|
+
process.env.VUE_APP_API_ORIGIN,
|
|
231
|
+
process.env.VUE_APP_SDK_API_ORIGIN,
|
|
232
|
+
process.env.VUE_APP_USER_API_ORIGIN,
|
|
233
|
+
].filter(Boolean);
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
configureWebpack: {
|
|
237
|
+
plugins: [
|
|
238
|
+
versionInjector({ requestHeaders: { include: apiOrigins } }),
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
dev / sandbox / prod each get the right origins automatically — no giant hardcoded list.
|
|
245
|
+
|
|
246
|
+
If all APIs live under a few fixed base domains, a single RegExp also works (new subdomains match automatically):
|
|
153
247
|
|
|
154
248
|
```ts
|
|
155
249
|
versionInjector({
|
|
156
250
|
requestHeaders: {
|
|
157
|
-
|
|
158
|
-
buildTimeHeaderName: 'X-Client-Build-Time', // default
|
|
159
|
-
// Cross-origin URLs to include (string = URL prefix, RegExp = full URL test).
|
|
160
|
-
// Same-origin requests are always included.
|
|
161
|
-
include: ['https://api.example.com/', /\.internal\.example\.com/],
|
|
251
|
+
include: [/^https:\/\/[^/]*\.(example\.io|example\.dev|sandbox-example\.com)\//],
|
|
162
252
|
},
|
|
163
253
|
});
|
|
164
254
|
```
|
|
165
255
|
|
|
166
|
-
>
|
|
167
|
-
|
|
168
|
-
|
|
256
|
+
> Only include API origins you actually `fetch`/`XHR` **and** whose CORS you control. Don't add CDNs or third-party SDK script hosts — that only triggers preflights and can break asset loading.
|
|
257
|
+
|
|
258
|
+
### ⚠️ Cross-origin requires backend cooperation (CORS preflight)
|
|
259
|
+
|
|
260
|
+
Custom headers on cross-origin requests trigger an `OPTIONS` preflight. **Every** API service matched by `include` must allow the two headers, or the browser blocks the request:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
Access-Control-Allow-Headers: X-Client-Version, X-Client-Build-Time
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
This is exactly why cross-origin injection is opt-in via `include` rather than on by default.
|
|
267
|
+
|
|
268
|
+
### 🔍 Same-origin or cross-origin?
|
|
269
|
+
|
|
270
|
+
Open the browser Network tab and look at the request's **real URL**:
|
|
271
|
+
- `http://localhost:9040/api/...` (via dev proxy) → **same-origin**, `requestHeaders: true` is enough, no `include`.
|
|
272
|
+
- `https://api.xxx.com/...` (direct) → **cross-origin**, must be in `include` + backend must allow the headers.
|
|
169
273
|
|
|
170
274
|
---
|
|
171
275
|
|
package/README.zh-CN.md
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
✅ 完美兼容多页面应用(MPA)
|
|
17
17
|
✅ 支持自定义版本号、项目名、时间格式,默认读取 `package.json`
|
|
18
18
|
✅ 控制台输出支持自动适配深/浅主题配色
|
|
19
|
+
✅ 时间格式支持 dayjs 风格字符串(`YYYY-MM-DD HH:mm:ss`),零额外依赖
|
|
20
|
+
✅ 可选:给 `fetch` / `XMLHttpRequest` 请求自动注入版本请求头(`X-Client-Version`),在后端日志中定位客户端版本
|
|
19
21
|
|
|
20
22
|
---
|
|
21
23
|
|
|
@@ -131,46 +133,149 @@ export default {
|
|
|
131
133
|
| `version` | `string` | 自定义版本号 | 自动读取 package.json |
|
|
132
134
|
| `name` | `string` | 自定义项目名 | 自动读取 package.json |
|
|
133
135
|
| `log` | `boolean` | 是否输出控制台日志 | `true` |
|
|
134
|
-
| `formatDate` | `(date: Date) => string` |
|
|
136
|
+
| `formatDate` | `string \| ((date: Date) => string)` | 自定义构建时间格式:支持 dayjs 风格字符串(如 `'YYYY-MM-DD HH:mm:ss'`)或函数 | ISO 格式 |
|
|
135
137
|
| `requestHeaders` | `boolean \| RequestHeadersOptions` | 给发出的请求自动附加版本/构建时间请求头 | `false` |
|
|
138
|
+
| `nonce` | `string` | 给注入的内联 `<script>` 加 CSP nonce(严格 CSP 站点需设置) | — |
|
|
136
139
|
|
|
137
140
|
> `version` 和 `name` 可以单独传入,缺失的一项会自动从最近的 `package.json` 读取。
|
|
138
141
|
|
|
139
142
|
---
|
|
140
143
|
|
|
141
|
-
##
|
|
144
|
+
## **📅 构建时间格式化 `formatDate`**
|
|
142
145
|
|
|
143
|
-
|
|
146
|
+
`formatDate` 支持两种写法,同时作用于**控制台构建时间**和 **`X-Client-Build-Time` 请求头**:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
// 1) dayjs 风格字符串(内置轻量实现,无需安装 dayjs)
|
|
150
|
+
versionInjector({ formatDate: 'YYYY-MM-DD HH:mm:ss' }); // 2024-04-01 12:30:45
|
|
151
|
+
|
|
152
|
+
// 2) 自定义函数
|
|
153
|
+
versionInjector({ formatDate: (date) => date.getTime().toString() }); // 时间戳
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
支持的 token:`YYYY YY MMMM MMM MM M DD D dddd ddd dd d HH H hh h mm m ss s SSS SS S A a`。
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## **📡 请求头注入(在后端日志中定位客户端版本)**
|
|
161
|
+
|
|
162
|
+
开启 `requestHeaders` 后,插件会在页面最前面 patch `window.fetch` 和 `XMLHttpRequest`,让请求自动带上版本与构建时间——排查前后端日志时,一眼就能看出是哪个客户端、哪个版本发出的请求。默认注入两个头:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
X-Client-Version: my-app/1.2.3
|
|
166
|
+
X-Client-Build-Time: 2024-04-01 12:00:00
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
> 底层是 patch `fetch` / `XMLHttpRequest`,所以 **axios、umi-request 等主流库都自动生效**;`navigator.sendBeacon` 和 WebSocket 无法携带自定义头,不在覆盖范围。
|
|
170
|
+
|
|
171
|
+
### 子配置 `RequestHeadersOptions`
|
|
172
|
+
|
|
173
|
+
| 选项 | 类型 | 说明 | 默认值 |
|
|
174
|
+
|---|---|---|---|
|
|
175
|
+
| `versionHeaderName` | `string` | 版本头名称,值为 `${name}/${version}` | `'X-Client-Version'` |
|
|
176
|
+
| `buildTimeHeaderName` | `string` | 构建时间头名称,值为 `formatDate` 的输出 | `'X-Client-Build-Time'` |
|
|
177
|
+
| `include` | `(string \| RegExp)[]` | 额外注入的**跨域**地址白名单:字符串按 URL 前缀匹配,正则按完整 URL 测试。**同源请求始终注入** | `[]` |
|
|
178
|
+
|
|
179
|
+
### 场景 1:同源请求(最简单)
|
|
180
|
+
|
|
181
|
+
页面与 API 同域,或本地走 dev-server 代理(请求发到 `/api`,浏览器视角是同源):
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
versionInjector({ requestHeaders: true }); // true = 默认配置,仅同源
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 场景 2:自定义请求头名称
|
|
144
188
|
|
|
145
189
|
```ts
|
|
146
190
|
versionInjector({
|
|
147
|
-
requestHeaders:
|
|
191
|
+
requestHeaders: {
|
|
192
|
+
versionHeaderName: 'X-App-Version',
|
|
193
|
+
buildTimeHeaderName: 'X-App-Build',
|
|
194
|
+
},
|
|
148
195
|
});
|
|
149
196
|
```
|
|
150
197
|
|
|
151
|
-
|
|
198
|
+
### 场景 3:单个跨域 API(生产最常见)
|
|
152
199
|
|
|
200
|
+
前端和 API 不同源,把 API 域名加进 `include`(字符串 = URL 前缀):
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
versionInjector({
|
|
204
|
+
requestHeaders: { include: ['https://api.example.com'] },
|
|
205
|
+
});
|
|
153
206
|
```
|
|
154
|
-
|
|
155
|
-
|
|
207
|
+
|
|
208
|
+
### 场景 4:多个跨域 API / 正则批量匹配
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
versionInjector({
|
|
212
|
+
requestHeaders: {
|
|
213
|
+
include: [
|
|
214
|
+
'https://api.example.com',
|
|
215
|
+
'https://auth.example.com',
|
|
216
|
+
/^https:\/\/[^/]*\.example\.com\//, // 匹配 *.example.com 所有子域
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
});
|
|
156
220
|
```
|
|
157
221
|
|
|
158
|
-
|
|
222
|
+
### 场景 5:Monorepo + 全跨域(最容易踩坑)⭐
|
|
223
|
+
|
|
224
|
+
多包共用一份构建配置、每个子应用又连不同环境(dev / sandbox / prod)的跨域 API——这是最难配的场景。两个关键点:
|
|
225
|
+
|
|
226
|
+
**① `include` 只需在共享的根配置里写一次**,所有子包继承即可(把插件放进共享的 `configureWebpack` / `vite` 配置)。
|
|
227
|
+
|
|
228
|
+
**② 域名随环境变化,别硬编码——用环境变量动态拼**。每个子应用的 `.env.*` 里通常已有 API 域名变量,直接读:
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
// 共享的根构建配置(以 webpack 为例)
|
|
232
|
+
const versionInjector = require('unplugin-version-injector/webpack');
|
|
233
|
+
|
|
234
|
+
// 这些变量由各子应用 / 各环境的 .env 提供
|
|
235
|
+
const apiOrigins = [
|
|
236
|
+
process.env.VUE_APP_API_ORIGIN,
|
|
237
|
+
process.env.VUE_APP_SDK_API_ORIGIN,
|
|
238
|
+
process.env.VUE_APP_USER_API_ORIGIN,
|
|
239
|
+
].filter(Boolean); // 去掉未定义的
|
|
240
|
+
|
|
241
|
+
module.exports = {
|
|
242
|
+
configureWebpack: {
|
|
243
|
+
plugins: [
|
|
244
|
+
versionInjector({ requestHeaders: { include: apiOrigins } }),
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
这样 dev / sandbox / prod 各自带对应域名,不用维护一份大清单。
|
|
251
|
+
|
|
252
|
+
若所有 API 都在固定的几个主域名下,也可以直接用一条正则(新增子域自动命中):
|
|
159
253
|
|
|
160
254
|
```ts
|
|
161
255
|
versionInjector({
|
|
162
256
|
requestHeaders: {
|
|
163
|
-
|
|
164
|
-
buildTimeHeaderName: 'X-Client-Build-Time', // 默认值
|
|
165
|
-
// 跨域白名单:字符串按 URL 前缀匹配,正则按完整 URL 测试;同源请求始终注入
|
|
166
|
-
include: ['https://api.example.com/', /\.internal\.example\.com/],
|
|
257
|
+
include: [/^https:\/\/[^/]*\.(example\.io|example\.dev|sandbox-example\.com)\//],
|
|
167
258
|
},
|
|
168
259
|
});
|
|
169
260
|
```
|
|
170
261
|
|
|
171
|
-
>
|
|
172
|
-
|
|
173
|
-
|
|
262
|
+
> 只把**你自己发 fetch/XHR、且能改 CORS 的 API 域名**放进去。CDN、第三方 SDK 脚本域(你控制不了 CORS)不要加,否则只会触发预检导致资源加载失败。
|
|
263
|
+
|
|
264
|
+
### ⚠️ 跨域必读:后端要放行(CORS 预检)
|
|
265
|
+
|
|
266
|
+
给跨域请求加自定义头,浏览器会先发一个 `OPTIONS` 预检。`include` 命中的**每一个** API 服务都必须在响应里放行这两个头,否则请求会被浏览器拦掉:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
Access-Control-Allow-Headers: X-Client-Version, X-Client-Build-Time
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
多后端场景要逐个确认。这也是跨域注入必须通过 `include` 显式开启、而非默认全开的原因。
|
|
273
|
+
|
|
274
|
+
### 🔍 怎么判断请求是同源还是跨域?
|
|
275
|
+
|
|
276
|
+
打开浏览器 Network,看请求的**真实 URL**:
|
|
277
|
+
- `http://localhost:9040/api/...`(走 dev 代理)→ **同源**,`requestHeaders: true` 就够,不用 `include`;
|
|
278
|
+
- `https://api.xxx.com/...`(直连)→ **跨域**,必须加进 `include` + 后端放行。
|
|
174
279
|
|
|
175
280
|
---
|
|
176
281
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { UnpluginInstance, UnpluginFactory } from 'unplugin';
|
|
2
|
-
import { V as VersionInjectorOptions } from './types-
|
|
3
|
-
export { R as RequestHeadersOptions } from './types-
|
|
2
|
+
import { V as VersionInjectorOptions } from './types-CLRw9rBI.mjs';
|
|
3
|
+
export { R as RequestHeadersOptions } from './types-CLRw9rBI.mjs';
|
|
4
4
|
|
|
5
5
|
declare const unpluginFactory: UnpluginFactory<VersionInjectorOptions | undefined>;
|
|
6
6
|
declare const VersionInjector: UnpluginInstance<VersionInjectorOptions | undefined>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { UnpluginInstance, UnpluginFactory } from 'unplugin';
|
|
2
|
-
import { V as VersionInjectorOptions } from './types-
|
|
3
|
-
export { R as RequestHeadersOptions } from './types-
|
|
2
|
+
import { V as VersionInjectorOptions } from './types-CLRw9rBI.js';
|
|
3
|
+
export { R as RequestHeadersOptions } from './types-CLRw9rBI.js';
|
|
4
4
|
|
|
5
5
|
declare const unpluginFactory: UnpluginFactory<VersionInjectorOptions | undefined>;
|
|
6
6
|
declare const VersionInjector: UnpluginInstance<VersionInjectorOptions | undefined>;
|
package/dist/index.js
CHANGED
|
@@ -102,18 +102,27 @@ function toScriptString(value) {
|
|
|
102
102
|
var INJECTED_MARK = 'data-injected="unplugin-version-injector"';
|
|
103
103
|
var HEADERS_INJECTED_MARK = 'data-injected="unplugin-version-injector-headers"';
|
|
104
104
|
var HEADER_NAME_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
105
|
+
var DEFAULT_VERSION_HEADER = "X-Client-Version";
|
|
106
|
+
var DEFAULT_BUILD_TIME_HEADER = "X-Client-Build-Time";
|
|
105
107
|
function normalizeRequestHeaders(opt) {
|
|
106
|
-
var _a, _b, _c;
|
|
107
108
|
if (!opt) return null;
|
|
108
109
|
const o = typeof opt === "object" ? opt : {};
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
110
|
+
const pickHeaderName = (value, fallback) => {
|
|
111
|
+
if (value == null) return fallback;
|
|
112
|
+
if (HEADER_NAME_RE.test(value)) return value;
|
|
113
|
+
console.warn(
|
|
114
|
+
`[VersionInjector] invalid request header name "${value}", falling back to "${fallback}"`
|
|
115
|
+
);
|
|
116
|
+
return fallback;
|
|
117
|
+
};
|
|
118
|
+
const include = (Array.isArray(o.include) ? o.include : []).filter(
|
|
119
|
+
(p) => typeof p === "string" || p instanceof RegExp
|
|
120
|
+
);
|
|
121
|
+
return {
|
|
122
|
+
versionHeaderName: pickHeaderName(o.versionHeaderName, DEFAULT_VERSION_HEADER),
|
|
123
|
+
buildTimeHeaderName: pickHeaderName(o.buildTimeHeaderName, DEFAULT_BUILD_TIME_HEADER),
|
|
124
|
+
include
|
|
125
|
+
};
|
|
117
126
|
}
|
|
118
127
|
function sanitizeHeaderValue(value) {
|
|
119
128
|
return value.replace(/[^\t\x20-\x7E]/g, "").trim();
|
|
@@ -124,6 +133,7 @@ function serializeInclude(patterns) {
|
|
|
124
133
|
);
|
|
125
134
|
return `[${items.join(", ")}]`;
|
|
126
135
|
}
|
|
136
|
+
var HEAD_OPEN_RE = /<head(\s[^>]*)?>/i;
|
|
127
137
|
function createVersionInjector(options = {}) {
|
|
128
138
|
const pkg = options.version && options.name ? { version: options.version, name: options.name } : getPackageVersion();
|
|
129
139
|
const version = options.version || pkg.version;
|
|
@@ -131,25 +141,34 @@ function createVersionInjector(options = {}) {
|
|
|
131
141
|
const formatDateOpt = options.formatDate;
|
|
132
142
|
const formatDate2 = typeof formatDateOpt === "string" ? (date) => formatDate(date, formatDateOpt) : formatDateOpt != null ? formatDateOpt : defaultFormatDate;
|
|
133
143
|
const headersConfig = normalizeRequestHeaders(options.requestHeaders);
|
|
144
|
+
let cachedBuildTime = null;
|
|
145
|
+
const getBuildTime = () => {
|
|
146
|
+
if (cachedBuildTime === null) cachedBuildTime = formatDate2(/* @__PURE__ */ new Date());
|
|
147
|
+
return cachedBuildTime;
|
|
148
|
+
};
|
|
149
|
+
const nonceAttr = options.nonce ? ` nonce="${escapeHtml(options.nonce)}"` : "";
|
|
134
150
|
const metaTag = `<meta name="version" content="${escapeHtml(version)}">
|
|
135
151
|
<meta name="project" content="${escapeHtml(name)}">
|
|
136
152
|
`;
|
|
137
153
|
const buildLogScript = (buildTime) => `
|
|
138
|
-
<script ${INJECTED_MARK}>
|
|
154
|
+
<script${nonceAttr} ${INJECTED_MARK}>
|
|
139
155
|
(function () {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
156
|
+
try {
|
|
157
|
+
var isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
158
|
+
var bg = isDark ? '#ffffff' : '#1e1e1e';
|
|
159
|
+
var base = 'background: ' + bg + '; border-radius: 4px; padding: 4px; font-size: 12px;';
|
|
160
|
+
console.log('%c' + ${toScriptString(` ${name}@${version} `)}, base + ' color: #00c853;');
|
|
161
|
+
console.log('%c' + ${toScriptString(` Build Time: ${buildTime} `)}, base + ' color: #ffab00;');
|
|
162
|
+
} catch (e) {}
|
|
145
163
|
})();
|
|
146
164
|
</script>`;
|
|
147
165
|
const buildHeaderScript = (buildTime, cfg) => {
|
|
148
166
|
const versionValue = sanitizeHeaderValue(`${name}/${version}`);
|
|
149
167
|
const buildValue = sanitizeHeaderValue(buildTime);
|
|
150
168
|
return `
|
|
151
|
-
<script ${HEADERS_INJECTED_MARK}>
|
|
169
|
+
<script${nonceAttr} ${HEADERS_INJECTED_MARK}>
|
|
152
170
|
(function () {
|
|
171
|
+
try {
|
|
153
172
|
if (window.__UVI_HEADERS_PATCHED__) return;
|
|
154
173
|
window.__UVI_HEADERS_PATCHED__ = true;
|
|
155
174
|
var VERSION_HEADER = ${toScriptString(cfg.versionHeaderName)};
|
|
@@ -173,13 +192,21 @@ function createVersionInjector(options = {}) {
|
|
|
173
192
|
}
|
|
174
193
|
}
|
|
175
194
|
|
|
195
|
+
function matchesPrefix(href, p) {
|
|
196
|
+
if (href.indexOf(p) !== 0) return false;
|
|
197
|
+
// \u524D\u7F00\u672C\u8EAB\u4EE5 / \u7ED3\u5C3E\uFF0C\u6216\u524D\u7F00\u540E\u7D27\u8DDF\u8FB9\u754C\u5B57\u7B26\uFF0C\u907F\u514D 'https://api.x.com' \u547D\u4E2D 'https://api.x.com.evil.com'
|
|
198
|
+
if (p.charAt(p.length - 1) === '/') return true;
|
|
199
|
+
var next = href.charAt(p.length);
|
|
200
|
+
return next === '' || next === '/' || next === '?' || next === '#';
|
|
201
|
+
}
|
|
202
|
+
|
|
176
203
|
function shouldInject(url) {
|
|
177
204
|
var u = resolveUrl(url == null ? '' : String(url));
|
|
178
205
|
if (!u || (u.protocol !== 'http:' && u.protocol !== 'https:')) return false;
|
|
179
206
|
if (u.protocol === location.protocol && u.host === location.host) return true;
|
|
180
207
|
for (var i = 0; i < INCLUDE.length; i++) {
|
|
181
208
|
var p = INCLUDE[i];
|
|
182
|
-
if (typeof p === 'string' ? u.href
|
|
209
|
+
if (typeof p === 'string' ? matchesPrefix(u.href, p) : p.test(u.href)) return true;
|
|
183
210
|
}
|
|
184
211
|
return false;
|
|
185
212
|
}
|
|
@@ -228,29 +255,37 @@ function createVersionInjector(options = {}) {
|
|
|
228
255
|
};
|
|
229
256
|
}
|
|
230
257
|
}
|
|
258
|
+
} catch (e) {}
|
|
231
259
|
})();
|
|
232
260
|
</script>`;
|
|
233
261
|
};
|
|
234
262
|
const processHtml = function processHtml2(html) {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
263
|
+
const original = html;
|
|
264
|
+
try {
|
|
265
|
+
const buildTime = getBuildTime();
|
|
266
|
+
if (!html.includes('<meta name="version"')) {
|
|
267
|
+
html = html.replace(HEAD_OPEN_RE, (match) => `${match}
|
|
238
268
|
${metaTag}`);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
269
|
+
}
|
|
270
|
+
if (headersConfig && !html.includes(HEADERS_INJECTED_MARK)) {
|
|
271
|
+
const headerScript = buildHeaderScript(buildTime, headersConfig);
|
|
272
|
+
html = html.replace(HEAD_OPEN_RE, (match) => `${match}
|
|
243
273
|
${headerScript}
|
|
244
274
|
`);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
275
|
+
}
|
|
276
|
+
if (options.log !== false && !html.includes(INJECTED_MARK)) {
|
|
277
|
+
const logScript = buildLogScript(buildTime);
|
|
278
|
+
html = html.replace(/<\/body>/i, () => ` ${logScript}
|
|
249
279
|
</body>`);
|
|
280
|
+
}
|
|
281
|
+
return html;
|
|
282
|
+
} catch (err) {
|
|
283
|
+
console.warn("[VersionInjector] injection failed, returning original HTML unchanged:", err);
|
|
284
|
+
return original;
|
|
250
285
|
}
|
|
251
|
-
return html;
|
|
252
286
|
};
|
|
253
287
|
processHtml.resetBuildTime = () => {
|
|
288
|
+
cachedBuildTime = null;
|
|
254
289
|
};
|
|
255
290
|
return processHtml;
|
|
256
291
|
}
|
|
@@ -305,6 +340,10 @@ var unpluginFactory = (options = {}) => {
|
|
|
305
340
|
const inject = createVersionInjector(options);
|
|
306
341
|
return {
|
|
307
342
|
name: PLUGIN_NAME,
|
|
343
|
+
// 每轮(重)构建开始时重置构建时间缓存:watch 重建刷新时间,同一次构建里 MPA 各页保持一致
|
|
344
|
+
buildStart() {
|
|
345
|
+
inject.resetBuildTime();
|
|
346
|
+
},
|
|
308
347
|
vite: {
|
|
309
348
|
transformIndexHtml(html) {
|
|
310
349
|
return inject(html);
|