isotone 1.1.2 → 1.2.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/AGENTS.md +84 -6
- package/CHANGELOG.md +46 -0
- package/README.md +64 -14
- package/dist/{IterationPanel-phJRYtUO.d.ts → IterationPanel-Bq77d5NT.d.ts} +3 -1
- package/dist/{chunk-FKEKBZ3E.js → chunk-2FY2JNPG.js} +95 -9
- package/dist/chunk-2FY2JNPG.js.map +1 -0
- package/dist/chunk-BDTB3DFR.js +100 -0
- package/dist/chunk-BDTB3DFR.js.map +1 -0
- package/dist/{chunk-PUXQGUMQ.js → chunk-BTJHQXF7.js} +104 -5
- package/dist/chunk-BTJHQXF7.js.map +1 -0
- package/dist/chunk-JCTWHR7U.js +13 -0
- package/dist/chunk-JCTWHR7U.js.map +1 -0
- package/dist/{chunk-I7CZUPDO.js → chunk-KRS57POF.js} +17 -33
- package/dist/chunk-KRS57POF.js.map +1 -0
- package/dist/chunk-L6O4HYGP.js +1150 -0
- package/dist/chunk-L6O4HYGP.js.map +1 -0
- package/dist/chunk-MX4RQJFV.js +9 -0
- package/dist/chunk-MX4RQJFV.js.map +1 -0
- package/dist/chunk-YPDV3PX4.js +142 -0
- package/dist/chunk-YPDV3PX4.js.map +1 -0
- package/dist/cli.js +1079 -148
- package/dist/cli.js.map +1 -1
- package/dist/core.d.ts +135 -10
- package/dist/core.js +12 -3
- package/dist/index.d.ts +13 -28
- package/dist/index.js +618 -281
- package/dist/index.js.map +1 -1
- package/dist/isotone.global.js +98 -42
- package/dist/serve.d.ts +5 -3
- package/dist/serve.js +60 -23
- package/dist/serve.js.map +1 -1
- package/dist/server.d.ts +7 -0
- package/dist/server.js +24 -11
- package/dist/server.js.map +1 -1
- package/dist/vite-plugin.d.ts +4 -3
- package/dist/vite-plugin.js +48 -128
- package/dist/vite-plugin.js.map +1 -1
- package/docs/blast.md +164 -0
- package/docs/integrations.md +75 -15
- package/docs/migration.md +19 -0
- package/docs/updates.md +110 -4
- package/docs/variants.md +102 -9
- package/package.json +2 -1
- package/dist/chunk-A4DX2V4C.js +0 -7
- package/dist/chunk-A4DX2V4C.js.map +0 -1
- package/dist/chunk-FKEKBZ3E.js.map +0 -1
- package/dist/chunk-I7CZUPDO.js.map +0 -1
- package/dist/chunk-PUXQGUMQ.js.map +0 -1
- package/dist/chunk-RQ2YI5KS.js +0 -24
- package/dist/chunk-RQ2YI5KS.js.map +0 -1
- package/dist/chunk-TGAU3WTF.js +0 -549
- package/dist/chunk-TGAU3WTF.js.map +0 -1
- package/dist/chunk-X6V2AKJL.js +0 -375
- package/dist/chunk-X6V2AKJL.js.map +0 -1
package/dist/serve.d.ts
CHANGED
|
@@ -6,9 +6,11 @@ declare function readBundle(): string;
|
|
|
6
6
|
|
|
7
7
|
declare const DEFAULT_PORT = 4123;
|
|
8
8
|
interface ServeIterationOptions {
|
|
9
|
-
/** Background npm notice, at most once a day. false disables the check
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
/** Background npm notice, at most once a day. false disables the check;
|
|
10
|
+
* 'note' also files an update note for your agent once per new version. */
|
|
11
|
+
checkForUpdates?: boolean | 'note';
|
|
12
|
+
/** Project directory for the suggested update command and for
|
|
13
|
+
* `.isotone/knobs.json`. Defaults to the notes directory. */
|
|
12
14
|
projectRoot?: string;
|
|
13
15
|
/** Defaults to auto (page background); system follows the OS preference. */
|
|
14
16
|
theme?: IterationTheme;
|
package/dist/serve.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
autoUpdateNote,
|
|
3
|
+
readBundle,
|
|
4
|
+
updateResponse
|
|
5
|
+
} from "./chunk-YPDV3PX4.js";
|
|
4
6
|
import {
|
|
5
7
|
HttpError,
|
|
6
8
|
checkOrigin,
|
|
@@ -8,20 +10,42 @@ import {
|
|
|
8
10
|
readNodeJson,
|
|
9
11
|
sendFailure
|
|
10
12
|
} from "./chunk-DDMHJ23I.js";
|
|
13
|
+
import {
|
|
14
|
+
guardedScriptTag
|
|
15
|
+
} from "./chunk-MX4RQJFV.js";
|
|
11
16
|
import {
|
|
12
17
|
UPDATE_ENDPOINT,
|
|
13
18
|
startUpdateCheck
|
|
14
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-KRS57POF.js";
|
|
15
20
|
import {
|
|
16
21
|
NOTES_ENDPOINT,
|
|
17
22
|
ensureFileIgnored,
|
|
18
|
-
notesResponse
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
notesResponse,
|
|
24
|
+
readKnobs
|
|
25
|
+
} from "./chunk-L6O4HYGP.js";
|
|
26
|
+
import "./chunk-JCTWHR7U.js";
|
|
27
|
+
import {
|
|
28
|
+
VARIANTS_ENDPOINT
|
|
29
|
+
} from "./chunk-BTJHQXF7.js";
|
|
21
30
|
|
|
22
31
|
// src/serve/index.ts
|
|
23
32
|
import { createServer } from "http";
|
|
24
33
|
import { dirname, resolve } from "path";
|
|
34
|
+
|
|
35
|
+
// src/serve/mount-config.ts
|
|
36
|
+
function serveMountOptions(origin, projectRoot, opts) {
|
|
37
|
+
const knobs = readKnobs({ root: projectRoot }).length > 0;
|
|
38
|
+
return {
|
|
39
|
+
endpoint: `${origin}${NOTES_ENDPOINT}`,
|
|
40
|
+
...knobs ? { variants: "knobs", variantsEndpoint: `${origin}${VARIANTS_ENDPOINT}` } : {},
|
|
41
|
+
updateEndpoint: `${origin}${UPDATE_ENDPOINT}`,
|
|
42
|
+
key: opts.key ?? "",
|
|
43
|
+
rule: opts.rule ?? {},
|
|
44
|
+
theme: opts.theme
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/serve/index.ts
|
|
25
49
|
var DEFAULT_PORT = 4123;
|
|
26
50
|
var BUNDLE_PATH = "/isotone.js";
|
|
27
51
|
function allowedOrigin(origin) {
|
|
@@ -44,14 +68,8 @@ function cors(req, res) {
|
|
|
44
68
|
res.setHeader("Access-Control-Max-Age", "600");
|
|
45
69
|
}
|
|
46
70
|
}
|
|
47
|
-
function bundleWithMount(origin, opts) {
|
|
48
|
-
const config = JSON.stringify(
|
|
49
|
-
endpoint: `${origin}${NOTES_ENDPOINT}`,
|
|
50
|
-
updateEndpoint: `${origin}${UPDATE_ENDPOINT}`,
|
|
51
|
-
key: opts.key ?? "",
|
|
52
|
-
rule: opts.rule ?? {},
|
|
53
|
-
theme: opts.theme
|
|
54
|
-
});
|
|
71
|
+
function bundleWithMount(origin, opts, projectRoot) {
|
|
72
|
+
const config = JSON.stringify(serveMountOptions(origin, projectRoot, opts));
|
|
55
73
|
return `${readBundle()}
|
|
56
74
|
;window.__isotone&&window.__isotone.mount(${config});
|
|
57
75
|
`;
|
|
@@ -65,7 +83,7 @@ function escapeHtml(v) {
|
|
|
65
83
|
return v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
66
84
|
}
|
|
67
85
|
function indexPage(origin, file) {
|
|
68
|
-
const tag =
|
|
86
|
+
const tag = guardedScriptTag(`${origin}${BUNDLE_PATH}`);
|
|
69
87
|
const mark = bookmarklet(origin);
|
|
70
88
|
return `<!doctype html>
|
|
71
89
|
<html lang="en">
|
|
@@ -89,7 +107,7 @@ function indexPage(origin, file) {
|
|
|
89
107
|
box-shadow: 0 0 10px rgba(74,222,128,.5); }
|
|
90
108
|
h1 { font-family: var(--mono); font-size: 14px; font-weight: 500; letter-spacing: .02em; }
|
|
91
109
|
.sub { color: var(--muted); font-size: 13px; margin-bottom: 40px; }
|
|
92
|
-
.sub code { font-family: var(--mono); font-size: 11.5px; color: var(--fg); }
|
|
110
|
+
.sub code, p.note code { font-family: var(--mono); font-size: 11.5px; color: var(--fg); }
|
|
93
111
|
h2 { font-family: var(--mono); font-size: 10.5px; font-weight: 500;
|
|
94
112
|
letter-spacing: .08em; text-transform: uppercase; color: var(--faint);
|
|
95
113
|
margin: 32px 0 10px; }
|
|
@@ -122,12 +140,14 @@ function indexPage(origin, file) {
|
|
|
122
140
|
<header><span class="dot"></span><h1>Isotone</h1></header>
|
|
123
141
|
<p class="sub">Serving the feedback loop on <code>${escapeHtml(origin)}</code>.</p>
|
|
124
142
|
|
|
125
|
-
<h2>Script
|
|
143
|
+
<h2>Script for your dev pages</h2>
|
|
126
144
|
<div class="card row">
|
|
127
145
|
<pre>${escapeHtml(tag)}</pre>
|
|
128
146
|
<button type="button" class="copy" data-c="${escapeHtml(tag)}"
|
|
129
147
|
onclick="navigator.clipboard.writeText(this.dataset.c);this.textContent='copied';setTimeout(()=>this.textContent='copy',1200)">copy</button>
|
|
130
148
|
</div>
|
|
149
|
+
<p class="note">It loads the panel only on localhost, so it stays inert if the page ships.
|
|
150
|
+
Behind a development flag, a plain <code><script src="${escapeHtml(origin + BUNDLE_PATH)}" defer></code> works too.</p>
|
|
131
151
|
|
|
132
152
|
<h2>Bookmarklet \u2014 drag to your bookmarks bar</h2>
|
|
133
153
|
<a class="bm" href="${escapeHtml(mark)}">\u270E Iteration</a>
|
|
@@ -143,11 +163,14 @@ function indexPage(origin, file) {
|
|
|
143
163
|
}
|
|
144
164
|
function serveIteration(opts) {
|
|
145
165
|
const file = resolve(opts.file);
|
|
166
|
+
const projectRoot = resolve(opts.projectRoot ?? dirname(file));
|
|
146
167
|
const port = opts.port ?? DEFAULT_PORT;
|
|
147
168
|
const host = opts.host ?? "127.0.0.1";
|
|
148
169
|
if (!isLoopback(host)) throw new Error("isotone serve binds only to localhost, 127.0.0.1 or ::1.");
|
|
149
170
|
ensureFileIgnored(file);
|
|
150
|
-
const updates = startUpdateCheck(
|
|
171
|
+
const updates = startUpdateCheck(projectRoot, opts.checkForUpdates !== false);
|
|
172
|
+
if (opts.checkForUpdates === "note") void updates.ready.then(() => autoUpdateNote(file, updates.getSnapshot())).catch(() => {
|
|
173
|
+
});
|
|
151
174
|
let boundPort = port;
|
|
152
175
|
const server = createServer((req, res) => {
|
|
153
176
|
cors(req, res);
|
|
@@ -167,16 +190,18 @@ function serveIteration(opts) {
|
|
|
167
190
|
return;
|
|
168
191
|
}
|
|
169
192
|
if (path === UPDATE_ENDPOINT) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
193
|
+
(method === "POST" ? readNodeJson(req) : Promise.resolve(void 0)).then((body) => {
|
|
194
|
+
const out = updateResponse(file, updates.getSnapshot(), { method, body });
|
|
195
|
+
res.statusCode = out.status;
|
|
196
|
+
for (const [key, value] of Object.entries(out.headers)) res.setHeader(key, value);
|
|
197
|
+
res.end(JSON.stringify(out.body));
|
|
198
|
+
}).catch((error) => sendFailure(res, error));
|
|
174
199
|
return;
|
|
175
200
|
}
|
|
176
201
|
if (path === BUNDLE_PATH && method === "GET") {
|
|
177
202
|
let body;
|
|
178
203
|
try {
|
|
179
|
-
body = bundleWithMount(origin, opts);
|
|
204
|
+
body = bundleWithMount(origin, opts, projectRoot);
|
|
180
205
|
} catch (err) {
|
|
181
206
|
res.statusCode = 500;
|
|
182
207
|
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
@@ -189,6 +214,18 @@ function serveIteration(opts) {
|
|
|
189
214
|
res.end(body);
|
|
190
215
|
return;
|
|
191
216
|
}
|
|
217
|
+
if (path === VARIANTS_ENDPOINT || path.startsWith(`${VARIANTS_ENDPOINT}/`)) {
|
|
218
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
219
|
+
res.setHeader("cache-control", "no-store");
|
|
220
|
+
if (path === VARIANTS_ENDPOINT && method === "GET") {
|
|
221
|
+
res.statusCode = 200;
|
|
222
|
+
res.end(JSON.stringify({ variants: [], active: [], knobs: readKnobs({ root: projectRoot }) }));
|
|
223
|
+
} else {
|
|
224
|
+
res.statusCode = 405;
|
|
225
|
+
res.end(JSON.stringify({ error: "isotone serve has knobs only; file variants need the Vite plugin" }));
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
192
229
|
if (path === NOTES_ENDPOINT) {
|
|
193
230
|
if (method !== "GET" && method !== "POST" && method !== "DELETE") {
|
|
194
231
|
res.statusCode = 405;
|
package/dist/serve.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/serve/index.ts"],"sourcesContent":["/**\n * isotone — the standalone dev server.\n *\n * The plug-and-play path. Run it in the repo you are reviewing and it owns the\n * notes file, serves the endpoint, and hands out the panel as a single script\n * so the host app needs no build integration at all:\n *\n * npx isotone serve\n * # then, in dev only, one line in the host's HTML:\n * <script src=\"http://127.0.0.1:4123/isotone.js\" defer></script>\n *\n * Running it inside the repo is what keeps a note unambiguous: the process that\n * owns the file is the checkout the agent is working in, so a route and a\n * selector always point somewhere. Nothing about which port served the page has\n * to be mapped back to a repo.\n *\n * The Vite plugin remains the two-line convenience for a Vite host; this is for\n * every other runtime, and for a page whose dev server you cannot configure at\n * all (paste the bookmarklet the index page prints).\n */\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { dirname, resolve } from 'node:path';\n\nimport type { RouteGateOptions } from '../core/index';\nimport type { IterationTheme } from '../react/theme';\nimport { checkOrigin, HttpError, isLoopback, readNodeJson, sendFailure } from '../server/http';\nimport { ensureFileIgnored, notesResponse, NOTES_ENDPOINT } from '../store';\n\nimport { readBundle } from '../standalone/bundle';\nimport { startUpdateCheck, UPDATE_ENDPOINT } from '../update';\nexport { readBundle } from '../standalone/bundle';\n\nexport const DEFAULT_PORT = 4123;\nconst BUNDLE_PATH = '/isotone.js';\n\nexport interface ServeIterationOptions {\n /** Background npm notice, at most once a day. false disables the check. */\n checkForUpdates?: boolean;\n /** Project directory for the suggested update command. Defaults to the notes directory. */\n projectRoot?: string;\n /** Defaults to auto (page background); system follows the OS preference. */\n theme?: IterationTheme;\n /** Where notes persist. Git-ignored automatically via `.git/info/exclude`. */\n file: string;\n /** Defaults to 4123. */\n port?: number;\n /** Defaults to 127.0.0.1 — a dev loop has no business on 0.0.0.0. */\n host?: string;\n /** Route visibility rule handed to the panel. */\n rule?: RouteGateOptions;\n /** localStorage namespace for the panel. */\n key?: string;\n}\n\nexport interface ServedIteration {\n port: number;\n url: string;\n bundleUrl: string;\n close(): Promise<void>;\n}\n\n/**\n * Only localhost may talk to this server.\n *\n * The endpoint writes a file, and a wide-open `*` would let any page you happen\n * to visit post notes into the repo you are reviewing. Restricting the allowed\n * origin to loopback keeps a drive-by from a public site out while leaving\n * every real dev origin (any localhost port, http or https) working.\n */\nfunction allowedOrigin(origin: string | undefined): string | null {\n if (!origin) return null;\n try {\n const { hostname, protocol } = new URL(origin);\n const loopback =\n hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname === '::1';\n return loopback && (protocol === 'http:' || protocol === 'https:') ? origin : null;\n } catch {\n return null;\n }\n}\n\nfunction cors(req: IncomingMessage, res: ServerResponse): void {\n const origin = allowedOrigin(req.headers.origin);\n res.setHeader('Vary', 'Origin');\n if (origin) {\n res.setHeader('Access-Control-Allow-Origin', origin);\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n res.setHeader('Access-Control-Max-Age', '600');\n }\n}\n\n/** The bundle plus the mount call, with the config the server itself knows. */\nfunction bundleWithMount(origin: string, opts: ServeIterationOptions): string {\n const config = JSON.stringify({\n endpoint: `${origin}${NOTES_ENDPOINT}`,\n updateEndpoint: `${origin}${UPDATE_ENDPOINT}`,\n key: opts.key ?? '',\n rule: opts.rule ?? {},\n theme: opts.theme,\n });\n return `${readBundle()}\\n;window.__isotone&&window.__isotone.mount(${config});\\n`;\n}\n\nfunction bookmarklet(origin: string): string {\n return `javascript:(function(){var s=document.createElement('script');s.src=${JSON.stringify(\n `${origin}${BUNDLE_PATH}`,\n )};s.defer=true;document.body.appendChild(s);})()`;\n}\n\nfunction escapeHtml(v: string): string {\n return v\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n\nfunction indexPage(origin: string, file: string): string {\n const tag = `<script src=\"${origin}${BUNDLE_PATH}\" defer></script>`;\n const mark = bookmarklet(origin);\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Isotone</title>\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<style>\n * { box-sizing: border-box; margin: 0; }\n :root {\n --bg: #0d0d10; --card: #131317; --line: rgba(255,255,255,.09);\n --fg: #ececf1; --muted: #8f8f9a; --faint: #5f5f6a;\n --sans: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;\n --mono: ui-monospace, \"SF Mono\", Menlo, monospace;\n }\n body { background: var(--bg); color: var(--fg); font: 14px/1.6 var(--sans);\n -webkit-font-smoothing: antialiased; }\n .wrap { max-width: 540px; margin: 0 auto; padding: 12vh 24px 8vh; }\n header { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }\n .dot { width: 7px; height: 7px; border-radius: 9999px; background: #4ade80;\n box-shadow: 0 0 10px rgba(74,222,128,.5); }\n h1 { font-family: var(--mono); font-size: 14px; font-weight: 500; letter-spacing: .02em; }\n .sub { color: var(--muted); font-size: 13px; margin-bottom: 40px; }\n .sub code { font-family: var(--mono); font-size: 11.5px; color: var(--fg); }\n h2 { font-family: var(--mono); font-size: 10.5px; font-weight: 500;\n letter-spacing: .08em; text-transform: uppercase; color: var(--faint);\n margin: 32px 0 10px; }\n .card { background: var(--card); border: 1px solid var(--line);\n border-radius: 12px; padding: 13px 16px; }\n .card.row { display: flex; align-items: center; gap: 14px; }\n .card pre { font-family: var(--mono); font-size: 12px; line-height: 1.7;\n color: var(--fg); overflow-x: auto; white-space: pre;\n scrollbar-width: none; min-width: 0; }\n .card.row pre { flex: 1; }\n .card pre::-webkit-scrollbar { display: none; }\n .card pre .c { color: var(--faint); }\n .copy { flex: none; font-family: var(--mono); font-size: 10px;\n color: var(--faint); background: none; padding: 4px 9px;\n border: 1px solid var(--line); border-radius: 6px; cursor: pointer; }\n .copy:hover { color: var(--fg); border-color: rgba(255,255,255,.2); }\n p.note { color: var(--muted); font-size: 12.5px; margin-top: 10px; }\n a.bm { display: inline-flex; align-items: center; gap: 8px; margin-top: 2px;\n padding: 8px 14px; border: 1px solid var(--line); border-radius: 8px;\n background: var(--card); color: var(--fg); text-decoration: none;\n font-family: var(--mono); font-size: 12px; }\n a.bm:hover { border-color: rgba(255,255,255,.2); }\n footer { margin-top: 48px; padding-top: 16px; border-top: 1px solid var(--line);\n font-family: var(--mono); font-size: 10.5px; color: var(--faint);\n line-height: 2; overflow-wrap: anywhere; }\n</style>\n</head>\n<body>\n<div class=\"wrap\">\n <header><span class=\"dot\"></span><h1>Isotone</h1></header>\n <p class=\"sub\">Serving the feedback loop on <code>${escapeHtml(origin)}</code>.</p>\n\n <h2>Script tag for your dev page</h2>\n <div class=\"card row\">\n <pre>${escapeHtml(tag)}</pre>\n <button type=\"button\" class=\"copy\" data-c=\"${escapeHtml(tag)}\"\n onclick=\"navigator.clipboard.writeText(this.dataset.c);this.textContent='copied';setTimeout(()=>this.textContent='copy',1200)\">copy</button>\n </div>\n\n <h2>Bookmarklet — drag to your bookmarks bar</h2>\n <a class=\"bm\" href=\"${escapeHtml(mark)}\">✎ Iteration</a>\n\n <footer>\n notes → ${escapeHtml(file)}<br>\n loopback origins only\n </footer>\n</div>\n</body>\n</html>\n`;\n}\n\n/** Start the server. Resolves once it is listening. */\nexport function serveIteration(\n opts: ServeIterationOptions,\n): Promise<ServedIteration> {\n const file = resolve(opts.file);\n const port = opts.port ?? DEFAULT_PORT;\n const host = opts.host ?? '127.0.0.1';\n if (!isLoopback(host)) throw new Error('isotone serve binds only to localhost, 127.0.0.1 or ::1.');\n ensureFileIgnored(file);\n const updates = startUpdateCheck(opts.projectRoot ?? dirname(file), opts.checkForUpdates !== false);\n\n // The port we were ASKED for is not necessarily the one we got: 0 means \"any\n // free port\". The panel's endpoint has to be the bound one or the bundle\n // ships a URL that answers nothing.\n let boundPort = port;\n\n const server: Server = createServer((req, res) => {\n cors(req, res);\n const path = new URL(req.url ?? '/', 'http://localhost').pathname;\n const method = req.method ?? 'GET';\n const origin = `http://${host.includes(':') && !host.startsWith('[') ? `[${host}]` : host}:${boundPort}`;\n try {\n if (!isLoopback(new URL(`http://${req.headers.host ?? ''}`).hostname)) throw new HttpError(403, 'host not allowed');\n checkOrigin(req.headers.origin, req.headers.host ?? '', true);\n } catch (error) { sendFailure(res, error); return; }\n\n if (method === 'OPTIONS') {\n res.statusCode = 204;\n res.end();\n return;\n }\n\n if (path === UPDATE_ENDPOINT) {\n res.setHeader('content-type', 'application/json; charset=utf-8');\n res.setHeader('cache-control', 'no-store');\n res.statusCode = method === 'GET' ? 200 : 405;\n res.end(JSON.stringify(method === 'GET' ? updates.getSnapshot() : { error: 'method not allowed' }));\n return;\n }\n\n if (path === BUNDLE_PATH && method === 'GET') {\n let body: string;\n try {\n body = bundleWithMount(origin, opts);\n } catch (err) {\n res.statusCode = 500;\n res.setHeader('content-type', 'text/plain; charset=utf-8');\n res.end(String(err instanceof Error ? err.message : err));\n return;\n }\n res.statusCode = 200;\n res.setHeader('content-type', 'text/javascript; charset=utf-8');\n res.setHeader('cache-control', 'no-store');\n res.end(body);\n return;\n }\n\n if (path === NOTES_ENDPOINT) {\n if (method !== 'GET' && method !== 'POST' && method !== 'DELETE') {\n res.statusCode = 405;\n res.setHeader('content-type', 'application/json; charset=utf-8');\n res.end(JSON.stringify({ error: 'method not allowed' }));\n return;\n }\n (method === 'POST' ? readNodeJson(req) : Promise.resolve(undefined)).then((body) => {\n const out = notesResponse(file, { method, url: req.url, body });\n res.statusCode = out.status;\n for (const [key, value] of Object.entries(out.headers)) res.setHeader(key, value);\n res.end(JSON.stringify(out.body));\n }).catch((error) => sendFailure(res, error));\n return;\n }\n\n if (path === '/' && method === 'GET') {\n res.statusCode = 200;\n res.setHeader('content-type', 'text/html; charset=utf-8');\n res.end(indexPage(origin, file));\n return;\n }\n\n res.statusCode = 404;\n res.setHeader('content-type', 'text/plain; charset=utf-8');\n res.end('not found');\n });\n\n server.once('close', updates.dispose);\n return new Promise((ok, fail) => {\n server.once('error', (error) => { updates.dispose(); fail(error); });\n server.listen(port, host, () => {\n const actual = server.address();\n boundPort = typeof actual === 'object' && actual ? actual.port : port;\n const url = `http://${host.includes(':') && !host.startsWith('[') ? `[${host}]` : host}:${boundPort}`;\n ok({\n port: boundPort,\n url,\n bundleUrl: `${url}${BUNDLE_PATH}`,\n close: () =>\n new Promise<void>((done) => {\n server.close(() => done());\n }),\n });\n });\n });\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/serve/index.ts","../src/serve/mount-config.ts"],"sourcesContent":["/**\n * isotone — the standalone dev server.\n *\n * The plug-and-play path. Run it in the repo you are reviewing and it owns the\n * notes file, serves the endpoint, and hands out the panel as a single script\n * so the host app needs no build integration at all:\n *\n * npx isotone serve\n * # then, in dev only, one line in the host's HTML:\n * <script src=\"http://127.0.0.1:4123/isotone.js\" defer></script>\n *\n * Running it inside the repo is what keeps a note unambiguous: the process that\n * owns the file is the checkout the agent is working in, so a route and a\n * selector always point somewhere. Nothing about which port served the page has\n * to be mapped back to a repo.\n *\n * The Vite plugin remains the two-line convenience for a Vite host; this is for\n * every other runtime, and for a page whose dev server you cannot configure at\n * all (paste the bookmarklet the index page prints).\n */\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { dirname, resolve } from 'node:path';\n\nimport type { RouteGateOptions } from '../core/index';\nimport type { IterationTheme } from '../react/theme';\nimport { checkOrigin, HttpError, isLoopback, readNodeJson, sendFailure } from '../server/http';\nimport { ensureFileIgnored, notesResponse, NOTES_ENDPOINT } from '../store';\nimport { VARIANTS_ENDPOINT } from '../core/variants';\nimport { readKnobs } from '../variants-store';\n\nimport { readBundle } from '../standalone/bundle';\nimport { serveMountOptions } from './mount-config';\nimport { guardedScriptTag } from './tag';\nimport { startUpdateCheck, UPDATE_ENDPOINT } from '../update';\nimport { autoUpdateNote, updateResponse } from '../update-note';\nexport { readBundle } from '../standalone/bundle';\n\nexport const DEFAULT_PORT = 4123;\nconst BUNDLE_PATH = '/isotone.js';\n\nexport interface ServeIterationOptions {\n /** Background npm notice, at most once a day. false disables the check;\n * 'note' also files an update note for your agent once per new version. */\n checkForUpdates?: boolean | 'note';\n /** Project directory for the suggested update command and for\n * `.isotone/knobs.json`. Defaults to the notes directory. */\n projectRoot?: string;\n /** Defaults to auto (page background); system follows the OS preference. */\n theme?: IterationTheme;\n /** Where notes persist. Git-ignored automatically via `.git/info/exclude`. */\n file: string;\n /** Defaults to 4123. */\n port?: number;\n /** Defaults to 127.0.0.1 — a dev loop has no business on 0.0.0.0. */\n host?: string;\n /** Route visibility rule handed to the panel. */\n rule?: RouteGateOptions;\n /** localStorage namespace for the panel. */\n key?: string;\n}\n\nexport interface ServedIteration {\n port: number;\n url: string;\n bundleUrl: string;\n close(): Promise<void>;\n}\n\n/**\n * Only localhost may talk to this server.\n *\n * The endpoint writes a file, and a wide-open `*` would let any page you happen\n * to visit post notes into the repo you are reviewing. Restricting the allowed\n * origin to loopback keeps a drive-by from a public site out while leaving\n * every real dev origin (any localhost port, http or https) working.\n */\nfunction allowedOrigin(origin: string | undefined): string | null {\n if (!origin) return null;\n try {\n const { hostname, protocol } = new URL(origin);\n const loopback =\n hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname === '::1';\n return loopback && (protocol === 'http:' || protocol === 'https:') ? origin : null;\n } catch {\n return null;\n }\n}\n\nfunction cors(req: IncomingMessage, res: ServerResponse): void {\n const origin = allowedOrigin(req.headers.origin);\n res.setHeader('Vary', 'Origin');\n if (origin) {\n res.setHeader('Access-Control-Allow-Origin', origin);\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n res.setHeader('Access-Control-Max-Age', '600');\n }\n}\n\n/** The bundle plus the mount call, with the config the server itself knows. */\nfunction bundleWithMount(origin: string, opts: ServeIterationOptions, projectRoot: string): string {\n const config = JSON.stringify(serveMountOptions(origin, projectRoot, opts));\n return `${readBundle()}\\n;window.__isotone&&window.__isotone.mount(${config});\\n`;\n}\n\nfunction bookmarklet(origin: string): string {\n return `javascript:(function(){var s=document.createElement('script');s.src=${JSON.stringify(\n `${origin}${BUNDLE_PATH}`,\n )};s.defer=true;document.body.appendChild(s);})()`;\n}\n\nfunction escapeHtml(v: string): string {\n return v\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n\nfunction indexPage(origin: string, file: string): string {\n const tag = guardedScriptTag(`${origin}${BUNDLE_PATH}`);\n const mark = bookmarklet(origin);\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Isotone</title>\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<style>\n * { box-sizing: border-box; margin: 0; }\n :root {\n --bg: #0d0d10; --card: #131317; --line: rgba(255,255,255,.09);\n --fg: #ececf1; --muted: #8f8f9a; --faint: #5f5f6a;\n --sans: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;\n --mono: ui-monospace, \"SF Mono\", Menlo, monospace;\n }\n body { background: var(--bg); color: var(--fg); font: 14px/1.6 var(--sans);\n -webkit-font-smoothing: antialiased; }\n .wrap { max-width: 540px; margin: 0 auto; padding: 12vh 24px 8vh; }\n header { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }\n .dot { width: 7px; height: 7px; border-radius: 9999px; background: #4ade80;\n box-shadow: 0 0 10px rgba(74,222,128,.5); }\n h1 { font-family: var(--mono); font-size: 14px; font-weight: 500; letter-spacing: .02em; }\n .sub { color: var(--muted); font-size: 13px; margin-bottom: 40px; }\n .sub code, p.note code { font-family: var(--mono); font-size: 11.5px; color: var(--fg); }\n h2 { font-family: var(--mono); font-size: 10.5px; font-weight: 500;\n letter-spacing: .08em; text-transform: uppercase; color: var(--faint);\n margin: 32px 0 10px; }\n .card { background: var(--card); border: 1px solid var(--line);\n border-radius: 12px; padding: 13px 16px; }\n .card.row { display: flex; align-items: center; gap: 14px; }\n .card pre { font-family: var(--mono); font-size: 12px; line-height: 1.7;\n color: var(--fg); overflow-x: auto; white-space: pre;\n scrollbar-width: none; min-width: 0; }\n .card.row pre { flex: 1; }\n .card pre::-webkit-scrollbar { display: none; }\n .card pre .c { color: var(--faint); }\n .copy { flex: none; font-family: var(--mono); font-size: 10px;\n color: var(--faint); background: none; padding: 4px 9px;\n border: 1px solid var(--line); border-radius: 6px; cursor: pointer; }\n .copy:hover { color: var(--fg); border-color: rgba(255,255,255,.2); }\n p.note { color: var(--muted); font-size: 12.5px; margin-top: 10px; }\n a.bm { display: inline-flex; align-items: center; gap: 8px; margin-top: 2px;\n padding: 8px 14px; border: 1px solid var(--line); border-radius: 8px;\n background: var(--card); color: var(--fg); text-decoration: none;\n font-family: var(--mono); font-size: 12px; }\n a.bm:hover { border-color: rgba(255,255,255,.2); }\n footer { margin-top: 48px; padding-top: 16px; border-top: 1px solid var(--line);\n font-family: var(--mono); font-size: 10.5px; color: var(--faint);\n line-height: 2; overflow-wrap: anywhere; }\n</style>\n</head>\n<body>\n<div class=\"wrap\">\n <header><span class=\"dot\"></span><h1>Isotone</h1></header>\n <p class=\"sub\">Serving the feedback loop on <code>${escapeHtml(origin)}</code>.</p>\n\n <h2>Script for your dev pages</h2>\n <div class=\"card row\">\n <pre>${escapeHtml(tag)}</pre>\n <button type=\"button\" class=\"copy\" data-c=\"${escapeHtml(tag)}\"\n onclick=\"navigator.clipboard.writeText(this.dataset.c);this.textContent='copied';setTimeout(()=>this.textContent='copy',1200)\">copy</button>\n </div>\n <p class=\"note\">It loads the panel only on localhost, so it stays inert if the page ships.\n Behind a development flag, a plain <code><script src=\"${escapeHtml(origin + BUNDLE_PATH)}\" defer></code> works too.</p>\n\n <h2>Bookmarklet — drag to your bookmarks bar</h2>\n <a class=\"bm\" href=\"${escapeHtml(mark)}\">✎ Iteration</a>\n\n <footer>\n notes → ${escapeHtml(file)}<br>\n loopback origins only\n </footer>\n</div>\n</body>\n</html>\n`;\n}\n\n/** Start the server. Resolves once it is listening. */\nexport function serveIteration(\n opts: ServeIterationOptions,\n): Promise<ServedIteration> {\n const file = resolve(opts.file);\n const projectRoot = resolve(opts.projectRoot ?? dirname(file));\n const port = opts.port ?? DEFAULT_PORT;\n const host = opts.host ?? '127.0.0.1';\n if (!isLoopback(host)) throw new Error('isotone serve binds only to localhost, 127.0.0.1 or ::1.');\n ensureFileIgnored(file);\n const updates = startUpdateCheck(projectRoot, opts.checkForUpdates !== false);\n if (opts.checkForUpdates === 'note') void updates.ready.then(() => autoUpdateNote(file, updates.getSnapshot())).catch(() => {});\n\n // The port we were ASKED for is not necessarily the one we got: 0 means \"any\n // free port\". The panel's endpoint has to be the bound one or the bundle\n // ships a URL that answers nothing.\n let boundPort = port;\n\n const server: Server = createServer((req, res) => {\n cors(req, res);\n const path = new URL(req.url ?? '/', 'http://localhost').pathname;\n const method = req.method ?? 'GET';\n const origin = `http://${host.includes(':') && !host.startsWith('[') ? `[${host}]` : host}:${boundPort}`;\n try {\n if (!isLoopback(new URL(`http://${req.headers.host ?? ''}`).hostname)) throw new HttpError(403, 'host not allowed');\n checkOrigin(req.headers.origin, req.headers.host ?? '', true);\n } catch (error) { sendFailure(res, error); return; }\n\n if (method === 'OPTIONS') {\n res.statusCode = 204;\n res.end();\n return;\n }\n\n if (path === UPDATE_ENDPOINT) {\n // POST asks for the update note, built from this server's own check.\n (method === 'POST' ? readNodeJson(req) : Promise.resolve(undefined)).then((body) => {\n const out = updateResponse(file, updates.getSnapshot(), { method, body });\n res.statusCode = out.status;\n for (const [key, value] of Object.entries(out.headers)) res.setHeader(key, value);\n res.end(JSON.stringify(out.body));\n }).catch((error) => sendFailure(res, error));\n return;\n }\n\n if (path === BUNDLE_PATH && method === 'GET') {\n let body: string;\n try {\n body = bundleWithMount(origin, opts, projectRoot);\n } catch (err) {\n res.statusCode = 500;\n res.setHeader('content-type', 'text/plain; charset=utf-8');\n res.end(String(err instanceof Error ? err.message : err));\n return;\n }\n res.statusCode = 200;\n res.setHeader('content-type', 'text/javascript; charset=utf-8');\n res.setHeader('cache-control', 'no-store');\n res.end(body);\n return;\n }\n\n if (path === VARIANTS_ENDPOINT || path.startsWith(`${VARIANTS_ENDPOINT}/`)) {\n res.setHeader('content-type', 'application/json; charset=utf-8');\n res.setHeader('cache-control', 'no-store');\n if (path === VARIANTS_ENDPOINT && method === 'GET') {\n res.statusCode = 200;\n res.end(JSON.stringify({ variants: [], active: [], knobs: readKnobs({ root: projectRoot }) }));\n } else {\n res.statusCode = 405;\n res.end(JSON.stringify({ error: 'isotone serve has knobs only; file variants need the Vite plugin' }));\n }\n return;\n }\n\n if (path === NOTES_ENDPOINT) {\n if (method !== 'GET' && method !== 'POST' && method !== 'DELETE') {\n res.statusCode = 405;\n res.setHeader('content-type', 'application/json; charset=utf-8');\n res.end(JSON.stringify({ error: 'method not allowed' }));\n return;\n }\n (method === 'POST' ? readNodeJson(req) : Promise.resolve(undefined)).then((body) => {\n const out = notesResponse(file, { method, url: req.url, body });\n res.statusCode = out.status;\n for (const [key, value] of Object.entries(out.headers)) res.setHeader(key, value);\n res.end(JSON.stringify(out.body));\n }).catch((error) => sendFailure(res, error));\n return;\n }\n\n if (path === '/' && method === 'GET') {\n res.statusCode = 200;\n res.setHeader('content-type', 'text/html; charset=utf-8');\n res.end(indexPage(origin, file));\n return;\n }\n\n res.statusCode = 404;\n res.setHeader('content-type', 'text/plain; charset=utf-8');\n res.end('not found');\n });\n\n server.once('close', updates.dispose);\n return new Promise((ok, fail) => {\n server.once('error', (error) => { updates.dispose(); fail(error); });\n server.listen(port, host, () => {\n const actual = server.address();\n boundPort = typeof actual === 'object' && actual ? actual.port : port;\n const url = `http://${host.includes(':') && !host.startsWith('[') ? `[${host}]` : host}:${boundPort}`;\n ok({\n port: boundPort,\n url,\n bundleUrl: `${url}${BUNDLE_PATH}`,\n close: () =>\n new Promise<void>((done) => {\n server.close(() => done());\n }),\n });\n });\n });\n}\n","import type { RouteGateOptions } from '../core/index';\nimport { VARIANTS_ENDPOINT } from '../core/variants';\nimport type { MountIterationOptions } from '../react/IterationMount';\nimport type { IterationTheme } from '../react/theme';\nimport { NOTES_ENDPOINT } from '../store';\nimport { UPDATE_ENDPOINT } from '../update';\nimport { readKnobs } from '../variants-store';\n\n/**\n * What `isotone serve` hands `mountIteration` in its bundle.\n *\n * Knobs only: file variants need a bundler to swap modules, which a script tag\n * on another dev server does not have. So the panel gets a Variants tool only\n * when `.isotone/knobs.json` holds valid knobs, and its empty list then asks\n * for knobs rather than variants. The bundle is never cached, so reloading the\n * page after the first knob is written brings the tool in.\n */\nexport function serveMountOptions(\n origin: string,\n projectRoot: string,\n opts: { key?: string; rule?: RouteGateOptions; theme?: IterationTheme },\n): MountIterationOptions {\n const knobs = readKnobs({ root: projectRoot }).length > 0;\n return {\n endpoint: `${origin}${NOTES_ENDPOINT}`,\n ...(knobs ? { variants: 'knobs' as const, variantsEndpoint: `${origin}${VARIANTS_ENDPOINT}` } : {}),\n updateEndpoint: `${origin}${UPDATE_ENDPOINT}`,\n key: opts.key ?? '',\n rule: opts.rule ?? {},\n theme: opts.theme,\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/server.d.ts
CHANGED
|
@@ -103,6 +103,13 @@ interface ServeRequestOptions {
|
|
|
103
103
|
* Off by default: only the API's own origin is accepted.
|
|
104
104
|
*/
|
|
105
105
|
loopbackOrigins?: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Answer even when `NODE_ENV` is `production`. Off by default: the handler
|
|
108
|
+
* reads and writes a file in the project, which a deployed app has no
|
|
109
|
+
* business exposing, so a production runtime gets a 404 instead. Set it only
|
|
110
|
+
* for a deliberate, access-controlled review deployment.
|
|
111
|
+
*/
|
|
112
|
+
allowProduction?: boolean;
|
|
106
113
|
}
|
|
107
114
|
/**
|
|
108
115
|
* A convenience Next-compatible adapter: given a `Request`/`NextRequest` and the
|
package/dist/server.js
CHANGED
|
@@ -6,27 +6,38 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
ACTIVE_FILE,
|
|
8
8
|
DEFAULT_VARIANTS_DIR,
|
|
9
|
-
listVariants,
|
|
10
|
-
readActive,
|
|
11
|
-
variantsResponse,
|
|
12
|
-
writeActive
|
|
13
|
-
} from "./chunk-TGAU3WTF.js";
|
|
14
|
-
import {
|
|
15
|
-
VARIANTS_ENDPOINT
|
|
16
|
-
} from "./chunk-PUXQGUMQ.js";
|
|
17
|
-
import {
|
|
18
9
|
NOTES_ENDPOINT,
|
|
19
10
|
PAYLOAD_MAX,
|
|
20
11
|
ensureFileIgnored,
|
|
12
|
+
listVariants,
|
|
21
13
|
notesResponse,
|
|
14
|
+
readActive,
|
|
22
15
|
readNotes,
|
|
23
16
|
updateNotes,
|
|
17
|
+
variantsResponse,
|
|
18
|
+
writeActive,
|
|
24
19
|
writeNotes
|
|
25
|
-
} from "./chunk-
|
|
26
|
-
import "./chunk-
|
|
20
|
+
} from "./chunk-L6O4HYGP.js";
|
|
21
|
+
import "./chunk-JCTWHR7U.js";
|
|
22
|
+
import {
|
|
23
|
+
VARIANTS_ENDPOINT
|
|
24
|
+
} from "./chunk-BTJHQXF7.js";
|
|
27
25
|
|
|
28
26
|
// src/server/index.ts
|
|
27
|
+
function refusedInProduction(options) {
|
|
28
|
+
if (options.allowProduction || !isProductionRuntime()) return null;
|
|
29
|
+
return {
|
|
30
|
+
status: 404,
|
|
31
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
32
|
+
body: { error: "isotone is development-only; this endpoint is off because NODE_ENV is production" }
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function isProductionRuntime() {
|
|
36
|
+
return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
|
|
37
|
+
}
|
|
29
38
|
async function serveNotesRequest(file, request, options = {}) {
|
|
39
|
+
const refused = refusedInProduction(options);
|
|
40
|
+
if (refused) return refused;
|
|
30
41
|
try {
|
|
31
42
|
const url = new URL(request.url);
|
|
32
43
|
checkOrigin(request.headers.get("origin"), url.host, options.loopbackOrigins);
|
|
@@ -41,6 +52,8 @@ async function serveNotesRequest(file, request, options = {}) {
|
|
|
41
52
|
}
|
|
42
53
|
}
|
|
43
54
|
async function serveVariantsRequest(opts, request, options = {}) {
|
|
55
|
+
const refused = refusedInProduction(options);
|
|
56
|
+
if (refused) return refused;
|
|
44
57
|
try {
|
|
45
58
|
const url = new URL(request.url);
|
|
46
59
|
checkOrigin(request.headers.get("origin"), url.host, options.loopbackOrigins);
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server/index.ts"],"sourcesContent":["import { checkOrigin, httpFailure, readWebJson } from './http';\n/**\n * isotone — server.\n *\n * The transport-agnostic side of the loop: the file store and a pure request\n * handler that any dev runtime (Vite middleware, Next route handler, Bun.serve,\n * Vercel function, plain `node:http`) can render. No React and no Vite here —\n * keep this subpath framework-free.\n */\n\nexport {\n ensureFileIgnored,\n notesResponse,\n readNotes,\n writeNotes,\n updateNotes,\n NOTES_ENDPOINT,\n PAYLOAD_MAX,\n} from '../store';\nimport { notesResponse } from '../store';\nexport type {\n NotesRequest,\n NotesResponse,\n StoredNote,\n} from '../store';\n\nexport {\n ACTIVE_FILE,\n DEFAULT_VARIANTS_DIR,\n listVariants,\n readActive,\n variantsResponse,\n writeActive,\n} from '../variants-store';\nimport { variantsResponse } from '../variants-store';\nimport type { VariantsStoreOptions } from '../variants-store';\nexport type { VariantsStoreOptions } from '../variants-store';\nexport { VARIANTS_ENDPOINT } from '../core/variants';\nimport { VARIANTS_ENDPOINT } from '../core/variants';\n\nexport interface ServeRequestOptions {\n /**\n * Accept requests from any loopback page (localhost, 127.0.0.1, [::1]) on\n * any port. Needed when the app's dev server and this API listen on\n * different ports, since the page's origin then never matches the API host.\n * Off by default: only the API's own origin is accepted.\n */\n loopbackOrigins?: boolean;\n}\n\n/**\n * A convenience Next-compatible adapter: given a `Request`/`NextRequest` and the\n * `file`, return a `NextResponse`-shaped `{ status, headers, body }`. Frameworks\n * that expose a `(req, res)` pair call `notesResponse` and render it directly.\n */\nexport async function serveNotesRequest(\n file: string,\n request: Request,\n options: ServeRequestOptions = {},\n): Promise<{ status: number; headers: Record<string, string>; body: unknown }> {\n try {\n const url = new URL(request.url);\n checkOrigin(request.headers.get('origin'), url.host, options.loopbackOrigins);\n const body = request.method === 'POST' ? await readWebJson(request) : undefined;\n return notesResponse(file, {\n method: request.method,\n url: `${url.pathname}?${url.searchParams.toString()}`,\n body,\n });\n } catch (error) {\n return httpFailure(error);\n }\n}\n\n/**\n * Like {@link serveNotesRequest}, for the variants endpoint: given a\n * `Request` whose path ends with `VARIANTS_ENDPOINT` (or a sub-path of it,\n * such as `.../active`), strip the mount prefix before handing the rest to\n * `variantsResponse`, which routes on the path relative to the mount.\n */\nexport async function serveVariantsRequest(\n opts: VariantsStoreOptions,\n request: Request,\n options: ServeRequestOptions = {},\n): Promise<{ status: number; headers: Record<string, string>; body: unknown }> {\n try {\n const url = new URL(request.url);\n checkOrigin(request.headers.get('origin'), url.host, options.loopbackOrigins);\n const body = request.method === 'POST' ? await readWebJson(request) : undefined;\n const mounted = url.pathname.startsWith(VARIANTS_ENDPOINT)\n ? url.pathname.slice(VARIANTS_ENDPOINT.length)\n : url.pathname;\n return variantsResponse(opts, {\n method: request.method,\n url: `${mounted || '/'}?${url.searchParams.toString()}`,\n body,\n });\n } catch (error) {\n return httpFailure(error);\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/server/index.ts"],"sourcesContent":["import { checkOrigin, httpFailure, readWebJson } from './http';\n/**\n * isotone — server.\n *\n * The transport-agnostic side of the loop: the file store and a pure request\n * handler that any dev runtime (Vite middleware, Next route handler, Bun.serve,\n * Vercel function, plain `node:http`) can render. No React and no Vite here —\n * keep this subpath framework-free.\n */\n\nexport {\n ensureFileIgnored,\n notesResponse,\n readNotes,\n writeNotes,\n updateNotes,\n NOTES_ENDPOINT,\n PAYLOAD_MAX,\n} from '../store';\nimport { notesResponse } from '../store';\nexport type {\n NotesRequest,\n NotesResponse,\n StoredNote,\n} from '../store';\n\nexport {\n ACTIVE_FILE,\n DEFAULT_VARIANTS_DIR,\n listVariants,\n readActive,\n variantsResponse,\n writeActive,\n} from '../variants-store';\nimport { variantsResponse } from '../variants-store';\nimport type { VariantsStoreOptions } from '../variants-store';\nexport type { VariantsStoreOptions } from '../variants-store';\nexport { VARIANTS_ENDPOINT } from '../core/variants';\nimport { VARIANTS_ENDPOINT } from '../core/variants';\n\nexport interface ServeRequestOptions {\n /**\n * Accept requests from any loopback page (localhost, 127.0.0.1, [::1]) on\n * any port. Needed when the app's dev server and this API listen on\n * different ports, since the page's origin then never matches the API host.\n * Off by default: only the API's own origin is accepted.\n */\n loopbackOrigins?: boolean;\n /**\n * Answer even when `NODE_ENV` is `production`. Off by default: the handler\n * reads and writes a file in the project, which a deployed app has no\n * business exposing, so a production runtime gets a 404 instead. Set it only\n * for a deliberate, access-controlled review deployment.\n */\n allowProduction?: boolean;\n}\n\n/** The answer a production runtime gets unless the host opted in. */\nfunction refusedInProduction(options: ServeRequestOptions): { status: number; headers: Record<string, string>; body: unknown } | null {\n if (options.allowProduction || !isProductionRuntime()) return null;\n return {\n status: 404,\n headers: { 'content-type': 'application/json; charset=utf-8' },\n body: { error: 'isotone is development-only; this endpoint is off because NODE_ENV is production' },\n };\n}\n\n/**\n * Whether this process runs as a production build. Only an explicit\n * `NODE_ENV=production` counts: test runners and hand-started dev servers\n * often leave it unset, and every common production runtime (`next start`,\n * Vercel, Netlify, most container images) sets it.\n */\nfunction isProductionRuntime(): boolean {\n return typeof process !== 'undefined' && process.env?.NODE_ENV === 'production';\n}\n\n/**\n * A convenience Next-compatible adapter: given a `Request`/`NextRequest` and the\n * `file`, return a `NextResponse`-shaped `{ status, headers, body }`. Frameworks\n * that expose a `(req, res)` pair call `notesResponse` and render it directly.\n */\nexport async function serveNotesRequest(\n file: string,\n request: Request,\n options: ServeRequestOptions = {},\n): Promise<{ status: number; headers: Record<string, string>; body: unknown }> {\n const refused = refusedInProduction(options);\n if (refused) return refused;\n try {\n const url = new URL(request.url);\n checkOrigin(request.headers.get('origin'), url.host, options.loopbackOrigins);\n const body = request.method === 'POST' ? await readWebJson(request) : undefined;\n return notesResponse(file, {\n method: request.method,\n url: `${url.pathname}?${url.searchParams.toString()}`,\n body,\n });\n } catch (error) {\n return httpFailure(error);\n }\n}\n\n/**\n * Like {@link serveNotesRequest}, for the variants endpoint: given a\n * `Request` whose path ends with `VARIANTS_ENDPOINT` (or a sub-path of it,\n * such as `.../active`), strip the mount prefix before handing the rest to\n * `variantsResponse`, which routes on the path relative to the mount.\n */\nexport async function serveVariantsRequest(\n opts: VariantsStoreOptions,\n request: Request,\n options: ServeRequestOptions = {},\n): Promise<{ status: number; headers: Record<string, string>; body: unknown }> {\n const refused = refusedInProduction(options);\n if (refused) return refused;\n try {\n const url = new URL(request.url);\n checkOrigin(request.headers.get('origin'), url.host, options.loopbackOrigins);\n const body = request.method === 'POST' ? await readWebJson(request) : undefined;\n const mounted = url.pathname.startsWith(VARIANTS_ENDPOINT)\n ? url.pathname.slice(VARIANTS_ENDPOINT.length)\n : url.pathname;\n return variantsResponse(opts, {\n method: request.method,\n url: `${mounted || '/'}?${url.searchParams.toString()}`,\n body,\n });\n } catch (error) {\n return httpFailure(error);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,SAAS,oBAAoB,SAAyG;AACpI,MAAI,QAAQ,mBAAmB,CAAC,oBAAoB,EAAG,QAAO;AAC9D,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,kCAAkC;AAAA,IAC7D,MAAM,EAAE,OAAO,mFAAmF;AAAA,EACpG;AACF;AAQA,SAAS,sBAA+B;AACtC,SAAO,OAAO,YAAY,eAAe,QAAQ,KAAK,aAAa;AACrE;AAOA,eAAsB,kBACpB,MACA,SACA,UAA+B,CAAC,GAC6C;AAC7E,QAAM,UAAU,oBAAoB,OAAO;AAC3C,MAAI,QAAS,QAAO;AACpB,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,gBAAY,QAAQ,QAAQ,IAAI,QAAQ,GAAG,IAAI,MAAM,QAAQ,eAAe;AAC5E,UAAM,OAAO,QAAQ,WAAW,SAAS,MAAM,YAAY,OAAO,IAAI;AACtE,WAAO,cAAc,MAAM;AAAA,MACzB,QAAQ,QAAQ;AAAA,MAChB,KAAK,GAAG,IAAI,QAAQ,IAAI,IAAI,aAAa,SAAS,CAAC;AAAA,MACnD;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,YAAY,KAAK;AAAA,EAC1B;AACF;AAQA,eAAsB,qBACpB,MACA,SACA,UAA+B,CAAC,GAC6C;AAC7E,QAAM,UAAU,oBAAoB,OAAO;AAC3C,MAAI,QAAS,QAAO;AACpB,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,gBAAY,QAAQ,QAAQ,IAAI,QAAQ,GAAG,IAAI,MAAM,QAAQ,eAAe;AAC5E,UAAM,OAAO,QAAQ,WAAW,SAAS,MAAM,YAAY,OAAO,IAAI;AACtE,UAAM,UAAU,IAAI,SAAS,WAAW,iBAAiB,IACrD,IAAI,SAAS,MAAM,kBAAkB,MAAM,IAC3C,IAAI;AACR,WAAO,iBAAiB,MAAM;AAAA,MAC5B,QAAQ,QAAQ;AAAA,MAChB,KAAK,GAAG,WAAW,GAAG,IAAI,IAAI,aAAa,SAAS,CAAC;AAAA,MACrD;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,YAAY,KAAK;AAAA,EAC1B;AACF;","names":[]}
|
package/dist/vite-plugin.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { RouteGateOptions } from './core.js';
|
|
2
|
-
import { I as IterationShortcut } from './IterationPanel-
|
|
2
|
+
import { I as IterationShortcut } from './IterationPanel-Bq77d5NT.js';
|
|
3
3
|
import { I as IterationTheme } from './theme-BWv_ZNhC.js';
|
|
4
4
|
import 'react';
|
|
5
5
|
|
|
6
6
|
interface IterationViteOptions {
|
|
7
7
|
/** Inject the bundled panel. false keeps endpoints and variants for a manual mount. */
|
|
8
8
|
panel?: boolean;
|
|
9
|
-
/** Background npm notice, at most once a day. false disables the check
|
|
10
|
-
|
|
9
|
+
/** Background npm notice, at most once a day. false disables the check;
|
|
10
|
+
* 'note' also files an update note for your agent once per new version. */
|
|
11
|
+
checkForUpdates?: boolean | 'note';
|
|
11
12
|
/** Defaults to auto (page background); system follows the OS preference. */
|
|
12
13
|
theme?: IterationTheme;
|
|
13
14
|
/** Defaults to Cmd/Ctrl+Shift+Period; customize or disable with false. */
|
package/dist/vite-plugin.js
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
autoUpdateNote,
|
|
3
|
+
readBundle,
|
|
4
|
+
updateResponse
|
|
5
|
+
} from "./chunk-YPDV3PX4.js";
|
|
4
6
|
import {
|
|
5
7
|
checkOrigin,
|
|
6
8
|
readNodeJson,
|
|
7
9
|
sendFailure
|
|
8
10
|
} from "./chunk-DDMHJ23I.js";
|
|
11
|
+
import {
|
|
12
|
+
isRouteModule,
|
|
13
|
+
scanExports
|
|
14
|
+
} from "./chunk-BDTB3DFR.js";
|
|
15
|
+
import {
|
|
16
|
+
UPDATE_ENDPOINT,
|
|
17
|
+
startUpdateCheck
|
|
18
|
+
} from "./chunk-KRS57POF.js";
|
|
9
19
|
import {
|
|
10
20
|
ACTIVE_FILE,
|
|
21
|
+
NOTES_ENDPOINT,
|
|
22
|
+
ensureDirIgnored,
|
|
23
|
+
ensureFileIgnored,
|
|
11
24
|
listVariants,
|
|
25
|
+
notesResponse,
|
|
12
26
|
overrideSourcePath,
|
|
13
27
|
readActive,
|
|
14
28
|
readVariant,
|
|
@@ -17,22 +31,12 @@ import {
|
|
|
17
31
|
variantDir,
|
|
18
32
|
variantsDir,
|
|
19
33
|
variantsResponse
|
|
20
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-L6O4HYGP.js";
|
|
35
|
+
import "./chunk-JCTWHR7U.js";
|
|
21
36
|
import {
|
|
22
37
|
VARIANTS_ENDPOINT,
|
|
23
38
|
overlayGroups
|
|
24
|
-
} from "./chunk-
|
|
25
|
-
import {
|
|
26
|
-
UPDATE_ENDPOINT,
|
|
27
|
-
startUpdateCheck
|
|
28
|
-
} from "./chunk-I7CZUPDO.js";
|
|
29
|
-
import {
|
|
30
|
-
NOTES_ENDPOINT,
|
|
31
|
-
ensureDirIgnored,
|
|
32
|
-
ensureFileIgnored,
|
|
33
|
-
notesResponse
|
|
34
|
-
} from "./chunk-X6V2AKJL.js";
|
|
35
|
-
import "./chunk-A4DX2V4C.js";
|
|
39
|
+
} from "./chunk-BTJHQXF7.js";
|
|
36
40
|
|
|
37
41
|
// src/vite/index.ts
|
|
38
42
|
import { existsSync, mkdirSync, readFileSync, realpathSync } from "fs";
|
|
@@ -41,101 +45,6 @@ import {
|
|
|
41
45
|
normalizePath
|
|
42
46
|
} from "vite";
|
|
43
47
|
|
|
44
|
-
// src/vite/exports.ts
|
|
45
|
-
var ROUTE_EXPORTS = /* @__PURE__ */ new Set([
|
|
46
|
-
"loader",
|
|
47
|
-
"action",
|
|
48
|
-
"clientLoader",
|
|
49
|
-
"clientAction",
|
|
50
|
-
"headers",
|
|
51
|
-
"links",
|
|
52
|
-
"meta",
|
|
53
|
-
"handle",
|
|
54
|
-
"shouldRevalidate",
|
|
55
|
-
"middleware",
|
|
56
|
-
"clientMiddleware",
|
|
57
|
-
"unstable_middleware",
|
|
58
|
-
"unstable_clientMiddleware",
|
|
59
|
-
"HydrateFallback"
|
|
60
|
-
]);
|
|
61
|
-
var IDENT = /^[A-Za-z_$][\w$]*$/;
|
|
62
|
-
function scanExports(code) {
|
|
63
|
-
const names = /* @__PURE__ */ new Set();
|
|
64
|
-
const lines = code.split("\n");
|
|
65
|
-
for (let i = 0; i < lines.length; i++) {
|
|
66
|
-
const line = (lines[i] ?? "").replace(/^(?:\/\*.*?\*\/\s*)+/, "");
|
|
67
|
-
if (!line.startsWith("export")) continue;
|
|
68
|
-
const rest = line.slice("export".length);
|
|
69
|
-
if (rest !== "" && !/^[\s{*]/.test(rest)) continue;
|
|
70
|
-
const body = rest.trim();
|
|
71
|
-
if (body.startsWith("{") || body.startsWith("type {")) {
|
|
72
|
-
if (body.startsWith("type")) continue;
|
|
73
|
-
let clause = body;
|
|
74
|
-
while (!clause.includes("}") && i + 1 < lines.length) clause += ` ${lines[++i]}`;
|
|
75
|
-
const inner = clause.slice(clause.indexOf("{") + 1, clause.indexOf("}"));
|
|
76
|
-
for (const raw of inner.split(",")) {
|
|
77
|
-
const spec = raw.replace(/\/\/.*$/, "").trim();
|
|
78
|
-
if (!spec || spec.startsWith("type ")) continue;
|
|
79
|
-
const as = spec.split(/\s+as\s+/);
|
|
80
|
-
const name = (as[1] ?? as[0] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
81
|
-
if (!name) return null;
|
|
82
|
-
names.add(name);
|
|
83
|
-
}
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
if (/^type\s*\*/.test(body)) continue;
|
|
87
|
-
const star = body.match(/^\*\s+as\s+([A-Za-z_$][\w$]*)/);
|
|
88
|
-
if (star?.[1]) {
|
|
89
|
-
names.add(star[1]);
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
if (body.startsWith("*")) return null;
|
|
93
|
-
if (/^default\b/.test(body)) {
|
|
94
|
-
names.add("default");
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
if (/^(?:declare\s|type\s|interface\s)/.test(body)) continue;
|
|
98
|
-
if (/^(?:=|import\s|namespace\s|module\s|as\s)/.test(body)) return null;
|
|
99
|
-
const decl = body.match(
|
|
100
|
-
/^(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)|^(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)|^(?:const\s+)?enum\s+([A-Za-z_$][\w$]*)/
|
|
101
|
-
);
|
|
102
|
-
const declared = decl?.[1] ?? decl?.[2] ?? decl?.[3];
|
|
103
|
-
if (declared) {
|
|
104
|
-
names.add(declared);
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
const variable = body.match(/^(?:const|let|var)\s+(.*)$/);
|
|
108
|
-
if (variable) {
|
|
109
|
-
const target = (variable[1] ?? "").trim();
|
|
110
|
-
if (target.startsWith("{") || target.startsWith("[")) return null;
|
|
111
|
-
const name = target.match(/^([A-Za-z_$][\w$]*)/)?.[1];
|
|
112
|
-
if (!name) return null;
|
|
113
|
-
if (hasSecondDeclarator(target)) return null;
|
|
114
|
-
names.add(name);
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
for (const name of names) if (name !== "default" && !IDENT.test(name)) return null;
|
|
120
|
-
return [...names];
|
|
121
|
-
}
|
|
122
|
-
function isRouteModule(names) {
|
|
123
|
-
return names.includes("default") && names.some((name) => ROUTE_EXPORTS.has(name));
|
|
124
|
-
}
|
|
125
|
-
function hasSecondDeclarator(declaration) {
|
|
126
|
-
const text = declaration.replace(/\/\/.*$/, "").replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`/g, '""');
|
|
127
|
-
let depth = 0;
|
|
128
|
-
let initialized = false;
|
|
129
|
-
for (let i = 0; i < text.length; i++) {
|
|
130
|
-
const ch = text[i];
|
|
131
|
-
if (ch === "(" || ch === "[" || ch === "{" || ch === "<") depth++;
|
|
132
|
-
else if (ch === ")" || ch === "]" || ch === "}" || ch === ">" && text[i - 1] !== "=") depth = Math.max(0, depth - 1);
|
|
133
|
-
else if (depth === 0 && ch === "=" && text[i + 1] !== "=" && text[i + 1] !== ">" && text[i - 1] !== "=") initialized = true;
|
|
134
|
-
else if (depth === 0 && ch === "," && initialized) return true;
|
|
135
|
-
}
|
|
136
|
-
return depth === 0 && /,\s*$/.test(text);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
48
|
// src/vite/live.ts
|
|
140
49
|
var VERSION_QUERY = "isotone";
|
|
141
50
|
var BASE_VERSION = "_base";
|
|
@@ -747,12 +656,16 @@ function iteration(options = {}) {
|
|
|
747
656
|
};
|
|
748
657
|
const updates = startUpdateCheck(server.config.root, options.checkForUpdates !== false);
|
|
749
658
|
server.httpServer?.once("close", updates.dispose);
|
|
659
|
+
if (options.checkForUpdates === "note") {
|
|
660
|
+
void updates.ready.then(() => autoUpdateNote(file, updates.getSnapshot())).catch(() => {
|
|
661
|
+
});
|
|
662
|
+
}
|
|
750
663
|
server.middlewares.use(UPDATE_ENDPOINT, (req, res) => {
|
|
751
664
|
if (!guardOrigin(req, res)) return;
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
665
|
+
const method = req.method ?? "";
|
|
666
|
+
(method === "POST" ? readNodeJson(req) : Promise.resolve(void 0)).then((body) => {
|
|
667
|
+
answer(res, () => updateResponse(file, updates.getSnapshot(), { method, body }));
|
|
668
|
+
}).catch((error) => sendFailure(res, error));
|
|
756
669
|
});
|
|
757
670
|
server.middlewares.use(NOTES_ENDPOINT, (req, res, next) => {
|
|
758
671
|
if (!guardOrigin(req, res)) return;
|
|
@@ -768,20 +681,23 @@ function iteration(options = {}) {
|
|
|
768
681
|
if (variantsOptions) {
|
|
769
682
|
activeIds = readActive(vopts);
|
|
770
683
|
live = (variantsOptions.live ?? hasReact) && server.environments !== void 0;
|
|
771
|
-
const
|
|
684
|
+
const graphs = () => server.environments ? Object.values(server.environments).map((env) => env.moduleGraph) : [server.moduleGraph];
|
|
685
|
+
const covered = /* @__PURE__ */ new Set();
|
|
772
686
|
const signature = () => JSON.stringify(readManifests().map((v) => [v.id, v.group ?? "", Object.keys(v.files).sort()]));
|
|
773
|
-
let listed =
|
|
687
|
+
let listed = signature();
|
|
774
688
|
reloadPage = () => {
|
|
775
689
|
if (reloadTimer) clearTimeout(reloadTimer);
|
|
776
690
|
reloadTimer = setTimeout(() => {
|
|
777
691
|
reloadTimer = null;
|
|
778
|
-
const
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
692
|
+
const paths = /* @__PURE__ */ new Set([...dispatched.keys(), ...versionsByFile().keys(), ...covered]);
|
|
693
|
+
for (const graph of graphs()) {
|
|
694
|
+
for (const path of paths) {
|
|
695
|
+
for (const mod of graph.getModulesByFile(path) ?? []) graph.invalidateModule(mod);
|
|
696
|
+
}
|
|
697
|
+
const runtime = graph.getModuleById(RESOLVED_LIVE_MODULE);
|
|
698
|
+
if (runtime) graph.invalidateModule(runtime);
|
|
782
699
|
}
|
|
783
|
-
|
|
784
|
-
if (runtime) graph.invalidateModule(runtime);
|
|
700
|
+
covered.clear();
|
|
785
701
|
dispatched.clear();
|
|
786
702
|
server.ws.send({ type: "full-reload", path: "*" });
|
|
787
703
|
}, 50);
|
|
@@ -881,13 +797,17 @@ function iteration(options = {}) {
|
|
|
881
797
|
if (path === activeFile) {
|
|
882
798
|
void activate(readActive(vopts));
|
|
883
799
|
} else if (isInside(vDir, path)) {
|
|
800
|
+
for (const variant of manifests ?? []) {
|
|
801
|
+
for (const key of Object.keys(variant.files)) covered.add(normalizePath(join(vopts.root, key)));
|
|
802
|
+
}
|
|
884
803
|
forgetManifests();
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
804
|
+
const stillActive = readActive(vopts);
|
|
805
|
+
const activeChanged = stillActive.join() !== currentActive().join();
|
|
806
|
+
if (activeChanged) void activate(stillActive);
|
|
807
|
+
const now = signature();
|
|
808
|
+
if (now !== listed) {
|
|
809
|
+
listed = now;
|
|
810
|
+
if (live || activeChanged || stillActive.length > 0) reloadPage();
|
|
891
811
|
}
|
|
892
812
|
}
|
|
893
813
|
});
|