@vcp-dev/site-edit-bridge 0.3.15 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auto-bootstrap.d.ts +12 -1
- package/dist/auto-bootstrap.js +1 -1
- package/dist/client.d.ts +74 -1
- package/dist/client.js +1 -1
- package/dist/index.js +3 -3
- package/dist/next-dev-server.d.ts +3 -0
- package/dist/next-dev-server.js +3 -3
- package/dist/protocol.d.ts +62 -3
- package/dist/server.d.ts +10 -0
- package/dist/server.js +2 -2
- package/dist/start-next-dev-server.js +1 -0
- package/dist/window-message.d.ts +40 -2
- package/package.json +3 -3
package/dist/protocol.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { SiteEditCommandSuccess, SiteEditEvent, SiteEditGetClassNameSourceCommandResult, SiteEditObjectEditContext, SiteEditObjectSummary, SiteEditOperationCommandResult, SiteEditOperationRequest, SiteEditRenderDocument } from "@vcp-dev/contracts";
|
|
1
|
+
import type { SiteEditCommandSuccess, SiteEditEvent, SiteEditGetClassNameSourceCommandResult, SiteEditObjectEditContext, SiteEditObjectSummary, SiteEditOperationCommandResult, SiteEditOperationRequest, SiteEditRenderDocument, SiteEditResolveSourceLocationInput, SiteEditSourceFileResponse, SiteEditSourceLocation, SiteEditSourceTreeResponse } from "@vcp-dev/contracts";
|
|
2
2
|
import { type CompactRenderDocument } from "./document-codec.js";
|
|
3
3
|
type Awaitable<T> = T | Promise<T>;
|
|
4
|
-
export type SiteEditBridgeRequestKind = "get-document" | "get-object-summary" | "get-object-edit-context" | "get-class-name-source" | "execute" | "undo" | "redo";
|
|
4
|
+
export type SiteEditBridgeRequestKind = "get-document" | "get-object-summary" | "get-object-edit-context" | "get-class-name-source" | "get-source-tree" | "get-source-file" | "resolve-source-location" | "execute" | "undo" | "redo";
|
|
5
5
|
export type SiteEditBridgeRequest = {
|
|
6
6
|
type: "request";
|
|
7
7
|
payload: {
|
|
@@ -17,6 +17,27 @@ export type SiteEditBridgeRequest = {
|
|
|
17
17
|
key: string;
|
|
18
18
|
routeId?: string;
|
|
19
19
|
};
|
|
20
|
+
} | {
|
|
21
|
+
type: "request";
|
|
22
|
+
payload: {
|
|
23
|
+
id: string;
|
|
24
|
+
kind: "get-source-tree";
|
|
25
|
+
};
|
|
26
|
+
} | {
|
|
27
|
+
type: "request";
|
|
28
|
+
payload: {
|
|
29
|
+
id: string;
|
|
30
|
+
kind: "get-source-file";
|
|
31
|
+
path: string;
|
|
32
|
+
};
|
|
33
|
+
} | {
|
|
34
|
+
type: "request";
|
|
35
|
+
payload: {
|
|
36
|
+
id: string;
|
|
37
|
+
kind: "resolve-source-location";
|
|
38
|
+
key: string;
|
|
39
|
+
routeId?: string;
|
|
40
|
+
};
|
|
20
41
|
} | {
|
|
21
42
|
type: "request";
|
|
22
43
|
payload: {
|
|
@@ -54,6 +75,27 @@ export type SiteEditBridgeResponse = {
|
|
|
54
75
|
kind: "get-document";
|
|
55
76
|
result: SiteEditCommandSuccess<SiteEditRenderDocument> | SiteEditCommandSuccess<CompactRenderDocument>;
|
|
56
77
|
};
|
|
78
|
+
} | {
|
|
79
|
+
type: "response";
|
|
80
|
+
payload: {
|
|
81
|
+
id: string;
|
|
82
|
+
kind: "get-source-tree";
|
|
83
|
+
result: SiteEditCommandSuccess<SiteEditSourceTreeResponse>;
|
|
84
|
+
};
|
|
85
|
+
} | {
|
|
86
|
+
type: "response";
|
|
87
|
+
payload: {
|
|
88
|
+
id: string;
|
|
89
|
+
kind: "get-source-file";
|
|
90
|
+
result: SiteEditCommandSuccess<SiteEditSourceFileResponse>;
|
|
91
|
+
};
|
|
92
|
+
} | {
|
|
93
|
+
type: "response";
|
|
94
|
+
payload: {
|
|
95
|
+
id: string;
|
|
96
|
+
kind: "resolve-source-location";
|
|
97
|
+
result: SiteEditCommandSuccess<SiteEditSourceLocation>;
|
|
98
|
+
};
|
|
57
99
|
} | {
|
|
58
100
|
type: "response";
|
|
59
101
|
payload: {
|
|
@@ -97,12 +139,29 @@ export type SiteEditBridgeEventMessage = {
|
|
|
97
139
|
event: SiteEditEvent;
|
|
98
140
|
};
|
|
99
141
|
};
|
|
100
|
-
export type
|
|
142
|
+
export type SiteEditBridgeAuthMessage = {
|
|
143
|
+
type: "auth";
|
|
144
|
+
payload: {
|
|
145
|
+
key: string;
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
export type SiteEditBridgeAuthOkMessage = {
|
|
149
|
+
type: "auth-ok";
|
|
150
|
+
payload: {
|
|
151
|
+
ok: true;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
export type SiteEditBridgeMessage = SiteEditBridgeAuthMessage | SiteEditBridgeAuthOkMessage | SiteEditBridgeRequest | SiteEditBridgeResponse | SiteEditBridgeError | SiteEditBridgeEventMessage;
|
|
101
155
|
export type SiteEditBridgeEngine = {
|
|
102
156
|
getDocument(routeId: string): Awaitable<SiteEditCommandSuccess<SiteEditRenderDocument>>;
|
|
103
157
|
getObjectSummary(key: string, routeId?: string): Awaitable<SiteEditCommandSuccess<SiteEditObjectSummary>>;
|
|
104
158
|
getObjectEditContext(key: string, routeId?: string): Awaitable<SiteEditCommandSuccess<SiteEditObjectEditContext>>;
|
|
105
159
|
getClassNameSource(key: string, routeId?: string): Awaitable<SiteEditGetClassNameSourceCommandResult>;
|
|
160
|
+
getSourceTree(): Awaitable<SiteEditCommandSuccess<SiteEditSourceTreeResponse>>;
|
|
161
|
+
getSourceFile(input: {
|
|
162
|
+
path: string;
|
|
163
|
+
}): Awaitable<SiteEditCommandSuccess<SiteEditSourceFileResponse>>;
|
|
164
|
+
resolveSourceLocation(input: SiteEditResolveSourceLocationInput): Awaitable<SiteEditCommandSuccess<SiteEditSourceLocation>>;
|
|
106
165
|
execute(request: SiteEditOperationRequest): Awaitable<SiteEditOperationCommandResult>;
|
|
107
166
|
undo(): Awaitable<SiteEditOperationCommandResult>;
|
|
108
167
|
redo(): Awaitable<SiteEditOperationCommandResult>;
|
package/dist/server.d.ts
CHANGED
|
@@ -5,6 +5,11 @@ import { type SiteEditBridgeEngine } from "./protocol.js";
|
|
|
5
5
|
export type SiteEditBridgeRuntimeOptions = {
|
|
6
6
|
routes?: SiteEditNextRouteConfig[];
|
|
7
7
|
watch_dirs?: string[];
|
|
8
|
+
watch_debounce_ms?: number;
|
|
9
|
+
watch_max_dirs?: number;
|
|
10
|
+
watch_max_files?: number;
|
|
11
|
+
watch_max_depth?: number;
|
|
12
|
+
watch_max_startup_ms?: number;
|
|
8
13
|
};
|
|
9
14
|
export type SiteEditBridgeServerOptions = {
|
|
10
15
|
siteId: string;
|
|
@@ -18,6 +23,7 @@ export type SiteEditBridgeServerOptions = {
|
|
|
18
23
|
allowedOrigins?: string[];
|
|
19
24
|
runtime?: SiteEditBridgeRuntimeOptions;
|
|
20
25
|
};
|
|
26
|
+
export declare const SITE_EDIT_BRIDGE_MAX_PAYLOAD_BYTES: number;
|
|
21
27
|
export declare function ensureSiteEditBridgeServerStarted(options: SiteEditBridgeServerOptions): SiteEditBridgeServer;
|
|
22
28
|
export declare function attachSiteEditBridgeServer(server: HttpServer, options: SiteEditBridgeServerOptions): SiteEditBridgeServer;
|
|
23
29
|
export declare class SiteEditBridgeServer {
|
|
@@ -28,6 +34,8 @@ export declare class SiteEditBridgeServer {
|
|
|
28
34
|
private routes;
|
|
29
35
|
private stopWatching;
|
|
30
36
|
private stopWebSocketServer;
|
|
37
|
+
private readonly pendingChangedFiles;
|
|
38
|
+
private fileChangeTimer;
|
|
31
39
|
private isStarted;
|
|
32
40
|
constructor(options: Required<SiteEditBridgeServerOptions>, server?: HttpServer, closeHttpServerOnStop?: boolean);
|
|
33
41
|
start(options?: {
|
|
@@ -38,6 +46,8 @@ export declare class SiteEditBridgeServer {
|
|
|
38
46
|
private activateRoute;
|
|
39
47
|
private normalizeRequest;
|
|
40
48
|
private handleFileChanged;
|
|
49
|
+
private handleFilesChanged;
|
|
50
|
+
private flushChangedFiles;
|
|
41
51
|
private refreshRoutes;
|
|
42
52
|
private resolveRouteId;
|
|
43
53
|
private getRuntime;
|
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import C from"node:http";import{randomBytes as M,timingSafeEqual as P}from"node:crypto";import W from"node:path";import{WebSocket as D,WebSocketServer as J}from"ws";import{createPreviewSiteEditEngineRuntime as H}from"@vcp-dev/site-edit-engine";import{resolveNextAppRouteId as _,scanNextAppRouterRoutes as $}from"@vcp-dev/site-edit-engine/next-app-router";import{SiteEditSourceWatcher as U}from"@vcp-dev/site-edit-engine/source-watcher";function S(t){let e=new Map,s=[],n=new Map,r=[],o=i=>{let a=e.get(i);if(typeof a=="number")return a;let h=s.length;return s.push(i),e.set(i,h),h},u=i=>{let a=null;for(let h of j(i)){let R=`${a??"root"}\0${h}`,w=n.get(R);if(typeof w=="number"){a=w;continue}let b=r.length;r.push({parent:a,token:o(h)}),n.set(R,b),a=b}if(a===null)throw new Error("Document key must not be empty");return a};return{encoding:"segment-trie-v1",siteId:t.siteId,snapshotId:t.snapshotId,routeId:t.routeId,version:t.version,tokens:s,nodes:r,rootKeyNodeIds:t.rootKeys.map(u),entries:t.entries.map(i=>({keyNodeId:u(i.key),tag:i.tag,label:i.label,parentKeyNodeId:i.parentKey?u(i.parentKey):null,childKeyNodeIds:i.childKeys.map(u),operationSummary:i.operationSummary}))}}function j(t){if(!t)return[];let e=[],s=0,n=I(t,s);if(n<0)return e.push(t),e;for(e.push(t.slice(0,n)),s=n;s<t.length;){if(n=I(t,s+(t.startsWith("::",s)?2:1)),n<0){e.push(t.slice(s));break}e.push(t.slice(s,n)),s=n}return e}function I(t,e){let s=t.indexOf("/",e),n=t.indexOf("::",e);return s<0?n:n<0?s:Math.min(s,n)}var d=class t{constructor(){}static parseMessage(e,s={}){let n=JSON.parse(e);if(!t.isMessage(n,s))throw new Error("Invalid site edit bridge message");return n}static stringifyMessage(e){return JSON.stringify(e)}static extractRequestInfo(e){if(typeof e=="object"&&e!==null&&"payload"in e&&typeof e.payload=="object"&&e.payload!==null){let s=e.payload,n=typeof s.id=="string"?s.id:"unknown",r=t.isRequestKind(s.kind)?s.kind:"get-document";return{id:n,kind:r}}return{id:"unknown",kind:"get-document"}}static isRequestKind(e){return e==="get-document"||e==="get-object-summary"||e==="get-object-edit-context"||e==="get-class-name-source"||e==="execute"||e==="undo"||e==="redo"}static isMessage(e,s={}){return!c(e)||typeof e.type!="string"?!1:e.type==="request"?t.isRequestPayload(e.payload,s):e.type==="response"||e.type==="error"?c(e.payload)&&typeof e.payload.id=="string":e.type==="event"?c(e.payload)&&c(e.payload.event):!1}static isRequestPayload(e,s={}){if(!c(e)||typeof e.id!="string"||!t.isRequestKind(e.kind))return!1;switch(e.kind){case"get-document":return typeof e.routeId=="string";case"get-object-summary":case"get-object-edit-context":case"get-class-name-source":return typeof e.key=="string"&&(e.routeId===void 0||typeof e.routeId=="string");case"execute":return B(e.request)&&(s.validateOperationRequest?.(e.request)??!0);case"undo":case"redo":return!0}}},p=class{constructor(e){this.options=e,this.messageQueue=new g,this.eventDispatcher=new f(s=>{this.sendJson(s)}),this.unsubscribe=e.engine.subscribe(s=>{this.eventDispatcher.push(s)}),e.socket.onmessage=s=>{try{this.messageQueue.enqueue(()=>this.handleRawMessage(s.data))}catch(n){this.sendJson({type:"error",payload:{id:"unknown",kind:"get-document",message:n instanceof Error?n.message:"Bridge queue overflow"}}),e.socket.close(1013,"bridge busy")}},e.socket.onclose=()=>{this.unsubscribe()}}async handleMessage(e){try{let s=e;if(s.type!=="request")return;let n=s.payload,{id:r}=n;switch(n.kind){case"get-document":this.sendJson({type:"response",payload:{id:r,kind:n.kind,result:{ok:!0,data:S((await this.options.engine.getDocument(n.routeId)).data)}}});return;case"get-object-summary":this.sendJson({type:"response",payload:{id:r,kind:n.kind,result:await this.options.engine.getObjectSummary(n.key,n.routeId)}});return;case"get-object-edit-context":this.sendJson({type:"response",payload:{id:r,kind:n.kind,result:await this.options.engine.getObjectEditContext(n.key,n.routeId)}});return;case"get-class-name-source":this.sendJson({type:"response",payload:{id:r,kind:n.kind,result:await this.options.engine.getClassNameSource(n.key,n.routeId)}});return;case"execute":await this.handleBufferedOperation(r,n.kind,()=>this.options.engine.execute(n.request));return;case"undo":await this.handleBufferedOperation(r,n.kind,()=>this.options.engine.undo());return;case"redo":await this.handleBufferedOperation(r,n.kind,()=>this.options.engine.redo());return}}catch(s){let n=d.extractRequestInfo(e);this.sendJson({type:"error",payload:{...n,message:s instanceof Error?s.message:"Unknown websocket operation failure"}})}}async handleRawMessage(e){try{await this.handleMessage(d.parseMessage(e,{validateOperationRequest:this.options.validateOperationRequest}))}catch(s){let n=s instanceof Error?s.message:"Invalid websocket message",r=N(e);this.sendJson({type:"error",payload:{...r,message:n}})}}sendJson(e){this.options.socket.send(d.stringifyMessage(e))}async handleBufferedOperation(e,s,n){this.eventDispatcher.beginBuffering();try{let r=await n();this.sendJson({type:"response",payload:{id:e,kind:s,result:r}}),this.eventDispatcher.flush()}catch(r){this.sendJson({type:"error",payload:{id:e,kind:s,message:r instanceof Error?r.message:"Unknown websocket operation failure"}}),this.eventDispatcher.flush()}}},f=class{constructor(e){this.send=e,this.bufferedEvents=null}push(e){let s={type:"event",payload:{event:e}};if(this.bufferedEvents){this.bufferedEvents.push(s);return}this.send(s)}beginBuffering(){this.bufferedEvents=[]}flush(){let e=this.bufferedEvents??[];this.bufferedEvents=null,e.forEach(s=>{this.send(s)})}},g=class{constructor(e=64){this.maxQueued=e,this.queue=Promise.resolve(),this.queued=0}enqueue(e){if(this.queued>=this.maxQueued)throw new Error("Bridge request queue is full");this.queued+=1;let s=async()=>{this.queued-=1,await e()},n=this.queue.then(s,s);return this.queue=n.then(()=>{},()=>{}),n}};function c(t){return typeof t=="object"&&t!==null}function B(t){return c(t)&&typeof t.id=="string"&&typeof t.siteId=="string"&&typeof t.baseSnapshotId=="string"&&(t.routeId===void 0||typeof t.routeId=="string")&&typeof t.kind=="string"&&c(t.target)&&typeof t.documentVersion=="number"&&Number.isInteger(t.documentVersion)&&t.documentVersion>=0&&c(t.params)}function N(t){try{return d.extractRequestInfo(JSON.parse(t))}catch{return{id:"unknown",kind:"get-document"}}}import{SiteEditOperationRequestSchema as F}from"@vcp-dev/contracts";var z="127.0.0.1",L=30218,V="/__vcp/site-edit/bridge",A=128*1024,k="__VCP_SITE_EDIT_BRIDGE_SERVERS__";function fe(t){let e=x(t),s=E(e),n=O(),r=n.get(s);if(r)return r;let o=new l(e);n.set(s,o);try{o.start()}catch(u){throw n.delete(s),o.stop(),u}return o}function ge(t,e){let s=new l(x(e),t,!1);return s.start({listen:!1}),s}var l=class{constructor(e,s=C.createServer((r,o)=>{o.writeHead(404),o.end("VCP site edit bridge server")}),n=!0){this.options=e,this.server=s,this.closeHttpServerOnStop=n,this.runtime=null,this.routes=[],this.stopWatching=null,this.stopWebSocketServer=null,this.isStarted=!1}start(e={}){if(this.isStarted)return;this.refreshRoutes(),this.runtime=H({siteId:this.options.siteId,snapshotId:this.options.snapshotId,projectRoot:this.options.projectRoot,routes:this.routes});let s=T({server:this.server,path:this.options.path,sessionKey:this.options.sessionKey,host:this.options.host,allowedOrigins:this.options.allowedOrigins,engine:this.createBridgeEngine()});this.stopWebSocketServer=s.stop,this.stopWatching=new U({projectRoot:this.options.projectRoot,watch_dirs:this.options.runtime.watch_dirs,on_change:n=>this.handleFileChanged(n)}).start(),e.listen!==!1&&this.server.listen(this.options.port,this.options.host),this.isStarted=!0}stop(){this.stopWatching?.(),this.stopWatching=null,this.stopWebSocketServer?.(),this.stopWebSocketServer=null,this.runtime?.dispose(),this.runtime=null,this.closeHttpServerOnStop&&this.server.close();let e=O(),s=E(this.options);e.get(s)===this&&e.delete(s),this.isStarted=!1}createBridgeEngine(){return{getDocument:async e=>{let{runtime:s,resolvedRouteId:n}=await this.activateRoute(e);return s.getDocument(n)},getObjectSummary:async(e,s)=>this.getRuntime().getObjectSummary(e,s?this.resolveRouteId(s):void 0),getObjectEditContext:async(e,s)=>this.getRuntime().getObjectEditContext(e,s?this.resolveRouteId(s):void 0),getClassNameSource:async(e,s)=>{let n=this.getRuntime(),r=s?this.resolveRouteId(s):void 0;return await n.refreshSourceForKey(e,r),n.getClassNameSource(e,r)},execute:async e=>this.getRuntime().executeOperation(this.normalizeRequest(e)),undo:async()=>this.getRuntime().undo(),redo:async()=>this.getRuntime().redo(),subscribe:e=>this.getRuntime().subscribe(e)}}async activateRoute(e){let s=this.getRuntime(),n=this.resolveRouteId(e);return await s.setActiveRoute(n),{runtime:s,resolvedRouteId:n}}normalizeRequest(e){return e.target.kind!=="route"?e.routeId?{...e,routeId:this.resolveRouteId(e.routeId)}:e:{...e,target:{...e.target,routeId:this.resolveRouteId(e.target.routeId)}}}handleFileChanged(e){this.refreshRoutes(),this.runtime?.notifyFileChanged(e)}refreshRoutes(){let e=this.options.runtime.routes?.length?this.options.runtime.routes:$(this.options.projectRoot),s=new Map(this.routes.map(r=>[r.routeId,r.entryFile])),n=new Set(e.map(r=>r.routeId));this.routes=e;for(let r of e)s.get(r.routeId)!==r.entryFile&&this.runtime?.registerRoute(r.routeId,r.entryFile);for(let r of s.keys())n.has(r)||this.runtime?.unregisterRoute(r)}resolveRouteId(e){let s=_(e,this.routes,this.options.basePath);if(!s)throw new Error(`Route is not indexed by the preview bridge: ${e}`);return s}getRuntime(){if(!this.runtime)throw new Error("VCP site edit preview bridge runtime is not ready");return this.runtime}};function x(t){return{siteId:t.siteId,snapshotId:t.snapshotId,projectRoot:W.resolve(t.projectRoot??process.cwd()),basePath:t.basePath??"",host:t.host??z,port:t.port??L,path:t.path??V,sessionKey:t.sessionKey??te(),allowedOrigins:t.allowedOrigins??[],runtime:t.runtime??{}}}function O(){let t=globalThis;return t[k]??=new Map,t[k]}function E(t){return`${t.projectRoot}:${t.host}:${t.port}:${t.path}`}function T(t){let e=new J({maxPayload:A,noServer:!0}),s=(n,r,o)=>{if(Q(n,t)){if(!G(n,t)){r.write(`HTTP/1.1 401 Unauthorized\r
|
|
1
|
+
import D from"node:http";import{randomBytes as N,timingSafeEqual as H}from"node:crypto";import P from"node:path";import{WebSocket as C,WebSocketServer as J}from"ws";import{createPreviewSiteEditEngineRuntime as W}from"@vcp-dev/site-edit-engine";import{resolveNextAppRouteId as A,scanNextAppRouterRoutes as U}from"@vcp-dev/site-edit-engine/next-app-router";import{SiteEditSourceWatcher as L}from"@vcp-dev/site-edit-engine/source-watcher";function k(t){let e=new Map,s=[],n=new Map,r=[],o=a=>{let u=e.get(a);if(typeof u=="number")return u;let h=s.length;return s.push(a),e.set(a,h),h},i=a=>{let u=null;for(let h of j(a)){let R=`${u??"root"}\0${h}`,w=n.get(R);if(typeof w=="number"){u=w;continue}let S=r.length;r.push({parent:u,token:o(h)}),n.set(R,S),u=S}if(u===null)throw new Error("Document key must not be empty");return u};return{encoding:"segment-trie-v1",siteId:t.siteId,snapshotId:t.snapshotId,routeId:t.routeId,version:t.version,tokens:s,nodes:r,rootKeyNodeIds:t.rootKeys.map(i),entries:t.entries.map(a=>({keyNodeId:i(a.key),tag:a.tag,label:a.label,parentKeyNodeId:a.parentKey?i(a.parentKey):null,childKeyNodeIds:a.childKeys.map(i),operationSummary:a.operationSummary}))}}function j(t){if(!t)return[];let e=[],s=0,n=b(t,s);if(n<0)return e.push(t),e;for(e.push(t.slice(0,n)),s=n;s<t.length;){if(n=b(t,s+(t.startsWith("::",s)?2:1)),n<0){e.push(t.slice(s));break}e.push(t.slice(s,n)),s=n}return e}function b(t,e){let s=t.indexOf("/",e),n=t.indexOf("::",e);return s<0?n:n<0?s:Math.min(s,n)}var d=class t{constructor(){}static parseMessage(e,s={}){let n=JSON.parse(e);if(!t.isMessage(n,s))throw new Error("Invalid site edit bridge message");return n}static stringifyMessage(e){return JSON.stringify(e)}static extractRequestInfo(e){if(typeof e=="object"&&e!==null&&"payload"in e&&typeof e.payload=="object"&&e.payload!==null){let s=e.payload,n=typeof s.id=="string"?s.id:"unknown",r=t.isRequestKind(s.kind)?s.kind:"get-document";return{id:n,kind:r}}return{id:"unknown",kind:"get-document"}}static isRequestKind(e){return e==="get-document"||e==="get-object-summary"||e==="get-object-edit-context"||e==="get-class-name-source"||e==="get-source-tree"||e==="get-source-file"||e==="resolve-source-location"||e==="execute"||e==="undo"||e==="redo"}static isMessage(e,s={}){return!c(e)||typeof e.type!="string"?!1:e.type==="request"?t.isRequestPayload(e.payload,s):e.type==="response"||e.type==="error"?c(e.payload)&&typeof e.payload.id=="string":e.type==="event"?c(e.payload)&&c(e.payload.event):e.type==="auth"?c(e.payload)&&typeof e.payload.key=="string":e.type==="auth-ok"?c(e.payload)&&e.payload.ok===!0:!1}static isRequestPayload(e,s={}){if(!c(e)||typeof e.id!="string"||!t.isRequestKind(e.kind))return!1;switch(e.kind){case"get-document":return typeof e.routeId=="string";case"get-object-summary":case"get-object-edit-context":case"get-class-name-source":case"resolve-source-location":return typeof e.key=="string"&&(e.routeId===void 0||typeof e.routeId=="string");case"get-source-tree":return!0;case"get-source-file":return typeof e.path=="string";case"execute":return M(e.request)&&(s.validateOperationRequest?.(e.request)??!0);case"undo":case"redo":return!0}}},l=class{constructor(e){this.options=e,this.messageQueue=new g,this.eventDispatcher=new f(s=>{this.sendJson(s)}),this.unsubscribe=e.engine.subscribe(s=>{this.eventDispatcher.push(s)}),e.socket.onmessage=s=>{try{this.messageQueue.enqueue(()=>this.handleRawMessage(s.data))}catch(n){this.sendJson({type:"error",payload:{id:"unknown",kind:"get-document",message:n instanceof Error?n.message:"Bridge queue overflow"}}),e.socket.close(1013,"bridge busy")}},e.socket.onclose=()=>{this.unsubscribe()}}async handleMessage(e){try{let s=e;if(s.type!=="request")return;let n=s.payload,{id:r}=n;switch(n.kind){case"get-document":this.sendJson({type:"response",payload:{id:r,kind:n.kind,result:{ok:!0,data:k((await this.options.engine.getDocument(n.routeId)).data)}}});return;case"get-object-summary":this.sendJson({type:"response",payload:{id:r,kind:n.kind,result:await this.options.engine.getObjectSummary(n.key,n.routeId)}});return;case"get-object-edit-context":this.sendJson({type:"response",payload:{id:r,kind:n.kind,result:await this.options.engine.getObjectEditContext(n.key,n.routeId)}});return;case"get-class-name-source":this.sendJson({type:"response",payload:{id:r,kind:n.kind,result:await this.options.engine.getClassNameSource(n.key,n.routeId)}});return;case"get-source-tree":this.sendJson({type:"response",payload:{id:r,kind:n.kind,result:await this.options.engine.getSourceTree()}});return;case"get-source-file":this.sendJson({type:"response",payload:{id:r,kind:n.kind,result:await this.options.engine.getSourceFile({path:n.path})}});return;case"resolve-source-location":this.sendJson({type:"response",payload:{id:r,kind:n.kind,result:await this.options.engine.resolveSourceLocation({key:n.key,routeId:n.routeId})}});return;case"execute":await this.handleBufferedOperation(r,n.kind,()=>this.options.engine.execute(n.request));return;case"undo":await this.handleBufferedOperation(r,n.kind,()=>this.options.engine.undo());return;case"redo":await this.handleBufferedOperation(r,n.kind,()=>this.options.engine.redo());return}}catch(s){let n=d.extractRequestInfo(e);this.sendJson({type:"error",payload:{...n,message:s instanceof Error?s.message:"Unknown websocket operation failure"}})}}async handleRawMessage(e){try{await this.handleMessage(d.parseMessage(e,{validateOperationRequest:this.options.validateOperationRequest}))}catch(s){let n=s instanceof Error?s.message:"Invalid websocket message",r=F(e);this.sendJson({type:"error",payload:{...r,message:n}})}}sendJson(e){this.options.socket.send(d.stringifyMessage(e))}async handleBufferedOperation(e,s,n){this.eventDispatcher.beginBuffering();try{let r=await n();this.sendJson({type:"response",payload:{id:e,kind:s,result:r}}),this.eventDispatcher.flush()}catch(r){this.sendJson({type:"error",payload:{id:e,kind:s,message:r instanceof Error?r.message:"Unknown websocket operation failure"}}),this.eventDispatcher.flush()}}},f=class{constructor(e){this.send=e,this.bufferedEvents=null}push(e){let s={type:"event",payload:{event:e}};if(this.bufferedEvents){this.bufferedEvents.push(s);return}this.send(s)}beginBuffering(){this.bufferedEvents=[]}flush(){let e=this.bufferedEvents??[];this.bufferedEvents=null,e.forEach(s=>{this.send(s)})}},g=class{constructor(e=64){this.maxQueued=e,this.queue=Promise.resolve(),this.queued=0}enqueue(e){if(this.queued>=this.maxQueued)throw new Error("Bridge request queue is full");this.queued+=1;let s=async()=>{this.queued-=1,await e()},n=this.queue.then(s,s);return this.queue=n.then(()=>{},()=>{}),n}};function c(t){return typeof t=="object"&&t!==null}function M(t){return c(t)&&typeof t.id=="string"&&typeof t.siteId=="string"&&typeof t.baseSnapshotId=="string"&&(t.routeId===void 0||typeof t.routeId=="string")&&typeof t.kind=="string"&&c(t.target)&&typeof t.documentVersion=="number"&&Number.isInteger(t.documentVersion)&&t.documentVersion>=0&&c(t.params)}function F(t){try{return d.extractRequestInfo(JSON.parse(t))}catch{return{id:"unknown",kind:"get-document"}}}import{SiteEditOperationRequestSchema as $}from"@vcp-dev/contracts";var z="127.0.0.1",V=30218,G="/__vcp/site-edit/bridge",Q=6*1024*1024,Y=3e3,x=50,_="__VCP_SITE_EDIT_BRIDGE_SERVERS__";function Ie(t){let e=O(t),s=K(e),n=T(),r=n.get(s);if(r)return r;let o=new p(e);n.set(s,o);try{o.start()}catch(i){throw n.delete(s),o.stop(),i}return o}function Re(t,e){let s=new p(O(e),t,!1);return s.start({listen:!1}),s}var p=class{constructor(e,s=D.createServer((r,o)=>{o.writeHead(404),o.end("VCP site edit bridge server")}),n=!0){this.options=e,this.server=s,this.closeHttpServerOnStop=n,this.runtime=null,this.routes=[],this.stopWatching=null,this.stopWebSocketServer=null,this.pendingChangedFiles=new Set,this.fileChangeTimer=null,this.isStarted=!1}start(e={}){if(this.isStarted)return;this.refreshRoutes(),this.runtime=W({siteId:this.options.siteId,snapshotId:this.options.snapshotId,projectRoot:this.options.projectRoot,routes:this.routes});let s=X({server:this.server,path:this.options.path,sessionKey:this.options.sessionKey,host:this.options.host,allowedOrigins:this.options.allowedOrigins,engine:this.createBridgeEngine()});this.stopWebSocketServer=s.stop,this.stopWatching=new L({projectRoot:this.options.projectRoot,watch_dirs:this.options.runtime.watch_dirs,debounce_ms:this.options.runtime.watch_debounce_ms??x,max_dirs:this.options.runtime.watch_max_dirs,max_files:this.options.runtime.watch_max_files,max_depth:this.options.runtime.watch_max_depth,max_startup_ms:this.options.runtime.watch_max_startup_ms,on_change:n=>this.handleFileChanged(n),on_batch_change:n=>this.handleFilesChanged(n)}).start(),e.listen!==!1&&this.server.listen(this.options.port,this.options.host),this.isStarted=!0}stop(){this.stopWatching?.(),this.stopWatching=null,this.flushChangedFiles(),this.fileChangeTimer&&(clearTimeout(this.fileChangeTimer),this.fileChangeTimer=null),this.stopWebSocketServer?.(),this.stopWebSocketServer=null,this.runtime?.dispose(),this.runtime=null,this.closeHttpServerOnStop&&this.server.close();let e=T(),s=K(this.options);e.get(s)===this&&e.delete(s),this.isStarted=!1}createBridgeEngine(){return{getDocument:async e=>{let{runtime:s,resolvedRouteId:n}=await this.activateRoute(e);return s.getDocument(n)},getObjectSummary:async(e,s)=>this.getRuntime().getObjectSummary(e,s?this.resolveRouteId(s):void 0),getObjectEditContext:async(e,s)=>this.getRuntime().getObjectEditContext(e,s?this.resolveRouteId(s):void 0),getClassNameSource:async(e,s)=>{let n=this.getRuntime(),r=s?this.resolveRouteId(s):void 0;return await n.refreshSourceForKey(e,r),n.getClassNameSource(e,r)},getSourceTree:async()=>this.getRuntime().getSourceTree(),getSourceFile:async e=>this.getRuntime().getSourceFile(e),resolveSourceLocation:async e=>this.getRuntime().resolveSourceLocation({key:e.key,routeId:e.routeId?this.resolveRouteId(e.routeId):void 0}),execute:async e=>this.getRuntime().executeOperation(this.normalizeRequest(e)),undo:async()=>this.getRuntime().undo(),redo:async()=>this.getRuntime().redo(),subscribe:e=>this.getRuntime().subscribe(e)}}async activateRoute(e){let s=this.getRuntime(),n=this.resolveRouteId(e);return await s.setActiveRoute(n),{runtime:s,resolvedRouteId:n}}normalizeRequest(e){return e.target.kind!=="route"?e.routeId?{...e,routeId:this.resolveRouteId(e.routeId)}:e:{...e,target:{...e.target,routeId:this.resolveRouteId(e.target.routeId)}}}handleFileChanged(e){this.handleFilesChanged([e])}handleFilesChanged(e){for(let n of e)this.pendingChangedFiles.add(n);this.fileChangeTimer&&clearTimeout(this.fileChangeTimer);let s=this.options.runtime.watch_debounce_ms??x;this.fileChangeTimer=setTimeout(()=>{this.fileChangeTimer=null,this.flushChangedFiles()},s),this.fileChangeTimer?.unref?.()}flushChangedFiles(){if(this.fileChangeTimer&&(clearTimeout(this.fileChangeTimer),this.fileChangeTimer=null),this.pendingChangedFiles.size===0)return;let e=[...this.pendingChangedFiles].sort((s,n)=>s.localeCompare(n));this.pendingChangedFiles.clear(),this.refreshRoutes();for(let s of e)this.runtime?.notifyFileChanged(s)}refreshRoutes(){let e=this.options.runtime.routes?.length?this.options.runtime.routes:U(this.options.projectRoot),s=new Map(this.routes.map(r=>[r.routeId,r.entryFile])),n=new Set(e.map(r=>r.routeId));this.routes=e;for(let r of e)s.get(r.routeId)!==r.entryFile&&this.runtime?.registerRoute(r.routeId,r.entryFile);for(let r of s.keys())n.has(r)||this.runtime?.unregisterRoute(r)}resolveRouteId(e){let s=A(e,this.routes,this.options.basePath);if(!s)throw new Error(`Route is not indexed by the preview bridge: ${e}`);return s}getRuntime(){if(!this.runtime)throw new Error("VCP site edit preview bridge runtime is not ready");return this.runtime}};function O(t){return{siteId:t.siteId,snapshotId:t.snapshotId,projectRoot:P.resolve(t.projectRoot??process.cwd()),basePath:t.basePath??"",host:t.host??z,port:t.port??V,path:t.path??G,sessionKey:t.sessionKey??oe(),allowedOrigins:t.allowedOrigins??[],runtime:t.runtime??{}}}function T(){let t=globalThis;return t[_]??=new Map,t[_]}function K(t){return`${t.projectRoot}:${t.host}:${t.port}:${t.path}`}function X(t){let e=new J({maxPayload:Q,noServer:!0}),s=new WeakSet,n=o=>{new l({socket:new y(o),engine:t.engine,validateOperationRequest:i=>$.safeParse(i).success})},r=(o,i,a)=>{if(!Z(o,t))return;let u=ee(o,t);if(!u){i.write(`HTTP/1.1 401 Unauthorized\r
|
|
2
2
|
Connection: close\r
|
|
3
3
|
\r
|
|
4
|
-
`),
|
|
4
|
+
`),i.destroy();return}u.authenticatedByHeader&&s.add(o),e.handleUpgrade(o,i,a,h=>{e.emit("connection",h,o)})};return t.server.on("upgrade",r),e.on("connection",(o,i)=>{if(s.has(i)){n(o);return}te(o,t.sessionKey,()=>{n(o)})}),e.stop=()=>{t.server.off("upgrade",r);for(let o of e.clients)o.close(1001,"bridge stopped");e.close()},e}var y=class{constructor(e){this.socket=e,this.onmessage=null,this.onclose=null,e.on("message",s=>{this.onmessage?.({data:s.toString()})}),e.on("close",()=>{this.onclose?.()})}send(e){this.socket.readyState===C.OPEN&&this.socket.send(e)}close(e,s){this.socket.close(e,s)}};function Z(t,e){return new URL(t.url??"/","http://localhost").pathname===e.path}function ee(t,e){if(new URL(t.url??"/","http://localhost").searchParams.has("vcp_bridge_key"))return null;let n=m(t.headers["x-vcp-site-edit-key"]),r=B(n,e.sessionKey),o=m(t.headers.host),i=m(t.headers.origin);return o&&!se(o,e.host,i)?null:i?re(i,o,e.allowedOrigins)?{authenticatedByHeader:r}:null:r?{authenticatedByHeader:!0}:null}function te(t,e,s){let n=setTimeout(()=>{t.close(1008,"bridge authentication timeout")},Y),r,o=()=>{clearTimeout(n),t.off("message",r),t.off("close",o)},i=()=>{o(),t.close(1008,"bridge authentication failed")};r=a=>{try{let u=d.parseMessage(a.toString());if(u.type!=="auth"||!B(u.payload.key,e)){i();return}o(),t.readyState===C.OPEN&&t.send(d.stringifyMessage({type:"auth-ok",payload:{ok:!0}})),s()}catch{i()}},t.on("message",r),t.on("close",o)}function B(t,e){if(!t)return!1;let s=Buffer.from(t),n=Buffer.from(e);return s.length===n.length&&H(s,n)}function se(t,e,s){let n=v(t),r=v(e);return n===r||E(r)&&E(n)&&(!s||I(s,t))||ne(r)&&(!s||I(s,t))}function ne(t){return t==="0.0.0.0"||t==="::"}function E(t){return t==="localhost"||t==="127.0.0.1"||t==="::1"}function re(t,e,s){return s.includes(t)||(e?I(t,e):!1)}function I(t,e){try{return new URL(t).host===q(e)}catch{return!1}}function v(t){let e=q(t);return e.startsWith("[")?e.slice(1,e.indexOf("]")):e.split(":").length-1===1?e.split(":",1)[0]??"":e}function q(t){return t.trim().toLowerCase()}function m(t){return Array.isArray(t)?t[0]:t}function oe(){return N(32).toString("base64url")}export{Q as SITE_EDIT_BRIDGE_MAX_PAYLOAD_BYTES,p as SiteEditBridgeServer,Re as attachSiteEditBridgeServer,X as attachSiteEditWebSocketServer,Ie as ensureSiteEditBridgeServerStarted};
|
|
@@ -17,6 +17,7 @@ console.log(JSON.stringify({
|
|
|
17
17
|
projectRoot: projectRoot,
|
|
18
18
|
preview_url: `http://127.0.0.1:${port}`,
|
|
19
19
|
bridge_path: "/__vcp/site-edit/bridge",
|
|
20
|
+
bundler: preview.bundler,
|
|
20
21
|
}, null, 2));
|
|
21
22
|
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
22
23
|
process.on(signal, () => {
|
package/dist/window-message.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SiteEditCommandSuccess, SiteEditEvent, SiteEditObjectEditContext, SiteEditObjectSummary, SiteEditGetClassNameSourceCommandResult, SiteEditOperationCommandResult, SiteEditOperationRequest, SiteEditRenderDocument } from "@vcp-dev/contracts";
|
|
1
|
+
import type { SiteEditCommandSuccess, SiteEditEvent, SiteEditObjectEditContext, SiteEditObjectSummary, SiteEditGetClassNameSourceCommandResult, SiteEditOperationCommandResult, SiteEditOperationRequest, SiteEditRenderDocument, SiteEditSourceFileResponse, SiteEditSourceLocation, SiteEditSourceTreeResponse } from "@vcp-dev/contracts";
|
|
2
2
|
export declare const VCP_WINDOW_MESSAGE_SOURCE = "vcp-window-message";
|
|
3
3
|
export declare const VCP_WINDOW_MESSAGE_VERSION = 1;
|
|
4
4
|
export type VcpWindowMessageEnvelope<TNamespace extends string = string, TChannel extends string = string, TBody = unknown> = {
|
|
@@ -41,6 +41,12 @@ export type SiteEditFrameRuntimeNodePatch = {
|
|
|
41
41
|
attributes?: Record<string, string | null>;
|
|
42
42
|
className?: string;
|
|
43
43
|
style?: Record<string, string | number | null | undefined>;
|
|
44
|
+
styleDeclarations?: SiteEditFrameRuntimeStyleDeclaration[];
|
|
45
|
+
};
|
|
46
|
+
export type SiteEditFrameRuntimeStyleDeclaration = {
|
|
47
|
+
property: string;
|
|
48
|
+
value: string | number | null | undefined;
|
|
49
|
+
priority?: "important";
|
|
44
50
|
};
|
|
45
51
|
export type SiteEditFrameRuntimeNodePatchPreviewMeta = {
|
|
46
52
|
sessionId: string;
|
|
@@ -79,6 +85,14 @@ export type SiteEditFrameRuntimeDropTarget = {
|
|
|
79
85
|
};
|
|
80
86
|
rejectionReason?: SiteEditFrameRuntimeDropRejectionReason;
|
|
81
87
|
};
|
|
88
|
+
export type SiteEditFrameRuntimeBridgeStatusCode = "connecting" | "ready" | "reconnecting" | "failed" | "reload-required";
|
|
89
|
+
export type SiteEditFrameRuntimeBridgeStatus = {
|
|
90
|
+
state: SiteEditFrameRuntimeBridgeStatusCode;
|
|
91
|
+
routeId?: string;
|
|
92
|
+
attempt?: number;
|
|
93
|
+
retryInMs?: number;
|
|
94
|
+
messageKey?: string;
|
|
95
|
+
};
|
|
82
96
|
export type SiteEditFrameRuntimeRequestBody = {
|
|
83
97
|
type: "request";
|
|
84
98
|
id: string;
|
|
@@ -88,6 +102,26 @@ export type SiteEditFrameRuntimeRequestBody = {
|
|
|
88
102
|
y: number;
|
|
89
103
|
detailLevel?: SiteEditFrameRuntimeHitDetailLevel;
|
|
90
104
|
};
|
|
105
|
+
} | {
|
|
106
|
+
type: "request";
|
|
107
|
+
id: string;
|
|
108
|
+
kind: "bridge-get-source-tree";
|
|
109
|
+
payload: Record<string, never>;
|
|
110
|
+
} | {
|
|
111
|
+
type: "request";
|
|
112
|
+
id: string;
|
|
113
|
+
kind: "bridge-get-source-file";
|
|
114
|
+
payload: {
|
|
115
|
+
path: string;
|
|
116
|
+
};
|
|
117
|
+
} | {
|
|
118
|
+
type: "request";
|
|
119
|
+
id: string;
|
|
120
|
+
kind: "bridge-resolve-source-location";
|
|
121
|
+
payload: {
|
|
122
|
+
key: string;
|
|
123
|
+
routeId?: string;
|
|
124
|
+
};
|
|
91
125
|
} | {
|
|
92
126
|
type: "request";
|
|
93
127
|
id: string;
|
|
@@ -257,7 +291,7 @@ export type SiteEditFrameRuntimeResponseBody = {
|
|
|
257
291
|
type: "response";
|
|
258
292
|
id: string;
|
|
259
293
|
ok: boolean;
|
|
260
|
-
result?: SiteEditFrameRuntimeStarted | SiteEditFrameRuntimeHit | SiteEditFrameRuntimeNodePatchAck | SiteEditFrameRuntimeHit[] | SiteEditFrameRuntimeDropTarget | SiteEditFrameRuntimeScrollMetrics | SiteEditCommandSuccess<SiteEditRenderDocument> | SiteEditCommandSuccess<SiteEditObjectSummary> | SiteEditCommandSuccess<SiteEditObjectEditContext> | SiteEditGetClassNameSourceCommandResult | SiteEditOperationCommandResult | (SiteEditFrameRuntimeScrollMetrics & {
|
|
294
|
+
result?: SiteEditFrameRuntimeStarted | SiteEditFrameRuntimeHit | SiteEditFrameRuntimeNodePatchAck | SiteEditFrameRuntimeHit[] | SiteEditFrameRuntimeDropTarget | SiteEditFrameRuntimeScrollMetrics | SiteEditCommandSuccess<SiteEditRenderDocument> | SiteEditCommandSuccess<SiteEditObjectSummary> | SiteEditCommandSuccess<SiteEditObjectEditContext> | SiteEditGetClassNameSourceCommandResult | SiteEditCommandSuccess<SiteEditSourceTreeResponse> | SiteEditCommandSuccess<SiteEditSourceFileResponse> | SiteEditCommandSuccess<SiteEditSourceLocation> | SiteEditOperationCommandResult | (SiteEditFrameRuntimeScrollMetrics & {
|
|
261
295
|
scrolled: true;
|
|
262
296
|
}) | {
|
|
263
297
|
interactionEnabled: boolean;
|
|
@@ -313,6 +347,10 @@ export type SiteEditFrameRuntimeEventBody = {
|
|
|
313
347
|
routeId: string;
|
|
314
348
|
version?: number;
|
|
315
349
|
};
|
|
350
|
+
} | {
|
|
351
|
+
type: "event";
|
|
352
|
+
kind: "bridge-status";
|
|
353
|
+
payload: SiteEditFrameRuntimeBridgeStatus;
|
|
316
354
|
} | {
|
|
317
355
|
type: "event";
|
|
318
356
|
kind: "scroll";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vcp-dev/site-edit-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"@vcp-dev/contracts": "0.
|
|
78
|
-
"@vcp-dev/site-edit-engine": "0.
|
|
77
|
+
"@vcp-dev/contracts": "0.4.1",
|
|
78
|
+
"@vcp-dev/site-edit-engine": "0.4.1",
|
|
79
79
|
"@types/ws": "^8.18.1",
|
|
80
80
|
"ws": "^8.20.1"
|
|
81
81
|
},
|