twd-relay 0.3.0 → 1.0.0

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 +1 @@
1
- "use strict";var P=Object.create;var C=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var L=Object.getOwnPropertyNames;var $=Object.getPrototypeOf,z=Object.prototype.hasOwnProperty;var I=(n,o,i,f)=>{if(o&&typeof o=="object"||typeof o=="function")for(let l of L(o))!z.call(n,l)&&l!==i&&C(n,l,{get:()=>o[l],enumerable:!(f=A(o,l))||f.enumerable});return n};var M=(n,o,i)=>(i=n!=null?P($(n)):{},I(o||!n||!n.__esModule?C(i,"default",{value:n,enumerable:!0}):i,n));Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function j(n){return`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}${n}`}function p(n,o){if(!n.parent)return"";const i=o.get(n.parent);return i?i.name:""}function B(n){const o=n?.url??j(n?.path??"/__twd/ws"),i=n?.reconnect??!0,f=n?.reconnectInterval??2e3,l=n?.log??!1,T="[twd-relay]";function m(...t){l&&console.info(T,...t)}function S(...t){console.warn(T,...t)}let r=null,w=!1,g=null;function a(t){r&&r.readyState===WebSocket.OPEN&&r.send(JSON.stringify(t))}function u(){window.dispatchEvent(new CustomEvent("twd:state-change"))}async function N(){const t=window.__TWD_STATE__;if(!t){S("TWD not initialized — make sure twd-js is loaded before running tests"),a({type:"error",code:"NO_TWD",message:"TWD not initialized"});return}const s=t.handlers,y=Array.from(s.values()).filter(e=>e.type==="test").length;a({type:"run:start",testCount:y});let c=0,_=0,b=0;const h=performance.now(),O={onStart(e){e.status="running",u(),a({type:"test:start",id:e.id,name:e.name,suite:p(e,s)})},onPass(e){c++,e.status="pass",u(),a({type:"test:pass",id:e.id,name:e.name,suite:p(e,s),duration:performance.now()-h})},onFail(e,d){_++,e.status="fail",e.logs=[d.message],u(),a({type:"test:fail",id:e.id,name:e.name,suite:p(e,s),error:d.message,duration:performance.now()-h})},onSkip(e){b++,e.status="skip",u(),a({type:"test:skip",id:e.id,name:e.name,suite:p(e,s)})},onSuiteStart(e){e.status="running",u()},onSuiteEnd(e){e.status="idle",u()}};try{const{TestRunner:e}=await import("twd-js/runner");await new e(O).runAll()}catch(e){const d=e instanceof Error?e.message:String(e);S("Runner error:",d),a({type:"error",code:"RUNNER_ERROR",message:d})}const D=performance.now()-h;a({type:"run:complete",passed:c,failed:_,skipped:b,duration:D}),u()}function R(){const t=window.__TWD_STATE__;if(!t){a({type:"error",code:"NO_TWD",message:"TWD not initialized"});return}const s=t.handlers,y=[];for(const[,c]of s)c.type==="test"&&y.push({id:c.id,name:c.name,suite:p(c,s),status:c.status??"idle"});a({type:"status:result",tests:y})}function W(t){let s;try{s=JSON.parse(t.data)}catch{return}s.type==="run"?(m("Received run command — running tests..."),N()):s.type==="status"&&R()}function k(){i&&!w&&(m(`Reconnecting in ${f}ms...`),g=setTimeout(()=>{E()},f))}function E(){r&&(r.readyState===WebSocket.OPEN||r.readyState===WebSocket.CONNECTING)||(w=!1,m("Connecting to",o),r=new WebSocket(o),r.addEventListener("open",()=>{a({type:"hello",role:"browser"}),m("Connected to relay — ready to receive run/status commands")}),r.addEventListener("message",W),r.addEventListener("close",t=>{if(r=null,t.reason==="Replaced by new browser"){S("Another browser instance connected — this instance will not reconnect");return}w||m("Disconnected",t.code?`(code ${t.code})`:"",t.reason||""),k()}),r.addEventListener("error",()=>{}))}function v(){w=!0,g&&(clearTimeout(g),g=null),r&&(r.close(1e3,"Client disconnecting"),r=null)}return{connect:E,disconnect:v,get connected(){return r!==null&&r.readyState===WebSocket.OPEN}}}exports.createBrowserClient=B;
1
+ var I=Object.create;var O=Object.defineProperty;var J=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var z=Object.getPrototypeOf,B=Object.prototype.hasOwnProperty;var j=(t,o,l,p)=>{if(o&&typeof o=="object"||typeof o=="function")for(let u of M(o))!B.call(t,u)&&u!==l&&O(t,u,{get:()=>o[u],enumerable:!(p=J(o,u))||p.enumerable});return t};var U=(t,o,l)=>(l=t!=null?I(z(t)):{},j(o||!t||!t.__esModule?O(l,"default",{value:t,enumerable:!0}):l,t));Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function x(t){return`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}${t}`}function g(t,o){if(!t.parent)return"";const l=o.get(t.parent);return l?l.name:""}function F(t){const o=t?.url??x(t?.path??"/__twd/ws"),l=t?.reconnect??!0,p=t?.reconnectInterval??2e3,u=t?.log??!1,C="[twd-relay]";function w(...n){u&&console.info(C,...n)}function N(...n){console.warn(C,...n)}let r=null,h=!1,S=null;function s(n){r&&r.readyState===WebSocket.OPEN&&r.send(JSON.stringify(n))}function f(){window.dispatchEvent(new CustomEvent("twd:state-change"))}async function R(n){const i=window.__TWD_STATE__;if(!i){N("TWD not initialized — make sure twd-js is loaded before running tests"),s({type:"error",code:"NO_TWD",message:"TWD not initialized"});return}const d=i.handlers;let c;if(n&&n.length>0){const e=n.map(m=>m.toLowerCase()),a=[];for(const[,m]of d)if(m.type==="test"){const y=m.name.toLowerCase();e.some(P=>y.includes(P))&&a.push(m.id)}if(a.length===0){const m=Array.from(d.values()).filter(y=>y.type==="test").map(y=>y.name);s({type:"run:start",testCount:0}),s({type:"error",code:"NO_MATCH",message:`No tests matched: ${JSON.stringify(n)}. Available tests: ${JSON.stringify(m)}`}),s({type:"run:complete",passed:0,failed:0,skipped:0,duration:0});return}c=a}s({type:"run:start",testCount:c?c.length:Array.from(d.values()).filter(e=>e.type==="test").length});let _=0,b=0,v=0;const T=performance.now(),L={onStart(e){e.status="running",f(),s({type:"test:start",id:e.id,name:e.name,suite:g(e,d)})},onPass(e){_++,e.status="pass",f(),s({type:"test:pass",id:e.id,name:e.name,suite:g(e,d),duration:performance.now()-T})},onFail(e,a){b++,e.status="fail",e.logs=[a.message],f(),s({type:"test:fail",id:e.id,name:e.name,suite:g(e,d),error:a.message,duration:performance.now()-T})},onSkip(e){v++,e.status="skip",f(),s({type:"test:skip",id:e.id,name:e.name,suite:g(e,d)})},onSuiteStart(e){e.status="running",f()},onSuiteEnd(e){e.status="idle",f()}};try{const{TestRunner:e}=await import("twd-js/runner"),a=new e(L);c?await a.runByIds(c):await a.runAll()}catch(e){const a=e instanceof Error?e.message:String(e);N("Runner error:",a),s({type:"error",code:"RUNNER_ERROR",message:a})}const $=performance.now()-T;s({type:"run:complete",passed:_,failed:b,skipped:v,duration:$}),f()}function W(){const n=window.__TWD_STATE__;if(!n){s({type:"error",code:"NO_TWD",message:"TWD not initialized"});return}const i=n.handlers,d=[];for(const[,c]of i)c.type==="test"&&d.push({id:c.id,name:c.name,suite:g(c,i),status:c.status??"idle"});s({type:"status:result",tests:d})}function k(n){let i;try{i=JSON.parse(n.data)}catch{return}i.type==="run"?(w("Received run command — running tests..."),R(Array.isArray(i.testNames)?i.testNames:void 0)):i.type==="status"&&W()}function A(){l&&!h&&(w(`Reconnecting in ${p}ms...`),S=setTimeout(()=>{E()},p))}function E(){r&&(r.readyState===WebSocket.OPEN||r.readyState===WebSocket.CONNECTING)||(h=!1,w("Connecting to",o),r=new WebSocket(o),r.addEventListener("open",()=>{s({type:"hello",role:"browser"}),w("Connected to relay — ready to receive run/status commands")}),r.addEventListener("message",k),r.addEventListener("close",n=>{if(r=null,n.reason==="Replaced by new browser"){N("Another browser instance connected — this instance will not reconnect");return}h||w("Disconnected",n.code?`(code ${n.code})`:"",n.reason||""),A()}),r.addEventListener("error",()=>{}))}function D(){h=!0,S&&(clearTimeout(S),S=null),r&&(r.close(1e3,"Client disconnecting"),r=null)}return{connect:E,disconnect:D,get connected(){return r!==null&&r.readyState===WebSocket.OPEN}}}exports.createBrowserClient=F;
@@ -1,141 +1,194 @@
1
- function O(s) {
2
- return `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}${s}`;
1
+ function I(i) {
2
+ return `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}${i}`;
3
3
  }
4
- function d(s, u) {
5
- if (!s.parent) return "";
6
- const f = u.get(s.parent);
7
- return f ? f.name : "";
4
+ function m(i, p) {
5
+ if (!i.parent) return "";
6
+ const w = p.get(i.parent);
7
+ return w ? w.name : "";
8
8
  }
9
- function A(s) {
10
- const u = s?.url ?? O(s?.path ?? "/__twd/ws"), f = s?.reconnect ?? !0, S = s?.reconnectInterval ?? 2e3, N = s?.log ?? !1, h = "[twd-relay]";
11
- function l(...n) {
12
- N && console.info(h, ...n);
9
+ function P(i) {
10
+ const p = i?.url ?? I(i?.path ?? "/__twd/ws"), w = i?.reconnect ?? !0, N = i?.reconnectInterval ?? 2e3, v = i?.log ?? !1, T = "[twd-relay]";
11
+ function u(...t) {
12
+ v && console.info(T, ...t);
13
13
  }
14
- function g(...n) {
15
- console.warn(h, ...n);
14
+ function h(...t) {
15
+ console.warn(T, ...t);
16
16
  }
17
- let t = null, p = !1, m = null;
18
- function o(n) {
19
- t && t.readyState === WebSocket.OPEN && t.send(JSON.stringify(n));
17
+ let n = null, y = !1, g = null;
18
+ function r(t) {
19
+ n && n.readyState === WebSocket.OPEN && n.send(JSON.stringify(t));
20
20
  }
21
- function i() {
21
+ function l() {
22
22
  window.dispatchEvent(new CustomEvent("twd:state-change"));
23
23
  }
24
- async function C() {
25
- const n = window.__TWD_STATE__;
26
- if (!n) {
27
- g("TWD not initialized — make sure twd-js is loaded before running tests"), o({ type: "error", code: "NO_TWD", message: "TWD not initialized" });
24
+ async function O(t) {
25
+ const o = window.__TWD_STATE__;
26
+ if (!o) {
27
+ h("TWD not initialized — make sure twd-js is loaded before running tests"), r({
28
+ type: "error",
29
+ code: "NO_TWD",
30
+ message: "TWD not initialized"
31
+ });
28
32
  return;
29
33
  }
30
- const r = n.handlers, w = Array.from(r.values()).filter((e) => e.type === "test").length;
31
- o({ type: "run:start", testCount: w });
32
- let a = 0, T = 0, _ = 0;
33
- const y = performance.now(), v = {
34
+ const c = o.handlers;
35
+ let a;
36
+ if (t && t.length > 0) {
37
+ const e = t.map((d) => d.toLowerCase()), s = [];
38
+ for (const [, d] of c) if (d.type === "test") {
39
+ const f = d.name.toLowerCase();
40
+ e.some(($) => f.includes($)) && s.push(d.id);
41
+ }
42
+ if (s.length === 0) {
43
+ const d = Array.from(c.values()).filter((f) => f.type === "test").map((f) => f.name);
44
+ r({
45
+ type: "run:start",
46
+ testCount: 0
47
+ }), r({
48
+ type: "error",
49
+ code: "NO_MATCH",
50
+ message: `No tests matched: ${JSON.stringify(t)}. Available tests: ${JSON.stringify(d)}`
51
+ }), r({
52
+ type: "run:complete",
53
+ passed: 0,
54
+ failed: 0,
55
+ skipped: 0,
56
+ duration: 0
57
+ });
58
+ return;
59
+ }
60
+ a = s;
61
+ }
62
+ r({
63
+ type: "run:start",
64
+ testCount: a ? a.length : Array.from(c.values()).filter((e) => e.type === "test").length
65
+ });
66
+ let E = 0, _ = 0, b = 0;
67
+ const S = performance.now(), D = {
34
68
  onStart(e) {
35
- e.status = "running", i(), o({
69
+ e.status = "running", l(), r({
36
70
  type: "test:start",
37
71
  id: e.id,
38
72
  name: e.name,
39
- suite: d(e, r)
73
+ suite: m(e, c)
40
74
  });
41
75
  },
42
76
  onPass(e) {
43
- a++, e.status = "pass", i(), o({
77
+ E++, e.status = "pass", l(), r({
44
78
  type: "test:pass",
45
79
  id: e.id,
46
80
  name: e.name,
47
- suite: d(e, r),
48
- duration: performance.now() - y
81
+ suite: m(e, c),
82
+ duration: performance.now() - S
49
83
  });
50
84
  },
51
- onFail(e, c) {
52
- T++, e.status = "fail", e.logs = [c.message], i(), o({
85
+ onFail(e, s) {
86
+ _++, e.status = "fail", e.logs = [s.message], l(), r({
53
87
  type: "test:fail",
54
88
  id: e.id,
55
89
  name: e.name,
56
- suite: d(e, r),
57
- error: c.message,
58
- duration: performance.now() - y
90
+ suite: m(e, c),
91
+ error: s.message,
92
+ duration: performance.now() - S
59
93
  });
60
94
  },
61
95
  onSkip(e) {
62
- _++, e.status = "skip", i(), o({
96
+ b++, e.status = "skip", l(), r({
63
97
  type: "test:skip",
64
98
  id: e.id,
65
99
  name: e.name,
66
- suite: d(e, r)
100
+ suite: m(e, c)
67
101
  });
68
102
  },
69
103
  onSuiteStart(e) {
70
- e.status = "running", i();
104
+ e.status = "running", l();
71
105
  },
72
106
  onSuiteEnd(e) {
73
- e.status = "idle", i();
107
+ e.status = "idle", l();
74
108
  }
75
109
  };
76
110
  try {
77
- const { TestRunner: e } = await import("twd-js/runner");
78
- await new e(v).runAll();
111
+ const { TestRunner: e } = await import("twd-js/runner"), s = new e(D);
112
+ a ? await s.runByIds(a) : await s.runAll();
79
113
  } catch (e) {
80
- const c = e instanceof Error ? e.message : String(e);
81
- g("Runner error:", c), o({ type: "error", code: "RUNNER_ERROR", message: c });
114
+ const s = e instanceof Error ? e.message : String(e);
115
+ h("Runner error:", s), r({
116
+ type: "error",
117
+ code: "RUNNER_ERROR",
118
+ message: s
119
+ });
82
120
  }
83
- const D = performance.now() - y;
84
- o({ type: "run:complete", passed: a, failed: T, skipped: _, duration: D }), i();
121
+ const L = performance.now() - S;
122
+ r({
123
+ type: "run:complete",
124
+ passed: E,
125
+ failed: _,
126
+ skipped: b,
127
+ duration: L
128
+ }), l();
85
129
  }
86
130
  function R() {
87
- const n = window.__TWD_STATE__;
88
- if (!n) {
89
- o({ type: "error", code: "NO_TWD", message: "TWD not initialized" });
131
+ const t = window.__TWD_STATE__;
132
+ if (!t) {
133
+ r({
134
+ type: "error",
135
+ code: "NO_TWD",
136
+ message: "TWD not initialized"
137
+ });
90
138
  return;
91
139
  }
92
- const r = n.handlers, w = [];
93
- for (const [, a] of r)
94
- a.type === "test" && w.push({
95
- id: a.id,
96
- name: a.name,
97
- suite: d(a, r),
98
- status: a.status ?? "idle"
99
- });
100
- o({ type: "status:result", tests: w });
140
+ const o = t.handlers, c = [];
141
+ for (const [, a] of o) a.type === "test" && c.push({
142
+ id: a.id,
143
+ name: a.name,
144
+ suite: m(a, o),
145
+ status: a.status ?? "idle"
146
+ });
147
+ r({
148
+ type: "status:result",
149
+ tests: c
150
+ });
101
151
  }
102
- function W(n) {
103
- let r;
152
+ function W(t) {
153
+ let o;
104
154
  try {
105
- r = JSON.parse(n.data);
155
+ o = JSON.parse(t.data);
106
156
  } catch {
107
157
  return;
108
158
  }
109
- r.type === "run" ? (l("Received run command — running tests..."), C()) : r.type === "status" && R();
159
+ o.type === "run" ? (u("Received run command — running tests..."), O(Array.isArray(o.testNames) ? o.testNames : void 0)) : o.type === "status" && R();
110
160
  }
111
- function b() {
112
- f && !p && (l(`Reconnecting in ${S}ms...`), m = setTimeout(() => {
113
- E();
114
- }, S));
161
+ function k() {
162
+ w && !y && (u(`Reconnecting in ${N}ms...`), g = setTimeout(() => {
163
+ C();
164
+ }, N));
115
165
  }
116
- function E() {
117
- t && (t.readyState === WebSocket.OPEN || t.readyState === WebSocket.CONNECTING) || (p = !1, l("Connecting to", u), t = new WebSocket(u), t.addEventListener("open", () => {
118
- o({ type: "hello", role: "browser" }), l("Connected to relay — ready to receive run/status commands");
119
- }), t.addEventListener("message", W), t.addEventListener("close", (n) => {
120
- if (t = null, n.reason === "Replaced by new browser") {
121
- g("Another browser instance connected this instance will not reconnect");
166
+ function C() {
167
+ n && (n.readyState === WebSocket.OPEN || n.readyState === WebSocket.CONNECTING) || (y = !1, u("Connecting to", p), n = new WebSocket(p), n.addEventListener("open", () => {
168
+ r({
169
+ type: "hello",
170
+ role: "browser"
171
+ }), u("Connected to relayready to receive run/status commands");
172
+ }), n.addEventListener("message", W), n.addEventListener("close", (t) => {
173
+ if (n = null, t.reason === "Replaced by new browser") {
174
+ h("Another browser instance connected — this instance will not reconnect");
122
175
  return;
123
176
  }
124
- p || l("Disconnected", n.code ? `(code ${n.code})` : "", n.reason || ""), b();
125
- }), t.addEventListener("error", () => {
177
+ y || u("Disconnected", t.code ? `(code ${t.code})` : "", t.reason || ""), k();
178
+ }), n.addEventListener("error", () => {
126
179
  }));
127
180
  }
128
- function k() {
129
- p = !0, m && (clearTimeout(m), m = null), t && (t.close(1e3, "Client disconnecting"), t = null);
181
+ function A() {
182
+ y = !0, g && (clearTimeout(g), g = null), n && (n.close(1e3, "Client disconnecting"), n = null);
130
183
  }
131
184
  return {
132
- connect: E,
133
- disconnect: k,
185
+ connect: C,
186
+ disconnect: A,
134
187
  get connected() {
135
- return t !== null && t.readyState === WebSocket.OPEN;
188
+ return n !== null && n.readyState === WebSocket.OPEN;
136
189
  }
137
190
  };
138
191
  }
139
192
  export {
140
- A as createBrowserClient
193
+ P as createBrowserClient
141
194
  };
package/dist/cli.js CHANGED
@@ -1,259 +1,261 @@
1
1
  #!/usr/bin/env node
2
2
  import { createServer } from "http";
3
- import WebSocket$1, { WebSocketServer, WebSocket } from "ws";
4
- const DEFAULT_PATH = "/__twd/ws";
3
+ import WebSocket, { WebSocket as WebSocket$1, WebSocketServer } from "ws";
4
+ //#region src/relay/createTwdRelay.ts
5
+ var DEFAULT_PATH = "/__twd/ws";
5
6
  function createTwdRelay(server, options) {
6
- const path = options?.path ?? DEFAULT_PATH;
7
- const onError = options?.onError;
8
- const wss = new WebSocketServer({ noServer: true });
9
- let browser = null;
10
- const clients = /* @__PURE__ */ new Set();
11
- let runInProgress = false;
12
- function sendError(ws, code, message) {
13
- const msg = { type: "error", code, message };
14
- if (ws.readyState === WebSocket.OPEN) {
15
- ws.send(JSON.stringify(msg));
16
- }
17
- }
18
- function broadcastToClients(data) {
19
- for (const client of clients) {
20
- if (client.readyState === WebSocket.OPEN) {
21
- client.send(data);
22
- }
23
- }
24
- }
25
- function sendConnectedStatus(ws) {
26
- const msg = { type: "connected", browser: browser !== null && browser.readyState === WebSocket.OPEN };
27
- if (ws.readyState === WebSocket.OPEN) {
28
- ws.send(JSON.stringify(msg));
29
- }
30
- }
31
- function notifyBrowserDisconnected() {
32
- broadcastToClients(JSON.stringify({ type: "connected", browser: false }));
33
- }
34
- function handleBrowserMessage(data) {
35
- let parsed;
36
- try {
37
- parsed = JSON.parse(data);
38
- } catch {
39
- return;
40
- }
41
- if (parsed.type === "run:complete") {
42
- runInProgress = false;
43
- }
44
- broadcastToClients(data);
45
- }
46
- function handleClientMessage(ws, data) {
47
- let parsed;
48
- try {
49
- parsed = JSON.parse(data);
50
- } catch {
51
- sendError(ws, "INVALID_MESSAGE", "Invalid JSON");
52
- return;
53
- }
54
- if (!parsed.type) {
55
- sendError(ws, "INVALID_MESSAGE", 'Missing "type" field');
56
- return;
57
- }
58
- if (parsed.type === "run") {
59
- if (!browser || browser.readyState !== WebSocket.OPEN) {
60
- sendError(ws, "NO_BROWSER", "No browser connected");
61
- return;
62
- }
63
- if (runInProgress) {
64
- sendError(ws, "RUN_IN_PROGRESS", "A test run is already in progress");
65
- return;
66
- }
67
- runInProgress = true;
68
- browser.send(data);
69
- return;
70
- }
71
- if (parsed.type === "status") {
72
- if (!browser || browser.readyState !== WebSocket.OPEN) {
73
- sendError(ws, "NO_BROWSER", "No browser connected");
74
- return;
75
- }
76
- browser.send(data);
77
- return;
78
- }
79
- sendError(ws, "UNKNOWN_COMMAND", `Unknown command: ${parsed.type}`);
80
- }
81
- function handleConnection(ws) {
82
- let identified = false;
83
- const identifyHandler = (raw) => {
84
- const data = typeof raw === "string" ? raw : raw.toString();
85
- if (!identified) {
86
- let parsed;
87
- try {
88
- parsed = JSON.parse(data);
89
- } catch {
90
- sendError(ws, "INVALID_MESSAGE", "Invalid JSON");
91
- return;
92
- }
93
- if (parsed.type !== "hello" || parsed.role !== "browser" && parsed.role !== "client") {
94
- sendError(ws, "INVALID_MESSAGE", 'First message must be a hello with role "browser" or "client"');
95
- return;
96
- }
97
- identified = true;
98
- if (parsed.role === "browser") {
99
- if (browser && browser.readyState === WebSocket.OPEN) {
100
- browser.close(1e3, "Replaced by new browser");
101
- }
102
- browser = ws;
103
- runInProgress = false;
104
- ws.on("close", () => {
105
- if (browser === ws) {
106
- browser = null;
107
- runInProgress = false;
108
- notifyBrowserDisconnected();
109
- }
110
- });
111
- broadcastToClients(JSON.stringify({ type: "connected", browser: true }));
112
- ws.on("message", (raw2) => {
113
- const msg = typeof raw2 === "string" ? raw2 : raw2.toString();
114
- handleBrowserMessage(msg);
115
- });
116
- } else {
117
- clients.add(ws);
118
- ws.on("close", () => {
119
- clients.delete(ws);
120
- });
121
- sendConnectedStatus(ws);
122
- ws.on("message", (raw2) => {
123
- const msg = typeof raw2 === "string" ? raw2 : raw2.toString();
124
- handleClientMessage(ws, msg);
125
- });
126
- }
127
- ws.removeListener("message", identifyHandler);
128
- }
129
- };
130
- ws.on("message", identifyHandler);
131
- ws.on("error", (err) => {
132
- if (onError) onError(err);
133
- });
134
- }
135
- const upgradeHandler = (request, socket, head) => {
136
- const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
137
- if (url.pathname === path) {
138
- wss.handleUpgrade(request, socket, head, (ws) => {
139
- handleConnection(ws);
140
- });
141
- }
142
- };
143
- server.on("upgrade", upgradeHandler);
144
- wss.on("error", (err) => {
145
- if (onError) onError(err);
146
- });
147
- return {
148
- close() {
149
- server.removeListener("upgrade", upgradeHandler);
150
- for (const client of clients) {
151
- client.close(1e3, "Relay shutting down");
152
- }
153
- clients.clear();
154
- if (browser && browser.readyState === WebSocket.OPEN) {
155
- browser.close(1e3, "Relay shutting down");
156
- }
157
- browser = null;
158
- runInProgress = false;
159
- wss.close();
160
- },
161
- get browserConnected() {
162
- return browser !== null && browser.readyState === WebSocket.OPEN;
163
- },
164
- get clientCount() {
165
- return clients.size;
166
- }
167
- };
7
+ const path = options?.path ?? DEFAULT_PATH;
8
+ const onError = options?.onError;
9
+ const wss = new WebSocketServer({ noServer: true });
10
+ let browser = null;
11
+ const clients = /* @__PURE__ */ new Set();
12
+ let runInProgress = false;
13
+ function sendError(ws, code, message) {
14
+ const msg = {
15
+ type: "error",
16
+ code,
17
+ message
18
+ };
19
+ if (ws.readyState === WebSocket$1.OPEN) ws.send(JSON.stringify(msg));
20
+ }
21
+ function broadcastToClients(data) {
22
+ for (const client of clients) if (client.readyState === WebSocket$1.OPEN) client.send(data);
23
+ }
24
+ function sendConnectedStatus(ws) {
25
+ const msg = {
26
+ type: "connected",
27
+ browser: browser !== null && browser.readyState === WebSocket$1.OPEN
28
+ };
29
+ if (ws.readyState === WebSocket$1.OPEN) ws.send(JSON.stringify(msg));
30
+ }
31
+ function notifyBrowserDisconnected() {
32
+ broadcastToClients(JSON.stringify({
33
+ type: "connected",
34
+ browser: false
35
+ }));
36
+ }
37
+ function handleBrowserMessage(data) {
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(data);
41
+ } catch {
42
+ return;
43
+ }
44
+ if (parsed.type === "run:complete") runInProgress = false;
45
+ broadcastToClients(data);
46
+ }
47
+ function handleClientMessage(ws, data) {
48
+ let parsed;
49
+ try {
50
+ parsed = JSON.parse(data);
51
+ } catch {
52
+ sendError(ws, "INVALID_MESSAGE", "Invalid JSON");
53
+ return;
54
+ }
55
+ if (!parsed.type) {
56
+ sendError(ws, "INVALID_MESSAGE", "Missing \"type\" field");
57
+ return;
58
+ }
59
+ if (parsed.type === "run") {
60
+ if (!browser || browser.readyState !== WebSocket$1.OPEN) {
61
+ sendError(ws, "NO_BROWSER", "No browser connected");
62
+ return;
63
+ }
64
+ if (runInProgress) {
65
+ sendError(ws, "RUN_IN_PROGRESS", "A test run is already in progress");
66
+ return;
67
+ }
68
+ runInProgress = true;
69
+ browser.send(data);
70
+ return;
71
+ }
72
+ if (parsed.type === "status") {
73
+ if (!browser || browser.readyState !== WebSocket$1.OPEN) {
74
+ sendError(ws, "NO_BROWSER", "No browser connected");
75
+ return;
76
+ }
77
+ browser.send(data);
78
+ return;
79
+ }
80
+ sendError(ws, "UNKNOWN_COMMAND", `Unknown command: ${parsed.type}`);
81
+ }
82
+ function handleConnection(ws) {
83
+ let identified = false;
84
+ const identifyHandler = (raw) => {
85
+ const data = typeof raw === "string" ? raw : raw.toString();
86
+ if (!identified) {
87
+ let parsed;
88
+ try {
89
+ parsed = JSON.parse(data);
90
+ } catch {
91
+ sendError(ws, "INVALID_MESSAGE", "Invalid JSON");
92
+ return;
93
+ }
94
+ if (parsed.type !== "hello" || parsed.role !== "browser" && parsed.role !== "client") {
95
+ sendError(ws, "INVALID_MESSAGE", "First message must be a hello with role \"browser\" or \"client\"");
96
+ return;
97
+ }
98
+ identified = true;
99
+ if (parsed.role === "browser") {
100
+ if (browser && browser.readyState === WebSocket$1.OPEN) browser.close(1e3, "Replaced by new browser");
101
+ browser = ws;
102
+ runInProgress = false;
103
+ ws.on("close", () => {
104
+ if (browser === ws) {
105
+ browser = null;
106
+ runInProgress = false;
107
+ notifyBrowserDisconnected();
108
+ }
109
+ });
110
+ broadcastToClients(JSON.stringify({
111
+ type: "connected",
112
+ browser: true
113
+ }));
114
+ ws.on("message", (raw) => {
115
+ handleBrowserMessage(typeof raw === "string" ? raw : raw.toString());
116
+ });
117
+ } else {
118
+ clients.add(ws);
119
+ ws.on("close", () => {
120
+ clients.delete(ws);
121
+ });
122
+ sendConnectedStatus(ws);
123
+ ws.on("message", (raw) => {
124
+ handleClientMessage(ws, typeof raw === "string" ? raw : raw.toString());
125
+ });
126
+ }
127
+ ws.removeListener("message", identifyHandler);
128
+ }
129
+ };
130
+ ws.on("message", identifyHandler);
131
+ ws.on("error", (err) => {
132
+ if (onError) onError(err);
133
+ });
134
+ }
135
+ const upgradeHandler = (request, socket, head) => {
136
+ if (new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`).pathname === path) wss.handleUpgrade(request, socket, head, (ws) => {
137
+ handleConnection(ws);
138
+ });
139
+ };
140
+ server.on("upgrade", upgradeHandler);
141
+ wss.on("error", (err) => {
142
+ if (onError) onError(err);
143
+ });
144
+ return {
145
+ close() {
146
+ server.removeListener("upgrade", upgradeHandler);
147
+ for (const client of clients) client.close(1e3, "Relay shutting down");
148
+ clients.clear();
149
+ if (browser && browser.readyState === WebSocket$1.OPEN) browser.close(1e3, "Relay shutting down");
150
+ browser = null;
151
+ runInProgress = false;
152
+ wss.close();
153
+ },
154
+ get browserConnected() {
155
+ return browser !== null && browser.readyState === WebSocket$1.OPEN;
156
+ },
157
+ get clientCount() {
158
+ return clients.size;
159
+ }
160
+ };
168
161
  }
162
+ //#endregion
163
+ //#region src/cli/run.ts
169
164
  function run(options) {
170
- const { port, timeout, path, host } = options;
171
- const url = `ws://${host}:${port}${path}`;
172
- console.log(`Connecting to ${url}...`);
173
- const ws = new WebSocket$1(url);
174
- let runSent = false;
175
- let runComplete = false;
176
- let failed = false;
177
- const timer = setTimeout(() => {
178
- console.error(`
179
- Timeout: no run:complete received within ${timeout / 1e3}s`);
180
- ws.close();
181
- process.exit(1);
182
- }, timeout);
183
- ws.on("open", () => {
184
- ws.send(JSON.stringify({ type: "hello", role: "client" }));
185
- });
186
- ws.on("message", (data) => {
187
- const msg = JSON.parse(data.toString());
188
- switch (msg.type) {
189
- case "connected":
190
- if (msg.browser && !runSent) {
191
- runSent = true;
192
- console.log("Browser connected, triggering test run...\n");
193
- ws.send(JSON.stringify({ type: "run", scope: "all" }));
194
- } else if (!msg.browser) {
195
- console.log("Waiting for browser to connect...");
196
- }
197
- break;
198
- case "run:start":
199
- console.log(`Running ${msg.testCount} test(s)...
200
- `);
201
- break;
202
- case "test:start":
203
- console.log(` RUN: ${msg.suite} > ${msg.name}`);
204
- break;
205
- case "test:pass":
206
- console.log(` PASS: ${msg.suite} > ${msg.name} (${msg.duration}ms)`);
207
- break;
208
- case "test:fail":
209
- failed = true;
210
- console.log(` FAIL: ${msg.suite} > ${msg.name} (${msg.duration}ms)`);
211
- if (msg.error) {
212
- console.log(` Error: ${msg.error}`);
213
- }
214
- break;
215
- case "test:skip":
216
- console.log(` SKIP: ${msg.suite} > ${msg.name}`);
217
- break;
218
- case "run:complete": {
219
- const duration = (msg.duration / 1e3).toFixed(1);
220
- console.log(`
221
- --- Run complete ---`);
222
- console.log(`Passed: ${msg.passed} | Failed: ${msg.failed} | Skipped: ${msg.skipped}`);
223
- console.log(`Duration: ${duration}s`);
224
- runComplete = true;
225
- clearTimeout(timer);
226
- ws.close();
227
- process.exit(failed || msg.failed > 0 ? 1 : 0);
228
- break;
229
- }
230
- case "error":
231
- console.error(`Error [${msg.code}]: ${msg.message}`);
232
- break;
233
- }
234
- });
235
- ws.on("close", () => {
236
- clearTimeout(timer);
237
- if (!runComplete) {
238
- console.error("Connection closed before run completed");
239
- process.exit(1);
240
- }
241
- });
242
- ws.on("error", (err) => {
243
- clearTimeout(timer);
244
- console.error(`Connection error: ${err.message}`);
245
- process.exit(1);
246
- });
165
+ const { port, timeout, path, host, testNames } = options;
166
+ const url = `ws://${host}:${port}${path}`;
167
+ console.log(`Connecting to ${url}...`);
168
+ const ws = new WebSocket(url);
169
+ let runSent = false;
170
+ let runComplete = false;
171
+ let failed = false;
172
+ const timer = setTimeout(() => {
173
+ console.error(`\nTimeout: no run:complete received within ${timeout / 1e3}s`);
174
+ ws.close();
175
+ process.exit(1);
176
+ }, timeout);
177
+ ws.on("open", () => {
178
+ ws.send(JSON.stringify({
179
+ type: "hello",
180
+ role: "client"
181
+ }));
182
+ });
183
+ ws.on("message", (data) => {
184
+ const msg = JSON.parse(data.toString());
185
+ switch (msg.type) {
186
+ case "connected":
187
+ if (msg.browser && !runSent) {
188
+ runSent = true;
189
+ console.log("Browser connected, triggering test run...\n");
190
+ const runMsg = {
191
+ type: "run",
192
+ scope: "all"
193
+ };
194
+ if (testNames?.length) runMsg.testNames = testNames;
195
+ ws.send(JSON.stringify(runMsg));
196
+ } else if (!msg.browser) console.log("Waiting for browser to connect...");
197
+ break;
198
+ case "run:start":
199
+ console.log(`Running ${msg.testCount} test(s)...\n`);
200
+ break;
201
+ case "test:start":
202
+ console.log(` RUN: ${msg.suite} > ${msg.name}`);
203
+ break;
204
+ case "test:pass":
205
+ console.log(` PASS: ${msg.suite} > ${msg.name} (${msg.duration}ms)`);
206
+ break;
207
+ case "test:fail":
208
+ failed = true;
209
+ console.log(` FAIL: ${msg.suite} > ${msg.name} (${msg.duration}ms)`);
210
+ if (msg.error) console.log(` Error: ${msg.error}`);
211
+ break;
212
+ case "test:skip":
213
+ console.log(` SKIP: ${msg.suite} > ${msg.name}`);
214
+ break;
215
+ case "run:complete": {
216
+ const duration = (msg.duration / 1e3).toFixed(1);
217
+ console.log(`\n--- Run complete ---`);
218
+ console.log(`Passed: ${msg.passed} | Failed: ${msg.failed} | Skipped: ${msg.skipped}`);
219
+ console.log(`Duration: ${duration}s`);
220
+ runComplete = true;
221
+ clearTimeout(timer);
222
+ ws.close();
223
+ process.exit(failed || msg.failed > 0 ? 1 : 0);
224
+ break;
225
+ }
226
+ case "error":
227
+ console.error(`Error [${msg.code}]: ${msg.message}`);
228
+ break;
229
+ }
230
+ });
231
+ ws.on("close", () => {
232
+ clearTimeout(timer);
233
+ if (!runComplete) {
234
+ console.error("Connection closed before run completed");
235
+ process.exit(1);
236
+ }
237
+ });
238
+ ws.on("error", (err) => {
239
+ clearTimeout(timer);
240
+ console.error(`Connection error: ${err.message}`);
241
+ process.exit(1);
242
+ });
247
243
  }
248
- const args = process.argv.slice(2);
249
- const subcommand = args.find((a) => !a.startsWith("--"));
244
+ //#endregion
245
+ //#region src/cli/standalone.ts
246
+ var args = process.argv.slice(2);
247
+ var subcommand = args.find((a) => !a.startsWith("--"));
250
248
  function parseFlag(name) {
251
- const idx = args.indexOf(name);
252
- if (idx !== -1 && args[idx + 1]) return args[idx + 1];
253
- return void 0;
249
+ const idx = args.indexOf(name);
250
+ if (idx !== -1 && args[idx + 1]) return args[idx + 1];
251
+ }
252
+ function parseFlagAll(name) {
253
+ const values = [];
254
+ for (let i = 0; i < args.length; i++) if (args[i] === name && args[i + 1] && !args[i + 1].startsWith("--")) values.push(args[i + 1]);
255
+ return values;
254
256
  }
255
257
  function printHelp() {
256
- console.log(`Usage: twd-relay [command] [options]
258
+ console.log(`Usage: twd-relay [command] [options]
257
259
 
258
260
  Commands:
259
261
  (none), serve Start the standalone relay server (default)
@@ -268,6 +270,7 @@ Options for run:
268
270
  --host <host> Relay host to connect to (default: localhost)
269
271
  --path <path> WebSocket path (default: /__twd/ws)
270
272
  --timeout <ms> Timeout in ms (default: 180000)
273
+ --test <name> Filter tests by name substring (repeatable)
271
274
 
272
275
  Examples:
273
276
  twd-relay # start relay on port 9876
@@ -275,61 +278,71 @@ Examples:
275
278
  twd-relay run # trigger run via Vite dev server on 5173
276
279
  twd-relay run --port 9876 # trigger run on custom port
277
280
  twd-relay run --host 192.168.1.10 --path /app/__twd/ws
278
- twd-relay run --timeout 30000 # custom timeout`);
281
+ twd-relay run --timeout 30000 # custom timeout
282
+ twd-relay run --test "login" # run tests matching "login"
283
+ twd-relay run --test "login" --test "signup" # run multiple`);
279
284
  }
280
285
  if (args.includes("--help") || args.includes("-h")) {
281
- printHelp();
282
- process.exit(0);
286
+ printHelp();
287
+ process.exit(0);
283
288
  }
284
289
  if (subcommand === "run") {
285
- const portStr = parseFlag("--port");
286
- const timeoutStr = parseFlag("--timeout");
287
- const pathFlag = parseFlag("--path") ?? "/__twd/ws";
288
- const hostFlag = parseFlag("--host") ?? "localhost";
289
- const port = portStr ? parseInt(portStr, 10) : 5173;
290
- if (isNaN(port)) {
291
- console.error("Invalid port number:", portStr);
292
- process.exit(1);
293
- }
294
- const timeout = timeoutStr ? parseInt(timeoutStr, 10) : 18e4;
295
- if (isNaN(timeout)) {
296
- console.error("Invalid timeout value:", timeoutStr);
297
- process.exit(1);
298
- }
299
- run({ port, timeout, path: pathFlag, host: hostFlag });
290
+ const portStr = parseFlag("--port");
291
+ const timeoutStr = parseFlag("--timeout");
292
+ const pathFlag = parseFlag("--path") ?? "/__twd/ws";
293
+ const hostFlag = parseFlag("--host") ?? "localhost";
294
+ const port = portStr ? parseInt(portStr, 10) : 5173;
295
+ if (isNaN(port)) {
296
+ console.error("Invalid port number:", portStr);
297
+ process.exit(1);
298
+ }
299
+ const timeout = timeoutStr ? parseInt(timeoutStr, 10) : 18e4;
300
+ if (isNaN(timeout)) {
301
+ console.error("Invalid timeout value:", timeoutStr);
302
+ process.exit(1);
303
+ }
304
+ const testNames = parseFlagAll("--test");
305
+ run({
306
+ port,
307
+ timeout,
308
+ path: pathFlag,
309
+ host: hostFlag,
310
+ testNames: testNames.length > 0 ? testNames : void 0
311
+ });
300
312
  } else if (!subcommand || subcommand === "serve") {
301
- let shutdown = function() {
302
- console.log("\nShutting down...");
303
- relay.close();
304
- server.close(() => {
305
- process.exit(0);
306
- });
307
- };
308
- const portStr = parseFlag("--port");
309
- const pathFlag = parseFlag("--path") ?? "/__twd/ws";
310
- let port = 9876;
311
- if (portStr) {
312
- port = parseInt(portStr, 10);
313
- if (isNaN(port)) {
314
- console.error("Invalid port number:", portStr);
315
- process.exit(1);
316
- }
317
- }
318
- const server = createServer();
319
- const relay = createTwdRelay(server, {
320
- path: pathFlag,
321
- onError(err) {
322
- console.error("[twd-relay] Error:", err.message);
323
- }
324
- });
325
- server.listen(port, () => {
326
- console.log(`TWD Relay running on ws://localhost:${port}${pathFlag}`);
327
- console.log("Waiting for connections...");
328
- });
329
- process.on("SIGINT", shutdown);
330
- process.on("SIGTERM", shutdown);
313
+ const portStr = parseFlag("--port");
314
+ const pathFlag = parseFlag("--path") ?? "/__twd/ws";
315
+ let port = 9876;
316
+ if (portStr) {
317
+ port = parseInt(portStr, 10);
318
+ if (isNaN(port)) {
319
+ console.error("Invalid port number:", portStr);
320
+ process.exit(1);
321
+ }
322
+ }
323
+ const server = createServer();
324
+ const relay = createTwdRelay(server, {
325
+ path: pathFlag,
326
+ onError(err) {
327
+ console.error("[twd-relay] Error:", err.message);
328
+ }
329
+ });
330
+ server.listen(port, () => {
331
+ console.log(`TWD Relay running on ws://localhost:${port}${pathFlag}`);
332
+ console.log("Waiting for connections...");
333
+ });
334
+ function shutdown() {
335
+ console.log("\nShutting down...");
336
+ relay.close();
337
+ server.close(() => {
338
+ process.exit(0);
339
+ });
340
+ }
341
+ process.on("SIGINT", shutdown);
342
+ process.on("SIGTERM", shutdown);
331
343
  } else {
332
- console.error(`Unknown command: ${subcommand}`);
333
- printHelp();
334
- process.exit(1);
344
+ console.error(`Unknown command: ${subcommand}`);
345
+ printHelp();
346
+ process.exit(1);
335
347
  }
348
+ //#endregion
package/dist/index.cjs.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./createTwdRelay-VRmjHboP.cjs");exports.createTwdRelay=e.c;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./relay-CpUORBd7.cjs");exports.createTwdRelay=e.createTwdRelay;
package/dist/index.d.ts CHANGED
@@ -28,6 +28,7 @@ export declare type InboundMessage = HelloMessage | Command | BrowserEvent;
28
28
  export declare interface RunCommand {
29
29
  type: 'run';
30
30
  scope: 'all';
31
+ testNames?: string[];
31
32
  }
32
33
 
33
34
  export declare interface RunCompleteEvent {
@@ -78,7 +79,7 @@ export declare interface TestStartEvent {
78
79
  suite: string;
79
80
  }
80
81
 
81
- export declare type TwdErrorCode = 'NO_BROWSER' | 'RUN_IN_PROGRESS' | 'UNKNOWN_COMMAND' | 'INVALID_MESSAGE';
82
+ export declare type TwdErrorCode = 'NO_BROWSER' | 'RUN_IN_PROGRESS' | 'UNKNOWN_COMMAND' | 'INVALID_MESSAGE' | 'NO_MATCH';
82
83
 
83
84
  export declare interface TwdErrorMessage {
84
85
  type: 'error';
package/dist/index.es.js CHANGED
@@ -1,4 +1,4 @@
1
- import { c as r } from "./createTwdRelay-22etCW_8.js";
1
+ import { t as r } from "./relay-C8UTSEbG.js";
2
2
  export {
3
3
  r as createTwdRelay
4
4
  };
@@ -0,0 +1,132 @@
1
+ import { WebSocket as s, WebSocketServer as I } from "ws";
2
+ var P = "/__twd/ws";
3
+ function _(y, p) {
4
+ const g = p?.path ?? P, f = p?.onError, u = new I({ noServer: !0 });
5
+ let t = null;
6
+ const a = /* @__PURE__ */ new Set();
7
+ let c = !1;
8
+ function i(e, n, r) {
9
+ const o = {
10
+ type: "error",
11
+ code: n,
12
+ message: r
13
+ };
14
+ e.readyState === s.OPEN && e.send(JSON.stringify(o));
15
+ }
16
+ function S(e) {
17
+ for (const n of a) n.readyState === s.OPEN && n.send(e);
18
+ }
19
+ function O(e) {
20
+ const n = {
21
+ type: "connected",
22
+ browser: t !== null && t.readyState === s.OPEN
23
+ };
24
+ e.readyState === s.OPEN && e.send(JSON.stringify(n));
25
+ }
26
+ function E() {
27
+ S(JSON.stringify({
28
+ type: "connected",
29
+ browser: !1
30
+ }));
31
+ }
32
+ function h(e) {
33
+ let n;
34
+ try {
35
+ n = JSON.parse(e);
36
+ } catch {
37
+ return;
38
+ }
39
+ n.type === "run:complete" && (c = !1), S(e);
40
+ }
41
+ function b(e, n) {
42
+ let r;
43
+ try {
44
+ r = JSON.parse(n);
45
+ } catch {
46
+ i(e, "INVALID_MESSAGE", "Invalid JSON");
47
+ return;
48
+ }
49
+ if (!r.type) {
50
+ i(e, "INVALID_MESSAGE", 'Missing "type" field');
51
+ return;
52
+ }
53
+ if (r.type === "run") {
54
+ if (!t || t.readyState !== s.OPEN) {
55
+ i(e, "NO_BROWSER", "No browser connected");
56
+ return;
57
+ }
58
+ if (c) {
59
+ i(e, "RUN_IN_PROGRESS", "A test run is already in progress");
60
+ return;
61
+ }
62
+ c = !0, t.send(n);
63
+ return;
64
+ }
65
+ if (r.type === "status") {
66
+ if (!t || t.readyState !== s.OPEN) {
67
+ i(e, "NO_BROWSER", "No browser connected");
68
+ return;
69
+ }
70
+ t.send(n);
71
+ return;
72
+ }
73
+ i(e, "UNKNOWN_COMMAND", `Unknown command: ${r.type}`);
74
+ }
75
+ function m(e) {
76
+ let n = !1;
77
+ const r = (o) => {
78
+ const A = typeof o == "string" ? o : o.toString();
79
+ if (!n) {
80
+ let d;
81
+ try {
82
+ d = JSON.parse(A);
83
+ } catch {
84
+ i(e, "INVALID_MESSAGE", "Invalid JSON");
85
+ return;
86
+ }
87
+ if (d.type !== "hello" || d.role !== "browser" && d.role !== "client") {
88
+ i(e, "INVALID_MESSAGE", 'First message must be a hello with role "browser" or "client"');
89
+ return;
90
+ }
91
+ n = !0, d.role === "browser" ? (t && t.readyState === s.OPEN && t.close(1e3, "Replaced by new browser"), t = e, c = !1, e.on("close", () => {
92
+ t === e && (t = null, c = !1, E());
93
+ }), S(JSON.stringify({
94
+ type: "connected",
95
+ browser: !0
96
+ })), e.on("message", (l) => {
97
+ h(typeof l == "string" ? l : l.toString());
98
+ })) : (a.add(e), e.on("close", () => {
99
+ a.delete(e);
100
+ }), O(e), e.on("message", (l) => {
101
+ b(e, typeof l == "string" ? l : l.toString());
102
+ })), e.removeListener("message", r);
103
+ }
104
+ };
105
+ e.on("message", r), e.on("error", (o) => {
106
+ f && f(o);
107
+ });
108
+ }
109
+ const N = (e, n, r) => {
110
+ new URL(e.url ?? "/", `http://${e.headers.host ?? "localhost"}`).pathname === g && u.handleUpgrade(e, n, r, (o) => {
111
+ m(o);
112
+ });
113
+ };
114
+ return y.on("upgrade", N), u.on("error", (e) => {
115
+ f && f(e);
116
+ }), {
117
+ close() {
118
+ y.removeListener("upgrade", N);
119
+ for (const e of a) e.close(1e3, "Relay shutting down");
120
+ a.clear(), t && t.readyState === s.OPEN && t.close(1e3, "Relay shutting down"), t = null, c = !1, u.close();
121
+ },
122
+ get browserConnected() {
123
+ return t !== null && t.readyState === s.OPEN;
124
+ },
125
+ get clientCount() {
126
+ return a.size;
127
+ }
128
+ };
129
+ }
130
+ export {
131
+ _ as t
132
+ };
@@ -0,0 +1 @@
1
+ let o=require("ws");var R="/__twd/ws";function W(y,N){const p=N?.path??R,f=N?.onError,u=new o.WebSocketServer({noServer:!0});let t=null;const l=new Set;let a=!1;function c(e,n,r){const s={type:"error",code:n,message:r};e.readyState===o.WebSocket.OPEN&&e.send(JSON.stringify(s))}function S(e){for(const n of l)n.readyState===o.WebSocket.OPEN&&n.send(e)}function b(e){const n={type:"connected",browser:t!==null&&t.readyState===o.WebSocket.OPEN};e.readyState===o.WebSocket.OPEN&&e.send(JSON.stringify(n))}function O(){S(JSON.stringify({type:"connected",browser:!1}))}function E(e){let n;try{n=JSON.parse(e)}catch{return}n.type==="run:complete"&&(a=!1),S(e)}function h(e,n){let r;try{r=JSON.parse(n)}catch{c(e,"INVALID_MESSAGE","Invalid JSON");return}if(!r.type){c(e,"INVALID_MESSAGE",'Missing "type" field');return}if(r.type==="run"){if(!t||t.readyState!==o.WebSocket.OPEN){c(e,"NO_BROWSER","No browser connected");return}if(a){c(e,"RUN_IN_PROGRESS","A test run is already in progress");return}a=!0,t.send(n);return}if(r.type==="status"){if(!t||t.readyState!==o.WebSocket.OPEN){c(e,"NO_BROWSER","No browser connected");return}t.send(n);return}c(e,"UNKNOWN_COMMAND",`Unknown command: ${r.type}`)}function m(e){let n=!1;const r=s=>{const P=typeof s=="string"?s:s.toString();if(!n){let d;try{d=JSON.parse(P)}catch{c(e,"INVALID_MESSAGE","Invalid JSON");return}if(d.type!=="hello"||d.role!=="browser"&&d.role!=="client"){c(e,"INVALID_MESSAGE",'First message must be a hello with role "browser" or "client"');return}n=!0,d.role==="browser"?(t&&t.readyState===o.WebSocket.OPEN&&t.close(1e3,"Replaced by new browser"),t=e,a=!1,e.on("close",()=>{t===e&&(t=null,a=!1,O())}),S(JSON.stringify({type:"connected",browser:!0})),e.on("message",i=>{E(typeof i=="string"?i:i.toString())})):(l.add(e),e.on("close",()=>{l.delete(e)}),b(e),e.on("message",i=>{h(e,typeof i=="string"?i:i.toString())})),e.removeListener("message",r)}};e.on("message",r),e.on("error",s=>{f&&f(s)})}const g=(e,n,r)=>{new URL(e.url??"/",`http://${e.headers.host??"localhost"}`).pathname===p&&u.handleUpgrade(e,n,r,s=>{m(s)})};return y.on("upgrade",g),u.on("error",e=>{f&&f(e)}),{close(){y.removeListener("upgrade",g);for(const e of l)e.close(1e3,"Relay shutting down");l.clear(),t&&t.readyState===o.WebSocket.OPEN&&t.close(1e3,"Relay shutting down"),t=null,a=!1,u.close()},get browserConnected(){return t!==null&&t.readyState===o.WebSocket.OPEN},get clientCount(){return l.size}}}Object.defineProperty(exports,"createTwdRelay",{enumerable:!0,get:function(){return W}});
package/dist/vite.cjs.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const n=require("./createTwdRelay-VRmjHboP.cjs");function a(r){let t="/";return{name:"twd-relay",configResolved(e){t=e.base},configureServer(e){if(!e.httpServer)return;const o=r?.path??t.replace(/\/$/,"")+"/__twd/ws",c=n.c(e.httpServer,{path:o});e.httpServer.on("close",()=>c.close())}}}exports.twdRemote=a;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const l=require("./relay-CpUORBd7.cjs");function n(r){let t="/";return{name:"twd-relay",configResolved(e){t=e.base},configureServer(e){if(!e.httpServer)return;const o=r?.path??t.replace(/\/$/,"")+"/__twd/ws",a=l.createTwdRelay(e.httpServer,{path:o});e.httpServer.on("close",()=>a.close())}}}exports.twdRemote=n;
package/dist/vite.es.js CHANGED
@@ -1,4 +1,4 @@
1
- import { c } from "./createTwdRelay-22etCW_8.js";
1
+ import { t as n } from "./relay-C8UTSEbG.js";
2
2
  function l(r) {
3
3
  let t = "/";
4
4
  return {
@@ -8,7 +8,7 @@ function l(r) {
8
8
  },
9
9
  configureServer(e) {
10
10
  if (!e.httpServer) return;
11
- const o = r?.path ?? t.replace(/\/$/, "") + "/__twd/ws", a = c(e.httpServer, { path: o });
11
+ const o = r?.path ?? t.replace(/\/$/, "") + "/__twd/ws", a = n(e.httpServer, { path: o });
12
12
  e.httpServer.on("close", () => a.close());
13
13
  }
14
14
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twd-relay",
3
- "version": "0.3.0",
3
+ "version": "1.0.0",
4
4
  "description": "WebSocket relay for TWD — enables AI agents and external tools to run and observe in-browser tests",
5
5
  "license": "MIT",
6
6
  "author": "BRIKEV",
@@ -60,14 +60,15 @@
60
60
  }
61
61
  },
62
62
  "dependencies": {
63
- "ws": "^8.19.0"
63
+ "ws": "^8.20.0"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/ws": "^8.18.1",
67
- "@vitest/coverage-v8": "^4.0.18",
67
+ "@vitest/coverage-v8": "^4.1.0",
68
+ "esbuild": "^0.27.4",
68
69
  "typescript": "^5.9.3",
69
- "vite": "^7.3.1",
70
+ "vite": "^8.0.1",
70
71
  "vite-plugin-dts": "^4.5.4",
71
- "vitest": "^4.0.18"
72
+ "vitest": "^4.1.0"
72
73
  }
73
74
  }
@@ -1,123 +0,0 @@
1
- import { WebSocketServer as P, WebSocket as o } from "ws";
2
- const R = "/__twd/ws";
3
- function J(p, N) {
4
- const E = N?.path ?? R, f = N?.onError, u = new P({ noServer: !0 });
5
- let t = null;
6
- const c = /* @__PURE__ */ new Set();
7
- let a = !1;
8
- function s(e, n, r) {
9
- const i = { type: "error", code: n, message: r };
10
- e.readyState === o.OPEN && e.send(JSON.stringify(i));
11
- }
12
- function S(e) {
13
- for (const n of c)
14
- n.readyState === o.OPEN && n.send(e);
15
- }
16
- function h(e) {
17
- const n = { type: "connected", browser: t !== null && t.readyState === o.OPEN };
18
- e.readyState === o.OPEN && e.send(JSON.stringify(n));
19
- }
20
- function m() {
21
- S(JSON.stringify({ type: "connected", browser: !1 }));
22
- }
23
- function b(e) {
24
- let n;
25
- try {
26
- n = JSON.parse(e);
27
- } catch {
28
- return;
29
- }
30
- n.type === "run:complete" && (a = !1), S(e);
31
- }
32
- function A(e, n) {
33
- let r;
34
- try {
35
- r = JSON.parse(n);
36
- } catch {
37
- s(e, "INVALID_MESSAGE", "Invalid JSON");
38
- return;
39
- }
40
- if (!r.type) {
41
- s(e, "INVALID_MESSAGE", 'Missing "type" field');
42
- return;
43
- }
44
- if (r.type === "run") {
45
- if (!t || t.readyState !== o.OPEN) {
46
- s(e, "NO_BROWSER", "No browser connected");
47
- return;
48
- }
49
- if (a) {
50
- s(e, "RUN_IN_PROGRESS", "A test run is already in progress");
51
- return;
52
- }
53
- a = !0, t.send(n);
54
- return;
55
- }
56
- if (r.type === "status") {
57
- if (!t || t.readyState !== o.OPEN) {
58
- s(e, "NO_BROWSER", "No browser connected");
59
- return;
60
- }
61
- t.send(n);
62
- return;
63
- }
64
- s(e, "UNKNOWN_COMMAND", `Unknown command: ${r.type}`);
65
- }
66
- function I(e) {
67
- let n = !1;
68
- const r = (i) => {
69
- const y = typeof i == "string" ? i : i.toString();
70
- if (!n) {
71
- let d;
72
- try {
73
- d = JSON.parse(y);
74
- } catch {
75
- s(e, "INVALID_MESSAGE", "Invalid JSON");
76
- return;
77
- }
78
- if (d.type !== "hello" || d.role !== "browser" && d.role !== "client") {
79
- s(e, "INVALID_MESSAGE", 'First message must be a hello with role "browser" or "client"');
80
- return;
81
- }
82
- n = !0, d.role === "browser" ? (t && t.readyState === o.OPEN && t.close(1e3, "Replaced by new browser"), t = e, a = !1, e.on("close", () => {
83
- t === e && (t = null, a = !1, m());
84
- }), S(JSON.stringify({ type: "connected", browser: !0 })), e.on("message", (l) => {
85
- const g = typeof l == "string" ? l : l.toString();
86
- b(g);
87
- })) : (c.add(e), e.on("close", () => {
88
- c.delete(e);
89
- }), h(e), e.on("message", (l) => {
90
- const g = typeof l == "string" ? l : l.toString();
91
- A(e, g);
92
- })), e.removeListener("message", r);
93
- }
94
- };
95
- e.on("message", r), e.on("error", (i) => {
96
- f && f(i);
97
- });
98
- }
99
- const O = (e, n, r) => {
100
- new URL(e.url ?? "/", `http://${e.headers.host ?? "localhost"}`).pathname === E && u.handleUpgrade(e, n, r, (y) => {
101
- I(y);
102
- });
103
- };
104
- return p.on("upgrade", O), u.on("error", (e) => {
105
- f && f(e);
106
- }), {
107
- close() {
108
- p.removeListener("upgrade", O);
109
- for (const e of c)
110
- e.close(1e3, "Relay shutting down");
111
- c.clear(), t && t.readyState === o.OPEN && t.close(1e3, "Relay shutting down"), t = null, a = !1, u.close();
112
- },
113
- get browserConnected() {
114
- return t !== null && t.readyState === o.OPEN;
115
- },
116
- get clientCount() {
117
- return c.size;
118
- }
119
- };
120
- }
121
- export {
122
- J as c
123
- };
@@ -1 +0,0 @@
1
- "use strict";const o=require("ws"),I="/__twd/ws";function P(N,p){const b=p?.path??I,f=p?.onError,S=new o.WebSocketServer({noServer:!0});let t=null;const l=new Set;let a=!1;function s(e,n,r){const c={type:"error",code:n,message:r};e.readyState===o.WebSocket.OPEN&&e.send(JSON.stringify(c))}function u(e){for(const n of l)n.readyState===o.WebSocket.OPEN&&n.send(e)}function E(e){const n={type:"connected",browser:t!==null&&t.readyState===o.WebSocket.OPEN};e.readyState===o.WebSocket.OPEN&&e.send(JSON.stringify(n))}function h(){u(JSON.stringify({type:"connected",browser:!1}))}function m(e){let n;try{n=JSON.parse(e)}catch{return}n.type==="run:complete"&&(a=!1),u(e)}function W(e,n){let r;try{r=JSON.parse(n)}catch{s(e,"INVALID_MESSAGE","Invalid JSON");return}if(!r.type){s(e,"INVALID_MESSAGE",'Missing "type" field');return}if(r.type==="run"){if(!t||t.readyState!==o.WebSocket.OPEN){s(e,"NO_BROWSER","No browser connected");return}if(a){s(e,"RUN_IN_PROGRESS","A test run is already in progress");return}a=!0,t.send(n);return}if(r.type==="status"){if(!t||t.readyState!==o.WebSocket.OPEN){s(e,"NO_BROWSER","No browser connected");return}t.send(n);return}s(e,"UNKNOWN_COMMAND",`Unknown command: ${r.type}`)}function A(e){let n=!1;const r=c=>{const y=typeof c=="string"?c:c.toString();if(!n){let d;try{d=JSON.parse(y)}catch{s(e,"INVALID_MESSAGE","Invalid JSON");return}if(d.type!=="hello"||d.role!=="browser"&&d.role!=="client"){s(e,"INVALID_MESSAGE",'First message must be a hello with role "browser" or "client"');return}n=!0,d.role==="browser"?(t&&t.readyState===o.WebSocket.OPEN&&t.close(1e3,"Replaced by new browser"),t=e,a=!1,e.on("close",()=>{t===e&&(t=null,a=!1,h())}),u(JSON.stringify({type:"connected",browser:!0})),e.on("message",i=>{const g=typeof i=="string"?i:i.toString();m(g)})):(l.add(e),e.on("close",()=>{l.delete(e)}),E(e),e.on("message",i=>{const g=typeof i=="string"?i:i.toString();W(e,g)})),e.removeListener("message",r)}};e.on("message",r),e.on("error",c=>{f&&f(c)})}const O=(e,n,r)=>{new URL(e.url??"/",`http://${e.headers.host??"localhost"}`).pathname===b&&S.handleUpgrade(e,n,r,y=>{A(y)})};return N.on("upgrade",O),S.on("error",e=>{f&&f(e)}),{close(){N.removeListener("upgrade",O);for(const e of l)e.close(1e3,"Relay shutting down");l.clear(),t&&t.readyState===o.WebSocket.OPEN&&t.close(1e3,"Relay shutting down"),t=null,a=!1,S.close()},get browserConnected(){return t!==null&&t.readyState===o.WebSocket.OPEN},get clientCount(){return l.size}}}exports.c=P;