@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/debugHtml.d.ts +20 -0
- package/dist/debugHtml.d.ts.map +1 -0
- package/dist/debugTools.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1246 -111
- package/dist/libBuildUtils.d.ts +44 -0
- package/dist/libBuildUtils.d.ts.map +1 -0
- package/dist/loaderUmdWrapper.d.ts +13 -0
- package/dist/loaderUmdWrapper.d.ts.map +1 -0
- package/package.json +2 -2
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
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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"><script></span> <span class="kw">src</span>=<span class="str">"https://unpkg.com/vue@3/dist/vue.global.prod.js"</span> <span class="kw"></script></span>
|
|
358
|
+
|
|
359
|
+
<span class="cmt">// 2. 加载包装器脚本</span>
|
|
360
|
+
<span class="kw"><script></span> <span class="kw">src</span>=<span class="str">"\${originCandidates[0] || 'http://localhost:5173'}/__dev_to__/vue/loader/{ComponentName}.js"</span> <span class="kw"></script></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/<name>/<name>.js</span>
|
|
390
|
+
CSS: <span class="str">dist/<name>/<name>.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
|
-
|
|
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('${
|
|
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('${
|
|
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('${
|
|
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('${
|
|
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('${
|
|
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: ${
|
|
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 };
|