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 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` | Custom build time formatter | ISO 8601 format |
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 every API request carries the client version — making it trivial to tell which client build produced a request in backend/API logs:
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: true, // same-origin requests only
185
+ requestHeaders: {
186
+ versionHeaderName: 'X-App-Version',
187
+ buildTimeHeaderName: 'X-App-Build',
188
+ },
142
189
  });
143
190
  ```
144
191
 
145
- Every same-origin request then includes:
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
- X-Client-Version: my-app/1.2.3
149
- X-Client-Build-Time: 2024-04-01T12:00:00.000Z
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
- Full configuration:
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
- versionHeaderName: 'X-Client-Version', // default
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
- > ⚠️ **CORS**: custom headers on cross-origin requests trigger a preflight the server must allow them via `Access-Control-Allow-Headers`. That's why cross-origin injection is opt-in through `include`.
167
- >
168
- > Note: `navigator.sendBeacon` and WebSocket connections cannot carry custom headers; the patch covers `fetch` and `XMLHttpRequest` (which includes axios and most HTTP clients).
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` | 自定义时间格式函数 | ISO 格式 |
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
- 开启 `requestHeaders` 后,插件会在页面最前面 patch `window.fetch` 和 `XMLHttpRequest`,让所有 API 请求自动带上版本信息——排查前后端请求日志时可以直接看出是哪个客户端、哪个版本发出的请求:
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: true, // 默认仅同源请求
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
- X-Client-Version: my-app/1.2.3
155
- X-Client-Build-Time: 2024-04-01T12:00:00.000Z
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
- versionHeaderName: 'X-Client-Version', // 默认值
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
- > ⚠️ **CORS 注意**:给跨域请求加自定义头会触发预检(preflight),服务端必须在 `Access-Control-Allow-Headers` 中放行这两个头,否则请求会失败——所以跨域注入设计为通过 `include` 显式开启。
172
- >
173
- > 说明:`navigator.sendBeacon` WebSocket 无法携带自定义请求头;补丁覆盖 `fetch` 与 `XMLHttpRequest`(axios 等主流 HTTP 客户端底层都走这两条路)。
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-Cc-nzIS0.mjs';
3
- export { R as RequestHeadersOptions } from './types-Cc-nzIS0.mjs';
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-Cc-nzIS0.js';
3
- export { R as RequestHeadersOptions } from './types-Cc-nzIS0.js';
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 versionHeaderName = (_a = o.versionHeaderName) != null ? _a : "X-Client-Version";
110
- const buildTimeHeaderName = (_b = o.buildTimeHeaderName) != null ? _b : "X-Client-Build-Time";
111
- for (const h of [versionHeaderName, buildTimeHeaderName]) {
112
- if (!HEADER_NAME_RE.test(h)) {
113
- throw new Error(`[VersionInjector] invalid request header name: "${h}"`);
114
- }
115
- }
116
- return { versionHeaderName, buildTimeHeaderName, include: (_c = o.include) != null ? _c : [] };
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
- var isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
141
- var bg = isDark ? '#ffffff' : '#1e1e1e';
142
- var base = 'background: ' + bg + '; border-radius: 4px; padding: 4px; font-size: 12px;';
143
- console.log('%c' + ${toScriptString(` ${name}@${version} `)}, base + ' color: #00c853;');
144
- console.log('%c' + ${toScriptString(` Build Time: ${buildTime} `)}, base + ' color: #ffab00;');
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.indexOf(p) === 0 : p.test(u.href)) return true;
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 buildTime = formatDate2(/* @__PURE__ */ new Date());
236
- if (!html.includes('<meta name="version"')) {
237
- html = html.replace(/<head[^>]*>/i, (match) => `${match}
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
- if (headersConfig && !html.includes(HEADERS_INJECTED_MARK)) {
241
- const headerScript = buildHeaderScript(buildTime, headersConfig);
242
- html = html.replace(/<head[^>]*>/i, (match) => `${match}
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
- if (options.log !== false && !html.includes(INJECTED_MARK)) {
247
- const logScript = buildLogScript(buildTime);
248
- html = html.replace(/<\/body>/i, () => ` ${logScript}
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);