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.
- package/CHANGELOG.md +127 -0
- package/README.md +208 -359
- package/dist/{budget-LkNjGtRc.d.ts → budget-CA1MS7lB.d.cts} +20 -10
- package/dist/{budget-COBu7jBU.d.cts → budget-ok1VDJ4t.d.ts} +20 -10
- package/dist/cli.cjs +322 -53
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +322 -53
- package/dist/cli.js.map +1 -1
- package/dist/{devtools-BkCct3cJ.d.cts → devtools-Cl5n_52v.d.ts} +17 -2
- package/dist/{devtools-CZebzpF6.d.ts → devtools-UwQx2VYS.d.cts} +17 -2
- package/dist/index.cjs +537 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +137 -8
- package/dist/index.d.ts +137 -8
- package/dist/index.js +521 -50
- package/dist/index.js.map +1 -1
- package/dist/jest-setup.cjs +1690 -0
- package/dist/jest-setup.cjs.map +1 -0
- package/dist/jest-setup.d.cts +8 -0
- package/dist/jest-setup.d.ts +8 -0
- package/dist/jest-setup.js +1688 -0
- package/dist/jest-setup.js.map +1 -0
- package/dist/jest.cjs +1777 -0
- package/dist/jest.cjs.map +1 -0
- package/dist/jest.d.cts +23 -0
- package/dist/jest.d.ts +23 -0
- package/dist/jest.js +1768 -0
- package/dist/jest.js.map +1 -0
- package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-B5poAEvE.d.cts} +8 -4
- package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-CqQW0Eum.d.ts} +8 -4
- package/dist/playwright.cjs +230 -28
- package/dist/playwright.cjs.map +1 -1
- package/dist/playwright.d.cts +4 -4
- package/dist/playwright.d.ts +4 -4
- package/dist/playwright.js +230 -28
- package/dist/playwright.js.map +1 -1
- package/dist/relay.cjs +71 -19
- package/dist/relay.cjs.map +1 -1
- package/dist/relay.d.cts +25 -3
- package/dist/relay.d.ts +25 -3
- package/dist/relay.js +71 -19
- package/dist/relay.js.map +1 -1
- package/dist/rerender-lens.iife.js +457 -62
- package/dist/runner-report-Bvqoe89M.d.ts +46 -0
- package/dist/runner-report-CvpDg2zt.d.cts +46 -0
- package/dist/setup.cjs +209 -35
- package/dist/setup.cjs.map +1 -1
- package/dist/setup.js +209 -35
- package/dist/setup.js.map +1 -1
- package/dist/{types-BzUEVkxJ.d.cts → types-DwNN5cWe.d.cts} +82 -2
- package/dist/{types-BzUEVkxJ.d.ts → types-DwNN5cWe.d.ts} +82 -2
- package/dist/vite.d.cts +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vitest-setup.cjs +357 -44
- package/dist/vitest-setup.cjs.map +1 -1
- package/dist/vitest-setup.d.cts +4 -4
- package/dist/vitest-setup.d.ts +4 -4
- package/dist/vitest-setup.js +357 -44
- package/dist/vitest-setup.js.map +1 -1
- package/dist/vitest.cjs +420 -80
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.d.cts +21 -39
- package/dist/vitest.d.ts +21 -39
- package/dist/vitest.js +421 -81
- package/dist/vitest.js.map +1 -1
- package/package.json +41 -4
- package/panel/panel.css +49 -0
- 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
|
|
47
|
+
const token = options.token || null;
|
|
48
|
+
const apps = /* @__PURE__ */ new Map();
|
|
46
49
|
const panels = /* @__PURE__ */ new Set();
|
|
47
50
|
let url = "";
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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(
|
|
68
|
+
res.write(text);
|
|
55
69
|
} catch {
|
|
56
|
-
|
|
70
|
+
apps.delete(res);
|
|
71
|
+
panels.delete(res);
|
|
57
72
|
}
|
|
58
73
|
}
|
|
59
74
|
};
|
|
60
|
-
const
|
|
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)
|
|
64
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
135
|
+
apps.delete(res);
|
|
136
|
+
panels.delete(res);
|
|
97
137
|
});
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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());
|
package/dist/relay.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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\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":";;;;;;;;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;ACkD9D,IAAM,MAAA,GAAS,gBAAA;AACf,IAAM,QAAA,GAAW,CAAA;AAEjB,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,KAAA,GAAQ,QAAQ,KAAA,IAAS,IAAA;AAC/B,EAAA,MAAM,IAAA,uBAAW,GAAA,EAA8B;AAC/C,EAAA,MAAM,MAAA,uBAAa,GAAA,EAAoB;AACvC,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,IAAI,SAAA,GAAY,CAAA;AAChB,EAAA,MAAM,UAAA,GAAa,MAAe,KAAA,GAAQ,CAAA,EAAG,GAAG,CAAA,OAAA,EAAU,kBAAA,CAAmB,KAAK,CAAC,CAAA,CAAA,GAAK,GAAA;AAExF,EAAA,MAAM,OAAO,CAAC,OAAA,KAA6B,SAAS,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC;;AAAA,CAAA;AAC3E,EAAA,MAAM,MAAA,GAAS,CAAC,GAAA,EAAqB,OAAA,KAA2B;AAC9D,IAAA,IAAI;AACF,MAAA,GAAA,CAAI,KAAA,CAAM,IAAA,CAAK,OAAO,CAAC,CAAA;AAAA,IACzB,CAAA,CAAA,MAAQ;AACN,MAAA,IAAA,CAAK,OAAO,GAAG,CAAA;AACf,MAAA,MAAA,CAAO,OAAO,GAAG,CAAA;AAAA,IACnB;AAAA,EACF,CAAA;AACA,EAAA,MAAM,IAAA,GAAO,CAAC,OAAA,EAAmC,OAAA,KAA2B;AAC1E,IAAA,MAAM,IAAA,GAAO,KAAK,OAAO,CAAA;AACzB,IAAA,KAAA,MAAW,GAAA,IAAO,CAAC,GAAG,OAAO,CAAA,EAAG;AAC9B,MAAA,IAAI;AACF,QAAA,GAAA,CAAI,MAAM,IAAI,CAAA;AAAA,MAChB,CAAA,CAAA,MAAQ;AACN,QAAA,IAAA,CAAK,OAAO,GAAG,CAAA;AACf,QAAA,MAAA,CAAO,OAAO,GAAG,CAAA;AAAA,MACnB;AAAA,IACF;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,UAAU,MAAkB,CAAC,GAAG,IAAA,CAAK,QAAQ,CAAA;AACnD,EAAA,MAAM,SAAS,OAAgB,EAAE,CAAC,MAAM,GAAG,MAAM,OAAA,EAAS,QAAA,EAAU,MAAM,OAAA,EAAS,OAAA,EAAS,EAAE,IAAA,EAAM,IAAA,CAAK,MAAM,IAAA,EAAM,OAAA,IAAU,EAAE,CAAA;AACjI,EAAA,MAAM,QAAA,GAAW,CAAC,EAAA,KAAsC;AACtD,IAAA,KAAA,MAAW,CAAC,KAAK,GAAG,CAAA,IAAK,MAAM,IAAI,GAAA,CAAI,EAAA,KAAO,EAAA,EAAI,OAAO,GAAA;AACzD,IAAA,OAAO,IAAA;AAAA,EACT,CAAA;AAEA,EAAA,MAAM,KAAA,GAAQ,CAAC,OAAA,EAAkB,IAAA,KAA0B;AACzD,IAAA,IAAI,CAAC,OAAA,IAAW,OAAO,OAAA,KAAY,QAAA,EAAU;AAC7C,IAAA,MAAM,CAAA,GAAI,OAAA;AACV,IAAA,IAAI,CAAA,CAAE,sBAAsB,IAAA,EAAM;AAEhC,MAAA,MAAM,MAAA,GAAS,OAAO,CAAA,CAAE,GAAA,KAAQ,WAAW,QAAA,CAAS,CAAA,CAAE,GAAG,CAAA,GAAI,IAAA;AAC7D,MAAA,IAAI,MAAA,EAAQ,MAAA,CAAO,MAAA,EAAQ,CAAC,CAAA;AAAA,WACvB,IAAA,CAAK,IAAA,CAAK,IAAA,EAAK,EAAG,CAAC,CAAA;AAAA,IAC1B,WAAW,CAAA,CAAE,cAAA,KAAmB,IAAA,IAAQ,CAAA,CAAE,wBAAwB,IAAA,EAAM;AACtE,MAAA,IAAA,CAAK,MAAA,EAAQ,OAAO,EAAE,GAAG,GAAG,GAAA,EAAK,IAAA,CAAK,EAAA,EAAG,GAAI,CAAC,CAAA;AAAA,IAChD;AAAA,EACF,CAAA;AAGA,EAAA,MAAM,UAAA,GAAa,CAAC,GAAA,EAAsB,CAAA,KAAoB;AAC5D,IAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,IAAA,MAAM,KAAA,GAAQ,CAAA,CAAE,YAAA,CAAa,GAAA,CAAI,OAAO,CAAA,KAAM,OAAO,GAAA,CAAI,OAAA,CAAQ,uBAAuB,CAAA,KAAM,QAAA,GAAW,GAAA,CAAI,OAAA,CAAQ,uBAAuB,CAAA,GAAI,EAAA,CAAA;AAChJ,IAAA,OAAO,KAAA,KAAU,KAAA;AAAA,EACnB,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,mBAAmB,UAAA,EAAY,CAAC,CAAA,CAAE,CAAA;AACjF,MAAA,GAAA,CAAI,GAAA,EAAI;AACR,MAAA;AAAA,IACF;AACA,IAAA,IAAI,6BAAA,CAA8B,KAAK,CAAA,CAAE,QAAQ,KAAK,CAAC,UAAA,CAAW,GAAA,EAAK,CAAC,CAAA,EAAG;AACzE,MAAA,GAAA,CAAI,UAAA,GAAa,GAAA;AACjB,MAAA,GAAA,CAAI,SAAA,CAAU,gBAAgB,YAAY,CAAA;AAC1C,MAAA,GAAA,CAAI,IAAI,sFAAsF,CAAA;AAC9F,MAAA;AAAA,IACF;AACA,IAAA,IAAI,CAAA,CAAE,QAAA,KAAa,SAAA,IAAa,GAAA,CAAI,WAAW,KAAA,EAAO;AACpD,MAAA,MAAM,KAAA,GAAQ,CAAA,CAAE,YAAA,CAAa,GAAA,CAAI,MAAM,CAAA,KAAM,KAAA;AAC7C,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,MAAM,GAAA,GAAuB,KAAA,GAAQ,EAAE,EAAA,EAAI,CAAA,CAAA,EAAI,WAAW,CAAA,CAAA,EAAI,KAAA,EAAA,CAAQ,CAAA,CAAE,YAAA,CAAa,GAAA,CAAI,OAAO,KAAK,GAAA,CAAI,OAAA,CAAQ,MAAA,IAAU,EAAA,EAAI,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA,IAAK,CAAA,IAAA,EAAO,SAAA,GAAY,CAAC,CAAA,CAAA,EAAG,GAAI,IAAA;AAC3K,MAAA,IAAI,GAAA,EAAK,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,GAAG,CAAA;AAAA,WACrB,MAAA,CAAO,IAAI,GAAG,CAAA;AACnB,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;AACf,QAAA,MAAA,CAAO,OAAO,GAAG,CAAA;AAAA,MACnB,CAAC,CAAA;AAID,MAAA,IAAI,GAAA,EAAK;AACP,QAAA,MAAA,CAAO,GAAA,EAAK,EAAE,mBAAA,EAAqB,IAAA,EAAM,SAAS,QAAA,EAAU,GAAA,EAAK,GAAA,CAAI,EAAA,EAAI,CAAA;AACzE,QAAA,IAAA,CAAK,MAAA,EAAQ,QAAQ,CAAA;AACrB,QAAA,GAAA,CAAI,GAAG,OAAA,EAAS,MAAM,KAAK,MAAA,EAAQ,MAAA,EAAQ,CAAC,CAAA;AAAA,MAC9C,CAAA,MAAO;AACL,QAAA,MAAA,CAAO,GAAA,EAAK,QAAQ,CAAA;AAAA,MACtB;AACA,MAAA;AAAA,IACF;AACA,IAAA,IAAI,CAAA,CAAE,aAAa,cAAA,EAAgB;AACjC,MAAA,GAAA,CAAI,UAAA,GAAa,GAAA;AACjB,MAAA,GAAA,CAAI,GAAA,EAAI;AACR,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,MAAM,MAAA,GAAS,CAAA,CAAE,YAAA,CAAa,GAAA,CAAI,KAAK,CAAA;AACvC,QAAA,MAAM,IAAA,GAAO,MAAA,GAAU,OAAA,EAAQ,CAAE,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,MAAM,CAAA,IAAK,KAAA,CAAA,GAAa,KAAA,CAAA;AAC9E,QAAA,KAAA,MAAW,CAAA,IAAK,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,GAAI,MAAA,GAAS,CAAC,MAAM,CAAA,EAAG,KAAA,CAAM,CAAA,EAAG,IAAI,CAAA;AACxE,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,IAAI,IAAA,CAAK,SAAA,CAAU,EAAE,EAAA,EAAI,MAAM,IAAA,EAAM,IAAA,CAAK,IAAA,EAAM,MAAA,EAAQ,OAAO,IAAA,EAAM,IAAA,EAAM,OAAA,EAAQ,EAAG,CAAC,CAAA;AAC3F,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,YAAY,UAAA,EAAW;AAAA,QACvB,IAAA;AAAA,QACA,MAAA,EAAQ,OAAO,EAAE,IAAA,EAAM,KAAK,IAAA,EAAM,MAAA,EAAQ,OAAO,IAAA,EAAK,CAAA;AAAA,QACtD,OAAA;AAAA,QACA,KAAA,EAAO,MACL,IAAI,OAAA,CAAc,CAAC,IAAA,KAAS;AAC1B,UAAA,KAAA,MAAW,GAAA,IAAO,CAAC,GAAG,IAAA,CAAK,IAAA,IAAQ,GAAG,MAAM,CAAA,EAAG,GAAA,CAAI,GAAA,EAAI;AACvD,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 (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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
43
|
+
const token = options.token || null;
|
|
44
|
+
const apps = /* @__PURE__ */ new Map();
|
|
42
45
|
const panels = /* @__PURE__ */ new Set();
|
|
43
46
|
let url = "";
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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(
|
|
64
|
+
res.write(text);
|
|
51
65
|
} catch {
|
|
52
|
-
|
|
66
|
+
apps.delete(res);
|
|
67
|
+
panels.delete(res);
|
|
53
68
|
}
|
|
54
69
|
}
|
|
55
70
|
};
|
|
56
|
-
const
|
|
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)
|
|
60
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
131
|
+
apps.delete(res);
|
|
132
|
+
panels.delete(res);
|
|
93
133
|
});
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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":";;;;;;AA8DA,IAAM,MAAA,GAAS,gBAAA;AACf,IAAM,QAAA,GAAW,CAAA;AAEjB,IAAM,OAA+B,EAAE,OAAA,EAAS,4BAA4B,KAAA,EAAO,gCAAA,EAAkC,QAAQ,yBAAA,EAA0B;AAEhJ,SAAS,YAAA,GAA8B;AAC5C,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,aAAA,CAAc,MAAA,CAAA,IAAA,CAAY,GAAG,CAAC,CAAA;AACnD,EAAA,KAAA,MAAW,OAAO,CAAC,IAAA,CAAK,MAAM,IAAA,EAAM,OAAO,GAAG,IAAA,CAAK,IAAA,EAAM,IAAA,EAAM,WAAW,GAAG,IAAA,CAAK,IAAA,EAAM,MAAM,IAAA,EAAM,WAAW,CAAC,CAAA,EAAG;AACjH,IAAA,IAAI,UAAA,CAAW,IAAA,CAAK,GAAA,EAAK,YAAY,CAAC,CAAA,IAAK,UAAA,CAAW,IAAA,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,KAAA,GAAQ,QAAQ,KAAA,IAAS,IAAA;AAC/B,EAAA,MAAM,IAAA,uBAAW,GAAA,EAA8B;AAC/C,EAAA,MAAM,MAAA,uBAAa,GAAA,EAAoB;AACvC,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,IAAI,SAAA,GAAY,CAAA;AAChB,EAAA,MAAM,UAAA,GAAa,MAAe,KAAA,GAAQ,CAAA,EAAG,GAAG,CAAA,OAAA,EAAU,kBAAA,CAAmB,KAAK,CAAC,CAAA,CAAA,GAAK,GAAA;AAExF,EAAA,MAAM,OAAO,CAAC,OAAA,KAA6B,SAAS,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC;;AAAA,CAAA;AAC3E,EAAA,MAAM,MAAA,GAAS,CAAC,GAAA,EAAqB,OAAA,KAA2B;AAC9D,IAAA,IAAI;AACF,MAAA,GAAA,CAAI,KAAA,CAAM,IAAA,CAAK,OAAO,CAAC,CAAA;AAAA,IACzB,CAAA,CAAA,MAAQ;AACN,MAAA,IAAA,CAAK,OAAO,GAAG,CAAA;AACf,MAAA,MAAA,CAAO,OAAO,GAAG,CAAA;AAAA,IACnB;AAAA,EACF,CAAA;AACA,EAAA,MAAM,IAAA,GAAO,CAAC,OAAA,EAAmC,OAAA,KAA2B;AAC1E,IAAA,MAAM,IAAA,GAAO,KAAK,OAAO,CAAA;AACzB,IAAA,KAAA,MAAW,GAAA,IAAO,CAAC,GAAG,OAAO,CAAA,EAAG;AAC9B,MAAA,IAAI;AACF,QAAA,GAAA,CAAI,MAAM,IAAI,CAAA;AAAA,MAChB,CAAA,CAAA,MAAQ;AACN,QAAA,IAAA,CAAK,OAAO,GAAG,CAAA;AACf,QAAA,MAAA,CAAO,OAAO,GAAG,CAAA;AAAA,MACnB;AAAA,IACF;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,UAAU,MAAkB,CAAC,GAAG,IAAA,CAAK,QAAQ,CAAA;AACnD,EAAA,MAAM,SAAS,OAAgB,EAAE,CAAC,MAAM,GAAG,MAAM,OAAA,EAAS,QAAA,EAAU,MAAM,OAAA,EAAS,OAAA,EAAS,EAAE,IAAA,EAAM,IAAA,CAAK,MAAM,IAAA,EAAM,OAAA,IAAU,EAAE,CAAA;AACjI,EAAA,MAAM,QAAA,GAAW,CAAC,EAAA,KAAsC;AACtD,IAAA,KAAA,MAAW,CAAC,KAAK,GAAG,CAAA,IAAK,MAAM,IAAI,GAAA,CAAI,EAAA,KAAO,EAAA,EAAI,OAAO,GAAA;AACzD,IAAA,OAAO,IAAA;AAAA,EACT,CAAA;AAEA,EAAA,MAAM,KAAA,GAAQ,CAAC,OAAA,EAAkB,IAAA,KAA0B;AACzD,IAAA,IAAI,CAAC,OAAA,IAAW,OAAO,OAAA,KAAY,QAAA,EAAU;AAC7C,IAAA,MAAM,CAAA,GAAI,OAAA;AACV,IAAA,IAAI,CAAA,CAAE,sBAAsB,IAAA,EAAM;AAEhC,MAAA,MAAM,MAAA,GAAS,OAAO,CAAA,CAAE,GAAA,KAAQ,WAAW,QAAA,CAAS,CAAA,CAAE,GAAG,CAAA,GAAI,IAAA;AAC7D,MAAA,IAAI,MAAA,EAAQ,MAAA,CAAO,MAAA,EAAQ,CAAC,CAAA;AAAA,WACvB,IAAA,CAAK,IAAA,CAAK,IAAA,EAAK,EAAG,CAAC,CAAA;AAAA,IAC1B,WAAW,CAAA,CAAE,cAAA,KAAmB,IAAA,IAAQ,CAAA,CAAE,wBAAwB,IAAA,EAAM;AACtE,MAAA,IAAA,CAAK,MAAA,EAAQ,OAAO,EAAE,GAAG,GAAG,GAAA,EAAK,IAAA,CAAK,EAAA,EAAG,GAAI,CAAC,CAAA;AAAA,IAChD;AAAA,EACF,CAAA;AAGA,EAAA,MAAM,UAAA,GAAa,CAAC,GAAA,EAAsB,CAAA,KAAoB;AAC5D,IAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,IAAA,MAAM,KAAA,GAAQ,CAAA,CAAE,YAAA,CAAa,GAAA,CAAI,OAAO,CAAA,KAAM,OAAO,GAAA,CAAI,OAAA,CAAQ,uBAAuB,CAAA,KAAM,QAAA,GAAW,GAAA,CAAI,OAAA,CAAQ,uBAAuB,CAAA,GAAI,EAAA,CAAA;AAChJ,IAAA,OAAO,KAAA,KAAU,KAAA;AAAA,EACnB,CAAA;AAEA,EAAA,MAAM,MAAA,GAAS,YAAA,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,mBAAmB,UAAA,EAAY,CAAC,CAAA,CAAE,CAAA;AACjF,MAAA,GAAA,CAAI,GAAA,EAAI;AACR,MAAA;AAAA,IACF;AACA,IAAA,IAAI,6BAAA,CAA8B,KAAK,CAAA,CAAE,QAAQ,KAAK,CAAC,UAAA,CAAW,GAAA,EAAK,CAAC,CAAA,EAAG;AACzE,MAAA,GAAA,CAAI,UAAA,GAAa,GAAA;AACjB,MAAA,GAAA,CAAI,SAAA,CAAU,gBAAgB,YAAY,CAAA;AAC1C,MAAA,GAAA,CAAI,IAAI,sFAAsF,CAAA;AAC9F,MAAA;AAAA,IACF;AACA,IAAA,IAAI,CAAA,CAAE,QAAA,KAAa,SAAA,IAAa,GAAA,CAAI,WAAW,KAAA,EAAO;AACpD,MAAA,MAAM,KAAA,GAAQ,CAAA,CAAE,YAAA,CAAa,GAAA,CAAI,MAAM,CAAA,KAAM,KAAA;AAC7C,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,MAAM,GAAA,GAAuB,KAAA,GAAQ,EAAE,EAAA,EAAI,CAAA,CAAA,EAAI,WAAW,CAAA,CAAA,EAAI,KAAA,EAAA,CAAQ,CAAA,CAAE,YAAA,CAAa,GAAA,CAAI,OAAO,KAAK,GAAA,CAAI,OAAA,CAAQ,MAAA,IAAU,EAAA,EAAI,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA,IAAK,CAAA,IAAA,EAAO,SAAA,GAAY,CAAC,CAAA,CAAA,EAAG,GAAI,IAAA;AAC3K,MAAA,IAAI,GAAA,EAAK,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,GAAG,CAAA;AAAA,WACrB,MAAA,CAAO,IAAI,GAAG,CAAA;AACnB,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;AACf,QAAA,MAAA,CAAO,OAAO,GAAG,CAAA;AAAA,MACnB,CAAC,CAAA;AAID,MAAA,IAAI,GAAA,EAAK;AACP,QAAA,MAAA,CAAO,GAAA,EAAK,EAAE,mBAAA,EAAqB,IAAA,EAAM,SAAS,QAAA,EAAU,GAAA,EAAK,GAAA,CAAI,EAAA,EAAI,CAAA;AACzE,QAAA,IAAA,CAAK,MAAA,EAAQ,QAAQ,CAAA;AACrB,QAAA,GAAA,CAAI,GAAG,OAAA,EAAS,MAAM,KAAK,MAAA,EAAQ,MAAA,EAAQ,CAAC,CAAA;AAAA,MAC9C,CAAA,MAAO;AACL,QAAA,MAAA,CAAO,GAAA,EAAK,QAAQ,CAAA;AAAA,MACtB;AACA,MAAA;AAAA,IACF;AACA,IAAA,IAAI,CAAA,CAAE,aAAa,cAAA,EAAgB;AACjC,MAAA,GAAA,CAAI,UAAA,GAAa,GAAA;AACjB,MAAA,GAAA,CAAI,GAAA,EAAI;AACR,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,MAAM,MAAA,GAAS,CAAA,CAAE,YAAA,CAAa,GAAA,CAAI,KAAK,CAAA;AACvC,QAAA,MAAM,IAAA,GAAO,MAAA,GAAU,OAAA,EAAQ,CAAE,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,MAAM,CAAA,IAAK,KAAA,CAAA,GAAa,KAAA,CAAA;AAC9E,QAAA,KAAA,MAAW,CAAA,IAAK,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,GAAI,MAAA,GAAS,CAAC,MAAM,CAAA,EAAG,KAAA,CAAM,CAAA,EAAG,IAAI,CAAA;AACxE,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,IAAI,IAAA,CAAK,SAAA,CAAU,EAAE,EAAA,EAAI,MAAM,IAAA,EAAM,IAAA,CAAK,IAAA,EAAM,MAAA,EAAQ,OAAO,IAAA,EAAM,IAAA,EAAM,OAAA,EAAQ,EAAG,CAAC,CAAA;AAC3F,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,IAAI,YAAA,CAAa,IAAA,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,YAAY,UAAA,EAAW;AAAA,QACvB,IAAA;AAAA,QACA,MAAA,EAAQ,OAAO,EAAE,IAAA,EAAM,KAAK,IAAA,EAAM,MAAA,EAAQ,OAAO,IAAA,EAAK,CAAA;AAAA,QACtD,OAAA;AAAA,QACA,KAAA,EAAO,MACL,IAAI,OAAA,CAAc,CAAC,IAAA,KAAS;AAC1B,UAAA,KAAA,MAAW,GAAA,IAAO,CAAC,GAAG,IAAA,CAAK,IAAA,IAAQ,GAAG,MAAM,CAAA,EAAG,GAAA,CAAI,GAAA,EAAI;AACvD,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.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"]}
|