@thomas.f/prnext 0.1.15-alpha → 0.1.17-alpha
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/AI.md +63 -0
- package/README.md +387 -12
- package/build/ADAPTERS.md +27 -2
- package/build/actions.mjs +5 -0
- package/build/analyze.mjs +184 -0
- package/build/app-config.mjs +7 -8
- package/build/app-static.mjs +12 -7
- package/build/app.mjs +23 -10
- package/build/build-lock.mjs +6 -6
- package/build/bundle-runtime.mjs +20 -5
- package/build/check.mjs +56 -11
- package/build/compiler.mjs +25 -1
- package/build/concurrent.mjs +12 -0
- package/build/config.mjs +21 -13
- package/build/css-modules.mjs +15 -4
- package/build/dev-compiler-health.mjs +11 -0
- package/build/dev-css-update.mjs +83 -0
- package/build/dev-dependencies.mjs +44 -0
- package/build/dev-discovery.mjs +28 -0
- package/build/dev-worker.mjs +14 -5
- package/build/dev.mjs +55 -4
- package/build/edge.mjs +3 -2
- package/build/framework-imports.mjs +40 -0
- package/build/incremental.mjs +8 -4
- package/build/index.mjs +96 -101
- package/build/inspect.mjs +3 -0
- package/build/inspection-summary.mjs +37 -4
- package/build/loader-cache.mjs +53 -26
- package/build/metadata-routes.mjs +2 -3
- package/build/migrate.mjs +60 -57
- package/build/npm-packages.mjs +3 -18
- package/build/package-manager.mjs +308 -0
- package/build/persistent-cache.mjs +97 -0
- package/build/precompress.mjs +62 -30
- package/build/prerender-pages.mjs +90 -0
- package/build/prerender-worker.mjs +10 -0
- package/build/production-builder.mjs +80 -0
- package/build/scan.mjs +4 -4
- package/build/source-analysis.mjs +42 -0
- package/build/static-adapter.mjs +13 -5
- package/build/static-input.mjs +48 -0
- package/build/styles.mjs +15 -3
- package/build/transform-cache.mjs +37 -4
- package/build/type-command.mjs +41 -0
- package/build/typed-routes.mjs +91 -0
- package/build/typescript-dev-worker.mjs +18 -0
- package/build/typescript-dev.mjs +22 -0
- package/build/typescript-watch.mjs +22 -0
- package/build/typescript.mjs +5 -4
- package/build/webpack-graph.mjs +164 -27
- package/build/worker-preload.mjs +9 -0
- package/cli.mjs +105 -121
- package/compat/index.d.ts +21 -7
- package/compat/link.d.cts +4 -3
- package/compat/navigation.d.cts +4 -3
- package/compat/router.d.cts +4 -3
- package/compat/typed-route.d.cts +17 -0
- package/docs/ai/compatibility.md +106 -0
- package/docs/ai/data.md +128 -0
- package/docs/ai/deployment.md +111 -0
- package/docs/ai/manifest.json +11 -0
- package/docs/ai/quickstart.md +146 -0
- package/docs/ai/routing.md +132 -0
- package/docs/ai/troubleshooting.md +120 -0
- package/index.mjs +2 -0
- package/native/diagnostics.mjs +34 -0
- package/native/resolve.mjs +7 -2
- package/package.json +28 -10
- package/release/ai-docs.mjs +48 -0
- package/release/verify.mjs +6 -0
- package/runtime/ai-docs.mjs +202 -0
- package/runtime/app-client.mjs +17 -6
- package/runtime/app-render.mjs +12 -0
- package/runtime/cloud/archive.mjs +5 -6
- package/runtime/compile-cache.mjs +8 -0
- package/runtime/dev-ai-brands.mjs +26 -0
- package/runtime/dev-ai-style.mjs +99 -0
- package/runtime/dev-build-reasons.mjs +25 -0
- package/runtime/dev-cache-control.mjs +53 -0
- package/runtime/dev-client.mjs +239 -45
- package/runtime/dev-console-copy.mjs +19 -0
- package/runtime/dev-console.mjs +106 -0
- package/runtime/dev-dependency-graph.mjs +40 -0
- package/runtime/dev-element-picker-style.mjs +27 -0
- package/runtime/dev-element-picker.mjs +213 -0
- package/runtime/dev-error-overlay-style.mjs +41 -0
- package/runtime/dev-error-overlay.mjs +167 -0
- package/runtime/dev-error-report.mjs +121 -0
- package/runtime/dev-file-changes.mjs +1 -1
- package/runtime/dev-gateway.mjs +44 -0
- package/runtime/dev-host-relay.mjs +66 -0
- package/runtime/dev-indicator.mjs +186 -0
- package/runtime/dev-issue-motion.mjs +214 -0
- package/runtime/dev-lazy-tool.mjs +18 -0
- package/runtime/dev-logo.mjs +2 -0
- package/runtime/dev-notification.mjs +81 -0
- package/runtime/dev-popout.mjs +34 -0
- package/runtime/dev-position.mjs +107 -0
- package/runtime/dev-preview-touch.mjs +122 -0
- package/runtime/dev-runtime-pool.mjs +45 -0
- package/runtime/dev-runtime-spare.mjs +57 -0
- package/runtime/dev-tools-client.mjs +155 -0
- package/runtime/dev-tools-mcp.mjs +126 -0
- package/runtime/dev-tools-metrics.mjs +31 -0
- package/runtime/dev-tools-model.mjs +42 -0
- package/runtime/dev-tools-panel.mjs +341 -0
- package/runtime/dev-tools-result.mjs +37 -0
- package/runtime/dev-tools-server.mjs +256 -0
- package/runtime/dev-tools-source.mjs +104 -0
- package/runtime/dev-tooltip.mjs +94 -0
- package/runtime/dev-ui-icons.mjs +8 -0
- package/runtime/dev-workbench-style.mjs +106 -0
- package/runtime/dev-workbench.mjs +428 -0
- package/runtime/dev-worker-logs.mjs +31 -0
- package/runtime/development.mjs +307 -0
- package/runtime/env.mjs +2 -1
- package/runtime/pages-client.mjs +10 -1
- package/runtime/pages-navigation.mjs +6 -1
- package/runtime/persistent/client.mjs +6 -5
- package/runtime/persistent/daemon.mjs +66 -56
- package/runtime/persistent/native-proxy.mjs +63 -0
- package/runtime/preload.mjs +12 -0
- package/runtime/rsc-worker.mjs +7 -0
- package/runtime/typed-route.mjs +19 -0
- package/runtime/watch-project.mjs +12 -5
- package/runtime/worker.mjs +36 -2
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { svg } from './dev-ui-icons.mjs';
|
|
2
|
+
import { devErrorOverlayStyle } from './dev-error-overlay-style.mjs';
|
|
3
|
+
|
|
4
|
+
const hydrationMessage = /\bHydration failed because\b|\bA tree hydrated but some attributes\b|\bText content does not match server-rendered HTML\b/i;
|
|
5
|
+
const buildSourcePrefix = /^(?:(?:[A-Za-z]:[\\/]|\/)[^:\r\n]*|[^\s:\r\n]+)\.(?:[cm]?[jt]sx?|css|scss|sass|less|json|html?|vue|svelte|astro)(?:\(\d+(?:,\d+)?\)|:\d+(?::\d+)?)?:\s*/i;
|
|
6
|
+
|
|
7
|
+
function stripBuildSourcePrefixes(line) {
|
|
8
|
+
// Compiler errors can nest an entry filename around an original source path.
|
|
9
|
+
// Remove only recognizable source prefixes, never arbitrary colon-separated text.
|
|
10
|
+
for (let count = 0; count < 16; count++) {
|
|
11
|
+
const next = line.replace(buildSourcePrefix, '');
|
|
12
|
+
if (next === line) break;
|
|
13
|
+
line = next;
|
|
14
|
+
}
|
|
15
|
+
return line;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Only the summary is shortened. The original message and stack stay in the dialog.
|
|
19
|
+
export function summarizeDevError(report, { recoverable = false, kind = 'runtime' } = {}) {
|
|
20
|
+
const message = typeof report === 'string' ? report : String(report?.message ?? '');
|
|
21
|
+
const normalizedKind = String(kind).toLowerCase();
|
|
22
|
+
const hydration = hydrationMessage.test(message);
|
|
23
|
+
const title = hydration ? 'Hydration mismatch' : ['typescript', 'type', 'typecheck'].includes(normalizedKind) ? 'TypeScript'
|
|
24
|
+
: normalizedKind === 'build' ? 'Build error' : recoverable || normalizedKind === 'recoverable' ? 'Recovered error' : 'Runtime error';
|
|
25
|
+
let firstLine = message.split(/\r?\n/).find(line => line.trim())?.replace(/^(?:Uncaught\s+)?(?:[\w.]*Error):\s*/, '').trim() || '';
|
|
26
|
+
if (normalizedKind === 'build') firstLine = stripBuildSourcePrefixes(firstLine);
|
|
27
|
+
const description = hydration ? 'The server and browser rendered different output.' : firstLine
|
|
28
|
+
|| (title === 'Build error' ? 'Compilation could not finish.' : title === 'TypeScript' ? 'TypeScript reported a problem in your project.' : 'An error occurred while running this page.');
|
|
29
|
+
return { title, description: description.length > 240 ? description.slice(0, 237).trimEnd() + '…' : description, hydration };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createDevErrorOverlay({ onDismiss = () => {}, onSendToAI = () => {}, onSelectIssue = () => {}, onCopyForAI = () => {} } = {}) {
|
|
33
|
+
const document = globalThis.document;
|
|
34
|
+
const node = (tag, className, text) => { const element = document.createElement(tag); if (className) element.className = className; if (text !== undefined) element.textContent = text; return element; };
|
|
35
|
+
const host = node('div'); host.id = '__prnext_dev_error__';
|
|
36
|
+
const shadow = host.attachShadow({ mode: 'open' }), style = node('style'); style.textContent = devErrorOverlayStyle;
|
|
37
|
+
const dialog = node('dialog'); dialog.setAttribute('role', 'alertdialog'); dialog.setAttribute('aria-label', 'PRNext development error');
|
|
38
|
+
const icon = paths => { const result = node('span', 'icon'); result.innerHTML = svg(paths); return result; };
|
|
39
|
+
const header = node('header', 'error-header'), brand = node('div', 'error-brand');
|
|
40
|
+
const count = node('span', 'error-count'); count.setAttribute('aria-live', 'polite'); count.setAttribute('aria-atomic', 'true');
|
|
41
|
+
brand.append(node('strong', '', 'PRNext'), node('span', 'error-brand-label', 'Errors'), count);
|
|
42
|
+
const actions = node('div', 'error-header-actions');
|
|
43
|
+
const copy = node('button', 'error-copy'), copyIcon = icon(''), copyLabel = node('span', '', 'Copy for AI'); copy.type = 'button'; copy.id = 'copy-for-ai'; copy.append(copyIcon, copyLabel);
|
|
44
|
+
const ai = node('button', 'error-ai'); ai.type = 'button'; ai.append(icon('<path d="m12 3 2.2 6.8L21 12l-6.8 2.2L12 21l-2.2-6.8L3 12l6.8-2.2L12 3Z"/>'), node('span', '', 'Send to AI'));
|
|
45
|
+
const dismiss = node('button', 'error-dismiss'); dismiss.type = 'button'; dismiss.autofocus = true; dismiss.setAttribute('aria-label', 'Dismiss'); dismiss.title = 'Dismiss (Esc)'; dismiss.append(icon('<path d="m6 6 12 12M6 18 18 6"/>'));
|
|
46
|
+
const copyStatus = node('span', 'sr-only'); copyStatus.id = 'error-copy-status'; copyStatus.setAttribute('role', 'status'); copyStatus.setAttribute('aria-atomic', 'true');
|
|
47
|
+
actions.append(copy, ai, dismiss); header.append(brand, actions, copyStatus);
|
|
48
|
+
const issueRegion = node('section', 'error-issue-region');
|
|
49
|
+
const issueList = node('div', 'error-list'); issueList.setAttribute('role', 'group'); issueList.setAttribute('aria-label', 'Errors');
|
|
50
|
+
const omittedNote = node('p', 'error-list-note'); omittedNote.hidden = true; issueRegion.append(issueList, omittedNote);
|
|
51
|
+
const body = node('div', 'error-body'), intro = node('div', 'error-intro'), heading = node('h1'), title = node('button', 'error-title-toggle'), summary = node('p', 'error-summary');
|
|
52
|
+
body.id = 'error-details'; body.hidden = true; body.setAttribute('role', 'region'); body.setAttribute('aria-labelledby', 'error-title');
|
|
53
|
+
title.type = 'button'; title.id = 'error-title'; title.setAttribute('aria-controls', body.id); title.setAttribute('aria-expanded', 'false');
|
|
54
|
+
summary.id = 'error-summary'; heading.append(title); intro.append(heading, summary);
|
|
55
|
+
const editorActions = node('div', 'error-editor-actions'), editor = node('a'); editor.id = 'editor'; editor.hidden = true; editorActions.hidden = true;
|
|
56
|
+
editor.append(icon('<path d="M14 3h7v7m0-7L10 14"/><path d="M10 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5"/>'), node('span', '', 'Open in editor'));
|
|
57
|
+
editorActions.append(editor);
|
|
58
|
+
const source = node('section', 'source'); source.id = 'source'; source.hidden = true; source.setAttribute('aria-label', 'Source');
|
|
59
|
+
const sourceHead = node('div', 'source-head'), location = node('code', 'location'); location.id = 'source-location';
|
|
60
|
+
sourceHead.append(icon('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6Z"/><path d="M14 2v6h6"/>'), location);
|
|
61
|
+
const sourceFrame = node('pre'); sourceFrame.id = 'source-frame'; sourceFrame.tabIndex = 0; sourceFrame.setAttribute('aria-label', 'Source code');
|
|
62
|
+
source.append(sourceHead, sourceFrame);
|
|
63
|
+
const disclosures = node('div', 'error-disclosures');
|
|
64
|
+
const messageDisclosure = node('details', 'error-disclosure'); messageDisclosure.id = 'message-disclosure';
|
|
65
|
+
const fullMessage = node('pre', 'error-full'); fullMessage.id = 'message'; fullMessage.tabIndex = 0; fullMessage.setAttribute('aria-label', 'Full error message');
|
|
66
|
+
messageDisclosure.append(node('summary', '', 'Full message'), fullMessage);
|
|
67
|
+
const stackDisclosure = node('details', 'error-disclosure'); stackDisclosure.id = 'stack-disclosure';
|
|
68
|
+
const stack = node('pre', 'error-full error-stack'); stack.id = 'details'; stack.tabIndex = 0; stack.setAttribute('aria-label', 'Error stack trace');
|
|
69
|
+
stackDisclosure.append(node('summary', '', 'Stack trace'), stack); disclosures.append(messageDisclosure, stackDisclosure);
|
|
70
|
+
const sourceStatus = node('p', 'error-source-status'); sourceStatus.id = 'source-feedback'; sourceStatus.setAttribute('role', 'status');
|
|
71
|
+
body.append(intro, source, editorActions, disclosures, sourceStatus); dialog.append(header, issueRegion, body); shadow.append(style, dialog);
|
|
72
|
+
const rows = new Map();
|
|
73
|
+
let disposed = false, lastReport, selectedId, dismissing = false, expanded = false;
|
|
74
|
+
function setExpanded(value) {
|
|
75
|
+
expanded = Boolean(value);
|
|
76
|
+
const restoreFocus = !expanded && body.contains(shadow.activeElement);
|
|
77
|
+
body.hidden = !expanded; title.setAttribute('aria-expanded', String(expanded));
|
|
78
|
+
for (const [id, row] of rows) row.button.setAttribute('aria-expanded', String(expanded && id === selectedId));
|
|
79
|
+
if (expanded) dialog.setAttribute('aria-describedby', summary.id); else dialog.removeAttribute('aria-describedby');
|
|
80
|
+
if (restoreFocus) rows.get(selectedId)?.button.focus({ preventScroll: true });
|
|
81
|
+
}
|
|
82
|
+
title.onclick = () => { if (!disposed) setExpanded(!expanded); };
|
|
83
|
+
function requestDismiss() {
|
|
84
|
+
if (disposed || dismissing) return;
|
|
85
|
+
dismissing = true; if (dialog.open) dialog.close(); onDismiss();
|
|
86
|
+
}
|
|
87
|
+
dismiss.onclick = requestDismiss;
|
|
88
|
+
dialog.addEventListener('cancel', event => { event.preventDefault(); requestDismiss(); });
|
|
89
|
+
ai.onclick = () => { if (!disposed) onSendToAI(); };
|
|
90
|
+
copy.onclick = () => { if (!disposed) onCopyForAI(); };
|
|
91
|
+
function setCopyStatus(value = 'idle') {
|
|
92
|
+
if (disposed) return;
|
|
93
|
+
const status = value === 'copied' || value === 'error' ? value : 'idle';
|
|
94
|
+
copy.dataset.status = status;
|
|
95
|
+
copyLabel.textContent = status === 'copied' ? 'Copied' : status === 'error' ? 'Copy failed' : 'Copy for AI';
|
|
96
|
+
copy.setAttribute('aria-label', status === 'idle' ? 'Copy for AI' : `Copy for AI. ${copyLabel.textContent}.`);
|
|
97
|
+
copyIcon.innerHTML = svg(status === 'copied' ? '<path d="m5 12 4 4L19 6"/>' : status === 'error' ? '<circle cx="12" cy="12" r="9"/><path d="M12 7v6m0 3h.01"/>' : '<rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V4H4v12h4"/>');
|
|
98
|
+
copyStatus.textContent = status === 'copied' ? 'Copied for AI.' : status === 'error' ? 'Could not copy for AI.' : '';
|
|
99
|
+
}
|
|
100
|
+
function syncIssues(report, options) {
|
|
101
|
+
const issues = Array.isArray(options.issues) && options.issues.length ? options.issues : [{ id: 'current', report, kind: options.kind, recoverable: options.recoverable }];
|
|
102
|
+
const nextSelected = issues.find(issue => issue.id === options.selectedId)?.id ?? issues.find(issue => issue.report === report)?.id ?? issues[0].id;
|
|
103
|
+
const live = new Set(issues.map(issue => issue.id));
|
|
104
|
+
for (const [id, row] of rows) if (!live.has(id)) { row.button.remove(); rows.delete(id); }
|
|
105
|
+
issues.forEach((issue, index) => {
|
|
106
|
+
let row = rows.get(issue.id);
|
|
107
|
+
if (!row) {
|
|
108
|
+
const button = node('button', 'error-row'); button.type = 'button'; button.dataset.issueId = String(issue.id);
|
|
109
|
+
const type = node('span', 'error-row-type'), dot = node('span', 'error-row-dot'), label = node('span'); dot.setAttribute('aria-hidden', 'true'); type.append(dot, label);
|
|
110
|
+
const message = node('span', 'error-row-message'), file = node('code', 'error-row-file'); file.hidden = true;
|
|
111
|
+
const chevron = node('span', 'error-row-chevron'); chevron.setAttribute('aria-hidden', 'true');
|
|
112
|
+
button.setAttribute('aria-controls', body.id);
|
|
113
|
+
button.append(type, message, file, chevron); button.onclick = () => {
|
|
114
|
+
if (disposed) return;
|
|
115
|
+
const next = selectedId !== issue.id || !expanded;
|
|
116
|
+
onSelectIssue(issue.id); setExpanded(next);
|
|
117
|
+
};
|
|
118
|
+
row = { button, label, message, file }; rows.set(issue.id, row);
|
|
119
|
+
}
|
|
120
|
+
const next = summarizeDevError(issue.report, issue);
|
|
121
|
+
const message = typeof issue.report === 'string' ? issue.report : String(issue.report?.message ?? ''), stack = String(issue.report?.stack ?? '');
|
|
122
|
+
if (row.reportMessage !== message || row.reportStack !== stack || row.kind !== issue.kind) { row.file.textContent = ''; row.file.hidden = true; }
|
|
123
|
+
row.reportMessage = message; row.reportStack = stack; row.kind = issue.kind; row.label.textContent = next.title; row.message.textContent = next.description; row.message.title = next.description;
|
|
124
|
+
row.message.hidden = next.hydration;
|
|
125
|
+
row.button.dataset.kind = next.hydration ? 'hydration' : issue.kind || 'runtime'; row.button.dataset.recoverable = String(issue.recoverable === true); row.button.setAttribute('aria-pressed', String(issue.id === nextSelected));
|
|
126
|
+
row.button.setAttribute('aria-expanded', String(expanded && issue.id === nextSelected));
|
|
127
|
+
if (issueList.children[index] !== row.button) issueList.insertBefore(row.button, issueList.children[index] || null);
|
|
128
|
+
});
|
|
129
|
+
const omitted = Number.isFinite(options.omitted) ? Math.max(0, Math.trunc(options.omitted)) : 0, total = issues.length + omitted;
|
|
130
|
+
count.textContent = String(total); count.setAttribute('aria-label', `${total} ${total === 1 ? 'error' : 'errors'}`);
|
|
131
|
+
omittedNote.hidden = omitted === 0; omittedNote.textContent = omitted ? `${issues.length} shown · ${omitted} more` : '';
|
|
132
|
+
return nextSelected;
|
|
133
|
+
}
|
|
134
|
+
function setSourceStatus(text) { if (!disposed) sourceStatus.textContent = text || ''; }
|
|
135
|
+
function setSource(value) {
|
|
136
|
+
if (disposed) return;
|
|
137
|
+
const file = typeof value?.file === 'string' ? value.file : '';
|
|
138
|
+
const line = Number.isFinite(value?.line) ? value.line : undefined, column = Number.isFinite(value?.column) ? value.column : undefined;
|
|
139
|
+
location.textContent = file + (line ? ':' + line : '') + (column ? ':' + column : '');
|
|
140
|
+
location.title = location.textContent;
|
|
141
|
+
const frame = typeof value?.frame === 'string' ? value.frame : Array.isArray(value?.excerpt) ? value.excerpt.map(entry => `${entry.line === line ? '>' : ' '} ${entry.line} | ${entry.text}`).join('\n') : '';
|
|
142
|
+
sourceFrame.textContent = frame; sourceFrame.hidden = !frame; sourceHead.hidden = !file; source.hidden = !file && !frame;
|
|
143
|
+
editor.hidden = !/^(?:vscode|cursor):\/\/file\//.test(value?.editorUrl || '');
|
|
144
|
+
if (editor.hidden) editor.removeAttribute('href'); else editor.href = value.editorUrl;
|
|
145
|
+
editorActions.hidden = editor.hidden;
|
|
146
|
+
const row = rows.get(selectedId); if (row) { row.file.textContent = location.textContent; row.file.title = location.textContent; row.file.hidden = !file; }
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
host,
|
|
150
|
+
render(report, options = {}) {
|
|
151
|
+
if (disposed) return;
|
|
152
|
+
const nextSelected = syncIssues(report, options), next = summarizeDevError(report, options), changed = report !== lastReport || nextSelected !== selectedId;
|
|
153
|
+
selectedId = nextSelected;
|
|
154
|
+
title.textContent = next.title; summary.textContent = next.description;
|
|
155
|
+
fullMessage.textContent = typeof report === 'string' ? report : String(report?.message ?? '');
|
|
156
|
+
stack.textContent = String(report?.stack ?? ''); stackDisclosure.hidden = !stack.textContent;
|
|
157
|
+
dialog.dataset.kind = next.hydration ? 'hydration' : options.kind || 'runtime'; dialog.dataset.recoverable = String(options.recoverable === true);
|
|
158
|
+
if (changed) { setExpanded(false); messageDisclosure.open = false; stackDisclosure.open = false; setSource(undefined); setSourceStatus(''); setCopyStatus('idle'); body.scrollTop = 0; }
|
|
159
|
+
lastReport = report; dismissing = false;
|
|
160
|
+
if (!host.isConnected) document.documentElement.append(host);
|
|
161
|
+
if (!dialog.open) { dialog.showModal(); dismiss.focus({ preventScroll: true }); }
|
|
162
|
+
},
|
|
163
|
+
setSource, setSourceStatus, setCopyStatus,
|
|
164
|
+
focusDismiss() { if (!disposed && dialog.open) dismiss.focus({ preventScroll: true }); },
|
|
165
|
+
dispose() { if (disposed) return; disposed = true; rows.clear(); if (dialog.open) dialog.close(); host.remove(); },
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Pure diagnostic snapshots and clipboard reports; no browser or connection state.
|
|
2
|
+
export const MAX_DEV_ISSUES = 50;
|
|
3
|
+
export const MAX_DEV_ISSUE_TEXT = 48 * 1024;
|
|
4
|
+
export const MAX_DEV_SOURCE_BYTES = 48 * 1024;
|
|
5
|
+
export const MAX_DEV_AI_REPORT_BYTES = 512 * 1024;
|
|
6
|
+
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
const read = (value, key) => { try { return value?.[key]; } catch { return undefined; } };
|
|
9
|
+
const string = value => { try { return String(value ?? ''); } catch { return '[unavailable diagnostic]'; } };
|
|
10
|
+
const provided = value => value !== undefined && value !== null;
|
|
11
|
+
|
|
12
|
+
function diagnostic(error) {
|
|
13
|
+
const name = read(error, 'name'), message = read(error, 'message'), stack = read(error, 'stack');
|
|
14
|
+
return { name: typeof name === 'string' ? name : 'Error', message: string(message || error), stack: string(stack || message || error) };
|
|
15
|
+
}
|
|
16
|
+
function boundedReport(value) {
|
|
17
|
+
return { name: value.name.slice(0, 256), message: value.message.slice(0, MAX_DEV_ISSUE_TEXT), stack: value.stack.slice(0, MAX_DEV_ISSUE_TEXT) };
|
|
18
|
+
}
|
|
19
|
+
const report = error => boundedReport(diagnostic(error));
|
|
20
|
+
function hash(value) {
|
|
21
|
+
let result = 2166136261;
|
|
22
|
+
for (let index = 0; index < value.length; index++) result = Math.imul(result ^ value.charCodeAt(index), 16777619);
|
|
23
|
+
return (result >>> 0).toString(36);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Current issues only. Preserve the original error identity separately from its bounded snapshot. */
|
|
27
|
+
export function collectDevIssues({ critical, criticalKind = 'runtime', typeErrors = [], recoverable } = {}) {
|
|
28
|
+
const types = [], unique = new Set(), groups = new Map();
|
|
29
|
+
for (const error of Array.isArray(typeErrors) ? typeErrors : []) {
|
|
30
|
+
const value = diagnostic(error), key = JSON.stringify([value.name, value.message, value.stack]);
|
|
31
|
+
if (unique.has(key)) continue;
|
|
32
|
+
unique.add(key);
|
|
33
|
+
const base = 'ts-' + hash(key), item = { key, base, error, report: boundedReport(value) };
|
|
34
|
+
types.push(item);
|
|
35
|
+
if (!groups.has(base)) groups.set(base, []);
|
|
36
|
+
groups.get(base).push(item);
|
|
37
|
+
}
|
|
38
|
+
// Rare short-hash collisions get deterministic suffixes independent of display order.
|
|
39
|
+
for (const group of groups.values()) {
|
|
40
|
+
group.sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0);
|
|
41
|
+
for (let index = 0; index < group.length; index++) group[index].id = group[index].base + (index ? '-' + (index + 1) : '');
|
|
42
|
+
}
|
|
43
|
+
const issues = [], hasCritical = provided(critical), hasRecoverable = provided(recoverable);
|
|
44
|
+
if (hasCritical) issues.push({ id: 'critical', error: critical, report: report(critical), kind: string(criticalKind || 'runtime').slice(0, 64), recoverable: false });
|
|
45
|
+
const available = MAX_DEV_ISSUES - Number(hasCritical) - Number(hasRecoverable);
|
|
46
|
+
for (const item of types.slice(0, available)) issues.push({ id: item.id, error: item.error, report: item.report, kind: 'typescript', recoverable: false });
|
|
47
|
+
if (hasRecoverable) issues.push({ id: 'recoverable', error: recoverable, report: report(recoverable), kind: 'recoverable', recoverable: true });
|
|
48
|
+
return { issues, omitted: Math.max(0, types.length - available) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pathname(value) {
|
|
52
|
+
try { return new URL(string(value || '/'), 'http://prnext.local').pathname.slice(0, 2048) || '/'; } catch { return '/'; }
|
|
53
|
+
}
|
|
54
|
+
const bytes = value => encoder.encode(value).length;
|
|
55
|
+
function clipBytes(value, limit) {
|
|
56
|
+
if (limit <= 0) return '';
|
|
57
|
+
if (bytes(value) <= limit) return value;
|
|
58
|
+
let low = 0, high = Math.min(value.length, limit);
|
|
59
|
+
while (low < high) {
|
|
60
|
+
const middle = Math.ceil((low + high) / 2);
|
|
61
|
+
if (bytes(value.slice(0, middle)) <= limit) low = middle; else high = middle - 1;
|
|
62
|
+
}
|
|
63
|
+
// Never split a UTF-16 pair at the byte boundary.
|
|
64
|
+
if (low && /[\uD800-\uDBFF]/.test(value[low - 1]) && /[\uDC00-\uDFFF]/.test(value[low] || '')) low--;
|
|
65
|
+
return value.slice(0, low);
|
|
66
|
+
}
|
|
67
|
+
function stackWithoutMessage({ name, message, stack }) {
|
|
68
|
+
for (const prefix of [name + ': ' + message, message]) {
|
|
69
|
+
if (stack === prefix || prefix.startsWith(stack)) return '';
|
|
70
|
+
for (const newline of ['\r\n', '\n']) if (stack.startsWith(prefix + newline)) return stack.slice(prefix.length + newline.length);
|
|
71
|
+
}
|
|
72
|
+
return stack;
|
|
73
|
+
}
|
|
74
|
+
function sourceFor(sourceById, id) {
|
|
75
|
+
try {
|
|
76
|
+
if (sourceById instanceof Map) return sourceById.get(id);
|
|
77
|
+
return sourceById && Object.hasOwn(sourceById, id) ? sourceById[id] : undefined;
|
|
78
|
+
} catch { return undefined; }
|
|
79
|
+
}
|
|
80
|
+
function sourceText(source) {
|
|
81
|
+
if (!source || typeof source !== 'object') return '';
|
|
82
|
+
const file = read(source, 'file'), frame = read(source, 'frame');
|
|
83
|
+
if (typeof file !== 'string' && typeof frame !== 'string') return '';
|
|
84
|
+
const line = read(source, 'line'), column = read(source, 'column');
|
|
85
|
+
let location = typeof file === 'string' ? file.slice(0, 2048) : '';
|
|
86
|
+
if (location && Number.isSafeInteger(line) && line > 0) {
|
|
87
|
+
location += ':' + line;
|
|
88
|
+
if (Number.isSafeInteger(column) && column > 0) location += ':' + column;
|
|
89
|
+
}
|
|
90
|
+
const original = [location ? 'Source: ' + location : '', typeof frame === 'string' ? frame : ''].filter(Boolean).join('\n');
|
|
91
|
+
if (bytes(original) <= MAX_DEV_SOURCE_BYTES) return original;
|
|
92
|
+
const note = '\n[Source frame truncated at 48 KiB.]';
|
|
93
|
+
return clipBytes(original, MAX_DEV_SOURCE_BYTES - bytes(note)) + note;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** This preserves recorded diagnostic text; it does not claim to sanitize user-logged secrets. */
|
|
97
|
+
export function formatDevIssuesForAI(issues, { route = '/', sourceById, omitted = 0 } = {}) {
|
|
98
|
+
const values = Array.isArray(issues) ? issues : [], selected = values.slice(0, MAX_DEV_ISSUES);
|
|
99
|
+
const knownOmitted = Number.isSafeInteger(omitted) && omitted > 0 ? omitted : 0;
|
|
100
|
+
let omittedCount = knownOmitted + Math.max(0, values.length - selected.length), truncated = false, limitedFields = false;
|
|
101
|
+
let output = 'Fix all errors below.\n\nRoute: ' + pathname(route);
|
|
102
|
+
// Reserve enough UTF-8 bytes for explicit omission/truncation notices.
|
|
103
|
+
const budget = MAX_DEV_AI_REPORT_BYTES - 512;
|
|
104
|
+
for (let index = 0; index < selected.length; index++) {
|
|
105
|
+
const issue = selected[index] || {}, detail = report(read(issue, 'report') || read(issue, 'error'));
|
|
106
|
+
const stack = stackWithoutMessage(detail), source = sourceText(sourceFor(sourceById, read(issue, 'id')));
|
|
107
|
+
limitedFields ||= detail.message.length === MAX_DEV_ISSUE_TEXT || detail.stack.length === MAX_DEV_ISSUE_TEXT;
|
|
108
|
+
const block = '\n\n' + (index + 1) + '. ' + string(read(issue, 'kind') || 'runtime').slice(0, 64) + ' — ' + detail.name
|
|
109
|
+
+ '\n' + detail.message + (stack ? '\n\nStack:\n' + stack : '') + (source ? '\n\n' + source : '');
|
|
110
|
+
const remaining = budget - bytes(output);
|
|
111
|
+
if (bytes(block) > remaining) {
|
|
112
|
+
output += clipBytes(block, remaining);truncated = true;omittedCount += selected.length - index - 1;break;
|
|
113
|
+
}
|
|
114
|
+
output += block;
|
|
115
|
+
}
|
|
116
|
+
if (!selected.length) output += '\n\nNo current errors.';
|
|
117
|
+
if (limitedFields) output += '\n\n[Diagnostic message and stack fields are limited to 48 KiB each.]';
|
|
118
|
+
if (truncated) output += '\n\n[Report truncated at 512 KiB.]';
|
|
119
|
+
if (omittedCount) output += '\n\n[' + omittedCount + ' additional current issue' + (omittedCount === 1 ? '' : 's') + ' omitted.]';
|
|
120
|
+
return output;
|
|
121
|
+
}
|
|
@@ -105,6 +105,6 @@ export function createDevChangeFilter(root, {maxEntries=512,maxBytes=1024*1024}=
|
|
|
105
105
|
}
|
|
106
106
|
return changed;
|
|
107
107
|
};
|
|
108
|
-
filter.acceptInputs=inputs=>{for(const [file,value] of inputs)remember(file,value);};
|
|
108
|
+
filter.acceptInputs=inputs=>{const changed=[];for(const [file,value] of inputs){if(fingerprints.get(file)?.value!==value)changed.push(file);remember(file,value);}return changed;};
|
|
109
109
|
return filter;
|
|
110
110
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {spawn} from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export class DevGateway {
|
|
4
|
+
constructor(binary,root,{hostname,port,onEnsure,onOpen,onClose,onFatal}) {
|
|
5
|
+
this.next=0;this.pending=new Map();this.stopping=false;
|
|
6
|
+
this.child=spawn(binary,['dev-gateway',root,'--hostname',hostname,'--port',String(port)],{stdio:['pipe','pipe','inherit'],windowsHide:true});
|
|
7
|
+
this.closed=new Promise(resolve=>this.child.once('close',resolve));
|
|
8
|
+
this.ready=new Promise((resolve,reject)=>{
|
|
9
|
+
const timer=setTimeout(()=>{reject(Error('Development listener did not start'));this.child.kill();},15000);
|
|
10
|
+
let buffer='';this.child.stdout.setEncoding('utf8');
|
|
11
|
+
this.child.stdout.on('data',chunk=>{
|
|
12
|
+
buffer+=chunk;if(buffer.length>1024*1024){this.child.kill();return;}
|
|
13
|
+
for(let end;(end=buffer.indexOf('\n'))>=0;){
|
|
14
|
+
const line=buffer.slice(0,end);buffer=buffer.slice(end+1);let message;
|
|
15
|
+
try{message=JSON.parse(line);}catch{this.child.kill();return;}
|
|
16
|
+
if(message.type==='ready'){clearTimeout(timer);resolve(message);continue;}
|
|
17
|
+
if(message.type==='open'){onOpen(message);continue;}
|
|
18
|
+
if(message.type==='close'){onClose(message);continue;}
|
|
19
|
+
if(message.type==='ensure'){
|
|
20
|
+
Promise.resolve().then(()=>onEnsure(message.path,message.client)).then(value=>this.request('result',{request:message.id,value}),error=>this.request('result',{request:message.id,value:{error:String(error.message).slice(0,8000)}})).catch(error=>{if(!this.stopping)onFatal(error);});continue;
|
|
21
|
+
}
|
|
22
|
+
const pending=this.pending.get(message.id);if(!pending)continue;
|
|
23
|
+
this.pending.delete(message.id);clearTimeout(pending.timer);message.ok?pending.resolve(message.value):pending.reject(Error(message.error));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
const failed=error=>{clearTimeout(timer);reject(error);for(const pending of this.pending.values()){clearTimeout(pending.timer);pending.reject(error);}this.pending.clear();if(!this.stopping)onFatal(error);};
|
|
27
|
+
this.child.once('error',failed);this.child.once('exit',(code,signal)=>failed(Error(`Development listener exited ${signal||code}`)));
|
|
28
|
+
});
|
|
29
|
+
this.child.stdin.on('error',()=>{});
|
|
30
|
+
}
|
|
31
|
+
async request(command,values={}) {
|
|
32
|
+
await this.ready;
|
|
33
|
+
if(this.stopping||this.child.exitCode!==null)throw Error('Development listener is closed');
|
|
34
|
+
const id=++this.next;
|
|
35
|
+
const line=JSON.stringify({id,command,...values})+'\n';
|
|
36
|
+
if(Buffer.byteLength(line)>65536)throw Error(`Development ${command} control exceeds the 64 KiB frame budget`);
|
|
37
|
+
return new Promise((resolve,reject)=>{
|
|
38
|
+
const timer=setTimeout(()=>{this.pending.delete(id);reject(Error(`Development ${command} control timed out`));this.child.kill();},35000);
|
|
39
|
+
this.pending.set(id,{resolve,reject,timer});
|
|
40
|
+
this.child.stdin.write(line);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async close(){this.stopping=true;this.child.stdin.end();const timer=setTimeout(()=>this.child.kill('SIGKILL'),6000);try{await this.closed;}finally{clearTimeout(timer);}}
|
|
44
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {setTimeout as delay} from 'node:timers/promises';
|
|
3
|
+
import {createCloudClient} from './cloud/client.mjs';
|
|
4
|
+
import {cloudOrigin,credentialPath,linkPath,readState} from './cloud/state.mjs';
|
|
5
|
+
|
|
6
|
+
// Explicit --host-ai only. The cloud never receives a localhost capability or
|
|
7
|
+
// an address to fetch: this process pulls a bounded, allowlisted command queue.
|
|
8
|
+
const allowed = new Set(['dev_snapshot','dev_connection','dev_logs','dev_routes','dev_route_ensure','dev_cache_invalidate','dev_verify','dev_source','dev_ai_context']);
|
|
9
|
+
export async function startDevHostRelay({root,tools,env=process.env,fetcher=fetch,interval=1000,signal:parentSignal}) {
|
|
10
|
+
const controller=new AbortController(),signal=AbortSignal.any([controller.signal,...(parentSignal?[parentSignal]:[])]);
|
|
11
|
+
const link=await readState(linkPath(root));
|
|
12
|
+
const origin=cloudOrigin(env.PRNEXT_CLOUD_URL||link?.origin||'https://app.prnext.dev');
|
|
13
|
+
const credential=await readState(credentialPath(origin,env));
|
|
14
|
+
const log=(level,message)=>tools.addServerLog(level,message,{process:'host-relay'});
|
|
15
|
+
const status=value=>tools.setHostRelayState?.({enabled:true,linked:false,origin,...value});
|
|
16
|
+
status({});
|
|
17
|
+
if(!credential||credential.origin!==origin||!/^prn_[A-Za-z0-9_-]{43}$/.test(credential.accessToken||'')||credential.expiresAt<=Date.now()) {
|
|
18
|
+
log('warn',`Host AI relay is off. Run prnext login --server ${origin}, then restart with --host-ai.`);
|
|
19
|
+
status({error:'Run prnext login to connect this terminal.'});
|
|
20
|
+
return {close:async()=>{}};
|
|
21
|
+
}
|
|
22
|
+
const api=createCloudClient(origin,{token:credential.accessToken,signal,fetcher});
|
|
23
|
+
let relayId,stopped=false;
|
|
24
|
+
const register=async()=>{
|
|
25
|
+
const reply=await api('/ai/dev/register',{method:'POST',body:{session:tools.session,title:path.basename(root).slice(0,80),...(link?.origin===origin&&link.projectId?{projectId:link.projectId}:{})}});
|
|
26
|
+
if(typeof reply.id!=='string'||reply.id.length>100)throw Error('Host returned an invalid development relay.');
|
|
27
|
+
relayId=reply.id;status({linked:true,lastSeen:Date.now()});log('info','Host AI relay ready. Your existing Host MCP can discover this local development session.');
|
|
28
|
+
};
|
|
29
|
+
try{await register();}catch(error){const message=error.status===404?'This Host server needs the local development relay update. Local MCP remains available.':`Host AI relay unavailable: ${error.message}`;status({error:message});log('warn',message);return {close:async()=>{}};}
|
|
30
|
+
async function work(command) {
|
|
31
|
+
let result;
|
|
32
|
+
if(!command||typeof command.id!=='string'||!allowed.has(command.tool)||!command.args||typeof command.args!=='object'||Array.isArray(command.args))
|
|
33
|
+
result={error:'Unsupported development command.'};
|
|
34
|
+
else if(!Number.isFinite(command.expiresAt)||command.expiresAt<=Date.now())result={error:'Development command expired before execution.'};
|
|
35
|
+
else {
|
|
36
|
+
try{result={result:await tools.executeTool(command.tool,command.args,{client:`Host MCP: ${String(command.client||'assistant').slice(0,70)}`})};}
|
|
37
|
+
catch(error){result={error:String(error.message).slice(0,1000)};}
|
|
38
|
+
}
|
|
39
|
+
if(!stopped)await api('/ai/dev/result',{method:'POST',body:{id:relayId,commandId:command.id,...result}});
|
|
40
|
+
}
|
|
41
|
+
const running=(async()=>{
|
|
42
|
+
let failures=0;
|
|
43
|
+
while(!stopped){
|
|
44
|
+
try{
|
|
45
|
+
const reply=await api('/ai/dev/poll',{method:'POST',body:{id:relayId}});
|
|
46
|
+
status({linked:true,lastSeen:Date.now()});
|
|
47
|
+
for(const command of (Array.isArray(reply.commands)?reply.commands:[]).slice(0,1)) {if(stopped||signal.aborted)break;await work(command);}
|
|
48
|
+
failures=0;
|
|
49
|
+
}catch(error){
|
|
50
|
+
if(stopped)break;
|
|
51
|
+
status({error:error.message});
|
|
52
|
+
if(error.status===401||error.status===403){log('warn','Host AI relay disconnected: terminal access expired or was revoked.');break;}
|
|
53
|
+
if(error.status===404){try{await register();failures=0;}catch{failures++;}}
|
|
54
|
+
else failures++;
|
|
55
|
+
if(failures===1)log('warn','Host AI relay reconnecting; no command is replayed.');
|
|
56
|
+
}
|
|
57
|
+
try{await delay(Math.min(10000,interval*2**Math.min(failures,3)),undefined,{signal});}catch{break;}
|
|
58
|
+
}
|
|
59
|
+
})();
|
|
60
|
+
return {id:relayId,async close(){
|
|
61
|
+
if(stopped)return;stopped=true;controller.abort();status({enabled:false});
|
|
62
|
+
// Abort long local verification without blocking development shutdown.
|
|
63
|
+
await Promise.race([running,delay(100)]);
|
|
64
|
+
try{await createCloudClient(origin,{token:credential.accessToken,signal:AbortSignal.timeout(1500),fetcher})('/ai/dev/close',{method:'POST',body:{id:relayId}});}catch{}
|
|
65
|
+
}};
|
|
66
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import logo from './dev-logo.mjs';
|
|
2
|
+
import { createLazyDevTool } from './dev-lazy-tool.mjs';
|
|
3
|
+
import { popoutIcon } from './dev-popout.mjs';
|
|
4
|
+
import { deviceIcons } from './dev-ui-icons.mjs';
|
|
5
|
+
import { createDevPosition } from './dev-position.mjs';
|
|
6
|
+
import { createDevIssueMotion } from './dev-issue-motion.mjs';
|
|
7
|
+
|
|
8
|
+
// This module is imported only by the development browser entry. No framework
|
|
9
|
+
// contexts, application CSS, network requests or additional SSE streams.
|
|
10
|
+
export function createDevIndicator({ position = 'bottom-left', snapshot, showIssues, capture, tools }) {
|
|
11
|
+
const host = document.createElement('prnext-dev-tools');
|
|
12
|
+
host.id = '__prnext_dev_tools__';
|
|
13
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
14
|
+
const style = document.createElement('style');
|
|
15
|
+
style.textContent = `
|
|
16
|
+
:host{all:initial;position:fixed;bottom:max(18px,env(safe-area-inset-bottom));left:max(18px,env(safe-area-inset-left));width:44px;height:44px;--issue-rgb:240,92,102;--issue-alpha:.5;--origin-x:22px;--origin-y:calc(100% - 22px);--flip-x:1;--flip-y:1;--panel-from:8px;z-index:2147483646;color-scheme:dark;font:13px/1.5 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#ededf0;text-align:left;direction:ltr}
|
|
17
|
+
:host([data-position^=top]){top:max(18px,env(safe-area-inset-top));bottom:auto;--origin-y:22px;--flip-y:-1;--panel-from:-8px}:host([data-position$=right]){right:max(18px,env(safe-area-inset-right));left:auto;--origin-x:calc(100% - 22px);--flip-x:-1}
|
|
18
|
+
*,*::before,*::after{box-sizing:border-box}button{font:inherit;color:inherit;-webkit-tap-highlight-color:transparent;cursor:pointer}button:focus-visible{outline:1px solid #858585;outline-offset:-2px}button:disabled{cursor:default}svg{width:16px;height:16px;flex:none}
|
|
19
|
+
.trigger{position:relative;z-index:3;display:grid;place-items:center;width:44px;height:44px;padding:0;border:1px solid #303030;border-radius:999px;background:#080808;box-shadow:0 2px 5px #0006,0 8px 22px #0004,inset 0 1px 0 #ffffff06;user-select:none;cursor:grab}:host([data-dragging=true]) .trigger{cursor:grabbing;scale:1.06;border-color:#777;box-shadow:0 10px 24px #0008}
|
|
20
|
+
.trigger:hover{border-color:#555;background:#101010}.trigger[aria-expanded=true]{border-color:transparent;background:transparent;box-shadow:none}.trigger img{width:28px;height:28px;border-radius:50%;pointer-events:none;filter:grayscale(1) contrast(1.25)}
|
|
21
|
+
.bubble{position:absolute;bottom:0;left:0;z-index:1;pointer-events:none;filter:drop-shadow(0 12px 22px #0008) drop-shadow(0 2px 5px #0006)}.bubble-mask{width:100%;height:100%;clip-path:circle(22px at var(--origin-x) var(--origin-y))}.bubble-mask[data-open=true]{clip-path:circle(var(--bubble-radius) at var(--origin-x) var(--origin-y))}.bubble svg{display:block;width:100%;height:100%;transform:scale(var(--flip-x),var(--flip-y));transform-origin:center}.bubble path{fill:#080808;stroke:#292929;stroke-width:1}
|
|
22
|
+
.ring{position:absolute;inset:3px;pointer-events:none}.ring svg{display:block;width:100%;height:100%;overflow:visible}.ring circle{fill:none;stroke:transparent;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:0 50 0 50;transform:rotate(-113.4deg);transform-origin:center}
|
|
23
|
+
.trigger[data-busy=true] .ring circle{stroke:#b3c1ff;stroke-dasharray:25 0 25 50}.trigger[data-issue-severity=error] .ring circle{stroke:rgba(var(--issue-rgb),var(--issue-alpha));stroke-dasharray:13 37 13 37}.trigger[data-issue-severity=critical] .ring circle{stroke:rgba(var(--issue-rgb),var(--issue-alpha));stroke-dasharray:50 0 50 0}
|
|
24
|
+
.issue-motion{position:absolute;top:0;left:0;width:1px;height:1px;overflow:visible;z-index:4;pointer-events:none;visibility:hidden;opacity:var(--issue-alpha)}.issue-motion[data-running=true]{visibility:visible}.issue-motion-path,.issue-motion-ring,.issue-motion-streak{stroke:rgb(var(--issue-rgb))}
|
|
25
|
+
:host([data-issue-transfer]) .trigger[data-issue-severity=error] .ring circle,:host([data-issue-transfer]) .trigger[data-issue-severity=critical] .ring circle{transition:none}
|
|
26
|
+
:host([data-issue-transfer=moving]) .trigger[data-issue-severity=error] .ring circle,:host([data-issue-transfer=moving]) .trigger[data-issue-severity=critical] .ring circle,:host([data-issue-transfer=open]) .trigger[data-issue-severity=error] .ring circle,:host([data-issue-transfer=open]) .trigger[data-issue-severity=critical] .ring circle{stroke:transparent}
|
|
27
|
+
:host([data-issue-transfer=moving]) .action-issues{border-color:transparent}
|
|
28
|
+
.panel{position:absolute;bottom:36px;left:0;width:264px;max-width:calc(100vw - 36px);max-height:calc(100dvh - 100px);overflow:auto;overscroll-behavior:contain;padding:8px 8px 12px;border:0;border-radius:20px;background:transparent;scrollbar-color:#333 #080808;transform-origin:var(--origin-x) bottom}
|
|
29
|
+
:host([data-position^=top]) .bubble{top:0;bottom:auto}:host([data-position^=top]) .panel{top:36px;bottom:auto;transform-origin:var(--origin-x) top}:host([data-position$=right]) .bubble,:host([data-position$=right]) .panel{right:0;left:auto}
|
|
30
|
+
.corner-hints{position:fixed;inset:0;pointer-events:none;z-index:0}.corner-hint{position:absolute;width:44px;height:44px;bottom:max(18px,env(safe-area-inset-bottom));left:max(18px,env(safe-area-inset-left));border:1px dashed #ffffff35;border-radius:50%;background:#08080888}.corner-hint[data-corner^=top]{top:max(18px,env(safe-area-inset-top));bottom:auto}.corner-hint[data-corner$=right]{right:max(18px,env(safe-area-inset-right));left:auto}.corner-hint[data-active=true]{border-style:solid;border-color:#aaa;background:#ffffff16;scale:1.1}
|
|
31
|
+
:host([hidden]),[hidden]{display:none!important}.panel{z-index:2}.panel:not([data-open=true]){opacity:0;pointer-events:none}
|
|
32
|
+
.panel:focus{outline:none}.trigger:focus-visible{outline-offset:-4px}.action:focus-visible{background:#171717}
|
|
33
|
+
.action{display:flex;align-items:center;gap:10px;width:100%;min-height:40px;border:0;padding:9px 10px;border-radius:11px;background:transparent;text-decoration:none;text-align:left;font-size:12px}.action:hover:not(:disabled){background:#161616}.action:disabled{color:#a1a1a1}.action .hint{margin-left:auto;color:#a1a1a1;font-size:11px}.divider{height:1px;margin:6px 8px;background:#202020}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
|
|
34
|
+
.action-issues{border:1px solid rgba(var(--issue-rgb),var(--issue-alpha));padding:8px 9px}.action-issues .hint{color:rgb(var(--issue-rgb))}
|
|
35
|
+
.action-split{display:flex;align-items:center}.action-split .action{flex:1;min-width:0}.detach{display:grid;place-items:center;flex:none;width:32px;min-height:36px;border:0;border-radius:8px;background:transparent;color:#aaa;padding:4px}.detach:hover{background:#161616;color:#fff}.detach svg{width:14px;height:14px}
|
|
36
|
+
.devices{display:flex;gap:3px;padding:4px;background:#111;border-radius:11px;margin:6px 4px}.device-choice{position:relative;display:grid;place-items:center;flex:1;min-width:0;min-height:34px;padding:6px 0;border:0;border-radius:8px;background:transparent;color:#aaa}.device-choice:hover{background:#1c1c1c;color:#fff}.device-choice[aria-pressed=true]{background:#222;color:#fff}.devices .detach{width:32px;min-height:34px}.device-choice::after{content:attr(data-tooltip);position:absolute;bottom:calc(100% + 8px);left:0;background:#1b1b1b;color:#eee;padding:6px 9px;border-radius:7px;font-size:11px;white-space:nowrap;box-shadow:0 3px 14px #0005;opacity:0;visibility:hidden;pointer-events:none;z-index:4}.device-choice:hover::after,.device-choice:focus-visible::after{opacity:1;visibility:visible}
|
|
37
|
+
@media(prefers-reduced-motion:no-preference){.trigger{transition:background-color 150ms,border-color 150ms,box-shadow 150ms,transform 150ms,scale 180ms}.trigger:active{transform:scale(.96)}.ring circle{transition:stroke 160ms ease-out,stroke-dasharray 160ms ease-out}.corner-hint{transition:background-color 150ms,border-color 150ms,scale 180ms}.bubble-mask{transition:clip-path 260ms cubic-bezier(.3,0,.3,1)}.bubble-mask[data-open=true]{transition-duration:360ms;transition-timing-function:cubic-bezier(.16,1,.3,1)}.panel{transition:opacity 110ms ease-out,transform 230ms cubic-bezier(.16,1,.3,1)}.panel:not([data-open=true]){transform:translateY(var(--panel-from)) scale(.97)}.panel[data-open=true]{transition:opacity 120ms ease-out 60ms,transform 180ms cubic-bezier(.16,1,.3,1) 60ms}:host([data-position^=top]) .bubble-mask{transition-duration:180ms}:host([data-position^=top]) .bubble-mask[data-open=true]{transition-duration:220ms}:host([data-position^=top]) .panel{transition:opacity 90ms ease-out,transform 180ms cubic-bezier(.16,1,.3,1)}:host([data-position^=top]) .panel[data-open=true]{transition:opacity 90ms ease-out,transform 140ms cubic-bezier(.16,1,.3,1)}.trigger[data-busy=true] .ring{animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}}
|
|
38
|
+
@media(pointer:coarse){.action,.device-choice,.devices .detach{min-height:44px}.devices .detach{width:44px}}
|
|
39
|
+
@media(forced-colors:active){.trigger,.action-issues{border-color:ButtonText}.bubble path{fill:Canvas;stroke:ButtonText}.trigger[data-busy=true] .ring circle,.trigger[data-issue-severity=error] .ring circle,.trigger[data-issue-severity=critical] .ring circle{stroke:CanvasText}.issue-motion{opacity:1}.issue-motion-path,.issue-motion-ring,.issue-motion-streak{stroke:CanvasText}button:focus-visible{outline:2px solid Highlight;outline-offset:-3px}}
|
|
40
|
+
`;
|
|
41
|
+
const trigger = document.createElement('button');
|
|
42
|
+
trigger.type = 'button'; trigger.className = 'trigger';
|
|
43
|
+
trigger.setAttribute('aria-label', 'Open PRNext developer tools');
|
|
44
|
+
trigger.setAttribute('aria-haspopup', 'dialog');
|
|
45
|
+
trigger.setAttribute('aria-expanded', 'false'); trigger.setAttribute('aria-controls', 'tools');
|
|
46
|
+
const mark = document.createElement('img'); mark.src = logo; mark.alt = ''; mark.draggable = false;
|
|
47
|
+
const ring = document.createElement('span'); ring.className = 'ring'; ring.setAttribute('aria-hidden', 'true');
|
|
48
|
+
const ringSvg=document.createElementNS('http://www.w3.org/2000/svg','svg');ringSvg.setAttribute('viewBox','0 0 36 36');
|
|
49
|
+
const ringCircle=document.createElementNS('http://www.w3.org/2000/svg','circle');
|
|
50
|
+
for(const[name,value]of Object.entries({cx:'18',cy:'18',r:'17.25',pathLength:'100'}))ringCircle.setAttribute(name,value);
|
|
51
|
+
ringSvg.append(ringCircle);ring.append(ringSvg);
|
|
52
|
+
trigger.append(mark, ring);
|
|
53
|
+
const panel = document.createElement('section');
|
|
54
|
+
panel.id = 'tools'; panel.className = 'panel'; panel.hidden = true; panel.inert = true; panel.tabIndex = -1;
|
|
55
|
+
panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-label', 'PRNext developer tools');
|
|
56
|
+
const bubble=document.createElement('div');bubble.className='bubble';bubble.hidden=true;bubble.setAttribute('aria-hidden','true');
|
|
57
|
+
const mask=document.createElement('div');mask.className='bubble-mask';
|
|
58
|
+
const surface=document.createElementNS('http://www.w3.org/2000/svg','svg');surface.setAttribute('preserveAspectRatio','none');
|
|
59
|
+
const shape=document.createElementNS('http://www.w3.org/2000/svg','path');surface.append(shape);mask.append(surface);bubble.append(mask);
|
|
60
|
+
function measureBubble(){
|
|
61
|
+
if(panel.hidden)return;
|
|
62
|
+
const width=panel.offsetWidth,height=panel.offsetHeight;
|
|
63
|
+
bubble.style.width=width+'px';bubble.style.height=(height+36)+'px';bubble.style.setProperty('--bubble-radius',Math.ceil(Math.hypot(width-22,height+14)+2)+'px');surface.setAttribute('viewBox',`0 0 ${width} ${height+36}`);
|
|
64
|
+
// One contour: rounded menu, curved shoulder, and the logo's circular foot.
|
|
65
|
+
shape.setAttribute('d',`M20.5 .5H${width-20.5}Q${width-.5} .5 ${width-.5} 20.5V${height-20}Q${width-.5} ${height} ${width-20.5} ${height}H64Q44 ${height} 44 ${height+14}A21.75 21.75 0 0 1 .5 ${height+14}V20.5Q.5 .5 20.5 .5Z`);
|
|
66
|
+
issueMotion?.refresh();
|
|
67
|
+
}
|
|
68
|
+
// Only constant icon markup goes through innerHTML. Labels use textContent.
|
|
69
|
+
const icon = paths => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
|
|
70
|
+
function action(label, drawing, handler) {
|
|
71
|
+
const button = document.createElement('button'); button.type = 'button'; button.className = 'action';
|
|
72
|
+
button.innerHTML = icon(drawing); const text = document.createElement('span'); text.textContent = label;
|
|
73
|
+
button.append(text); button.onclick = handler; panel.append(button); return button;
|
|
74
|
+
}
|
|
75
|
+
const issues = action('View issues', '<circle cx="12" cy="12" r="8.5"/><path d="M12 7.5v5m0 3.5h.01"/>', () => { close(false); showIssues(); });
|
|
76
|
+
issues.classList.add('action-issues');
|
|
77
|
+
const issueCount = document.createElement('span'); issueCount.className = 'hint'; issues.append(issueCount);
|
|
78
|
+
action('Reload page', '<path d="M20 7v5h-5M4 17v-5h5"/><path d="M6 7a7 7 0 0 1 12-1l2 6M4 12l2 6a7 7 0 0 0 12-1"/>', () => location.reload());
|
|
79
|
+
let workbench;
|
|
80
|
+
const toolError=error=>{feedback.textContent='Could not open development tool. Try again.';console.error(error);};
|
|
81
|
+
const getWorkbench = () => workbench ||= createLazyDevTool(async()=>{
|
|
82
|
+
const {createDevWorkbench}=await import('./dev-workbench.mjs');
|
|
83
|
+
return createDevWorkbench({shadow,capture,tools,restoreFocus:()=>trigger.focus()});
|
|
84
|
+
},{onError:toolError,reserve(method,args){
|
|
85
|
+
if(!['popConsole','popPreview'].includes(method))return;
|
|
86
|
+
try{return window.open('about:blank','_blank',`popup=yes,width=${method==='popConsole'?860:args[0]==='tablet'?1000:560},height=${method==='popConsole'?520:960},resizable=yes,scrollbars=yes`);}catch{return null;}
|
|
87
|
+
}});
|
|
88
|
+
function detachButton(label,handler) {
|
|
89
|
+
const button=document.createElement('button');button.type='button';button.className='detach';button.innerHTML=popoutIcon;
|
|
90
|
+
button.setAttribute('aria-label',label);button.title=label;button.onclick=handler;return button;
|
|
91
|
+
}
|
|
92
|
+
const consoleAction=action('Client console', '<path d="m5 7 5 5-5 5m8 0h6"/>', () => { close(false); getWorkbench().showConsole(); });
|
|
93
|
+
const consoleRow=document.createElement('div');consoleRow.className='action-split';consoleAction.replaceWith(consoleRow);
|
|
94
|
+
consoleRow.append(consoleAction,detachButton('Open console in new window',()=>{close(false);getWorkbench().popConsole();}));
|
|
95
|
+
let inspector;
|
|
96
|
+
const getInspector = () => inspector ||= createLazyDevTool(async()=>{
|
|
97
|
+
const {createDevToolsPanel}=await import('./dev-tools-panel.mjs');
|
|
98
|
+
return createDevToolsPanel({shadow,tools,restoreFocus:()=>trigger.focus()});
|
|
99
|
+
},{onError:toolError});
|
|
100
|
+
if (tools) {
|
|
101
|
+
action('Inspect','<path d="M3 12h4l3-8 4 16 3-8h4"/>',()=>{close(false);getInspector().open('requests');});
|
|
102
|
+
action('Select element for AI','<path d="m4 3 7 17 2-7 7-2L4 3Z"/>',()=>{close(false);getInspector().pick();});
|
|
103
|
+
}
|
|
104
|
+
const devices = document.createElement('div'); devices.className = 'devices'; devices.setAttribute('role','group'); devices.setAttribute('aria-label','Preview device');
|
|
105
|
+
let selectedDevice='desktop',previewSelected=false;
|
|
106
|
+
const deviceChoices=new Map();
|
|
107
|
+
const currentDevice=()=>previewSelected?workbench?.previewState().device||selectedDevice:selectedDevice;
|
|
108
|
+
function updateDevices(){
|
|
109
|
+
const current=currentDevice();
|
|
110
|
+
for(const [value,button] of deviceChoices)button.setAttribute('aria-pressed',String(value===current));
|
|
111
|
+
devicePopout.title=`Open ${current} in new window`;
|
|
112
|
+
}
|
|
113
|
+
for (const [value,label,width] of [
|
|
114
|
+
['desktop','Desktop','Responsive'],['phone','Phone','390px'],['tablet','Tablet','768px'],
|
|
115
|
+
]) {
|
|
116
|
+
const button = document.createElement('button'); button.type = 'button'; button.className='device-choice'; button.innerHTML = deviceIcons[value];
|
|
117
|
+
button.setAttribute('aria-label',label);button.setAttribute('aria-description',width);button.dataset.tooltip=`${label} · ${width}`;
|
|
118
|
+
button.onclick = () => { selectedDevice=value;previewSelected=true;close(false);getWorkbench().openPreview(value);updateDevices(); };
|
|
119
|
+
deviceChoices.set(value,button);devices.append(button);
|
|
120
|
+
}
|
|
121
|
+
const devicePopout=detachButton('Open device preview in new window',()=>{const value=currentDevice();close(false);getWorkbench().popPreview(value);});
|
|
122
|
+
devices.append(devicePopout);updateDevices();
|
|
123
|
+
panel.append(devices);
|
|
124
|
+
const divider = document.createElement('div'); divider.className = 'divider'; panel.append(divider);
|
|
125
|
+
action('Hide until reload', '<path d="m3 3 18 18M10.6 10.6a2 2 0 0 0 2.8 2.8M9.5 5.2A10 10 0 0 1 12 5c6 0 10 7 10 7a17 17 0 0 1-3 3.5M6.3 6.3A22 22 0 0 0 2 12s4 7 10 7a10 10 0 0 0 4.5-1.1"/>', () => {
|
|
126
|
+
close(false);issueMotion.closeImmediately(); workbench?.hide(); inspector?.hide(); host.hidden = true; restorePageFocus();
|
|
127
|
+
});
|
|
128
|
+
const live = document.createElement('span'); live.className = 'sr-only'; live.setAttribute('role', 'status');
|
|
129
|
+
const feedback = document.createElement('span'); feedback.className = 'sr-only'; feedback.setAttribute('role', 'status');
|
|
130
|
+
shadow.append(style,bubble,panel,trigger,live,feedback); document.body.append(host);
|
|
131
|
+
const issueMotion = createDevIssueMotion({ host, shadow, ringSvg, issues, panel });
|
|
132
|
+
const geometry=new ResizeObserver(measureBubble);geometry.observe(panel);
|
|
133
|
+
let opened = false, timer, previousSummary, previousPageFocus = document.activeElement;
|
|
134
|
+
function restorePageFocus() {
|
|
135
|
+
if (previousPageFocus?.isConnected && previousPageFocus !== document.body && previousPageFocus !== host) previousPageFocus.focus();
|
|
136
|
+
else { const body = document.body, old = body.getAttribute('tabindex'); body.tabIndex = -1; body.focus(); if (old === null) body.removeAttribute('tabindex'); else body.setAttribute('tabindex', old); }
|
|
137
|
+
}
|
|
138
|
+
function close(restore) {
|
|
139
|
+
opened = false; clearTimeout(timer); trigger.setAttribute('aria-expanded', 'false');
|
|
140
|
+
issueMotion.update({severity:trigger.dataset.issueSeverity,open:false});
|
|
141
|
+
panel.dataset.open = 'false';mask.dataset.open='false';panel.inert = true; panel.setAttribute('aria-hidden', 'true');
|
|
142
|
+
if (restore) trigger.focus();
|
|
143
|
+
timer = setTimeout(() => { if (!opened) {panel.hidden=true;bubble.hidden=true;} }, matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 280);
|
|
144
|
+
}
|
|
145
|
+
function update() {
|
|
146
|
+
// App Router's global error boundary can replace the document body.
|
|
147
|
+
if (!host.isConnected && document.body) document.body.append(host);
|
|
148
|
+
const info = snapshot();
|
|
149
|
+
const count = info.issues || 0;
|
|
150
|
+
const severity=count?(info.criticalIssues>0?'critical':'error'):'none';
|
|
151
|
+
const status = count ? 'error' : info.connection !== 'connected' ? 'reconnecting' : info.building ? 'building' : 'ready';
|
|
152
|
+
const label = count ? `${count} ${count === 1 ? 'issue' : 'issues'}` : status === 'building' ? 'Compiling…' : status === 'reconnecting' ? 'Connecting…' : 'Ready';
|
|
153
|
+
if (count === 0 && shadow.activeElement === issues) panel.focus();
|
|
154
|
+
issues.hidden = count === 0; issueCount.textContent = String(count);
|
|
155
|
+
trigger.dataset.busy = String(info.building && !count); trigger.title = `PRNext · ${label}`;
|
|
156
|
+
trigger.dataset.issueSeverity=severity;
|
|
157
|
+
issueMotion.update({severity,open:opened&&!panel.hidden});
|
|
158
|
+
if(count)trigger.setAttribute('aria-description',severity==='critical'?'Critical error. Open issues to view details.':'Non-blocking issue. Open issues to view details.');else trigger.removeAttribute('aria-description');
|
|
159
|
+
trigger.setAttribute('aria-label', 'Open PRNext developer tools' + (count ? ` (${label})` : ''));
|
|
160
|
+
const summary=count?`${label}. ${severity==='critical'?'Critical error':'Non-blocking issue'}`:label;
|
|
161
|
+
if (previousSummary !== summary) { live.textContent = `PRNext: ${summary}`; previousSummary = summary; }
|
|
162
|
+
}
|
|
163
|
+
trigger.onclick = () => {
|
|
164
|
+
if (opened) { close(true); return; }
|
|
165
|
+
opened = true; clearTimeout(timer); feedback.textContent = '';
|
|
166
|
+
update();updateDevices(); panel.hidden = false;bubble.hidden=false;panel.inert = false;measureBubble();
|
|
167
|
+
panel.removeAttribute('aria-hidden'); trigger.setAttribute('aria-expanded', 'true');
|
|
168
|
+
mask.getBoundingClientRect();panel.dataset.open='true';mask.dataset.open='true';panel.focus();
|
|
169
|
+
issueMotion.update({severity:trigger.dataset.issueSeverity,open:true});
|
|
170
|
+
};
|
|
171
|
+
const placement = createDevPosition({ host, trigger, position, announce: text => { feedback.textContent = text; }, onDrag() {
|
|
172
|
+
close(false);issueMotion.closeImmediately(); panel.hidden = true; bubble.hidden = true;
|
|
173
|
+
} });
|
|
174
|
+
const controller = new AbortController(), options = { capture: true, signal: controller.signal };
|
|
175
|
+
document.addEventListener('pointerdown', event => { if (opened && !event.composedPath().some(node => node === panel || node === trigger)) close(false); }, options);
|
|
176
|
+
document.addEventListener('focusin', event => {
|
|
177
|
+
if (!event.composedPath().includes(host)) { previousPageFocus = event.target; if (opened) close(false); }
|
|
178
|
+
}, options);
|
|
179
|
+
shadow.addEventListener('focusin', event => { if (opened && !panel.contains(event.target) && event.target !== trigger) close(false); }, {signal:controller.signal});
|
|
180
|
+
shadow.addEventListener('keydown', event => { if (event.key === 'Escape' && opened) { event.preventDefault(); event.stopPropagation(); close(true); } });
|
|
181
|
+
globalThis.addEventListener('popstate', update, { signal: controller.signal });
|
|
182
|
+
update();
|
|
183
|
+
const contextListener = event => { close(false); getInspector().open('ai', event.detail); };
|
|
184
|
+
if (tools) globalThis.addEventListener('prnext:devtools:context', contextListener, {signal:controller.signal});
|
|
185
|
+
return { update, dispose() { clearTimeout(timer);issueMotion.dispose();placement.dispose();geometry.disconnect();controller.abort();workbench?.dispose();inspector?.dispose();host.remove(); } };
|
|
186
|
+
}
|