@thomas.f/prnext 0.1.16-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.
Files changed (126) hide show
  1. package/AI.md +63 -0
  2. package/README.md +384 -9
  3. package/build/ADAPTERS.md +27 -2
  4. package/build/actions.mjs +5 -0
  5. package/build/analyze.mjs +184 -0
  6. package/build/app-config.mjs +7 -8
  7. package/build/app-static.mjs +12 -7
  8. package/build/app.mjs +23 -10
  9. package/build/build-lock.mjs +6 -6
  10. package/build/bundle-runtime.mjs +20 -5
  11. package/build/check.mjs +56 -11
  12. package/build/compiler.mjs +25 -1
  13. package/build/concurrent.mjs +12 -0
  14. package/build/config.mjs +21 -13
  15. package/build/css-modules.mjs +15 -4
  16. package/build/dev-compiler-health.mjs +11 -0
  17. package/build/dev-css-update.mjs +83 -0
  18. package/build/dev-dependencies.mjs +44 -0
  19. package/build/dev-discovery.mjs +28 -0
  20. package/build/dev-worker.mjs +14 -5
  21. package/build/dev.mjs +55 -4
  22. package/build/edge.mjs +3 -2
  23. package/build/framework-imports.mjs +40 -0
  24. package/build/incremental.mjs +8 -4
  25. package/build/index.mjs +96 -101
  26. package/build/inspect.mjs +3 -0
  27. package/build/inspection-summary.mjs +37 -4
  28. package/build/loader-cache.mjs +53 -26
  29. package/build/metadata-routes.mjs +2 -3
  30. package/build/migrate.mjs +60 -57
  31. package/build/npm-packages.mjs +3 -18
  32. package/build/package-manager.mjs +308 -0
  33. package/build/persistent-cache.mjs +97 -0
  34. package/build/precompress.mjs +62 -30
  35. package/build/prerender-pages.mjs +90 -0
  36. package/build/prerender-worker.mjs +10 -0
  37. package/build/production-builder.mjs +80 -0
  38. package/build/scan.mjs +4 -4
  39. package/build/source-analysis.mjs +42 -0
  40. package/build/static-adapter.mjs +13 -5
  41. package/build/static-input.mjs +48 -0
  42. package/build/styles.mjs +15 -3
  43. package/build/transform-cache.mjs +37 -4
  44. package/build/type-command.mjs +41 -0
  45. package/build/typed-routes.mjs +91 -0
  46. package/build/typescript-dev-worker.mjs +18 -0
  47. package/build/typescript-dev.mjs +22 -0
  48. package/build/typescript-watch.mjs +22 -0
  49. package/build/typescript.mjs +5 -4
  50. package/build/webpack-graph.mjs +164 -27
  51. package/build/worker-preload.mjs +9 -0
  52. package/cli.mjs +105 -121
  53. package/compat/index.d.ts +21 -7
  54. package/compat/link.d.cts +4 -3
  55. package/compat/navigation.d.cts +4 -3
  56. package/compat/router.d.cts +4 -3
  57. package/compat/typed-route.d.cts +17 -0
  58. package/docs/ai/compatibility.md +106 -0
  59. package/docs/ai/data.md +128 -0
  60. package/docs/ai/deployment.md +111 -0
  61. package/docs/ai/manifest.json +11 -0
  62. package/docs/ai/quickstart.md +146 -0
  63. package/docs/ai/routing.md +132 -0
  64. package/docs/ai/troubleshooting.md +120 -0
  65. package/index.mjs +2 -0
  66. package/native/diagnostics.mjs +34 -0
  67. package/native/resolve.mjs +7 -2
  68. package/package.json +25 -7
  69. package/release/ai-docs.mjs +48 -0
  70. package/release/verify.mjs +2 -0
  71. package/runtime/ai-docs.mjs +202 -0
  72. package/runtime/app-client.mjs +17 -6
  73. package/runtime/app-render.mjs +12 -0
  74. package/runtime/cloud/archive.mjs +5 -6
  75. package/runtime/compile-cache.mjs +8 -0
  76. package/runtime/dev-ai-brands.mjs +26 -0
  77. package/runtime/dev-ai-style.mjs +99 -0
  78. package/runtime/dev-build-reasons.mjs +25 -0
  79. package/runtime/dev-cache-control.mjs +53 -0
  80. package/runtime/dev-client.mjs +239 -45
  81. package/runtime/dev-console-copy.mjs +19 -0
  82. package/runtime/dev-console.mjs +106 -0
  83. package/runtime/dev-dependency-graph.mjs +40 -0
  84. package/runtime/dev-element-picker-style.mjs +27 -0
  85. package/runtime/dev-element-picker.mjs +213 -0
  86. package/runtime/dev-error-overlay-style.mjs +41 -0
  87. package/runtime/dev-error-overlay.mjs +167 -0
  88. package/runtime/dev-error-report.mjs +121 -0
  89. package/runtime/dev-file-changes.mjs +1 -1
  90. package/runtime/dev-gateway.mjs +44 -0
  91. package/runtime/dev-host-relay.mjs +66 -0
  92. package/runtime/dev-indicator.mjs +186 -0
  93. package/runtime/dev-issue-motion.mjs +214 -0
  94. package/runtime/dev-lazy-tool.mjs +18 -0
  95. package/runtime/dev-logo.mjs +2 -0
  96. package/runtime/dev-notification.mjs +81 -0
  97. package/runtime/dev-popout.mjs +34 -0
  98. package/runtime/dev-position.mjs +107 -0
  99. package/runtime/dev-preview-touch.mjs +122 -0
  100. package/runtime/dev-runtime-pool.mjs +45 -0
  101. package/runtime/dev-runtime-spare.mjs +57 -0
  102. package/runtime/dev-tools-client.mjs +155 -0
  103. package/runtime/dev-tools-mcp.mjs +126 -0
  104. package/runtime/dev-tools-metrics.mjs +31 -0
  105. package/runtime/dev-tools-model.mjs +42 -0
  106. package/runtime/dev-tools-panel.mjs +341 -0
  107. package/runtime/dev-tools-result.mjs +37 -0
  108. package/runtime/dev-tools-server.mjs +256 -0
  109. package/runtime/dev-tools-source.mjs +104 -0
  110. package/runtime/dev-tooltip.mjs +94 -0
  111. package/runtime/dev-ui-icons.mjs +8 -0
  112. package/runtime/dev-workbench-style.mjs +106 -0
  113. package/runtime/dev-workbench.mjs +428 -0
  114. package/runtime/dev-worker-logs.mjs +31 -0
  115. package/runtime/development.mjs +307 -0
  116. package/runtime/env.mjs +2 -1
  117. package/runtime/pages-client.mjs +10 -1
  118. package/runtime/pages-navigation.mjs +6 -1
  119. package/runtime/persistent/client.mjs +6 -5
  120. package/runtime/persistent/daemon.mjs +66 -56
  121. package/runtime/persistent/native-proxy.mjs +63 -0
  122. package/runtime/preload.mjs +12 -0
  123. package/runtime/rsc-worker.mjs +7 -0
  124. package/runtime/typed-route.mjs +19 -0
  125. package/runtime/watch-project.mjs +12 -5
  126. package/runtime/worker.mjs +36 -2
