vite-plugin-preloader 1.1.3 → 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.d.mts CHANGED
@@ -1,17 +1,50 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
3
  interface PreloadRoute {
4
+ /** 路由路径,如 /dashboard。含 :param 的动态路由会被自动过滤 */
4
5
  path: string;
6
+ /** 可选。组件文件路径,不填则自动推断为 @/views/{path}/index.vue */
5
7
  component?: string;
8
+ /** 备注说明,仅用于日志显示 */
6
9
  reason?: string;
10
+ /** 优先级,数字越小越优先加载,默认 2 */
7
11
  priority?: number;
8
12
  }
13
+ type StatusPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";
9
14
  interface PreloaderOptions {
10
- routes: (string | PreloadRoute)[];
15
+ /**
16
+ * 要预加载的路由列表,支持字符串或对象格式
17
+ * 含 :param 的动态路由段会被自动过滤
18
+ * 默认 []
19
+ */
20
+ routes?: (string | PreloadRoute)[];
21
+ /**
22
+ * 页面加载完成后延迟多少毫秒再触发状态显示
23
+ * 默认 2000ms
24
+ */
11
25
  delay?: number;
26
+ /**
27
+ * 是否开启调试日志与 window.__preloaderDebug 调试工具
28
+ * 默认:开发环境 true,生产环境 false
29
+ */
12
30
  debug?: boolean;
31
+ /**
32
+ * 是否在页面角落显示预加载状态提示
33
+ * 默认 true
34
+ */
35
+ showStatus?: boolean;
36
+ /**
37
+ * 状态提示的显示位置
38
+ * 默认 'bottom-right'
39
+ */
40
+ statusPosition?: StatusPosition;
41
+ /**
42
+ * 要排除预加载的路由路径列表(精确匹配或前缀匹配)
43
+ * 例:['/404', '/login', '/admin']
44
+ */
45
+ exclude?: string[];
13
46
  }
14
47
 
15
- declare function preloaderPlugin(options: PreloaderOptions): Plugin;
48
+ declare function preloaderPlugin(userOptions?: PreloaderOptions): Plugin;
16
49
 
17
- export { type PreloadRoute, type PreloaderOptions, preloaderPlugin as default };
50
+ export { type PreloadRoute, type PreloaderOptions, type StatusPosition, preloaderPlugin as default };
package/dist/index.d.ts CHANGED
@@ -1,17 +1,50 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
3
  interface PreloadRoute {
4
+ /** 路由路径,如 /dashboard。含 :param 的动态路由会被自动过滤 */
4
5
  path: string;
6
+ /** 可选。组件文件路径,不填则自动推断为 @/views/{path}/index.vue */
5
7
  component?: string;
8
+ /** 备注说明,仅用于日志显示 */
6
9
  reason?: string;
10
+ /** 优先级,数字越小越优先加载,默认 2 */
7
11
  priority?: number;
8
12
  }
13
+ type StatusPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";
9
14
  interface PreloaderOptions {
10
- routes: (string | PreloadRoute)[];
15
+ /**
16
+ * 要预加载的路由列表,支持字符串或对象格式
17
+ * 含 :param 的动态路由段会被自动过滤
18
+ * 默认 []
19
+ */
20
+ routes?: (string | PreloadRoute)[];
21
+ /**
22
+ * 页面加载完成后延迟多少毫秒再触发状态显示
23
+ * 默认 2000ms
24
+ */
11
25
  delay?: number;
26
+ /**
27
+ * 是否开启调试日志与 window.__preloaderDebug 调试工具
28
+ * 默认:开发环境 true,生产环境 false
29
+ */
12
30
  debug?: boolean;
31
+ /**
32
+ * 是否在页面角落显示预加载状态提示
33
+ * 默认 true
34
+ */
35
+ showStatus?: boolean;
36
+ /**
37
+ * 状态提示的显示位置
38
+ * 默认 'bottom-right'
39
+ */
40
+ statusPosition?: StatusPosition;
41
+ /**
42
+ * 要排除预加载的路由路径列表(精确匹配或前缀匹配)
43
+ * 例:['/404', '/login', '/admin']
44
+ */
45
+ exclude?: string[];
13
46
  }
14
47
 
15
- declare function preloaderPlugin(options: PreloaderOptions): Plugin;
48
+ declare function preloaderPlugin(userOptions?: PreloaderOptions): Plugin;
16
49
 
