@vcp-dev/site-edit-bridge 0.4.0 → 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.
@@ -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 SiteEditBridgeMessage = SiteEditBridgeRequest | SiteEditBridgeResponse | SiteEditBridgeError | SiteEditBridgeEventMessage;
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
- `),r.destroy();return}e.handleUpgrade(n,r,o,u=>{e.emit("connection",u,n)})}};return t.server.on("upgrade",s),e.on("connection",n=>{new p({socket:new y(n),engine:t.engine,validateOperationRequest:r=>F.safeParse(r).success})}),e.stop=()=>{t.server.off("upgrade",s);for(let n of e.clients)n.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===D.OPEN&&this.socket.send(e)}close(e,s){this.socket.close(e,s)}};function Q(t,e){return new URL(t.url??"/","http://localhost").pathname===e.path}function G(t,e){let n=new URL(t.url??"/","http://localhost").searchParams.get("vcp_bridge_key")??m(t.headers["x-vcp-site-edit-key"]);if(!X(n,e.sessionKey))return!1;let r=m(t.headers.host),o=m(t.headers.origin);return r&&!Y(r,e.host,o)?!1:o?e.allowedOrigins.includes(o)||ee(o)||(r?K(o,r):!1):!0}function X(t,e){if(!t)return!1;let s=Buffer.from(t),n=Buffer.from(e);return s.length===n.length&&P(s,n)}function Y(t,e,s){let n=v(t),r=v(e);return n===r||n==="localhost"||n==="127.0.0.1"||n==="::1"||Z(r)&&!!(s&&K(s,t))}function Z(t){return t==="0.0.0.0"||t==="::"}function ee(t){try{let e=new URL(t);return e.hostname==="localhost"||e.hostname==="127.0.0.1"||e.hostname==="::1"}catch{return!1}}function K(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 te(){return M(32).toString("base64url")}export{l as SiteEditBridgeServer,ge as attachSiteEditBridgeServer,T as attachSiteEditWebSocketServer,fe as ensureSiteEditBridgeServerStarted};
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};
@@ -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.4.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.4.0",
78
- "@vcp-dev/site-edit-engine": "0.4.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
  },