isotone 0.4.0 → 0.4.2
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 +22 -0
- package/dist/chunk-A4DX2V4C.js +7 -0
- package/dist/chunk-A4DX2V4C.js.map +1 -0
- package/dist/{chunk-DD767R7E.js → chunk-AP7O357L.js} +6 -2
- package/dist/chunk-AP7O357L.js.map +1 -0
- package/dist/{chunk-7WGLOXRQ.js → chunk-DD6BKC3R.js} +4 -46
- package/dist/chunk-DD6BKC3R.js.map +1 -0
- package/dist/{chunk-QT27DAPQ.js → chunk-LHCQL5PA.js} +2 -3
- package/dist/{chunk-QT27DAPQ.js.map → chunk-LHCQL5PA.js.map} +1 -1
- package/dist/{chunk-SNGBNVH2.js → chunk-OSETWKGE.js} +1 -27
- package/dist/{chunk-SNGBNVH2.js.map → chunk-OSETWKGE.js.map} +1 -1
- package/dist/{chunk-GDVZ3B6N.js → chunk-UYMMI2IH.js} +43 -6
- package/dist/chunk-UYMMI2IH.js.map +1 -0
- package/dist/cli.js +11 -310
- package/dist/cli.js.map +1 -1
- package/dist/core.d.ts +1 -0
- package/dist/core.js +3 -2
- package/dist/index.js +14 -6
- package/dist/index.js.map +1 -1
- package/dist/isotone.global.js +18 -18
- package/dist/serve.js +3 -2
- package/dist/serve.js.map +1 -1
- package/dist/server.js +4 -3
- package/dist/server.js.map +1 -1
- package/dist/vite-plugin.js +5 -4
- package/dist/vite-plugin.js.map +1 -1
- package/docs/migration.md +33 -83
- package/docs/updates.md +3 -3
- package/package.json +1 -1
- package/dist/chunk-7WGLOXRQ.js.map +0 -1
- package/dist/chunk-DD767R7E.js.map +0 -1
- package/dist/chunk-GDVZ3B6N.js.map +0 -1
package/dist/serve.js
CHANGED
|
@@ -11,12 +11,13 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
UPDATE_ENDPOINT,
|
|
13
13
|
startUpdateCheck
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-LHCQL5PA.js";
|
|
15
15
|
import {
|
|
16
16
|
NOTES_ENDPOINT,
|
|
17
17
|
ensureFileIgnored,
|
|
18
18
|
notesResponse
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-AP7O357L.js";
|
|
20
|
+
import "./chunk-A4DX2V4C.js";
|
|
20
21
|
|
|
21
22
|
// src/serve/index.ts
|
|
22
23
|
import { createServer } from "http";
|
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"],"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":[]}
|
package/dist/server.js
CHANGED
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
readActive,
|
|
11
11
|
variantsResponse,
|
|
12
12
|
writeActive
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-DD6BKC3R.js";
|
|
14
14
|
import {
|
|
15
15
|
VARIANTS_ENDPOINT
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-OSETWKGE.js";
|
|
17
17
|
import {
|
|
18
18
|
NOTES_ENDPOINT,
|
|
19
19
|
PAYLOAD_MAX,
|
|
@@ -22,7 +22,8 @@ import {
|
|
|
22
22
|
readNotes,
|
|
23
23
|
updateNotes,
|
|
24
24
|
writeNotes
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-AP7O357L.js";
|
|
26
|
+
import "./chunk-A4DX2V4C.js";
|
|
26
27
|
|
|
27
28
|
// src/server/index.ts
|
|
28
29
|
async function serveNotesRequest(file, request) {
|
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\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): 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);\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): 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);\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\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): 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);\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): 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);\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,eAAsB,kBACpB,MACA,SAC6E;AAC7E,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,gBAAY,QAAQ,QAAQ,IAAI,QAAQ,GAAG,IAAI,IAAI;AACnD,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,SAC6E;AAC7E,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,gBAAY,QAAQ,QAAQ,IAAI,QAAQ,GAAG,IAAI,IAAI;AACnD,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.js
CHANGED
|
@@ -15,20 +15,21 @@ import {
|
|
|
15
15
|
resolveOverride,
|
|
16
16
|
variantsDir,
|
|
17
17
|
variantsResponse
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-DD6BKC3R.js";
|
|
19
19
|
import {
|
|
20
20
|
VARIANTS_ENDPOINT
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-OSETWKGE.js";
|
|
22
22
|
import {
|
|
23
23
|
UPDATE_ENDPOINT,
|
|
24
24
|
startUpdateCheck
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-LHCQL5PA.js";
|
|
26
26
|
import {
|
|
27
27
|
NOTES_ENDPOINT,
|
|
28
28
|
ensureDirIgnored,
|
|
29
29
|
ensureFileIgnored,
|
|
30
30
|
notesResponse
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-AP7O357L.js";
|
|
32
|
+
import "./chunk-A4DX2V4C.js";
|
|
32
33
|
|
|
33
34
|
// src/vite/index.ts
|
|
34
35
|
import { existsSync, mkdirSync, readFileSync, realpathSync } from "fs";
|
package/dist/vite-plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/vite/index.ts"],"sourcesContent":["/**\n * Development-only notes, source variants and a self-contained panel for Vite.\n * Inject through the HTML response so SSR frameworks work too. The loader lives\n * in <head> and mounts after load, leaving the host's hydration and HMR alone.\n */\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { existsSync, mkdirSync, readFileSync, realpathSync } from 'node:fs';\nimport { dirname, isAbsolute, join, relative, resolve } from 'node:path';\n\n// `normalizePath` is a plain function, so importing it as a value does not\n// pull vite's nominal `Plugin` type into the structural return type below.\nimport { type HmrContext, normalizePath, type Plugin, type ResolvedConfig } from 'vite';\n\nimport { ensureDirIgnored, ensureFileIgnored, notesResponse, NOTES_ENDPOINT } from '../store';\nimport type { RouteGateOptions } from '../core/index';\nimport type { IterationShortcut } from '../react/IterationPanel';\nimport type { IterationTheme } from '../react/theme';\nimport { readBundle } from '../standalone/bundle';\nimport { startUpdateCheck, UPDATE_ENDPOINT } from '../update';\nimport { checkOrigin, readNodeJson, sendFailure } from '../server/http';\nimport { VARIANTS_ENDPOINT } from '../core/variants';\nimport {\n ACTIVE_FILE,\n listVariants,\n overrideSourcePath,\n readActive,\n readVariant,\n resolveOverride,\n variantsDir,\n variantsResponse,\n type VariantsStoreOptions,\n} from '../variants-store';\n\nexport interface IterationViteOptions {\n /** Inject the bundled panel. false keeps endpoints and variants for a manual mount. */\n panel?: boolean;\n /** Background npm notice, at most once a day. false disables the check. */\n checkForUpdates?: boolean;\n /** Defaults to auto (page background); system follows the OS preference. */\n theme?: IterationTheme;\n /** Defaults to Cmd/Ctrl+Shift+Period; customize or disable with false. */\n shortcut?: IterationShortcut | false;\n /** Where notes persist. Relative paths resolve against Vite's `root` (the\n * directory of the app, whatever the shell's cwd), so a monorepo package\n * started from the workspace root still writes next to its own config.\n * Defaults to `.iteration-feedback.json`; git-ignored automatically via\n * `.git/info/exclude`, with the pattern anchored at the repository root. */\n file?: string;\n /** Route visibility rule (denylist/allowlist) for the injected panel. */\n rule?: RouteGateOptions;\n /** localStorage namespace override. */\n key?: string;\n /**\n * Overlay variants: alternative source files under `.isotone/variants/\n * <id>/<repo-relative path>` that `load` swaps in for the real ones while\n * the variant is active. On by default; `false` turns the whole feature\n * off (no endpoint, no watcher, no tab in the panel).\n */\n variants?:\n | false\n | {\n /** The project the manifests' file paths are relative to. Defaults to\n * Vite's root. A sibling dev server that aliases into another package\n * (a preview app rendering another package's source) points this at that\n * package so the same variants apply to both. Relative to Vite's root\n * or absolute. */\n root?: string;\n /** Where the variants live, relative to `root`. Default\n * `.isotone/variants`. */\n dir?: string;\n /** Keep the variants dir out of git via `.git/info/exclude`. Off by\n * default: the files are the reviewable artefact and commit with\n * the change they propose. */\n ignored?: boolean;\n };\n}\n\n/**\n * What `iteration()` hands back.\n *\n * Only `name` is public, and that is deliberate. isotone is consumed as a\n * linked (`file:` / workspace) dependency, so TypeScript resolves the `vite`\n * import from *this* package's `node_modules`, not the consumer's. Two vite\n * type instances are nominally incompatible — rollup's plugin container\n * carries a private `_pluginContextMap` — and a single unassignable entry\n * poisons the overload resolution for the consumer's whole `plugins: []`\n * array, so every other plugin in it errors too. Vite's own `Plugin` requires\n * nothing but `name`, so a structural shape drops into any vite version's\n * `PluginOption` while the hooks below stay checked against the real vite\n * types inside this file.\n */\nexport interface IterationPlugin {\n name: string;\n /**\n * Declared with the loosest shape that still drops into vite's own\n * signature: a method, so its parameter is bivariant and vite's\n * `HmrContext` satisfies it, returning `void`, which any real return type\n * satisfies. The implementation returns vite's `ModuleNode[]`; naming that\n * type here would bring the nominal vite instance back.\n */\n handleHotUpdate?(ctx: { file: string }): void;\n}\n\nconst DEFAULT_FILE = '.iteration-feedback.json';\nconst INJECTED = new WeakSet<ServerResponse>();\nconst BUNDLE_PATH = '/@isotone/panel.js';\nconst MANIFEST = 'variant.json';\n\n/**\n * Import queries that hand the file to one of vite's own loaders (assets,\n * workers, inline HTML scripts). Those read the file from disk themselves\n * and shape the module around it; a `pre` load returning the override's\n * text in their place would break the module, so such requests are left to\n * them and show the real file.\n */\nconst SPECIAL_QUERY = /[?&](raw|url|inline|no-inline|worker|sharedworker|html-proxy)(?:[&=]|$)/;\n\n/** `dir` itself or a path below it; never a sibling whose name merely\n * starts the same (`src-old` under `src`). */\nfunction isInside(dir: string, path: string): boolean {\n const rel = relative(dir, path);\n return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));\n}\n\n/** The canonical form of a directory, or the path as given when it does not\n * exist yet (nothing to canonicalize; the comparison can only fail open). */\nfunction realDir(path: string): string {\n try {\n return realpathSync(path);\n } catch {\n return path;\n }\n}\n\nexport function iteration(options: IterationViteOptions = {}): IterationPlugin {\n let file = resolve(options.file ?? DEFAULT_FILE);\n let isDev = false;\n let isBuild = false;\n let isAsset: (file: string) => boolean = () => false;\n\n const variantsOptions = options.variants === false ? null : (options.variants ?? {});\n let vopts: VariantsStoreOptions = {\n root: resolve(variantsOptions?.root ?? '.'),\n ...(variantsOptions?.dir ? { dir: variantsOptions.dir } : {}),\n };\n let vDir = variantsDir(vopts);\n let activeFile = join(vopts.root, ACTIVE_FILE);\n\n // The active set, and which override (if any) answers each source path\n // under it. `resolveOverride` re-reads the manifests every call, and load()\n // asks once per module per page, so memoize per active set and drop the\n // memo whenever the set or anything under the variants dir changes.\n // `undefined` means not read yet: configureServer primes it, and a load()\n // that somehow runs first reads the file itself.\n let activeIds: string[] | undefined;\n const overrides = new Map<string, string | null>();\n // Real source paths activate() is reporting as changed right now. Their\n // hot update must go through, where an ordinary edit of an overridden\n // real file is dropped (see handleHotUpdate).\n const swapping = new Set<string>();\n\n const currentActive = (): string[] => {\n // A build takes its set from the environment: `ISOTONE_VARIANT=a,b`.\n if (isBuild) return parseVariantList(process.env.ISOTONE_VARIANT);\n if (activeIds === undefined) activeIds = readActive(vopts);\n return activeIds;\n };\n\n const overrideFor = (active: string[], path: string): string | null => {\n if (active.length === 0) return null;\n // Sorted: the same set in another order answers the same for every path.\n const key = `${[...active].sort().join(',')}\\0${path}`;\n let hit = overrides.get(key);\n if (hit === undefined) {\n hit = resolveOverride(vopts, active, path);\n overrides.set(key, hit);\n }\n return hit;\n };\n\n /** Whether load() may swap `path` at all: a real file of the project the\n * variants describe, not a dependency and not an override itself. */\n // Module ids reach load() with forward slashes on every platform, while a\n // watcher path is native; splitting on either separator covers both.\n const overridable = (path: string): boolean =>\n isAbsolute(path) &&\n !path.split(/[\\\\/]/).includes('node_modules') &&\n isInside(vopts.root, path) &&\n !isInside(vDir, path) &&\n !isAsset(path);\n\n // `satisfies`, not an annotation: the hooks are checked against vite's\n // types, but the object keeps its own shape, so `handleHotUpdate` stays a\n // plain method and drops into the structural type below.\n const plugin = {\n name: 'iso:iteration',\n enforce: 'pre',\n configResolved(config: ResolvedConfig) {\n isDev = config.command === 'serve';\n isBuild = config.command === 'build';\n isAsset = config.assetsInclude;\n file = resolve(config.root, options.file ?? DEFAULT_FILE);\n // Vite hands load() realpath'd module ids but leaves `config.root` as\n // configured, so a root reached through a symlink would never contain\n // any module and no override would ever apply. Canonicalize it here.\n vopts = {\n root: realDir(resolve(config.root, variantsOptions?.root ?? '.')),\n ...(variantsOptions?.dir ? { dir: variantsOptions.dir } : {}),\n };\n vDir = variantsDir(vopts);\n activeFile = join(vopts.root, ACTIVE_FILE);\n },\n load(id) {\n if (!variantsOptions || SPECIAL_QUERY.test(id)) return null;\n // `?v=<hash>`, `?t=<timestamp>`, `?import`: vite's own request\n // decorations, never part of the path on disk.\n const path = id.replace(/[?#].*$/, '');\n if (!overridable(path)) return null;\n const override = overrideFor(currentActive(), path);\n if (!override) return null;\n return { code: readFileSync(override, 'utf8'), map: null };\n },\n handleHotUpdate(ctx: HmrContext) {\n if (!variantsOptions) return;\n const { file: changed, server } = ctx;\n if (isInside(vDir, changed)) {\n overrides.clear();\n const hit = overrideSourcePath(vopts, changed);\n // The manifest, or a file directly in the variants dir: nothing the\n // browser has loaded. The panel re-lists variants when it opens.\n if (!hit || hit.file === MANIFEST) return [];\n if (!currentActive().includes(hit.id)) return [];\n // The browser holds the real file's URL, so the override's edit has\n // to be delivered as if the real file changed.\n // The module graph keys files by their forward-slash form, which is\n // what `join` produces on POSIX and not on Windows.\n const source = normalizePath(join(vopts.root, hit.file));\n return [...(server.moduleGraph.getModulesByFile(source) ?? [])];\n }\n // A change activate() reported: let Vite compute the update as usual.\n if (swapping.delete(changed)) return;\n // The real source of an overridden file: the browser is showing the\n // override, so there is nothing to update from this edit.\n if (overridable(changed) && overrideFor(currentActive(), changed)) return [];\n return;\n },\n configureServer(server) {\n if (!isDev) return;\n // Here, not at construction: a production build should not touch .git.\n ensureFileIgnored(file);\n\n // Say where the notes go, after Vite's own banner so it is not buried\n // under it. Portable setups fail quietly otherwise: a reviewer writes\n // notes and the agent looks in the wrong directory.\n server.httpServer?.once('listening', () => {\n const shown = relative(server.config.root, file) || file;\n server.config.logger.info(\n ` isotone notes → ${shown} (${NOTES_ENDPOINT})`,\n );\n if (variantsOptions && existsSync(vDir)) {\n const shownDir = relative(server.config.root, vDir) || vDir;\n const n = listVariants(vopts).length;\n server.config.logger.info(\n ` isotone variants → ${shownDir} (${n} variant${n === 1 ? '' : 's'})`,\n );\n }\n });\n\n const guardOrigin = (req: IncomingMessage, res: ServerResponse): boolean => {\n try { checkOrigin(req.headers.origin, req.headers.host ?? ''); return true; }\n catch (error) { sendFailure(res, error); return false; }\n };\n\n const updates = startUpdateCheck(server.config.root, options.checkForUpdates !== false);\n server.httpServer?.once('close', updates.dispose);\n server.middlewares.use(UPDATE_ENDPOINT, (req, res) => {\n if (!guardOrigin(req, res)) return;\n res.setHeader('content-type', 'application/json; charset=utf-8');\n res.setHeader('cache-control', 'no-store');\n res.statusCode = req.method === 'GET' ? 200 : 405;\n res.end(JSON.stringify(req.method === 'GET' ? updates.getSnapshot() : { error: 'method not allowed' }));\n });\n\n // Notes endpoint → local file.\n server.middlewares.use(NOTES_ENDPOINT, (req, res, next) => {\n if (!guardOrigin(req, res)) return;\n const method = req.method ?? '';\n if (method !== 'GET' && method !== 'POST' && method !== 'DELETE') {\n next();\n return;\n }\n (method === 'POST' ? readNodeJson(req) : Promise.resolve(undefined)).then((body) => {\n answer(res, () => notesResponse(file, { method, url: req.url, body }));\n }).catch((error) => sendFailure(res, error));\n });\n\n if (variantsOptions) {\n // Read the pointer now, before anything can rewrite it: activate()\n // diffs the previous set against the new one, and the endpoint has\n // already written the file by the time it runs, so a lazy first read\n // would see the new set twice and switch nothing.\n activeIds = readActive(vopts);\n\n /**\n * Switch the browser to the set `next` without reloading the page.\n * Every file overridden by a variant that entered or left the set\n * is reported to Vite as if it had just been edited on disk: Vite\n * invalidates it, runs load() again (which now answers with the\n * other content) and sends the same hot update an editor save\n * would, so React Fast Refresh swaps the component in place and the\n * reviewer keeps their scroll, their form state and the open panel.\n * Variants in both sets are untouched, so switching one group leaves\n * the others' components alone. Files the browser has not loaded\n * are skipped; Vite decides on its own when a module cannot be\n * hot-swapped and falls back to a reload for that one, exactly as\n * it would for a hand edit.\n */\n const activate = (next: string[], removedFiles: string[] = []) => {\n const prev = currentActive();\n const changed = [\n ...prev.filter((id) => !next.includes(id)),\n ...next.filter((id) => !prev.includes(id)),\n ];\n if (changed.length === 0 && removedFiles.length === 0) return;\n activeIds = next;\n overrides.clear();\n const sources = new Set<string>(removedFiles.map((file) => join(vopts.root, file)));\n for (const id of changed) {\n const manifest = readVariant(vopts, id);\n for (const key of Object.keys(manifest?.files ?? {})) {\n sources.add(join(vopts.root, key));\n }\n }\n for (const source of sources) {\n const loaded = server.moduleGraph.getModulesByFile(normalizePath(source));\n if (!loaded || loaded.size === 0) continue;\n swapping.add(source);\n server.watcher.emit('change', source);\n }\n };\n\n // The exclude helpers canonicalize through the filesystem, so the\n // active file's directory has to exist before its pattern can land;\n // the panel's first POST would otherwise leave an untracked file.\n mkdirSync(dirname(activeFile), { recursive: true });\n ensureFileIgnored(activeFile);\n if (variantsOptions.ignored) {\n mkdirSync(vDir, { recursive: true });\n ensureDirIgnored(vDir);\n }\n\n server.middlewares.use(VARIANTS_ENDPOINT, (req, res, next) => {\n if (!guardOrigin(req, res)) return;\n const method = req.method ?? '';\n if (method !== 'GET' && method !== 'POST') {\n next();\n return;\n }\n // A POST switches every open tab of this dev server to another set\n // of files. A cross-site page can fire a `no-cors` form post at\n // localhost, but only with a text/plain or form content type; a\n // JSON content type needs a preflight, which vite's CORS does not\n // grant to an unknown origin. So the header is the gate.\n if (method === 'POST' && !isJsonRequest(req)) {\n res.statusCode = 415;\n res.setHeader('content-type', 'application/json; charset=utf-8');\n res.end(JSON.stringify({ error: 'content-type must be application/json' }));\n return;\n }\n (method === 'POST' ? readNodeJson(req) : Promise.resolve(undefined)).then((body) => {\n // connect strips the mount from `req.url`, which is the shape\n // variantsResponse routes on (\"/\", \"/active?...\").\n const result = answer(res, () =>\n variantsResponse(vopts, { method, url: req.url, body }),\n );\n if (result?.status === 200 && method === 'POST') {\n const updated = result.body as { active: string[]; changedFiles?: string[] };\n activate(updated.active, updated.changedFiles);\n }\n }).catch((error) => sendFailure(res, error));\n });\n\n // The CLI's `variant use` writes the active file with no request to\n // this server, and an agent edits overrides in place: both have to\n // reach the browser. The active file may not exist yet, so watch its\n // directory; chokidar reports files that appear later.\n server.watcher.add(dirname(activeFile));\n server.watcher.add(vDir);\n server.watcher.on('all', (_event: string, changed: string) => {\n const path = resolve(changed);\n if (path === activeFile) {\n activate(readActive(vopts));\n } else if (isInside(vDir, path)) {\n // Covers adds and unlinks, which handleHotUpdate never sees.\n overrides.clear();\n }\n });\n }\n\n if (options.panel === false) return;\n\n // A self-contained bundle keeps the panel out of the host's dependency\n // graph and works in React, Vue, Svelte and plain Vite apps alike.\n server.middlewares.use(BUNDLE_PATH, (_req, res) => {\n res.setHeader('content-type', 'text/javascript; charset=utf-8');\n res.setHeader('cache-control', 'no-store');\n res.end(readBundle());\n });\n const mountCode = mountModuleSource(options.rule ?? {}, options.key ?? '', variantsOptions !== null, options.shortcut, options.theme);\n\n server.middlewares.use((req, res, next) => {\n // Only navigations: buffering every response to look for </body>\n // would tax each of the hundreds of module requests a dev page makes.\n if (!(req.headers.accept ?? '').includes('text/html')) {\n next();\n return;\n }\n // Vite's ETag is computed from the document before the mount script\n // is appended, so a conditional request validates against the wrong\n // bytes: a browser holding a copy from before this plugin was added\n // gets 304 and never sees the panel. Ask for the full document.\n delete req.headers['if-none-match'];\n delete req.headers['if-modified-since'];\n injectHtmlResponse(res, mountCode);\n next();\n });\n },\n } satisfies Plugin;\n\n return plugin;\n}\n\n/** `ISOTONE_VARIANT=a, b` → `['a', 'b']`; unset or blank → `[]`. */\nfunction parseVariantList(raw: string | undefined): string[] {\n return (raw ?? '')\n .split(',')\n .map((id) => id.trim())\n .filter(Boolean);\n}\n\n/** Whether the request declares a JSON body (`application/json`, with or\n * without a charset parameter). */\nfunction isJsonRequest(req: IncomingMessage): boolean {\n const type = req.headers['content-type'];\n return typeof type === 'string' && type.toLowerCase().includes('application/json');\n}\n\n/**\n * Write a store response, or a 500 when the store throws. A store that\n * cannot write (permissions, a read-only mount) must still answer, or the\n * panel's request hangs and the reviewer sees nothing; a 500 sends a note\n * to the localStorage fallback. Returns the response so the caller can act\n * on a success, `null` on a throw.\n */\nfunction answer(\n res: ServerResponse,\n respond: () => { status: number; headers: Record<string, string>; body: unknown },\n): { status: number; body: unknown } | null {\n try {\n const { status, headers, body } = respond();\n res.statusCode = status;\n for (const [k, v] of Object.entries(headers)) res.setHeader(k, v);\n res.end(JSON.stringify(body));\n return { status, body };\n } catch (cause) {\n sendFailure(res, cause);\n return null;\n }\n}\n\n/** Load after the host has mounted, without importing its React or HMR runtime. */\nfunction mountModuleSource(rule: RouteGateOptions, key: string, variants: boolean, shortcut?: IterationShortcut | false, theme?: IterationTheme): string {\n const config = JSON.stringify({ endpoint: NOTES_ENDPOINT, updateEndpoint: UPDATE_ENDPOINT, key, shortcut, theme, rule, ...(variants ? { variants: 'overlay' } : {}) }).replace(/</g, '\\\\u003c');\n return `\n const start = () => {\n const mount = () => window.__isotone.mount(${config});\n if (window.__isotone) return mount();\n const script = document.createElement('script');\n script.src = ${JSON.stringify(BUNDLE_PATH)};\n script.onload = mount;\n document.head.appendChild(script);\n };\n if (document.readyState === 'complete') start();\n else window.addEventListener('load', start, { once: true });\n `;\n}\n\nfunction injectHtmlResponse(res: ServerResponse, code: string): void {\n if (INJECTED.has(res)) return;\n INJECTED.add(res);\n\n // Buffer the whole document and send it once from `end`. Passing chunks\n // through as they arrive would mean re-sending the document to inject, and\n // a streamed SSR body (React Router, Next) is chunked anyway, so nothing is\n // lost by waiting for `</body>`. Only navigations reach here (see the\n // `accept` check above), so this never buffers an SSE or module response.\n const originalWrite = res.write.bind(res);\n const originalEnd = res.end.bind(res);\n const originalSetHeader = res.setHeader.bind(res);\n const originalWriteHead = res.writeHead.bind(res);\n const chunks: Buffer[] = [];\n let buffering = true;\n\n // Web-stream adapters hand `res.write` a Uint8Array, which is neither a\n // string nor a Buffer; `chunk.toString('utf8')` on one yields a\n // comma-separated byte list, so wrap it instead of stringifying it.\n const toBuffer = (chunk: unknown): Buffer | null => {\n if (typeof chunk === 'string') return Buffer.from(chunk, 'utf8');\n if (Buffer.isBuffer(chunk)) return chunk;\n if (chunk instanceof Uint8Array) {\n return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);\n }\n return null;\n };\n\n // The injected script changes the bytes, so an upstream `Content-Length`\n // (Vite's `send`, a framework's own header) would truncate the document at\n // the original size, and an upstream `ETag` would describe a document the\n // browser never receives. Drop both; the response goes chunked and the\n // browser revalidates every time, which a dev page should anyway.\n const STRIPPED = new Set(['content-length', 'etag', 'last-modified']);\n const isLength = (name: string) => STRIPPED.has(name.toLowerCase());\n res.setHeader = ((name: string, value: number | string | readonly string[]) =>\n isLength(name) ? res : originalSetHeader(name, value)) as ServerResponse['setHeader'];\n res.writeHead = ((status: number, ...rest: unknown[]) => {\n const headers = rest[rest.length - 1];\n if (Array.isArray(headers)) {\n rest[rest.length - 1] = headers.filter((entry, i, all) => {\n // Either [[k, v], ...] or a flat [k, v, k, v, ...] list.\n if (Array.isArray(entry)) return !isLength(String(entry[0]));\n const key = i % 2 === 0 ? entry : all[i - 1];\n return !isLength(String(key));\n });\n } else if (headers && typeof headers === 'object') {\n for (const key of Object.keys(headers)) {\n if (isLength(key)) delete (headers as Record<string, unknown>)[key];\n }\n }\n return originalWriteHead(status, ...(rest as []));\n }) as unknown as ServerResponse['writeHead'];\n\n res.write = ((chunk: unknown, ...rest: unknown[]) => {\n if (!buffering) return originalWrite(chunk as string, ...(rest as []));\n const buf = toBuffer(chunk);\n if (buf) chunks.push(buf);\n const cb = rest.find((r): r is () => void => typeof r === 'function');\n if (cb) queueMicrotask(cb);\n return true;\n }) as unknown as ServerResponse['write'];\n\n res.end = ((...args: unknown[]) => {\n const buf = toBuffer(args[0]);\n if (buf) chunks.push(buf);\n const cb = args.find((a): a is () => void => typeof a === 'function');\n buffering = false;\n const body = Buffer.concat(chunks).toString('utf8');\n if (body.includes('<html') && (body.includes('<head') || body.includes('</body>'))) {\n const script = `<script type=\"module\" data-isotone-inject>${code}</script>`;\n if (!res.headersSent) originalSetHeader('Cache-Control', 'no-store');\n return originalEnd(withNativeFetch(injectIntoHead(body, script)), cb);\n }\n return originalEnd(Buffer.concat(chunks), cb);\n }) as unknown as ServerResponse['end'];\n}\n\n/**\n * Put the mount loader script in `<head>`, after the opening head tag. React\n * never hydrates head contents it receives as part of a server document —\n * `<head>` is a special host that React replaces wholesale, unlike `<body>`,\n * and any stray script inside it is inert for React — so this cannot trip the\n * \"script tag while rendering React component\" warning or the hydration\n * discard that follows it in a whole-document React Router app. Falls back to\n * just before `</head>`, then just before `</body>` for a fragment without a\n * head, then appends, matching what `withNativeFetch` does below.\n */\nfunction injectIntoHead(html: string, script: string): string {\n const head = html.match(/<head[^>]*>/i);\n if (head?.index !== undefined) {\n const at = head.index + head[0].length;\n return `${html.slice(0, at)}${script}${html.slice(at)}`;\n }\n const headEnd = html.match(/<\\/head>/i);\n if (headEnd?.index !== undefined) {\n return `${html.slice(0, headEnd.index)}${script}${html.slice(headEnd.index)}`;\n }\n const body = html.match(/<body[^>]*>/i);\n if (body?.index !== undefined) {\n const at = body.index + body[0].length;\n return `${html.slice(0, at)}${script}${html.slice(at)}`;\n }\n return `${html}${script}`;\n}\n\n/**\n * A one-line script as early in the document as possible that keeps a\n * reference to the browser's own `fetch`. Apps replace `window.fetch` for\n * their own reasons — a preview app answering its backend calls, a mock\n * layer, an auth shim — and a stub that answers 404 to anything it does not\n * know would swallow the panel's saves; the store then falls back to\n * localStorage and the reviewer's notes look lost. The store prefers this\n * reference when it exists. Placed right after `<head>` (or `<body>` when\n * there is no head) so it runs before any app module.\n */\nexport function withNativeFetch(html: string): string {\n if (html.includes('__isotoneFetch')) return html;\n const capture =\n '<script data-isotone-native>window.__isotoneFetch=window.fetch.bind(window)</script>';\n const head = html.match(/<head[^>]*>/i);\n if (head?.index !== undefined) {\n const at = head.index + head[0].length;\n return `${html.slice(0, at)}${capture}${html.slice(at)}`;\n }\n const body = html.match(/<body[^>]*>/i);\n if (body?.index !== undefined) {\n const at = body.index + body[0].length;\n return `${html.slice(0, at)}${capture}${html.slice(at)}`;\n }\n return html;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAMA,SAAS,YAAY,WAAW,cAAc,oBAAoB;AAClE,SAAS,SAAS,YAAY,MAAM,UAAU,eAAe;AAI7D,SAA0B,qBAAuD;AA4FjF,IAAM,eAAe;AACrB,IAAM,WAAW,oBAAI,QAAwB;AAC7C,IAAM,cAAc;AACpB,IAAM,WAAW;AASjB,IAAM,gBAAgB;AAItB,SAAS,SAAS,KAAa,MAAuB;AACpD,QAAM,MAAM,SAAS,KAAK,IAAI;AAC9B,SAAO,QAAQ,MAAO,CAAC,IAAI,WAAW,IAAI,KAAK,CAAC,WAAW,GAAG;AAChE;AAIA,SAAS,QAAQ,MAAsB;AACrC,MAAI;AACF,WAAO,aAAa,IAAI;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,UAAU,UAAgC,CAAC,GAAoB;AAC7E,MAAI,OAAO,QAAQ,QAAQ,QAAQ,YAAY;AAC/C,MAAI,QAAQ;AACZ,MAAI,UAAU;AACd,MAAI,UAAqC,MAAM;AAE/C,QAAM,kBAAkB,QAAQ,aAAa,QAAQ,OAAQ,QAAQ,YAAY,CAAC;AAClF,MAAI,QAA8B;AAAA,IAChC,MAAM,QAAQ,iBAAiB,QAAQ,GAAG;AAAA,IAC1C,GAAI,iBAAiB,MAAM,EAAE,KAAK,gBAAgB,IAAI,IAAI,CAAC;AAAA,EAC7D;AACA,MAAI,OAAO,YAAY,KAAK;AAC5B,MAAI,aAAa,KAAK,MAAM,MAAM,WAAW;AAQ7C,MAAI;AACJ,QAAM,YAAY,oBAAI,IAA2B;AAIjD,QAAM,WAAW,oBAAI,IAAY;AAEjC,QAAM,gBAAgB,MAAgB;AAEpC,QAAI,QAAS,QAAO,iBAAiB,QAAQ,IAAI,eAAe;AAChE,QAAI,cAAc,OAAW,aAAY,WAAW,KAAK;AACzD,WAAO;AAAA,EACT;AAEA,QAAM,cAAc,CAAC,QAAkB,SAAgC;AACrE,QAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,UAAM,MAAM,GAAG,CAAC,GAAG,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,CAAC,KAAK,IAAI;AACpD,QAAI,MAAM,UAAU,IAAI,GAAG;AAC3B,QAAI,QAAQ,QAAW;AACrB,YAAM,gBAAgB,OAAO,QAAQ,IAAI;AACzC,gBAAU,IAAI,KAAK,GAAG;AAAA,IACxB;AACA,WAAO;AAAA,EACT;AAMA,QAAM,cAAc,CAAC,SACnB,WAAW,IAAI,KACf,CAAC,KAAK,MAAM,OAAO,EAAE,SAAS,cAAc,KAC5C,SAAS,MAAM,MAAM,IAAI,KACzB,CAAC,SAAS,MAAM,IAAI,KACpB,CAAC,QAAQ,IAAI;AAKf,QAAM,SAAS;AAAA,IACb,MAAM;AAAA,IACN,SAAS;AAAA,IACT,eAAe,QAAwB;AACrC,cAAQ,OAAO,YAAY;AAC3B,gBAAU,OAAO,YAAY;AAC7B,gBAAU,OAAO;AACjB,aAAO,QAAQ,OAAO,MAAM,QAAQ,QAAQ,YAAY;AAIxD,cAAQ;AAAA,QACN,MAAM,QAAQ,QAAQ,OAAO,MAAM,iBAAiB,QAAQ,GAAG,CAAC;AAAA,QAChE,GAAI,iBAAiB,MAAM,EAAE,KAAK,gBAAgB,IAAI,IAAI,CAAC;AAAA,MAC7D;AACA,aAAO,YAAY,KAAK;AACxB,mBAAa,KAAK,MAAM,MAAM,WAAW;AAAA,IAC3C;AAAA,IACA,KAAK,IAAI;AACP,UAAI,CAAC,mBAAmB,cAAc,KAAK,EAAE,EAAG,QAAO;AAGvD,YAAM,OAAO,GAAG,QAAQ,WAAW,EAAE;AACrC,UAAI,CAAC,YAAY,IAAI,EAAG,QAAO;AAC/B,YAAM,WAAW,YAAY,cAAc,GAAG,IAAI;AAClD,UAAI,CAAC,SAAU,QAAO;AACtB,aAAO,EAAE,MAAM,aAAa,UAAU,MAAM,GAAG,KAAK,KAAK;AAAA,IAC3D;AAAA,IACA,gBAAgB,KAAiB;AAC/B,UAAI,CAAC,gBAAiB;AACtB,YAAM,EAAE,MAAM,SAAS,OAAO,IAAI;AAClC,UAAI,SAAS,MAAM,OAAO,GAAG;AAC3B,kBAAU,MAAM;AAChB,cAAM,MAAM,mBAAmB,OAAO,OAAO;AAG7C,YAAI,CAAC,OAAO,IAAI,SAAS,SAAU,QAAO,CAAC;AAC3C,YAAI,CAAC,cAAc,EAAE,SAAS,IAAI,EAAE,EAAG,QAAO,CAAC;AAK/C,cAAM,SAAS,cAAc,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC;AACvD,eAAO,CAAC,GAAI,OAAO,YAAY,iBAAiB,MAAM,KAAK,CAAC,CAAE;AAAA,MAChE;AAEA,UAAI,SAAS,OAAO,OAAO,EAAG;AAG9B,UAAI,YAAY,OAAO,KAAK,YAAY,cAAc,GAAG,OAAO,EAAG,QAAO,CAAC;AAC3E;AAAA,IACF;AAAA,IACA,gBAAgB,QAAQ;AACtB,UAAI,CAAC,MAAO;AAEZ,wBAAkB,IAAI;AAKtB,aAAO,YAAY,KAAK,aAAa,MAAM;AACzC,cAAM,QAAQ,SAAS,OAAO,OAAO,MAAM,IAAI,KAAK;AACpD,eAAO,OAAO,OAAO;AAAA,UACnB,2BAAsB,KAAK,MAAM,cAAc;AAAA,QACjD;AACA,YAAI,mBAAmB,WAAW,IAAI,GAAG;AACvC,gBAAM,WAAW,SAAS,OAAO,OAAO,MAAM,IAAI,KAAK;AACvD,gBAAM,IAAI,aAAa,KAAK,EAAE;AAC9B,iBAAO,OAAO,OAAO;AAAA,YACnB,8BAAyB,QAAQ,MAAM,CAAC,WAAW,MAAM,IAAI,KAAK,GAAG;AAAA,UACvE;AAAA,QACF;AAAA,MACF,CAAC;AAED,YAAM,cAAc,CAAC,KAAsB,QAAiC;AAC1E,YAAI;AAAE,sBAAY,IAAI,QAAQ,QAAQ,IAAI,QAAQ,QAAQ,EAAE;AAAG,iBAAO;AAAA,QAAM,SACrE,OAAO;AAAE,sBAAY,KAAK,KAAK;AAAG,iBAAO;AAAA,QAAO;AAAA,MACzD;AAEA,YAAM,UAAU,iBAAiB,OAAO,OAAO,MAAM,QAAQ,oBAAoB,KAAK;AACtF,aAAO,YAAY,KAAK,SAAS,QAAQ,OAAO;AAChD,aAAO,YAAY,IAAI,iBAAiB,CAAC,KAAK,QAAQ;AACpD,YAAI,CAAC,YAAY,KAAK,GAAG,EAAG;AAC5B,YAAI,UAAU,gBAAgB,iCAAiC;AAC/D,YAAI,UAAU,iBAAiB,UAAU;AACzC,YAAI,aAAa,IAAI,WAAW,QAAQ,MAAM;AAC9C,YAAI,IAAI,KAAK,UAAU,IAAI,WAAW,QAAQ,QAAQ,YAAY,IAAI,EAAE,OAAO,qBAAqB,CAAC,CAAC;AAAA,MACxG,CAAC;AAGD,aAAO,YAAY,IAAI,gBAAgB,CAAC,KAAK,KAAK,SAAS;AACzD,YAAI,CAAC,YAAY,KAAK,GAAG,EAAG;AAC5B,cAAM,SAAS,IAAI,UAAU;AAC7B,YAAI,WAAW,SAAS,WAAW,UAAU,WAAW,UAAU;AAChE,eAAK;AACL;AAAA,QACF;AACA,SAAC,WAAW,SAAS,aAAa,GAAG,IAAI,QAAQ,QAAQ,MAAS,GAAG,KAAK,CAAC,SAAS;AAClF,iBAAO,KAAK,MAAM,cAAc,MAAM,EAAE,QAAQ,KAAK,IAAI,KAAK,KAAK,CAAC,CAAC;AAAA,QACvE,CAAC,EAAE,MAAM,CAAC,UAAU,YAAY,KAAK,KAAK,CAAC;AAAA,MAC7C,CAAC;AAED,UAAI,iBAAiB;AAKnB,oBAAY,WAAW,KAAK;AAgB5B,cAAM,WAAW,CAAC,MAAgB,eAAyB,CAAC,MAAM;AAChE,gBAAM,OAAO,cAAc;AAC3B,gBAAM,UAAU;AAAA,YACd,GAAG,KAAK,OAAO,CAAC,OAAO,CAAC,KAAK,SAAS,EAAE,CAAC;AAAA,YACzC,GAAG,KAAK,OAAO,CAAC,OAAO,CAAC,KAAK,SAAS,EAAE,CAAC;AAAA,UAC3C;AACA,cAAI,QAAQ,WAAW,KAAK,aAAa,WAAW,EAAG;AACvD,sBAAY;AACZ,oBAAU,MAAM;AAChB,gBAAM,UAAU,IAAI,IAAY,aAAa,IAAI,CAACA,UAAS,KAAK,MAAM,MAAMA,KAAI,CAAC,CAAC;AAClF,qBAAW,MAAM,SAAS;AACxB,kBAAM,WAAW,YAAY,OAAO,EAAE;AACtC,uBAAW,OAAO,OAAO,KAAK,UAAU,SAAS,CAAC,CAAC,GAAG;AACpD,sBAAQ,IAAI,KAAK,MAAM,MAAM,GAAG,CAAC;AAAA,YACnC;AAAA,UACF;AACA,qBAAW,UAAU,SAAS;AAC5B,kBAAM,SAAS,OAAO,YAAY,iBAAiB,cAAc,MAAM,CAAC;AACxE,gBAAI,CAAC,UAAU,OAAO,SAAS,EAAG;AAClC,qBAAS,IAAI,MAAM;AACnB,mBAAO,QAAQ,KAAK,UAAU,MAAM;AAAA,UACtC;AAAA,QACF;AAKA,kBAAU,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AAClD,0BAAkB,UAAU;AAC5B,YAAI,gBAAgB,SAAS;AAC3B,oBAAU,MAAM,EAAE,WAAW,KAAK,CAAC;AACnC,2BAAiB,IAAI;AAAA,QACvB;AAEA,eAAO,YAAY,IAAI,mBAAmB,CAAC,KAAK,KAAK,SAAS;AAC5D,cAAI,CAAC,YAAY,KAAK,GAAG,EAAG;AAC5B,gBAAM,SAAS,IAAI,UAAU;AAC7B,cAAI,WAAW,SAAS,WAAW,QAAQ;AACzC,iBAAK;AACL;AAAA,UACF;AAMA,cAAI,WAAW,UAAU,CAAC,cAAc,GAAG,GAAG;AAC5C,gBAAI,aAAa;AACjB,gBAAI,UAAU,gBAAgB,iCAAiC;AAC/D,gBAAI,IAAI,KAAK,UAAU,EAAE,OAAO,wCAAwC,CAAC,CAAC;AAC1E;AAAA,UACF;AACA,WAAC,WAAW,SAAS,aAAa,GAAG,IAAI,QAAQ,QAAQ,MAAS,GAAG,KAAK,CAAC,SAAS;AAGlF,kBAAM,SAAS;AAAA,cAAO;AAAA,cAAK,MACzB,iBAAiB,OAAO,EAAE,QAAQ,KAAK,IAAI,KAAK,KAAK,CAAC;AAAA,YACxD;AACA,gBAAI,QAAQ,WAAW,OAAO,WAAW,QAAQ;AAC/C,oBAAM,UAAU,OAAO;AACvB,uBAAS,QAAQ,QAAQ,QAAQ,YAAY;AAAA,YAC/C;AAAA,UACF,CAAC,EAAE,MAAM,CAAC,UAAU,YAAY,KAAK,KAAK,CAAC;AAAA,QAC7C,CAAC;AAMD,eAAO,QAAQ,IAAI,QAAQ,UAAU,CAAC;AACtC,eAAO,QAAQ,IAAI,IAAI;AACvB,eAAO,QAAQ,GAAG,OAAO,CAAC,QAAgB,YAAoB;AAC5D,gBAAM,OAAO,QAAQ,OAAO;AAC5B,cAAI,SAAS,YAAY;AACvB,qBAAS,WAAW,KAAK,CAAC;AAAA,UAC5B,WAAW,SAAS,MAAM,IAAI,GAAG;AAE/B,sBAAU,MAAM;AAAA,UAClB;AAAA,QACF,CAAC;AAAA,MACH;AAEA,UAAI,QAAQ,UAAU,MAAO;AAI7B,aAAO,YAAY,IAAI,aAAa,CAAC,MAAM,QAAQ;AACjD,YAAI,UAAU,gBAAgB,gCAAgC;AAC9D,YAAI,UAAU,iBAAiB,UAAU;AACzC,YAAI,IAAI,WAAW,CAAC;AAAA,MACtB,CAAC;AACD,YAAM,YAAY,kBAAkB,QAAQ,QAAQ,CAAC,GAAG,QAAQ,OAAO,IAAI,oBAAoB,MAAM,QAAQ,UAAU,QAAQ,KAAK;AAEpI,aAAO,YAAY,IAAI,CAAC,KAAK,KAAK,SAAS;AAGzC,YAAI,EAAE,IAAI,QAAQ,UAAU,IAAI,SAAS,WAAW,GAAG;AACrD,eAAK;AACL;AAAA,QACF;AAKA,eAAO,IAAI,QAAQ,eAAe;AAClC,eAAO,IAAI,QAAQ,mBAAmB;AACtC,2BAAmB,KAAK,SAAS;AACjC,aAAK;AAAA,MACP,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAGA,SAAS,iBAAiB,KAAmC;AAC3D,UAAQ,OAAO,IACZ,MAAM,GAAG,EACT,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,EACrB,OAAO,OAAO;AACnB;AAIA,SAAS,cAAc,KAA+B;AACpD,QAAM,OAAO,IAAI,QAAQ,cAAc;AACvC,SAAO,OAAO,SAAS,YAAY,KAAK,YAAY,EAAE,SAAS,kBAAkB;AACnF;AASA,SAAS,OACP,KACA,SAC0C;AAC1C,MAAI;AACF,UAAM,EAAE,QAAQ,SAAS,KAAK,IAAI,QAAQ;AAC1C,QAAI,aAAa;AACjB,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,OAAO,EAAG,KAAI,UAAU,GAAG,CAAC;AAChE,QAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAC5B,WAAO,EAAE,QAAQ,KAAK;AAAA,EACxB,SAAS,OAAO;AACd,gBAAY,KAAK,KAAK;AACtB,WAAO;AAAA,EACT;AACF;AAGA,SAAS,kBAAkB,MAAwB,KAAa,UAAmB,UAAsC,OAAgC;AACvJ,QAAM,SAAS,KAAK,UAAU,EAAE,UAAU,gBAAgB,gBAAgB,iBAAiB,KAAK,UAAU,OAAO,MAAM,GAAI,WAAW,EAAE,UAAU,UAAU,IAAI,CAAC,EAAG,CAAC,EAAE,QAAQ,MAAM,SAAS;AAC9L,SAAO;AAAA;AAAA,mDAE0C,MAAM;AAAA;AAAA;AAAA,qBAGpC,KAAK,UAAU,WAAW,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOhD;AAEA,SAAS,mBAAmB,KAAqB,MAAoB;AACnE,MAAI,SAAS,IAAI,GAAG,EAAG;AACvB,WAAS,IAAI,GAAG;AAOhB,QAAM,gBAAgB,IAAI,MAAM,KAAK,GAAG;AACxC,QAAM,cAAc,IAAI,IAAI,KAAK,GAAG;AACpC,QAAM,oBAAoB,IAAI,UAAU,KAAK,GAAG;AAChD,QAAM,oBAAoB,IAAI,UAAU,KAAK,GAAG;AAChD,QAAM,SAAmB,CAAC;AAC1B,MAAI,YAAY;AAKhB,QAAM,WAAW,CAAC,UAAkC;AAClD,QAAI,OAAO,UAAU,SAAU,QAAO,OAAO,KAAK,OAAO,MAAM;AAC/D,QAAI,OAAO,SAAS,KAAK,EAAG,QAAO;AACnC,QAAI,iBAAiB,YAAY;AAC/B,aAAO,OAAO,KAAK,MAAM,QAAQ,MAAM,YAAY,MAAM,UAAU;AAAA,IACrE;AACA,WAAO;AAAA,EACT;AAOA,QAAM,WAAW,oBAAI,IAAI,CAAC,kBAAkB,QAAQ,eAAe,CAAC;AACpE,QAAM,WAAW,CAAC,SAAiB,SAAS,IAAI,KAAK,YAAY,CAAC;AAClE,MAAI,aAAa,CAAC,MAAc,UAC9B,SAAS,IAAI,IAAI,MAAM,kBAAkB,MAAM,KAAK;AACtD,MAAI,aAAa,CAAC,WAAmB,SAAoB;AACvD,UAAM,UAAU,KAAK,KAAK,SAAS,CAAC;AACpC,QAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,WAAK,KAAK,SAAS,CAAC,IAAI,QAAQ,OAAO,CAAC,OAAO,GAAG,QAAQ;AAExD,YAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,CAAC,SAAS,OAAO,MAAM,CAAC,CAAC,CAAC;AAC3D,cAAM,MAAM,IAAI,MAAM,IAAI,QAAQ,IAAI,IAAI,CAAC;AAC3C,eAAO,CAAC,SAAS,OAAO,GAAG,CAAC;AAAA,MAC9B,CAAC;AAAA,IACH,WAAW,WAAW,OAAO,YAAY,UAAU;AACjD,iBAAW,OAAO,OAAO,KAAK,OAAO,GAAG;AACtC,YAAI,SAAS,GAAG,EAAG,QAAQ,QAAoC,GAAG;AAAA,MACpE;AAAA,IACF;AACA,WAAO,kBAAkB,QAAQ,GAAI,IAAW;AAAA,EAClD;AAEA,MAAI,SAAS,CAAC,UAAmB,SAAoB;AACnD,QAAI,CAAC,UAAW,QAAO,cAAc,OAAiB,GAAI,IAAW;AACrE,UAAM,MAAM,SAAS,KAAK;AAC1B,QAAI,IAAK,QAAO,KAAK,GAAG;AACxB,UAAM,KAAK,KAAK,KAAK,CAAC,MAAuB,OAAO,MAAM,UAAU;AACpE,QAAI,GAAI,gBAAe,EAAE;AACzB,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,IAAI,SAAoB;AACjC,UAAM,MAAM,SAAS,KAAK,CAAC,CAAC;AAC5B,QAAI,IAAK,QAAO,KAAK,GAAG;AACxB,UAAM,KAAK,KAAK,KAAK,CAAC,MAAuB,OAAO,MAAM,UAAU;AACpE,gBAAY;AACZ,UAAM,OAAO,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM;AAClD,QAAI,KAAK,SAAS,OAAO,MAAM,KAAK,SAAS,OAAO,KAAK,KAAK,SAAS,SAAS,IAAI;AAClF,YAAM,SAAS,6CAA6C,IAAI;AAChE,UAAI,CAAC,IAAI,YAAa,mBAAkB,iBAAiB,UAAU;AACnE,aAAO,YAAY,gBAAgB,eAAe,MAAM,MAAM,CAAC,GAAG,EAAE;AAAA,IACtE;AACA,WAAO,YAAY,OAAO,OAAO,MAAM,GAAG,EAAE;AAAA,EAC9C;AACF;AAYA,SAAS,eAAe,MAAc,QAAwB;AAC5D,QAAM,OAAO,KAAK,MAAM,cAAc;AACtC,MAAI,MAAM,UAAU,QAAW;AAC7B,UAAM,KAAK,KAAK,QAAQ,KAAK,CAAC,EAAE;AAChC,WAAO,GAAG,KAAK,MAAM,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,KAAK,MAAM,EAAE,CAAC;AAAA,EACvD;AACA,QAAM,UAAU,KAAK,MAAM,WAAW;AACtC,MAAI,SAAS,UAAU,QAAW;AAChC,WAAO,GAAG,KAAK,MAAM,GAAG,QAAQ,KAAK,CAAC,GAAG,MAAM,GAAG,KAAK,MAAM,QAAQ,KAAK,CAAC;AAAA,EAC7E;AACA,QAAM,OAAO,KAAK,MAAM,cAAc;AACtC,MAAI,MAAM,UAAU,QAAW;AAC7B,UAAM,KAAK,KAAK,QAAQ,KAAK,CAAC,EAAE;AAChC,WAAO,GAAG,KAAK,MAAM,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,KAAK,MAAM,EAAE,CAAC;AAAA,EACvD;AACA,SAAO,GAAG,IAAI,GAAG,MAAM;AACzB;AAYO,SAAS,gBAAgB,MAAsB;AACpD,MAAI,KAAK,SAAS,gBAAgB,EAAG,QAAO;AAC5C,QAAM,UACJ;AACF,QAAM,OAAO,KAAK,MAAM,cAAc;AACtC,MAAI,MAAM,UAAU,QAAW;AAC7B,UAAM,KAAK,KAAK,QAAQ,KAAK,CAAC,EAAE;AAChC,WAAO,GAAG,KAAK,MAAM,GAAG,EAAE,CAAC,GAAG,OAAO,GAAG,KAAK,MAAM,EAAE,CAAC;AAAA,EACxD;AACA,QAAM,OAAO,KAAK,MAAM,cAAc;AACtC,MAAI,MAAM,UAAU,QAAW;AAC7B,UAAM,KAAK,KAAK,QAAQ,KAAK,CAAC,EAAE;AAChC,WAAO,GAAG,KAAK,MAAM,GAAG,EAAE,CAAC,GAAG,OAAO,GAAG,KAAK,MAAM,EAAE,CAAC;AAAA,EACxD;AACA,SAAO;AACT;","names":["file"]}
|
|
1
|
+
{"version":3,"sources":["../src/vite/index.ts"],"sourcesContent":["/**\n * Development-only notes, source variants and a self-contained panel for Vite.\n * Inject through the HTML response so SSR frameworks work too. The loader lives\n * in <head> and mounts after load, leaving the host's hydration and HMR alone.\n */\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { existsSync, mkdirSync, readFileSync, realpathSync } from 'node:fs';\nimport { dirname, isAbsolute, join, relative, resolve } from 'node:path';\n\n// `normalizePath` is a plain function, so importing it as a value does not\n// pull vite's nominal `Plugin` type into the structural return type below.\nimport { type HmrContext, normalizePath, type Plugin, type ResolvedConfig } from 'vite';\n\nimport { ensureDirIgnored, ensureFileIgnored, notesResponse, NOTES_ENDPOINT } from '../store';\nimport type { RouteGateOptions } from '../core/index';\nimport type { IterationShortcut } from '../react/IterationPanel';\nimport type { IterationTheme } from '../react/theme';\nimport { readBundle } from '../standalone/bundle';\nimport { startUpdateCheck, UPDATE_ENDPOINT } from '../update';\nimport { checkOrigin, readNodeJson, sendFailure } from '../server/http';\nimport { VARIANTS_ENDPOINT } from '../core/variants';\nimport {\n ACTIVE_FILE,\n listVariants,\n overrideSourcePath,\n readActive,\n readVariant,\n resolveOverride,\n variantsDir,\n variantsResponse,\n type VariantsStoreOptions,\n} from '../variants-store';\n\nexport interface IterationViteOptions {\n /** Inject the bundled panel. false keeps endpoints and variants for a manual mount. */\n panel?: boolean;\n /** Background npm notice, at most once a day. false disables the check. */\n checkForUpdates?: boolean;\n /** Defaults to auto (page background); system follows the OS preference. */\n theme?: IterationTheme;\n /** Defaults to Cmd/Ctrl+Shift+Period; customize or disable with false. */\n shortcut?: IterationShortcut | false;\n /** Where notes persist. Relative paths resolve against Vite's `root` (the\n * directory of the app, whatever the shell's cwd), so a monorepo package\n * started from the workspace root still writes next to its own config.\n * Defaults to `.iteration-feedback.json`; git-ignored automatically via\n * `.git/info/exclude`, with the pattern anchored at the repository root. */\n file?: string;\n /** Route visibility rule (denylist/allowlist) for the injected panel. */\n rule?: RouteGateOptions;\n /** localStorage namespace override. */\n key?: string;\n /**\n * Overlay variants: alternative source files under `.isotone/variants/\n * <id>/<repo-relative path>` that `load` swaps in for the real ones while\n * the variant is active. On by default; `false` turns the whole feature\n * off (no endpoint, no watcher, no tab in the panel).\n */\n variants?:\n | false\n | {\n /** The project the manifests' file paths are relative to. Defaults to\n * Vite's root. A sibling dev server that aliases into another package\n * (a preview app rendering another package's source) points this at that\n * package so the same variants apply to both. Relative to Vite's root\n * or absolute. */\n root?: string;\n /** Where the variants live, relative to `root`. Default\n * `.isotone/variants`. */\n dir?: string;\n /** Keep the variants dir out of git via `.git/info/exclude`. Off by\n * default: the files are the reviewable artefact and commit with\n * the change they propose. */\n ignored?: boolean;\n };\n}\n\n/**\n * What `iteration()` hands back.\n *\n * Only `name` is public, and that is deliberate. isotone is consumed as a\n * linked (`file:` / workspace) dependency, so TypeScript resolves the `vite`\n * import from *this* package's `node_modules`, not the consumer's. Two vite\n * type instances are nominally incompatible — rollup's plugin container\n * carries a private `_pluginContextMap` — and a single unassignable entry\n * poisons the overload resolution for the consumer's whole `plugins: []`\n * array, so every other plugin in it errors too. Vite's own `Plugin` requires\n * nothing but `name`, so a structural shape drops into any vite version's\n * `PluginOption` while the hooks below stay checked against the real vite\n * types inside this file.\n */\nexport interface IterationPlugin {\n name: string;\n /**\n * Declared with the loosest shape that still drops into vite's own\n * signature: a method, so its parameter is bivariant and vite's\n * `HmrContext` satisfies it, returning `void`, which any real return type\n * satisfies. The implementation returns vite's `ModuleNode[]`; naming that\n * type here would bring the nominal vite instance back.\n */\n handleHotUpdate?(ctx: { file: string }): void;\n}\n\nconst DEFAULT_FILE = '.iteration-feedback.json';\nconst INJECTED = new WeakSet<ServerResponse>();\nconst BUNDLE_PATH = '/@isotone/panel.js';\nconst MANIFEST = 'variant.json';\n\n/**\n * Import queries that hand the file to one of vite's own loaders (assets,\n * workers, inline HTML scripts). Those read the file from disk themselves\n * and shape the module around it; a `pre` load returning the override's\n * text in their place would break the module, so such requests are left to\n * them and show the real file.\n */\nconst SPECIAL_QUERY = /[?&](raw|url|inline|no-inline|worker|sharedworker|html-proxy)(?:[&=]|$)/;\n\n/** `dir` itself or a path below it; never a sibling whose name merely\n * starts the same (`src-old` under `src`). */\nfunction isInside(dir: string, path: string): boolean {\n const rel = relative(dir, path);\n return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));\n}\n\n/** The canonical form of a directory, or the path as given when it does not\n * exist yet (nothing to canonicalize; the comparison can only fail open). */\nfunction realDir(path: string): string {\n try {\n return realpathSync(path);\n } catch {\n return path;\n }\n}\n\nexport function iteration(options: IterationViteOptions = {}): IterationPlugin {\n let file = resolve(options.file ?? DEFAULT_FILE);\n let isDev = false;\n let isBuild = false;\n let isAsset: (file: string) => boolean = () => false;\n\n const variantsOptions = options.variants === false ? null : (options.variants ?? {});\n let vopts: VariantsStoreOptions = {\n root: resolve(variantsOptions?.root ?? '.'),\n ...(variantsOptions?.dir ? { dir: variantsOptions.dir } : {}),\n };\n let vDir = variantsDir(vopts);\n let activeFile = join(vopts.root, ACTIVE_FILE);\n\n // The active set, and which override (if any) answers each source path\n // under it. `resolveOverride` re-reads the manifests every call, and load()\n // asks once per module per page, so memoize per active set and drop the\n // memo whenever the set or anything under the variants dir changes.\n // `undefined` means not read yet: configureServer primes it, and a load()\n // that somehow runs first reads the file itself.\n let activeIds: string[] | undefined;\n const overrides = new Map<string, string | null>();\n // Real source paths activate() is reporting as changed right now. Their\n // hot update must go through, where an ordinary edit of an overridden\n // real file is dropped (see handleHotUpdate).\n const swapping = new Set<string>();\n\n const currentActive = (): string[] => {\n // A build takes its set from the environment: `ISOTONE_VARIANT=a,b`.\n if (isBuild) return parseVariantList(process.env.ISOTONE_VARIANT);\n if (activeIds === undefined) activeIds = readActive(vopts);\n return activeIds;\n };\n\n const overrideFor = (active: string[], path: string): string | null => {\n if (active.length === 0) return null;\n // Sorted: the same set in another order answers the same for every path.\n const key = `${[...active].sort().join(',')}\\0${path}`;\n let hit = overrides.get(key);\n if (hit === undefined) {\n hit = resolveOverride(vopts, active, path);\n overrides.set(key, hit);\n }\n return hit;\n };\n\n /** Whether load() may swap `path` at all: a real file of the project the\n * variants describe, not a dependency and not an override itself. */\n // Module ids reach load() with forward slashes on every platform, while a\n // watcher path is native; splitting on either separator covers both.\n const overridable = (path: string): boolean =>\n isAbsolute(path) &&\n !path.split(/[\\\\/]/).includes('node_modules') &&\n isInside(vopts.root, path) &&\n !isInside(vDir, path) &&\n !isAsset(path);\n\n // `satisfies`, not an annotation: the hooks are checked against vite's\n // types, but the object keeps its own shape, so `handleHotUpdate` stays a\n // plain method and drops into the structural type below.\n const plugin = {\n name: 'iso:iteration',\n enforce: 'pre',\n configResolved(config: ResolvedConfig) {\n isDev = config.command === 'serve';\n isBuild = config.command === 'build';\n isAsset = config.assetsInclude;\n file = resolve(config.root, options.file ?? DEFAULT_FILE);\n // Vite hands load() realpath'd module ids but leaves `config.root` as\n // configured, so a root reached through a symlink would never contain\n // any module and no override would ever apply. Canonicalize it here.\n vopts = {\n root: realDir(resolve(config.root, variantsOptions?.root ?? '.')),\n ...(variantsOptions?.dir ? { dir: variantsOptions.dir } : {}),\n };\n vDir = variantsDir(vopts);\n activeFile = join(vopts.root, ACTIVE_FILE);\n },\n load(id) {\n if (!variantsOptions || SPECIAL_QUERY.test(id)) return null;\n // `?v=<hash>`, `?t=<timestamp>`, `?import`: vite's own request\n // decorations, never part of the path on disk.\n const path = id.replace(/[?#].*$/, '');\n if (!overridable(path)) return null;\n const override = overrideFor(currentActive(), path);\n if (!override) return null;\n return { code: readFileSync(override, 'utf8'), map: null };\n },\n handleHotUpdate(ctx: HmrContext) {\n if (!variantsOptions) return;\n const { file: changed, server } = ctx;\n if (isInside(vDir, changed)) {\n overrides.clear();\n const hit = overrideSourcePath(vopts, changed);\n // The manifest, or a file directly in the variants dir: nothing the\n // browser has loaded. The panel re-lists variants when it opens.\n if (!hit || hit.file === MANIFEST) return [];\n if (!currentActive().includes(hit.id)) return [];\n // The browser holds the real file's URL, so the override's edit has\n // to be delivered as if the real file changed.\n // The module graph keys files by their forward-slash form, which is\n // what `join` produces on POSIX and not on Windows.\n const source = normalizePath(join(vopts.root, hit.file));\n return [...(server.moduleGraph.getModulesByFile(source) ?? [])];\n }\n // A change activate() reported: let Vite compute the update as usual.\n if (swapping.delete(changed)) return;\n // The real source of an overridden file: the browser is showing the\n // override, so there is nothing to update from this edit.\n if (overridable(changed) && overrideFor(currentActive(), changed)) return [];\n return;\n },\n configureServer(server) {\n if (!isDev) return;\n // Here, not at construction: a production build should not touch .git.\n ensureFileIgnored(file);\n\n // Say where the notes go, after Vite's own banner so it is not buried\n // under it. Portable setups fail quietly otherwise: a reviewer writes\n // notes and the agent looks in the wrong directory.\n server.httpServer?.once('listening', () => {\n const shown = relative(server.config.root, file) || file;\n server.config.logger.info(\n ` isotone notes → ${shown} (${NOTES_ENDPOINT})`,\n );\n if (variantsOptions && existsSync(vDir)) {\n const shownDir = relative(server.config.root, vDir) || vDir;\n const n = listVariants(vopts).length;\n server.config.logger.info(\n ` isotone variants → ${shownDir} (${n} variant${n === 1 ? '' : 's'})`,\n );\n }\n });\n\n const guardOrigin = (req: IncomingMessage, res: ServerResponse): boolean => {\n try { checkOrigin(req.headers.origin, req.headers.host ?? ''); return true; }\n catch (error) { sendFailure(res, error); return false; }\n };\n\n const updates = startUpdateCheck(server.config.root, options.checkForUpdates !== false);\n server.httpServer?.once('close', updates.dispose);\n server.middlewares.use(UPDATE_ENDPOINT, (req, res) => {\n if (!guardOrigin(req, res)) return;\n res.setHeader('content-type', 'application/json; charset=utf-8');\n res.setHeader('cache-control', 'no-store');\n res.statusCode = req.method === 'GET' ? 200 : 405;\n res.end(JSON.stringify(req.method === 'GET' ? updates.getSnapshot() : { error: 'method not allowed' }));\n });\n\n // Notes endpoint → local file.\n server.middlewares.use(NOTES_ENDPOINT, (req, res, next) => {\n if (!guardOrigin(req, res)) return;\n const method = req.method ?? '';\n if (method !== 'GET' && method !== 'POST' && method !== 'DELETE') {\n next();\n return;\n }\n (method === 'POST' ? readNodeJson(req) : Promise.resolve(undefined)).then((body) => {\n answer(res, () => notesResponse(file, { method, url: req.url, body }));\n }).catch((error) => sendFailure(res, error));\n });\n\n if (variantsOptions) {\n // Read the pointer now, before anything can rewrite it: activate()\n // diffs the previous set against the new one, and the endpoint has\n // already written the file by the time it runs, so a lazy first read\n // would see the new set twice and switch nothing.\n activeIds = readActive(vopts);\n\n /**\n * Switch the browser to the set `next` without reloading the page.\n * Every file overridden by a variant that entered or left the set\n * is reported to Vite as if it had just been edited on disk: Vite\n * invalidates it, runs load() again (which now answers with the\n * other content) and sends the same hot update an editor save\n * would, so React Fast Refresh swaps the component in place and the\n * reviewer keeps their scroll, their form state and the open panel.\n * Variants in both sets are untouched, so switching one group leaves\n * the others' components alone. Files the browser has not loaded\n * are skipped; Vite decides on its own when a module cannot be\n * hot-swapped and falls back to a reload for that one, exactly as\n * it would for a hand edit.\n */\n const activate = (next: string[], removedFiles: string[] = []) => {\n const prev = currentActive();\n const changed = [\n ...prev.filter((id) => !next.includes(id)),\n ...next.filter((id) => !prev.includes(id)),\n ];\n if (changed.length === 0 && removedFiles.length === 0) return;\n activeIds = next;\n overrides.clear();\n const sources = new Set<string>(removedFiles.map((file) => join(vopts.root, file)));\n for (const id of changed) {\n const manifest = readVariant(vopts, id);\n for (const key of Object.keys(manifest?.files ?? {})) {\n sources.add(join(vopts.root, key));\n }\n }\n for (const source of sources) {\n const loaded = server.moduleGraph.getModulesByFile(normalizePath(source));\n if (!loaded || loaded.size === 0) continue;\n swapping.add(source);\n server.watcher.emit('change', source);\n }\n };\n\n // The exclude helpers canonicalize through the filesystem, so the\n // active file's directory has to exist before its pattern can land;\n // the panel's first POST would otherwise leave an untracked file.\n mkdirSync(dirname(activeFile), { recursive: true });\n ensureFileIgnored(activeFile);\n if (variantsOptions.ignored) {\n mkdirSync(vDir, { recursive: true });\n ensureDirIgnored(vDir);\n }\n\n server.middlewares.use(VARIANTS_ENDPOINT, (req, res, next) => {\n if (!guardOrigin(req, res)) return;\n const method = req.method ?? '';\n if (method !== 'GET' && method !== 'POST') {\n next();\n return;\n }\n // A POST switches every open tab of this dev server to another set\n // of files. A cross-site page can fire a `no-cors` form post at\n // localhost, but only with a text/plain or form content type; a\n // JSON content type needs a preflight, which vite's CORS does not\n // grant to an unknown origin. So the header is the gate.\n if (method === 'POST' && !isJsonRequest(req)) {\n res.statusCode = 415;\n res.setHeader('content-type', 'application/json; charset=utf-8');\n res.end(JSON.stringify({ error: 'content-type must be application/json' }));\n return;\n }\n (method === 'POST' ? readNodeJson(req) : Promise.resolve(undefined)).then((body) => {\n // connect strips the mount from `req.url`, which is the shape\n // variantsResponse routes on (\"/\", \"/active?...\").\n const result = answer(res, () =>\n variantsResponse(vopts, { method, url: req.url, body }),\n );\n if (result?.status === 200 && method === 'POST') {\n const updated = result.body as { active: string[]; changedFiles?: string[] };\n activate(updated.active, updated.changedFiles);\n }\n }).catch((error) => sendFailure(res, error));\n });\n\n // The CLI's `variant use` writes the active file with no request to\n // this server, and an agent edits overrides in place: both have to\n // reach the browser. The active file may not exist yet, so watch its\n // directory; chokidar reports files that appear later.\n server.watcher.add(dirname(activeFile));\n server.watcher.add(vDir);\n server.watcher.on('all', (_event: string, changed: string) => {\n const path = resolve(changed);\n if (path === activeFile) {\n activate(readActive(vopts));\n } else if (isInside(vDir, path)) {\n // Covers adds and unlinks, which handleHotUpdate never sees.\n overrides.clear();\n }\n });\n }\n\n if (options.panel === false) return;\n\n // A self-contained bundle keeps the panel out of the host's dependency\n // graph and works in React, Vue, Svelte and plain Vite apps alike.\n server.middlewares.use(BUNDLE_PATH, (_req, res) => {\n res.setHeader('content-type', 'text/javascript; charset=utf-8');\n res.setHeader('cache-control', 'no-store');\n res.end(readBundle());\n });\n const mountCode = mountModuleSource(options.rule ?? {}, options.key ?? '', variantsOptions !== null, options.shortcut, options.theme);\n\n server.middlewares.use((req, res, next) => {\n // Only navigations: buffering every response to look for </body>\n // would tax each of the hundreds of module requests a dev page makes.\n if (!(req.headers.accept ?? '').includes('text/html')) {\n next();\n return;\n }\n // Vite's ETag is computed from the document before the mount script\n // is appended, so a conditional request validates against the wrong\n // bytes: a browser holding a copy from before this plugin was added\n // gets 304 and never sees the panel. Ask for the full document.\n delete req.headers['if-none-match'];\n delete req.headers['if-modified-since'];\n injectHtmlResponse(res, mountCode);\n next();\n });\n },\n } satisfies Plugin;\n\n return plugin;\n}\n\n/** `ISOTONE_VARIANT=a, b` → `['a', 'b']`; unset or blank → `[]`. */\nfunction parseVariantList(raw: string | undefined): string[] {\n return (raw ?? '')\n .split(',')\n .map((id) => id.trim())\n .filter(Boolean);\n}\n\n/** Whether the request declares a JSON body (`application/json`, with or\n * without a charset parameter). */\nfunction isJsonRequest(req: IncomingMessage): boolean {\n const type = req.headers['content-type'];\n return typeof type === 'string' && type.toLowerCase().includes('application/json');\n}\n\n/**\n * Write a store response, or a 500 when the store throws. A store that\n * cannot write (permissions, a read-only mount) must still answer, or the\n * panel's request hangs and the reviewer sees nothing; a 500 sends a note\n * to the localStorage fallback. Returns the response so the caller can act\n * on a success, `null` on a throw.\n */\nfunction answer(\n res: ServerResponse,\n respond: () => { status: number; headers: Record<string, string>; body: unknown },\n): { status: number; body: unknown } | null {\n try {\n const { status, headers, body } = respond();\n res.statusCode = status;\n for (const [k, v] of Object.entries(headers)) res.setHeader(k, v);\n res.end(JSON.stringify(body));\n return { status, body };\n } catch (cause) {\n sendFailure(res, cause);\n return null;\n }\n}\n\n/** Load after the host has mounted, without importing its React or HMR runtime. */\nfunction mountModuleSource(rule: RouteGateOptions, key: string, variants: boolean, shortcut?: IterationShortcut | false, theme?: IterationTheme): string {\n const config = JSON.stringify({ endpoint: NOTES_ENDPOINT, updateEndpoint: UPDATE_ENDPOINT, key, shortcut, theme, rule, ...(variants ? { variants: 'overlay' } : {}) }).replace(/</g, '\\\\u003c');\n return `\n const start = () => {\n const mount = () => window.__isotone.mount(${config});\n if (window.__isotone) return mount();\n const script = document.createElement('script');\n script.src = ${JSON.stringify(BUNDLE_PATH)};\n script.onload = mount;\n document.head.appendChild(script);\n };\n if (document.readyState === 'complete') start();\n else window.addEventListener('load', start, { once: true });\n `;\n}\n\nfunction injectHtmlResponse(res: ServerResponse, code: string): void {\n if (INJECTED.has(res)) return;\n INJECTED.add(res);\n\n // Buffer the whole document and send it once from `end`. Passing chunks\n // through as they arrive would mean re-sending the document to inject, and\n // a streamed SSR body (React Router, Next) is chunked anyway, so nothing is\n // lost by waiting for `</body>`. Only navigations reach here (see the\n // `accept` check above), so this never buffers an SSE or module response.\n const originalWrite = res.write.bind(res);\n const originalEnd = res.end.bind(res);\n const originalSetHeader = res.setHeader.bind(res);\n const originalWriteHead = res.writeHead.bind(res);\n const chunks: Buffer[] = [];\n let buffering = true;\n\n // Web-stream adapters hand `res.write` a Uint8Array, which is neither a\n // string nor a Buffer; `chunk.toString('utf8')` on one yields a\n // comma-separated byte list, so wrap it instead of stringifying it.\n const toBuffer = (chunk: unknown): Buffer | null => {\n if (typeof chunk === 'string') return Buffer.from(chunk, 'utf8');\n if (Buffer.isBuffer(chunk)) return chunk;\n if (chunk instanceof Uint8Array) {\n return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);\n }\n return null;\n };\n\n // The injected script changes the bytes, so an upstream `Content-Length`\n // (Vite's `send`, a framework's own header) would truncate the document at\n // the original size, and an upstream `ETag` would describe a document the\n // browser never receives. Drop both; the response goes chunked and the\n // browser revalidates every time, which a dev page should anyway.\n const STRIPPED = new Set(['content-length', 'etag', 'last-modified']);\n const isLength = (name: string) => STRIPPED.has(name.toLowerCase());\n res.setHeader = ((name: string, value: number | string | readonly string[]) =>\n isLength(name) ? res : originalSetHeader(name, value)) as ServerResponse['setHeader'];\n res.writeHead = ((status: number, ...rest: unknown[]) => {\n const headers = rest[rest.length - 1];\n if (Array.isArray(headers)) {\n rest[rest.length - 1] = headers.filter((entry, i, all) => {\n // Either [[k, v], ...] or a flat [k, v, k, v, ...] list.\n if (Array.isArray(entry)) return !isLength(String(entry[0]));\n const key = i % 2 === 0 ? entry : all[i - 1];\n return !isLength(String(key));\n });\n } else if (headers && typeof headers === 'object') {\n for (const key of Object.keys(headers)) {\n if (isLength(key)) delete (headers as Record<string, unknown>)[key];\n }\n }\n return originalWriteHead(status, ...(rest as []));\n }) as unknown as ServerResponse['writeHead'];\n\n res.write = ((chunk: unknown, ...rest: unknown[]) => {\n if (!buffering) return originalWrite(chunk as string, ...(rest as []));\n const buf = toBuffer(chunk);\n if (buf) chunks.push(buf);\n const cb = rest.find((r): r is () => void => typeof r === 'function');\n if (cb) queueMicrotask(cb);\n return true;\n }) as unknown as ServerResponse['write'];\n\n res.end = ((...args: unknown[]) => {\n const buf = toBuffer(args[0]);\n if (buf) chunks.push(buf);\n const cb = args.find((a): a is () => void => typeof a === 'function');\n buffering = false;\n const body = Buffer.concat(chunks).toString('utf8');\n if (body.includes('<html') && (body.includes('<head') || body.includes('</body>'))) {\n const script = `<script type=\"module\" data-isotone-inject>${code}</script>`;\n if (!res.headersSent) originalSetHeader('Cache-Control', 'no-store');\n return originalEnd(withNativeFetch(injectIntoHead(body, script)), cb);\n }\n return originalEnd(Buffer.concat(chunks), cb);\n }) as unknown as ServerResponse['end'];\n}\n\n/**\n * Put the mount loader script in `<head>`, after the opening head tag. React\n * never hydrates head contents it receives as part of a server document —\n * `<head>` is a special host that React replaces wholesale, unlike `<body>`,\n * and any stray script inside it is inert for React — so this cannot trip the\n * \"script tag while rendering React component\" warning or the hydration\n * discard that follows it in a whole-document React Router app. Falls back to\n * just before `</head>`, then just before `</body>` for a fragment without a\n * head, then appends, matching what `withNativeFetch` does below.\n */\nfunction injectIntoHead(html: string, script: string): string {\n const head = html.match(/<head[^>]*>/i);\n if (head?.index !== undefined) {\n const at = head.index + head[0].length;\n return `${html.slice(0, at)}${script}${html.slice(at)}`;\n }\n const headEnd = html.match(/<\\/head>/i);\n if (headEnd?.index !== undefined) {\n return `${html.slice(0, headEnd.index)}${script}${html.slice(headEnd.index)}`;\n }\n const body = html.match(/<body[^>]*>/i);\n if (body?.index !== undefined) {\n const at = body.index + body[0].length;\n return `${html.slice(0, at)}${script}${html.slice(at)}`;\n }\n return `${html}${script}`;\n}\n\n/**\n * A one-line script as early in the document as possible that keeps a\n * reference to the browser's own `fetch`. Apps replace `window.fetch` for\n * their own reasons — a preview app answering its backend calls, a mock\n * layer, an auth shim — and a stub that answers 404 to anything it does not\n * know would swallow the panel's saves; the store then falls back to\n * localStorage and the reviewer's notes look lost. The store prefers this\n * reference when it exists. Placed right after `<head>` (or `<body>` when\n * there is no head) so it runs before any app module.\n */\nexport function withNativeFetch(html: string): string {\n if (html.includes('__isotoneFetch')) return html;\n const capture =\n '<script data-isotone-native>window.__isotoneFetch=window.fetch.bind(window)</script>';\n const head = html.match(/<head[^>]*>/i);\n if (head?.index !== undefined) {\n const at = head.index + head[0].length;\n return `${html.slice(0, at)}${capture}${html.slice(at)}`;\n }\n const body = html.match(/<body[^>]*>/i);\n if (body?.index !== undefined) {\n const at = body.index + body[0].length;\n return `${html.slice(0, at)}${capture}${html.slice(at)}`;\n }\n return html;\n}\n"],"mappings":"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","names":["file"]}
|
package/docs/migration.md
CHANGED
|
@@ -1,83 +1,33 @@
|
|
|
1
|
-
# Move to Isotone
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
`.iteration-feedback.json`, and `/api/iteration/*`
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
the
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
In a Git checkout, the plan covers tracked supported text files across the
|
|
35
|
-
checkout and local variant files. It skips dependencies, build outputs,
|
|
36
|
-
symlinks, lockfiles, and feedback data. Review custom or untracked integration
|
|
37
|
-
files separately. If both data directories or both dependency names exist,
|
|
38
|
-
resolve that conflict before retrying; the command does not merge them.
|
|
39
|
-
|
|
40
|
-
Run the package-manager install command printed by the migration. It does not
|
|
41
|
-
install packages or rewrite the lockfile itself. When manager detection is
|
|
42
|
-
ambiguous, choose explicitly with `--manager npm|pnpm|yarn|bun`. Use `--json` to
|
|
43
|
-
give a coding agent a structured plan or result.
|
|
44
|
-
|
|
45
|
-
Review the diff and commit the manifest, lockfile, and source changes together.
|
|
46
|
-
Keep local notes and variants out of Git. Restart the development server, reopen
|
|
47
|
-
the app on the same origin, and confirm that notes and variant previews remain
|
|
48
|
-
available. Run the project's checks and verify that its production build does
|
|
49
|
-
not include the panel.
|
|
50
|
-
|
|
51
|
-
## Manual migration
|
|
52
|
-
|
|
53
|
-
For a setup the command cannot recognize:
|
|
54
|
-
|
|
55
|
-
1. Replace `iso-iterate` with `isotone@0.4.0` in the same dependency section and
|
|
56
|
-
run your package manager's install command.
|
|
57
|
-
2. Change package imports such as `iso-iterate/vite` to `isotone/vite`; keep
|
|
58
|
-
exported names such as `iteration`, `IterationPanel`, and `mountIteration`.
|
|
59
|
-
3. Change CLI commands to `isotone`, standalone script URLs to `/isotone.js`, and
|
|
60
|
-
environment variables to `ISOTONE_UPDATE_CHECK` and `ISOTONE_VARIANT`.
|
|
61
|
-
4. Move `.iso-iterate/` to `.isotone/` and update custom paths and ignore rules.
|
|
62
|
-
Keep `.iteration-feedback.json` and any custom notes filename unchanged.
|
|
63
|
-
5. Update the project's agent instructions, restart, and check the integration.
|
|
64
|
-
|
|
65
|
-
If variant override files import the old package, prefer the migration command:
|
|
66
|
-
it also updates the corresponding stored base snapshots. A manual import-only
|
|
67
|
-
edit can make those variants appear stale; review and sync them before promotion.
|
|
68
|
-
|
|
69
|
-
Do not run both versions of the panel at once or delete the old package's data
|
|
70
|
-
to fix a migration problem. Restore your backup if needed and compare the
|
|
71
|
-
printed plan with your custom setup.
|
|
72
|
-
|
|
73
|
-
## Browser data
|
|
74
|
-
|
|
75
|
-
On the same browser origin, Isotone carries over the previous default storage
|
|
76
|
-
keys for offline notes, drafts, and knob selections. Notes merge by ID; current
|
|
77
|
-
Isotone entries win a collision. Current drafts and knob values also take
|
|
78
|
-
precedence. An old entry is removed only after the migrated value has been
|
|
79
|
-
written and read back successfully. Custom storage keys stay unchanged.
|
|
80
|
-
|
|
81
|
-
Keep the same browser profile and development URL until pending notes have
|
|
82
|
-
reached the project's notes file. A different port, host, or protocol has separate
|
|
83
|
-
browser storage; neither the CLI migration nor a package update can read it.
|
|
1
|
+
# Move a project to Isotone
|
|
2
|
+
|
|
3
|
+
For the few projects using `iso-iterate`, make the rename in each project with
|
|
4
|
+
its coding agent. The package and CLI are now `isotone`; the exported API names,
|
|
5
|
+
`.iteration-feedback.json`, and `/api/iteration/*` stay the same.
|
|
6
|
+
|
|
7
|
+
1. Stop the development server. Save your current work and back up ignored
|
|
8
|
+
`.iso-iterate/` data and the notes file.
|
|
9
|
+
2. Replace the `iso-iterate` dependency with `isotone` in the same dependency
|
|
10
|
+
section. Run the project's package manager so it updates its lockfile.
|
|
11
|
+
3. Change package imports (for example, `iso-iterate/vite` to `isotone/vite`),
|
|
12
|
+
CLI commands, standalone script URLs (`/isotone.js`), environment variables
|
|
13
|
+
(`ISOTONE_UPDATE_CHECK` and `ISOTONE_VARIANT`), and agent instructions.
|
|
14
|
+
Keep API names such as `iteration`, `IterationPanel`, and `mountIteration`.
|
|
15
|
+
4. Move the entire `.iso-iterate/` directory to `.isotone/`, including
|
|
16
|
+
`knobs.json`, `active.json`, and any local variants. Update ignore rules,
|
|
17
|
+
including a `.git/info/exclude` entry for `.isotone/active.json` if the
|
|
18
|
+
old active file was ignored there. Check each variant preview
|
|
19
|
+
before promotion: a stored base snapshot containing the old package import
|
|
20
|
+
can appear stale after the rename. Recreate or sync that variant against the
|
|
21
|
+
renamed source rather than promoting it blindly. Leave
|
|
22
|
+
`.iteration-feedback.json` and custom notes filenames unchanged.
|
|
23
|
+
5. Review the diff, run the project's checks, restart its development server,
|
|
24
|
+
and confirm the panel, existing notes, and variants. Keep the panel and
|
|
25
|
+
endpoints out of production builds.
|
|
26
|
+
|
|
27
|
+
On the same browser origin, Isotone carries forward the previous default
|
|
28
|
+
storage keys for offline notes, drafts, and knob selections. A different host,
|
|
29
|
+
port, or browser profile has separate storage. Custom keys stay unchanged.
|
|
30
|
+
|
|
31
|
+
Use `isotone@0.4.1` or newer. The older 0.4.0 package includes a one-time
|
|
32
|
+
`migrate` command but also has note-saving bugs fixed in 0.4.1; use this guide
|
|
33
|
+
for the work repositories. Existing `iso-iterate` versions remain installable.
|
package/docs/updates.md
CHANGED
|
@@ -35,9 +35,9 @@ The suggested command pins the specific version you just checked, rather than a
|
|
|
35
35
|
`latest` tag that could move before installation. For example:
|
|
36
36
|
|
|
37
37
|
```sh
|
|
38
|
-
npm install -D --save-exact isotone@0.4.
|
|
39
|
-
pnpm add -D --exact isotone@0.4.
|
|
40
|
-
yarn add -D --exact isotone@0.4.
|
|
38
|
+
npm install -D --save-exact isotone@0.4.2
|
|
39
|
+
pnpm add -D --exact isotone@0.4.2
|
|
40
|
+
yarn add -D --exact isotone@0.4.2
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
These examples illustrate the syntax; use the version reported by your check.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "isotone",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Local feedback and design variants for your running web app. Pin elements, leave notes, and give your coding agent the context to make the change.",
|