vite-plugin-preloader 1.1.2 → 2.0.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/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  // vite-plugin-preloader - 智能路由预加载插件
2
2
  "use strict";
3
+ var __create = Object.create;
3
4
  var __defProp = Object.defineProperty;
4
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
6
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
9
  var __export = (target, all) => {
8
10
  for (var name in all)
@@ -16,6 +18,14 @@ var __copyProps = (to, from, except, desc) => {
16
18
  }
17
19
  return to;
18
20
  };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
19
29
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
30
 
21
31
  // src/index.ts
@@ -26,314 +36,290 @@ __export(index_exports, {
26
36
  module.exports = __toCommonJS(index_exports);
27
37
 
28
38
  // src/runtime.ts
29
- var runtimeTemplate = `// \u{1F680} Auto-generated by vite-plugin-preloader
30
- (function() {
31
- 'use strict';
32
-
33
- // \u{1F3AF} \u9884\u52A0\u8F7D\u914D\u7F6E\uFF08\u6784\u5EFA\u65F6\u6CE8\u5165\uFF09
34
- const PRELOAD_ROUTES = __PRELOAD_ROUTES__
35
- const PRELOAD_OPTIONS = __PRELOAD_OPTIONS__
36
-
37
- class PreloaderManager {
38
- constructor() {
39
- this.preloadedRoutes = new Set()
40
- this.isPreloading = false
41
- this.stats = {
42
- total: 0, completed: 0, failed: 0, startTime: 0, endTime: 0
43
- }
44
- this.statusElement = null
45
- }
46
-
47
- async start() {
48
- if (this.isPreloading) return
49
-
50
- this.isPreloading = true
51
- this.stats = {
52
- total: PRELOAD_ROUTES.length,
53
- completed: 0, failed: 0,
54
- startTime: Date.now(), endTime: 0
55
- }
56
-
57
- if (PRELOAD_OPTIONS.debug) {
58
- console.log(\`\u{1F680} [\u9884\u52A0\u8F7D] \u5F00\u59CB\u9884\u52A0\u8F7D \${PRELOAD_ROUTES.length} \u4E2A\u9875\u9762\`)
59
- }
60
-
61
- // \u663E\u793A\u72B6\u6001\u6307\u793A\u5668
62
- this.showStatus()
63
-
64
- const sortedRoutes = [...PRELOAD_ROUTES].sort((a, b) => a.priority - b.priority)
65
-
66
- for (const route of sortedRoutes) {
67
- await this.preloadSingle(route)
68
- this.updateStatus()
69
- await this.sleep(100)
70
- }
71
-
72
- this.stats.endTime = Date.now()
73
- this.isPreloading = false
74
-
75
- if (PRELOAD_OPTIONS.debug) {
76
- console.log(\`\u{1F389} [\u9884\u52A0\u8F7D] \u5B8C\u6210! \u8017\u65F6 \${this.stats.endTime - this.stats.startTime}ms\`)
77
- }
78
-
79
- // \u9690\u85CF\u72B6\u6001\u6307\u793A\u5668
80
- this.hideStatus()
81
- }
82
-
83
- async preloadSingle(route) {
84
- if (this.preloadedRoutes.has(route.path)) return
85
-
86
- try {
87
- const startTime = Date.now()
88
- let componentPath = route.component
89
-
90
- // \u5728\u5F00\u53D1\u73AF\u5883\u4E2D\uFF0CVite \u4F1A\u5904\u7406\u6A21\u5757\u89E3\u6790
91
- // \u6211\u4EEC\u9700\u8981\u4F7F\u7528\u6B63\u786E\u7684\u6A21\u5757 ID
92
- if (componentPath.startsWith('@/')) {
93
- // \u5BF9\u4E8E Vite\uFF0C@ \u522B\u540D\u5E94\u8BE5\u5728\u6784\u5EFA\u65F6\u5C31\u88AB\u89E3\u6790
94
- // \u4F46\u5728\u8FD0\u884C\u65F6\u6211\u4EEC\u9700\u8981\u4F7F\u7528\u5B9E\u9645\u7684\u8DEF\u5F84
95
- componentPath = componentPath.replace('@/', '/src/')
96
- }
97
-
98
- // \u786E\u4FDD\u8DEF\u5F84\u4EE5 / \u5F00\u5934\uFF08\u76F8\u5BF9\u4E8E\u9879\u76EE\u6839\u76EE\u5F55\uFF09
99
- if (!componentPath.startsWith('/') && !componentPath.startsWith('./')) {
100
- componentPath = '/' + componentPath
101
- }
102
-
103
- if (PRELOAD_OPTIONS.debug) {
104
- console.log(\`\u{1F50D} [\u9884\u52A0\u8F7D] \u5C1D\u8BD5\u52A0\u8F7D: \${componentPath}\`)
105
- }
106
-
107
- const module = await import(/* @vite-ignore */ componentPath)
108
- const loadTime = Date.now() - startTime
109
-
110
- this.preloadedRoutes.add(route.path)
111
- this.stats.completed++
112
-
113
- if (PRELOAD_OPTIONS.debug) {
114
- console.log(\`\u2705 [\u9884\u52A0\u8F7D] \${route.path} (\${loadTime}ms) - \${route.reason}\`)
115
- }
116
- } catch (error) {
117
- this.stats.failed++
118
- if (PRELOAD_OPTIONS.debug) {
119
- console.error(\`\u274C [\u9884\u52A0\u8F7D] \${route.path} \u5931\u8D25:\`, error)
120
- }
121
- }
122
- }
123
-
124
- showStatus() {
125
- if (!PRELOAD_OPTIONS.showStatus || !document.body) return
126
-
127
- // \u6DFB\u52A0\u6837\u5F0F
128
- if (!document.getElementById('preloader-styles')) {
129
- const style = document.createElement('style')
130
- style.id = 'preloader-styles'
131
- const position = PRELOAD_OPTIONS.statusPosition.replace('-', ': 20px; ') + ': 20px;'
132
- style.textContent = \`
133
- .preloader-status {
134
- position: fixed; \${position}
135
- background: rgba(0,0,0,0.8); color: white; padding: 8px 16px;
136
- border-radius: 6px; font-size: 12px; z-index: 9999;
137
- pointer-events: none; font-family: system-ui;
138
- transition: opacity 0.3s ease;
139
- }
140
- .preloader-status.fade-out {
141
- opacity: 0;
142
- }
143
- \`
144
- document.head.appendChild(style)
145
- }
146
-
147
- // \u521B\u5EFA\u72B6\u6001\u5143\u7D20
148
- this.statusElement = document.createElement('div')
149
- this.statusElement.className = 'preloader-status'
150
- this.updateStatus()
151
- document.body.appendChild(this.statusElement)
152
- }
153
-
154
- updateStatus() {
155
- if (!this.statusElement) return
156
- this.statusElement.textContent = \`\u{1F504} \u6B63\u5728\u4F18\u5316\u9875\u9762... \${this.stats.completed}/\${this.stats.total}\`
157
- }
158
-
159
- hideStatus() {
160
- if (!this.statusElement) return
161
- this.statusElement.classList.add('fade-out')
162
- setTimeout(() => {
163
- if (this.statusElement && this.statusElement.parentNode) {
164
- this.statusElement.parentNode.removeChild(this.statusElement)
165
- }
166
- }, 300)
167
- }
168
-
169
- sleep(ms) {
170
- return new Promise(resolve => setTimeout(resolve, ms))
171
- }
172
-
173
- isPreloaded(path) {
174
- return this.preloadedRoutes.has(path)
175
- }
176
-
177
- getStats() {
178
- return {
179
- ...this.stats,
180
- preloadedPaths: Array.from(this.preloadedRoutes),
181
- isPreloading: this.isPreloading
39
+ var STATUS_STYLES = {
40
+ "bottom-right": "bottom:16px;right:16px",
41
+ "bottom-left": "bottom:16px;left:16px",
42
+ "top-right": "top:16px;right:16px",
43
+ "top-left": "top:16px;left:16px"
44
+ };
45
+ function buildRuntimeScript(config) {
46
+ const posStyle = STATUS_STYLES[config.statusPosition] ?? STATUS_STYLES["bottom-right"];
47
+ const configJson = JSON.stringify(config);
48
+ const statusUiCode = config.showStatus ? `
49
+ function showStatus(){
50
+ var links=document.querySelectorAll('link[data-preloader]');
51
+ if(!links.length)return;
52
+ var el=document.createElement('div');
53
+ el.id='__preloader_ui__';
54
+ el.style.cssText='position:fixed;${posStyle};z-index:2147483647;'
55
+ +'background:rgba(15,15,15,0.82);color:#fff;font-size:12px;line-height:1.5;'
56
+ +'padding:7px 14px;border-radius:20px;pointer-events:none;'
57
+ +'transition:opacity 0.4s ease;font-family:system-ui,-apple-system,sans-serif;'
58
+ +'backdrop-filter:blur(4px);box-shadow:0 2px 8px rgba(0,0,0,0.3);';
59
+ el.textContent='\u26A1 \u6B63\u5728\u4F18\u5316 '+links.length+' \u4E2A\u9875\u9762...';
60
+ document.body&&document.body.appendChild(el);
61
+ var done=0,total=links.length;
62
+ function tick(){
63
+ done++;
64
+ el.textContent=done>=total?'\u2705 \u9875\u9762\u4F18\u5316\u5B8C\u6210\uFF08'+total+'\u4E2A\uFF09':'\u26A1 \u4F18\u5316\u4E2D '+done+'/'+total+'...';
65
+ if(done>=total){
66
+ setTimeout(function(){
67
+ el.style.opacity='0';
68
+ setTimeout(function(){el&&el.parentNode&&el.parentNode.removeChild(el);},420);
69
+ },2200);
182
70
  }
183
71
  }
72
+ links.forEach(function(l){l.addEventListener('load',tick);l.addEventListener('error',tick);});
73
+ setTimeout(function(){el&&el.parentNode&&el.parentNode.removeChild(el);},9000);
184
74
  }
185
-
186
- // \u{1F680} \u5168\u5C40\u5B9E\u4F8B
187
- const preloader = new PreloaderManager()
188
-
189
- // \u{1F6E0}\uFE0F \u5F00\u53D1\u73AF\u5883\u8C03\u8BD5\u5DE5\u5177
190
- if (PRELOAD_OPTIONS.debug) {
191
- window.preloaderDebug = {
192
- stats: () => preloader.getStats(),
193
- restart: () => preloader.start(),
194
- check: (path) => preloader.isPreloaded(path),
195
- help: () => console.log('\u{1F6E0}\uFE0F \u9884\u52A0\u8F7D\u8C03\u8BD5: stats() | restart() | check(path)')
196
- }
197
- console.log('\u{1F6E0}\uFE0F \u9884\u52A0\u8F7D\u8C03\u8BD5\u5DE5\u5177: window.preloaderDebug')
198
- }
199
-
200
- // \u{1F680} \u81EA\u52A8\u542F\u52A8 - \u7B49\u5F85 DOM \u52A0\u8F7D\u5B8C\u6210
201
- function autoStart() {
202
- if (document.readyState === 'loading') {
203
- document.addEventListener('DOMContentLoaded', () => {
204
- setTimeout(() => preloader.start(), PRELOAD_OPTIONS.delay)
205
- })
206
- } else {
207
- setTimeout(() => preloader.start(), PRELOAD_OPTIONS.delay)
208
- }
75
+ if(document.readyState==='loading'){
76
+ document.addEventListener('DOMContentLoaded',function(){setTimeout(showStatus,C.delay);});
77
+ }else{
78
+ setTimeout(showStatus,C.delay);
79
+ }` : "";
80
+ const debugCode = config.debug ? `
81
+ console.group('%c\u{1F680} vite-plugin-preloader v2','color:#4ade80;font-weight:700');
82
+ console.log('\u5DF2\u6CE8\u5165 '+C.routes.length+' \u4E2A <link rel=\\"prefetch\\"> \u6807\u7B7E\uFF08\u6D4F\u89C8\u5668\u539F\u751F\u8C03\u5EA6\uFF09');
83
+ C.routes.forEach(function(r){
84
+ var ok=r.chunkPath;
85
+ console.log((ok?'\u2705 ':'\u26A0\uFE0F ')+r.path+(ok?' \u2192 '+r.chunkPath:' (chunk \u672A\u5339\u914D\uFF0C\u4EC5\u5F00\u53D1\u73AF\u5883\u6709\u6548)')+(r.reason?' | '+r.reason:''));
86
+ });
87
+ console.groupEnd();
88
+ window.__preloaderDebug={
89
+ routes:C.routes,
90
+ check:function(p){return C.routes.some(function(r){return r.path===p&&!!r.chunkPath;})},
91
+ help:function(){console.log('__preloaderDebug: .routes | .check(path)');}
92
+ };
93
+ console.log('%c\u{1F6E0} \u8C03\u8BD5: window.__preloaderDebug','color:#94a3b8;font-size:11px');` : "";
94
+ if (!statusUiCode && !debugCode) return "";
95
+ return `<script>/* vite-plugin-preloader v2 - status/debug */
96
+ (function(){
97
+ var C=${configJson};
98
+ var conn=navigator.connection||navigator.mozConnection||navigator.webkitConnection;
99
+ if(conn&&(conn.saveData||['slow-2g','2g'].indexOf(conn.effectiveType)>=0)){
100
+ if(C.debug)console.warn('[\u9884\u52A0\u8F7D] \u68C0\u6D4B\u5230\u7701\u6D41/\u4F4E\u901F\u7F51\u7EDC\uFF0C\u8DF3\u8FC7\u72B6\u6001\u663E\u793A');
101
+ return;
209
102
  }
210
-
211
- // \u7ACB\u5373\u6267\u884C\u81EA\u52A8\u542F\u52A8
212
- autoStart()
213
-
214
- // \u5BFC\u51FA\u5230\u5168\u5C40\uFF08\u53EF\u9009\u4F7F\u7528\uFF09
215
- window.usePreloader = () => ({
216
- start: () => preloader.start(),
217
- isPreloaded: (path) => preloader.isPreloaded(path),
218
- getStats: () => preloader.getStats()
219
- })
220
-
221
- })();`;
103
+ ${debugCode}
104
+ ${statusUiCode}
105
+ })();
106
+ </script>`;
107
+ }
222
108
 
223
109
  // src/generator.ts
110
+ var import_vite = require("vite");
111
+ var import_node_path = __toESM(require("path"));
224
112
  var CodeGenerator = class {
225
113
  constructor(options) {
226
114
  this.options = options;
115
+ /** @ 别名对应的相对路径(相对于 viteRoot),默认 'src' */
116
+ this.srcAlias = "src";
117
+ this.viteRoot = process.cwd();
227
118
  }
228
119
  /**
229
- * 生成运行时代码
120
+ * 从 Vite resolvedConfig 中读取项目根目录和 @ 别名
121
+ * 必须在 configResolved hook 中调用
230
122
  */
231
- generateRuntime() {
232
- const routes = this.processRoutes();
233
- const options = this.processOptions();
234
- return runtimeTemplate.replace("__PRELOAD_ROUTES__", JSON.stringify(routes, null, 2)).replace("__PRELOAD_OPTIONS__", JSON.stringify(options, null, 2));
123
+ setViteConfig(config) {
124
+ this.viteRoot = config.root;
125
+ const alias = config.resolve?.alias;
126
+ const entries = Array.isArray(alias) ? alias : Object.entries(alias ?? {}).map(([find, replacement]) => ({ find, replacement }));
127
+ const atEntry = entries.find(
128
+ (a) => a.find === "@" || a.find instanceof RegExp && a.find.source === "^@/"
129
+ );
130
+ if (atEntry && typeof atEntry.replacement === "string") {
131
+ const rel = (0, import_vite.normalizePath)(import_node_path.default.relative(this.viteRoot, atEntry.replacement));
132
+ if (rel) this.srcAlias = rel;
133
+ }
235
134
  }
236
135
  /**
237
- * 处理路由配置
136
+ * 将用户路由配置解析为内部 ResolvedRoute 列表
137
+ * 同时过滤:含 :param 的动态路由段、exclude 列表匹配项
238
138
  */
239
- processRoutes() {
139
+ resolveRoutes() {
140
+ if (!Array.isArray(this.options.routes) || this.options.routes.length === 0) {
141
+ return [];
142
+ }
143
+ const { exclude } = this.options;
240
144
  return this.options.routes.map((route) => {
241
145
  if (typeof route === "string") {
242
- const componentPath2 = this.inferComponentPath(route);
243
146
  return {
244
147
  path: route,
245
- component: componentPath2,
148
+ component: this.inferComponentPath(route),
246
149
  reason: "\u81EA\u52A8\u63A8\u65AD\u7684\u9884\u52A0\u8F7D\u9875\u9762",
247
150
  priority: 2
248
151
  };
249
152
  }
250
- const componentPath = route.component || this.inferComponentPath(route.path);
251
153
  return {
252
154
  path: route.path,
253
- component: componentPath,
254
- reason: route.reason || "\u7528\u6237\u914D\u7F6E\u7684\u9884\u52A0\u8F7D\u9875\u9762",
255
- priority: route.priority || 2
155
+ component: route.component ?? this.inferComponentPath(route.path),
156
+ reason: route.reason ?? "\u7528\u6237\u914D\u7F6E\u7684\u9884\u52A0\u8F7D\u9875\u9762",
157
+ priority: route.priority ?? 2
256
158
  };
257
- });
159
+ }).filter((route) => {
160
+ if (/:\w+/.test(route.path)) return false;
161
+ if (exclude.length > 0 && exclude.some(
162
+ (e) => route.path === e || route.path.startsWith(e.replace(/\/$/, "") + "/")
163
+ )) {
164
+ return false;
165
+ }
166
+ return true;
167
+ }).sort((a, b) => a.priority - b.priority);
258
168
  }
259
169
  /**
260
- * 处理选项配置
170
+ * 根据 Rollup OutputBundle 中的 facadeModuleId 为各路由匹配实际 chunk 文件路径
171
+ * 仅在生产构建的 generateBundle hook 中调用
261
172
  */
262
- processOptions() {
263
- const isDev = process.env.NODE_ENV !== "production";
264
- return {
265
- delay: this.options.delay ?? 2e3,
266
- // 默认2秒
267
- showStatus: this.options.showStatus ?? true,
268
- // 默认显示状态
269
- statusPosition: this.options.statusPosition ?? "bottom-right",
270
- // 默认右下角
271
- debug: this.options.debug ?? isDev
272
- // 开发环境默认开启调试,生产环境默认关闭
273
- };
173
+ fillChunkPaths(routes, bundle) {
174
+ const chunkMap = /* @__PURE__ */ new Map();
175
+ for (const [fileName, chunkOrAsset] of Object.entries(bundle)) {
176
+ if (chunkOrAsset.type !== "chunk") continue;
177
+ const chunk = chunkOrAsset;
178
+ if (!chunk.facadeModuleId) continue;
179
+ chunkMap.set((0, import_vite.normalizePath)(chunk.facadeModuleId), "/" + fileName);
180
+ }
181
+ return routes.map((route) => {
182
+ const absPath = (0, import_vite.normalizePath)(this.resolveComponentAbsPath(route.component));
183
+ const chunkPath = chunkMap.get(absPath);
184
+ return chunkPath ? { ...route, chunkPath } : route;
185
+ });
274
186
  }
275
187
  /**
276
- * 推断组件路径 - 使用 Vite 别名格式
188
+ * 生成 <link> 标签字符串(注入到 </body> 前)
189
+ * 生产:<link rel="prefetch"> 指向实际 chunk(浏览器空闲时拉取,完全不阻塞主线程)
190
+ * 开发:<link rel="modulepreload"> 指向源文件(Vite dev server 处理解析)
277
191
  */
278
- inferComponentPath(routePath) {
279
- const cleanPath = routePath.replace(/^\//, "");
280
- if (cleanPath.startsWith("demo/")) {
281
- return `@/views/${cleanPath}/index.vue`;
192
+ generateLinkTags(routes, isBuild) {
193
+ const tags = [];
194
+ for (const route of routes) {
195
+ if (isBuild) {
196
+ if (!route.chunkPath) continue;
197
+ tags.push(
198
+ `<link rel="prefetch" href="${route.chunkPath}" as="script" crossorigin data-preloader="${encodeURIComponent(route.path)}">`
199
+ );
200
+ } else {
201
+ const devPath = route.component.startsWith("@/") ? route.component.replace("@/", `/${this.srcAlias}/`) : route.component;
202
+ tags.push(
203
+ `<link rel="modulepreload" href="${devPath}" data-preloader="${encodeURIComponent(route.path)}">`
204
+ );
205
+ }
282
206
  }
283
- const pathSegments = cleanPath.split("/");
284
- return `@/views/${pathSegments.join("/")}/index.vue`;
207
+ return tags.join("\n");
285
208
  }
286
209
  /**
287
- * 生成注入到 HTML 头部的脚本
210
+ * 生成可选的状态 UI 和调试工具脚本
211
+ * showStatus=false 且 debug=false 时返回空字符串(0 运行时开销)
288
212
  */
289
- generateHtmlInject() {
290
- return `<script type="module">
291
- ${this.generateRuntime()}
292
- </script>`;
213
+ generateRuntimeScript(routes, isBuild) {
214
+ return buildRuntimeScript({
215
+ delay: this.options.delay,
216
+ debug: this.options.debug,
217
+ showStatus: this.options.showStatus,
218
+ statusPosition: this.options.statusPosition,
219
+ isBuild,
220
+ routes: routes.map((r) => ({
221
+ path: r.path,
222
+ chunkPath: r.chunkPath ?? null,
223
+ reason: r.reason
224
+ }))
225
+ });
226
+ }
227
+ // --------------------------------------------------------------------------
228
+ // 私有工具方法
229
+ // --------------------------------------------------------------------------
230
+ /** 根据路由路径推断 Vue 组件路径(@/views/{path}/index.vue) */
231
+ inferComponentPath(routePath) {
232
+ const cleanPath = routePath.replace(/^\//, "").replace(/\/$/, "");
233
+ return `@/views/${cleanPath}/index.vue`;
234
+ }
235
+ /** 将 @/ 别名路径或相对路径解析为绝对路径,用于与 facadeModuleId 比对 */
236
+ resolveComponentAbsPath(componentPath) {
237
+ if (componentPath.startsWith("@/")) {
238
+ return import_node_path.default.join(this.viteRoot, this.srcAlias, componentPath.slice(2));
239
+ }
240
+ if (import_node_path.default.isAbsolute(componentPath)) {
241
+ return componentPath;
242
+ }
243
+ return import_node_path.default.join(this.viteRoot, componentPath);
293
244
  }
294
245
  };
295
246
 
296
247
  // src/index.ts
297
- function preloaderPlugin(options) {
298
- let generator;
299
- const isDev = process.env.NODE_ENV !== "production";
300
- const finalOptions = {
301
- debug: isDev,
302
- // 开发环境默认开启调试
303
- delay: 2e3,
304
- // 默认2秒
305
- showStatus: true,
306
- // 默认显示状态
307
- statusPosition: "bottom-right",
308
- // 默认右下角
309
- ...options
310
- // 用户配置覆盖默认配置
248
+ function preloaderPlugin(userOptions = {}) {
249
+ const options = {
250
+ routes: Array.isArray(userOptions.routes) ? userOptions.routes : [],
251
+ delay: userOptions.delay ?? 2e3,
252
+ debug: userOptions.debug ?? process.env.NODE_ENV !== "production",
253
+ showStatus: userOptions.showStatus ?? true,
254
+ statusPosition: userOptions.statusPosition ?? "bottom-right",
255
+ exclude: userOptions.exclude ?? []
311
256
  };
257
+ const generator = new CodeGenerator(options);
258
+ let resolvedRoutes = [];
259
+ let isBuild = false;
312
260
  return {
313
261
  name: "vite-plugin-preloader",
314
- // 🎯 设置插件执行顺序
262
+ // post 确保在其他插件(如 @vitejs/plugin-vue)处理完后执行
315
263
  enforce: "post",
316
- configResolved() {
317
- generator = new CodeGenerator(finalOptions);
318
- if (finalOptions.debug) {
319
- console.log(`\u{1F680} [\u9884\u52A0\u8F7D\u63D2\u4EF6] \u5DF2\u542F\u7528\uFF0C\u9884\u52A0\u8F7D ${finalOptions.routes.length} \u4E2A\u9875\u9762\uFF0C\u8BE6\u60C5\u8BF7\u67E5\u770B\u6D4F\u89C8\u5668\u63A7\u5236\u53F0`);
264
+ // SSR 构建时不注入客户端预加载脚本
265
+ apply(_, { isSsrBuild }) {
266
+ return !isSsrBuild;
267
+ },
268
+ // ✅ 正确读取 viteConfig:alias、root、command
269
+ configResolved(config) {
270
+ isBuild = config.command === "build";
271
+ generator.setViteConfig(config);
272
+ resolvedRoutes = generator.resolveRoutes();
273
+ if (options.debug) {
274
+ const total = Array.isArray(userOptions.routes) ? userOptions.routes.length : 0;
275
+ const filtered = total - resolvedRoutes.length;
276
+ console.log(
277
+ `\u{1F680} [\u9884\u52A0\u8F7D\u63D2\u4EF6 v2] \u5DF2\u542F\u7528\uFF0C${resolvedRoutes.length} \u4E2A\u8DEF\u7531\u5F85\u9884\u52A0\u8F7D` + (filtered > 0 ? `\uFF08\u5DF2\u8FC7\u6EE4 ${filtered} \u4E2A\u52A8\u6001/\u6392\u9664\u8DEF\u7531\uFF09` : "")
278
+ );
279
+ if (!isBuild) {
280
+ console.log(
281
+ ' \u5F00\u53D1\u6A21\u5F0F\uFF1A\u6CE8\u5165 <link rel="modulepreload">\uFF0C\u751F\u4EA7\u6784\u5EFA\u540E\u5207\u6362\u4E3A <link rel="prefetch">'
282
+ );
283
+ }
320
284
  }
321
285
  },
322
- // 🎨 HTML 转换 - 直接注入脚本到 HTML
286
+ // 生产构建阶段:扫描 bundle,为路由匹配真实 chunk hash 路径
287
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
288
+ generateBundle(_opts, bundle) {
289
+ if (!isBuild) return;
290
+ resolvedRoutes = generator.fillChunkPaths(resolvedRoutes, bundle);
291
+ if (options.debug) {
292
+ const matched = resolvedRoutes.filter((r) => r.chunkPath).length;
293
+ const unmatched = resolvedRoutes.filter((r) => !r.chunkPath);
294
+ console.log(
295
+ `\u{1F4E6} [\u9884\u52A0\u8F7D\u63D2\u4EF6 v2] bundle \u626B\u63CF\u5B8C\u6210\uFF1A${matched}/${resolvedRoutes.length} \u4E2A\u8DEF\u7531\u6210\u529F\u5339\u914D chunk`
296
+ );
297
+ unmatched.forEach((r) => {
298
+ console.warn(
299
+ ` \u26A0\uFE0F \u672A\u627E\u5230 chunk\uFF1A${r.path} \u2192 ${r.component}\uFF08\u7EC4\u4EF6\u8DEF\u5F84\u53EF\u80FD\u4E0D\u5339\u914D\uFF09`
300
+ );
301
+ });
302
+ }
303
+ },
304
+ // ✅ 注入 <link> 标签 + 可选运行时脚本到 </body> 前
323
305
  transformIndexHtml(html) {
324
- const inject = generator.generateHtmlInject();
325
- return html.replace("</head>", `${inject}
326
- </head>`);
306
+ const linkTags = generator.generateLinkTags(resolvedRoutes, isBuild);
307
+ const runtimeScript = generator.generateRuntimeScript(
308
+ resolvedRoutes,
309
+ isBuild
310
+ );
311
+ const inject = [linkTags, runtimeScript].filter(Boolean).join("\n");
312
+ if (!inject) return html;
313
+ if (html.includes("</body>")) {
314
+ return html.replace("</body>", `${inject}
315
+ </body>`);
316
+ }
317
+ return html + "\n" + inject;
327
318
  },
328
- // 🔥 HMR 支持
319
+ // HMR:vite.config 变更时触发完整刷新
329
320
  handleHotUpdate(ctx) {
330
321
  if (ctx.file.includes("vite.config")) {
331
- if (finalOptions.debug) {
332
- console.log("\u{1F504} [\u9884\u52A0\u8F7D\u63D2\u4EF6] \u914D\u7F6E\u5DF2\u66F4\u65B0");
333
- }
334
- ctx.server.ws.send({
335
- type: "full-reload"
336
- });
322
+ ctx.server.ws.send({ type: "full-reload" });
337
323
  return [];
338
324
  }
339
325
  return void 0;