create-next-imagicma 0.1.9 → 0.1.11
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/package.json +1 -1
- package/template-hono/client/index.html +6 -1
- package/template-hono/client/public/imagicma-preview-feedback.js +734 -0
- package/template-hono/client/src/lib/imagicma-preview-picker.ts +179 -21
- package/template-hono/server/index.ts +40 -6
- package/template-hono/vite.config.ts +40 -6
package/package.json
CHANGED
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>hono-app</title>
|
|
7
7
|
<script src="/imagicma-picker-bridge.js"></script>
|
|
8
|
+
<script src="/imagicma-preview-feedback.js"></script>
|
|
8
9
|
</head>
|
|
9
10
|
<body class="antialiased">
|
|
10
11
|
<div id="root"></div>
|
|
11
|
-
<script type="module"
|
|
12
|
+
<script type="module">
|
|
13
|
+
import("/src/main.tsx").catch((error) => {
|
|
14
|
+
window.__IMAGICMA_PREVIEW_FEEDBACK__?.reportModuleBootstrapError(error);
|
|
15
|
+
});
|
|
16
|
+
</script>
|
|
12
17
|
</body>
|
|
13
18
|
</html>
|
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
var PREVIEW_REPAIR_CHANNEL = 'imagicma.preview-repair';
|
|
3
|
+
var PREVIEW_REPAIR_VERSION = 1;
|
|
4
|
+
var PROD_PARENT_ORIGINS = {
|
|
5
|
+
'https://agentma.cn': true,
|
|
6
|
+
'https://imagicma.cn': true,
|
|
7
|
+
};
|
|
8
|
+
var LOCAL_PARENT_RE = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i;
|
|
9
|
+
var LOCAL_IMAGICMA_PARENT_RE = /^https?:\/\/([a-z0-9-]+\.)?local\.(agentma\.cn|imagicma\.cn)(:\d+)?$/i;
|
|
10
|
+
var MAX_ERROR_MESSAGE_LENGTH = 4000;
|
|
11
|
+
var MAX_RESOURCE_CANDIDATES = 12;
|
|
12
|
+
var PANEL_ID = 'imagicma-preview-feedback-root';
|
|
13
|
+
|
|
14
|
+
var state = {
|
|
15
|
+
panel: null,
|
|
16
|
+
titleEl: null,
|
|
17
|
+
bodyEl: null,
|
|
18
|
+
metaEl: null,
|
|
19
|
+
detailEl: null,
|
|
20
|
+
statusEl: null,
|
|
21
|
+
repairButton: null,
|
|
22
|
+
refreshButton: null,
|
|
23
|
+
error: null,
|
|
24
|
+
requestInFlight: false,
|
|
25
|
+
lastSignature: '',
|
|
26
|
+
observer: null,
|
|
27
|
+
parentOrigin: '',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
var originalConsoleError = typeof console !== 'undefined' && typeof console.error === 'function'
|
|
31
|
+
? console.error.bind(console)
|
|
32
|
+
: null;
|
|
33
|
+
|
|
34
|
+
window.__IMAGICMA_PREVIEW_FEEDBACK__ = {
|
|
35
|
+
reportModuleBootstrapError: function (error) {
|
|
36
|
+
if (!error) return;
|
|
37
|
+
resolveLatestViteErrorDetails().then(function (details) {
|
|
38
|
+
if (details) {
|
|
39
|
+
renderError(details);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
reportStartupError(
|
|
43
|
+
trimText(error.name) || 'ModuleBootstrapError',
|
|
44
|
+
trimText(error.message) || normalizeWhitespace(error),
|
|
45
|
+
trimText(error.stack) || trimText(error.message),
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (originalConsoleError) {
|
|
52
|
+
console.error = function () {
|
|
53
|
+
maybeCaptureViteConsoleError(arguments);
|
|
54
|
+
return originalConsoleError.apply(console, arguments);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function trimText(value) {
|
|
59
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeWhitespace(value) {
|
|
63
|
+
return trimText(String(value || '').replace(/\r\n?/g, '\n').replace(/\n{3,}/g, '\n\n'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function truncateText(value, maxLength) {
|
|
67
|
+
var text = normalizeWhitespace(value);
|
|
68
|
+
if (!text) return '';
|
|
69
|
+
return text.length > maxLength ? text.slice(0, maxLength) + '\n…' : text;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function stringifyConsoleArgs(args) {
|
|
73
|
+
return Array.prototype.map.call(args, function (arg) {
|
|
74
|
+
if (typeof arg === 'string') return arg;
|
|
75
|
+
if (arg && typeof arg === 'object') {
|
|
76
|
+
var message = trimText(arg.message);
|
|
77
|
+
var stack = trimText(arg.stack);
|
|
78
|
+
if (message && stack) return message + '\n' + stack;
|
|
79
|
+
if (message) return message;
|
|
80
|
+
try {
|
|
81
|
+
return JSON.stringify(arg);
|
|
82
|
+
} catch (_error) {
|
|
83
|
+
return String(arg);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return String(arg);
|
|
87
|
+
}).join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function maybeCaptureViteConsoleError(argsLike) {
|
|
91
|
+
var text = normalizeWhitespace(stringifyConsoleArgs(argsLike));
|
|
92
|
+
if (!text) return;
|
|
93
|
+
if (text.indexOf('[vite] Internal Server Error') !== 0) return;
|
|
94
|
+
|
|
95
|
+
renderError({
|
|
96
|
+
title: '预览编译失败',
|
|
97
|
+
errorName: 'ViteConsoleError',
|
|
98
|
+
errorMessage: text,
|
|
99
|
+
errorStack: text,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isAllowedParentOrigin(origin) {
|
|
104
|
+
if (!origin) return false;
|
|
105
|
+
return !!PROD_PARENT_ORIGINS[origin] || LOCAL_PARENT_RE.test(origin) || LOCAL_IMAGICMA_PARENT_RE.test(origin);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveParentOrigin() {
|
|
109
|
+
if (state.parentOrigin && isAllowedParentOrigin(state.parentOrigin)) {
|
|
110
|
+
return state.parentOrigin;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (window.parent === window) {
|
|
114
|
+
state.parentOrigin = '';
|
|
115
|
+
return '';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
var referrer = trimText(document.referrer);
|
|
119
|
+
if (!referrer) {
|
|
120
|
+
state.parentOrigin = '';
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
var origin = new URL(referrer).origin;
|
|
126
|
+
state.parentOrigin = isAllowedParentOrigin(origin) ? origin : '';
|
|
127
|
+
return state.parentOrigin;
|
|
128
|
+
} catch (_error) {
|
|
129
|
+
state.parentOrigin = '';
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function createRequestId() {
|
|
135
|
+
if (window.crypto && typeof window.crypto.randomUUID === 'function') {
|
|
136
|
+
return window.crypto.randomUUID();
|
|
137
|
+
}
|
|
138
|
+
return 'repair_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function hasBootstrappedApp() {
|
|
142
|
+
var root = document.getElementById('root');
|
|
143
|
+
if (!root) return false;
|
|
144
|
+
if (root.childElementCount > 0) return true;
|
|
145
|
+
return trimText(root.textContent).length > 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildSummary(details) {
|
|
149
|
+
var message = truncateText(details.errorMessage, MAX_ERROR_MESSAGE_LENGTH);
|
|
150
|
+
if (!message) {
|
|
151
|
+
return '检测到预览初始化失败,请刷新页面后重试。';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
var lines = message.split('\n').map(trimText).filter(Boolean);
|
|
155
|
+
if (lines.length === 0) {
|
|
156
|
+
return '检测到预览初始化失败,请刷新页面后重试。';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
var firstLine = lines[0];
|
|
160
|
+
if (/^Internal Server Error$/i.test(firstLine) && lines[1]) {
|
|
161
|
+
firstLine = lines[1];
|
|
162
|
+
}
|
|
163
|
+
return firstLine;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getNormalizedLines(text) {
|
|
167
|
+
return normalizeWhitespace(text).split('\n').map(trimText).filter(Boolean);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function extractKeyErrorLine(text) {
|
|
171
|
+
var lines = getNormalizedLines(text);
|
|
172
|
+
for (var index = 0; index < lines.length; index += 1) {
|
|
173
|
+
var line = lines[index];
|
|
174
|
+
if (!line) continue;
|
|
175
|
+
if (/^\[vite\]/i.test(line)) continue;
|
|
176
|
+
if (/^Internal Server Error$/i.test(line)) continue;
|
|
177
|
+
if (/^plugin:/i.test(line)) continue;
|
|
178
|
+
if (/^id:/i.test(line)) continue;
|
|
179
|
+
if (/^at\s+/i.test(line)) continue;
|
|
180
|
+
if (/^\(?Error overlay failed to load\)?$/i.test(line)) continue;
|
|
181
|
+
return line;
|
|
182
|
+
}
|
|
183
|
+
return '';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function extractFieldValue(text, fieldName) {
|
|
187
|
+
var escapedFieldName = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
188
|
+
var regexp = new RegExp('^' + escapedFieldName + ':\\s*(.+)$', 'mi');
|
|
189
|
+
var match = normalizeWhitespace(text).match(regexp);
|
|
190
|
+
return match && match[1] ? trimText(match[1]) : '';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function extractFrameSnippet(text) {
|
|
194
|
+
var lines = normalizeWhitespace(text).split('\n');
|
|
195
|
+
var caretIndex = -1;
|
|
196
|
+
var index = 0;
|
|
197
|
+
for (; index < lines.length; index += 1) {
|
|
198
|
+
if (lines[index].indexOf('^') >= 0) {
|
|
199
|
+
caretIndex = index;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (caretIndex >= 0) {
|
|
205
|
+
var start = Math.max(0, caretIndex - 2);
|
|
206
|
+
var end = Math.min(lines.length, caretIndex + 2);
|
|
207
|
+
return lines.slice(start, end).join('\n').trim();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
var frameLines = lines.filter(function (line) {
|
|
211
|
+
return /^\s*(>?[\s\d|]+|at\s+)/.test(line);
|
|
212
|
+
});
|
|
213
|
+
return frameLines.slice(0, 5).join('\n').trim();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function buildRenderedDetail(error) {
|
|
217
|
+
var sourceText = [error.errorMessage, error.errorStack].filter(Boolean).join('\n\n');
|
|
218
|
+
var keyErrorLine = extractKeyErrorLine(sourceText);
|
|
219
|
+
var frameSnippet = extractFrameSnippet(sourceText);
|
|
220
|
+
var plugin = extractFieldValue(sourceText, 'plugin');
|
|
221
|
+
var id = extractFieldValue(sourceText, 'id');
|
|
222
|
+
return {
|
|
223
|
+
keyErrorLine: keyErrorLine,
|
|
224
|
+
detailText: frameSnippet ? ('定位片段\n' + frameSnippet) : (keyErrorLine || error.errorMessage),
|
|
225
|
+
metaText: [
|
|
226
|
+
plugin ? ('plugin: ' + plugin) : '',
|
|
227
|
+
id ? ('id: ' + id) : '',
|
|
228
|
+
].filter(Boolean).join(' '),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function getViteOverlayText(overlay) {
|
|
233
|
+
if (!overlay) return '';
|
|
234
|
+
var root = overlay.shadowRoot || overlay;
|
|
235
|
+
return truncateText(root.textContent || overlay.textContent || '', MAX_ERROR_MESSAGE_LENGTH);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function sameOriginUrl(url) {
|
|
239
|
+
try {
|
|
240
|
+
var parsed = new URL(url, window.location.href);
|
|
241
|
+
return parsed.origin === window.location.origin ? parsed : null;
|
|
242
|
+
} catch (_error) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function isLikelySourceRequest(pathname) {
|
|
248
|
+
return (
|
|
249
|
+
pathname.indexOf('/src/') === 0
|
|
250
|
+
|| pathname.indexOf('/@fs/') === 0
|
|
251
|
+
|| /\.(?:[cm]?js|[jt]sx?|css)$/.test(pathname)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function listRecentResourceCandidates() {
|
|
256
|
+
if (!window.performance || typeof window.performance.getEntriesByType !== 'function') {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
var entries = window.performance.getEntriesByType('resource');
|
|
261
|
+
var seen = {};
|
|
262
|
+
var urls = [];
|
|
263
|
+
|
|
264
|
+
for (var index = entries.length - 1; index >= 0; index -= 1) {
|
|
265
|
+
var entry = entries[index];
|
|
266
|
+
if (!entry || !entry.name) continue;
|
|
267
|
+
var parsed = sameOriginUrl(entry.name);
|
|
268
|
+
if (!parsed) continue;
|
|
269
|
+
if (!isLikelySourceRequest(parsed.pathname)) continue;
|
|
270
|
+
if (parsed.pathname.indexOf('/imagicma-preview-feedback.js') >= 0) continue;
|
|
271
|
+
if (parsed.pathname.indexOf('/@vite/client') >= 0) continue;
|
|
272
|
+
if (seen[parsed.href]) continue;
|
|
273
|
+
seen[parsed.href] = true;
|
|
274
|
+
urls.push(parsed.href);
|
|
275
|
+
if (urls.length >= MAX_RESOURCE_CANDIDATES) {
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return urls;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function parseViteErrorPayloadFromHtml(html) {
|
|
284
|
+
var text = typeof html === 'string' ? html : '';
|
|
285
|
+
if (!text) return null;
|
|
286
|
+
|
|
287
|
+
var match = text.match(/const error = (\{[\s\S]*?\})\s*try\s*\{/);
|
|
288
|
+
if (!match || !match[1]) return null;
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
return JSON.parse(match[1]);
|
|
292
|
+
} catch (_error) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function buildDetailedViteMessage(payload, requestUrl) {
|
|
298
|
+
if (!payload) return '';
|
|
299
|
+
|
|
300
|
+
var sections = ['[vite] Internal Server Error'];
|
|
301
|
+
var message = trimText(payload.message);
|
|
302
|
+
var frame = trimText(payload.frame);
|
|
303
|
+
var plugin = trimText(payload.plugin);
|
|
304
|
+
var id = trimText(payload.id) || trimText(requestUrl);
|
|
305
|
+
var stack = trimText(payload.stack);
|
|
306
|
+
|
|
307
|
+
if (message) sections.push(message);
|
|
308
|
+
if (frame) sections.push(frame);
|
|
309
|
+
if (plugin) sections.push('plugin: ' + plugin);
|
|
310
|
+
if (id) sections.push('id: ' + id);
|
|
311
|
+
if (stack) sections.push(stack);
|
|
312
|
+
|
|
313
|
+
return sections.join('\n\n');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function fetchViteErrorDetailsFromUrl(url) {
|
|
317
|
+
return window.fetch(url, {
|
|
318
|
+
credentials: 'same-origin',
|
|
319
|
+
cache: 'no-store',
|
|
320
|
+
}).then(function (response) {
|
|
321
|
+
if (!response || response.status < 500) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return response.text().then(function (html) {
|
|
326
|
+
var payload = parseViteErrorPayloadFromHtml(html);
|
|
327
|
+
if (!payload) return null;
|
|
328
|
+
|
|
329
|
+
var detailedMessage = buildDetailedViteMessage(payload, url);
|
|
330
|
+
return {
|
|
331
|
+
title: '预览编译失败',
|
|
332
|
+
errorName: trimText(payload.plugin) || 'ViteCompileError',
|
|
333
|
+
errorMessage: detailedMessage || trimText(payload.message) || 'Vite 编译失败',
|
|
334
|
+
errorStack: [trimText(payload.frame), trimText(payload.stack)].filter(Boolean).join('\n\n') || detailedMessage,
|
|
335
|
+
};
|
|
336
|
+
}).catch(function () {
|
|
337
|
+
return null;
|
|
338
|
+
});
|
|
339
|
+
}).catch(function () {
|
|
340
|
+
return null;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function resolveLatestViteErrorDetails() {
|
|
345
|
+
var candidates = listRecentResourceCandidates();
|
|
346
|
+
if (candidates.length === 0) {
|
|
347
|
+
return Promise.resolve(null);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
var index = 0;
|
|
351
|
+
|
|
352
|
+
function next() {
|
|
353
|
+
if (index >= candidates.length) {
|
|
354
|
+
return Promise.resolve(null);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
var candidate = candidates[index];
|
|
358
|
+
index += 1;
|
|
359
|
+
return fetchViteErrorDetailsFromUrl(candidate).then(function (details) {
|
|
360
|
+
if (details) return details;
|
|
361
|
+
return next();
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return next();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function dismissPanel() {
|
|
369
|
+
if (state.panel && state.panel.parentNode) {
|
|
370
|
+
state.panel.parentNode.removeChild(state.panel);
|
|
371
|
+
}
|
|
372
|
+
state.panel = null;
|
|
373
|
+
state.titleEl = null;
|
|
374
|
+
state.bodyEl = null;
|
|
375
|
+
state.metaEl = null;
|
|
376
|
+
state.detailEl = null;
|
|
377
|
+
state.statusEl = null;
|
|
378
|
+
state.repairButton = null;
|
|
379
|
+
state.refreshButton = null;
|
|
380
|
+
state.error = null;
|
|
381
|
+
state.requestInFlight = false;
|
|
382
|
+
state.lastSignature = '';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function ensurePanel() {
|
|
386
|
+
if (state.panel && document.body.contains(state.panel)) {
|
|
387
|
+
return state.panel;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
var panel = document.createElement('div');
|
|
391
|
+
panel.id = PANEL_ID;
|
|
392
|
+
panel.setAttribute('role', 'alert');
|
|
393
|
+
panel.style.position = 'fixed';
|
|
394
|
+
panel.style.inset = '0';
|
|
395
|
+
panel.style.zIndex = '2147483647';
|
|
396
|
+
panel.style.display = 'flex';
|
|
397
|
+
panel.style.alignItems = 'center';
|
|
398
|
+
panel.style.justifyContent = 'center';
|
|
399
|
+
panel.style.padding = '24px';
|
|
400
|
+
panel.style.background = 'linear-gradient(135deg, #243b53 0%, #3f497f 48%, #145374 100%)';
|
|
401
|
+
panel.style.color = '#fff';
|
|
402
|
+
panel.style.fontFamily = 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif';
|
|
403
|
+
|
|
404
|
+
var card = document.createElement('div');
|
|
405
|
+
card.style.width = 'min(720px, 100%)';
|
|
406
|
+
card.style.maxHeight = 'min(88vh, 900px)';
|
|
407
|
+
card.style.overflow = 'auto';
|
|
408
|
+
card.style.borderRadius = '28px';
|
|
409
|
+
card.style.border = '1px solid rgba(255, 255, 255, 0.16)';
|
|
410
|
+
card.style.background = 'rgba(7, 16, 34, 0.38)';
|
|
411
|
+
card.style.boxShadow = '0 28px 80px rgba(2, 8, 23, 0.45)';
|
|
412
|
+
card.style.backdropFilter = 'blur(18px)';
|
|
413
|
+
card.style.padding = '32px';
|
|
414
|
+
|
|
415
|
+
var badge = document.createElement('div');
|
|
416
|
+
badge.textContent = 'Preview bootstrap error';
|
|
417
|
+
badge.style.display = 'inline-flex';
|
|
418
|
+
badge.style.alignItems = 'center';
|
|
419
|
+
badge.style.borderRadius = '999px';
|
|
420
|
+
badge.style.padding = '6px 10px';
|
|
421
|
+
badge.style.fontSize = '12px';
|
|
422
|
+
badge.style.letterSpacing = '0.08em';
|
|
423
|
+
badge.style.textTransform = 'uppercase';
|
|
424
|
+
badge.style.color = 'rgba(255,255,255,0.82)';
|
|
425
|
+
badge.style.background = 'rgba(255,255,255,0.1)';
|
|
426
|
+
|
|
427
|
+
var title = document.createElement('h1');
|
|
428
|
+
title.style.margin = '18px 0 10px';
|
|
429
|
+
title.style.fontSize = 'clamp(28px, 4vw, 36px)';
|
|
430
|
+
title.style.lineHeight = '1.1';
|
|
431
|
+
title.style.fontWeight = '700';
|
|
432
|
+
|
|
433
|
+
var body = document.createElement('p');
|
|
434
|
+
body.style.margin = '0';
|
|
435
|
+
body.style.fontSize = '15px';
|
|
436
|
+
body.style.lineHeight = '1.7';
|
|
437
|
+
body.style.color = 'rgba(255,255,255,0.82)';
|
|
438
|
+
|
|
439
|
+
var meta = document.createElement('p');
|
|
440
|
+
meta.style.margin = '12px 0 0';
|
|
441
|
+
meta.style.fontSize = '12px';
|
|
442
|
+
meta.style.lineHeight = '1.6';
|
|
443
|
+
meta.style.color = 'rgba(255,255,255,0.62)';
|
|
444
|
+
meta.style.display = 'none';
|
|
445
|
+
|
|
446
|
+
var detail = document.createElement('pre');
|
|
447
|
+
detail.style.margin = '18px 0 0';
|
|
448
|
+
detail.style.padding = '18px';
|
|
449
|
+
detail.style.borderRadius = '20px';
|
|
450
|
+
detail.style.background = 'rgba(2, 6, 23, 0.42)';
|
|
451
|
+
detail.style.border = '1px solid rgba(255, 255, 255, 0.08)';
|
|
452
|
+
detail.style.whiteSpace = 'pre-wrap';
|
|
453
|
+
detail.style.wordBreak = 'break-word';
|
|
454
|
+
detail.style.fontSize = '13px';
|
|
455
|
+
detail.style.lineHeight = '1.65';
|
|
456
|
+
detail.style.color = 'rgba(255,255,255,0.92)';
|
|
457
|
+
|
|
458
|
+
var status = document.createElement('p');
|
|
459
|
+
status.style.minHeight = '22px';
|
|
460
|
+
status.style.margin = '16px 0 0';
|
|
461
|
+
status.style.fontSize = '13px';
|
|
462
|
+
status.style.lineHeight = '1.6';
|
|
463
|
+
status.style.color = 'rgba(255,255,255,0.76)';
|
|
464
|
+
|
|
465
|
+
var actions = document.createElement('div');
|
|
466
|
+
actions.style.display = 'flex';
|
|
467
|
+
actions.style.flexWrap = 'wrap';
|
|
468
|
+
actions.style.gap = '12px';
|
|
469
|
+
actions.style.marginTop = '24px';
|
|
470
|
+
|
|
471
|
+
var repairButton = document.createElement('button');
|
|
472
|
+
repairButton.type = 'button';
|
|
473
|
+
repairButton.textContent = '一键修复';
|
|
474
|
+
repairButton.style.height = '42px';
|
|
475
|
+
repairButton.style.padding = '0 18px';
|
|
476
|
+
repairButton.style.borderRadius = '999px';
|
|
477
|
+
repairButton.style.border = 'none';
|
|
478
|
+
repairButton.style.background = '#ffffff';
|
|
479
|
+
repairButton.style.color = '#0f172a';
|
|
480
|
+
repairButton.style.fontSize = '14px';
|
|
481
|
+
repairButton.style.fontWeight = '600';
|
|
482
|
+
repairButton.style.cursor = 'pointer';
|
|
483
|
+
|
|
484
|
+
var refreshButton = document.createElement('button');
|
|
485
|
+
refreshButton.type = 'button';
|
|
486
|
+
refreshButton.textContent = '刷新页面';
|
|
487
|
+
refreshButton.style.height = '42px';
|
|
488
|
+
refreshButton.style.padding = '0 18px';
|
|
489
|
+
refreshButton.style.borderRadius = '999px';
|
|
490
|
+
refreshButton.style.border = '1px solid rgba(255,255,255,0.18)';
|
|
491
|
+
refreshButton.style.background = 'rgba(255,255,255,0.08)';
|
|
492
|
+
refreshButton.style.color = '#ffffff';
|
|
493
|
+
refreshButton.style.fontSize = '14px';
|
|
494
|
+
refreshButton.style.fontWeight = '600';
|
|
495
|
+
refreshButton.style.cursor = 'pointer';
|
|
496
|
+
|
|
497
|
+
repairButton.addEventListener('click', function () {
|
|
498
|
+
void sendRepairRequest();
|
|
499
|
+
});
|
|
500
|
+
refreshButton.addEventListener('click', function () {
|
|
501
|
+
window.location.reload();
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
actions.appendChild(repairButton);
|
|
505
|
+
actions.appendChild(refreshButton);
|
|
506
|
+
|
|
507
|
+
card.appendChild(badge);
|
|
508
|
+
card.appendChild(title);
|
|
509
|
+
card.appendChild(body);
|
|
510
|
+
card.appendChild(meta);
|
|
511
|
+
card.appendChild(detail);
|
|
512
|
+
card.appendChild(status);
|
|
513
|
+
card.appendChild(actions);
|
|
514
|
+
panel.appendChild(card);
|
|
515
|
+
document.body.appendChild(panel);
|
|
516
|
+
|
|
517
|
+
state.panel = panel;
|
|
518
|
+
state.titleEl = title;
|
|
519
|
+
state.bodyEl = body;
|
|
520
|
+
state.metaEl = meta;
|
|
521
|
+
state.detailEl = detail;
|
|
522
|
+
state.statusEl = status;
|
|
523
|
+
state.repairButton = repairButton;
|
|
524
|
+
state.refreshButton = refreshButton;
|
|
525
|
+
|
|
526
|
+
return panel;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function updateRepairButtonState() {
|
|
530
|
+
if (!state.repairButton) return;
|
|
531
|
+
var canRepair = !!resolveParentOrigin() && window.parent !== window && !!state.error;
|
|
532
|
+
state.repairButton.disabled = state.requestInFlight || !canRepair;
|
|
533
|
+
state.repairButton.style.opacity = state.requestInFlight || !canRepair ? '0.7' : '1';
|
|
534
|
+
state.repairButton.style.cursor = state.requestInFlight || !canRepair ? 'not-allowed' : 'pointer';
|
|
535
|
+
if (state.requestInFlight) {
|
|
536
|
+
state.repairButton.textContent = '同步中...';
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
state.repairButton.textContent = canRepair ? '一键修复' : '仅支持在主界面预览中修复';
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function renderError(details) {
|
|
543
|
+
var signature = [
|
|
544
|
+
trimText(details.title),
|
|
545
|
+
trimText(details.errorName),
|
|
546
|
+
trimText(details.errorMessage),
|
|
547
|
+
].join('|');
|
|
548
|
+
if (signature === state.lastSignature) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
state.lastSignature = signature;
|
|
553
|
+
state.error = {
|
|
554
|
+
title: trimText(details.title) || '预览暂时不可用',
|
|
555
|
+
errorName: trimText(details.errorName) || 'PreviewBootstrapError',
|
|
556
|
+
errorMessage: truncateText(details.errorMessage, MAX_ERROR_MESSAGE_LENGTH),
|
|
557
|
+
errorStack: truncateText(details.errorStack || details.errorMessage, MAX_ERROR_MESSAGE_LENGTH),
|
|
558
|
+
timestamp: Date.now(),
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
ensurePanel();
|
|
562
|
+
|
|
563
|
+
state.titleEl.textContent = state.error.title;
|
|
564
|
+
var renderedDetail = buildRenderedDetail(state.error);
|
|
565
|
+
state.bodyEl.textContent = renderedDetail.keyErrorLine
|
|
566
|
+
? ('关键报错:' + renderedDetail.keyErrorLine)
|
|
567
|
+
: '检测到预览初始化阶段的错误。你可以把错误信息同步回主界面,让系统生成修复草稿,或先刷新页面。';
|
|
568
|
+
if (state.metaEl) {
|
|
569
|
+
state.metaEl.textContent = renderedDetail.metaText;
|
|
570
|
+
state.metaEl.style.display = renderedDetail.metaText ? 'block' : 'none';
|
|
571
|
+
}
|
|
572
|
+
state.detailEl.textContent = renderedDetail.detailText;
|
|
573
|
+
state.statusEl.textContent = '';
|
|
574
|
+
state.requestInFlight = false;
|
|
575
|
+
updateRepairButtonState();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function sendRepairRequest() {
|
|
579
|
+
var parentOrigin = resolveParentOrigin();
|
|
580
|
+
if (!state.error || !parentOrigin || window.parent === window || state.requestInFlight) {
|
|
581
|
+
updateRepairButtonState();
|
|
582
|
+
return Promise.resolve();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
state.requestInFlight = true;
|
|
586
|
+
state.statusEl.textContent = '正在同步修复草稿到主界面…';
|
|
587
|
+
updateRepairButtonState();
|
|
588
|
+
|
|
589
|
+
var requestId = createRequestId();
|
|
590
|
+
|
|
591
|
+
return new Promise(function (resolve) {
|
|
592
|
+
var timer = window.setTimeout(function () {
|
|
593
|
+
cleanup();
|
|
594
|
+
state.requestInFlight = false;
|
|
595
|
+
state.statusEl.textContent = '主界面响应超时,请稍后重试或先刷新页面。';
|
|
596
|
+
updateRepairButtonState();
|
|
597
|
+
resolve();
|
|
598
|
+
}, 4000);
|
|
599
|
+
|
|
600
|
+
function cleanup() {
|
|
601
|
+
window.clearTimeout(timer);
|
|
602
|
+
window.removeEventListener('message', handleAck);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function handleAck(event) {
|
|
606
|
+
if (event.source !== window.parent) return;
|
|
607
|
+
if (event.origin !== parentOrigin) return;
|
|
608
|
+
var data = event.data;
|
|
609
|
+
if (!data || data.channel !== PREVIEW_REPAIR_CHANNEL || data.version !== PREVIEW_REPAIR_VERSION) return;
|
|
610
|
+
if (data.type !== 'IMAGICMA_PREVIEW_REPAIR_ACK' || data.requestId !== requestId) return;
|
|
611
|
+
|
|
612
|
+
cleanup();
|
|
613
|
+
state.requestInFlight = false;
|
|
614
|
+
state.statusEl.textContent = trimText(data.payload && data.payload.message)
|
|
615
|
+
|| (data.payload && data.payload.status === 'ok'
|
|
616
|
+
? '已同步修复草稿到主界面,请回到对话区确认发送。'
|
|
617
|
+
: '同步失败,请稍后重试。');
|
|
618
|
+
updateRepairButtonState();
|
|
619
|
+
resolve();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
window.addEventListener('message', handleAck);
|
|
623
|
+
|
|
624
|
+
try {
|
|
625
|
+
window.parent.postMessage(
|
|
626
|
+
{
|
|
627
|
+
channel: PREVIEW_REPAIR_CHANNEL,
|
|
628
|
+
version: PREVIEW_REPAIR_VERSION,
|
|
629
|
+
type: 'IMAGICMA_PREVIEW_REPAIR_REQUEST',
|
|
630
|
+
requestId: requestId,
|
|
631
|
+
payload: {
|
|
632
|
+
pageUrl: window.location.href,
|
|
633
|
+
errorName: state.error.errorName,
|
|
634
|
+
errorMessage: state.error.errorMessage,
|
|
635
|
+
errorStack: state.error.errorStack,
|
|
636
|
+
timestamp: state.error.timestamp,
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
parentOrigin,
|
|
640
|
+
);
|
|
641
|
+
} catch (_error) {
|
|
642
|
+
cleanup();
|
|
643
|
+
state.requestInFlight = false;
|
|
644
|
+
state.statusEl.textContent = '同步失败,请稍后重试或先刷新页面。';
|
|
645
|
+
updateRepairButtonState();
|
|
646
|
+
resolve();
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function maybeReportOverlay() {
|
|
652
|
+
var overlay = document.querySelector('vite-error-overlay');
|
|
653
|
+
if (!overlay) {
|
|
654
|
+
if (state.panel && hasBootstrappedApp()) {
|
|
655
|
+
dismissPanel();
|
|
656
|
+
}
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
window.setTimeout(function () {
|
|
661
|
+
var text = getViteOverlayText(overlay);
|
|
662
|
+
if (!text) return;
|
|
663
|
+
try {
|
|
664
|
+
overlay.style.display = 'none';
|
|
665
|
+
} catch (_error) {
|
|
666
|
+
// Ignore Vite overlay style write failures.
|
|
667
|
+
}
|
|
668
|
+
renderError({
|
|
669
|
+
title: '预览编译失败',
|
|
670
|
+
errorName: 'ViteCompileError',
|
|
671
|
+
errorMessage: text,
|
|
672
|
+
errorStack: text,
|
|
673
|
+
});
|
|
674
|
+
}, 0);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function reportStartupError(errorName, errorMessage, errorStack) {
|
|
678
|
+
if (hasBootstrappedApp()) return;
|
|
679
|
+
if (!trimText(errorMessage)) return;
|
|
680
|
+
renderError({
|
|
681
|
+
title: '预览加载失败',
|
|
682
|
+
errorName: trimText(errorName) || 'PreviewBootstrapError',
|
|
683
|
+
errorMessage: errorMessage,
|
|
684
|
+
errorStack: errorStack || errorMessage,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
window.addEventListener('message', function (event) {
|
|
689
|
+
if (event.source !== window.parent) return;
|
|
690
|
+
if (!isAllowedParentOrigin(event.origin)) return;
|
|
691
|
+
state.parentOrigin = event.origin;
|
|
692
|
+
updateRepairButtonState();
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
window.addEventListener('error', function (event) {
|
|
696
|
+
var error = event && event.error;
|
|
697
|
+
var errorName = trimText(error && error.name) || 'PreviewBootstrapError';
|
|
698
|
+
var errorMessage = trimText(error && error.message) || trimText(event && event.message);
|
|
699
|
+
var errorStack = trimText(error && error.stack);
|
|
700
|
+
reportStartupError(errorName, errorMessage, errorStack);
|
|
701
|
+
}, true);
|
|
702
|
+
|
|
703
|
+
window.addEventListener('unhandledrejection', function (event) {
|
|
704
|
+
var reason = event && event.reason;
|
|
705
|
+
if (!reason) return;
|
|
706
|
+
|
|
707
|
+
if (typeof reason === 'string') {
|
|
708
|
+
reportStartupError('UnhandledRejection', reason, reason);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
reportStartupError(
|
|
713
|
+
trimText(reason.name) || 'UnhandledRejection',
|
|
714
|
+
trimText(reason.message) || normalizeWhitespace(reason),
|
|
715
|
+
trimText(reason.stack) || trimText(reason.message),
|
|
716
|
+
);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
state.observer = new MutationObserver(function () {
|
|
720
|
+
maybeReportOverlay();
|
|
721
|
+
});
|
|
722
|
+
state.observer.observe(document.documentElement, {
|
|
723
|
+
childList: true,
|
|
724
|
+
subtree: true,
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
if (document.readyState === 'loading') {
|
|
728
|
+
document.addEventListener('DOMContentLoaded', maybeReportOverlay, { once: true });
|
|
729
|
+
} else {
|
|
730
|
+
maybeReportOverlay();
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
window.setTimeout(maybeReportOverlay, 300);
|
|
734
|
+
})();
|
|
@@ -12,6 +12,8 @@ const DEBUG_ATTR_PATH = "data-imagicma-path";
|
|
|
12
12
|
const DEBUG_ATTR_LINE = "data-imagicma-line";
|
|
13
13
|
const DEBUG_ATTR_FILE = "data-imagicma-file";
|
|
14
14
|
const DEBUG_ATTR_COMPONENT = "data-imagicma-component";
|
|
15
|
+
const RUNTIME_TEXT_SEGMENT_ATTR = "data-imagicma-runtime-text-segment";
|
|
16
|
+
const RUNTIME_TEXT_SEGMENT_INDEX_ATTR = "data-imagicma-text-segment-index";
|
|
15
17
|
const SORT_DRAG_THRESHOLD_PX = 4;
|
|
16
18
|
|
|
17
19
|
type PreviewPickerMode = "single" | "design";
|
|
@@ -40,10 +42,16 @@ type PreviewSourceStyleField =
|
|
|
40
42
|
| "margin"
|
|
41
43
|
| "padding";
|
|
42
44
|
|
|
43
|
-
type PreviewSourceTextBinding =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
type PreviewSourceTextBinding =
|
|
46
|
+
| {
|
|
47
|
+
kind: "jsx-text";
|
|
48
|
+
source: PreviewSourceRef;
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
kind: "jsx-text-segment";
|
|
52
|
+
source: PreviewSourceRef;
|
|
53
|
+
segmentIndex: number;
|
|
54
|
+
};
|
|
47
55
|
|
|
48
56
|
type PreviewSourceRemoveBinding = {
|
|
49
57
|
kind: "jsx-element";
|
|
@@ -172,6 +180,7 @@ type RuntimeState = {
|
|
|
172
180
|
parentOrigin: string | null;
|
|
173
181
|
activeSessionId: string | null;
|
|
174
182
|
enabled: boolean;
|
|
183
|
+
nodeIdIndex: Map<string, HTMLElement[]>;
|
|
175
184
|
selectedElement: HTMLElement | null;
|
|
176
185
|
hoveredElement: HTMLElement | null;
|
|
177
186
|
overlayRoot: HTMLDivElement | null;
|
|
@@ -318,6 +327,15 @@ function setSemanticAttribute(element: HTMLElement, name: string, value: string
|
|
|
318
327
|
element.setAttribute(name, nextValue);
|
|
319
328
|
}
|
|
320
329
|
|
|
330
|
+
function copySemanticDebugAttributes(source: HTMLElement, target: HTMLElement) {
|
|
331
|
+
[DEBUG_ATTR_ID, DEBUG_ATTR_PATH, DEBUG_ATTR_LINE, DEBUG_ATTR_FILE, DEBUG_ATTR_COMPONENT].forEach((attribute) => {
|
|
332
|
+
const value = trimText(source.getAttribute(attribute));
|
|
333
|
+
if (value) {
|
|
334
|
+
target.setAttribute(attribute, value);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
321
339
|
function toSyntheticNodeId(element: HTMLElement): string | null {
|
|
322
340
|
const debugId = getSourceId(element);
|
|
323
341
|
if (debugId) return `runtime:${debugId}`;
|
|
@@ -348,8 +366,87 @@ function annotateBaseSemanticNode(element: HTMLElement) {
|
|
|
348
366
|
setSemanticAttribute(element, "data-imagicma-source-file", getSourceFile(element));
|
|
349
367
|
}
|
|
350
368
|
|
|
351
|
-
function
|
|
352
|
-
|
|
369
|
+
function isRuntimeTextSegmentElement(element: HTMLElement): boolean {
|
|
370
|
+
return element.getAttribute(RUNTIME_TEXT_SEGMENT_ATTR) === "true";
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function getOwnedMeaningfulTextNodes(element: HTMLElement): Text[] {
|
|
374
|
+
const nodes: Text[] = [];
|
|
375
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
376
|
+
let current = walker.nextNode();
|
|
377
|
+
while (current) {
|
|
378
|
+
if (current instanceof Text && trimText(current.textContent).length > 0) {
|
|
379
|
+
const owner = current.parentElement?.closest<HTMLElement>("[data-imagicma-node-id]");
|
|
380
|
+
if (owner === element) {
|
|
381
|
+
nodes.push(current);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
current = walker.nextNode();
|
|
385
|
+
}
|
|
386
|
+
return nodes;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function createRuntimeTextSegmentNodeId(element: HTMLElement, segmentIndex: number): string | null {
|
|
390
|
+
const ownerNodeId = getElementNodeId(element) || toSyntheticNodeId(element);
|
|
391
|
+
if (!ownerNodeId) return null;
|
|
392
|
+
return `${ownerNodeId}::text:${segmentIndex}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function annotateRuntimeTextSegments(element: HTMLElement) {
|
|
396
|
+
if (isRuntimeTextSegmentElement(element)) return;
|
|
397
|
+
if (!getSourceMetadata(element)) return;
|
|
398
|
+
|
|
399
|
+
const ownedTextNodes = getOwnedMeaningfulTextNodes(element);
|
|
400
|
+
const shouldSegmentText = ownedTextNodes.length > 1 || (ownedTextNodes.length > 0 && element.children.length > 0);
|
|
401
|
+
if (!shouldSegmentText) return;
|
|
402
|
+
|
|
403
|
+
ownedTextNodes.forEach((textNode, segmentIndex) => {
|
|
404
|
+
if (textNode.parentElement && isRuntimeTextSegmentElement(textNode.parentElement)) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const nodeId = createRuntimeTextSegmentNodeId(element, segmentIndex);
|
|
409
|
+
if (!nodeId || !textNode.parentNode) return;
|
|
410
|
+
|
|
411
|
+
const wrapper = document.createElement("span");
|
|
412
|
+
copySemanticDebugAttributes(element, wrapper);
|
|
413
|
+
setSemanticAttribute(wrapper, RUNTIME_TEXT_SEGMENT_ATTR, "true", true);
|
|
414
|
+
setSemanticAttribute(wrapper, RUNTIME_TEXT_SEGMENT_INDEX_ATTR, String(segmentIndex), true);
|
|
415
|
+
setSemanticAttribute(wrapper, "data-imagicma-node-id", nodeId, true);
|
|
416
|
+
setSemanticAttribute(wrapper, "data-imagicma-kind", "text", true);
|
|
417
|
+
setSemanticAttribute(wrapper, "data-imagicma-source-file", getSourceFile(element), true);
|
|
418
|
+
textNode.parentNode.insertBefore(wrapper, textNode);
|
|
419
|
+
wrapper.appendChild(textNode);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function buildNodeIdIndex(): Map<string, HTMLElement[]> {
|
|
424
|
+
const index = new Map<string, HTMLElement[]>();
|
|
425
|
+
document.querySelectorAll<HTMLElement>("[data-imagicma-node-id]").forEach((element) => {
|
|
426
|
+
const nodeId = getElementNodeId(element);
|
|
427
|
+
if (!nodeId) return;
|
|
428
|
+
const peers = index.get(nodeId);
|
|
429
|
+
if (peers) {
|
|
430
|
+
peers.push(element);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
index.set(nodeId, [element]);
|
|
434
|
+
});
|
|
435
|
+
return index;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function hydrateRuntimeSemantics(state?: RuntimeState) {
|
|
439
|
+
document.querySelectorAll<HTMLElement>(`[${DEBUG_ATTR_ID}]`).forEach((element) => {
|
|
440
|
+
if (isRuntimeTextSegmentElement(element)) return;
|
|
441
|
+
annotateBaseSemanticNode(element);
|
|
442
|
+
});
|
|
443
|
+
document.querySelectorAll<HTMLElement>(`[${DEBUG_ATTR_ID}]`).forEach((element) => {
|
|
444
|
+
if (isRuntimeTextSegmentElement(element)) return;
|
|
445
|
+
annotateRuntimeTextSegments(element);
|
|
446
|
+
});
|
|
447
|
+
if (state) {
|
|
448
|
+
state.nodeIdIndex = buildNodeIdIndex();
|
|
449
|
+
}
|
|
353
450
|
}
|
|
354
451
|
|
|
355
452
|
function getRepeatItemRoot(element: HTMLElement): HTMLElement | null {
|
|
@@ -406,6 +503,40 @@ function getElementNodeId(element: HTMLElement | null | undefined): string {
|
|
|
406
503
|
return trimText(element?.getAttribute("data-imagicma-node-id"));
|
|
407
504
|
}
|
|
408
505
|
|
|
506
|
+
function getRepeatItemScopeKey(element: HTMLElement | null | undefined): string | null {
|
|
507
|
+
const repeatRoot = element ? getRepeatItemRoot(element) : null;
|
|
508
|
+
const groupKey = trimText(repeatRoot?.getAttribute("data-imagicma-repeat-group"));
|
|
509
|
+
const itemKey = trimText(repeatRoot?.getAttribute("data-imagicma-sort-key"));
|
|
510
|
+
if (!groupKey || !itemKey) return null;
|
|
511
|
+
return `${groupKey}::${itemKey}`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function getIndexedElementsByNodeKey(state: RuntimeState, nodeKey: string): HTMLElement[] {
|
|
515
|
+
const cached = state.nodeIdIndex.get(nodeKey)?.filter((element) => element.isConnected) ?? [];
|
|
516
|
+
if (cached.length > 0) return cached;
|
|
517
|
+
|
|
518
|
+
return Array.from(
|
|
519
|
+
document.querySelectorAll<HTMLElement>(
|
|
520
|
+
`[data-imagicma-node-id="${escapeAttributeValue(nodeKey)}"]`,
|
|
521
|
+
),
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function isRepeatScopedNodeIdentity(peers: HTMLElement[]): boolean {
|
|
526
|
+
if (peers.length <= 1) return false;
|
|
527
|
+
|
|
528
|
+
const seenScopes = new Set<string>();
|
|
529
|
+
for (const peer of peers) {
|
|
530
|
+
const scopeKey = getRepeatItemScopeKey(peer);
|
|
531
|
+
if (!scopeKey || seenScopes.has(scopeKey)) {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
seenScopes.add(scopeKey);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return seenScopes.size === peers.length;
|
|
538
|
+
}
|
|
539
|
+
|
|
409
540
|
function getDirectSemanticSiblingItems(parent: HTMLElement | null): HTMLElement[] {
|
|
410
541
|
if (!parent) return [];
|
|
411
542
|
|
|
@@ -606,6 +737,12 @@ function isSimpleTextElement(element: HTMLElement): boolean {
|
|
|
606
737
|
return trimText(element.textContent).length > 0;
|
|
607
738
|
}
|
|
608
739
|
|
|
740
|
+
function getRuntimeTextSegmentIndex(element: HTMLElement): number | null {
|
|
741
|
+
if (!isRuntimeTextSegmentElement(element)) return null;
|
|
742
|
+
const rawValue = Number(trimText(element.getAttribute(RUNTIME_TEXT_SEGMENT_INDEX_ATTR)));
|
|
743
|
+
return Number.isInteger(rawValue) && rawValue >= 0 ? rawValue : null;
|
|
744
|
+
}
|
|
745
|
+
|
|
609
746
|
const INLINE_STYLE_BINDINGS: Array<{ field: PreviewSourceStyleField; cssName: string }> = [
|
|
610
747
|
{ field: "fontSize", cssName: "font-size" },
|
|
611
748
|
{ field: "fontWeight", cssName: "font-weight" },
|
|
@@ -651,7 +788,14 @@ function buildSourceBindings(element: HTMLElement, source: ReturnType<typeof get
|
|
|
651
788
|
},
|
|
652
789
|
};
|
|
653
790
|
|
|
654
|
-
|
|
791
|
+
const textSegmentIndex = getRuntimeTextSegmentIndex(element);
|
|
792
|
+
if (textSegmentIndex !== null) {
|
|
793
|
+
bindings.textContent = {
|
|
794
|
+
kind: "jsx-text-segment",
|
|
795
|
+
source,
|
|
796
|
+
segmentIndex: textSegmentIndex,
|
|
797
|
+
};
|
|
798
|
+
} else if (isSimpleTextElement(element)) {
|
|
655
799
|
bindings.textContent = {
|
|
656
800
|
kind: "jsx-text",
|
|
657
801
|
source,
|
|
@@ -1617,7 +1761,7 @@ function applySortOverrides(state: RuntimeState, entry: PreviewOverridePageEntry
|
|
|
1617
1761
|
}
|
|
1618
1762
|
|
|
1619
1763
|
function reapplyVisualState(state: RuntimeState) {
|
|
1620
|
-
hydrateRuntimeSemantics();
|
|
1764
|
+
hydrateRuntimeSemantics(state);
|
|
1621
1765
|
resetDraftDom(state);
|
|
1622
1766
|
|
|
1623
1767
|
const styleEl = ensureDraftStyleEl(state);
|
|
@@ -1648,18 +1792,33 @@ function reapplyVisualState(state: RuntimeState) {
|
|
|
1648
1792
|
}
|
|
1649
1793
|
}
|
|
1650
1794
|
|
|
1651
|
-
function isNodeSelectableElement(element: HTMLElement): boolean {
|
|
1652
|
-
|
|
1795
|
+
function isNodeSelectableElement(state: RuntimeState, element: HTMLElement): boolean {
|
|
1796
|
+
const nodeId = getElementNodeId(element);
|
|
1797
|
+
if (!nodeId) return false;
|
|
1798
|
+
|
|
1799
|
+
const peers = getIndexedElementsByNodeKey(state, nodeId);
|
|
1800
|
+
return peers.length <= 1 || isRepeatScopedNodeIdentity(peers);
|
|
1653
1801
|
}
|
|
1654
1802
|
|
|
1655
1803
|
function isOverlayElement(state: RuntimeState, element: HTMLElement): boolean {
|
|
1656
1804
|
return Boolean(state.overlayRoot && state.overlayRoot.contains(element));
|
|
1657
1805
|
}
|
|
1658
1806
|
|
|
1659
|
-
function findElementByNodeKey(nodeKey: string): HTMLElement | null {
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1807
|
+
function findElementByNodeKey(state: RuntimeState, nodeKey: string): HTMLElement | null {
|
|
1808
|
+
const peers = getIndexedElementsByNodeKey(state, nodeKey);
|
|
1809
|
+
return peers.length === 1 ? peers[0] ?? null : null;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function resolveSelectedElementForSync(state: RuntimeState, nodeKey: string | null): HTMLElement | null {
|
|
1813
|
+
const normalizedNodeKey = trimText(nodeKey);
|
|
1814
|
+
if (!normalizedNodeKey) return null;
|
|
1815
|
+
|
|
1816
|
+
const currentSelection = state.selectedElement;
|
|
1817
|
+
if (currentSelection?.isConnected && getElementNodeId(currentSelection) === normalizedNodeKey) {
|
|
1818
|
+
return currentSelection;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
return findElementByNodeKey(state, normalizedNodeKey);
|
|
1663
1822
|
}
|
|
1664
1823
|
|
|
1665
1824
|
function buildSemanticPath(target: HTMLElement, event: MouseEvent): HTMLElement[] {
|
|
@@ -1686,7 +1845,7 @@ function findSelectableElement(state: RuntimeState, event: MouseEvent): HTMLElem
|
|
|
1686
1845
|
return null;
|
|
1687
1846
|
}
|
|
1688
1847
|
|
|
1689
|
-
return buildSemanticPath(hit, event).find((candidate) => !isOverlayElement(state, candidate) && isNodeSelectableElement(candidate)) ?? null;
|
|
1848
|
+
return buildSemanticPath(hit, event).find((candidate) => !isOverlayElement(state, candidate) && isNodeSelectableElement(state, candidate)) ?? null;
|
|
1690
1849
|
}
|
|
1691
1850
|
|
|
1692
1851
|
function postToParent(state: RuntimeState, message: Record<string, unknown>) {
|
|
@@ -1707,6 +1866,7 @@ function createRuntimeState(): RuntimeState {
|
|
|
1707
1866
|
parentOrigin: getBoundPreviewParentOrigin(),
|
|
1708
1867
|
activeSessionId: null,
|
|
1709
1868
|
enabled: false,
|
|
1869
|
+
nodeIdIndex: new Map(),
|
|
1710
1870
|
selectedElement: null,
|
|
1711
1871
|
hoveredElement: null,
|
|
1712
1872
|
overlayRoot: null,
|
|
@@ -1768,7 +1928,7 @@ export function installPreviewPickerRuntime() {
|
|
|
1768
1928
|
parentOrigin: state.parentOrigin,
|
|
1769
1929
|
frameInstanceId: state.frameInstanceId,
|
|
1770
1930
|
});
|
|
1771
|
-
hydrateRuntimeSemantics();
|
|
1931
|
+
hydrateRuntimeSemantics(state);
|
|
1772
1932
|
state.throttledRecalculate = throttleRAF(() => {
|
|
1773
1933
|
reapplyVisualState(state);
|
|
1774
1934
|
});
|
|
@@ -1822,7 +1982,7 @@ export function installPreviewPickerRuntime() {
|
|
|
1822
1982
|
window.addEventListener("resize", () => state.throttledRecalculate?.());
|
|
1823
1983
|
state.mutationObserver = new MutationObserver(() => {
|
|
1824
1984
|
if (state.suppressMutationObserver > 0) return;
|
|
1825
|
-
hydrateRuntimeSemantics();
|
|
1985
|
+
hydrateRuntimeSemantics(state);
|
|
1826
1986
|
state.throttledRecalculate?.();
|
|
1827
1987
|
});
|
|
1828
1988
|
state.mutationObserver.observe(document.body, {
|
|
@@ -1940,7 +2100,7 @@ export function installPreviewPickerRuntime() {
|
|
|
1940
2100
|
if (event.data.frameInstanceId !== state.frameInstanceId) return;
|
|
1941
2101
|
|
|
1942
2102
|
if (event.data.type === "IMAGICMA_PICKER_STATE_SYNC") {
|
|
1943
|
-
hydrateRuntimeSemantics();
|
|
2103
|
+
hydrateRuntimeSemantics(state);
|
|
1944
2104
|
state.activeSessionId = event.data.sessionId;
|
|
1945
2105
|
state.enabled = event.data.payload.mode === "picking";
|
|
1946
2106
|
state.persistedOverrides = event.data.payload.overrides;
|
|
@@ -1952,9 +2112,7 @@ export function installPreviewPickerRuntime() {
|
|
|
1952
2112
|
orderedSortKeys: event.data.payload.pendingSort.orderedSortKeys,
|
|
1953
2113
|
}
|
|
1954
2114
|
: null;
|
|
1955
|
-
state.selectedElement = event.data.payload.selectedNodeId
|
|
1956
|
-
? findElementByNodeKey(event.data.payload.selectedNodeId)
|
|
1957
|
-
: null;
|
|
2115
|
+
state.selectedElement = resolveSelectedElementForSync(state, event.data.payload.selectedNodeId);
|
|
1958
2116
|
state.hoveredElement = null;
|
|
1959
2117
|
clearOverlay(state);
|
|
1960
2118
|
const sortableMetadata = state.selectedElement ? getSortableMetadata(state.selectedElement) : null;
|
|
@@ -8,6 +8,11 @@ const LAUNCH_TOKEN_FILE = path.resolve(
|
|
|
8
8
|
".imagicma",
|
|
9
9
|
"launch-token.json",
|
|
10
10
|
);
|
|
11
|
+
const RUNTIME_ENV_FILE = path.resolve(
|
|
12
|
+
process.cwd(),
|
|
13
|
+
".imagicma",
|
|
14
|
+
"runtime.env",
|
|
15
|
+
);
|
|
11
16
|
|
|
12
17
|
function isScriptLaunch(mode: "dev" | "start") {
|
|
13
18
|
return (
|
|
@@ -51,18 +56,47 @@ async function assertStartAuthorized() {
|
|
|
51
56
|
);
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
async function readRuntimeEnvPort() {
|
|
60
|
+
try {
|
|
61
|
+
const raw = await fs.readFile(RUNTIME_ENV_FILE, "utf8");
|
|
62
|
+
const lines = raw.split(/\r?\n/);
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
const trimmed = line.trim();
|
|
65
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
66
|
+
const separatorIndex = trimmed.indexOf("=");
|
|
67
|
+
if (separatorIndex <= 0) continue;
|
|
68
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
69
|
+
if (key !== "PORT") continue;
|
|
70
|
+
return trimmed.slice(separatorIndex + 1).trim();
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function resolveRuntimePort(raw = process.env.PORT) {
|
|
83
|
+
let candidate = raw;
|
|
84
|
+
if (candidate === undefined || candidate === null || candidate === "") {
|
|
85
|
+
candidate = await readRuntimeEnvPort();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (candidate === undefined || candidate === null || candidate === "") {
|
|
89
|
+
throw new Error("[imagicma] 缺少端口配置:请通过 .imagicma/runtime.env 或 PORT 提供运行时端口");
|
|
57
90
|
}
|
|
58
91
|
|
|
59
|
-
const port = Number(
|
|
92
|
+
const port = Number(candidate);
|
|
60
93
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
61
94
|
throw new Error(
|
|
62
|
-
`[imagicma] 无效端口配置:PORT=${JSON.stringify(
|
|
95
|
+
`[imagicma] 无效端口配置:PORT=${JSON.stringify(candidate)}(期望 1-65535 的整数)`,
|
|
63
96
|
);
|
|
64
97
|
}
|
|
65
98
|
|
|
99
|
+
process.env.PORT = String(port);
|
|
66
100
|
return port;
|
|
67
101
|
}
|
|
68
102
|
|
|
@@ -70,7 +104,7 @@ async function main() {
|
|
|
70
104
|
await assertStartAuthorized();
|
|
71
105
|
|
|
72
106
|
const app = createApp({ serveClient: true });
|
|
73
|
-
const port = resolveRuntimePort();
|
|
107
|
+
const port = await resolveRuntimePort();
|
|
74
108
|
|
|
75
109
|
serve(
|
|
76
110
|
{
|
|
@@ -14,6 +14,11 @@ const LAUNCH_TOKEN_FILE = path.resolve(
|
|
|
14
14
|
".imagicma",
|
|
15
15
|
"launch-token.json",
|
|
16
16
|
);
|
|
17
|
+
const RUNTIME_ENV_FILE = path.resolve(
|
|
18
|
+
__dirname,
|
|
19
|
+
".imagicma",
|
|
20
|
+
"runtime.env",
|
|
21
|
+
);
|
|
17
22
|
|
|
18
23
|
function isScriptLaunch(mode: "dev" | "start") {
|
|
19
24
|
return (
|
|
@@ -22,18 +27,47 @@ function isScriptLaunch(mode: "dev" | "start") {
|
|
|
22
27
|
);
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
async function readRuntimeEnvPort() {
|
|
31
|
+
try {
|
|
32
|
+
const raw = await fs.readFile(RUNTIME_ENV_FILE, "utf8");
|
|
33
|
+
const lines = raw.split(/\r?\n/);
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
const trimmed = line.trim();
|
|
36
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
37
|
+
const separatorIndex = trimmed.indexOf("=");
|
|
38
|
+
if (separatorIndex <= 0) continue;
|
|
39
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
40
|
+
if (key !== "PORT") continue;
|
|
41
|
+
return trimmed.slice(separatorIndex + 1).trim();
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function resolveRuntimePort(raw = process.env.PORT) {
|
|
54
|
+
let candidate = raw;
|
|
55
|
+
if (candidate === undefined || candidate === null || candidate === "") {
|
|
56
|
+
candidate = await readRuntimeEnvPort();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (candidate === undefined || candidate === null || candidate === "") {
|
|
60
|
+
throw new Error("[imagicma] 缺少端口配置:请通过 .imagicma/runtime.env 或 PORT 提供运行时端口");
|
|
28
61
|
}
|
|
29
62
|
|
|
30
|
-
const port = Number(
|
|
63
|
+
const port = Number(candidate);
|
|
31
64
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
32
65
|
throw new Error(
|
|
33
|
-
`[imagicma] 无效端口配置:PORT=${JSON.stringify(
|
|
66
|
+
`[imagicma] 无效端口配置:PORT=${JSON.stringify(candidate)}(期望 1-65535 的整数)`,
|
|
34
67
|
);
|
|
35
68
|
}
|
|
36
69
|
|
|
70
|
+
process.env.PORT = String(port);
|
|
37
71
|
return port;
|
|
38
72
|
}
|
|
39
73
|
|
|
@@ -73,7 +107,7 @@ async function assertLaunchAuthorized(mode: "dev" | "start") {
|
|
|
73
107
|
}
|
|
74
108
|
|
|
75
109
|
export default defineConfig(async ({ command }) => {
|
|
76
|
-
const runtimePort = command === "serve" ? resolveRuntimePort() : null;
|
|
110
|
+
const runtimePort = command === "serve" ? await resolveRuntimePort() : null;
|
|
77
111
|
const enableComponentDebugger = command === "serve";
|
|
78
112
|
const componentDebugger =
|
|
79
113
|
enableComponentDebugger
|