rerender-lens 0.4.0 → 0.7.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +208 -359
  3. package/dist/{budget-LkNjGtRc.d.ts → budget-CA1MS7lB.d.cts} +20 -10
  4. package/dist/{budget-COBu7jBU.d.cts → budget-ok1VDJ4t.d.ts} +20 -10
  5. package/dist/cli.cjs +322 -53
  6. package/dist/cli.cjs.map +1 -1
  7. package/dist/cli.js +322 -53
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{devtools-BkCct3cJ.d.cts → devtools-Cl5n_52v.d.ts} +17 -2
  10. package/dist/{devtools-CZebzpF6.d.ts → devtools-UwQx2VYS.d.cts} +17 -2
  11. package/dist/index.cjs +537 -49
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +137 -8
  14. package/dist/index.d.ts +137 -8
  15. package/dist/index.js +521 -50
  16. package/dist/index.js.map +1 -1
  17. package/dist/jest-setup.cjs +1690 -0
  18. package/dist/jest-setup.cjs.map +1 -0
  19. package/dist/jest-setup.d.cts +8 -0
  20. package/dist/jest-setup.d.ts +8 -0
  21. package/dist/jest-setup.js +1688 -0
  22. package/dist/jest-setup.js.map +1 -0
  23. package/dist/jest.cjs +1777 -0
  24. package/dist/jest.cjs.map +1 -0
  25. package/dist/jest.d.cts +23 -0
  26. package/dist/jest.d.ts +23 -0
  27. package/dist/jest.js +1768 -0
  28. package/dist/jest.js.map +1 -0
  29. package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-B5poAEvE.d.cts} +8 -4
  30. package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-CqQW0Eum.d.ts} +8 -4
  31. package/dist/playwright.cjs +230 -28
  32. package/dist/playwright.cjs.map +1 -1
  33. package/dist/playwright.d.cts +4 -4
  34. package/dist/playwright.d.ts +4 -4
  35. package/dist/playwright.js +230 -28
  36. package/dist/playwright.js.map +1 -1
  37. package/dist/relay.cjs +71 -19
  38. package/dist/relay.cjs.map +1 -1
  39. package/dist/relay.d.cts +25 -3
  40. package/dist/relay.d.ts +25 -3
  41. package/dist/relay.js +71 -19
  42. package/dist/relay.js.map +1 -1
  43. package/dist/rerender-lens.iife.js +457 -62
  44. package/dist/runner-report-Bvqoe89M.d.ts +46 -0
  45. package/dist/runner-report-CvpDg2zt.d.cts +46 -0
  46. package/dist/setup.cjs +209 -35
  47. package/dist/setup.cjs.map +1 -1
  48. package/dist/setup.js +209 -35
  49. package/dist/setup.js.map +1 -1
  50. package/dist/{types-BzUEVkxJ.d.cts → types-DwNN5cWe.d.cts} +82 -2
  51. package/dist/{types-BzUEVkxJ.d.ts → types-DwNN5cWe.d.ts} +82 -2
  52. package/dist/vite.d.cts +1 -1
  53. package/dist/vite.d.ts +1 -1
  54. package/dist/vitest-setup.cjs +357 -44
  55. package/dist/vitest-setup.cjs.map +1 -1
  56. package/dist/vitest-setup.d.cts +4 -4
  57. package/dist/vitest-setup.d.ts +4 -4
  58. package/dist/vitest-setup.js +357 -44
  59. package/dist/vitest-setup.js.map +1 -1
  60. package/dist/vitest.cjs +420 -80
  61. package/dist/vitest.cjs.map +1 -1
  62. package/dist/vitest.d.cts +21 -39
  63. package/dist/vitest.d.ts +21 -39
  64. package/dist/vitest.js +421 -81
  65. package/dist/vitest.js.map +1 -1
  66. package/package.json +41 -4
  67. package/panel/panel.css +49 -0
  68. package/panel/panel.js +871 -353
package/dist/relay.cjs CHANGED
@@ -8,6 +8,8 @@ var url = require('url');
8
8
  // node_modules/tsup/assets/cjs_shims.js
9
9
  var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
10
10
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
11
+ var MARKER = "__rerenderLens";
12
+ var PROTOCOL = 2;
11
13
  var MIME = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8" };
