@thomas.f/prnext 0.1.16-alpha → 0.1.18-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 +35 -503
- package/build/ADAPTERS.md +27 -2
- package/build/actions.mjs +6 -1
- package/build/analyze.mjs +340 -0
- package/build/app-config.mjs +20 -8
- package/build/app-static.mjs +41 -14
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +99 -31
- package/build/build-lock.mjs +6 -6
- package/build/bundle-runtime.mjs +39 -11
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/check.mjs +56 -11
- package/build/compact-css.mjs +53 -0
- package/build/compilation-cache.mjs +269 -0
- package/build/compilation-store.mjs +142 -0
- package/build/compiler.mjs +51 -4
- package/build/concurrent.mjs +12 -0
- package/build/config.mjs +50 -13
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +65 -10
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-compiler-health.mjs +11 -0
- package/build/dev-css-update.mjs +85 -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 +64 -6
- package/build/dynamic.mjs +1 -1
- package/build/edge.mjs +5 -4
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +50 -0
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/incremental.mjs +8 -4
- package/build/index.mjs +140 -112
- package/build/inspect.mjs +3 -0
- package/build/inspection-summary.mjs +37 -4
- package/build/instrumentation.mjs +30 -0
- package/build/loader-cache.mjs +53 -26
- package/build/metadata-routes.mjs +3 -4
- package/build/migrate.mjs +60 -57
- package/build/migration-audit.mjs +1 -1
- package/build/native-type-config.mjs +37 -0
- package/build/npm-packages.mjs +3 -18
- package/build/package-manager.mjs +308 -0
- package/build/persistent-cache.mjs +112 -0
- package/build/precompress.mjs +62 -30
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +100 -0
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +10 -0
- package/build/production-builder.mjs +129 -0
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/scan.mjs +4 -4
- package/build/source-analysis.mjs +42 -0
- package/build/static-adapter.mjs +13 -5
- package/build/static-export.mjs +4 -4
- package/build/static-input.mjs +48 -0
- package/build/styles.mjs +17 -3
- package/build/transform-cache.mjs +37 -4
- package/build/transform.mjs +1 -1
- package/build/type-command.mjs +41 -0
- package/build/typed-routes.mjs +95 -0
- package/build/typescript-dev-worker.mjs +18 -0
- package/build/typescript-dev.mjs +49 -0
- package/build/typescript-native-worker.mjs +55 -0
- package/build/typescript-native.mjs +18 -0
- package/build/typescript-toolchain.mjs +19 -0
- package/build/typescript-watch.mjs +22 -0
- package/build/typescript.mjs +11 -8
- package/build/webpack-graph.mjs +170 -30
- package/build/worker-memory.mjs +9 -0
- package/build/worker-preload.mjs +9 -0
- package/cli.mjs +162 -122
- package/compat/activity-cache.cjs +26 -0
- package/compat/after.cjs +6 -4
- package/compat/app-layout-context.cjs +52 -2
- package/compat/base64.cjs +10 -0
- package/compat/cache-handlers.cjs +12 -2
- package/compat/cache-wire.cjs +28 -0
- package/compat/cache.cjs +2 -1
- package/compat/cache.d.cts +4 -0
- package/compat/cookies.cjs +21 -5
- package/compat/data-cache.cjs +12 -8
- package/compat/flight-delta.cjs +150 -0
- package/compat/headers.cjs +4 -2
- package/compat/incremental-cache.cjs +20 -7
- package/compat/index.d.ts +89 -10
- package/compat/link.cjs +12 -9
- package/compat/link.d.cts +4 -3
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/navigation.d.cts +4 -3
- package/compat/offline-store.cjs +11 -0
- package/compat/offline.cjs +5 -0
- package/compat/offline.d.cts +2 -0
- package/compat/redis-cache.cjs +201 -0
- package/compat/redis-cache.d.cts +37 -0
- package/compat/router.d.cts +4 -3
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/typed-route.d.cts +17 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +275 -0
- package/docs/ai/data.md +288 -0
- package/docs/ai/deployment.md +269 -0
- package/docs/ai/manifest.json +11 -0
- package/docs/ai/quickstart.md +171 -0
- package/docs/ai/routing.md +214 -0
- package/docs/ai/troubleshooting.md +155 -0
- package/index.mjs +2 -0
- package/native/diagnostics.mjs +34 -0
- package/native/resolve.mjs +7 -2
- package/package.json +44 -7
- package/release/ai-docs.mjs +48 -0
- package/release/verify.mjs +2 -0
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/ai-docs.mjs +202 -0
- package/runtime/app-client.mjs +226 -68
- package/runtime/app-content.mjs +4 -3
- package/runtime/app-partial-client.mjs +3 -0
- package/runtime/app-partial.mjs +1 -1
- package/runtime/app-prefetch-constants.mjs +4 -0
- package/runtime/app-prefetch.mjs +2 -3
- package/runtime/app-render-key.mjs +5 -0
- package/runtime/app-render.mjs +100 -28
- package/runtime/app-routing.mjs +22 -9
- package/runtime/app-static.mjs +18 -8
- package/runtime/app-stream-html.mjs +10 -53
- package/runtime/app-style-selection.mjs +34 -0
- package/runtime/app-styles.mjs +154 -0
- package/runtime/cache-components-rsc.mjs +4 -1
- package/runtime/cache-request.mjs +2 -1
- package/runtime/cloud/archive.mjs +5 -6
- package/runtime/compile-cache.mjs +8 -0
- package/runtime/deployment-client.mjs +19 -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 +245 -46
- package/runtime/dev-compiler-broker.mjs +42 -0
- 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 +191 -0
- package/runtime/dev-issue-motion.mjs +214 -0
- package/runtime/dev-lazy-tool.mjs +34 -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 +324 -0
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/env.mjs +2 -1
- package/runtime/fetch-cache.mjs +10 -8
- package/runtime/flight-delta.mjs +50 -0
- package/runtime/flight-tap.mjs +28 -0
- package/runtime/html-only.mjs +27 -0
- package/runtime/http.mjs +2 -0
- package/runtime/incremental-cache.mjs +25 -4
- package/runtime/instrumentation.mjs +49 -0
- package/runtime/navigation-recovery.mjs +36 -0
- package/runtime/navigation.mjs +7 -0
- package/runtime/node-html-stream.mjs +47 -0
- package/runtime/pages-client.mjs +24 -8
- package/runtime/pages-navigation.mjs +54 -64
- package/runtime/persistent/client.mjs +6 -5
- package/runtime/persistent/daemon.mjs +66 -56
- package/runtime/persistent/native-proxy.mjs +63 -0
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/preload.mjs +12 -0
- package/runtime/profile.mjs +14 -0
- package/runtime/profiles.json +2 -2
- package/runtime/render.mjs +3 -0
- package/runtime/route-pattern.mjs +44 -0
- package/runtime/rsc-worker.mjs +41 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/typed-route.mjs +19 -0
- package/runtime/watch-project.mjs +29 -18
- package/runtime/worker.mjs +102 -24
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Leave space for the inherited control envelope below its 64 KiB limit.
|
|
2
|
+
export const MAX_DEV_NOTIFICATION_BYTES=60*1024;
|
|
3
|
+
const size=value=>Buffer.byteLength(JSON.stringify(value));
|
|
4
|
+
|
|
5
|
+
// Measure the JSON representation too: control characters and lone surrogates
|
|
6
|
+
// expand when escaped, independently of their UTF-8 source length.
|
|
7
|
+
function boundedString(value,limit){
|
|
8
|
+
if(size(value)<=limit)return value;
|
|
9
|
+
const suffix='…';
|
|
10
|
+
const prefix=end=>{
|
|
11
|
+
if(end>0&&end<value.length&&/[\uD800-\uDBFF]/.test(value[end-1])&&/[\uDC00-\uDFFF]/.test(value[end]))end--;
|
|
12
|
+
return value.slice(0,end);
|
|
13
|
+
};
|
|
14
|
+
let low=0,high=value.length;
|
|
15
|
+
while(low<high){const middle=Math.ceil((low+high)/2);if(size(prefix(middle)+suffix)<=limit)low=middle;else high=middle-1;}
|
|
16
|
+
return prefix(low)+suffix;
|
|
17
|
+
}
|
|
18
|
+
function boundedList(value,{count,itemBytes,totalBytes}){
|
|
19
|
+
const result=[];let bytes=2;
|
|
20
|
+
for(const entry of value.slice(0,count)){
|
|
21
|
+
if(typeof entry!=='string')continue;
|
|
22
|
+
const comma=result.length?1:0,remaining=totalBytes-bytes-comma;
|
|
23
|
+
if(remaining<5)break;
|
|
24
|
+
const text=boundedString(entry,Math.min(itemBytes,remaining));
|
|
25
|
+
result.push(text);bytes+=size(text)+comma;
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
const serialized=value=>{try{return JSON.stringify(value);}catch{return undefined;}};
|
|
30
|
+
|
|
31
|
+
/** Keep the wire event and state file identical, bounded, and conservative. */
|
|
32
|
+
export function prepareDevNotification(value){
|
|
33
|
+
let notification={...value};const truncated=new Set();
|
|
34
|
+
for(const [key,limits]of Object.entries({changed:{count:32,itemBytes:512,totalBytes:16*1024},typeErrors:{count:32,itemBytes:4096,totalBytes:8*1024}})){
|
|
35
|
+
if(notification[key]===undefined)continue;
|
|
36
|
+
const original=notification[key],next=Array.isArray(original)?boundedList(original,limits):[];
|
|
37
|
+
if(serialized(original)!==JSON.stringify(next))truncated.add(key);
|
|
38
|
+
notification[key]=next;
|
|
39
|
+
}
|
|
40
|
+
if(notification.error!==undefined){
|
|
41
|
+
const next=typeof notification.error==='string'?boundedString(notification.error,8*1024):'Development compilation failed.';
|
|
42
|
+
if(next!==notification.error)truncated.add('error');notification.error=next;
|
|
43
|
+
}
|
|
44
|
+
if(truncated.has('changed')&&Object.hasOwn(notification,'affectedRoutes')){delete notification.affectedRoutes;truncated.add('affectedRoutes');}
|
|
45
|
+
const stamp=()=>{
|
|
46
|
+
if(truncated.size){notification.notificationTruncated=true;notification.truncatedFields=[...truncated];}
|
|
47
|
+
};
|
|
48
|
+
const trimSelection=()=>{
|
|
49
|
+
for(const key of ['routes','compiler','transforms','affectedRoutes'])if(Object.hasOwn(notification,key)){delete notification[key];truncated.add(key);}
|
|
50
|
+
};
|
|
51
|
+
const fits=text=>typeof text==='string'&&Buffer.byteLength(text)<=MAX_DEV_NOTIFICATION_BYTES;
|
|
52
|
+
stamp();let text=serialized(notification);
|
|
53
|
+
if(fits(text))return {notification,text};
|
|
54
|
+
|
|
55
|
+
// Removing affectedRoutes asks every active page to refresh. Never send a
|
|
56
|
+
// shortened selection, which could silently leave an affected page stale.
|
|
57
|
+
trimSelection();stamp();text=serialized(notification);
|
|
58
|
+
if(fits(text))return {notification,text};
|
|
59
|
+
|
|
60
|
+
// Unexpected metadata or large identities must not defeat the final bound.
|
|
61
|
+
// Preserve complete usable IDs/URLs; an omitted client manifest causes the
|
|
62
|
+
// browser's existing conservative reload path instead of fetching a bad URL.
|
|
63
|
+
const previous=notification;notification={};truncated.add('metadata');
|
|
64
|
+
for(const [key,limit]of Object.entries({state:128,buildId:512,session:512,clientManifest:8192,pagesManifest:8192})){
|
|
65
|
+
if(typeof previous[key]==='string'&&size(previous[key])<=limit)notification[key]=previous[key];
|
|
66
|
+
else if(previous[key]!==undefined)truncated.add(key);
|
|
67
|
+
}
|
|
68
|
+
for(const key of ['sequence','listenerPid'])if(Number.isSafeInteger(previous[key])&&previous[key]>=0)notification[key]=previous[key];
|
|
69
|
+
for(const key of ['safeMode','cssOnly'])if(typeof previous[key]==='boolean')notification[key]=previous[key];
|
|
70
|
+
for(const key of ['changed','typeErrors','error'])if(previous[key]!==undefined)notification[key]=previous[key];
|
|
71
|
+
if(previous.reason&&typeof previous.reason==='object'){
|
|
72
|
+
notification.reason={};
|
|
73
|
+
for(const [key,limit]of Object.entries({code:256,summary:2048}))if(typeof previous.reason[key]==='string')notification.reason[key]=boundedString(previous.reason[key],limit);
|
|
74
|
+
if(serialized(previous.reason)!==JSON.stringify(notification.reason))truncated.add('reason');
|
|
75
|
+
}
|
|
76
|
+
stamp();text=JSON.stringify(notification);
|
|
77
|
+
// The allowlisted fields have a combined byte budget below 60 KiB. Keep this
|
|
78
|
+
// assertion next to serialization so future additions cannot break the RPC.
|
|
79
|
+
if(!fits(text))throw Error('Development notification exceeds its byte budget');
|
|
80
|
+
return {notification,text};
|
|
81
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Created synchronously from a user click; never opens a background popup.
|
|
2
|
+
export function createToolWindow({ owner, title, width, height, onClose, reservedWindow }) {
|
|
3
|
+
let popup=reservedWindow;
|
|
4
|
+
try { if(popup===undefined)popup = owner.open('about:blank', '_blank', `popup=yes,width=${width},height=${height},resizable=yes,scrollbars=yes`); }
|
|
5
|
+
catch { return null; }
|
|
6
|
+
if (!popup) return null;
|
|
7
|
+
let finished = false, timer;
|
|
8
|
+
function cleanup() {
|
|
9
|
+
if (finished) return;
|
|
10
|
+
finished = true; owner.clearInterval(timer);
|
|
11
|
+
owner.removeEventListener('pagehide', close);
|
|
12
|
+
onClose?.();
|
|
13
|
+
}
|
|
14
|
+
function close() { try { if (!popup.closed) popup.close(); } finally { cleanup(); } }
|
|
15
|
+
try {
|
|
16
|
+
// The UI uses the retained window reference; it needs no reverse opener.
|
|
17
|
+
popup.opener = null;
|
|
18
|
+
const document = popup.document;
|
|
19
|
+
document.title = title; document.documentElement.lang = 'en';
|
|
20
|
+
const viewport=document.createElement('meta');viewport.name='viewport';viewport.content='width=device-width, initial-scale=1';document.head.append(viewport);
|
|
21
|
+
document.body.style.cssText = 'margin:0;background:#080808;color-scheme:dark';
|
|
22
|
+
const host = document.createElement('prnext-dev-window'), shadow = host.attachShadow({mode:'open'});
|
|
23
|
+
const style = document.createElement('style');
|
|
24
|
+
style.textContent = `:host{display:block;color-scheme:dark}*,*::before,*::after{box-sizing:border-box}[hidden]{display:none!important}svg{width:16px;height:16px;flex:none}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}`;
|
|
25
|
+
shadow.append(style); document.body.replaceChildren(host);
|
|
26
|
+
popup.addEventListener('pagehide', cleanup, {once:true});
|
|
27
|
+
owner.addEventListener('pagehide', close, {once:true});
|
|
28
|
+
timer = owner.setInterval(() => { if (popup.closed) cleanup(); }, 500);
|
|
29
|
+
return { window:popup, shadow, close, focus() { try { popup.focus(); } catch {} } };
|
|
30
|
+
} catch { close(); return null; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const popoutIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 3h7v7m0-7L11 13"/><path d="M10 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5"/></svg>';
|
|
34
|
+
export const dockIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 14H3V7m0 7L13 4"/><path d="M3 18v1a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2h-1"/></svg>';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Pointer capture stays on the logo: dragging never installs page-wide styles.
|
|
2
|
+
// A tab-local preference cannot override a newly configured default corner.
|
|
3
|
+
const corners = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
|
4
|
+
const preference = 'prnext:dev-corner:v1';
|
|
5
|
+
|
|
6
|
+
export function createDevPosition({ host, trigger, position, onDrag, announce }) {
|
|
7
|
+
const view = host.ownerDocument.defaultView;
|
|
8
|
+
const controller = new AbortController(), events = { signal: controller.signal };
|
|
9
|
+
let current = corners.includes(position) ? position : 'bottom-left';
|
|
10
|
+
const configured = current;
|
|
11
|
+
try {
|
|
12
|
+
const saved = JSON.parse(view.sessionStorage.getItem(preference));
|
|
13
|
+
if (saved?.configured === configured && corners.includes(saved.corner)) current = saved.corner;
|
|
14
|
+
} catch { /* Storage can be disabled by the browser. */ }
|
|
15
|
+
host.dataset.position = current;
|
|
16
|
+
trigger.style.touchAction = 'none';
|
|
17
|
+
trigger.setAttribute('aria-description', 'Drag to a corner. Use Alt + arrow keys to move the logo.');
|
|
18
|
+
trigger.setAttribute('aria-keyshortcuts', 'Alt+ArrowUp Alt+ArrowDown Alt+ArrowLeft Alt+ArrowRight');
|
|
19
|
+
const hints = view.document.createElement('div'); hints.className = 'corner-hints'; hints.hidden = true; hints.setAttribute('aria-hidden', 'true');
|
|
20
|
+
for (const corner of corners) {
|
|
21
|
+
const node = view.document.createElement('span'); node.className = 'corner-hint'; node.dataset.corner = corner; hints.append(node);
|
|
22
|
+
}
|
|
23
|
+
host.shadowRoot.append(hints);
|
|
24
|
+
let drag, motion, suppressClick = false;
|
|
25
|
+
function viewport() {
|
|
26
|
+
const port = view.visualViewport;
|
|
27
|
+
return { x: port?.offsetLeft || 0, y: port?.offsetTop || 0, width: port?.width || view.innerWidth, height: port?.height || view.innerHeight };
|
|
28
|
+
}
|
|
29
|
+
function nearest(x, y) {
|
|
30
|
+
const port = viewport();
|
|
31
|
+
return `${y < port.y + port.height / 2 ? 'top' : 'bottom'}-${x < port.x + port.width / 2 ? 'left' : 'right'}`;
|
|
32
|
+
}
|
|
33
|
+
function highlight(corner) {
|
|
34
|
+
for (const node of hints.children) node.dataset.active = String(node.dataset.corner === corner);
|
|
35
|
+
}
|
|
36
|
+
function clearCoordinates() {
|
|
37
|
+
for (const name of ['top', 'right', 'bottom', 'left']) host.style.removeProperty(name);
|
|
38
|
+
}
|
|
39
|
+
function settle(corner, from, save = true) {
|
|
40
|
+
motion?.cancel(); motion = null;
|
|
41
|
+
current = corner; host.dataset.position = corner; clearCoordinates();
|
|
42
|
+
const target = host.getBoundingClientRect();
|
|
43
|
+
if (from && !view.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
44
|
+
motion = host.animate([
|
|
45
|
+
{ transform: `translate(${from.x - target.x}px,${from.y - target.y}px)` },
|
|
46
|
+
{ transform: 'translate(0,0)' },
|
|
47
|
+
], { duration: 420, easing: 'cubic-bezier(.16,1,.3,1)' });
|
|
48
|
+
}
|
|
49
|
+
if (save) {
|
|
50
|
+
try { view.sessionStorage.setItem(preference, JSON.stringify({ configured, corner })); } catch {}
|
|
51
|
+
announce(`Logo moved to ${corner.replace('-', ' ')}.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function finish(cancel = false) {
|
|
55
|
+
if (!drag) return;
|
|
56
|
+
const state = drag; drag = null;
|
|
57
|
+
if (state.moved) {
|
|
58
|
+
suppressClick = true;
|
|
59
|
+
const from = host.getBoundingClientRect();
|
|
60
|
+
hints.hidden = true; delete host.dataset.dragging;
|
|
61
|
+
settle(cancel ? state.corner : nearest(from.x + 22, from.y + 22), from, !cancel);
|
|
62
|
+
}
|
|
63
|
+
if (trigger.hasPointerCapture(state.pointer)) trigger.releasePointerCapture(state.pointer);
|
|
64
|
+
}
|
|
65
|
+
trigger.addEventListener('pointerdown', event => {
|
|
66
|
+
if (drag || event.button !== 0 || !event.isPrimary) return;
|
|
67
|
+
suppressClick = false;
|
|
68
|
+
const rect = host.getBoundingClientRect();
|
|
69
|
+
drag = { pointer: event.pointerId, x: event.clientX, y: event.clientY, rect, corner: current, moved: false };
|
|
70
|
+
trigger.setPointerCapture(event.pointerId);
|
|
71
|
+
}, events);
|
|
72
|
+
trigger.addEventListener('pointermove', event => {
|
|
73
|
+
if (!drag || event.pointerId !== drag.pointer) return;
|
|
74
|
+
const dx = event.clientX - drag.x, dy = event.clientY - drag.y;
|
|
75
|
+
if (!drag.moved && Math.hypot(dx, dy) < 6) return;
|
|
76
|
+
if (!drag.moved) {
|
|
77
|
+
drag.moved = true; motion?.cancel(); motion = null; onDrag();
|
|
78
|
+
host.dataset.dragging = 'true'; hints.hidden = false;
|
|
79
|
+
}
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
const port = viewport();
|
|
82
|
+
const x = Math.max(port.x + 4, Math.min(port.x + port.width - 48, drag.rect.x + dx));
|
|
83
|
+
const y = Math.max(port.y + 4, Math.min(port.y + port.height - 48, drag.rect.y + dy));
|
|
84
|
+
host.style.left = x + 'px'; host.style.top = y + 'px'; host.style.right = 'auto'; host.style.bottom = 'auto';
|
|
85
|
+
highlight(nearest(x + 22, y + 22));
|
|
86
|
+
}, events);
|
|
87
|
+
trigger.addEventListener('pointerup', event => { if(event.pointerId === drag?.pointer) finish(); }, events);
|
|
88
|
+
trigger.addEventListener('pointercancel', event => { if(event.pointerId === drag?.pointer) finish(true); }, events);
|
|
89
|
+
trigger.addEventListener('lostpointercapture', event => { if(event.pointerId === drag?.pointer) finish(true); }, events);
|
|
90
|
+
trigger.addEventListener('dragstart', event => event.preventDefault(), events);
|
|
91
|
+
trigger.addEventListener('click', event => {
|
|
92
|
+
if (suppressClick && event.detail !== 0) { suppressClick = false; event.preventDefault(); event.stopImmediatePropagation(); }
|
|
93
|
+
}, { ...events, capture: true });
|
|
94
|
+
trigger.addEventListener('keydown', event => {
|
|
95
|
+
if (event.key === 'Escape' && drag?.moved) { event.preventDefault(); event.stopPropagation(); finish(true); }
|
|
96
|
+
if (!event.altKey || !['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) return;
|
|
97
|
+
event.preventDefault(); event.stopPropagation();
|
|
98
|
+
const rect = host.getBoundingClientRect(); onDrag();
|
|
99
|
+
const [vertical, horizontal] = current.split('-');
|
|
100
|
+
settle(`${event.key === 'ArrowUp' ? 'top' : event.key === 'ArrowDown' ? 'bottom' : vertical}-${event.key === 'ArrowLeft' ? 'left' : event.key === 'ArrowRight' ? 'right' : horizontal}`, rect);
|
|
101
|
+
}, events);
|
|
102
|
+
function resize() { if (drag) finish(true); motion?.cancel(); motion = null; }
|
|
103
|
+
view.addEventListener('resize', resize, events);
|
|
104
|
+
view.addEventListener('blur', () => finish(true), events);
|
|
105
|
+
view.visualViewport?.addEventListener('resize', resize, events);
|
|
106
|
+
return { dispose() { controller.abort(); motion?.cancel(); hints.remove(); } };
|
|
107
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Mouse panning for same-origin mobile previews. Real touch/pen events retain
|
|
2
|
+
// their native behavior; inputs and application-owned drag surfaces stay native.
|
|
3
|
+
export function createPreviewTouch(document) {
|
|
4
|
+
const view = document.defaultView, root = document.documentElement;
|
|
5
|
+
if (!view?.PointerEvent || !root) return { cancel() {}, dispose() {} };
|
|
6
|
+
const controller = new view.AbortController(), capture = { capture: true, signal: controller.signal };
|
|
7
|
+
const reduced = view.matchMedia('(prefers-reduced-motion: reduce)');
|
|
8
|
+
let gesture, momentum = 0, blockedClick, sendingCancel = false;
|
|
9
|
+
|
|
10
|
+
function stopMomentum() { if (momentum) { try { view.cancelAnimationFrame(momentum); } catch {} } momentum = 0; }
|
|
11
|
+
function release(state) {
|
|
12
|
+
try { if (root.hasPointerCapture(state.pointer)) root.releasePointerCapture(state.pointer); } catch {}
|
|
13
|
+
}
|
|
14
|
+
function cancel() {
|
|
15
|
+
stopMomentum(); const state = gesture; gesture = undefined; blockedClick = undefined;
|
|
16
|
+
if (state) release(state);
|
|
17
|
+
}
|
|
18
|
+
function scrollPlan(event) {
|
|
19
|
+
const elements = event.composedPath().filter(node => node?.nodeType === 1);
|
|
20
|
+
if (elements.some(node => node.matches('input,textarea,select,option,video,audio,canvas,[role="slider"],[role="spinbutton"],[draggable="true"]') || node.isContentEditable)) return;
|
|
21
|
+
const chain = []; let x = true, y = true;
|
|
22
|
+
for (const node of elements) {
|
|
23
|
+
const style = view.getComputedStyle(node), action = style.touchAction;
|
|
24
|
+
if (action === 'none') return;
|
|
25
|
+
if (/pan-/.test(action)) {
|
|
26
|
+
x &&= /pan-(x|left|right)/.test(action); y &&= /pan-(y|up|down)/.test(action);
|
|
27
|
+
}
|
|
28
|
+
const viewport = node === document.scrollingElement;
|
|
29
|
+
// With visible root overflow, body overflow controls the viewport.
|
|
30
|
+
const overflowX = viewport && style.overflowX === 'visible' ? view.getComputedStyle(document.body || node).overflowX : style.overflowX;
|
|
31
|
+
const overflowY = viewport && style.overflowY === 'visible' ? view.getComputedStyle(document.body || node).overflowY : style.overflowY;
|
|
32
|
+
const canX = (viewport ? !/hidden|clip/.test(overflowX) : /auto|scroll|overlay/.test(overflowX)) && node.scrollWidth > node.clientWidth + 1;
|
|
33
|
+
const canY = (viewport ? !/hidden|clip/.test(overflowY) : /auto|scroll|overlay/.test(overflowY)) && node.scrollHeight > node.clientHeight + 1;
|
|
34
|
+
if (canX || canY) chain.push({ node, x: canX, y: canY, overscrollX: style.overscrollBehaviorX, overscrollY: style.overscrollBehaviorY });
|
|
35
|
+
}
|
|
36
|
+
if (chain.length && (x || y)) return { chain, x, y };
|
|
37
|
+
}
|
|
38
|
+
function pan(state, distance) {
|
|
39
|
+
let remaining = distance, consumed = 0;
|
|
40
|
+
const horizontal = state.axis === 'x', property = horizontal ? 'scrollLeft' : 'scrollTop';
|
|
41
|
+
for (const entry of state.chain) {
|
|
42
|
+
if (!entry[state.axis]) continue;
|
|
43
|
+
const before = entry.node[property];
|
|
44
|
+
entry.node.scrollBy({ [horizontal ? 'left' : 'top']: remaining, behavior: 'instant' });
|
|
45
|
+
const moved = entry.node[property] - before; consumed += moved; remaining -= moved;
|
|
46
|
+
if (Math.abs(remaining) < .5 || /contain|none/.test(entry[horizontal ? 'overscrollX' : 'overscrollY'])) break;
|
|
47
|
+
}
|
|
48
|
+
return consumed;
|
|
49
|
+
}
|
|
50
|
+
function coast(state) {
|
|
51
|
+
if (reduced.matches || Math.abs(state.velocity) < .08 || view.performance.now() - state.time > 80) return;
|
|
52
|
+
const started = view.performance.now(); let previous = started, velocity = state.velocity;
|
|
53
|
+
const tick = now => {
|
|
54
|
+
momentum = 0;
|
|
55
|
+
const elapsed = Math.min(32, now - previous); previous = now;
|
|
56
|
+
if (now - started > 700 || Math.abs(velocity) < .02) return;
|
|
57
|
+
const moved = pan(state, velocity * elapsed);
|
|
58
|
+
velocity *= Math.exp(-elapsed / 160);
|
|
59
|
+
if (Math.abs(moved) > .1) momentum = view.requestAnimationFrame(tick);
|
|
60
|
+
};
|
|
61
|
+
momentum = view.requestAnimationFrame(tick);
|
|
62
|
+
}
|
|
63
|
+
function begin(event) {
|
|
64
|
+
if (event.pointerType !== 'mouse') { cancel(); return; }
|
|
65
|
+
if (event.pointerType !== 'mouse' || !event.isPrimary || event.button !== 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.defaultPrevented) return;
|
|
66
|
+
cancel(); const plan = scrollPlan(event); if (!plan) return;
|
|
67
|
+
gesture = { ...plan, pointer: event.pointerId, target: event.target, startX: event.clientX, startY: event.clientY,
|
|
68
|
+
lastX: event.clientX, lastY: event.clientY, time: view.performance.now(), velocity: 0, axis: undefined };
|
|
69
|
+
}
|
|
70
|
+
function move(event) {
|
|
71
|
+
const state = gesture; if (!state || event.pointerId !== state.pointer) return;
|
|
72
|
+
const now = view.performance.now();
|
|
73
|
+
if (!state.axis) {
|
|
74
|
+
const dx = event.clientX - state.startX, dy = event.clientY - state.startY;
|
|
75
|
+
if (Math.hypot(dx, dy) < 6) return;
|
|
76
|
+
const axis = Math.abs(dx) > Math.abs(dy) ? 'x' : 'y';
|
|
77
|
+
if (!state[axis] || !state.chain.some(entry => entry[axis])) { gesture = undefined; return; }
|
|
78
|
+
state.axis = axis;
|
|
79
|
+
// Like native panning, release the control that received pointerdown so
|
|
80
|
+
// pressed controls and custom pointer capture do not remain stuck.
|
|
81
|
+
sendingCancel = true;
|
|
82
|
+
try { state.target.dispatchEvent(new view.PointerEvent('pointercancel', { bubbles: true, composed: true, pointerId: state.pointer, pointerType: 'mouse', isPrimary: true })); }
|
|
83
|
+
finally { sendingCancel = false; }
|
|
84
|
+
if (gesture !== state) return;
|
|
85
|
+
try { root.setPointerCapture(state.pointer); } catch {}
|
|
86
|
+
}
|
|
87
|
+
event.preventDefault(); event.stopImmediatePropagation();
|
|
88
|
+
const delta = state.axis === 'x' ? state.lastX - event.clientX : state.lastY - event.clientY;
|
|
89
|
+
const moved = pan(state, delta), elapsed = Math.max(8, now - state.time);
|
|
90
|
+
const speed = Math.max(-3, Math.min(3, moved / elapsed));
|
|
91
|
+
state.velocity = speed * .7 + state.velocity * .3;
|
|
92
|
+
state.lastX = event.clientX; state.lastY = event.clientY; state.time = now;
|
|
93
|
+
}
|
|
94
|
+
function end(event) {
|
|
95
|
+
const state = gesture; if (!state || event.pointerId !== state.pointer) return;
|
|
96
|
+
gesture = undefined;
|
|
97
|
+
if (state.axis) {
|
|
98
|
+
event.preventDefault(); event.stopImmediatePropagation();
|
|
99
|
+
blockedClick = { pointer: state.pointer, time: view.performance.now() }; coast(state);
|
|
100
|
+
}
|
|
101
|
+
release(state);
|
|
102
|
+
}
|
|
103
|
+
function preventActivation(event) {
|
|
104
|
+
if (!blockedClick || event.detail === 0 || view.performance.now() - blockedClick.time > 500) return;
|
|
105
|
+
if (event.pointerId !== undefined && event.pointerId !== blockedClick.pointer) return;
|
|
106
|
+
event.preventDefault(); event.stopImmediatePropagation();
|
|
107
|
+
if (event.type === 'click') blockedClick = undefined;
|
|
108
|
+
}
|
|
109
|
+
document.addEventListener('pointerdown', begin, capture);
|
|
110
|
+
document.addEventListener('pointermove', move, { ...capture, passive: false });
|
|
111
|
+
document.addEventListener('pointerup', end, capture);
|
|
112
|
+
document.addEventListener('pointercancel', event => { if (!sendingCancel && event.pointerId === gesture?.pointer) cancel(); }, capture);
|
|
113
|
+
document.addEventListener('lostpointercapture', event => { if (event.target === root && event.pointerId === gesture?.pointer) cancel(); }, capture);
|
|
114
|
+
document.addEventListener('click', preventActivation, capture);
|
|
115
|
+
document.addEventListener('mouseup', preventActivation, capture);
|
|
116
|
+
for (const name of ['selectstart', 'dragstart']) document.addEventListener(name, event => { if (gesture) event.preventDefault(); }, capture);
|
|
117
|
+
document.addEventListener('wheel', cancel, { ...capture, passive: true });
|
|
118
|
+
document.addEventListener('keydown', cancel, capture);
|
|
119
|
+
for (const name of ['blur', 'pagehide', 'resize']) view.addEventListener(name, cancel, { signal: controller.signal });
|
|
120
|
+
reduced.addEventListener('change', cancel, { signal: controller.signal });
|
|
121
|
+
return { cancel, dispose() { cancel(); controller.abort(); } };
|
|
122
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {spawn} from 'node:child_process';
|
|
2
|
+
import {randomBytes} from 'node:crypto';
|
|
3
|
+
import {writeFile,rm} from 'node:fs/promises';
|
|
4
|
+
import {fileURLToPath} from 'node:url';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {EventEmitter} from 'node:events';
|
|
7
|
+
|
|
8
|
+
export function createDevRuntimePool({binary,root,home,environment={},onChild=()=>{}}){
|
|
9
|
+
let spare,disposed=false;const children=new Set();
|
|
10
|
+
function create(){
|
|
11
|
+
if(disposed)throw Error('Development runtime is closed');
|
|
12
|
+
const token=randomBytes(32).toString('base64url');
|
|
13
|
+
const child=spawn(binary,['dev-spare',root,'--node',process.execPath,'--script',fileURLToPath(new URL('./dev-runtime-spare.mjs',import.meta.url))],{cwd:root,windowsHide:true,stdio:['pipe','pipe','pipe'],env:{...process.env,...environment,NODE_ENV:'development',PRNEXT_DEV_SPARE_TOKEN:token}});
|
|
14
|
+
children.add(child);onChild(child);
|
|
15
|
+
const messages=new EventEmitter();let buffer='';child.stdout.setEncoding('utf8');child.stdout.on('data',chunk=>{buffer+=chunk;if(buffer.length>65536){child.kill();return;}for(let end;(end=buffer.indexOf('\n'))>=0;){const line=buffer.slice(0,end);buffer=buffer.slice(end+1);try{const message=JSON.parse(line);if(message.type)messages.emit(message.type,message);}catch{}}});
|
|
16
|
+
child.once('exit',()=>{children.delete(child);if(spare?.child===child)spare=undefined;});
|
|
17
|
+
child.stdin.on('error',()=>{});
|
|
18
|
+
const wait=type=>new Promise((resolve,reject)=>{
|
|
19
|
+
const timeout=setTimeout(()=>fail(Error('Development runtime warmup timed out')),15000);
|
|
20
|
+
const clean=()=>{clearTimeout(timeout);messages.off(type,message);child.off('exit',exit);child.off('error',fail);};
|
|
21
|
+
const fail=error=>{clean();reject(error);};const exit=()=>fail(Error('Development runtime exited'));
|
|
22
|
+
const message=value=>{clean();resolve(value);};
|
|
23
|
+
messages.on(type,message);child.once('exit',exit);child.once('error',fail);
|
|
24
|
+
});
|
|
25
|
+
const ready=wait('warm');ready.catch(()=>{if(spare?.child===child)spare=undefined;void stop(child);});
|
|
26
|
+
return {child,token,ready,wait};
|
|
27
|
+
}
|
|
28
|
+
async function stop(child){
|
|
29
|
+
if(!child?.pid||child.exitCode!==null||child.signalCode)return;
|
|
30
|
+
await new Promise(resolve=>{const timer=setTimeout(()=>child.kill('SIGKILL'),30000);child.once('exit',()=>{clearTimeout(timer);resolve();});child.stdin.end();if(child.connected)child.disconnect();});
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
warm(){if(!spare)spare=create();return spare.ready;},
|
|
34
|
+
async take(directory){
|
|
35
|
+
const selected=spare||create();spare=undefined;
|
|
36
|
+
try{
|
|
37
|
+
const {port,guardPort}=await selected.ready,dist=path.join(root,directory),armed=selected.wait('armed');selected.child.stdin.write(JSON.stringify({type:'arm',dist})+'\n');await armed;
|
|
38
|
+
const descriptor=path.join(home,'transport-'+path.basename(directory)+'.json');
|
|
39
|
+
await writeFile(descriptor,JSON.stringify({project:root,dist,port,guardPort,token:selected.token}),{mode:0o600,flag:'wx'});
|
|
40
|
+
spare=create();return {child:selected.child,descriptor,pid:selected.child.pid,close:async()=>{await stop(selected.child);await rm(descriptor,{force:true});}};
|
|
41
|
+
}catch(error){await stop(selected.child);throw error;}
|
|
42
|
+
},
|
|
43
|
+
async close(){disposed=true;await Promise.all([...children].map(stop));},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// One prewarmed process becomes exactly one immutable development generation.
|
|
2
|
+
// It is owned by the CLI, never shared across builds or production projects.
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import {timingSafeEqual} from 'node:crypto';
|
|
5
|
+
import {realpath,readFile} from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import {pathToFileURL} from 'node:url';
|
|
8
|
+
import {createRequire} from 'node:module';
|
|
9
|
+
const root=await realpath(process.cwd()),token=process.env.PRNEXT_DEV_SPARE_TOKEN;
|
|
10
|
+
delete process.env.PRNEXT_DEV_SPARE_TOKEN;
|
|
11
|
+
if(!token)throw Error('Private development parent required');
|
|
12
|
+
const announce=value=>process.stdout.write(JSON.stringify(value)+'\n');
|
|
13
|
+
const spareHeartbeat=setInterval(()=>process.stdout.write('alive\n'),1000);spareHeartbeat.unref();
|
|
14
|
+
// Warm framework dependencies only; no application module or data function runs.
|
|
15
|
+
const require=createRequire(path.join(root,'package.json'));
|
|
16
|
+
try{require('react');require('react-dom/server.node');}catch{/* Actual bootstrap reports missing peers. */}
|
|
17
|
+
let armed,starting,ready;
|
|
18
|
+
const controls=new Map();
|
|
19
|
+
globalThis.__PRNEXT_DEV_WORKER_PULSE__=line=>{for(const[socket,watch]of controls)if(watch&&!socket.destroyed)socket.write(line);};
|
|
20
|
+
const broker=net.createServer(socket=>{
|
|
21
|
+
socket.setNoDelay(true);socket.setTimeout(10000,()=>socket.destroy());let source='';
|
|
22
|
+
socket.on('data',chunk=>{
|
|
23
|
+
source+=chunk;if(Buffer.byteLength(source)>8192){socket.destroy();return;}
|
|
24
|
+
if(!source.includes('\n'))return;socket.removeAllListeners('data');
|
|
25
|
+
void(async()=>{
|
|
26
|
+
const input=JSON.parse(source.trim()),supplied=Buffer.from(input.token||''),expected=Buffer.from(token);
|
|
27
|
+
if(supplied.length!==expected.length||!timingSafeEqual(supplied,expected)||!armed||input.project!==root||input.dist!==armed.dist)throw Error('Invalid generation capability');
|
|
28
|
+
if(!starting)starting=(async()=>{
|
|
29
|
+
const manifest=JSON.parse(await readFile(path.join(armed.dist,'manifest.json'),'utf8'));
|
|
30
|
+
if(manifest.dev!==true)throw Error('Development build required');
|
|
31
|
+
for(const key of ['PRNEXT_CACHE_URL','PRNEXT_CACHE_TOKEN'])delete process.env[key];
|
|
32
|
+
if(input.cache){process.env.PRNEXT_CACHE_URL=input.cache.url;process.env.PRNEXT_CACHE_TOKEN=input.cache.token;}
|
|
33
|
+
process.env.PRNEXT_WORKER_SOCKET=String(Math.max(1,Math.min(512,input.lanes||1)));
|
|
34
|
+
process.env.PRNEXT_WORKER_TOKEN=token;process.env.PRNEXT_WORKER_ACTIVITY='1';
|
|
35
|
+
clearInterval(spareHeartbeat);
|
|
36
|
+
process.argv=[process.execPath,path.join(armed.dist,'runtime/worker.mjs'),root,armed.dist];
|
|
37
|
+
const announced=new Promise(resolve=>{globalThis.__PRNEXT_DEV_WORKER_READY__=resolve;});
|
|
38
|
+
await import(pathToFileURL(process.argv[1]).href);ready=await announced;
|
|
39
|
+
return ready;
|
|
40
|
+
})().catch(()=>process.exit(1));
|
|
41
|
+
const info=await starting;if(!socket.destroyed){
|
|
42
|
+
socket.setTimeout(0);controls.set(socket,input.watch===true);socket.write(JSON.stringify(info)+'\n');
|
|
43
|
+
let command='';socket.on('data',chunk=>{command+=chunk;if(command.length>16){socket.destroy();return;}if(command==='retire\n')process.stdin.emit('end');});
|
|
44
|
+
}
|
|
45
|
+
})().catch(()=>socket.destroy());
|
|
46
|
+
});
|
|
47
|
+
socket.on('error',()=>{});
|
|
48
|
+
socket.on('close',()=>{const owned=controls.delete(socket);if(owned&&!controls.size&&ready)process.stdin.emit('end');});
|
|
49
|
+
});
|
|
50
|
+
broker.maxConnections=16;
|
|
51
|
+
broker.listen(0,'127.0.0.1',()=>{broker.unref();announce({type:'warm',port:broker.address().port});});
|
|
52
|
+
let control='';const arm=chunk=>{control+=chunk;if(control.length>8192)process.exit(1);if(!control.includes('\n'))return;process.stdin.off('data',arm);
|
|
53
|
+
let message;try{message=JSON.parse(control.trim());}catch{process.exit(1);}if(message?.type!=='arm'||armed)process.exit(1);
|
|
54
|
+
void realpath(message.dist).then(dist=>{if(!dist.startsWith(root+path.sep))throw Error('Generation outside project');armed={dist};announce({type:'armed'});}).catch(()=>process.exit(1));
|
|
55
|
+
};process.stdin.on('data',arm);
|
|
56
|
+
// Parent lifetime is guarded even while this is still an unused spare.
|
|
57
|
+
process.stdin.resume();process.stdin.once('end',()=>{broker.close();if(!starting)process.exit();});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { safeRequestPath, normalizeEntries } from './dev-tools-model.mjs';
|
|
2
|
+
|
|
3
|
+
// The development-only sidecar is bound to loopback and authenticated with an
|
|
4
|
+
// ephemeral session capability. Diagnostics stay local by default; the explicit
|
|
5
|
+
// Host AI relay shares tool responses only when the connected assistant asks.
|
|
6
|
+
export function createDevToolsClient({ capture, snapshot = () => ({}), config, target = globalThis } = {}) {
|
|
7
|
+
const nativeFetch = target.fetch?.bind(target), subscribers = new Set();
|
|
8
|
+
const clientId = target.crypto?.randomUUID?.() || 'browser-' + Math.random().toString(36).slice(2);
|
|
9
|
+
const valid = config?.version === 1 && typeof config.token === 'string' && /^http:\/\/(?:127\.0\.0\.1|localhost|\[::1\]):\d+$/.test(config.endpoint || '');
|
|
10
|
+
let disposed = false, data = {}, connection = valid ? 'connecting' : 'unavailable', polling = 0, pollTimer, pollingRequest;
|
|
11
|
+
let requests = [], serial = 0, logSerial = 0, reportTimer, lastSummary, lastContext, lastError;
|
|
12
|
+
let browserInFlight = 0; const browserQueue = [];
|
|
13
|
+
const pending = new Map(), originals = [], localEvents = new AbortController();
|
|
14
|
+
const base = () => target.location?.href || 'http://localhost/';
|
|
15
|
+
const state = () => ({ ...data, connection, available: valid, clientId, localRequests: requests.slice(), context: lastContext, error: lastError || data.error });
|
|
16
|
+
function notify() { for (const subscriber of subscribers) { try { subscriber(state()); } catch {} } }
|
|
17
|
+
async function request(path, body, timeout = 8000) {
|
|
18
|
+
if (!valid || disposed || !nativeFetch) throw Error('The local development tools are unavailable. Restart PRNext dev.');
|
|
19
|
+
const controller = new AbortController(); const timer = target.setTimeout(() => controller.abort(), timeout); pending.set(controller,timer);
|
|
20
|
+
try {
|
|
21
|
+
const response = await nativeFetch(config.endpoint + path, { method: 'POST', mode: 'cors', cache: 'no-store', credentials: 'omit',
|
|
22
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + config.token }, body: JSON.stringify(body), signal: controller.signal });
|
|
23
|
+
const result = await response.json(); if (!response.ok) {const error=Error(result?.error?.message || result?.error || 'Local tools request failed (' + response.status + ').');error.status=response.status;error.code=result?.code;throw error;}
|
|
24
|
+
connection = 'connected'; lastError = undefined; return result;
|
|
25
|
+
} catch (error) { if (!disposed) { connection = error.status&&![401,403].includes(error.status)?'connected':'disconnected'; lastError = error.name === 'AbortError' ? 'Local tools request timed out.' : error.message; } throw error; }
|
|
26
|
+
finally { target.clearTimeout(timer); pending.delete(controller); }
|
|
27
|
+
}
|
|
28
|
+
async function call(name, args = {}) {
|
|
29
|
+
try { return await request('/v1/tools/' + encodeURIComponent(name), args, name === 'dev_verify' ? 35000 : 10000); }
|
|
30
|
+
finally { notify(); }
|
|
31
|
+
}
|
|
32
|
+
async function refresh() {
|
|
33
|
+
if (!valid || disposed) return state();
|
|
34
|
+
if (pollingRequest) return pollingRequest;
|
|
35
|
+
pollingRequest = call('dev_snapshot').then(result => { data = result; notify(); return state(); }).catch(() => state()).finally(() => { pollingRequest = undefined; });
|
|
36
|
+
return pollingRequest;
|
|
37
|
+
}
|
|
38
|
+
function browser(kind, extras = {}) {
|
|
39
|
+
if (!valid || disposed) return Promise.resolve();
|
|
40
|
+
const info = snapshot();
|
|
41
|
+
const body = { session: config.session, clientId, buildId: info.buildId ?? null, sequence: info.sequence || 0,
|
|
42
|
+
path: target.location?.pathname || '/', issues: info.issues || 0, typeErrors: info.typeErrors || 0, building: Boolean(info.building),
|
|
43
|
+
confirmed: info.confirmed === true, hydrated: info.confirmed === true, kind, ...extras };
|
|
44
|
+
return new Promise(resolve => {
|
|
45
|
+
if (browserQueue.length >= 100) browserQueue.shift().resolve();
|
|
46
|
+
browserQueue.push({ body, resolve }); pumpBrowser();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function pumpBrowser() {
|
|
50
|
+
while (!disposed && browserInFlight < 2 && browserQueue.length) {
|
|
51
|
+
const job = browserQueue.shift(); browserInFlight++;
|
|
52
|
+
void request('/v1/browser', job.body).catch(() => {}).finally(() => { browserInFlight--;job.resolve();pumpBrowser(); });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function report() {
|
|
56
|
+
if (disposed) return;
|
|
57
|
+
const entries = capture?.snapshot().entries || [];
|
|
58
|
+
const brief = text => text.length > 8000 ? text.slice(0, 7900) + '\n[Truncated for the local AI bridge; full message remains in the browser console.]' : text;
|
|
59
|
+
const unsent = entries.filter(entry => entry.id > logSerial);
|
|
60
|
+
const fresh = unsent.slice(-20).map(entry => ({ ...entry, text: brief(entry.text) }));
|
|
61
|
+
if (fresh.length) logSerial = fresh.at(-1).id;
|
|
62
|
+
const info = snapshot(), summary = [info.buildId, info.sequence, info.issues, info.typeErrors, info.building, info.confirmed].join(':');
|
|
63
|
+
const errors = entries.filter(entry => entry.level === 'error').slice(-10).map(entry => ({ id: entry.id, text: brief(entry.text), time: entry.time }));
|
|
64
|
+
void browser(summary !== lastSummary && !info.building && info.confirmed === true ? 'applied' : info.issues || errors.length ? 'errors' : 'heartbeat', {
|
|
65
|
+
logs: fresh, droppedLogs: Math.max(0, unsent.length - 20) + (capture?.snapshot().dropped || 0), errors, issues: info.issues || 0, typeErrors: info.typeErrors || 0, building: Boolean(info.building),
|
|
66
|
+
visibility: target.document?.visibilityState || 'visible', hydrated: info.confirmed === true, confirmed: info.confirmed === true,
|
|
67
|
+
}); lastSummary = summary;
|
|
68
|
+
}
|
|
69
|
+
function scheduleReport() { if (!reportTimer && valid) reportTimer = target.setTimeout(() => { reportTimer = undefined; report(); }, 300); }
|
|
70
|
+
function recordRequest(entry) {
|
|
71
|
+
if (disposed) return;
|
|
72
|
+
const record = { ...entry, id: ++serial, source: 'browser' };
|
|
73
|
+
requests.push(record); if (requests.length > 300) requests.shift();
|
|
74
|
+
void browser('request', { request: record }); notify();
|
|
75
|
+
}
|
|
76
|
+
function ignored(url) {
|
|
77
|
+
return valid && url.origin === config.endpoint || /\/_prnext\/(?:dev|ensure|assets)\b/.test(url.pathname);
|
|
78
|
+
}
|
|
79
|
+
if (nativeFetch) {
|
|
80
|
+
const original = target.fetch;
|
|
81
|
+
const replacement = function (...args) {
|
|
82
|
+
let url, method = 'GET';
|
|
83
|
+
try { url = new URL(typeof args[0] === 'string' || args[0] instanceof URL ? String(args[0]) : args[0].url, base()); method = String(args[1]?.method || args[0]?.method || 'GET').toUpperCase(); } catch {}
|
|
84
|
+
const started = target.performance?.now?.() ?? Date.now(), time = Date.now();
|
|
85
|
+
return Reflect.apply(original, target, args).then(response => {
|
|
86
|
+
if (url && !ignored(url)) recordRequest({ method, path: safeRequestPath(url.href, base()), status: response.status, time,
|
|
87
|
+
duration: Math.max(0, (target.performance?.now?.() ?? Date.now()) - started), kind: 'fetch', failed: !response.ok });
|
|
88
|
+
return response;
|
|
89
|
+
}, error => {
|
|
90
|
+
if (url && !ignored(url)) recordRequest({ method, path: safeRequestPath(url.href, base()), status: null, time,
|
|
91
|
+
duration: Math.max(0, (target.performance?.now?.() ?? Date.now()) - started), kind: 'fetch', failed: true, error: error.name === 'AbortError' ? 'Aborted' : 'Network error' });
|
|
92
|
+
throw error;
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
try { target.fetch = replacement; originals.push(() => { if (target.fetch === replacement) target.fetch = original; }); } catch {}
|
|
96
|
+
}
|
|
97
|
+
const xhr = target.XMLHttpRequest?.prototype;
|
|
98
|
+
if (xhr) {
|
|
99
|
+
const open = xhr.open, send = xhr.send, records = new WeakMap();
|
|
100
|
+
const wrappedOpen = function (method, raw, ...args) { const result = Reflect.apply(open, this, [method, raw, ...args]); records.delete(this);try { const url = new URL(raw, base()); if (!ignored(url)) records.set(this, { method: String(method).toUpperCase(), path: safeRequestPath(url.href, base()), kind: 'xhr' }); } catch {} return result; };
|
|
101
|
+
const wrappedSend = function (...args) {
|
|
102
|
+
const entry = records.get(this); if (entry) { const start = target.performance?.now?.() ?? Date.now(), time = Date.now();
|
|
103
|
+
this.addEventListener('loadend', () => { recordRequest({ ...entry, time, status: this.status || null, failed: this.status === 0 || this.status >= 400,
|
|
104
|
+
duration: Math.max(0, (target.performance?.now?.() ?? Date.now()) - start) }); }, { once: true }); }
|
|
105
|
+
return Reflect.apply(send, this, args);
|
|
106
|
+
};
|
|
107
|
+
try { xhr.open = wrappedOpen; xhr.send = wrappedSend; originals.push(() => { if (xhr.open === wrappedOpen) xhr.open = open; if (xhr.send === wrappedSend) xhr.send = send; }); } catch {}
|
|
108
|
+
}
|
|
109
|
+
const unsubscribe = capture?.subscribe(() => { scheduleReport(); notify(); });
|
|
110
|
+
target.addEventListener?.('prnext:refresh', scheduleReport, { signal: localEvents.signal });
|
|
111
|
+
target.addEventListener?.('prnext:devtools:ready', scheduleReport, { signal: localEvents.signal });
|
|
112
|
+
target.addEventListener?.('popstate', scheduleReport, { signal: localEvents.signal });
|
|
113
|
+
target.addEventListener?.('prnext:devtools:ai', event => {
|
|
114
|
+
const detail = event.detail || {};
|
|
115
|
+
lastContext = detail.kind === 'error' ? { ...detail, text: detail.error?.message || String(detail.error || ''),
|
|
116
|
+
source: detail.source?.file ? `${detail.source.file}:${detail.source.line}:${detail.source.column}` : detail.source,
|
|
117
|
+
mappedSource: detail.source?.file ? detail.source : undefined,
|
|
118
|
+
} : detail;
|
|
119
|
+
notify(); target.dispatchEvent(new CustomEvent('prnext:devtools:context', { detail: lastContext }));
|
|
120
|
+
}, { signal: localEvents.signal });
|
|
121
|
+
const heartbeat = valid ? target.setInterval(report, 5000) : undefined;
|
|
122
|
+
if (valid) scheduleReport();
|
|
123
|
+
function watch() {
|
|
124
|
+
polling++; void refresh();
|
|
125
|
+
if (!pollTimer) pollTimer = target.setInterval(() => void refresh(), 1500);
|
|
126
|
+
let watching=true;
|
|
127
|
+
return () => { if(!watching)return;watching=false;if (--polling <= 0) { polling = 0; target.clearInterval(pollTimer); pollTimer = undefined; } };
|
|
128
|
+
}
|
|
129
|
+
function captureFor(store = capture) {
|
|
130
|
+
let clearedAt = 0;
|
|
131
|
+
return {
|
|
132
|
+
snapshot() { const captured = store?.snapshot() || { entries: [], dropped: 0 }; return { entries: normalizeEntries({ ...data, serverLogs: (data.serverLogs || []).filter(entry => (entry.time || entry.timestamp || 0) > clearedAt) }, captured.entries), dropped: captured.dropped }; },
|
|
133
|
+
subscribe(callback) { subscribers.add(callback); const stop = watch(), stopCapture = store !== capture ? store?.subscribe(callback) : undefined; return () => { subscribers.delete(callback); stop(); stopCapture?.(); }; },
|
|
134
|
+
clear() { store?.clear(); clearedAt = Date.now(); notify(); },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const unified = captureFor();
|
|
138
|
+
return {
|
|
139
|
+
call, refresh, snapshot: state, subscribe(callback) { subscribers.add(callback); return () => subscribers.delete(callback); }, watch,
|
|
140
|
+
consoleCapture: unified, captureFor, localCapture: capture, browser, report, endpoint: config?.endpoint,
|
|
141
|
+
setContext(context) { lastContext = context; notify(); },
|
|
142
|
+
async sendContext(context, prompt) {
|
|
143
|
+
const submitted=lastContext;
|
|
144
|
+
const latest=await refresh();
|
|
145
|
+
if(latest.connection!=='connected'||latest.ai?.connected!==true)throw Error('Connect your assistant before sending a request.');
|
|
146
|
+
let result;
|
|
147
|
+
try{result=await call('dev_ai_context', { kind: context.kind || 'selection', context, prompt });}
|
|
148
|
+
catch(error){if(error.code==='assistant_required'){data={...data,ai:{...data.ai,connected:false}};notify();}throw error;}
|
|
149
|
+
if(result.queued!==true||result.connected!==true){if(result.connected!==true){data={...data,ai:{...data.ai,connected:false}};notify();}throw Error('Your assistant is no longer connected. Reconnect and try again.');}
|
|
150
|
+
if(lastContext===submitted)lastContext = { ...context, sent: true }; notify(); return result;
|
|
151
|
+
},
|
|
152
|
+
dispose() { disposed = true; unsubscribe?.(); localEvents.abort(); target.clearInterval(heartbeat); target.clearInterval(pollTimer); target.clearTimeout(reportTimer);
|
|
153
|
+
for (const job of browserQueue.splice(0)) job.resolve();for (const [controller,timer] of pending) { controller.abort();target.clearTimeout(timer); } pending.clear();for (const restore of originals.reverse()) restore(); subscribers.clear(); requests = []; },
|
|
154
|
+
};
|
|
155
|
+
}
|