17
- export { type PreloadRoute, type PreloaderOptions, preloaderPlugin as default };
50
+ export { type PreloadRoute, type PreloaderOptions, type StatusPosition, preloaderPlugin as default };
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,261 +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
- }
45
-
46
- async start() {
47
- if (this.isPreloading) return
48
-
49
- this.isPreloading = true
50
- this.stats = {
51
- total: PRELOAD_ROUTES.length,
52
- completed: 0, failed: 0,
53
- startTime: Date.now(), endTime: 0
54
- }
55
-
56
- if (PRELOAD_OPTIONS.debug) {
57
- console.log(\`\u{1F680} [\u9884\u52A0\u8F7D] \u5F00\u59CB\u9884\u52A0\u8F7D \${PRELOAD_ROUTES.length} \u4E2A\u9875\u9762\`)
58
- }
59
-
60
- const sortedRoutes = [...PRELOAD_ROUTES].sort((a, b) => a.priority - b.priority)
61
-
62
- for (const route of sortedRoutes) {
63
- await this.preloadSingle(route)
64
- await this.sleep(100)
65
- }
66
-
67
- this.stats.endTime = Date.now()
68
- this.isPreloading = false
69
-
70
- if (PRELOAD_OPTIONS.debug) {
71
- console.log(\`\u{1F389} [\u9884\u52A0\u8F7D] \u5B8C\u6210! \u8017\u65F6 \${this.stats.endTime - this.stats.startTime}ms\`)
72
- }
73
- }
74
-
75
- async preloadSingle(route) {
76
- if (this.preloadedRoutes.has(route.path)) return
77
-
78
- try {
79
- const startTime = Date.now()
80
- let componentPath = route.component
81
-
82
- // \u5728\u5F00\u53D1\u73AF\u5883\u4E2D\uFF0CVite \u4F1A\u5904\u7406\u6A21\u5757\u89E3\u6790
83
- // \u6211\u4EEC\u9700\u8981\u4F7F\u7528\u6B63\u786E\u7684\u6A21\u5757 ID
84
- if (componentPath.startsWith('@/')) {
85
- // \u5BF9\u4E8E Vite\uFF0C@ \u522B\u540D\u5E94\u8BE5\u5728\u6784\u5EFA\u65F6\u5C31\u88AB\u89E3\u6790
86
- // \u4F46\u5728\u8FD0\u884C\u65F6\u6211\u4EEC\u9700\u8981\u4F7F\u7528\u5B9E\u9645\u7684\u8DEF\u5F84
87
- componentPath = componentPath.replace('@/', '/src/')
88
- }
89
-
90
- // \u786E\u4FDD\u8DEF\u5F84\u4EE5 / \u5F00\u5934\uFF08\u76F8\u5BF9\u4E8E\u9879\u76EE\u6839\u76EE\u5F55\uFF09
91
- if (!componentPath.startsWith('/') && !componentPath.startsWith('./')) {
92
- componentPath = '/' + componentPath
93
- }
94
-
95
- if (PRELOAD_OPTIONS.debug) {
96
- console.log(\`\u{1F50D} [\u9884\u52A0\u8F7D] \u5C1D\u8BD5\u52A0\u8F7D: \${componentPath}\`)
97
- }
98
-
99
- const module = await import(/* @vite-ignore */ componentPath)
100
- const loadTime = Date.now() - startTime
101
-
102
- this.preloadedRoutes.add(route.path)
103
- this.stats.completed++
104
-
105
- if (PRELOAD_OPTIONS.debug) {
106
- console.log(\`\u2705 [\u9884\u52A0\u8F7D] \${route.path} (\${loadTime}ms) - \${route.reason}\`)
107
- }
108
- } catch (error) {
109
- this.stats.failed++
110
- if (PRELOAD_OPTIONS.debug) {
111
- console.error(\`\u274C [\u9884\u52A0\u8F7D] \${route.path} \u5931\u8D25:\`, error)
112
- }
113
- }
114
- }
115
-
116
- sleep(ms) {
117
- return new Promise(resolve => setTimeout(resolve, ms))
118
- }
119
-
120
- isPreloaded(path) {
121
- return this.preloadedRoutes.has(path)
122
- }
123
-
124
- getStats() {
125
- return {
126
- ...this.stats,
127
- preloadedPaths: Array.from(this.preloadedRoutes),
128
- 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);
129
70
  }
130
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);
131
74
  }
132
-
133
- // \u{1F680} \u5168\u5C40\u5B9E\u4F8B
134
- const preloader = new PreloaderManager()
135
-
136
- // \u{1F6E0}\uFE0F \u5F00\u53D1\u73AF\u5883\u8C03\u8BD5\u5DE5\u5177
137
- if (PRELOAD_OPTIONS.debug) {
138
- window.preloaderDebug = {
139
- stats: () => preloader.getStats(),
140
- restart: () => preloader.start(),
141
- check: (path) => preloader.isPreloaded(path),
142
- help: () => console.log('\u{1F6E0}\uFE0F \u9884\u52A0\u8F7D\u8C03\u8BD5: stats() | restart() | check(path)')
143
- }
144
- console.log('\u{1F6E0}\uFE0F \u9884\u52A0\u8F7D\u8C03\u8BD5\u5DE5\u5177: window.preloaderDebug')
145
- }
146
-
147
- // \u{1F680} \u81EA\u52A8\u542F\u52A8 - \u7B49\u5F85 DOM \u52A0\u8F7D\u5B8C\u6210
148
- function autoStart() {
149
- if (document.readyState === 'loading') {
150
- document.addEventListener('DOMContentLoaded', () => {
151
- setTimeout(() => preloader.start(), PRELOAD_OPTIONS.delay)
152
- })
153
- } else {
154
- setTimeout(() => preloader.start(), PRELOAD_OPTIONS.delay)
155
- }
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;
156
102
  }
157
-
158
- // \u7ACB\u5373\u6267\u884C\u81EA\u52A8\u542F\u52A8
159
- autoStart()
160
-
161
- // \u5BFC\u51FA\u5230\u5168\u5C40\uFF08\u53EF\u9009\u4F7F\u7528\uFF09
162
- window.usePreloader = () => ({
163
- start: () => preloader.start(),
164
- isPreloaded: (path) => preloader.isPreloaded(path),
165
- getStats: () => preloader.getStats()
166
- })
167
-
168
- })();`;
103
+ ${debugCode}
104
+ ${statusUiCode}
105
+ })();
106
+ </script>`;
107
+ }
169
108
 
170
109
  // src/generator.ts
110
+ var import_vite = require("vite");
111
+ var import_node_path = __toESM(require("path"));
171
112
  var CodeGenerator = class {
172
113
  constructor(options) {
173
114
  this.options = options;
115
+ /** @ 别名对应的相对路径(相对于 viteRoot),默认 'src' */
116
+ this.srcAlias = "src";
117
+ this.viteRoot = process.cwd();
174
118
  }
175
119
  /**
176
- * 生成运行时代码
120
+ * 从 Vite resolvedConfig 中读取项目根目录和 @ 别名
121
+ * 必须在 configResolved hook 中调用
177
122
  */
178
- generateRuntime() {
179
- const routes = this.processRoutes();
180
- const options = this.processOptions();
181
- 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
+ }
182
134
  }
183
135
  /**
184
- * 处理路由配置
136
+ * 将用户路由配置解析为内部 ResolvedRoute 列表
137
+ * 同时过滤:含 :param 的动态路由段、exclude 列表匹配项
185
138
  */
186
- processRoutes() {
139
+ resolveRoutes() {
140
+ if (!Array.isArray(this.options.routes) || this.options.routes.length === 0) {
141
+ return [];
142
+ }
143
+ const { exclude } = this.options;
187
144
  return this.options.routes.map((route) => {
188
145
  if (typeof route === "string") {
189
- const componentPath2 = this.inferComponentPath(route);
190
146
  return {
191
147
  path: route,
192
- component: componentPath2,
148
+ component: this.inferComponentPath(route),
193
149
  reason: "\u81EA\u52A8\u63A8\u65AD\u7684\u9884\u52A0\u8F7D\u9875\u9762",
194
150
  priority: 2
195
151
  };
196
152
  }
197
- const componentPath = route.component || this.inferComponentPath(route.path);
198
153
  return {
199
154
  path: route.path,
200
- component: componentPath,
201
- reason: route.reason || "\u7528\u6237\u914D\u7F6E\u7684\u9884\u52A0\u8F7D\u9875\u9762",
202
- 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
203
158
  };
204
- });
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);
205
168
  }
206
169
  /**
207
- * 处理选项配置
170
+ * 根据 Rollup OutputBundle 中的 facadeModuleId 为各路由匹配实际 chunk 文件路径
171
+ * 仅在生产构建的 generateBundle hook 中调用
208
172
  */
209
- processOptions() {
210
- const isDev = process.env.NODE_ENV !== "production";
211
- return {
212
- delay: this.options.delay ?? 2e3,
213
- // 默认2秒
214
- showStatus: this.options.showStatus ?? true,
215
- // 默认显示状态
216
- statusPosition: this.options.statusPosition ?? "bottom-right",
217
- // 默认右下角
218
- debug: this.options.debug ?? isDev
219
- // 开发环境默认开启调试,生产环境默认关闭
220
- };
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
+ });
221
186
  }
222
187
  /**
223
- * 推断组件路径 - 使用 Vite 别名格式
188
+ * 生成 <link> 标签字符串(注入到 </body> 前)
189
+ * 生产:<link rel="prefetch"> 指向实际 chunk(浏览器空闲时拉取,完全不阻塞主线程)
190
+ * 开发:<link rel="modulepreload"> 指向源文件(Vite dev server 处理解析)
224
191
  */
225
- inferComponentPath(routePath) {
226
- const cleanPath = routePath.replace(/^\//, "");
227
- if (cleanPath.startsWith("demo/")) {
228
- 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
+ }
229
206
  }
230
- const pathSegments = cleanPath.split("/");
231
- return `@/views/${pathSegments.join("/")}/index.vue`;
207
+ return tags.join("\n");
232
208
  }
233
209
  /**
234
- * 生成注入到 HTML 头部的脚本
210
+ * 生成可选的状态 UI 和调试工具脚本
211
+ * showStatus=false 且 debug=false 时返回空字符串(0 运行时开销)
235
212
  */
236
- generateHtmlInject() {
237
- return `<script type="module">
238
- ${this.generateRuntime()}
239
- </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);
240
244
  }