12
14
  function findPanelDir() {
13
15
  const here = path.dirname(url.fileURLToPath(importMetaUrl));
@@ -42,26 +44,55 @@ function createRelayServer(options = {}) {
42
44
  const host = options.host ?? "127.0.0.1";
43
45
  const panelDir = options.panelDir === void 0 ? findPanelDir() : options.panelDir;
44
46
  const keepAliveMs = options.keepAliveMs ?? 15e3;
45
- const apps = /* @__PURE__ */ new Set();
47
+ const token = options.token || null;
48
+ const apps = /* @__PURE__ */ new Map();
46
49
  const panels = /* @__PURE__ */ new Set();
47
50
  let url = "";
48
- const send = (targets, message) => {
49
- const line = `data: ${JSON.stringify(message)}
51
+ let nextAppId = 1;
52
+ const connectUrl = () => token ? `${url}?token=${encodeURIComponent(token)}` : url;
53
+ const line = (message) => `data: ${JSON.stringify(message)}
50
54
 
51
55
  `;
52
- for (const res of targets) {
56
+ const sendTo = (res, message) => {
57
+ try {
58
+ res.write(line(message));
59
+ } catch {
60
+ apps.delete(res);
61
+ panels.delete(res);
62
+ }
63
+ };
64
+ const send = (targets, message) => {
65
+ const text = line(message);
66
+ for (const res of [...targets]) {
53
67
  try {
54
- res.write(line);
68
+ res.write(text);
55
69
  } catch {
56
- targets.delete(res);
70
+ apps.delete(res);
71
+ panels.delete(res);
57
72
  }
58
73
  }
59
74
  };
60
- const route = (message) => {
75
+ const appList = () => [...apps.values()];
76
+ const roster = () => ({ [MARKER]: true, version: PROTOCOL, type: "relay", payload: { apps: apps.size, list: appList() } });
77
+ const streamOf = (id) => {
78
+ for (const [res, app] of apps) if (app.id === id) return res;
79
+ return null;
80
+ };
81
+ const route = (message, from) => {
61
82
  if (!message || typeof message !== "object") return;
62
83
  const m = message;
63
- if (m.__rerenderLensCmd === true) send(apps, m);
64
- else if (m.__rerenderLens === true || m.__rerenderLensReply === true) send(panels, m);
84
+ if (m.__rerenderLensCmd === true) {
85
+ const target = typeof m.app === "string" ? streamOf(m.app) : null;
86
+ if (target) sendTo(target, m);
87
+ else send(apps.keys(), m);
88
+ } else if (m.__rerenderLens === true || m.__rerenderLensReply === true) {
89
+ send(panels, from ? { ...m, app: from.id } : m);
90
+ }
91
+ };
92
+ const authorized = (req, u) => {
93
+ if (!token) return true;
94
+ const given = u.searchParams.get("token") || (typeof req.headers["x-rerender-lens-token"] === "string" ? req.headers["x-rerender-lens-token"] : "");
95
+ return given === token;
65
96
  };
66
97
  const server = http.createServer(async (req, res) => {
67
98
  cors(res);
@@ -73,18 +104,26 @@ function createRelayServer(options = {}) {
73
104
  }
74
105
  if (u.pathname === "/" && req.method === "GET") {
75
106
  res.statusCode = 302;
76
- res.setHeader("Location", `/panel.html?relay=${encodeURIComponent(url)}`);
107
+ res.setHeader("Location", `/panel.html?relay=${encodeURIComponent(connectUrl())}`);
77
108
  res.end();
78
109
  return;
79
110
  }
111
+ if (/^\/(events|message|status)$/.test(u.pathname) && !authorized(req, u)) {
112
+ res.statusCode = 401;
113
+ res.setHeader("Content-Type", "text/plain");
114
+ res.end("rerender-lens relay: this relay needs the token from the URL it printed (?token=...)");
115
+ return;
116
+ }
80
117
  if (u.pathname === "/events" && req.method === "GET") {
81
- const role = u.searchParams.get("role") === "app" ? apps : panels;
118
+ const isApp = u.searchParams.get("role") === "app";
82
119
  res.statusCode = 200;
83
120
  res.setHeader("Content-Type", "text/event-stream");
84
121
  res.setHeader("Cache-Control", "no-store");
85
122
  res.setHeader("Connection", "keep-alive");
86
123
  res.write(": connected\n\n");
87
- role.add(res);
124
+ const app = isApp ? { id: `a${nextAppId++}`, label: (u.searchParams.get("label") || req.headers.origin || "").slice(0, 120) || `app ${nextAppId - 1}` } : null;
125
+ if (app) apps.set(res, app);
126
+ else panels.add(res);
88
127
  const ping = setInterval(() => {
89
128
  try {
90
129
  res.write(": ping\n\n");
@@ -93,18 +132,29 @@ function createRelayServer(options = {}) {
93
132
  }, keepAliveMs);
94
133
  req.on("close", () => {
95
134
  clearInterval(ping);
96
- role.delete(res);
135
+ apps.delete(res);
136
+ panels.delete(res);
97
137
  });
98
- if (role === apps) {
99
- send(panels, { __rerenderLens: true, version: 2, type: "relay", payload: { apps: apps.size } });
100
- req.on("close", () => send(panels, { __rerenderLens: true, version: 2, type: "relay", payload: { apps: apps.size } }));
138
+ if (app) {
139
+ sendTo(res, { __rerenderLensRelay: true, version: PROTOCOL, app: app.id });
140
+ send(panels, roster());
141
+ req.on("close", () => send(panels, roster()));
142
+ } else {
143
+ sendTo(res, roster());
101
144
  }
102
145
  return;
103
146
  }
147
+ if (u.pathname === "/favicon.ico") {
148
+ res.statusCode = 204;
149
+ res.end();
150
+ return;
151
+ }
104
152
  if (u.pathname === "/message" && req.method === "POST") {
105
153
  try {
106
154
  const parsed = JSON.parse(await readBody(req) || "null");
107
- for (const m of Array.isArray(parsed) ? parsed : [parsed]) route(m);
155
+ const sender = u.searchParams.get("app");
156
+ const from = sender ? appList().find((a) => a.id === sender) ?? void 0 : void 0;
157
+ for (const m of Array.isArray(parsed) ? parsed : [parsed]) route(m, from);
108
158
  res.statusCode = 204;
109
159
  res.end();
110
160
  } catch (e) {
@@ -117,7 +167,7 @@ function createRelayServer(options = {}) {
117
167
  if (u.pathname === "/status" && req.method === "GET") {
118
168
  res.statusCode = 200;
119
169
  res.setHeader("Content-Type", "application/json");
120
- res.end(JSON.stringify({ ok: true, apps: apps.size, panels: panels.size }));
170
+ res.end(JSON.stringify({ ok: true, apps: apps.size, panels: panels.size, list: appList() }));
121
171
  return;
122
172
  }
123
173
  const file = u.pathname.replace(/^\//, "");
@@ -147,10 +197,12 @@ function createRelayServer(options = {}) {
147
197
  resolve({
148
198
  server,
149
199
  url,
200
+ connectUrl: connectUrl(),
150
201
  port,
151
202
  counts: () => ({ apps: apps.size, panels: panels.size }),
203
+ appList,
152
204
  close: () => new Promise((done) => {
153
- for (const res of [...apps, ...panels]) res.end();
205
+ for (const res of [...apps.keys(), ...panels]) res.end();
154
206
  apps.clear();
155
207
  panels.clear();
156
208
  server.close(() => done());
@@ -1 +1 @@
1
- {"version":3,"sources":["../node_modules/tsup/assets/cjs_shims.js","../src/relay.ts"],"names":["dirname","fileURLToPath","join","existsSync","createServer","readFileSync"],"mappings":";;;;;;;;AAKA,IAAM,gBAAA,GAAmB,MACvB,OAAO,QAAA,KAAa,WAAA,GAChB,IAAI,GAAA,CAAI,CAAA,KAAA,EAAQ,UAAU,CAAA,CAAE,CAAA,CAAE,IAAA,GAC7B,QAAA,CAAS,aAAA,IAAiB,QAAA,CAAS,aAAA,CAAc,OAAA,CAAQ,WAAA,EAAY,KAAM,QAAA,GAC1E,QAAA,CAAS,aAAA,CAAc,GAAA,GACvB,IAAI,GAAA,CAAI,SAAA,EAAW,QAAA,CAAS,OAAO,CAAA,CAAE,IAAA;AAEtC,IAAM,gCAAgC,gBAAA,EAAiB;AC0B9D,IAAM,OAA+B,EAAE,OAAA,EAAS,4BAA4B,KAAA,EAAO,gCAAA,EAAkC,QAAQ,yBAAA,EAA0B;AAEhJ,SAAS,YAAA,GAA8B;AAC5C,EAAA,MAAM,IAAA,GAAOA,YAAA,CAAQC,iBAAA,CAAc,aAAe,CAAC,CAAA;AACnD,EAAA,KAAA,MAAW,OAAO,CAACC,SAAA,CAAK,MAAM,IAAA,EAAM,OAAO,GAAGA,SAAA,CAAK,IAAA,EAAM,IAAA,EAAM,WAAW,GAAGA,SAAA,CAAK,IAAA,EAAM,MAAM,IAAA,EAAM,WAAW,CAAC,CAAA,EAAG;AACjH,IAAA,IAAIC,aAAA,CAAWD,SAAA,CAAK,GAAA,EAAK,YAAY,CAAC,CAAA,IAAKC,aAAA,CAAWD,SAAA,CAAK,GAAA,EAAK,UAAU,CAAC,CAAA,EAAG,OAAO,GAAA;AAAA,EACvF;AACA,EAAA,OAAO,IAAA;AACT;AAEA,IAAM,IAAA,GAAO,CAAC,GAAA,KAA8B;AAC1C,EAAA,GAAA,CAAI,SAAA,CAAU,+BAA+B,GAAG,CAAA;AAChD,EAAA,GAAA,CAAI,SAAA,CAAU,gCAAgC,oBAAoB,CAAA;AAClE,EAAA,GAAA,CAAI,SAAA,CAAU,gCAAgC,cAAc,CAAA;AAC9D,CAAA;AAEA,SAAS,QAAA,CAAS,GAAA,EAAsB,KAAA,GAAQ,CAAA,GAAI,OAAO,IAAA,EAAuB;AAChF,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,IAAA,MAAM,SAAmB,EAAC;AAC1B,IAAA,IAAI,IAAA,GAAO,CAAA;AACX,IAAA,GAAA,CAAI,EAAA,CAAG,MAAA,EAAQ,CAAC,CAAA,KAAc;AAC5B,MAAA,IAAA,IAAQ,CAAA,CAAE,MAAA;AACV,MAAA,IAAI,OAAO,KAAA,EAAO;AAChB,QAAA,MAAA,CAAO,IAAI,KAAA,CAAM,gBAAgB,CAAC,CAAA;AAClC,QAAA,GAAA,CAAI,OAAA,EAAQ;AACZ,QAAA;AAAA,MACF;AACA,MAAA,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,IACf,CAAC,CAAA;AACD,IAAA,GAAA,CAAI,EAAA,CAAG,KAAA,EAAO,MAAM,OAAA,CAAQ,MAAA,CAAO,MAAA,CAAO,MAAM,CAAA,CAAE,QAAA,CAAS,MAAM,CAAC,CAAC,CAAA;AACnE,IAAA,GAAA,CAAI,EAAA,CAAG,SAAS,MAAM,CAAA;AAAA,EACxB,CAAC,CAAA;AACH;AAEO,SAAS,iBAAA,CAAkB,OAAA,GAAwB,EAAC,EAAyB;AAClF,EAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,IAAQ,WAAA;AAC7B,EAAA,MAAM,WAAW,OAAA,CAAQ,QAAA,KAAa,MAAA,GAAY,YAAA,KAAiB,OAAA,CAAQ,QAAA;AAC3E,EAAA,MAAM,WAAA,GAAc,QAAQ,WAAA,IAAe,IAAA;AAC3C,EAAA,MAAM,IAAA,uBAAW,GAAA,EAAoB;AACrC,EAAA,MAAM,MAAA,uBAAa,GAAA,EAAoB;AACvC,EAAA,IAAI,GAAA,GAAM,EAAA;AAEV,EAAA,MAAM,IAAA,GAAO,CAAC,OAAA,EAA8B,OAAA,KAA2B;AACrE,IAAA,MAAM,IAAA,GAAO,CAAA,MAAA,EAAS,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC;;AAAA,CAAA;AAC7C,IAAA,KAAA,MAAW,OAAO,OAAA,EAAS;AACzB,MAAA,IAAI;AACF,QAAA,GAAA,CAAI,MAAM,IAAI,CAAA;AAAA,MAChB,CAAA,CAAA,MAAQ;AACN,QAAA,OAAA,CAAQ,OAAO,GAAG,CAAA;AAAA,MACpB;AAAA,IACF;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,KAAA,GAAQ,CAAC,OAAA,KAA2B;AACxC,IAAA,IAAI,CAAC,OAAA,IAAW,OAAO,OAAA,KAAY,QAAA,EAAU;AAC7C,IAAA,MAAM,CAAA,GAAI,OAAA;AACV,IAAA,IAAI,CAAA,CAAE,iBAAA,KAAsB,IAAA,EAAM,IAAA,CAAK,MAAM,CAAC,CAAA;AAAA,SAAA,IACrC,CAAA,CAAE,mBAAmB,IAAA,IAAQ,CAAA,CAAE,wBAAwB,IAAA,EAAM,IAAA,CAAK,QAAQ,CAAC,CAAA;AAAA,EACtF,CAAA;AAEA,EAAA,MAAM,MAAA,GAASE,iBAAA,CAAa,OAAO,GAAA,EAAK,GAAA,KAAQ;AAC9C,IAAA,IAAA,CAAK,GAAG,CAAA;AACR,IAAA,MAAM,IAAI,IAAI,GAAA,CAAI,GAAA,CAAI,GAAA,IAAO,KAAK,cAAc,CAAA;AAChD,IAAA,IAAI,GAAA,CAAI,WAAW,SAAA,EAAW;AAC5B,MAAA,GAAA,CAAI,UAAA,GAAa,GAAA;AACjB,MAAA,GAAA,CAAI,GAAA,EAAI;AACR,MAAA;AAAA,IACF;AACA,IAAA,IAAI,CAAA,CAAE,QAAA,KAAa,GAAA,IAAO,GAAA,CAAI,WAAW,KAAA,EAAO;AAC9C,MAAA,GAAA,CAAI,UAAA,GAAa,GAAA;AACjB,MAAA,GAAA,CAAI,UAAU,UAAA,EAAY,CAAA,kBAAA,EAAqB,kBAAA,CAAmB,GAAG,CAAC,CAAA,CAAE,CAAA;AACxE,MAAA,GAAA,CAAI,GAAA,EAAI;AACR,MAAA;AAAA,IACF;AACA,IAAA,IAAI,CAAA,CAAE,QAAA,KAAa,SAAA,IAAa,GAAA,CAAI,WAAW,KAAA,EAAO;AACpD,MAAA,MAAM,OAAO,CAAA,CAAE,YAAA,CAAa,IAAI,MAAM,CAAA,KAAM,QAAQ,IAAA,GAAO,MAAA;AAC3D,MAAA,GAAA,CAAI,UAAA,GAAa,GAAA;AACjB,MAAA,GAAA,CAAI,SAAA,CAAU,gBAAgB,mBAAmB,CAAA;AACjD,MAAA,GAAA,CAAI,SAAA,CAAU,iBAAiB,UAAU,CAAA;AACzC,MAAA,GAAA,CAAI,SAAA,CAAU,cAAc,YAAY,CAAA;AACxC,MAAA,GAAA,CAAI,MAAM,iBAAiB,CAAA;AAC3B,MAAA,IAAA,CAAK,IAAI,GAAG,CAAA;AACZ,MAAA,MAAM,IAAA,GAAO,YAAY,MAAM;AAC7B,QAAA,IAAI;AACF,UAAA,GAAA,CAAI,MAAM,YAAY,CAAA;AAAA,QACxB,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF,GAAG,WAAW,CAAA;AACd,MAAA,GAAA,CAAI,EAAA,CAAG,SAAS,MAAM;AACpB,QAAA,aAAA,CAAc,IAAI,CAAA;AAClB,QAAA,IAAA,CAAK,OAAO,GAAG,CAAA;AAAA,MACjB,CAAC,CAAA;AAED,MAAA,IAAI,SAAS,IAAA,EAAM;AACjB,QAAA,IAAA,CAAK,MAAA,EAAQ,EAAE,cAAA,EAAgB,IAAA,EAAM,SAAS,CAAA,EAAG,IAAA,EAAM,OAAA,EAAS,OAAA,EAAS,EAAE,IAAA,EAAM,IAAA,CAAK,IAAA,IAAQ,CAAA;AAC9F,QAAA,GAAA,CAAI,GAAG,OAAA,EAAS,MAAM,KAAK,MAAA,EAAQ,EAAE,gBAAgB,IAAA,EAAM,OAAA,EAAS,GAAG,IAAA,EAAM,OAAA,EAAS,SAAS,EAAE,IAAA,EAAM,KAAK,IAAA,EAAK,EAAG,CAAC,CAAA;AAAA,MACvH;AACA,MAAA;AAAA,IACF;AACA,IAAA,IAAI,CAAA,CAAE,QAAA,KAAa,UAAA,IAAc,GAAA,CAAI,WAAW,MAAA,EAAQ;AACtD,MAAA,IAAI;AACF,QAAA,MAAM,SAAS,IAAA,CAAK,KAAA,CAAO,MAAM,QAAA,CAAS,GAAG,KAAM,MAAM,CAAA;AACzD,QAAA,KAAA,MAAW,CAAA,IAAK,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,GAAI,SAAS,CAAC,MAAM,CAAA,EAAG,KAAA,CAAM,CAAC,CAAA;AAClE,QAAA,GAAA,CAAI,UAAA,GAAa,GAAA;AACjB,QAAA,GAAA,CAAI,GAAA,EAAI;AAAA,MACV,SAAS,CAAA,EAAG;AACV,QAAA,GAAA,CAAI,UAAA,GAAa,GAAA;AACjB,QAAA,GAAA,CAAI,SAAA,CAAU,gBAAgB,YAAY,CAAA;AAC1C,QAAA,GAAA,CAAI,GAAA,CAAI,MAAA,CAAQ,CAAA,CAAY,OAAA,IAAW,CAAC,CAAC,CAAA;AAAA,MAC3C;AACA,MAAA;AAAA,IACF;AACA,IAAA,IAAI,CAAA,CAAE,QAAA,KAAa,SAAA,IAAa,GAAA,CAAI,WAAW,KAAA,EAAO;AACpD,MAAA,GAAA,CAAI,UAAA,GAAa,GAAA;AACjB,MAAA,GAAA,CAAI,SAAA,CAAU,gBAAgB,kBAAkB,CAAA;AAChD,MAAA,GAAA,CAAI,GAAA,CAAI,IAAA,CAAK,SAAA,CAAU,EAAE,EAAA,EAAI,IAAA,EAAM,IAAA,EAAM,IAAA,CAAK,IAAA,EAAM,MAAA,EAAQ,MAAA,CAAO,IAAA,EAAM,CAAC,CAAA;AAC1E,MAAA;AAAA,IACF;AACA,IAAA,MAAM,IAAA,GAAO,CAAA,CAAE,QAAA,CAAS,OAAA,CAAQ,OAAO,EAAE,CAAA;AACzC,IAAA,IAAI,yBAAyB,IAAA,CAAK,IAAI,CAAA,IAAK,GAAA,CAAI,WAAW,KAAA,EAAO;AAC/D,MAAA,IAAI,CAAC,QAAA,EAAU;AACb,QAAA,GAAA,CAAI,UAAA,GAAa,GAAA;AACjB,QAAA,GAAA,CAAI,SAAA,CAAU,gBAAgB,YAAY,CAAA;AAC1C,QAAA,GAAA,CAAI,IAAI,8DAA8D,CAAA;AACtE,QAAA;AAAA,MACF;AACA,MAAA,GAAA,CAAI,UAAA,GAAa,GAAA;AACjB,MAAA,GAAA,CAAI,SAAA,CAAU,cAAA,EAAgB,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,WAAA,CAAY,GAAG,CAAC,CAAC,CAAA,IAAK,0BAA0B,CAAA;AACnG,MAAA,GAAA,CAAI,SAAA,CAAU,iBAAiB,UAAU,CAAA;AACzC,MAAA,GAAA,CAAI,IAAIC,eAAA,CAAaH,SAAA,CAAK,QAAA,EAAU,IAAI,CAAC,CAAC,CAAA;AAC1C,MAAA;AAAA,IACF;AACA,IAAA,GAAA,CAAI,UAAA,GAAa,GAAA;AACjB,IAAA,GAAA,CAAI,SAAA,CAAU,gBAAgB,YAAY,CAAA;AAC1C,IAAA,GAAA,CAAI,IAAI,WAAW,CAAA;AAAA,EACrB,CAAC,CAAA;AAED,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,IAAA,MAAA,CAAO,IAAA,CAAK,SAAS,MAAM,CAAA;AAC3B,IAAA,MAAA,CAAO,MAAA,CAAO,OAAA,CAAQ,IAAA,IAAQ,IAAA,EAAM,MAAM,MAAM;AAC9C,MAAA,MAAM,IAAA,GAAO,OAAO,OAAA,EAAQ;AAC5B,MAAA,MAAM,IAAA,GAAO,OAAO,IAAA,KAAS,QAAA,IAAY,OAAO,IAAA,CAAK,IAAA,GAAO,QAAQ,IAAA,IAAQ,IAAA;AAC5E,MAAA,GAAA,GAAM,CAAA,OAAA,EAAU,SAAS,SAAA,IAAa,IAAA,KAAS,OAAO,WAAA,GAAc,IAAI,IAAI,IAAI,CAAA,CAAA;AAChF,MAAA,OAAA,CAAQ;AAAA,QACN,MAAA;AAAA,QACA,GAAA;AAAA,QACA,IAAA;AAAA,QACA,MAAA,EAAQ,OAAO,EAAE,IAAA,EAAM,KAAK,IAAA,EAAM,MAAA,EAAQ,OAAO,IAAA,EAAK,CAAA;AAAA,QACtD,KAAA,EAAO,MACL,IAAI,OAAA,CAAc,CAAC,IAAA,KAAS;AAC1B,UAAA,KAAA,MAAW,GAAA,IAAO,CAAC,GAAG,IAAA,EAAM,GAAG,MAAM,CAAA,MAAO,GAAA,EAAI;AAChD,UAAA,IAAA,CAAK,KAAA,EAAM;AACX,UAAA,MAAA,CAAO,KAAA,EAAM;AACb,UAAA,MAAA,CAAO,KAAA,CAAM,MAAM,IAAA,EAAM,CAAA;AAAA,QAC3B,CAAC;AAAA,OACJ,CAAA;AAAA,IACH,CAAC,CAAA;AAAA,EACH,CAAC,CAAA;AACH","file":"relay.cjs","sourcesContent":["// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","/**\n * A tiny local relay (`npx rerender-lens panel`): serves the panel and forwards messages between\n * app pages and panel pages over Server-Sent Events + POST, so any app (Next.js, Webpack, a\n * production-like build) gets the panel without the extension and without a same-origin channel.\n *\n * GET / redirect to /panel.html?relay=<this server>\n * GET /panel.(html|js|css) the panel page\n * GET /events?role=app SSE: commands from panels\n * GET /events?role=panel SSE: hello / report / clear from apps, replies to commands\n * POST /message one message or an array; `{ __rerenderLens }` and replies go to\n * panels, `{ __rerenderLensCmd }` goes to apps\n *\n * Dependency-free (node:http). CORS is open: the relay is a local dev tool, bind it to 127.0.0.1.\n */\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport interface RelayOptions {\n port?: number;\n host?: string;\n /** Directory with panel.html/js/css; found automatically in the package. */\n panelDir?: string | null;\n /** Milliseconds between SSE keep-alive comments. Default 15000. */\n keepAliveMs?: number;\n}\n\nexport interface RelayServer {\n server: Server;\n /** `http://127.0.0.1:4141` once listening. */\n url: string;\n port: number;\n close(): Promise<void>;\n /** Connected app and panel streams. */\n counts(): { apps: number; panels: number };\n}\n\nconst MIME: Record<string, string> = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8' };\n\nexport function findPanelDir(): string | null {\n const here = dirname(fileURLToPath(import.meta.url));\n for (const dir of [join(here, '..', 'panel'), join(here, '..', 'extension'), join(here, '..', '..', 'extension')]) {\n if (existsSync(join(dir, 'panel.html')) && existsSync(join(dir, 'panel.js'))) return dir;\n }\n return null;\n}\n\nconst cors = (res: ServerResponse): void => {\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n};\n\nfunction readBody(req: IncomingMessage, limit = 8 * 1024 * 1024): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n req.on('data', (c: Buffer) => {\n size += c.length;\n if (size > limit) {\n reject(new Error('body too large'));\n req.destroy();\n return;\n }\n chunks.push(c);\n });\n req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));\n req.on('error', reject);\n });\n}\n\nexport function createRelayServer(options: RelayOptions = {}): Promise<RelayServer> {\n const host = options.host ?? '127.0.0.1';\n const panelDir = options.panelDir === undefined ? findPanelDir() : options.panelDir;\n const keepAliveMs = options.keepAliveMs ?? 15_000;\n const apps = new Set<ServerResponse>();\n const panels = new Set<ServerResponse>();\n let url = '';\n\n const send = (targets: Set<ServerResponse>, message: unknown): void => {\n const line = `data: ${JSON.stringify(message)}\\n\\n`;\n for (const res of targets) {\n try {\n res.write(line);\n } catch {\n targets.delete(res);\n }\n }\n };\n\n const route = (message: unknown): void => {\n if (!message || typeof message !== 'object') return;\n const m = message as Record<string, unknown>;\n if (m.__rerenderLensCmd === true) send(apps, m);\n else if (m.__rerenderLens === true || m.__rerenderLensReply === true) send(panels, m);\n };\n\n const server = createServer(async (req, res) => {\n cors(res);\n const u = new URL(req.url || '/', 'http://relay');\n if (req.method === 'OPTIONS') {\n res.statusCode = 204;\n res.end();\n return;\n }\n if (u.pathname === '/' && req.method === 'GET') {\n res.statusCode = 302;\n res.setHeader('Location', `/panel.html?relay=${encodeURIComponent(url)}`);\n res.end();\n return;\n }\n if (u.pathname === '/events' && req.method === 'GET') {\n const role = u.searchParams.get('role') === 'app' ? apps : panels;\n res.statusCode = 200;\n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-store');\n res.setHeader('Connection', 'keep-alive');\n res.write(': connected\\n\\n');\n role.add(res);\n const ping = setInterval(() => {\n try {\n res.write(': ping\\n\\n');\n } catch {\n /* closed */\n }\n }, keepAliveMs);\n req.on('close', () => {\n clearInterval(ping);\n role.delete(res);\n });\n // Tell panels an app went away so they show \"disconnected\" instead of waiting.\n if (role === apps) {\n send(panels, { __rerenderLens: true, version: 2, type: 'relay', payload: { apps: apps.size } });\n req.on('close', () => send(panels, { __rerenderLens: true, version: 2, type: 'relay', payload: { apps: apps.size } }));\n }\n return;\n }\n if (u.pathname === '/message' && req.method === 'POST') {\n try {\n const parsed = JSON.parse((await readBody(req)) || 'null') as unknown;\n for (const m of Array.isArray(parsed) ? parsed : [parsed]) route(m);\n res.statusCode = 204;\n res.end();\n } catch (e) {\n res.statusCode = 400;\n res.setHeader('Content-Type', 'text/plain');\n res.end(String((e as Error).message || e));\n }\n return;\n }\n if (u.pathname === '/status' && req.method === 'GET') {\n res.statusCode = 200;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ ok: true, apps: apps.size, panels: panels.size }));\n return;\n }\n const file = u.pathname.replace(/^\\//, '');\n if (/^panel\\.(html|js|css)$/.test(file) && req.method === 'GET') {\n if (!panelDir) {\n res.statusCode = 500;\n res.setHeader('Content-Type', 'text/plain');\n res.end('rerender-lens: panel files not found (is the package built?)');\n return;\n }\n res.statusCode = 200;\n res.setHeader('Content-Type', MIME[file.slice(file.lastIndexOf('.'))] || 'application/octet-stream');\n res.setHeader('Cache-Control', 'no-store');\n res.end(readFileSync(join(panelDir, file)));\n return;\n }\n res.statusCode = 404;\n res.setHeader('Content-Type', 'text/plain');\n res.end('not found');\n });\n\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(options.port ?? 4141, host, () => {\n const addr = server.address();\n const port = typeof addr === 'object' && addr ? addr.port : options.port ?? 4141;\n url = `http://${host === '0.0.0.0' || host === '::' ? 'localhost' : host}:${port}`;\n resolve({\n server,\n url,\n port,\n counts: () => ({ apps: apps.size, panels: panels.size }),\n close: () =>\n new Promise<void>((done) => {\n for (const res of [...apps, ...panels]) res.end();\n apps.clear();\n panels.clear();\n server.close(() => done());\n }),\n });\n });\n });\n}\n"]}
1
+ {"version":3,"sources":["../node_modules/tsup/assets/cjs_shims.js","../src/relay.ts"],"names":["dirname","fileURLToPath","join","existsSync","createServer","readFileSync"],"mappings":"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","file":"relay.cjs","sourcesContent":["// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","/**\n * A tiny local relay (`npx rerender-lens panel`): serves the panel and forwards messages between\n * app pages and panel pages over Server-Sent Events + POST, so any app (Next.js, Webpack, a\n * production-like build) gets the panel without the extension and without a same-origin channel.\n *\n * GET / redirect to /panel.html?relay=<this server>\n * GET /panel.(html|js|css) the panel page\n * GET /events?role=app SSE: commands from panels (first message: this app's id)\n * GET /events?role=panel SSE: hello / report / clear from apps, replies to commands\n * POST /message one message or an array; `{ __rerenderLens }` and replies go to\n * panels, `{ __rerenderLensCmd }` goes to apps\n *\n * Several apps can share one relay. Each app connection gets an id; the relay tells the app its id,\n * stamps `app` on everything that app sends, and sends panels the roster whenever it changes. A\n * command carrying `app` goes to that one app, a command without it goes to all of them, so a panel\n * or a library that knows nothing about ids behaves exactly as before.\n *\n * Dependency-free (node:http). Bound to 127.0.0.1 by default, where CORS is open and no token is\n * needed. On any other host `rerender-lens panel` generates a token: `/events`, `/message` and\n * `/status` then require it (`?token=` or the `x-rerender-lens-token` header) and the URL it prints\n * carries it, because reports contain the app's prop, state and context values.\n */\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport interface RelayOptions {\n port?: number;\n host?: string;\n /** Directory with panel.html/js/css; found automatically in the package. */\n panelDir?: string | null;\n /** Milliseconds between SSE keep-alive comments. Default 15000. */\n keepAliveMs?: number;\n /**\n * Require this token on `/events`, `/message` and `/status`. `rerender-lens panel` generates one\n * whenever it binds to anything but loopback; the URL it prints carries it as `?token=`.\n */\n token?: string;\n}\n\n/** One connected app page. `label` is what the panel's app chooser shows. */\nexport interface RelayApp {\n id: string;\n label: string;\n}\n\nexport interface RelayServer {\n server: Server;\n /** `http://127.0.0.1:4141` once listening. */\n url: string;\n /** The URL to hand to a panel or an app: `url`, plus `?token=` when one is required. */\n connectUrl: string;\n port: number;\n close(): Promise<void>;\n /** Connected app and panel streams. */\n counts(): { apps: number; panels: number };\n /** The connected apps, in connection order. */\n appList(): RelayApp[];\n}\n\n/** Same marker and protocol the library and the panel use (`src/devtools.ts`); kept literal so the relay stays dependency-free. */\nconst MARKER = '__rerenderLens';\nconst PROTOCOL = 2;\n\nconst MIME: Record<string, string> = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8' };\n\nexport function findPanelDir(): string | null {\n const here = dirname(fileURLToPath(import.meta.url));\n for (const dir of [join(here, '..', 'panel'), join(here, '..', 'extension'), join(here, '..', '..', 'extension')]) {\n if (existsSync(join(dir, 'panel.html')) && existsSync(join(dir, 'panel.js'))) return dir;\n }\n return null;\n}\n\nconst cors = (res: ServerResponse): void => {\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n};\n\nfunction readBody(req: IncomingMessage, limit = 8 * 1024 * 1024): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n req.on('data', (c: Buffer) => {\n size += c.length;\n if (size > limit) {\n reject(new Error('body too large'));\n req.destroy();\n return;\n }\n chunks.push(c);\n });\n req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));\n req.on('error', reject);\n });\n}\n\nexport function createRelayServer(options: RelayOptions = {}): Promise<RelayServer> {\n const host = options.host ?? '127.0.0.1';\n const panelDir = options.panelDir === undefined ? findPanelDir() : options.panelDir;\n const keepAliveMs = options.keepAliveMs ?? 15_000;\n const token = options.token || null;\n const apps = new Map<ServerResponse, RelayApp>();\n const panels = new Set<ServerResponse>();\n let url = '';\n let nextAppId = 1;\n const connectUrl = (): string => (token ? `${url}?token=${encodeURIComponent(token)}` : url);\n\n const line = (message: unknown): string => `data: ${JSON.stringify(message)}\\n\\n`;\n const sendTo = (res: ServerResponse, message: unknown): void => {\n try {\n res.write(line(message));\n } catch {\n apps.delete(res);\n panels.delete(res);\n }\n };\n const send = (targets: Iterable<ServerResponse>, message: unknown): void => {\n const text = line(message);\n for (const res of [...targets]) {\n try {\n res.write(text);\n } catch {\n apps.delete(res);\n panels.delete(res);\n }\n }\n };\n\n const appList = (): RelayApp[] => [...apps.values()];\n const roster = (): unknown => ({ [MARKER]: true, version: PROTOCOL, type: 'relay', payload: { apps: apps.size, list: appList() } });\n const streamOf = (id: string): ServerResponse | null => {\n for (const [res, app] of apps) if (app.id === id) return res;\n return null;\n };\n\n const route = (message: unknown, from?: RelayApp): void => {\n if (!message || typeof message !== 'object') return;\n const m = message as Record<string, unknown>;\n if (m.__rerenderLensCmd === true) {\n // A command names its app when the panel is watching one of several; otherwise every app answers.\n const target = typeof m.app === 'string' ? streamOf(m.app) : null;\n if (target) sendTo(target, m);\n else send(apps.keys(), m);\n } else if (m.__rerenderLens === true || m.__rerenderLensReply === true) {\n send(panels, from ? { ...m, app: from.id } : m);\n }\n };\n\n /** With a token set, everything that carries data needs it; the static panel files do not. */\n const authorized = (req: IncomingMessage, u: URL): boolean => {\n if (!token) return true;\n const given = u.searchParams.get('token') || (typeof req.headers['x-rerender-lens-token'] === 'string' ? req.headers['x-rerender-lens-token'] : '');\n return given === token;\n };\n\n const server = createServer(async (req, res) => {\n cors(res);\n const u = new URL(req.url || '/', 'http://relay');\n if (req.method === 'OPTIONS') {\n res.statusCode = 204;\n res.end();\n return;\n }\n if (u.pathname === '/' && req.method === 'GET') {\n res.statusCode = 302;\n res.setHeader('Location', `/panel.html?relay=${encodeURIComponent(connectUrl())}`);\n res.end();\n return;\n }\n if (/^\\/(events|message|status)$/.test(u.pathname) && !authorized(req, u)) {\n res.statusCode = 401;\n res.setHeader('Content-Type', 'text/plain');\n res.end('rerender-lens relay: this relay needs the token from the URL it printed (?token=...)');\n return;\n }\n if (u.pathname === '/events' && req.method === 'GET') {\n const isApp = u.searchParams.get('role') === 'app';\n res.statusCode = 200;\n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-store');\n res.setHeader('Connection', 'keep-alive');\n res.write(': connected\\n\\n');\n const app: RelayApp | null = isApp ? { id: `a${nextAppId++}`, label: (u.searchParams.get('label') || req.headers.origin || '').slice(0, 120) || `app ${nextAppId - 1}` } : null;\n if (app) apps.set(res, app);\n else panels.add(res);\n const ping = setInterval(() => {\n try {\n res.write(': ping\\n\\n');\n } catch {\n /* closed */\n }\n }, keepAliveMs);\n req.on('close', () => {\n clearInterval(ping);\n apps.delete(res);\n panels.delete(res);\n });\n // Panels learn the roster as soon as their stream is open (that first message is also how a\n // panel knows it is safe to send commands: replies only reach panels that are already connected),\n // and again whenever an app comes or goes, so they show \"disconnected\" instead of waiting.\n if (app) {\n sendTo(res, { __rerenderLensRelay: true, version: PROTOCOL, app: app.id }); // the app stamps this on what it sends\n send(panels, roster());\n req.on('close', () => send(panels, roster()));\n } else {\n sendTo(res, roster());\n }\n return;\n }\n if (u.pathname === '/favicon.ico') {\n res.statusCode = 204;\n res.end();\n return;\n }\n if (u.pathname === '/message' && req.method === 'POST') {\n try {\n const parsed = JSON.parse((await readBody(req)) || 'null') as unknown;\n const sender = u.searchParams.get('app');\n const from = sender ? (appList().find((a) => a.id === sender) ?? undefined) : undefined;\n for (const m of Array.isArray(parsed) ? parsed : [parsed]) route(m, from);\n res.statusCode = 204;\n res.end();\n } catch (e) {\n res.statusCode = 400;\n res.setHeader('Content-Type', 'text/plain');\n res.end(String((e as Error).message || e));\n }\n return;\n }\n if (u.pathname === '/status' && req.method === 'GET') {\n res.statusCode = 200;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ ok: true, apps: apps.size, panels: panels.size, list: appList() }));\n return;\n }\n const file = u.pathname.replace(/^\\//, '');\n if (/^panel\\.(html|js|css)$/.test(file) && req.method === 'GET') {\n if (!panelDir) {\n res.statusCode = 500;\n res.setHeader('Content-Type', 'text/plain');\n res.end('rerender-lens: panel files not found (is the package built?)');\n return;\n }\n res.statusCode = 200;\n res.setHeader('Content-Type', MIME[file.slice(file.lastIndexOf('.'))] || 'application/octet-stream');\n res.setHeader('Cache-Control', 'no-store');\n res.end(readFileSync(join(panelDir, file)));\n return;\n }\n res.statusCode = 404;\n res.setHeader('Content-Type', 'text/plain');\n res.end('not found');\n });\n\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(options.port ?? 4141, host, () => {\n const addr = server.address();\n const port = typeof addr === 'object' && addr ? addr.port : options.port ?? 4141;\n url = `http://${host === '0.0.0.0' || host === '::' ? 'localhost' : host}:${port}`;\n resolve({\n server,\n url,\n connectUrl: connectUrl(),\n port,\n counts: () => ({ apps: apps.size, panels: panels.size }),\n appList,\n close: () =>\n new Promise<void>((done) => {\n for (const res of [...apps.keys(), ...panels]) res.end();\n apps.clear();\n panels.clear();\n server.close(() => done());\n }),\n });\n });\n });\n}\n"]}
package/dist/relay.d.cts CHANGED
@@ -7,12 +7,20 @@ import { Server } from 'node:http';
7
7
  *
8
8
  * GET / redirect to /panel.html?relay=<this server>
9
9
  * GET /panel.(html|js|css) the panel page
10
- * GET /events?role=app SSE: commands from panels
10
+ * GET /events?role=app SSE: commands from panels (first message: this app's id)
11
11
  * GET /events?role=panel SSE: hello / report / clear from apps, replies to commands
12
12
  * POST /message one message or an array; `{ __rerenderLens }` and replies go to
13
13
  * panels, `{ __rerenderLensCmd }` goes to apps
14
14
  *
15
- * Dependency-free (node:http). CORS is open: the relay is a local dev tool, bind it to 127.0.0.1.
15
+ * Several apps can share one relay. Each app connection gets an id; the relay tells the app its id,
16
+ * stamps `app` on everything that app sends, and sends panels the roster whenever it changes. A
17
+ * command carrying `app` goes to that one app, a command without it goes to all of them, so a panel
18
+ * or a library that knows nothing about ids behaves exactly as before.
19
+ *
20
+ * Dependency-free (node:http). Bound to 127.0.0.1 by default, where CORS is open and no token is
21
+ * needed. On any other host `rerender-lens panel` generates a token: `/events`, `/message` and
22
+ * `/status` then require it (`?token=` or the `x-rerender-lens-token` header) and the URL it prints
23
+ * carries it, because reports contain the app's prop, state and context values.
16
24
  */
17
25
 
18
26
  interface RelayOptions {
@@ -22,11 +30,23 @@ interface RelayOptions {
22
30
  panelDir?: string | null;
23
31
  /** Milliseconds between SSE keep-alive comments. Default 15000. */
24
32
  keepAliveMs?: number;
33
+ /**
34
+ * Require this token on `/events`, `/message` and `/status`. `rerender-lens panel` generates one
35
+ * whenever it binds to anything but loopback; the URL it prints carries it as `?token=`.
36
+ */
37
+ token?: string;
38
+ }
39
+ /** One connected app page. `label` is what the panel's app chooser shows. */
40
+ interface RelayApp {
41
+ id: string;
42
+ label: string;
25
43
  }
26
44
  interface RelayServer {
27
45
  server: Server;
28
46
  /** `http://127.0.0.1:4141` once listening. */
29
47
  url: string;
48
+ /** The URL to hand to a panel or an app: `url`, plus `?token=` when one is required. */
49
+ connectUrl: string;
30
50
  port: number;
31
51
  close(): Promise<void>;
32
52
  /** Connected app and panel streams. */
@@ -34,8 +54,10 @@ interface RelayServer {
34
54
  apps: number;
35
55
  panels: number;
36
56
  };
57
+ /** The connected apps, in connection order. */
58
+ appList(): RelayApp[];
37
59
  }
38
60
  declare function findPanelDir(): string | null;
39
61
  declare function createRelayServer(options?: RelayOptions): Promise<RelayServer>;
40
62
 
41
- export { type RelayOptions, type RelayServer, createRelayServer, findPanelDir };
63
+ export { type RelayApp, type RelayOptions, type RelayServer, createRelayServer, findPanelDir };
package/dist/relay.d.ts CHANGED
@@ -7,12 +7,20 @@ import { Server } from 'node:http';
7
7
  *
8
8
  * GET / redirect to /panel.html?relay=<this server>
9
9
  * GET /panel.(html|js|css) the panel page
10
- * GET /events?role=app SSE: commands from panels
10
+ * GET /events?role=app SSE: commands from panels (first message: this app's id)
11
11
  * GET /events?role=panel SSE: hello / report / clear from apps, replies to commands
12
12
  * POST /message one message or an array; `{ __rerenderLens }` and replies go to
13
13
  * panels, `{ __rerenderLensCmd }` goes to apps
14
14
  *
15
- * Dependency-free (node:http). CORS is open: the relay is a local dev tool, bind it to 127.0.0.1.
15
+ * Several apps can share one relay. Each app connection gets an id; the relay tells the app its id,
16
+ * stamps `app` on everything that app sends, and sends panels the roster whenever it changes. A
17
+ * command carrying `app` goes to that one app, a command without it goes to all of them, so a panel
18
+ * or a library that knows nothing about ids behaves exactly as before.
19
+ *
20
+ * Dependency-free (node:http). Bound to 127.0.0.1 by default, where CORS is open and no token is
21
+ * needed. On any other host `rerender-lens panel` generates a token: `/events`, `/message` and
22
+ * `/status` then require it (`?token=` or the `x-rerender-lens-token` header) and the URL it prints
23
+ * carries it, because reports contain the app's prop, state and context values.
16
24
  */
17
25
 
18
26
  interface RelayOptions {
@@ -22,11 +30,23 @@ interface RelayOptions {
22
30
  panelDir?: string | null;
23
31
  /** Milliseconds between SSE keep-alive comments. Default 15000. */
24
32
  keepAliveMs?: number;
33
+ /**
34
+ * Require this token on `/events`, `/message` and `/status`. `rerender-lens panel` generates one
35
+ * whenever it binds to anything but loopback; the URL it prints carries it as `?token=`.
36
+ */
37
+ token?: string;
38
+ }
39
+ /** One connected app page. `label` is what the panel's app chooser shows. */
40
+ interface RelayApp {
41
+ id: string;
42
+ label: string;
25
43
  }
26
44
  interface RelayServer {
27
45
  server: Server;
28
46
  /** `http://127.0.0.1:4141` once listening. */
29
47
  url: string;
48
+ /** The URL to hand to a panel or an app: `url`, plus `?token=` when one is required. */
49
+ connectUrl: string;
30
50
  port: number;
31
51
  close(): Promise<void>;
32
52
  /** Connected app and panel streams. */
@@ -34,8 +54,10 @@ interface RelayServer {
34
54
  apps: number;
35
55
  panels: number;
36
56
  };
57
+ /** The connected apps, in connection order. */
58
+ appList(): RelayApp[];
37
59
  }
38
60
  declare function findPanelDir(): string | null;
39
61
  declare function createRelayServer(options?: RelayOptions): Promise<RelayServer>;
40
62
 
41
- export { type RelayOptions, type RelayServer, createRelayServer, findPanelDir };
63
+ export { type RelayApp, type RelayOptions, type RelayServer, createRelayServer, findPanelDir };
package/dist/relay.js CHANGED
@@ -4,6 +4,8 @@ import { dirname, join } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
 
6
6
  // src/relay.ts
7
+ var MARKER = "__rerenderLens";
8
+ var PROTOCOL = 2;
7
9
  var MIME = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8" };
8
10
  function findPanelDir() {
9
11
  const here = dirname(fileURLToPath(import.meta.url));
@@ -38,26 +40,55 @@ function createRelayServer(options = {}) {
38
40
  const host = options.host ?? "127.0.0.1";
39
41
  const panelDir = options.panelDir === void 0 ? findPanelDir() : options.panelDir;
40
42
  const keepAliveMs = options.keepAliveMs ?? 15e3;
41
- const apps = /* @__PURE__ */ new Set();
43
+ const token = options.token || null;
44
+ const apps = /* @__PURE__ */ new Map();
42
45
  const panels = /* @__PURE__ */ new Set();
43
46
  let url = "";
44
- const send = (targets, message) => {
45
- const line = `data: ${JSON.stringify(message)}
47
+ let nextAppId = 1;
48
+ const connectUrl = () => token ? `${url}?token=${encodeURIComponent(token)}` : url;
49
+ const line = (message) => `data: ${JSON.stringify(message)}
46
50
 
47
51
  `;
48
- for (const res of targets) {
52
+ const sendTo = (res, message) => {
53
+ try {
54
+ res.write(line(message));
55
+ } catch {
56
+ apps.delete(res);
57
+ panels.delete(res);
58
+ }
59
+ };
60
+ const send = (targets, message) => {
61
+ const text = line(message);
62
+ for (const res of [...targets]) {
49
63
  try {
50
- res.write(line);
64
+ res.write(text);
51
65
  } catch {
52
- targets.delete(res);
66
+ apps.delete(res);
67
+ panels.delete(res);
53
68
  }
54
69
  }
55
70
  };
56
- const route = (message) => {
71
+ const appList = () => [...apps.values()];
72
+ const roster = () => ({ [MARKER]: true, version: PROTOCOL, type: "relay", payload: { apps: apps.size, list: appList() } });
73
+ const streamOf = (id) => {
74
+ for (const [res, app] of apps) if (app.id === id) return res;
75
+ return null;
76
+ };
77
+ const route = (message, from) => {
57
78
  if (!message || typeof message !== "object") return;
58
79
  const m = message;
59
- if (m.__rerenderLensCmd === true) send(apps, m);
60
- else if (m.__rerenderLens === true || m.__rerenderLensReply === true) send(panels, m);
80
+ if (m.__rerenderLensCmd === true) {
81
+ const target = typeof m.app === "string" ? streamOf(m.app) : null;
82
+ if (target) sendTo(target, m);
83
+ else send(apps.keys(), m);
84
+ } else if (m.__rerenderLens === true || m.__rerenderLensReply === true) {
85
+ send(panels, from ? { ...m, app: from.id } : m);
86
+ }
87
+ };
88
+ const authorized = (req, u) => {
89
+ if (!token) return true;
90
+ const given = u.searchParams.get("token") || (typeof req.headers["x-rerender-lens-token"] === "string" ? req.headers["x-rerender-lens-token"] : "");
91
+ return given === token;
61
92
  };
62
93
  const server = createServer(async (req, res) => {
63
94
  cors(res);
@@ -69,18 +100,26 @@ function createRelayServer(options = {}) {
69
100
  }
70
101
  if (u.pathname === "/" && req.method === "GET") {
71
102
  res.statusCode = 302;
72
- res.setHeader("Location", `/panel.html?relay=${encodeURIComponent(url)}`);
103
+ res.setHeader("Location", `/panel.html?relay=${encodeURIComponent(connectUrl())}`);
73
104
  res.end();
74
105
  return;
75
106
  }
107
+ if (/^\/(events|message|status)$/.test(u.pathname) && !authorized(req, u)) {
108
+ res.statusCode = 401;
109
+ res.setHeader("Content-Type", "text/plain");
110
+ res.end("rerender-lens relay: this relay needs the token from the URL it printed (?token=...)");
111
+ return;
112
+ }
76
113
  if (u.pathname === "/events" && req.method === "GET") {
77
- const role = u.searchParams.get("role") === "app" ? apps : panels;
114
+ const isApp = u.searchParams.get("role") === "app";
78
115
  res.statusCode = 200;
79
116
  res.setHeader("Content-Type", "text/event-stream");
80
117
  res.setHeader("Cache-Control", "no-store");
81
118
  res.setHeader("Connection", "keep-alive");
82
119
  res.write(": connected\n\n");
83
- role.add(res);
120
+ const app = isApp ? { id: `a${nextAppId++}`, label: (u.searchParams.get("label") || req.headers.origin || "").slice(0, 120) || `app ${nextAppId - 1}` } : null;
121
+ if (app) apps.set(res, app);
122
+ else panels.add(res);
84
123
  const ping = setInterval(() => {
85
124
  try {
86
125
  res.write(": ping\n\n");
@@ -89,18 +128,29 @@ function createRelayServer(options = {}) {
89
128
  }, keepAliveMs);
90
129
  req.on("close", () => {
91
130
  clearInterval(ping);
92
- role.delete(res);
131
+ apps.delete(res);
132
+ panels.delete(res);
93
133
  });
94
- if (role === apps) {
95
- send(panels, { __rerenderLens: true, version: 2, type: "relay", payload: { apps: apps.size } });
96
- req.on("close", () => send(panels, { __rerenderLens: true, version: 2, type: "relay", payload: { apps: apps.size } }));
134
+ if (app) {
135
+ sendTo(res, { __rerenderLensRelay: true, version: PROTOCOL, app: app.id });
136
+ send(panels, roster());
137
+ req.on("close", () => send(panels, roster()));
138
+ } else {
139
+ sendTo(res, roster());
97
140
  }
98
141
  return;
99
142
  }
143
+ if (u.pathname === "/favicon.ico") {
144
+ res.statusCode = 204;
145
+ res.end();
146
+ return;
147
+ }
100
148
  if (u.pathname === "/message" && req.method === "POST") {
101
149
  try {
102
150
  const parsed = JSON.parse(await readBody(req) || "null");
103
- for (const m of Array.isArray(parsed) ? parsed : [parsed]) route(m);
151
+ const sender = u.searchParams.get("app");
152
+ const from = sender ? appList().find((a) => a.id === sender) ?? void 0 : void 0;
153
+ for (const m of Array.isArray(parsed) ? parsed : [parsed]) route(m, from);
104
154
  res.statusCode = 204;
105
155
  res.end();
106
156
  } catch (e) {
@@ -113,7 +163,7 @@ function createRelayServer(options = {}) {
113
163
  if (u.pathname === "/status" && req.method === "GET") {
114
164
  res.statusCode = 200;
115
165
  res.setHeader("Content-Type", "application/json");
116
- res.end(JSON.stringify({ ok: true, apps: apps.size, panels: panels.size }));
166
+ res.end(JSON.stringify({ ok: true, apps: apps.size, panels: panels.size, list: appList() }));
117
167
  return;
118
168
  }
119
169
  const file = u.pathname.replace(/^\//, "");
@@ -143,10 +193,12 @@ function createRelayServer(options = {}) {
143
193
  resolve({
144
194
  server,
145
195
  url,
196
+ connectUrl: connectUrl(),
146
197
  port,
147
198
  counts: () => ({ apps: apps.size, panels: panels.size }),
199
+ appList,
148
200
  close: () => new Promise((done) => {
149
- for (const res of [...apps, ...panels]) res.end();
201
+ for (const res of [...apps.keys(), ...panels]) res.end();
150
202
  apps.clear();
151
203
  panels.clear();
152
204
  server.close(() => done());
package/dist/relay.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/relay.ts"],"names":[],"mappings":"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","file":"relay.js","sourcesContent":["/**\n * A tiny local relay (`npx rerender-lens panel`): serves the panel and forwards messages between\n * app pages and panel pages over Server-Sent Events + POST, so any app (Next.js, Webpack, a\n * production-like build) gets the panel without the extension and without a same-origin channel.\n *\n * GET / redirect to /panel.html?relay=<this server>\n * GET /panel.(html|js|css) the panel page\n * GET /events?role=app SSE: commands from panels\n * GET /events?role=panel SSE: hello / report / clear from apps, replies to commands\n * POST /message one message or an array; `{ __rerenderLens }` and replies go to\n * panels, `{ __rerenderLensCmd }` goes to apps\n *\n * Dependency-free (node:http). CORS is open: the relay is a local dev tool, bind it to 127.0.0.1.\n */\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport interface RelayOptions {\n port?: number;\n host?: string;\n /** Directory with panel.html/js/css; found automatically in the package. */\n panelDir?: string | null;\n /** Milliseconds between SSE keep-alive comments. Default 15000. */\n keepAliveMs?: number;\n}\n\nexport interface RelayServer {\n server: Server;\n /** `http://127.0.0.1:4141` once listening. */\n url: string;\n port: number;\n close(): Promise<void>;\n /** Connected app and panel streams. */\n counts(): { apps: number; panels: number };\n}\n\nconst MIME: Record<string, string> = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8' };\n\nexport function findPanelDir(): string | null {\n const here = dirname(fileURLToPath(import.meta.url));\n for (const dir of [join(here, '..', 'panel'), join(here, '..', 'extension'), join(here, '..', '..', 'extension')]) {\n if (existsSync(join(dir, 'panel.html')) && existsSync(join(dir, 'panel.js'))) return dir;\n }\n return null;\n}\n\nconst cors = (res: ServerResponse): void => {\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n};\n\nfunction readBody(req: IncomingMessage, limit = 8 * 1024 * 1024): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n req.on('data', (c: Buffer) => {\n size += c.length;\n if (size > limit) {\n reject(new Error('body too large'));\n req.destroy();\n return;\n }\n chunks.push(c);\n });\n req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));\n req.on('error', reject);\n });\n}\n\nexport function createRelayServer(options: RelayOptions = {}): Promise<RelayServer> {\n const host = options.host ?? '127.0.0.1';\n const panelDir = options.panelDir === undefined ? findPanelDir() : options.panelDir;\n const keepAliveMs = options.keepAliveMs ?? 15_000;\n const apps = new Set<ServerResponse>();\n const panels = new Set<ServerResponse>();\n let url = '';\n\n const send = (targets: Set<ServerResponse>, message: unknown): void => {\n const line = `data: ${JSON.stringify(message)}\\n\\n`;\n for (const res of targets) {\n try {\n res.write(line);\n } catch {\n targets.delete(res);\n }\n }\n };\n\n const route = (message: unknown): void => {\n if (!message || typeof message !== 'object') return;\n const m = message as Record<string, unknown>;\n if (m.__rerenderLensCmd === true) send(apps, m);\n else if (m.__rerenderLens === true || m.__rerenderLensReply === true) send(panels, m);\n };\n\n const server = createServer(async (req, res) => {\n cors(res);\n const u = new URL(req.url || '/', 'http://relay');\n if (req.method === 'OPTIONS') {\n res.statusCode = 204;\n res.end();\n return;\n }\n if (u.pathname === '/' && req.method === 'GET') {\n res.statusCode = 302;\n res.setHeader('Location', `/panel.html?relay=${encodeURIComponent(url)}`);\n res.end();\n return;\n }\n if (u.pathname === '/events' && req.method === 'GET') {\n const role = u.searchParams.get('role') === 'app' ? apps : panels;\n res.statusCode = 200;\n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-store');\n res.setHeader('Connection', 'keep-alive');\n res.write(': connected\\n\\n');\n role.add(res);\n const ping = setInterval(() => {\n try {\n res.write(': ping\\n\\n');\n } catch {\n /* closed */\n }\n }, keepAliveMs);\n req.on('close', () => {\n clearInterval(ping);\n role.delete(res);\n });\n // Tell panels an app went away so they show \"disconnected\" instead of waiting.\n if (role === apps) {\n send(panels, { __rerenderLens: true, version: 2, type: 'relay', payload: { apps: apps.size } });\n req.on('close', () => send(panels, { __rerenderLens: true, version: 2, type: 'relay', payload: { apps: apps.size } }));\n }\n return;\n }\n if (u.pathname === '/message' && req.method === 'POST') {\n try {\n const parsed = JSON.parse((await readBody(req)) || 'null') as unknown;\n for (const m of Array.isArray(parsed) ? parsed : [parsed]) route(m);\n res.statusCode = 204;\n res.end();\n } catch (e) {\n res.statusCode = 400;\n res.setHeader('Content-Type', 'text/plain');\n res.end(String((e as Error).message || e));\n }\n return;\n }\n if (u.pathname === '/status' && req.method === 'GET') {\n res.statusCode = 200;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ ok: true, apps: apps.size, panels: panels.size }));\n return;\n }\n const file = u.pathname.replace(/^\\//, '');\n if (/^panel\\.(html|js|css)$/.test(file) && req.method === 'GET') {\n if (!panelDir) {\n res.statusCode = 500;\n res.setHeader('Content-Type', 'text/plain');\n res.end('rerender-lens: panel files not found (is the package built?)');\n return;\n }\n res.statusCode = 200;\n res.setHeader('Content-Type', MIME[file.slice(file.lastIndexOf('.'))] || 'application/octet-stream');\n res.setHeader('Cache-Control', 'no-store');\n res.end(readFileSync(join(panelDir, file)));\n return;\n }\n res.statusCode = 404;\n res.setHeader('Content-Type', 'text/plain');\n res.end('not found');\n });\n\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(options.port ?? 4141, host, () => {\n const addr = server.address();\n const port = typeof addr === 'object' && addr ? addr.port : options.port ?? 4141;\n url = `http://${host === '0.0.0.0' || host === '::' ? 'localhost' : host}:${port}`;\n resolve({\n server,\n url,\n port,\n counts: () => ({ apps: apps.size, panels: panels.size }),\n close: () =>\n new Promise<void>((done) => {\n for (const res of [...apps, ...panels]) res.end();\n apps.clear();\n panels.clear();\n server.close(() => done());\n }),\n });\n });\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/relay.ts"],"names":[],"mappings":"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","file":"relay.js","sourcesContent":["/**\n * A tiny local relay (`npx rerender-lens panel`): serves the panel and forwards messages between\n * app pages and panel pages over Server-Sent Events + POST, so any app (Next.js, Webpack, a\n * production-like build) gets the panel without the extension and without a same-origin channel.\n *\n * GET / redirect to /panel.html?relay=<this server>\n * GET /panel.(html|js|css) the panel page\n * GET /events?role=app SSE: commands from panels (first message: this app's id)\n * GET /events?role=panel SSE: hello / report / clear from apps, replies to commands\n * POST /message one message or an array; `{ __rerenderLens }` and replies go to\n * panels, `{ __rerenderLensCmd }` goes to apps\n *\n * Several apps can share one relay. Each app connection gets an id; the relay tells the app its id,\n * stamps `app` on everything that app sends, and sends panels the roster whenever it changes. A\n * command carrying `app` goes to that one app, a command without it goes to all of them, so a panel\n * or a library that knows nothing about ids behaves exactly as before.\n *\n * Dependency-free (node:http). Bound to 127.0.0.1 by default, where CORS is open and no token is\n * needed. On any other host `rerender-lens panel` generates a token: `/events`, `/message` and\n * `/status` then require it (`?token=` or the `x-rerender-lens-token` header) and the URL it prints\n * carries it, because reports contain the app's prop, state and context values.\n */\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport interface RelayOptions {\n port?: number;\n host?: string;\n /** Directory with panel.html/js/css; found automatically in the package. */\n panelDir?: string | null;\n /** Milliseconds between SSE keep-alive comments. Default 15000. */\n keepAliveMs?: number;\n /**\n * Require this token on `/events`, `/message` and `/status`. `rerender-lens panel` generates one\n * whenever it binds to anything but loopback; the URL it prints carries it as `?token=`.\n */\n token?: string;\n}\n\n/** One connected app page. `label` is what the panel's app chooser shows. */\nexport interface RelayApp {\n id: string;\n label: string;\n}\n\nexport interface RelayServer {\n server: Server;\n /** `http://127.0.0.1:4141` once listening. */\n url: string;\n /** The URL to hand to a panel or an app: `url`, plus `?token=` when one is required. */\n connectUrl: string;\n port: number;\n close(): Promise<void>;\n /** Connected app and panel streams. */\n counts(): { apps: number; panels: number };\n /** The connected apps, in connection order. */\n appList(): RelayApp[];\n}\n\n/** Same marker and protocol the library and the panel use (`src/devtools.ts`); kept literal so the relay stays dependency-free. */\nconst MARKER = '__rerenderLens';\nconst PROTOCOL = 2;\n\nconst MIME: Record<string, string> = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8' };\n\nexport function findPanelDir(): string | null {\n const here = dirname(fileURLToPath(import.meta.url));\n for (const dir of [join(here, '..', 'panel'), join(here, '..', 'extension'), join(here, '..', '..', 'extension')]) {\n if (existsSync(join(dir, 'panel.html')) && existsSync(join(dir, 'panel.js'))) return dir;\n }\n return null;\n}\n\nconst cors = (res: ServerResponse): void => {\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n};\n\nfunction readBody(req: IncomingMessage, limit = 8 * 1024 * 1024): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n req.on('data', (c: Buffer) => {\n size += c.length;\n if (size > limit) {\n reject(new Error('body too large'));\n req.destroy();\n return;\n }\n chunks.push(c);\n });\n req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));\n req.on('error', reject);\n });\n}\n\nexport function createRelayServer(options: RelayOptions = {}): Promise<RelayServer> {\n const host = options.host ?? '127.0.0.1';\n const panelDir = options.panelDir === undefined ? findPanelDir() : options.panelDir;\n const keepAliveMs = options.keepAliveMs ?? 15_000;\n const token = options.token || null;\n const apps = new Map<ServerResponse, RelayApp>();\n const panels = new Set<ServerResponse>();\n let url = '';\n let nextAppId = 1;\n const connectUrl = (): string => (token ? `${url}?token=${encodeURIComponent(token)}` : url);\n\n const line = (message: unknown): string => `data: ${JSON.stringify(message)}\\n\\n`;\n const sendTo = (res: ServerResponse, message: unknown): void => {\n try {\n res.write(line(message));\n } catch {\n apps.delete(res);\n panels.delete(res);\n }\n };\n const send = (targets: Iterable<ServerResponse>, message: unknown): void => {\n const text = line(message);\n for (const res of [...targets]) {\n try {\n res.write(text);\n } catch {\n apps.delete(res);\n panels.delete(res);\n }\n }\n };\n\n const appList = (): RelayApp[] => [...apps.values()];\n const roster = (): unknown => ({ [MARKER]: true, version: PROTOCOL, type: 'relay', payload: { apps: apps.size, list: appList() } });\n const streamOf = (id: string): ServerResponse | null => {\n for (const [res, app] of apps) if (app.id === id) return res;\n return null;\n };\n\n const route = (message: unknown, from?: RelayApp): void => {\n if (!message || typeof message !== 'object') return;\n const m = message as Record<string, unknown>;\n if (m.__rerenderLensCmd === true) {\n // A command names its app when the panel is watching one of several; otherwise every app answers.\n const target = typeof m.app === 'string' ? streamOf(m.app) : null;\n if (target) sendTo(target, m);\n else send(apps.keys(), m);\n } else if (m.__rerenderLens === true || m.__rerenderLensReply === true) {\n send(panels, from ? { ...m, app: from.id } : m);\n }\n };\n\n /** With a token set, everything that carries data needs it; the static panel files do not. */\n const authorized = (req: IncomingMessage, u: URL): boolean => {\n if (!token) return true;\n const given = u.searchParams.get('token') || (typeof req.headers['x-rerender-lens-token'] === 'string' ? req.headers['x-rerender-lens-token'] : '');\n return given === token;\n };\n\n const server = createServer(async (req, res) => {\n cors(res);\n const u = new URL(req.url || '/', 'http://relay');\n if (req.method === 'OPTIONS') {\n res.statusCode = 204;\n res.end();\n return;\n }\n if (u.pathname === '/' && req.method === 'GET') {\n res.statusCode = 302;\n res.setHeader('Location', `/panel.html?relay=${encodeURIComponent(connectUrl())}`);\n res.end();\n return;\n }\n if (/^\\/(events|message|status)$/.test(u.pathname) && !authorized(req, u)) {\n res.statusCode = 401;\n res.setHeader('Content-Type', 'text/plain');\n res.end('rerender-lens relay: this relay needs the token from the URL it printed (?token=...)');\n return;\n }\n if (u.pathname === '/events' && req.method === 'GET') {\n const isApp = u.searchParams.get('role') === 'app';\n res.statusCode = 200;\n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-store');\n res.setHeader('Connection', 'keep-alive');\n res.write(': connected\\n\\n');\n const app: RelayApp | null = isApp ? { id: `a${nextAppId++}`, label: (u.searchParams.get('label') || req.headers.origin || '').slice(0, 120) || `app ${nextAppId - 1}` } : null;\n if (app) apps.set(res, app);\n else panels.add(res);\n const ping = setInterval(() => {\n try {\n res.write(': ping\\n\\n');\n } catch {\n /* closed */\n }\n }, keepAliveMs);\n req.on('close', () => {\n clearInterval(ping);\n apps.delete(res);\n panels.delete(res);\n });\n // Panels learn the roster as soon as their stream is open (that first message is also how a\n // panel knows it is safe to send commands: replies only reach panels that are already connected),\n // and again whenever an app comes or goes, so they show \"disconnected\" instead of waiting.\n if (app) {\n sendTo(res, { __rerenderLensRelay: true, version: PROTOCOL, app: app.id }); // the app stamps this on what it sends\n send(panels, roster());\n req.on('close', () => send(panels, roster()));\n } else {\n sendTo(res, roster());\n }\n return;\n }\n if (u.pathname === '/favicon.ico') {\n res.statusCode = 204;\n res.end();\n return;\n }\n if (u.pathname === '/message' && req.method === 'POST') {\n try {\n const parsed = JSON.parse((await readBody(req)) || 'null') as unknown;\n const sender = u.searchParams.get('app');\n const from = sender ? (appList().find((a) => a.id === sender) ?? undefined) : undefined;\n for (const m of Array.isArray(parsed) ? parsed : [parsed]) route(m, from);\n res.statusCode = 204;\n res.end();\n } catch (e) {\n res.statusCode = 400;\n res.setHeader('Content-Type', 'text/plain');\n res.end(String((e as Error).message || e));\n }\n return;\n }\n if (u.pathname === '/status' && req.method === 'GET') {\n res.statusCode = 200;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ ok: true, apps: apps.size, panels: panels.size, list: appList() }));\n return;\n }\n const file = u.pathname.replace(/^\\//, '');\n if (/^panel\\.(html|js|css)$/.test(file) && req.method === 'GET') {\n if (!panelDir) {\n res.statusCode = 500;\n res.setHeader('Content-Type', 'text/plain');\n res.end('rerender-lens: panel files not found (is the package built?)');\n return;\n }\n res.statusCode = 200;\n res.setHeader('Content-Type', MIME[file.slice(file.lastIndexOf('.'))] || 'application/octet-stream');\n res.setHeader('Cache-Control', 'no-store');\n res.end(readFileSync(join(panelDir, file)));\n return;\n }\n res.statusCode = 404;\n res.setHeader('Content-Type', 'text/plain');\n res.end('not found');\n });\n\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(options.port ?? 4141, host, () => {\n const addr = server.address();\n const port = typeof addr === 'object' && addr ? addr.port : options.port ?? 4141;\n url = `http://${host === '0.0.0.0' || host === '::' ? 'localhost' : host}:${port}`;\n resolve({\n server,\n url,\n connectUrl: connectUrl(),\n port,\n counts: () => ({ apps: apps.size, panels: panels.size }),\n appList,\n close: () =>\n new Promise<void>((done) => {\n for (const res of [...apps.keys(), ...panels]) res.end();\n apps.clear();\n panels.clear();\n server.close(() => done());\n }),\n });\n });\n });\n}\n"]}