@@ -0,0 +1,428 @@
1
+ // Development UI only. The responsive preview is a second, same-origin page
2
+ // with a real viewport, not a CSS width applied to the running application.
3
+ import { createToolWindow, popoutIcon, dockIcon } from './dev-popout.mjs';
4
+ import { version } from '../package.json';
5
+ import { deviceIcons, copyIcon, chevronIcon, svg } from './dev-ui-icons.mjs';
6
+ import { consolePrompt } from './dev-console-copy.mjs';
7
+ import { workbenchStyle } from './dev-workbench-style.mjs';
8
+ import { createDevTooltips } from './dev-tooltip.mjs';
9
+ import { createPreviewTouch } from './dev-preview-touch.mjs';
10
+
11
+ function contactCursor(pressed) {
12
+ const radius = pressed ? 8 : 10;
13
+ const image = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="${radius}" fill="white" fill-opacity="${pressed ? .35 : .16}" stroke="black" stroke-opacity=".4" stroke-width="3"/><circle cx="16" cy="16" r="${radius}" fill="none" stroke="white" stroke-opacity=".85" stroke-width="1"/></svg>`;
14
+ return `url("data:image/svg+xml,${encodeURIComponent(image)}") 16 16, auto`;
15
+ }
16
+ const contactCursorStyle = `:root,:root *{cursor:${contactCursor(false)}!important}:root:active,:root:active *{cursor:${contactCursor(true)}!important}`;
17
+
18
+ export function createDevWorkbench({ shadow, capture, tools, restoreFocus, sourceURL = () => globalThis.location.href, detached, dock, closeWindow }) {
19
+ const document = shadow.ownerDocument, view = document.defaultView;
20
+ const requestAnimationFrame = view.requestAnimationFrame.bind(view), cancelAnimationFrame = view.cancelAnimationFrame.bind(view);
21
+ const location = { get href() { return sourceURL(); }, get origin() { return new URL(sourceURL()).origin; } };
22
+ const style = document.createElement('style');
23
+ style.textContent = workbenchStyle +
24
+ (detached === 'console' ? '.wb-console{inset:0;width:100vw;height:100dvh;max-height:none;border:0;border-radius:0;box-shadow:none}' : '') +
25
+ (detached === 'preview' ? 'dialog.wb-preview{inset:0;width:100vw;height:100dvh;border:0;border-radius:0;box-shadow:none}' : '');
26
+ shadow.append(style);
27
+ const tooltips=createDevTooltips(shadow);
28
+ const controller = new AbortController(), events = {signal: controller.signal};
29
+ function element(tag, className, text) {
30
+ const node = document.createElement(tag); if (className) node.className = className;
31
+ if (text !== undefined) node.textContent = text; return node;
32
+ }
33
+ function button(label, handler, className = '') {
34
+ const node = element('button', className, label); node.type = 'button'; node.onclick = handler; return node;
35
+ }
36
+ function popoutButton(label, handler, isDock = false) {
37
+ const node = button('',handler,'wb-popout'); node.innerHTML = isDock ? dockIcon : popoutIcon;
38
+ node.setAttribute('aria-label',label); return tooltips.set(node,isDock?'Dock window':'Open in window');
39
+ }
40
+ function iconButton(label, drawing, handler, className = '') {
41
+ const node = button('', handler, 'wb-icon ' + className); node.innerHTML = svg(drawing);
42
+ node.setAttribute('aria-label', label); return tooltips.set(node,label);
43
+ }
44
+ const consolePanel = element('section', 'wb wb-console'); consolePanel.hidden = true;
45
+ consolePanel.setAttribute('role', 'dialog'); consolePanel.setAttribute('aria-label', 'Client console');
46
+ const heading = element('div', 'wb-head');
47
+ // Keep the capture context for reports and detached windows, outside the header.
48
+ let captureContext = 'Page';
49
+ const pausedButton = iconButton('Pause', '<path d="M9 5v14M15 5v14"/>', () => { paused = !paused; syncPauseButton(); render(); }, 'wb-push wb-pause');
50
+ pausedButton.firstElementChild.classList.add('wb-pause-icon');
51
+ pausedButton.insertAdjacentHTML('beforeend', svg('<path d="m9 5 10 7-10 7Z"/>'));
52
+ pausedButton.lastElementChild.classList.add('wb-resume-icon');
53
+ pausedButton.setAttribute('aria-pressed', 'false');
54
+ tooltips.set(pausedButton, 'Pause console', 'Messages keep recording');
55
+ const closeConsole = iconButton('Close console', '<path d="m6 6 12 12M6 18 18 6"/>', () => hideConsole(), 'wb-close');
56
+ const consolePopout = popoutButton(detached === 'console' ? 'Dock console' : 'Open console in new window', () => detached === 'console' ? dock?.() : popConsole(), detached === 'console');
57
+ heading.append(element('span','wb-title','Client console'), pausedButton, consolePopout, closeConsole);
58
+ const filters = element('div', 'wb-filters');
59
+ const searchRow = element('div', 'wb-search-row'), searchField = element('div', 'wb-search');
60
+ searchField.innerHTML = svg('<circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 4.5 4.5"/>');
61
+ const search = element('input'); search.type = 'search'; search.placeholder = 'Filter messages…'; search.setAttribute('aria-label', 'Filter console messages');
62
+ searchField.append(search);
63
+ const filterRow = element('div', 'wb-filter-row'), filterOptions = element('div', 'wb-filter-options');
64
+ filterOptions.setAttribute('role', 'group'); filterOptions.setAttribute('aria-label', 'Console filters');
65
+ const utilities = element('div', 'wb-console-utilities');
66
+ utilities.setAttribute('role', 'group'); utilities.setAttribute('aria-label', 'Console actions');
67
+ const level = element('select'); level.setAttribute('aria-label', 'Console level');
68
+ const sourceFilter = element('select'); sourceFilter.setAttribute('aria-label','Console source');
69
+ for(const [value,label] of [['all','All sources'],['browser','Browser'],['server','Server']]){const option=element('option','',label);option.value=value;sourceFilter.append(option);}
70
+ for (const [value, label] of [['all','All levels'],['error','Errors'],['warn','Warnings'],['info','Info'],['debug','Debug']]) {
71
+ const option = element('option', '', label); option.value = value; level.append(option);
72
+ }
73
+ const clear = button('Clear', () => { active?.clear(); frozen = []; render(true); });
74
+ const copy = button('Copy', async () => {
75
+ try { await view.navigator.clipboard.writeText(filtered().map(entry => `[${new Date(entry.time).toISOString()}] ${entry.source?'['+entry.source+'] ':''}${entry.level}: ${entry.text}`).join('\n')); copy.textContent = 'Copied'; feedback.textContent = 'Visible console messages copied.'; }
76
+ catch { copy.textContent = 'Failed'; feedback.textContent = 'Clipboard access unavailable.'; }
77
+ }, 'wb-copy'); copy.setAttribute('aria-label', 'Copy visible logs');
78
+ const aiGroup=element('div','wb-ai');
79
+ const aiCopy=button('',()=>copyForAI(filtered(),'logs',aiCopy),'wb-ai-main');aiCopy.innerHTML=copyIcon+'<span data-copy-label>Copy for AI</span>';aiCopy.setAttribute('aria-label','Copy for AI');
80
+ const aiToggle=button('',()=>aiMenuOpen?closeAIMenu(true):openAIMenu(),'wb-ai-toggle');aiToggle.innerHTML=chevronIcon;aiToggle.setAttribute('aria-label','AI copy options');aiToggle.setAttribute('aria-haspopup','menu');aiToggle.setAttribute('aria-expanded','false');
81
+ aiGroup.append(aiCopy,aiToggle);utilities.append(clear,copy);searchRow.append(searchField,utilities);
82
+ filterOptions.append(level);if(tools)filterOptions.append(sourceFilter);filterRow.append(filterOptions,aiGroup);filters.append(searchRow,filterRow);
83
+ tooltips.set(aiCopy, 'Copy for AI');
84
+ const aiMenu=element('div','wb wb-ai-menu');aiMenu.setAttribute('role','menu');aiMenu.setAttribute('aria-label','AI copy options');aiMenu.tabIndex=-1;aiMenu.popover='manual';
85
+ const errorsAI=button('Copy errors for AI',()=>{closeAIMenu(true);copyForAI((active?.snapshot().entries||[]).filter(entry=>entry.level==='error'),'errors',aiCopy);});
86
+ const visibleAI=button('Copy visible logs for AI',()=>{closeAIMenu(true);copyForAI(filtered(),'logs',aiCopy);});
87
+ errorsAI.title='All recorded errors, including those hidden by filters';visibleAI.title='Only messages matching the current filters';
88
+ for(const item of [errorsAI,visibleAI]){item.setAttribute('role','menuitem');item.tabIndex=-1;aiMenu.append(item);}
89
+ aiMenu.hidden=true;
90
+ const logs = element('div', 'wb-logs'); logs.tabIndex = 0; logs.setAttribute('role', 'region'); logs.setAttribute('aria-label', 'Console messages');
91
+ const footer = element('div', 'wb-foot');
92
+ const counts = element('span'), retained = element('span'); footer.append(counts, retained);
93
+ const feedback = element('span', 'sr-only'); feedback.setAttribute('role', 'status');
94
+ consolePanel.append(heading, filters, logs, footer, feedback, aiMenu); shadow.append(consolePanel);
95
+
96
+ let active = capture, rawActive = capture, unsubscribe, paused = false, frozen = [], visible = [], frame, previewStore;
97
+ let device = 'phone', rotated = false, customWidth, frameTimer, paint, disposed = false;
98
+ let frameEnabled = true, frameMotion;
99
+ try { frameEnabled = view.sessionStorage.getItem('prnext:dev-device-frame') !== 'false'; } catch {}
100
+ let viewportWidth=390,viewportScale=1,resizeStart,previewChrome;
101
+ let consoleWindow, previewWindow;
102
+ let aiMenuOpen=false;
103
+ const rowNodes=new Map();
104
+ function syncPauseButton() {
105
+ pausedButton.setAttribute('aria-pressed', String(paused));
106
+ pausedButton.setAttribute('aria-label', paused ? 'Resume' : 'Pause');
107
+ tooltips.set(pausedButton, paused ? 'Resume console' : 'Pause console', paused ? 'Show new messages' : 'Messages keep recording');
108
+ }
109
+ async function writeClipboard(text,control,label='Copied') {
110
+ feedback.textContent='';control.dataset.copyState='copying';control.setAttribute('aria-busy','true');
111
+ try {await view.navigator.clipboard.writeText(text);control.dataset.copyState='copied';feedback.textContent=label;
112
+ const textNode=control.querySelector('[data-copy-label]');if(textNode)textNode.textContent='Copied';
113
+ }catch{control.dataset.copyState='failed';feedback.textContent='Clipboard access unavailable. Allow clipboard access and try again.';
114
+ const textNode=control.querySelector('[data-copy-label]');if(textNode)textNode.textContent='Copy failed';}
115
+ finally{control.removeAttribute('aria-busy');}
116
+ }
117
+ function reportOptions(kind) {return {kind,version,route:currentURL(),source:captureContext,dropped:active?.snapshot().dropped||0};}
118
+ async function copyForAI(entries,kind,control) {
119
+ if(!entries.length)return;
120
+ await writeClipboard(consolePrompt(entries,reportOptions(kind)),control,kind==='errors'?'All captured errors copied for AI.':'Client messages copied for AI.');
121
+ }
122
+ function closeAIMenu(restore=false) {
123
+ aiMenuOpen=false;aiToggle.setAttribute('aria-expanded','false');
124
+ try{if(aiMenu.matches(':popover-open'))aiMenu.hidePopover();}catch{}aiMenu.hidden=true;
125
+ if(restore)aiToggle.focus();
126
+ }
127
+ function openAIMenu() {
128
+ aiMenuOpen=true;aiMenu.hidden=false;aiToggle.setAttribute('aria-expanded','true');aiMenu.showPopover();
129
+ const anchor=aiToggle.getBoundingClientRect(),box=aiMenu.getBoundingClientRect();
130
+ aiMenu.style.left=Math.max(8,Math.min(view.innerWidth-box.width-8,anchor.right-box.width))+'px';
131
+ aiMenu.style.top=(anchor.bottom+box.height+8<view.innerHeight?anchor.bottom+5:Math.max(8,anchor.top-box.height-5))+'px';
132
+ const first=[...aiMenu.children].find(item=>!item.disabled);(first||aiMenu).focus();
133
+ }
134
+ aiToggle.addEventListener('keydown',event=>{if(event.key==='ArrowDown'||event.key==='ArrowUp'){event.preventDefault();openAIMenu();}},events);
135
+ aiMenu.addEventListener('keydown',event=>{
136
+ const items=[...aiMenu.children].filter(item=>!item.disabled),index=items.indexOf(shadow.activeElement);
137
+ if(event.key==='Escape'){event.preventDefault();event.stopPropagation();closeAIMenu(true);}
138
+ else if(event.key==='Tab'){event.preventDefault();closeAIMenu(false);(event.shiftKey?aiCopy:logs).focus();}
139
+ else if(['ArrowDown','ArrowUp','Home','End'].includes(event.key)&&items.length){event.preventDefault();items[event.key==='Home'?0:event.key==='End'?items.length-1:index<0?(event.key==='ArrowUp'?items.length-1:0):(index+(event.key==='ArrowDown'?1:items.length-1))%items.length].focus();}
140
+ },events);
141
+ document.addEventListener('pointerdown',event=>{if(aiMenuOpen&&!event.composedPath().includes(aiMenu)&&!event.composedPath().includes(aiGroup))closeAIMenu();},{...events,capture:true});
142
+ document.addEventListener('focusin',event=>{if(aiMenuOpen&&!event.composedPath().includes(aiMenu)&&!event.composedPath().includes(aiGroup))closeAIMenu();},events);
143
+ function createRow(entry) {
144
+ const row=element('details','wb-entry');row.dataset.entry=String(entry.id);
145
+ const summary=element('summary','wb-line');summary.dataset.level=entry.level;
146
+ const timestamp=element('time','',new Date(entry.time).toLocaleTimeString([],{hour12:false}));timestamp.dateTime=new Date(entry.time).toISOString();
147
+ const caret=element('span','wb-entry-caret');caret.innerHTML=chevronIcon;
148
+ summary.append(timestamp,element('span','wb-level',entry.level),element('span','wb-message',(entry.source==='server'?'[server] ':'')+entry.text),caret);
149
+ const actions=element('div','wb-entry-actions');
150
+ const raw=button('',()=>writeClipboard(entry.text,raw,'Complete captured message copied.'));
151
+ raw.innerHTML=copyIcon+'<span data-copy-label>Copy message</span>';
152
+ const ai=button('',()=>copyForAI([entry],'message',ai));ai.innerHTML=copyIcon+'<span data-copy-label>Copy for AI</span>';
153
+ raw.setAttribute('aria-label','Copy message');ai.setAttribute('aria-label','Copy for AI');
154
+ actions.append(raw,ai);row.append(summary,actions);
155
+ const full=element('pre','wb-entry-full',entry.text);
156
+ row.addEventListener('toggle',()=>{if(row.open)row.append(full);else full.remove();});return row;
157
+ }
158
+ function filtered() {
159
+ const query = search.value.toLocaleLowerCase();
160
+ return visible.filter(entry => (sourceFilter.value==='all'||(entry.source||'browser')===sourceFilter.value) && (level.value === 'all' || entry.level === level.value || level.value === 'info' && entry.level === 'log') && (!query || entry.text.toLocaleLowerCase().includes(query)));
161
+ }
162
+ function render(force = false) {
163
+ if (disposed || consolePanel.hidden) return;
164
+ const data = active?.snapshot() || {entries:[],dropped:0};
165
+ if (!paused || force) frozen = data.entries;
166
+ visible = frozen;
167
+ const shown = filtered(), atBottom = logs.scrollHeight - logs.scrollTop - logs.clientHeight < 35, oldTop = logs.scrollTop;
168
+ const retainedIDs=new Set([...data.entries,...shown].map(entry=>entry.id));
169
+ for(const [id] of rowNodes)if(!retainedIDs.has(id))rowNodes.delete(id);
170
+ const nodes=shown.map(entry=>{let row=rowNodes.get(entry.id);if(!row){row=createRow(entry);rowNodes.set(entry.id,row);}return row;});
171
+ if(!shown.length)nodes.push(element('div','wb-empty',!active?'Waiting for preview…':data.entries.length?'No matching messages':'No client messages yet'));
172
+ // Leave existing rows in place so new logs preserve open messages and focus.
173
+ const wanted=new Set(nodes);for(const node of [...logs.children])if(!wanted.has(node))node.remove();
174
+ nodes.forEach((node,index)=>{if(logs.children[index]!==node)logs.insertBefore(node,logs.children[index]||null);});
175
+ logs.scrollTop = atBottom ? logs.scrollHeight : oldTop;
176
+ const sourceEntries=data.entries.filter(entry=>sourceFilter.value==='all'||(entry.source||'browser')===sourceFilter.value);
177
+ const errors = sourceEntries.filter(entry => entry.level === 'error').length, warnings = sourceEntries.filter(entry => entry.level === 'warn').length;
178
+ counts.textContent = `${errors} ${errors === 1 ? 'error' : 'errors'} · ${warnings} ${warnings === 1 ? 'warning' : 'warnings'}`;
179
+ retained.textContent = `${shown.length}/${data.entries.length} messages${paused ? ' · display paused' : ''}${data.dropped ? ' · older messages removed' : ''}`;
180
+ errorsAI.disabled=!data.entries.some(entry=>entry.level==='error');visibleAI.disabled=shown.length===0;aiCopy.disabled=shown.length===0;
181
+ }
182
+ function schedule() { if (!paint && !consolePanel.hidden) paint = requestAnimationFrame(() => { paint = null; render(); }); }
183
+ function subscribeStore() { unsubscribe?.(); unsubscribe=consolePanel.hidden?undefined:active?.subscribe(schedule); }
184
+ function useStore(store, label) {
185
+ closeAIMenu();rowNodes.clear();logs.replaceChildren();
186
+ unsubscribe?.(); rawActive=store;active = store&&tools?tools.captureFor(store):store; captureContext = label; frozen = []; paused = false;
187
+ syncPauseButton();
188
+ subscribeStore(); render(true);
189
+ consoleWindow?.tools.setCapture(store,label);
190
+ }
191
+ search.addEventListener('input', () => { copy.textContent = 'Copy'; render(); }, events);
192
+ level.addEventListener('change', () => { copy.textContent = 'Copy'; render(); }, events);
193
+ sourceFilter.addEventListener('change', () => { copy.textContent = 'Copy'; render(); }, events);
194
+ consolePanel.addEventListener('keydown', event => { if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); hideConsole(); } }, events);
195
+ function showConsole() { if (consoleWindow && !consoleWindow.window.closed) { consoleWindow.focus(); return; } consoleWindow?.close(); consolePanel.hidden = false;subscribeStore(); consoleToggle.setAttribute('aria-pressed','true'); copy.textContent = 'Copy'; render(true); search.focus(); }
196
+ function hideConsole() { tooltips.hide();closeAIMenu();if (detached === 'console') { closeWindow?.(); return; } consolePanel.hidden = true;unsubscribe?.();unsubscribe=undefined; consoleToggle.setAttribute('aria-pressed','false'); if (preview.open) consoleToggle.focus(); else restoreFocus(); }
197
+
198
+ const preview = element('dialog', 'wb wb-preview'); preview.setAttribute('aria-label', 'Device preview');
199
+ const toolbar = element('div','wb-preview-head'), devices = element('div','wb-devices'); devices.setAttribute('role','group'); devices.setAttribute('aria-label','Preview device');
200
+ function deviceButton(value,label,width) {
201
+ const node=button('',()=>setDevice(value),'wb-device');node.innerHTML=deviceIcons[value];node.setAttribute('aria-label',label);node.setAttribute('aria-description',`${width}px viewport`);return tooltips.set(node,label);
202
+ }
203
+ const phone=deviceButton('phone','Phone',390),tablet=deviceButton('tablet','Tablet',768),desktop=deviceButton('desktop','Desktop',1280);
204
+ devices.append(desktop,phone,tablet);
205
+ const dimensions = element('span','wb-dimensions');
206
+ const rotate = iconButton('Rotate', '<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"/><rect x="9" y="8" width="6" height="8" rx="1"/>', () => rotatePreview());
207
+ const frameToggle = iconButton('Device frame', '<path d="M8 3H5a2 2 0 0 0-2 2v3m13-5h3a2 2 0 0 1 2 2v3M3 16v3a2 2 0 0 0 2 2h3m8 0h3a2 2 0 0 0 2-2v-3"/><rect x="8" y="7" width="8" height="10" rx="1.5"/>', () => setFrame(!frameEnabled));
208
+ frameToggle.classList.add('wb-frame-toggle');frameToggle.append(element('span','','Frame'));
209
+ frameToggle.setAttribute('aria-pressed',String(frameEnabled));
210
+ const consoleToggle = iconButton('Console', '<path d="m5 7 5 5-5 5m8 0h6"/>', () => consolePanel.hidden ? showConsole() : hideConsole());
211
+ consoleToggle.setAttribute('aria-pressed','false');
212
+ const closePreview = iconButton('Close device preview', '<path d="m6 6 12 12M6 18 18 6"/>', () => preview.close(), 'wb-close');
213
+ const previewPopout = popoutButton(detached === 'preview' ? 'Dock device preview' : 'Open device preview in new window', () => detached === 'preview' ? dock?.() : popPreview(device), detached === 'preview');
214
+ const toolbarActions=element('div','wb-preview-actions');toolbarActions.append(rotate,frameToggle,consoleToggle,previewPopout,closePreview);
215
+ toolbar.append(devices,dimensions,toolbarActions);
216
+ const stage = element('div','wb-stage'), frameWrap = element('div','wb-frame-wrap'), screen = element('div','wb-screen'); frameWrap.append(screen);stage.append(frameWrap);
217
+ const deviceTop = element('div','wb-device-top'), deviceHome = element('div','wb-device-home');
218
+ for(let index=0;index<3;index++)deviceTop.append(element('i'));
219
+ deviceTop.setAttribute('aria-hidden','true');deviceHome.setAttribute('aria-hidden','true');frameWrap.append(deviceTop,deviceHome);
220
+ const resizeHandle=button('',()=>{},'wb-resize');resizeHandle.setAttribute('role','slider');resizeHandle.setAttribute('aria-label','Preview width');resizeHandle.setAttribute('aria-orientation','horizontal');resizeHandle.setAttribute('aria-valuemin','240');resizeHandle.setAttribute('aria-valuemax','2560');frameWrap.append(resizeHandle);
221
+ function setWidth(width) {customWidth=Math.max(240,Math.min(2560,Math.round(width)));sizePreview();}
222
+ resizeHandle.addEventListener('pointerdown',event=>{if(event.button!==0)return;event.preventDefault();resizeHandle.focus();resizeStart={x:event.clientX,width:viewportWidth,scale:viewportScale,pointer:event.pointerId};resizeHandle.setPointerCapture(event.pointerId);stage.dataset.resizing='true';},events);
223
+ resizeHandle.addEventListener('pointermove',event=>{if(resizeStart&&event.pointerId===resizeStart.pointer)setWidth(resizeStart.width+(event.clientX-resizeStart.x)/resizeStart.scale);},events);
224
+ function stopResize(){resizeStart=null;stage.dataset.resizing='false';}
225
+ resizeHandle.addEventListener('pointerup',stopResize,events);resizeHandle.addEventListener('pointercancel',stopResize,events);resizeHandle.addEventListener('lostpointercapture',stopResize,events);
226
+ resizeHandle.addEventListener('keydown',event=>{const step=event.shiftKey?50:10;if(['ArrowLeft','ArrowRight','Home','End'].includes(event.key)){event.preventDefault();setWidth(event.key==='Home'?240:event.key==='End'?2560:viewportWidth+(event.key==='ArrowLeft'?-step:step));}},events);
227
+ const note = element('div','wb-preview-note');
228
+ const previewStatus = element('span');note.setAttribute('role','status');note.hidden=true;
229
+ dimensions.setAttribute('aria-description','CSS viewport size in your current browser, without device OS or touch emulation.');
230
+ const retry = button('', () => { if (frame) { beginLoading(); frame.src = location.href; } },'wb-icon wb-preview-reload');
231
+ retry.innerHTML=svg('<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"/>');retry.setAttribute('aria-label','Reload preview');tooltips.set(retry,'Refresh preview');toolbarActions.prepend(retry);
232
+ tooltips.set(closePreview,'Close preview');
233
+ note.append(previewStatus); preview.append(toolbar,stage,note); shadow.append(preview);
234
+ function animateFrame() {
235
+ frameMotion?.cancel();frameMotion=null;
236
+ if(preview.open&&!view.matchMedia('(prefers-reduced-motion: reduce)').matches)
237
+ frameMotion=frameWrap.animate([{opacity:.65,translate:'0 4px'},{opacity:1,translate:'0 0'}],{duration:200,easing:'cubic-bezier(.16,1,.3,1)'});
238
+ }
239
+ function rotatePreview({animate=true}={}) {
240
+ if(!frame||!preview.open||device==='desktop')return;
241
+ const before=frameWrap.getBoundingClientRect(),computed=view.getComputedStyle(frameWrap);
242
+ const oldWidth=parseFloat(frameWrap.style.width),oldHeight=parseFloat(frameWrap.style.height);
243
+ const previous=new view.DOMMatrixReadOnly(computed.transform==='none'?undefined:computed.transform);
244
+ const oldOpacity=Number(computed.opacity);
245
+ rotated=!rotated;customWidth=undefined;sizePreview();
246
+ if(!animate||view.matchMedia('(prefers-reduced-motion: reduce)').matches)return;
247
+ const after=frameWrap.getBoundingClientRect(),width=parseFloat(frameWrap.style.width),height=parseFloat(frameWrap.style.height);
248
+ // Convert the current visual transform to the new orientation before
249
+ // cancelling an interrupted turn. The speaker stays on the same physical end.
250
+ const start=new view.DOMMatrix([previous.a,previous.b,previous.c,previous.d,0,0])
251
+ .rotate(rotated?90:-90).scale(oldHeight/width,oldWidth/height);
252
+ const angle=Math.atan2(start.b,start.a),scaleX=Math.hypot(start.a,start.b),scaleY=(start.a*start.d-start.b*start.c)/scaleX;
253
+ const dx=before.x+before.width/2-after.x-after.width/2,dy=before.y+before.height/2-after.y-after.height/2;
254
+ const availableWidth=Math.max(1,stage.clientWidth-32),availableHeight=Math.max(1,stage.clientHeight-32),keyframes=[];
255
+ // Fit the entire rotating silhouette, including its diagonal, to the canvas.
256
+ // These small transform-only keyframes run on the compositor, not a JS loop.
257
+ for(let index=0;index<=24;index++){
258
+ const progress=index/24,remaining=1-progress,radians=angle*remaining;
259
+ const sx=1+(scaleX-1)*remaining,sy=1+(scaleY-1)*remaining;
260
+ const boundsWidth=Math.abs(Math.cos(radians))*width*sx+Math.abs(Math.sin(radians))*height*sy;
261
+ const boundsHeight=Math.abs(Math.sin(radians))*width*sx+Math.abs(Math.cos(radians))*height*sy;
262
+ const fit=Math.min(1,availableWidth/boundsWidth,availableHeight/boundsHeight);
263
+ keyframes.push({offset:progress,transform:`translate(${dx*remaining}px,${dy*remaining}px) rotate(${radians*180/Math.PI}deg) scale(${sx*fit},${sy*fit})`,opacity:oldOpacity+(1-oldOpacity)*progress});
264
+ }
265
+ const motion=frameWrap.animate(keyframes,{duration:380,easing:'cubic-bezier(.22,1,.36,1)'});frameMotion=motion;
266
+ motion.onfinish=()=>{if(frameMotion===motion)frameMotion=null;};
267
+ }
268
+ function setFrame(enabled) {
269
+ frameEnabled=Boolean(enabled);frameToggle.setAttribute('aria-pressed',String(frameEnabled));
270
+ try{view.sessionStorage.setItem('prnext:dev-device-frame',String(frameEnabled));}catch{}
271
+ sizePreview();animateFrame();
272
+ }
273
+ function clearPreviewChrome() {
274
+ if(!previewChrome)return;
275
+ previewChrome.touch.dispose();
276
+ try{const {document,sheet}=previewChrome;document.adoptedStyleSheets=document.adoptedStyleSheets.filter(value=>value!==sheet);}catch{}
277
+ previewChrome=undefined;
278
+ }
279
+ function syncPreviewChrome() {
280
+ if(!frame)return;
281
+ try{
282
+ const child=frame.contentWindow;
283
+ if(child.location.origin!==location.origin){clearPreviewChrome();return;}
284
+ const childDocument=child.document;
285
+ // Navigation can expose the new Document before its root is parsed. The
286
+ // load/ready callback installs the cursor and panning together afterward.
287
+ if(!childDocument.documentElement){clearPreviewChrome();return;}
288
+ if(previewChrome?.document!==childDocument)clearPreviewChrome();
289
+ if(device==='desktop'){clearPreviewChrome();return;}
290
+ if(!previewChrome){
291
+ // A sheet owned by this preview document only. No overflow changes or
292
+ // application styles are modified; removing it restores the original UI.
293
+ const sheet=new child.CSSStyleSheet();
294
+ // Native cursors stay centered even when the device is scaled.
295
+ sheet.replaceSync(':root,:root *{scrollbar-width:none!important;scrollbar-gutter:auto!important}:root::-webkit-scrollbar,:root *::-webkit-scrollbar{display:none!important;width:0!important;height:0!important}' + contactCursorStyle);
296
+ previewChrome={document:childDocument,sheet,touch:createPreviewTouch(childDocument)};
297
+ }
298
+ if(!childDocument.adoptedStyleSheets.includes(previewChrome.sheet))childDocument.adoptedStyleSheets=[...childDocument.adoptedStyleSheets,previewChrome.sheet];
299
+ }catch{clearPreviewChrome();}
300
+ }
301
+ function sizePreview() {
302
+ if (!frame || !preview.open) return;
303
+ tooltips.hide();
304
+ frameMotion?.cancel();frameMotion=null;
305
+ // Scale the entire device, keeping its CSS viewport independent of decoration.
306
+ const inset = !frameEnabled ? [0,0,0,0] : device==='desktop' ? [32,6,6,6] : device==='tablet' ? [16,16,16,16] : [12,12,12,12];
307
+ const [top,right,bottom,left]=inset,chromeWidth=left+right,chromeHeight=top+bottom;
308
+ const availableWidth=Math.max(1,stage.clientWidth-40),availableHeight=Math.max(1,stage.clientHeight-40);
309
+ let [width,height] = device === 'phone' ? [390,844] : device==='tablet' ? [768,1024] : [Math.max(240,availableWidth-chromeWidth),Math.max(240,availableHeight-chromeHeight)];
310
+ if (rotated) [width,height] = [height,width];
311
+ if(customWidth!==undefined)width=customWidth;
312
+ const scale = Math.min(1,availableWidth/(width+chromeWidth),availableHeight/(height+chromeHeight));
313
+ viewportWidth=width;viewportScale=scale;
314
+ frame.style.width = width + 'px'; frame.style.height = height + 'px'; frame.style.transform = `scale(${scale})`;
315
+ screen.style.width=width*scale+'px';screen.style.height=height*scale+'px';screen.style.left=left*scale+'px';screen.style.top=top*scale+'px';
316
+ screen.style.borderRadius=!frameEnabled?'0px':device==='desktop'?`0 0 ${6*scale}px ${6*scale}px`:(device==='phone'?32:14)*scale+'px';
317
+ const outerWidth=(width+chromeWidth)*scale,outerHeight=(height+chromeHeight)*scale;
318
+ frameWrap.style.width=outerWidth+'px';frameWrap.style.height=outerHeight+'px';frameWrap.style.left=(stage.clientWidth-outerWidth)/2+'px';frameWrap.style.top=(stage.clientHeight-outerHeight)/2+'px';
319
+ frameWrap.style.borderRadius=(frameEnabled?(device==='phone'?44:device==='tablet'?30:12)*scale:0)+'px';frameWrap.style.setProperty('--s',String(scale));
320
+ frameWrap.dataset.device=device;frameWrap.dataset.frame=String(frameEnabled);frameWrap.dataset.rotated=String(rotated);
321
+ rotate.hidden=device==='desktop';
322
+ dimensions.textContent = `${width} × ${height}`;
323
+ dimensions.setAttribute('aria-label',`${width} by ${height} pixel viewport, ${Math.round(scale*100)} percent scale`);
324
+ for(const [node,value,defaultWidth,defaultHeight] of [[phone,'phone',390,844],[tablet,'tablet',768,1024],[desktop,'desktop',Math.max(240,availableWidth-12),Math.max(240,availableHeight-38)]]){
325
+ node.setAttribute('aria-pressed',String(device===value));
326
+ tooltips.set(node,node.getAttribute('aria-label'),`${device===value?width:defaultWidth} × ${device===value?height:defaultHeight}`);
327
+ node.setAttribute('aria-description',`${device===value?width:defaultWidth} by ${device===value?height:defaultHeight} pixel viewport`);
328
+ }
329
+ tooltips.set(rotate,'Rotate device');tooltips.set(frameToggle,frameEnabled?'Hide device frame':'Show device frame');
330
+ resizeHandle.setAttribute('aria-valuenow',String(width));resizeHandle.setAttribute('aria-valuetext',`${width}px`);
331
+ syncPreviewChrome();
332
+ }
333
+ const observer = new view.ResizeObserver(sizePreview); observer.observe(stage);
334
+ function attachPreview() {
335
+ if (!frame || !preview.open) return;
336
+ try {
337
+ const child = frame.contentWindow;
338
+ if (child.location.origin !== location.origin) throw new Error('External page');
339
+ syncPreviewChrome();
340
+ const store = child.__PRNEXT_DEV__?.consoleCapture;
341
+ if (store && store !== previewStore) { previewStore = store; useStore(store,'Preview'); }
342
+ if (store) { clearTimeout(frameTimer); previewStatus.textContent = '';note.hidden=true;preview.removeAttribute('aria-busy'); }
343
+ } catch { clearPreviewChrome();previewStore = null; useStore(null,'Preview'); previewStatus.textContent = 'Preview unavailable. Reload to return.';note.hidden=false;preview.removeAttribute('aria-busy'); }
344
+ }
345
+ view.addEventListener('message', event => {
346
+ if (frame && event.source === frame.contentWindow && event.origin === location.origin && event.data?.type === 'prnext:preview-ready') attachPreview();
347
+ }, events);
348
+ function setDevice(value) { device = value; rotated = false;customWidth=undefined;sizePreview();animateFrame(); }
349
+ function beginLoading() {
350
+ clearTimeout(frameTimer);clearPreviewChrome(); previewStore = null; useStore(null,'Preview'); previewStatus.textContent = '';note.hidden=true;preview.setAttribute('aria-busy','true');
351
+ frameTimer = setTimeout(() => { if (!previewStore) {previewStatus.textContent = 'Preview unavailable. Open it in a new window or reload.';note.hidden=false;preview.removeAttribute('aria-busy');} }, 15000);
352
+ }
353
+ function openPreview(value) {
354
+ if (previewWindow?.window.closed) previewWindow.close();
355
+ if (previewWindow) {previewWindow.tools.openPreview(value);previewWindow.focus();return;}
356
+ if (!preview.open) {
357
+ beginLoading();
358
+ frame = element('iframe','wb-frame'); frame.title = 'PRNext responsive preview'; frame.dataset.prnextPreview = 'true';
359
+ frame.addEventListener('load', attachPreview, events); frame.src = location.href; screen.append(frame);
360
+ if (!consoleWindow) preview.append(consolePanel);consoleToggle.setAttribute('aria-pressed',String(!consolePanel.hidden));preview.showModal();
361
+ }
362
+ setDevice(value); (value === 'phone' ? phone : value==='tablet'?tablet:desktop).focus();
363
+ }
364
+ preview.addEventListener('close', () => {
365
+ tooltips.hide();
366
+ clearTimeout(frameTimer);clearPreviewChrome(); frameMotion?.cancel();frameMotion=null;frame?.remove(); frame = null; previewStore = null;
367
+ shadow.append(consolePanel); useStore(capture,'Page'); restoreFocus();
368
+ if (detached === 'preview') closeWindow?.();
369
+ }, events);
370
+ const popupFeedback = element('div','wb wb-popup-feedback'); popupFeedback.hidden = true; popupFeedback.setAttribute('role','status');
371
+ shadow.append(popupFeedback);
372
+ function blocked() {
373
+ popupFeedback.textContent = 'Popup blocked. Allow popups for this site, then try again.'; popupFeedback.hidden = false;
374
+ if (preview.open) preview.append(popupFeedback);
375
+ else { showConsole(); consolePanel.append(popupFeedback); }
376
+ }
377
+ function currentURL() { try { const url = frame?.contentWindow?.location.href; if (url && new URL(url).origin === location.origin) return url; } catch {} return location.href; }
378
+ function createPopout(kind, value = device, reservedWindow) {
379
+ const existing = kind === 'console' ? consoleWindow : previewWindow;
380
+ if (existing && !existing.window.closed) { reservedWindow?.close();if (kind === 'preview') existing.tools.openPreview(value); existing.focus(); return existing; }
381
+ existing?.close();
382
+ let record;
383
+ const source = currentURL();
384
+ const popup = createToolWindow({owner:view,title:kind==='console'?'PRNext — Client console':'PRNext — Device preview',width:kind==='console'?860:value==='tablet'?1000:560,height:kind==='console'?520:960,
385
+ reservedWindow,onClose() { record?.tools.dispose(); if (kind==='console' && consoleWindow===record) consoleWindow = null; else if (kind==='preview' && previewWindow===record) previewWindow = null; },
386
+ });
387
+ if (!popup) { blocked(); return null; }
388
+ popupFeedback.hidden = true;
389
+ const child = createDevWorkbench({shadow:popup.shadow,capture:rawActive,tools,sourceURL:()=>source,detached:kind,restoreFocus:()=>{},closeWindow:()=>popup.close(),
390
+ dock() {
391
+ const last = child.previewState(), consoleState = child.consoleState(); popup.close();
392
+ if (kind === 'console') { setConsoleState(consoleState); showConsole(); }
393
+ else { openPreview(last.device);rotated=last.rotated;customWidth=last.customWidth;setFrame(last.frameEnabled);if(frame&&last.url!==location.href){beginLoading();frame.src=last.url;}sizePreview(); }
394
+ view.focus();
395
+ },
396
+ });
397
+ record = {...popup,tools:child};
398
+ if (kind === 'console') { consoleWindow = record; child.setCapture(rawActive,captureContext); child.setConsoleState(getConsoleState()); child.showConsole(); }
399
+ else { previewWindow = record; child.openPreview(value);child.setFrame(frameEnabled); if(value===device){if(rotated)child.rotate({animate:false});if(customWidth!==undefined)child.setWidth(customWidth);} }
400
+ popup.focus(); return record;
401
+ }
402
+ function popConsole(reservedWindow) { const popup = createPopout('console',device,reservedWindow); if (popup) {consolePanel.hidden = true;unsubscribe?.();unsubscribe=undefined;consoleToggle.setAttribute('aria-pressed','false');} }
403
+ function getConsoleState() { return {query:search.value,level:level.value,source:sourceFilter.value,paused}; }
404
+ function setConsoleState(state) { search.value=state.query;level.value=state.level;sourceFilter.value=state.source||'all';paused=state.paused;syncPauseButton();render(true); }
405
+ function popPreview(value = device,reservedWindow) {
406
+ if (value === 'desktop') {
407
+ // Desktop is the original application, with the browser's normal viewport.
408
+ try { const popup = reservedWindow===undefined?view.open(location.href,'_blank','popup=yes,width=1280,height=900,resizable=yes,scrollbars=yes'):reservedWindow; if (popup) { popup.opener=null;if(reservedWindow)popup.location.href=location.href;popup.focus(); } else blocked(); }
409
+ catch { blocked(); }
410
+ return;
411
+ }
412
+ const popup = createPopout('preview',value,reservedWindow); if (popup && preview.open) preview.close();
413
+ }
414
+ useStore(capture,'Page');
415
+ return {
416
+ showConsole, openPreview, popConsole, popPreview,
417
+ consoleState:getConsoleState,setConsoleState,
418
+ setCapture(store,label='Page') { capture=store;useStore(store,label); },
419
+ previewState:()=>({device,rotated,customWidth,frameEnabled,url:currentURL()}),setWidth,setFrame,
420
+ rotate:rotatePreview,
421
+ hide() { tooltips.hide();consoleWindow?.close(); previewWindow?.close(); consolePanel.hidden = true;unsubscribe?.();unsubscribe=undefined;if (preview.open) preview.close(); },
422
+ dispose() {
423
+ disposed = true;tooltips.dispose();closeAIMenu();clearPreviewChrome();rowNodes.clear();clearTimeout(frameTimer);frameMotion?.cancel();cancelAnimationFrame(paint);unsubscribe?.();observer.disconnect();controller.abort();
424
+ consoleWindow?.close(); previewWindow?.close();
425
+ if (preview.open) preview.close(); frame?.remove(); preview.remove(); consolePanel.remove(); popupFeedback.remove(); style.remove();
426
+ },
427
+ };
428
+ }
@@ -0,0 +1,31 @@
1
+ import {Console} from 'node:console';
2
+ import {Writable} from 'node:stream';
3
+ import {AsyncLocalStorage} from 'node:async_hooks';
4
+
5
+ const prefix='\x1ePRNEXT_DEV_LOG:';
6
+ /** Private stderr framing; the controller strips it before terminal output. */
7
+ export function parseDevLogLine(value){
8
+ if(!value.startsWith(prefix))return {text:value};
9
+ const end=value.indexOf('\x1f',prefix.length);if(end<0||end>2000)return {text:value};
10
+ try{
11
+ const fields=JSON.parse(value.slice(prefix.length,end));
12
+ if(!['info','warn','error','debug','trace'].includes(fields.level))return {text:value};
13
+ return {text:value.slice(end+1),level:fields.level,...(typeof fields.requestId==='string'?{requestId:fields.requestId.slice(0,100)}:{}),...(typeof fields.route==='string'?{route:fields.route.slice(0,1024)}:{}),structuredLevel:true};
14
+ }catch{return {text:value};}
15
+ }
16
+ export function createDevWorkerConsole({output=process.stderr}={}){
17
+ const scope=new AsyncLocalStorage();let level='info';
18
+ const stream=new Writable({write(chunk,encoding,done){output.write(chunk,encoding,done);}});
19
+ stream.on('error',()=>{});stream.isTTY=output.isTTY;stream.columns=output.columns;
20
+ const nativeWrite=stream.write.bind(stream);
21
+ stream.write=(chunk,...args)=>{
22
+ const context=scope.getStore()||{},metadata=JSON.stringify({level,...(context.requestId!==undefined?{requestId:String(context.requestId).slice(0,100)}:{}),...(context.route?{route:String(context.route).split(/[?#]/,1)[0].slice(0,1024)}:{})});
23
+ const framed=String(chunk).split(/(?<=\n)/).map(part=>part?prefix+metadata+'\x1f'+part:'').join('');return nativeWrite(framed,...args);
24
+ };
25
+ const logger=new Console({stdout:stream,stderr:stream});
26
+ for(const method of ['log','info','warn','error','debug','trace','table','dir','assert','group','groupCollapsed','groupEnd','clear','count','countReset','time','timeLog','timeEnd']){
27
+ const original=logger[method];if(typeof original!=='function')continue;
28
+ logger[method]=(...args)=>{const previous=level;level=method==='warn'?'warn':method==='error'||method==='assert'?'error':method==='debug'||method==='trace'?'debug':'info';try{return Reflect.apply(original,logger,args);}finally{level=previous;}};
29
+ }
30
+ return {console:logger,run:(metadata,callback)=>scope.run(metadata,callback)};
31
+ }