@dev-to/vue-plugin 0.2.2 → 0.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/dist/index.js CHANGED
@@ -4,9 +4,11 @@ import { DEV_TO_BASE_PATH, DEV_TO_DEBUG_HTML_PATH, DEV_TO_DEBUG_JSON_PATH, DEV_T
4
4
  import { exec } from "node:child_process";
5
5
  import node_fs from "node:fs";
6
6
  import { createRequire } from "node:module";
7
+ import { fileURLToPath } from "node:url";
7
8
  import node_os from "node:os";
9
+ import typescript from "typescript";
8
10
  const PLUGIN_NAME = `${DEV_TO_NAMESPACE}_${DEV_TO_VUE_NAMESPACE}`;
9
- const constants_PLUGIN_LOG_PREFIX = `[${DEV_TO_NAMESPACE}:${DEV_TO_VUE_NAMESPACE}]`;
11
+ const PLUGIN_LOG_PREFIX = `[${DEV_TO_NAMESPACE}:${DEV_TO_VUE_NAMESPACE}]`;
10
12
  const STABLE_BASE_PATH = DEV_TO_BASE_PATH;
11
13
  const STABLE_DISCOVERY_PATH = DEV_TO_DISCOVERY_PATH;
12
14
  const STABLE_DEBUG_HTML_PATH = DEV_TO_DEBUG_HTML_PATH;
@@ -18,6 +20,529 @@ const STABLE_LOADER_UMD_PATH = DEV_TO_VUE_LOADER_UMD_PATH;
18
20
  const STABLE_LOADER_BASE_PATH = DEV_TO_VUE_LOADER_BASE_PATH;
19
21
  const EVENT_FULL_RELOAD = DEV_TO_VUE_EVENT_FULL_RELOAD;
20
22
  const EVENT_HMR_UPDATE = DEV_TO_VUE_EVENT_HMR_UPDATE;
23
+ function renderDebugHtml(params) {
24
+ const { resolvedDevComponentMap, entryPathMap = {}, audit, stats, originCandidates, actualPort, configFilePath } = params;
25
+ const { defaultEntryAbs, defaultEntryExists, componentMapCount } = audit;
26
+ const hasConfig = Object.keys(resolvedDevComponentMap).length > 0;
27
+ const isWildcardOnly = hasConfig && 1 === Object.keys(resolvedDevComponentMap).length && resolvedDevComponentMap['*'];
28
+ const projectRoot = configFilePath ? node_path.dirname(configFilePath) : process.cwd();
29
+ const projectRootDisplay = projectRoot.replace(/\\/g, '/');
30
+ const getShortPath = (absPath)=>{
31
+ try {
32
+ const rel = node_path.relative(projectRoot, absPath).replace(/\\/g, '/');
33
+ return rel.startsWith('.') ? rel : `./${rel}`;
34
+ } catch {
35
+ return absPath;
36
+ }
37
+ };
38
+ const toVsCodeUrl = (p)=>`vscode://file/${p.replace(/\\/g, '/')}`;
39
+ const escapeHtml = (s)=>s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
40
+ const annotatedConfigHtml = (()=>{
41
+ if (!configFilePath || !node_fs.existsSync(configFilePath)) return '';
42
+ try {
43
+ const content = node_fs.readFileSync(configFilePath, 'utf-8');
44
+ const lines = [];
45
+ const dim = (s)=>`<span class="cmt-dim">${escapeHtml(s)}</span>`;
46
+ const map = (s)=>`<span class="cmt-mapping">${escapeHtml(s)}</span>`;
47
+ lines.push(dim('/**'));
48
+ lines.push(dim(` * ${PLUGIN_NAME} 解析结果:`));
49
+ lines.push(dim(` * - 默认入口: ${getShortPath(defaultEntryAbs)} (${defaultEntryExists ? '存在' : '缺失'})`));
50
+ lines.push(dim(' * - 组件映射解析 (Resolved Component Map):'));
51
+ Object.entries(resolvedDevComponentMap).forEach(([name, entry])=>{
52
+ lines.push(map(` * - ${name} -> ${entry}`));
53
+ });
54
+ lines.push(dim(' */'));
55
+ return `${lines.join('\n')}\n\n${escapeHtml(content)}`;
56
+ } catch (e) {
57
+ return escapeHtml(`// ${PLUGIN_LOG_PREFIX} 无法读取配置文件: ${e}`);
58
+ }
59
+ })();
60
+ return `<!doctype html>
61
+ <html lang="zh-CN">
62
+ <head>
63
+ <meta charset="utf-8">
64
+ <meta name="viewport" content="width=device-width,initial-scale=1">
65
+ <title>${PLUGIN_NAME} Debug</title>
66
+ <style>
67
+ :root { --p: #42b883; --t: #1e293b; --m: #64748b; --b: #e2e8f0; --r: 12px; }
68
+ * { box-sizing: border-box; }
69
+ body { font-family: -apple-system, system-ui, sans-serif; background: #f8fafc; color: #1a202c; margin: 0; padding: 24px; line-height: 1.6; }
70
+ .container { max-width: 1000px; margin: 0 auto; }
71
+ .header, .card { background: #fff; border-radius: var(--r); border: 1px solid var(--b); margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
72
+ .header { padding: 20px 32px; display: flex; justify-content: space-between; align-items: center; }
73
+ .card { padding: 24px; }
74
+ .header h1 { margin: 0; font-size: 24px; color: var(--t); }
75
+ .header p, .muted { color: var(--m); font-size: 14px; margin: 4px 0 0; }
76
+ .header-status { display: flex; gap: 12px; flex-wrap: wrap; }
77
+ .status-pill { border: 1px solid var(--b); font-weight: 500; display: flex; align-items: center; background: #f8fafc; padding: 6px 16px; border-radius: 20px; font-size: 13px; color: #475569; }
78
+ .status-pill b { color: var(--t); margin-left: 6px; }
79
+ .card h3 { margin: 0 0 20px; font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 8px; color: var(--t); }
80
+ .card h3::before { content: ''; width: 3px; height: 16px; background: var(--p); border-radius: 2px; }
81
+
82
+ .alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; font-size: 13px; display: flex; align-items: center; gap: 10px; border: 1px solid transparent; }
83
+ .alert-info { background: #f0fdf4; color: #166534; border-color: #bbf7d0; }
84
+ .alert-error { background: #fef2f2; color: #991b1b; border-color: #fee2e2; }
85
+ .alert-warning { background: #fffbeb; color: #92400e; border-color: #fef3c7; }
86
+
87
+ .setup-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 10px; margin: 12px 0 20px; }
88
+ .setup-card {
89
+ background: #fff; border: 1.5px solid var(--b); border-radius: 8px; padding: 10px 14px;
90
+ transition: all .2s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; display: flex; flex-direction: column;
91
+ }
92
+ .setup-card:hover { border-color: var(--p); background: #f0fdf4; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
93
+ .setup-card.active { border-color: var(--p); background: #f0fdf4; }
94
+ .setup-card .type { font-size: 9px; text-transform: uppercase; color: var(--m); font-weight: 700; margin-bottom: 6px; display: inline-flex; background: #f1f5f9; padding: 1px 6px; border-radius: 4px; width: fit-content; }
95
+ .setup-card.active .type { background: #dcfce7; color: var(--p); }
96
+ .setup-card .url { font-family: SFMono-Regular, Consolas, monospace; font-size: 13px; color: var(--t); font-weight: 600; margin-bottom: 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; width: 100%; }
97
+ .setup-card .action { font-size: 11px; color: var(--m); display: flex; align-items: center; gap: 4px; margin-top: auto; }
98
+ .setup-card:hover .action, .setup-card.active .action { color: var(--p); }
99
+
100
+ .manual-box { background: #fcfdfe; border: 1px solid var(--b); border-radius: 8px; margin-top: 16px; overflow: hidden; }
101
+ .manual-header { padding: 10px 16px; display: flex; justify-content: space-between; align-items: center; color: var(--m); font-size: 11px; font-weight: 600; background: #f8fafc; border-bottom: 1px solid var(--b); }
102
+ .copy-btn-link { background: #fff; border: 1px solid var(--b); color: var(--t); padding: 3px 10px; border-radius: 4px; cursor: pointer; font-size: 11px; transition: .2s; }
103
+ .copy-btn-link:hover { border-color: var(--p); color: var(--p); }
104
+ #fullCmdPreview { margin: 0; padding: 14px 16px; border: none; background: transparent; }
105
+
106
+ .info-grid { display: grid; grid-template-columns: 100px 1fr; gap: 12px 16px; margin-bottom: 24px; align-items: baseline; font-size: 13px; }
107
+ .info-label { color: var(--m); font-weight: 500; }
108
+
109
+ table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 14px; border: 1px solid #f1f5f9; border-radius: 8px; overflow: hidden; }
110
+ th, td { text-align: left; padding: 14px 16px; border-bottom: 1px solid #f1f5f9; }
111
+ th { background: #f8fafc; color: var(--m); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .05em; }
112
+ code, pre { font-family: SFMono-Regular, Consolas, monospace; font-size: .9em; }
113
+ code { background: #f1f5f9; padding: 3px 6px; border-radius: 4px; color: #475569; font-weight: 500; }
114
+ .code-name { color: #42b883; background: #f0fdf4; border: 1px solid #bbf7d0; }
115
+ .link-code { color: #42b883; text-decoration: none; padding: 2px 6px; border-radius: 4px; transition: .2s; display: inline-flex; align-items: center; gap: 4px; background: #f0fdf4; border: 1px solid #bbf7d0; }
116
+ .link-code:hover { background: #dcfce7; color: #166534; border-color: #86efac; }
117
+ .link-code code { background: 0 0; padding: 0; color: inherit; }
118
+ .link-code::after { content: '\\2197'; font-size: 11px; opacity: .6; }
119
+ details { margin-top: 16px; border: 1px solid #f1f5f9; border-radius: 10px; padding: 12px 16px; background: #fafbfc; }
120
+ summary { cursor: pointer; color: #475569; font-size: 14px; font-weight: 600; outline: 0; }
121
+ summary:hover { color: var(--p); }
122
+
123
+ pre { background: #f0fdf4; color: #2d3748; padding: 14px 16px; border-radius: 8px; font-size: 12px; border: 1px solid var(--b); margin: 12px 0; line-height: 1.6; overflow-x: auto; }
124
+ .cmt { color: #718096; font-style: italic; }
125
+ .kw { color: #42b883; font-weight: 600; }
126
+ .str { color: #059669; }
127
+ .val { color: #d97706; }
128
+
129
+ .stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
130
+ .stat-card { background: #f8fafc; padding: 20px; border-radius: 10px; text-align: center; border: 1px solid #f1f5f9; }
131
+ .stat-card .value { font-size: 24px; font-weight: 700; color: var(--p); margin-bottom: 4px; }
132
+ .stat-card .label { font-size: 12px; color: var(--m); text-transform: uppercase; font-weight: 600; }
133
+ .parameter-item { margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px dashed var(--b); }
134
+ .parameter-item:last-child { border-bottom: none; }
135
+ .parameter-name { font-weight: 600; color: var(--t); margin-bottom: 6px; display: block; font-size: 14px; }
136
+ .info-value, .parameter-info { font-size: 13px; color: #4a5568; line-height: 1.7; }
137
+ .cmt-dim { opacity: 0.4; }
138
+ .cmt-mapping { color: var(--p); font-weight: 600; }
139
+
140
+ /* 响应式优化:移动端 (480px以下) */
141
+ @media (max-width: 480px) {
142
+ body { padding: 12px; }
143
+ .header { padding: 16px; flex-direction: column; align-items: flex-start; gap: 16px; }
144
+ .header-status { width: 100%; gap: 8px; }
145
+ .status-pill { padding: 4px 12px; font-size: 12px; }
146
+ .card { padding: 16px; }
147
+ .info-grid { grid-template-columns: 1fr; gap: 4px; }
148
+ .info-label { font-size: 12px; margin-bottom: 2px; }
149
+ .stats-grid { grid-template-columns: 1fr; }
150
+ .stat-card { padding: 12px; }
151
+ .stat-card .value { font-size: 20px; }
152
+ table { display: table; width: 100%; border-radius: 6px; }
153
+ th, td { padding: 10px 8px; font-size: 12px; white-space: normal; overflow-wrap: anywhere; word-break: normal; }
154
+ th br, th .muted { display: none; }
155
+ pre { padding: 10px; font-size: 11px; }
156
+ .setup-grid { grid-template-columns: 1fr 1fr; gap: 8px; }
157
+ .setup-card { padding: 8px 10px; }
158
+ .setup-card .url { font-size: 12px; margin-bottom: 4px; }
159
+ .setup-card .action { font-size: 10px; }
160
+ .build-grid { grid-template-columns: 1fr !important; }
161
+ }
162
+
163
+ .build-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; }
164
+ .build-card { background:#f8fafc; padding:16px; border-radius:10px; border:1px solid #edf2f7; }
165
+ </style>
166
+ </head>
167
+ <body>
168
+ <div class="container">
169
+ <div class="header">
170
+ <div class="header-main">
171
+ <h1>${PLUGIN_NAME}</h1>
172
+ <p>Vue 组件开发调试面板 - Vite ESM + HMR</p>
173
+ </div>
174
+ <div class="header-status">
175
+ <div class="status-pill">组件<b>${componentMapCount}</b></div>
176
+ <div class="status-pill">入口<b style="color: ${defaultEntryExists ? '#10b981' : '#ef4444'}">${defaultEntryExists ? '✓' : '✗'}</b></div>
177
+ <div class="status-pill">端口<b id="actualPortDisplay">-</b></div>
178
+ </div>
179
+ </div>
180
+
181
+ <div class="card">
182
+ <h3>环境快速设置</h3>
183
+ <div class="alert alert-info">
184
+ <span></span>
185
+ <div>在宿主 Electron 的控制台 (DevTools Console) 执行下方卡片中的命令,即可完成环境切换。</div>
186
+ </div>
187
+
188
+ <div id="originGrid" class="setup-grid"></div>
189
+
190
+ <div class="manual-box">
191
+ <div class="manual-header">
192
+ <span>手动复制完整脚本</span>
193
+ <button id="copyFullCmd" class="copy-btn-link">复制原始命令</button>
194
+ </div>
195
+ <pre id="fullCmdPreview"></pre>
196
+ </div>
197
+ </div>
198
+
199
+ <div class="card">
200
+ <h3>当前组件配置</h3>
201
+
202
+ <div class="info-grid">
203
+ <div class="info-label">项目目录:</div>
204
+ <div class="info-value">
205
+ <a href="${toVsCodeUrl(projectRoot)}" class="link-code" title="点击在 IDE 中打开"><code>${escapeHtml(projectRootDisplay)}</code></a>
206
+ </div>
207
+ <div class="info-label">配置文件:</div>
208
+ <div class="info-value">
209
+ ${configFilePath ? `
210
+ <a href="${toVsCodeUrl(configFilePath)}" class="link-code" title="点击在 IDE 中打开"><code>${escapeHtml(node_path.basename(configFilePath))}</code></a>
211
+ ${annotatedConfigHtml ? `
212
+ <details style="margin-top: 8px; border: none; padding: 0; background: transparent; box-shadow: none;">
213
+ <summary style="font-size: 12px; color: var(--p); font-weight: 500;">查看配置源码与解析结果</summary>
214
+ <pre style="margin-top: 8px; max-height: 400px; overflow: auto; background: #f1f5f9; border-color: #cbd5e1; font-size: 11px; padding: 12px; border-radius: 6px;">${annotatedConfigHtml}</pre>
215
+ </details>
216
+ ` : ''}
217
+ ` : '<span class="muted">未找到</span>'}
218
+ </div>
219
+ </div>
220
+
221
+ ${!hasConfig || isWildcardOnly ? `
222
+ <div class="alert alert-info">
223
+ <span></span>
224
+ <div>
225
+ <b>全局通配模式已启用</b>
226
+ <div style="font-size: 13px; margin-top: 2px;">Map 中包含通配符 "*"。所有组件请求将默认加载入口。构建前请在 <code>vite.config.ts</code> 中显式指定组件映射。</div>
227
+ </div>
228
+ </div>
229
+ ` : ''}
230
+
231
+ ${!defaultEntryExists ? `
232
+ <div class="alert alert-error">
233
+ <span></span>
234
+ <div>
235
+ <b>默认入口文件缺失</b>
236
+ <div style="font-size: 13px; margin-top: 2px;">找不到路径:<a href="${toVsCodeUrl(defaultEntryAbs)}" class="link-code"><code>${escapeHtml(getShortPath(defaultEntryAbs))}</code></a></div>
237
+ </div>
238
+ </div>
239
+ ` : ''}
240
+
241
+ ${hasConfig ? `
242
+ <table>
243
+ <thead><tr><th>组件名称 <small class="muted">(Component Name)</small></th><th>映射入口 <small class="muted">(Short Path)</small></th><th>包装地址 <small class="muted">(UMD Wrapper)</small></th></tr></thead>
244
+ <tbody>
245
+ ${Object.entries(resolvedDevComponentMap).map(([name, entry])=>{
246
+ const abs = entryPathMap[name];
247
+ const displayPath = abs ? getShortPath(abs) : entry;
248
+ const wrapperUrl = (originCandidates[0] || 'http://localhost:5173') + '/__dev_to__/vue/loader/' + name + '.js';
249
+ const entryHtml = abs ? '<a href="' + toVsCodeUrl(abs) + '" class="link-code" title="点击在 IDE 中打开"><code>' + escapeHtml(displayPath) + '</code></a>' : '<code>' + escapeHtml(entry) + '</code>';
250
+ return '<tr><td><code class="code-name">' + name + "</code></td><td>" + entryHtml + '</td><td><div style="display: flex; align-items: center; gap: 6px;"><code style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px;">' + escapeHtml(wrapperUrl) + '</code><button class="copy-wrapper-btn" data-url="' + wrapperUrl + '" style="padding: 2px 8px; font-size: 11px; border: 1px solid var(--b); background: #fff; border-radius: 4px; cursor: pointer; color: var(--t); transition: .2s;" title="复制包装地址">Copy</button></div></td></tr>';
251
+ }).join('')}
252
+ </tbody>
253
+ </table>
254
+ ` : '<div class="alert alert-warning">未发现任何配置组件</div>'}
255
+
256
+ <details>
257
+ <summary>插件参数与配置说明 (Plugin API)</summary>
258
+ <div class="parameter-desc">
259
+ <div style="margin-bottom: 24px;">
260
+ <pre style="background: #f0fdf4; color: #166534; border-color: #bbf7d0; font-size: 14px; font-weight: 600;">devToVuePlugin(components?, options?)</pre>
261
+ <div class="muted" style="margin-top: 8px;">
262
+ 支持单组件简写、对象全量映射,以及透传 Vite 原生配置。
263
+ </div>
264
+ </div>
265
+
266
+ <div class="parameter-item">
267
+ <span class="parameter-name">1. components (第一个参数)</span>
268
+ <div class="parameter-info">
269
+ 定义组件名与本地入口文件的映射:
270
+ <ul style="margin-top: 8px;">
271
+ <li><b>单组件简写</b>:<code class="val">'Demo'</code> -> 自动关联 <code>{ Demo: '/' }</code>。</li>
272
+ <li><b>通配符映射</b>:<code class="val">'*'</code> -> 匹配所有组件名。支持 <code class="val">'/'</code> (默认入口) 或具体的相对/绝对路径。</li>
273
+ <li><b>多组件映射</b>:支持具体的相对/绝对路径。</li>
274
+ </ul>
275
+ <pre><span class="cmt">// Option 1: Shorthand (Default)</span>
276
+ devToVuePlugin(<span class="str">'Demo'</span>)
277
+
278
+ <span class="cmt">// Option 2: Explicit Mapping with Wildcard</span>
279
+ devToVuePlugin({
280
+ <span class="str">'*'</span>: <span class="str">'/'</span>, <span class="cmt">// Wildcard to default entry</span>
281
+ <span class="str">'Card'</span>: <span class="str">'src/Card.vue'</span> <span class="cmt">// Specific file</span>
282
+ })</pre>
283
+ <div class="muted" style="font-size: 12px; margin-top: 8px; background: #fffbeb; padding: 8px 12px; border-radius: 6px; border: 1px solid #fef3c7; color: #92400e;">
284
+ <b>关于默认入口 (/)</b>:表示使用工程默认入口文件。查找顺序:优先 <code>src/App.vue</code>,其次 <code>src/index.{vue,tsx,jsx}</code>。
285
+ </div>
286
+ </div>
287
+ </div>
288
+ <div class="parameter-item">
289
+ <span class="parameter-name">2. options (第二个参数)</span>
290
+ <div class="parameter-info">
291
+ 高级配置(深度合并):
292
+ <ul style="margin-top: 8px;">
293
+ <li><code class="kw">css</code>:
294
+ <ul>
295
+ <li><b>默认值:</b><code>{ modules: { generateScopedName: <span class="str">'[name]__[local]___[hash:base64:5]'</span> } }</code>。</li>
296
+ <li>传 <code class="kw">false</code> 禁用配置;传对象则进行深度合并。</li>
297
+ <li>详细配置请参考 <a href="https://cn.vite.dev/config/shared-options#css-modules" target="_blank" style="color:#42b883;">Vite CSS 官方文档</a></li>
298
+ </ul>
299
+ <pre><span class="cmt">// Disable plugin CSS config or provide custom overrides</span>
300
+ devToVuePlugin(<span class="str">'Demo'</span>, { css: <span class="kw">false</span> })
301
+ devToVuePlugin(<span class="str">'Demo'</span>, { css: { ... } })</pre>
302
+ </li>
303
+ <li style="margin-top: 12px;"><code class="kw">build</code>:
304
+ <ul>
305
+ <li><b>仅在 lib 构建模式下生效</b>。内置默认值:</li>
306
+ <pre style="font-size: 11px; color: #4a5568;">formats: [<span class="str">'umd'</span>], fileName: <span class="str">'[name].js'</span>, inlineDynamicImports: <span class="kw">true</span>,
307
+ external: [<span class="str">'vue'</span>],
308
+ globals: { vue: <span class="str">'Vue'</span> }</pre>
309
+ <li>合并规则:用户配置覆盖默认项。</li>
310
+ <li>详细配置请参考 <a href="https://cn.vite.dev/config/build-options" target="_blank" style="color:#42b883;">Vite 构建官方文档</a></li>
311
+ </ul>
312
+ <pre><span class="cmt">// Example: Disable asset inlining during build</span>
313
+ devToVuePlugin(<span class="str">'Demo'</span>, {
314
+ build: { assetsInlineLimit: <span class="val">0</span> }
315
+ })</pre>
316
+ </li>
317
+ <li style="margin-top: 12px;"><code class="kw">open</code>:
318
+ <ul>
319
+ <li><b>默认值:</b><code class="kw">false</code>。</li>
320
+ <li>是否在启动 Vite 开发服务器后自动在浏览器中打开此调试面板。</li>
321
+ </ul>
322
+ <pre><span class="cmt">// Enable auto-open</span>
323
+ devToVuePlugin(<span class="str">'Demo'</span>, { open: <span class="kw">true</span> })</pre>
324
+ </li>
325
+ </ul>
326
+ </div>
327
+ </div>
328
+ </div>
329
+ </details>
330
+ </div>
331
+
332
+ <div class="card">
333
+ <h3>UMD 动态包装器 (Auto-Generated Wrapper)</h3>
334
+ <p class="muted">无需额外配置,每个组件都自动生成一个轻量级 UMD 包装器,可直接在无 Vue 框架支持的宿主环境中使用。</p>
335
+
336
+ <div class="info-grid">
337
+ <div class="info-label">端点:</div>
338
+ <div class="info-value"><code>/__dev_to__/vue/loader/{ComponentName}.js</code></div>
339
+ <div class="info-label">作用:</div>
340
+ <div class="info-value">自动将组件导出为 Vue 组件实例,无需宿主集成 @dev-to/vue-loader</div>
341
+ <div class="info-label">依赖:</div>
342
+ <div class="info-value"><code>vue@3</code> (CDN 或本地)</div>
343
+ </div>
344
+
345
+ <details>
346
+ <summary>包装器工作原理与集成示例</summary>
347
+ <div style="margin-top: 12px;">
348
+ <h4 style="color: var(--t); font-size: 14px; margin-top: 0; margin-bottom: 8px;">什么是包装器?</h4>
349
+ <p class="muted" style="margin-bottom: 12px;">
350
+ 包装器是一个自动生成的 UMD 模块,它包装了原始的 render 函数并导出为 Vue 组件。
351
+ 这样,无论宿主是否集成了 VueLoader,都能直接作为 Vue 组件使用。
352
+ </p>
353
+
354
+ <h4 style="color: var(--t); font-size: 14px; margin-top: 16px; margin-bottom: 8px;">集成方式</h4>
355
+ <pre style="font-size: 12px; line-height: 1.7;">
356
+ <span class="cmt">// 1. 加载 Vue</span>
357
+ <span class="kw">&lt;script&gt;</span> <span class="kw">src</span>=<span class="str">"https://unpkg.com/vue@3/dist/vue.global.prod.js"</span> <span class="kw">&lt;/script&gt;</span>
358
+
359
+ <span class="cmt">// 2. 加载包装器脚本</span>
360
+ <span class="kw">&lt;script&gt;</span> <span class="kw">src</span>=<span class="str">"\${originCandidates[0] || 'http://localhost:5173'}/__dev_to__/vue/loader/{ComponentName}.js"</span> <span class="kw">&lt;/script&gt;</span>
361
+
362
+ <span class="cmt">// 3. 直接作为 Vue 组件使用</span>
363
+ <span class="kw">const</span> app = Vue.createApp(window.ComponentName);
364
+ app.mount(<span class="str">'#app'</span>);
365
+
366
+ <span class="cmt">// 或在宿主 Vue 应用中使用</span>
367
+ <span class="kw">const</span> Component = window.ComponentName;
368
+ app.component(<span class="str">'MyComponent'</span>, Component);</pre>
369
+
370
+ <h4 style="color: var(--t); font-size: 14px; margin-top: 16px; margin-bottom: 8px;">关键特性</h4>
371
+ <ul class="muted" style="margin: 8px 0; padding-left: 20px;">
372
+ <li><b>零配置</b>:自动为每个组件生成包装器,无需手动编写</li>
373
+ <li><b>兼容现有宿主</b>:支持 CommonJS、AMD、浏览器全局三种模式</li>
374
+ <li><b>自动依赖管理</b>:若未加载 Vue,包装器会自动从 CDN 加载(可配置)</li>
375
+ <li><b>轻量级</b>:仅包含加载逻辑,核心渲染由 VueLoader 负责</li>
376
+ </ul>
377
+ </div>
378
+ </details>
379
+ </div>
380
+
381
+ <div class="card">
382
+ <h3>构建与部署</h3>
383
+ <p class="muted">执行 <code>dev-to build</code>(等价于 <code>vite build --mode lib</code>)将组件打包为 UMD 格式以供发布。</p>
384
+
385
+ <div class="build-grid">
386
+ <div class="build-card">
387
+ <div style="font-weight:600; font-size:13px; margin-bottom:8px; color: var(--t);">输出结构 (Output)</div>
388
+ <pre style="margin:0; padding:0; background:transparent; border:none; font-size:12px; color:#4a5568;">
389
+ JS: <span class="str">dist/&lt;name&gt;/&lt;name&gt;.js</span>
390
+ CSS: <span class="str">dist/&lt;name&gt;/&lt;name&gt;.css</span></pre>
391
+ </div>
392
+ <div class="build-card">
393
+ <div style="font-weight:600; font-size:13px; margin-bottom:8px; color: var(--t);">外部依赖 (External)</div>
394
+ <pre style="margin:0; padding:0; background:transparent; border:none; font-size:12px; color:#4a5568;">
395
+ <span class="kw">vue</span> -> <span class="val">Vue</span></pre>
396
+ </div>
397
+ </div>
398
+
399
+ <details>
400
+ <summary>构建导出 (Export) 智能分析逻辑</summary>
401
+ <div style="margin-top: 12px; font-size: 13px;">
402
+ <p class="muted">插件会使用 AST 分析入口文件,确保 UMD 包具备正确的导出:</p>
403
+ <ul class="muted" style="line-height: 1.8;">
404
+ <li>如果有 <code>export default</code>,直接作为组件入口。</li>
405
+ <li>如果没有 Default 但只有一个命名导出,自动将其关联为 Default。</li>
406
+ <li>如果有多个命名导出,必须有一个与 <code>componentName</code> 同名,否则会报错提醒。</li>
407
+ </ul>
408
+ </div>
409
+ </details>
410
+ </div>
411
+
412
+ <div class="card">
413
+ <h3>运行指标 & 参考</h3>
414
+ <div class="stats-grid">
415
+ <div class="stat-card"><div class="value">${stats.contract.count}</div><div class="label">Contract 请求</div></div>
416
+ <div class="stat-card"><div class="value">${stats.init.count}</div><div class="label">Init 注入</div></div>
417
+ <div class="stat-card"><div class="value">${stats.runtime.count}</div><div class="label">Runtime 加载</div></div>
418
+ </div>
419
+
420
+ <details>
421
+ <summary>技术端点与 HMR 事件 (Internal Reference)</summary>
422
+ <div style="margin-top: 12px;">
423
+ <pre style="font-size: 12px; line-height: 1.7;">
424
+ <span class="kw">Endpoints:</span>
425
+ - Contract: <span class="str">${STABLE_CONTRACT_PATH}</span>
426
+ - Init: <span class="str">${STABLE_INIT_PATH}</span>
427
+ - Runtime: <span class="str">${STABLE_VUE_RUNTIME_PATH}</span>
428
+
429
+ <span class="kw">HMR Events:</span>
430
+ - Reload: <span class="val">${EVENT_FULL_RELOAD}</span>
431
+ - Update: <span class="val">${EVENT_HMR_UPDATE}</span></pre>
432
+ <p class="muted" style="font-size: 12px; margin-top: 12px; background: #fffbeb; padding: 10px 14px; border-radius: 6px; border: 1px solid #fef3c7; color: #92400e;">
433
+ <b>重要提示:</b>在 Electron 环境下,静态资源必须通过 <code>import</code> 引入才能被桥接插件正确拦截和路径重写。
434
+ </p>
435
+ </div>
436
+ </details>
437
+ </div>
438
+
439
+ <div style="text-align: center; margin-top: 32px; padding-bottom: 24px;">
440
+ <a href="${STABLE_DEBUG_JSON_PATH}" target="_blank" style="font-size: 13px; color: #42b883; text-decoration: none; font-weight: 500;">查看原始协议 JSON 数据 -></a>
441
+ </div>
442
+ </div>
443
+
444
+ <script>
445
+ (function() {
446
+ const serverOrigins = ${JSON.stringify(originCandidates)};
447
+ const current = location.origin;
448
+ const origins = [...serverOrigins];
449
+
450
+ // 确保当前访问地址也在候选列表中
451
+ if (!origins.includes(current)) origins.unshift(current);
452
+
453
+ const seen = new Set();
454
+ const uniqueOrigins = origins.filter(o => {
455
+ if (seen.has(o)) return false;
456
+ seen.add(o);
457
+ return true;
458
+ });
459
+
460
+ const grid = document.getElementById('originGrid');
461
+ const fullCmdPreview = document.getElementById('fullCmdPreview');
462
+ const copyFullBtn = document.getElementById('copyFullCmd');
463
+
464
+ function makeCmd(origin) {
465
+ return "localStorage.setItem('VITE_DEV_SERVER_ORIGIN', '" + origin + "'); location.reload();";
466
+ }
467
+
468
+ function selectOrigin(origin, card) {
469
+ // 更新卡片激活状态
470
+ document.querySelectorAll('.setup-card').forEach(c => c.classList.remove('active'));
471
+ if (card) card.classList.add('active');
472
+
473
+ // 更新下方预览脚本
474
+ fullCmdPreview.textContent = makeCmd(origin);
475
+ }
476
+
477
+ function copy(text, successCb) {
478
+ const ta = document.createElement('textarea');
479
+ ta.value = text;
480
+ document.body.appendChild(ta);
481
+ ta.select();
482
+ document.execCommand('copy');
483
+ document.body.removeChild(ta);
484
+ if (successCb) successCb();
485
+ }
486
+
487
+ uniqueOrigins.forEach(origin => {
488
+ const isLocal = origin.includes('localhost') || origin.includes('127.0.0.1');
489
+ const displayUrl = origin.indexOf('://') > -1 ? origin.split('://')[1] : origin;
490
+ const card = document.createElement('div');
491
+ card.className = 'setup-card' + (origin === current ? ' active' : '');
492
+ card.innerHTML =
493
+ '<span class="type">' + (isLocal ? '本地回路 (Local)' : '局域网 (LAN)') + '</span>' +
494
+ '<span class="url">' + displayUrl + '</span>' +
495
+ '<div class="action">点击复制切换命令</div>';
496
+ card.onclick = () => {
497
+ selectOrigin(origin, card);
498
+ copy(makeCmd(origin), () => {
499
+ const actionEl = card.querySelector('.action');
500
+ const originalAction = actionEl.innerHTML;
501
+ actionEl.innerHTML = '<span>OK</span> 命令已复制成功';
502
+ card.style.borderColor = '#10b981';
503
+ setTimeout(() => {
504
+ actionEl.innerHTML = originalAction;
505
+ card.style.borderColor = '';
506
+ }, 2000);
507
+ });
508
+ };
509
+ grid.appendChild(card);
510
+ });
511
+
512
+ selectOrigin(current, null); // 初始化预览
513
+ copyFullBtn.onclick = () => copy(fullCmdPreview.textContent, () => {
514
+ copyFullBtn.textContent = 'OK 已成功复制';
515
+ setTimeout(() => { copyFullBtn.textContent = '复制原始命令'; }, 2000);
516
+ });
517
+
518
+ // 绑定包装地址复制按钮事件
519
+ document.querySelectorAll('.copy-wrapper-btn').forEach(btn => {
520
+ btn.onclick = (e) => {
521
+ e.preventDefault();
522
+ const url = btn.getAttribute('data-url');
523
+ copy(url, () => {
524
+ const originalText = btn.textContent;
525
+ btn.textContent = 'OK';
526
+ btn.style.borderColor = '#10b981';
527
+ btn.style.color = '#10b981';
528
+ setTimeout(() => {
529
+ btn.textContent = originalText;
530
+ btn.style.borderColor = '';
531
+ btn.style.color = '';
532
+ }, 1500);
533
+ });
534
+ };
535
+ btn.onmouseover = () => { btn.style.borderColor = 'var(--p)'; btn.style.color = 'var(--p)'; };
536
+ btn.onmouseout = () => { btn.style.borderColor = ''; btn.style.color = ''; };
537
+ });
538
+
539
+ const serverActualPort = ${'number' == typeof actualPort ? actualPort : 'null'};
540
+ document.getElementById('actualPortDisplay').textContent = serverActualPort || location.port || '-';
541
+ })();
542
+ </script>
543
+ </body>
544
+ </html>`;
545
+ }
21
546
  function getLanIPv4Hosts() {
22
547
  const nets = node_os.networkInterfaces();
23
548
  const out = new Set();
@@ -26,6 +551,574 @@ function getLanIPv4Hosts() {
26
551
  }
27
552
  return Array.from(out);
28
553
  }
554
+ function toFsPathFromViteEntry(entry) {
555
+ if (!entry.startsWith('/@fs')) return null;
556
+ let p = entry.slice(4);
557
+ if (p.startsWith('/') && /\/[A-Za-z]:\//.test(p)) p = p.slice(1);
558
+ return p;
559
+ }
560
+ function tryResolveWithExtensions(p) {
561
+ const exts = [
562
+ '.vue',
563
+ '.tsx',
564
+ '.jsx',
565
+ '.ts',
566
+ '.js'
567
+ ];
568
+ if (node_fs.existsSync(p)) return p;
569
+ const parsed = node_path.parse(p);
570
+ if (parsed.ext) for (const ext of exts){
571
+ const cand = node_path.join(parsed.dir, `${parsed.name}${ext}`);
572
+ if (node_fs.existsSync(cand)) return cand;
573
+ }
574
+ else for (const ext of exts){
575
+ const cand = `${p}${ext}`;
576
+ if (node_fs.existsSync(cand)) return cand;
577
+ }
578
+ return null;
579
+ }
580
+ function resolveEntryAbsPath(rootDir, entry, defaultEntryAbs, fallbackRoot) {
581
+ const tryResolveWithBase = (baseDir)=>{
582
+ if ('/' === entry) {
583
+ if (!defaultEntryAbs) throw new Error(`${PLUGIN_LOG_PREFIX} defaultEntryAbs is required when entry is '/'`);
584
+ return defaultEntryAbs;
585
+ }
586
+ const fsPath = toFsPathFromViteEntry(entry);
587
+ if (fsPath) return tryResolveWithExtensions(fsPath);
588
+ if (node_path.isAbsolute(entry)) return tryResolveWithExtensions(entry);
589
+ if (entry.startsWith('/')) {
590
+ const maybe = node_path.resolve(baseDir, entry.slice(1));
591
+ return tryResolveWithExtensions(maybe);
592
+ }
593
+ const rel = node_path.resolve(baseDir, entry);
594
+ return tryResolveWithExtensions(rel);
595
+ };
596
+ const resolved = tryResolveWithBase(rootDir);
597
+ if (resolved) return resolved;
598
+ if (fallbackRoot && fallbackRoot !== rootDir) return tryResolveWithBase(fallbackRoot);
599
+ return resolved;
600
+ }
601
+ function isLibBuild(env) {
602
+ return env?.command === 'build' && env?.mode === 'lib';
603
+ }
604
+ function toSafeOutDirName(componentName) {
605
+ return componentName.replace(/[\\/]/g, '_').replace(/\.\./g, '_');
606
+ }
607
+ function toSafeUmdName(componentName) {
608
+ let s = componentName.replace(/[^A-Za-z0-9_$]+/g, '_');
609
+ if (!s) s = 'ViteDevComponent';
610
+ if (/^\d/.test(s)) s = `_${s}`;
611
+ return s;
612
+ }
613
+ function isValidJsIdentifier(name) {
614
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
615
+ }
616
+ function analyzeExports(filePath) {
617
+ if (!node_fs.existsSync(filePath)) return {
618
+ hasDefault: false,
619
+ namedExports: []
620
+ };
621
+ const content = node_fs.readFileSync(filePath, 'utf-8');
622
+ const namedExports = [];
623
+ let hasDefault = false;
624
+ const ext = node_path.extname(filePath).toLowerCase();
625
+ if ('.vue' === ext) {
626
+ hasDefault = true;
627
+ const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
628
+ if (scriptMatch) {
629
+ const scriptContent = scriptMatch[1];
630
+ const namedRegex = /export\s+(?:const|let|var|function|class)\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
631
+ let match;
632
+ while(null !== (match = namedRegex.exec(scriptContent)))namedExports.push(match[1]);
633
+ }
634
+ return {
635
+ hasDefault,
636
+ namedExports
637
+ };
638
+ }
639
+ let scriptKind = typescript.ScriptKind.TS;
640
+ if ('.tsx' === ext) scriptKind = typescript.ScriptKind.TSX;
641
+ else if ('.jsx' === ext) scriptKind = typescript.ScriptKind.JSX;
642
+ else if ('.js' === ext) scriptKind = typescript.ScriptKind.JS;
643
+ else if ('.ts' === ext) scriptKind = typescript.ScriptKind.TS;
644
+ let sourceFile;
645
+ try {
646
+ sourceFile = typescript.createSourceFile(filePath, content, typescript.ScriptTarget.Latest, true, scriptKind);
647
+ } catch (parseError) {
648
+ throw new Error(`${PLUGIN_LOG_PREFIX} 无法解析入口文件 "${filePath}"。\n解析错误: ${parseError instanceof Error ? parseError.message : String(parseError)}\n请确保文件是有效的 TypeScript/JavaScript 文件。`);
649
+ }
650
+ typescript.getPreEmitDiagnostics(typescript.createProgram([
651
+ filePath
652
+ ], {
653
+ target: typescript.ScriptTarget.Latest,
654
+ module: typescript.ModuleKind.ESNext,
655
+ jsx: scriptKind === typescript.ScriptKind.TSX || scriptKind === typescript.ScriptKind.JSX ? typescript.JsxEmit.React : void 0
656
+ }));
657
+ function visit(node) {
658
+ if (typescript.isExportAssignment(node)) {
659
+ if (true !== node.isExportEquals) hasDefault = true;
660
+ }
661
+ if (typescript.isFunctionDeclaration(node) || typescript.isClassDeclaration(node) || typescript.isVariableStatement(node) || typescript.isInterfaceDeclaration(node) || typescript.isTypeAliasDeclaration(node) || typescript.isEnumDeclaration(node)) {
662
+ const modifiers = typescript.getModifiers(node);
663
+ if (modifiers?.some((m)=>m.kind === typescript.SyntaxKind.ExportKeyword)) if (modifiers.some((m)=>m.kind === typescript.SyntaxKind.DefaultKeyword)) hasDefault = true;
664
+ else {
665
+ if (typescript.isFunctionDeclaration(node) && node.name) namedExports.push(node.name.text);
666
+ if (typescript.isClassDeclaration(node) && node.name) namedExports.push(node.name.text);
667
+ if (typescript.isVariableStatement(node)) node.declarationList.declarations.forEach((decl)=>{
668
+ if (typescript.isIdentifier(decl.name)) namedExports.push(decl.name.text);
669
+ });
670
+ if (typescript.isInterfaceDeclaration(node) && node.name) namedExports.push(node.name.text);
671
+ if (typescript.isTypeAliasDeclaration(node) && node.name) namedExports.push(node.name.text);
672
+ if (typescript.isEnumDeclaration(node) && node.name) namedExports.push(node.name.text);
673
+ }
674
+ }
675
+ if (typescript.isExportDeclaration(node) && node.exportClause) {
676
+ if (typescript.isNamedExports(node.exportClause)) node.exportClause.elements.forEach((element)=>{
677
+ if (element.name) {
678
+ const exportName = element.name.text;
679
+ const { propertyName } = element;
680
+ if (propertyName && 'default' === propertyName.text) namedExports.push(exportName);
681
+ else if ('default' === exportName) hasDefault = true;
682
+ else namedExports.push(exportName);
683
+ }
684
+ });
685
+ else if (typescript.isNamespaceExport(node.exportClause)) namedExports.push(node.exportClause.name.text);
686
+ }
687
+ typescript.isExportDeclaration(node) && node.exportClause;
688
+ typescript.forEachChild(node, visit);
689
+ }
690
+ visit(sourceFile);
691
+ const uniqueExports = Array.from(new Set(namedExports));
692
+ if (!hasDefault && 0 === uniqueExports.length) {
693
+ const hasDefaultRegex = /export\s+default\s+/;
694
+ const hasNamedRegex = /export\s+(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
695
+ const regexHasDefault = hasDefaultRegex.test(content);
696
+ const regexNamedMatches = [];
697
+ let match;
698
+ while(null !== (match = hasNamedRegex.exec(content)))regexNamedMatches.push(match[1]);
699
+ if (regexHasDefault || regexNamedMatches.length > 0) {
700
+ console.warn(`${PLUGIN_LOG_PREFIX} 警告:AST 分析未检测到导出,但正则检测到:\n 文件: ${filePath}\n 正则检测 default: ${regexHasDefault}\n 正则检测命名导出: ${regexNamedMatches.join(', ') || '无'}\n 这可能是 AST 解析问题,将尝试继续构建。`);
701
+ if (regexHasDefault) hasDefault = true;
702
+ if (regexNamedMatches.length > 0) namedExports.push(...regexNamedMatches);
703
+ }
704
+ }
705
+ return {
706
+ hasDefault,
707
+ namedExports: Array.from(new Set(namedExports))
708
+ };
709
+ }
710
+ function generateLibVirtualEntryCode(params) {
711
+ const { defaultEntryAbs, componentName } = params;
712
+ if (!node_fs.existsSync(defaultEntryAbs)) throw new Error(`${PLUGIN_LOG_PREFIX} 入口文件不存在: "${defaultEntryAbs}"\n请检查文件路径是否正确。`);
713
+ const actualFile = tryResolveWithExtensions(defaultEntryAbs) || defaultEntryAbs;
714
+ if (!node_fs.existsSync(actualFile)) throw new Error(`${PLUGIN_LOG_PREFIX} 入口文件不存在: "${defaultEntryAbs}"\n尝试解析后的路径: "${actualFile}"\n请检查文件路径是否正确。`);
715
+ const importTarget = actualFile;
716
+ let exports;
717
+ try {
718
+ exports = analyzeExports(actualFile);
719
+ } catch (error) {
720
+ const errorMsg = error instanceof Error ? error.message : String(error);
721
+ throw new Error(`${PLUGIN_LOG_PREFIX} 分析入口文件 "${actualFile}" 的导出时出错:\n${errorMsg}\n\n请检查文件:\n 1. 文件是否存在且可读\n 2. 文件是否有语法错误\n 3. 文件是否有导出(export default 或命名导出)\n\n原始路径: "${defaultEntryAbs}"\n实际解析路径: "${actualFile}"`);
722
+ }
723
+ let code;
724
+ if (exports.hasDefault || 0 !== exports.namedExports.length) if (exports.hasDefault) {
725
+ let prefer = '';
726
+ if (exports.namedExports.includes(componentName)) prefer = isValidJsIdentifier(componentName) ? `mod.${componentName}` : `mod[${JSON.stringify(componentName)}]`;
727
+ const pickedExpr = prefer ? `${prefer} || mod.default || mod` : 'mod.default || mod';
728
+ code = `/** AUTO-GENERATED by ${PLUGIN_NAME} */
729
+ import * as mod from ${JSON.stringify(importTarget)};
730
+ const picked = ${pickedExpr};
731
+ const Component = picked && picked.default ? picked.default : picked;
732
+ export default Component;
733
+ export * from ${JSON.stringify(importTarget)};
734
+ `;
735
+ } else if (1 === exports.namedExports.length) {
736
+ const singleExport = exports.namedExports[0];
737
+ const exportAccess = isValidJsIdentifier(singleExport) ? `mod.${singleExport}` : `mod[${JSON.stringify(singleExport)}]`;
738
+ code = `/** AUTO-GENERATED by ${PLUGIN_NAME} */
739
+ import * as mod from ${JSON.stringify(importTarget)};
740
+ const Component = ${exportAccess};
741
+ export default Component;
742
+ export * from ${JSON.stringify(importTarget)};
743
+ `;
744
+ } else {
745
+ const hasComponentNameExport = exports.namedExports.some((exp)=>exp === componentName);
746
+ if (hasComponentNameExport) {
747
+ const exportAccess = isValidJsIdentifier(componentName) ? `mod.${componentName}` : `mod[${JSON.stringify(componentName)}]`;
748
+ code = `/** AUTO-GENERATED by ${PLUGIN_NAME} */
749
+ import * as mod from ${JSON.stringify(importTarget)};
750
+ const Component = ${exportAccess};
751
+ export default Component;
752
+ export * from ${JSON.stringify(importTarget)};
753
+ `;
754
+ } else throw new Error(`${PLUGIN_LOG_PREFIX} Entry file "${defaultEntryAbs}" has multiple named exports (${exports.namedExports.join(', ')}), but none match componentName "${componentName}".\n\nPlease resolve this by:\n\n 1. Adding a default export (Recommended for Vue):\n export default defineComponent({ name: '${componentName}', ... })\n\n 2. Adding a named export called "${componentName}":\n export const ${componentName} = defineComponent({ ... })\n\n 3. Keeping only one named export (it will be used automatically).\n\nCurrent componentName: "${componentName}"\nCurrent named exports: ${exports.namedExports.join(', ')}`);
755
+ }
756
+ else throw new Error(`${PLUGIN_LOG_PREFIX} Entry file "${defaultEntryAbs}" does not have any exports.\n\nPlease ensure the file has one of the following:\n\n 1. export default (Recommended for Vue SFC):\n <script setup>\n // Your component logic\n </script>\n or\n export default defineComponent({ ... })\n\n 2. Named export matching componentName:\n export const ${componentName} = defineComponent({ ... })\n\n 3. A single named export (any name):\n export const MyComponent = defineComponent({ ... })\n // If there is only one named export, it will be used automatically.\n\nCurrent componentName: "${componentName}"`);
757
+ return code;
758
+ }
759
+ function getLibVirtualEntryPath(componentName) {
760
+ return `virtual:${PLUGIN_NAME}-lib-entry:${componentName}`;
761
+ }
762
+ function normalizeLibCss(outDir, baseName) {
763
+ if (!node_fs.existsSync(outDir)) return;
764
+ const target = node_path.join(outDir, `${baseName}.css`);
765
+ if (node_fs.existsSync(target)) return;
766
+ const cssCandidates = [];
767
+ const scanDir = (dir, depth)=>{
768
+ if (depth < 0 || !node_fs.existsSync(dir)) return;
769
+ const entries = node_fs.readdirSync(dir, {
770
+ withFileTypes: true
771
+ });
772
+ for (const e of entries){
773
+ const full = node_path.join(dir, e.name);
774
+ if (e.isDirectory()) scanDir(full, depth - 1);
775
+ else if (e.isFile() && e.name.endsWith('.css')) cssCandidates.push(full);
776
+ }
777
+ };
778
+ scanDir(outDir, 2);
779
+ if (1 !== cssCandidates.length) return;
780
+ const from = cssCandidates[0];
781
+ try {
782
+ node_fs.renameSync(from, target);
783
+ } catch {
784
+ try {
785
+ node_fs.copyFileSync(from, target);
786
+ node_fs.unlinkSync(from);
787
+ } catch {}
788
+ }
789
+ }
790
+ function resolveBuildTargets(params) {
791
+ const { componentMap, requestedRaw, defaultEntryAbs } = params;
792
+ const componentNames = Object.keys(componentMap);
793
+ const requestedList = requestedRaw ? requestedRaw.split(',').map((s)=>s.trim()).filter(Boolean) : [];
794
+ const actualConfiguredNames = componentNames.filter((n)=>'*' !== n);
795
+ if (actualConfiguredNames.length > 0) {
796
+ if (requestedList.length > 0) {
797
+ const picked = requestedList.filter((n)=>actualConfiguredNames.includes(n));
798
+ if (0 === picked.length) throw new Error(`${PLUGIN_LOG_PREFIX} 指定的 component 不在配置列表中:${requestedRaw}`);
799
+ return picked;
800
+ }
801
+ return actualConfiguredNames;
802
+ }
803
+ if (requestedList.length > 0) return requestedList;
804
+ const fallbackName = node_path.parse(defaultEntryAbs || 'index').name || 'index';
805
+ return [
806
+ fallbackName
807
+ ];
808
+ }
809
+ function generateLibBuildNextConfig(params) {
810
+ const { rootDir, picked, componentMap, resolvedConfig, options, userConfig, configDir } = params;
811
+ const outBase = toSafeOutDirName(picked);
812
+ const outDir = node_path.resolve(rootDir, 'dist', outBase);
813
+ let resolvedEntryAbs = resolvedConfig.defaultEntryAbs;
814
+ const entryAbs = (()=>{
815
+ const entryFromMap = componentMap[picked];
816
+ if (entryFromMap) {
817
+ const abs = resolveEntryAbsPath(rootDir, entryFromMap, resolvedConfig.defaultEntryAbs, configDir);
818
+ if (!abs) throw new Error(`${PLUGIN_LOG_PREFIX} 无法解析入口:component="${picked}", entry="${entryFromMap}"`);
819
+ resolvedEntryAbs = abs;
820
+ }
821
+ return getLibVirtualEntryPath(picked);
822
+ })();
823
+ const virtualEntryCode = (()=>{
824
+ const entryFromMap = componentMap[picked];
825
+ if (entryFromMap) {
826
+ const abs = resolveEntryAbsPath(rootDir, entryFromMap, resolvedConfig.defaultEntryAbs, configDir);
827
+ if (!abs) throw new Error(`${PLUGIN_LOG_PREFIX} 无法解析入口:component="${picked}", entry="${entryFromMap}"`);
828
+ resolvedEntryAbs = abs;
829
+ return generateLibVirtualEntryCode({
830
+ rootDir,
831
+ defaultEntryAbs: abs,
832
+ componentName: picked
833
+ });
834
+ }
835
+ resolvedEntryAbs = resolvedConfig.defaultEntryAbs;
836
+ return generateLibVirtualEntryCode({
837
+ rootDir,
838
+ defaultEntryAbs: resolvedConfig.defaultEntryAbs,
839
+ componentName: picked
840
+ });
841
+ })();
842
+ const next = {
843
+ root: rootDir,
844
+ define: {
845
+ ...userConfig.define || {},
846
+ 'process.env.NODE_ENV': JSON.stringify('production')
847
+ },
848
+ build: {
849
+ outDir,
850
+ emptyOutDir: true,
851
+ cssCodeSplit: false,
852
+ lib: {
853
+ entry: entryAbs,
854
+ name: toSafeUmdName(picked),
855
+ formats: [
856
+ 'umd'
857
+ ],
858
+ fileName: ()=>`${outBase}.js`
859
+ },
860
+ rollupOptions: {
861
+ external: [
862
+ 'vue'
863
+ ],
864
+ output: {
865
+ inlineDynamicImports: true,
866
+ exports: 'named',
867
+ globals: {
868
+ vue: 'Vue'
869
+ },
870
+ assetFileNames: (assetInfo)=>{
871
+ const name = assetInfo?.name || '';
872
+ if (name.endsWith('.css')) return `${outBase}.css`;
873
+ return 'assets/[name]-[hash][extname]';
874
+ }
875
+ }
876
+ }
877
+ }
878
+ };
879
+ if (options.build) {
880
+ const merged = mergeConfig({
881
+ build: next.build
882
+ }, {
883
+ build: options.build
884
+ });
885
+ next.build = merged.build;
886
+ }
887
+ return {
888
+ next,
889
+ outDir: next.build?.outDir || outDir,
890
+ outBase,
891
+ buildTargets: [],
892
+ virtualEntryCode,
893
+ resolvedEntryAbs
894
+ };
895
+ }
896
+ function createLoaderUmdWrapper(options) {
897
+ const { componentName, origin, contractEndpoint = STABLE_CONTRACT_PATH, vueLoaderUrl = 'https://cdn.jsdelivr.net/npm/@dev-to/vue-loader@latest/dist/index.umd.js' } = options;
898
+ const globalName = toSafeUmdName(componentName);
899
+ const code = `/**
900
+ * UMD Loader Wrapper for component: ${componentName}
901
+ * Global name: ${globalName}
902
+ * Generated by ${PLUGIN_LOG_PREFIX}
903
+ *
904
+ * This wrapper automatically exports a Vue component that can be used in any Vue environment.
905
+ * No need to manually integrate @dev-to/vue-loader.
906
+ *
907
+ * ============= Quick Start =============
908
+ *
909
+ * 1. Load Vue:
910
+ * <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
911
+ *
912
+ * 2. Load this wrapper:
913
+ * <script src="${origin}/__dev_to__/vue/loader/${componentName}.js"></script>
914
+ *
915
+ * 3. Use as a Vue component:
916
+ *
917
+ * // Option A: Direct Vue rendering
918
+ * const Component = window.${globalName};
919
+ * const app = Vue.createApp(Component);
920
+ * app.mount('#app');
921
+ *
922
+ * // Option B: Register as a component in an existing app
923
+ * const app = Vue.createApp({});
924
+ * app.component('${componentName}', window.${globalName});
925
+ * app.mount('#app');
926
+ *
927
+ * // Option C: Direct function call (legacy compatibility)
928
+ * window.${globalName}(document.getElementById('app'), { prop1: 'value1' })
929
+ * .then(app => console.log('Rendered'))
930
+ * .catch(err => console.error('Error:', err));
931
+ *
932
+ * ============= Features =============
933
+ * ✓ Zero configuration required
934
+ * ✓ Automatic Vue detection
935
+ * ✓ Supports CommonJS, AMD, and global scope
936
+ * ✓ Auto-loads VueLoader from CDN if needed
937
+ * ✓ Works in any Vue 3 environment
938
+ *
939
+ * Note: Make sure Vue 3 is available globally.
940
+ */
941
+ (function (root, factory) {
942
+ if (typeof exports === 'object' && typeof module !== 'undefined') {
943
+ // CommonJS
944
+ factory(exports, require('vue'), require('@dev-to/vue-loader'));
945
+ } else if (typeof define === 'function' && define.amd) {
946
+ // AMD
947
+ define(['exports', 'vue', '@dev-to/vue-loader'], factory);
948
+ } else {
949
+ // Browser globals
950
+ var globalObj = typeof globalThis !== 'undefined' ? globalThis : (typeof self !== 'undefined' ? self : root);
951
+ var tempExports = {};
952
+ factory(tempExports, globalObj.Vue, globalObj.DevToVueLoader);
953
+ globalObj.${globalName} = tempExports.default;
954
+ }
955
+ })(this, function (exports, Vue, VueLoaderModule) {
956
+ 'use strict';
957
+
958
+ var VueLoader = null;
959
+ var loadingPromise = null;
960
+
961
+ // Helper function to load a script dynamically
962
+ function loadScript(src) {
963
+ return new Promise(function(resolve, reject) {
964
+ var script = document.createElement('script');
965
+ script.src = src;
966
+ script.onload = resolve;
967
+ script.onerror = reject;
968
+ document.head.appendChild(script);
969
+ });
970
+ }
971
+
972
+ // Helper function to ensure VueLoader is loaded
973
+ function ensureVueLoaderLoaded() {
974
+ if (VueLoader) {
975
+ return Promise.resolve();
976
+ }
977
+
978
+ if (!loadingPromise) {
979
+ loadingPromise = (function() {
980
+ // First, try to get VueLoader from the global scope
981
+ if (typeof window !== 'undefined' && window.DevToVueLoader && window.DevToVueLoader.VueLoader) {
982
+ VueLoader = window.DevToVueLoader.VueLoader;
983
+ return Promise.resolve();
984
+ }
985
+
986
+ // If not available, load it from URL
987
+ console.log('${PLUGIN_LOG_PREFIX} Loading @dev-to/vue-loader...');
988
+ return loadScript(${JSON.stringify(vueLoaderUrl)})
989
+ .then(function() {
990
+ if (typeof window !== 'undefined' && window.DevToVueLoader && window.DevToVueLoader.VueLoader) {
991
+ VueLoader = window.DevToVueLoader.VueLoader;
992
+ console.log('${PLUGIN_LOG_PREFIX} VueLoader loaded successfully');
993
+ } else {
994
+ throw new Error('${PLUGIN_LOG_PREFIX} VueLoader not found after loading');
995
+ }
996
+ })
997
+ .catch(function(error) {
998
+ console.error('${PLUGIN_LOG_PREFIX} Failed to load VueLoader:', error);
999
+ throw error;
1000
+ });
1001
+ })();
1002
+ }
1003
+
1004
+ return loadingPromise;
1005
+ }
1006
+
1007
+ // Try to get VueLoader from the module if available
1008
+ if (VueLoaderModule && VueLoaderModule.VueLoader) {
1009
+ VueLoader = VueLoaderModule.VueLoader;
1010
+ }
1011
+
1012
+ // Component configuration
1013
+ var config = {
1014
+ origin: ${JSON.stringify(origin)},
1015
+ name: ${JSON.stringify(componentName)},
1016
+ contractEndpoint: ${JSON.stringify(contractEndpoint)}
1017
+ };
1018
+
1019
+ /**
1020
+ * Render the component using VueLoader
1021
+ */
1022
+ function render(targetElement, componentProps) {
1023
+ if (!targetElement) {
1024
+ throw new Error('${PLUGIN_LOG_PREFIX} Target element is required');
1025
+ }
1026
+
1027
+ if (!Vue || !Vue.createApp) {
1028
+ throw new Error('${PLUGIN_LOG_PREFIX} Vue 3 is not loaded');
1029
+ }
1030
+
1031
+ // Ensure VueLoader is available before rendering
1032
+ return ensureVueLoaderLoaded().then(function() {
1033
+ if (!VueLoader) {
1034
+ throw new Error('${PLUGIN_LOG_PREFIX} VueLoader initialization failed');
1035
+ }
1036
+
1037
+ // Create VueLoader component props
1038
+ var loaderProps = {
1039
+ origin: config.origin,
1040
+ name: config.name,
1041
+ contractEndpoint: config.contractEndpoint,
1042
+ componentProps: componentProps || {}
1043
+ };
1044
+
1045
+ // Create a wrapper component that uses VueLoader
1046
+ var WrapperComponent = {
1047
+ name: '${componentName}Wrapper',
1048
+ setup: function() {
1049
+ return function() {
1050
+ return Vue.h(VueLoader, loaderProps);
1051
+ };
1052
+ }
1053
+ };
1054
+
1055
+ // Render using Vue
1056
+ var app = Vue.createApp(WrapperComponent);
1057
+ app.mount(targetElement);
1058
+
1059
+ return app;
1060
+ });
1061
+ }
1062
+
1063
+ // Create a Vue component wrapper for the render function
1064
+ var ComponentWrapper = {
1065
+ name: '${componentName}',
1066
+ props: {
1067
+ componentProps: {
1068
+ type: Object,
1069
+ default: function() { return {}; }
1070
+ }
1071
+ },
1072
+ setup: function(props) {
1073
+ var containerRef = Vue.ref(null);
1074
+ var isFirstRender = Vue.ref(true);
1075
+
1076
+ Vue.onMounted(function() {
1077
+ if (!containerRef.value) return;
1078
+
1079
+ render(containerRef.value, props.componentProps).catch(function(err) {
1080
+ console.error('${PLUGIN_LOG_PREFIX} Failed to render ${componentName}:', err);
1081
+ console.error('${PLUGIN_LOG_PREFIX} Props:', props.componentProps);
1082
+ });
1083
+
1084
+ if (isFirstRender.value) {
1085
+ isFirstRender.value = false;
1086
+ if (typeof console !== 'undefined' && console.info) {
1087
+ console.info(
1088
+ '%c${PLUGIN_LOG_PREFIX}%c Successfully loaded and rendered component: %c${componentName}',
1089
+ 'color: #42b883; font-weight: bold;',
1090
+ 'color: #64748b;',
1091
+ 'color: #42b883; font-weight: bold;'
1092
+ );
1093
+ }
1094
+ }
1095
+ });
1096
+
1097
+ Vue.watch(function() { return props.componentProps; }, function(newProps) {
1098
+ if (containerRef.value) {
1099
+ // Re-render with new props
1100
+ containerRef.value.innerHTML = '';
1101
+ render(containerRef.value, newProps).catch(function(err) {
1102
+ console.error('${PLUGIN_LOG_PREFIX} Failed to re-render ${componentName}:', err);
1103
+ });
1104
+ }
1105
+ }, { deep: true });
1106
+
1107
+ return function() {
1108
+ return Vue.h('div', { ref: containerRef });
1109
+ };
1110
+ }
1111
+ };
1112
+
1113
+ // Also allow direct function call for backwards compatibility
1114
+ ComponentWrapper.render = render;
1115
+
1116
+ // Export the API
1117
+ exports.default = ComponentWrapper;
1118
+ });
1119
+ `;
1120
+ return code;
1121
+ }
29
1122
  function openBrowser(url) {
30
1123
  const bridgePath = STABLE_DEBUG_HTML_PATH;
31
1124
  if ('darwin' === process.platform) {
@@ -88,6 +1181,27 @@ function openBrowser(url) {
88
1181
  if ('win32' === process.platform) return void exec(`start "" "${url}"`);
89
1182
  exec(`xdg-open "${url}"`);
90
1183
  }
1184
+ function getVueLoaderUmdPath() {
1185
+ const require = createRequire(import.meta.url);
1186
+ try {
1187
+ const loaderPkgPath = require.resolve('@dev-to/vue-loader/package.json');
1188
+ const loaderPkgDir = node_path.dirname(loaderPkgPath);
1189
+ const umdPath = node_path.join(loaderPkgDir, 'dist/index.umd.js');
1190
+ if (node_fs.existsSync(umdPath)) return umdPath;
1191
+ } catch {}
1192
+ try {
1193
+ const loaderMainPath = require.resolve('@dev-to/vue-loader');
1194
+ const loaderPkgDir = node_path.dirname(node_path.dirname(loaderMainPath));
1195
+ const umdPath = node_path.join(loaderPkgDir, 'dist/index.umd.js');
1196
+ if (node_fs.existsSync(umdPath)) return umdPath;
1197
+ } catch {}
1198
+ try {
1199
+ const __dirname = node_path.dirname(fileURLToPath(import.meta.url));
1200
+ const umdPath = node_path.resolve(__dirname, '../../vue-loader/dist/index.umd.js');
1201
+ if (node_fs.existsSync(umdPath)) return umdPath;
1202
+ } catch {}
1203
+ return null;
1204
+ }
91
1205
  const globalState = globalThis;
92
1206
  let didOpenBrowser = Boolean(globalState[DEV_TO_VUE_DID_OPEN_BROWSER_KEY]);
93
1207
  function installDebugTools(server, ctx, state) {
@@ -223,6 +1337,8 @@ function installDebugTools(server, ctx, state) {
223
1337
  ];
224
1338
  const originCandidates = candidateHosts.map((h)=>`${proto}://${h}${actualPort ? `:${actualPort}` : ''}`);
225
1339
  const requestOrigin = hostHeader ? `${proto}://${hostHeader}` : originCandidates[0];
1340
+ const componentNames = Object.keys(ctx.contract?.dev?.componentMap || {});
1341
+ const libComponentExample = componentNames.slice(0, 2).join(',') || 'Demo';
226
1342
  res.statusCode = 200;
227
1343
  res.setHeader('Content-Type', 'application/json; charset=utf-8');
228
1344
  res.end(JSON.stringify({
@@ -245,26 +1361,143 @@ function installDebugTools(server, ctx, state) {
245
1361
  usage: {
246
1362
  localStorageKey: 'VITE_DEV_SERVER_ORIGIN',
247
1363
  suggested: requestOrigin,
248
- snippet: `localStorage.setItem('VITE_DEV_SERVER_ORIGIN', '${requestOrigin}'); location.reload();`
249
- }
1364
+ snippet: `localStorage.setItem('VITE_DEV_SERVER_ORIGIN', '${requestOrigin}'); location.reload();`,
1365
+ libBuild: {
1366
+ command: 'dev-to build',
1367
+ env: {
1368
+ DEV_TO_VUE_LIB_SECTION: libComponentExample
1369
+ },
1370
+ output: {
1371
+ dir: 'dist/<component>/',
1372
+ js: '<component>.js (UMD, 尽量单文件 bundle)',
1373
+ css: '<component>.css (如有样式)'
1374
+ },
1375
+ externals: [
1376
+ 'vue'
1377
+ ],
1378
+ umdGlobals: {
1379
+ vue: 'Vue'
1380
+ }
1381
+ }
1382
+ },
1383
+ tips: [
1384
+ '宿主侧需设置 localStorage.VITE_DEV_SERVER_ORIGIN(可从 originCandidates 里选择一个可访问的 origin)。',
1385
+ 'components 参数的 key 必须与后端返回的 componentName 完全一致(严格匹配)。',
1386
+ '如需产出可分发 UMD 包:使用 `dev-to build`(等价于 `vite build --mode lib`,仅构建 components 指定的组件,输出到 dist/<component>/)。'
1387
+ ]
250
1388
  }, null, 2));
251
1389
  return;
252
1390
  }
1391
+ if (pathname === STABLE_LOADER_UMD_PATH) {
1392
+ const vueLoaderUmdPath = getVueLoaderUmdPath();
1393
+ if (vueLoaderUmdPath) try {
1394
+ const umdCode = node_fs.readFileSync(vueLoaderUmdPath, 'utf-8');
1395
+ res.statusCode = 200;
1396
+ res.setHeader('Content-Type', "application/javascript; charset=utf-8");
1397
+ res.setHeader('Access-Control-Allow-Origin', '*');
1398
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
1399
+ res.end(umdCode);
1400
+ return;
1401
+ } catch (error) {
1402
+ console.warn(`[dev_to:vue] Failed to read local UMD: ${error}. Falling back to CDN.`);
1403
+ }
1404
+ const cdnUrl = 'https://cdn.jsdelivr.net/npm/@dev-to/vue-loader@latest/dist/index.umd.js';
1405
+ res.statusCode = 302;
1406
+ res.setHeader('Location', cdnUrl);
1407
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
1408
+ res.end();
1409
+ return;
1410
+ }
1411
+ if (pathname.startsWith(STABLE_LOADER_BASE_PATH)) {
1412
+ const loaderPathPattern = new RegExp(`^${STABLE_LOADER_BASE_PATH}/([^/]+)\\.js$`);
1413
+ const match = pathname.match(loaderPathPattern);
1414
+ if (match) {
1415
+ const componentName = match[1];
1416
+ const isHttps = !!server.config.server.https;
1417
+ const proto = isHttps ? 'https' : 'http';
1418
+ const hostHeader = String(req.headers.host || '');
1419
+ const addr = server.httpServer?.address();
1420
+ const actualPort = addr && 'object' == typeof addr ? addr.port : void 0;
1421
+ const origin = hostHeader ? `${proto}://${hostHeader}` : `${proto}://localhost${actualPort ? `:${actualPort}` : ''}`;
1422
+ const hasLocalUmd = null !== getVueLoaderUmdPath();
1423
+ const vueLoaderUrl = hasLocalUmd ? `${origin}${STABLE_LOADER_UMD_PATH}` : 'https://cdn.jsdelivr.net/npm/@dev-to/vue-loader@latest/dist/index.umd.js';
1424
+ const code = createLoaderUmdWrapper({
1425
+ componentName,
1426
+ origin,
1427
+ contractEndpoint: STABLE_CONTRACT_PATH,
1428
+ vueLoaderUrl
1429
+ });
1430
+ res.statusCode = 200;
1431
+ res.setHeader('Content-Type', "application/javascript; charset=utf-8");
1432
+ res.setHeader('Access-Control-Allow-Origin', '*');
1433
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
1434
+ res.end(code);
1435
+ return;
1436
+ }
1437
+ }
253
1438
  if (url.startsWith(STABLE_DEBUG_HTML_PATH)) {
254
- const isHttps = !!server.config.server.https;
255
- const proto = isHttps ? 'https' : 'http';
256
1439
  const addr = server.httpServer?.address();
257
1440
  const actualPort = addr && 'object' == typeof addr ? addr.port : void 0;
258
1441
  const lanHosts = getLanIPv4Hosts();
1442
+ const isHttps = !!server.config.server.https;
1443
+ const proto = isHttps ? 'https' : 'http';
259
1444
  const candidateHosts = [
260
1445
  'localhost',
261
1446
  '127.0.0.1',
262
1447
  ...lanHosts
263
1448
  ];
264
1449
  const originCandidates = candidateHosts.map((h)=>`${proto}://${h}${actualPort ? `:${actualPort}` : ''}`);
1450
+ const serverConfigLite = {
1451
+ host: server.config.server.host,
1452
+ port: server.config.server.port,
1453
+ strictPort: server.config.server.strictPort,
1454
+ cors: server.config.server.cors,
1455
+ https: !!server.config.server.https
1456
+ };
1457
+ let configFilePath;
1458
+ const rootDir = server.config.root || process.cwd();
1459
+ configFilePath = server.config.configFile || void 0;
1460
+ if (!configFilePath) try {
1461
+ const files = node_fs.readdirSync(rootDir);
1462
+ const configFile = files.find((file)=>/^vite\.config\.(ts|js|mjs|cjs|cts)$/.test(file));
1463
+ if (configFile) configFilePath = node_path.resolve(rootDir, configFile);
1464
+ } catch {
1465
+ const configFiles = [
1466
+ 'vite.config.ts',
1467
+ 'vite.config.js',
1468
+ 'vite.config.mjs',
1469
+ 'vite.config.cjs',
1470
+ 'vite.config.cts'
1471
+ ];
1472
+ for (const file of configFiles){
1473
+ const fullPath = node_path.resolve(rootDir, file);
1474
+ if (node_fs.existsSync(fullPath)) {
1475
+ configFilePath = fullPath;
1476
+ break;
1477
+ }
1478
+ }
1479
+ }
1480
+ const entryPathMap = {};
1481
+ for (const [componentName, entry] of Object.entries(ctx.contract.dev.componentMap)){
1482
+ if ('*' === componentName) {
1483
+ entryPathMap[componentName] = ctx.audit.defaultEntryAbs;
1484
+ continue;
1485
+ }
1486
+ if (entry.startsWith('/@fs')) {
1487
+ const fsPath = toFsPathFromViteEntry(entry);
1488
+ if (fsPath) entryPathMap[componentName] = fsPath;
1489
+ } else if ('/' === entry) entryPathMap[componentName] = ctx.audit.defaultEntryAbs;
1490
+ else if (!entry.startsWith('http://') && !entry.startsWith('https://') && !entry.startsWith('/')) entryPathMap[componentName] = node_path.resolve(rootDir, entry);
1491
+ }
265
1492
  const html = renderDebugHtml({
1493
+ resolvedDevComponentMap: ctx.contract.dev.componentMap,
1494
+ entryPathMap,
1495
+ audit: ctx.audit,
1496
+ stats: ctx.stats,
1497
+ serverConfigLite,
266
1498
  originCandidates,
267
- contract: ctx.contract
1499
+ actualPort: 'number' == typeof actualPort ? actualPort : void 0,
1500
+ configFilePath
268
1501
  });
269
1502
  res.statusCode = 200;
270
1503
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
@@ -274,104 +1507,6 @@ function installDebugTools(server, ctx, state) {
274
1507
  next();
275
1508
  });
276
1509
  }
277
- function renderDebugHtml(params) {
278
- const { originCandidates, contract } = params;
279
- const originsJson = JSON.stringify(originCandidates);
280
- const contractJson = JSON.stringify(contract, null, 2);
281
- return `<!doctype html>
282
- <html lang="en">
283
- <head>
284
- <meta charset="utf-8" />
285
- <meta name="viewport" content="width=device-width, initial-scale=1" />
286
- <title>DevTo Vue Debug</title>
287
- <style>
288
- body { font-family: system-ui, sans-serif; padding: 20px; color: #111827; }
289
- h1 { font-size: 20px; margin-bottom: 8px; }
290
- .card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; margin-bottom: 12px; }
291
- .origin { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
292
- button { border: 1px solid #d1d5db; background: #fff; padding: 4px 8px; border-radius: 4px; cursor: pointer; }
293
- pre { background: #f9fafb; padding: 10px; border-radius: 6px; overflow: auto; font-size: 12px; }
294
- code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
295
- </style>
296
- </head>
297
- <body>
298
- <h1>DevTo Vue Debug</h1>
299
- <div class="card">
300
- <div><strong>Set origin in host</strong></div>
301
- <div id="origin-list"></div>
302
- <pre id="origin-cmd"></pre>
303
- </div>
304
- <div class="card">
305
- <div><strong>Contract</strong></div>
306
- <pre>${contractJson}</pre>
307
- </div>
308
- <script>
309
- const origins = ${originsJson};
310
- const list = document.getElementById('origin-list');
311
- const cmd = document.getElementById('origin-cmd');
312
-
313
- function makeCmd(origin) {
314
- return "localStorage.setItem('VITE_DEV_SERVER_ORIGIN', '" + origin + "'); location.reload();";
315
- }
316
-
317
- function setCmd(origin) {
318
- cmd.textContent = makeCmd(origin);
319
- }
320
-
321
- function copy(text) {
322
- if (navigator.clipboard && navigator.clipboard.writeText) {
323
- return navigator.clipboard.writeText(text);
324
- }
325
- const ta = document.createElement('textarea');
326
- ta.value = text;
327
- document.body.appendChild(ta);
328
- ta.select();
329
- document.execCommand('copy');
330
- document.body.removeChild(ta);
331
- return Promise.resolve();
332
- }
333
-
334
- origins.forEach(origin => {
335
- const row = document.createElement('div');
336
- row.className = 'origin';
337
- const code = document.createElement('code');
338
- code.textContent = origin;
339
- const btn = document.createElement('button');
340
- btn.textContent = 'Copy';
341
- btn.onclick = () => {
342
- setCmd(origin);
343
- copy(makeCmd(origin));
344
- };
345
- row.appendChild(code);
346
- row.appendChild(btn);
347
- list.appendChild(row);
348
- });
349
-
350
- if (origins[0]) setCmd(origins[0]);
351
- </script>
352
- </body>
353
- </html>`;
354
- }
355
- function tryResolveWithExtensions(p) {
356
- const exts = [
357
- '.vue',
358
- '.tsx',
359
- '.jsx',
360
- '.ts',
361
- '.js'
362
- ];
363
- if (node_fs.existsSync(p)) return p;
364
- const parsed = node_path.parse(p);
365
- if (parsed.ext) for (const ext of exts){
366
- const cand = node_path.join(parsed.dir, `${parsed.name}${ext}`);
367
- if (node_fs.existsSync(cand)) return cand;
368
- }
369
- else for (const ext of exts){
370
- const cand = `${p}${ext}`;
371
- if (node_fs.existsSync(cand)) return cand;
372
- }
373
- return null;
374
- }
375
1510
  function toViteFsPath(filePath) {
376
1511
  const normalized = filePath.replace(/\\/g, '/');
377
1512
  return normalized.startsWith('/') ? `/@fs${normalized}` : `/@fs/${normalized}`;
@@ -487,7 +1622,7 @@ function transformAssetUrl(code, id) {
487
1622
  const ORIGIN = new URL(import.meta.url).origin;
488
1623
  return path.startsWith('http') ? path : ORIGIN + path;
489
1624
  } catch (e) {
490
- console.warn('${constants_PLUGIN_LOG_PREFIX} Failed to resolve static asset URL:', path, e);
1625
+ console.warn('${PLUGIN_LOG_PREFIX} Failed to resolve static asset URL:', path, e);
491
1626
  return path;
492
1627
  }
493
1628
  })()`);
@@ -525,7 +1660,7 @@ const __dev_to__resolveAsset = (path) => {
525
1660
  const origin = new URL(import.meta.url).origin;
526
1661
  return path.startsWith('/') ? origin + path : origin + '/' + path;
527
1662
  } catch (e) {
528
- console.warn('${constants_PLUGIN_LOG_PREFIX} Failed to resolve CSS asset URL:', path, e);
1663
+ console.warn('${PLUGIN_LOG_PREFIX} Failed to resolve CSS asset URL:', path, e);
529
1664
  return path;
530
1665
  }
531
1666
  };
@@ -549,7 +1684,7 @@ function createContractVirtualModuleCode(contract) {
549
1684
  const STATE = (G[DEBUG_KEY] ||= { logged: {} });
550
1685
  if (!STATE.logged.contract) {
551
1686
  STATE.logged.contract = true;
552
- console.groupCollapsed('${constants_PLUGIN_LOG_PREFIX} contract loaded');
1687
+ console.groupCollapsed('${PLUGIN_LOG_PREFIX} contract loaded');
553
1688
  console.log('Origin:', ORIGIN);
554
1689
  console.log('Paths:', CONTRACT.paths);
555
1690
  console.log('Events:', CONTRACT.events);
@@ -576,7 +1711,7 @@ function createInitVirtualModuleCode() {
576
1711
  const STATE = (G[DEBUG_KEY] ||= { logged: {} });
577
1712
  if (!STATE.logged.init) {
578
1713
  STATE.logged.init = true;
579
- console.groupCollapsed('${constants_PLUGIN_LOG_PREFIX} init loaded (HMR enabled)');
1714
+ console.groupCollapsed('${PLUGIN_LOG_PREFIX} init loaded (HMR enabled)');
580
1715
  console.log('Origin:', ORIGIN);
581
1716
  console.log('This module imports /@vite/client.');
582
1717
  console.groupEnd();
@@ -626,7 +1761,7 @@ function createVueRuntimeVirtualModuleCode() {
626
1761
  const STATE = (G[DEBUG_KEY] ||= { logged: {} });
627
1762
  if (!STATE.logged.runtime) {
628
1763
  STATE.logged.runtime = true;
629
- console.groupCollapsed('${constants_PLUGIN_LOG_PREFIX} vue-runtime loaded');
1764
+ console.groupCollapsed('${PLUGIN_LOG_PREFIX} vue-runtime loaded');
630
1765
  console.log('Origin:', ORIGIN);
631
1766
  console.log('Vue.version:', Vue?.version);
632
1767
  console.groupEnd();
@@ -687,7 +1822,7 @@ function devToVuePlugin(components, options) {
687
1822
  if (1 === Object.keys(resolvedConfig.componentMap).length && '/' === resolvedConfig.componentMap['*']) {
688
1823
  const warn = server.config.logger?.warn?.bind(server.config.logger) ?? console.warn;
689
1824
  warn('');
690
- warn(`Warning: ${constants_PLUGIN_LOG_PREFIX} No componentName configured. This works in dev mode but should be explicit for production builds.`);
1825
+ warn(`Warning: ${PLUGIN_LOG_PREFIX} No componentName configured. This works in dev mode but should be explicit for production builds.`);
691
1826
  warn('Use devToVuePlugin({ ComponentName: "src/ComponentName.vue" }) or devToVuePlugin({ ComponentName: "/" }).');
692
1827
  warn('');
693
1828
  }
@@ -767,4 +1902,4 @@ function devToVuePlugin(components, options) {
767
1902
  devCssAssetPlugin
768
1903
  ];
769
1904
  }
770
- export { devToVuePlugin };
1905
+ export { createLoaderUmdWrapper, devToVuePlugin, generateLibBuildNextConfig, generateLibVirtualEntryCode, getLibVirtualEntryPath, isLibBuild, normalizeLibCss, renderDebugHtml, resolveBuildTargets, toSafeOutDirName, toSafeUmdName };