241
245
  };
242
246
 
243
247
  // src/index.ts
244
- function preloaderPlugin(options) {
245
- let generator;
246
- const isDev = process.env.NODE_ENV !== "production";
247
- const finalOptions = {
248
- debug: isDev,
249
- // 开发环境默认开启调试
250
- delay: 2e3,
251
- // 默认2秒
252
- showStatus: true,
253
- // 默认显示状态
254
- statusPosition: "bottom-right",
255
- // 默认右下角
256
- ...options
257
- // 用户配置覆盖默认配置
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 ?? []
258
256
  };
257
+ const generator = new CodeGenerator(options);
258
+ let resolvedRoutes = [];
259
+ let isBuild = false;
259
260
  return {
260
261
  name: "vite-plugin-preloader",
261
- // 🎯 设置插件执行顺序
262
+ // post 确保在其他插件(如 @vitejs/plugin-vue)处理完后执行
262
263
  enforce: "post",
263
- configResolved() {
264
- generator = new CodeGenerator(finalOptions);
265
- if (finalOptions.debug) {
266
- 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
+ }
284
+ }
285
+ },
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
+ });
267
302
  }
268
303
  },
269
- // 🎨 HTML 转换 - 直接注入脚本到 HTML
304
+ // 注入 <link> 标签 + 可选运行时脚本到 </body> 前
270
305
  transformIndexHtml(html) {
271
- const inject = generator.generateHtmlInject();
272
- return html.replace("</head>", `${inject}
273
- </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;
274
318
  },
275
- // 🔥 HMR 支持
319
+ // HMR:vite.config 变更时触发完整刷新
276
320
  handleHotUpdate(ctx) {
277
321
  if (ctx.file.includes("vite.config")) {
278
- if (finalOptions.debug) {
279
- console.log("\u{1F504} [\u9884\u52A0\u8F7D\u63D2\u4EF6] \u914D\u7F6E\u5DF2\u66F4\u65B0");
280
- }
281
- ctx.server.ws.send({
282
- type: "full-reload"
283
- });
322
+ ctx.server.ws.send({ type: "full-reload" });
284
323
  return [];
285
324
  }
286
325
  return void 0;