isotone 1.0.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,17 +12,20 @@ import {
12
12
  overrideSourcePath,
13
13
  readActive,
14
14
  readVariant,
15
+ readVariants,
15
16
  resolveOverride,
17
+ variantDir,
16
18
  variantsDir,
17
19
  variantsResponse
18
- } from "./chunk-UT2NLGBM.js";
20
+ } from "./chunk-TGAU3WTF.js";
19
21
  import {
20
- VARIANTS_ENDPOINT
21
- } from "./chunk-OSETWKGE.js";
22
+ VARIANTS_ENDPOINT,
23
+ overlayGroups
24
+ } from "./chunk-PUXQGUMQ.js";
22
25
  import {
23
26
  UPDATE_ENDPOINT,
24
27
  startUpdateCheck
25
- } from "./chunk-MXGHTQB2.js";
28
+ } from "./chunk-B3JNZITE.js";
26
29
  import {
27
30
  NOTES_ENDPOINT,
28
31
  ensureDirIgnored,
@@ -34,12 +37,445 @@ import "./chunk-A4DX2V4C.js";
34
37
  // src/vite/index.ts
35
38
  import { existsSync, mkdirSync, readFileSync, realpathSync } from "fs";
36
39
  import { dirname, isAbsolute, join, relative, resolve } from "path";
37
- import { normalizePath } from "vite";
40
+ import {
41
+ normalizePath
42
+ } from "vite";
43
+
44
+ // src/vite/exports.ts
45
+ var ROUTE_EXPORTS = /* @__PURE__ */ new Set([
46
+ "loader",
47
+ "action",
48
+ "clientLoader",
49
+ "clientAction",
50
+ "headers",
51
+ "links",
52
+ "meta",
53
+ "handle",
54
+ "shouldRevalidate",
55
+ "middleware",
56
+ "clientMiddleware",
57
+ "unstable_middleware",
58
+ "unstable_clientMiddleware",
59
+ "HydrateFallback"
60
+ ]);
61
+ var IDENT = /^[A-Za-z_$][\w$]*$/;
62
+ function scanExports(code) {
63
+ const names = /* @__PURE__ */ new Set();
64
+ const lines = code.split("\n");
65
+ for (let i = 0; i < lines.length; i++) {
66
+ const line = (lines[i] ?? "").replace(/^(?:\/\*.*?\*\/\s*)+/, "");
67
+ if (!line.startsWith("export")) continue;
68
+ const rest = line.slice("export".length);
69
+ if (rest !== "" && !/^[\s{*]/.test(rest)) continue;
70
+ const body = rest.trim();
71
+ if (body.startsWith("{") || body.startsWith("type {")) {
72
+ if (body.startsWith("type")) continue;
73
+ let clause = body;
74
+ while (!clause.includes("}") && i + 1 < lines.length) clause += ` ${lines[++i]}`;
75
+ const inner = clause.slice(clause.indexOf("{") + 1, clause.indexOf("}"));
76
+ for (const raw of inner.split(",")) {
77
+ const spec = raw.replace(/\/\/.*$/, "").trim();
78
+ if (!spec || spec.startsWith("type ")) continue;
79
+ const as = spec.split(/\s+as\s+/);
80
+ const name = (as[1] ?? as[0] ?? "").trim().replace(/^["']|["']$/g, "");
81
+ if (!name) return null;
82
+ names.add(name);
83
+ }
84
+ continue;
85
+ }
86
+ if (/^type\s*\*/.test(body)) continue;
87
+ const star = body.match(/^\*\s+as\s+([A-Za-z_$][\w$]*)/);
88
+ if (star?.[1]) {
89
+ names.add(star[1]);
90
+ continue;
91
+ }
92
+ if (body.startsWith("*")) return null;
93
+ if (/^default\b/.test(body)) {
94
+ names.add("default");
95
+ continue;
96
+ }
97
+ if (/^(?:declare\s|type\s|interface\s)/.test(body)) continue;
98
+ if (/^(?:=|import\s|namespace\s|module\s|as\s)/.test(body)) return null;
99
+ const decl = body.match(
100
+ /^(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)|^(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)|^(?:const\s+)?enum\s+([A-Za-z_$][\w$]*)/
101
+ );
102
+ const declared = decl?.[1] ?? decl?.[2] ?? decl?.[3];
103
+ if (declared) {
104
+ names.add(declared);
105
+ continue;
106
+ }
107
+ const variable = body.match(/^(?:const|let|var)\s+(.*)$/);
108
+ if (variable) {
109
+ const target = (variable[1] ?? "").trim();
110
+ if (target.startsWith("{") || target.startsWith("[")) return null;
111
+ const name = target.match(/^([A-Za-z_$][\w$]*)/)?.[1];
112
+ if (!name) return null;
113
+ if (hasSecondDeclarator(target)) return null;
114
+ names.add(name);
115
+ continue;
116
+ }
117
+ return null;
118
+ }
119
+ for (const name of names) if (name !== "default" && !IDENT.test(name)) return null;
120
+ return [...names];
121
+ }
122
+ function isRouteModule(names) {
123
+ return names.includes("default") && names.some((name) => ROUTE_EXPORTS.has(name));
124
+ }
125
+ function hasSecondDeclarator(declaration) {
126
+ const text = declaration.replace(/\/\/.*$/, "").replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`/g, '""');
127
+ let depth = 0;
128
+ let initialized = false;
129
+ for (let i = 0; i < text.length; i++) {
130
+ const ch = text[i];
131
+ if (ch === "(" || ch === "[" || ch === "{" || ch === "<") depth++;
132
+ else if (ch === ")" || ch === "]" || ch === "}" || ch === ">" && text[i - 1] !== "=") depth = Math.max(0, depth - 1);
133
+ else if (depth === 0 && ch === "=" && text[i + 1] !== "=" && text[i + 1] !== ">" && text[i - 1] !== "=") initialized = true;
134
+ else if (depth === 0 && ch === "," && initialized) return true;
135
+ }
136
+ return depth === 0 && /,\s*$/.test(text);
137
+ }
138
+
139
+ // src/vite/live.ts
140
+ var VERSION_QUERY = "isotone";
141
+ var BASE_VERSION = "_base";
142
+ var LIVE_MODULE = "virtual:isotone/live";
143
+ var RESOLVED_LIVE_MODULE = "\0isotone-live";
144
+ var ACTIVE_EVENT = "isotone:active";
145
+ function liveStore(win, cfg) {
146
+ if (win.__isotoneLive && win.__isotoneLive.version === 1) return win.__isotoneLive;
147
+ const PARAM = "v";
148
+ const DEFAULT = "default";
149
+ const groups = cfg.groups;
150
+ const byId = new Map(groups.map((group) => [group.id, group]));
151
+ const listeners = /* @__PURE__ */ new Set();
152
+ const files = /* @__PURE__ */ new Map();
153
+ const history = win.history;
154
+ const replace = history.replaceState;
155
+ const push = history.pushState;
156
+ const decode = (text) => {
157
+ try {
158
+ return decodeURIComponent(text.replace(/\+/g, " "));
159
+ } catch {
160
+ return text;
161
+ }
162
+ };
163
+ const readParam = (search2) => {
164
+ for (const part of search2.replace(/^\?/, "").split("&")) {
165
+ const at = part.indexOf("=");
166
+ const key = at < 0 ? part : part.slice(0, at);
167
+ if (decode(key) === PARAM) return decode(at < 0 ? "" : part.slice(at + 1));
168
+ }
169
+ return null;
170
+ };
171
+ const parse = (search2) => {
172
+ const raw = readParam(search2);
173
+ if (raw === null) return { picks: null, foreign: [] };
174
+ const foreign = [];
175
+ const picks = /* @__PURE__ */ new Map();
176
+ for (const pair of raw.split(",")) {
177
+ if (!pair) continue;
178
+ const at = pair.indexOf(":");
179
+ const group = at < 0 ? void 0 : byId.get(pair.slice(0, at));
180
+ const value = at < 0 ? "" : pair.slice(at + 1);
181
+ if (group && (value === DEFAULT || group.members.includes(value))) picks.set(group.id, value);
182
+ else foreign.push(pair);
183
+ }
184
+ if (picks.size === 0) return { picks: null, foreign };
185
+ return { picks: [...picks.values()].filter((value) => value !== DEFAULT), foreign };
186
+ };
187
+ const search = (set) => {
188
+ const { foreign } = parse(win.location.search);
189
+ const mine = [];
190
+ for (const group of groups) {
191
+ const pick = group.members.find((member) => set.includes(member));
192
+ if (pick) mine.push(`${group.id}:${pick}`);
193
+ }
194
+ const value = [...foreign, ...mine].join(",");
195
+ const kept = win.location.search.replace(/^\?/, "").split("&").filter((part) => {
196
+ if (!part) return false;
197
+ const at = part.indexOf("=");
198
+ return decode(at < 0 ? part : part.slice(0, at)) !== PARAM;
199
+ });
200
+ if (value) kept.push(`${PARAM}=${encodeURIComponent(value).replace(/%3A/gi, ":").replace(/%2C/gi, ",")}`);
201
+ return kept.length ? `?${kept.join("&")}` : "";
202
+ };
203
+ const writeUrl = (set) => {
204
+ const next = `${win.location.pathname}${search(set)}${win.location.hash}`;
205
+ if (next !== `${win.location.pathname}${win.location.search}${win.location.hash}`) {
206
+ replace.call(history, history.state, "", next);
207
+ }
208
+ };
209
+ const fromUrl = parse(win.location.search).picks;
210
+ const server = cfg.active.slice();
211
+ let active = fromUrl ?? server.slice();
212
+ let explicit = fromUrl !== null;
213
+ let reloading = false;
214
+ const versionOf = (ids, set) => {
215
+ for (const id of set) if (ids.includes(id)) return id;
216
+ return null;
217
+ };
218
+ const needsReload = (from, to) => {
219
+ for (const file of files.values()) {
220
+ if (file.pinned && versionOf(file.ids, from) !== versionOf(file.ids, to)) return true;
221
+ }
222
+ return false;
223
+ };
224
+ const emit = () => {
225
+ for (const listener of listeners) listener();
226
+ };
227
+ const reload = () => {
228
+ reloading = true;
229
+ win.location.reload();
230
+ };
231
+ let syncing = false;
232
+ const freshLoad = () => {
233
+ try {
234
+ const entry = win.performance.getEntriesByType("navigation")[0];
235
+ if (entry?.type) return entry.type !== "reload";
236
+ return win.performance.navigation?.type !== 1;
237
+ } catch {
238
+ return true;
239
+ }
240
+ };
241
+ const syncServer = () => {
242
+ if (syncing || !freshLoad()) return;
243
+ syncing = true;
244
+ const key = "isotone:live-sync";
245
+ const target = JSON.stringify(active);
246
+ try {
247
+ const last = JSON.parse(win.sessionStorage.getItem(key) ?? "null");
248
+ if (last && last.target === target && Date.now() - last.at < 1e4) return;
249
+ win.sessionStorage.setItem(key, JSON.stringify({ target, at: Date.now() }));
250
+ } catch {
251
+ return;
252
+ }
253
+ const send = win.__isotoneFetch ?? win.fetch.bind(win);
254
+ send(`${cfg.endpoint}/active`, {
255
+ method: "POST",
256
+ headers: { "Content-Type": "application/json" },
257
+ body: JSON.stringify({ active })
258
+ }).then((res) => {
259
+ if (res.ok) reload();
260
+ }, () => {
261
+ });
262
+ };
263
+ const keep = (original) => function(...args) {
264
+ const result = original.apply(this, args);
265
+ if (explicit && parse(win.location.search).picks === null) writeUrl(active);
266
+ return result;
267
+ };
268
+ history.pushState = keep(push);
269
+ history.replaceState = keep(replace);
270
+ win.addEventListener("popstate", () => {
271
+ if (explicit) writeUrl(active);
272
+ });
273
+ const store = {
274
+ version: 1,
275
+ get active() {
276
+ return active;
277
+ },
278
+ get server() {
279
+ return server;
280
+ },
281
+ get explicit() {
282
+ return explicit;
283
+ },
284
+ subscribe(listener) {
285
+ listeners.add(listener);
286
+ return () => {
287
+ listeners.delete(listener);
288
+ };
289
+ },
290
+ register(key, ids, pinned) {
291
+ files.set(key, { ids, pinned });
292
+ if (cfg.ssr && pinned && win.document.readyState !== "complete" && versionOf(ids, active) !== versionOf(ids, server)) {
293
+ queueMicrotask(syncServer);
294
+ }
295
+ },
296
+ needsReload(next) {
297
+ return needsReload(active, next);
298
+ },
299
+ select(next) {
300
+ if (reloading) return true;
301
+ const reloadNeeded = needsReload(active, next);
302
+ active = next.slice();
303
+ explicit = next.length > 0;
304
+ writeUrl(active);
305
+ if (reloadNeeded) return true;
306
+ emit();
307
+ return false;
308
+ },
309
+ follow(next) {
310
+ if (explicit || reloading) return;
311
+ const same = next.length === active.length && next.every((id) => active.includes(id));
312
+ if (same) return;
313
+ const reloadNeeded = needsReload(active, next);
314
+ active = next.slice();
315
+ if (reloadNeeded) reload();
316
+ else emit();
317
+ },
318
+ reload
319
+ };
320
+ win.__isotoneLive = store;
321
+ return store;
322
+ }
323
+ function liveBinder(React, store) {
324
+ const MEMO = /* @__PURE__ */ Symbol.for("react.memo");
325
+ const FORWARD = /* @__PURE__ */ Symbol.for("react.forward_ref");
326
+ const LAZY = /* @__PURE__ */ Symbol.for("react.lazy");
327
+ const refAsProp = Number.parseInt(React.version, 10) >= 19;
328
+ const BUILTIN = /* @__PURE__ */ new Set([
329
+ "length",
330
+ "name",
331
+ "prototype",
332
+ "caller",
333
+ "arguments",
334
+ "displayName",
335
+ "$$typeof",
336
+ "render",
337
+ "type",
338
+ "compare",
339
+ "_payload",
340
+ "_init"
341
+ ]);
342
+ const isClass = (value) => typeof value === "function" && /^class[\s{]/.test(Function.prototype.toString.call(value));
343
+ const isComponent = (name, value, jsx) => {
344
+ if (typeof value === "function") {
345
+ const proto = value.prototype;
346
+ if (proto?.isReactComponent) return true;
347
+ if (isClass(value)) return false;
348
+ if (name !== "default") return /^[A-Z]/.test(name);
349
+ const own = value.name ?? "";
350
+ return /^[A-Z]/.test(own) || jsx && (own === "" || own === "default");
351
+ }
352
+ if (!value || typeof value !== "object") return false;
353
+ const tag = value.$$typeof;
354
+ return tag === MEMO || tag === FORWARD || tag === LAZY;
355
+ };
356
+ return {
357
+ file(key, ids, modules, names) {
358
+ const pick = (set) => {
359
+ for (const id of set) {
360
+ const hit = ids.indexOf(id);
361
+ if (hit >= 0) return hit;
362
+ }
363
+ const base = ids.indexOf(null);
364
+ return base >= 0 ? base : 0;
365
+ };
366
+ const out = {};
367
+ const renders = /* @__PURE__ */ new Map();
368
+ const jsx = /x$/.test(key);
369
+ let pinned = false;
370
+ for (const name of names) {
371
+ const values = modules.map((mod) => mod[name]);
372
+ const first = values[0];
373
+ if (values.every((value) => value === first)) {
374
+ out[name] = first;
375
+ continue;
376
+ }
377
+ const defined = values.filter((value) => value !== void 0);
378
+ if (defined.length > 0 && defined.every((value) => isComponent(name, value, jsx))) {
379
+ const render = (props, ref) => {
380
+ const index = React.useSyncExternalStore(
381
+ store.subscribe,
382
+ () => pick(store.active),
383
+ () => pick(store.server)
384
+ );
385
+ const component = values[index];
386
+ if (component == null) return null;
387
+ return React.createElement(component, ref == null ? props : { ...props, ref });
388
+ };
389
+ renders.set(name, render);
390
+ const wrapper = refAsProp ? (props) => render(props) : React.forwardRef(render);
391
+ const label = name === "default" ? defined[0].displayName ?? defined[0].name ?? "Variant" : name;
392
+ if (typeof wrapper === "function") Object.defineProperty(wrapper, "name", { value: label });
393
+ wrapper.displayName = label;
394
+ const statics = /* @__PURE__ */ new Set();
395
+ for (const value of defined) {
396
+ for (const prop of Object.getOwnPropertyNames(value)) if (!BUILTIN.has(prop)) statics.add(prop);
397
+ }
398
+ for (const prop of statics) {
399
+ if (prop in wrapper && !Object.getOwnPropertyDescriptor(wrapper, prop)?.configurable) continue;
400
+ Object.defineProperty(wrapper, prop, {
401
+ configurable: true,
402
+ enumerable: true,
403
+ get: () => values[pick(store.active)]?.[prop]
404
+ });
405
+ }
406
+ out[name] = wrapper;
407
+ continue;
408
+ }
409
+ pinned = true;
410
+ out[name] = values[pick(store.active)];
411
+ }
412
+ store.register(key, ids, pinned);
413
+ Object.defineProperty(out, "__isotoneRender", {
414
+ value: (name, props, ref) => {
415
+ const render = renders.get(name);
416
+ if (render) return render(props, ref);
417
+ const value = out[name];
418
+ return typeof value === "function" || value && typeof value === "object" ? React.createElement(value, ref == null ? props : { ...props, ref }) : null;
419
+ }
420
+ });
421
+ return out;
422
+ }
423
+ };
424
+ }
425
+ function liveStoreScript(cfg) {
426
+ return `(${liveStore.toString()})(window, ${safeJson(cfg)});`;
427
+ }
428
+ function liveRuntimeModule(fallback) {
429
+ return [
430
+ `import * as React from 'react';`,
431
+ `const store = (${liveStore.toString()})(window, ${safeJson(fallback)});`,
432
+ `export const live = (${liveBinder.toString()})(React, store);`,
433
+ `if (import.meta.hot) import.meta.hot.on(${JSON.stringify(ACTIVE_EVENT)}, (data) => store.follow(data.active));`,
434
+ ""
435
+ ].join("\n");
436
+ }
437
+ function dispatcherModule(key, basename, versions, names) {
438
+ const lines = [`import { live as __isotone } from ${JSON.stringify(LIVE_MODULE)};`];
439
+ versions.forEach((id, i) => {
440
+ const spec = `./${basename}?${VERSION_QUERY}=${id ?? BASE_VERSION}`;
441
+ lines.push(`import * as __v${i} from ${JSON.stringify(spec)};`);
442
+ });
443
+ const hoisted = (name) => name === "default" || /^[A-Z][\w$]*$/.test(name);
444
+ const local = (name) => name === "default" ? "__isotoneDefault" : name;
445
+ for (const name of names.filter(hoisted)) {
446
+ const head = name === "default" ? "export default function __isotoneDefault" : `export function ${name}`;
447
+ lines.push(`${head}(props, ref) { return __f.__isotoneRender(${JSON.stringify(name)}, props, ref); }`);
448
+ }
449
+ lines.push(
450
+ `const __f = __isotone.file(${JSON.stringify(key)}, ${JSON.stringify(versions)}, [${versions.map((_, i) => `__v${i}`).join(", ")}], ${JSON.stringify(names)});`
451
+ );
452
+ names.forEach((name, i) => {
453
+ if (hoisted(name)) {
454
+ lines.push(`${local(name)} = __f[${JSON.stringify(name)}];`);
455
+ return;
456
+ }
457
+ lines.push(`const __e${i} = __f[${JSON.stringify(name)}];`);
458
+ lines.push(`export { __e${i} as ${name} };`);
459
+ });
460
+ lines.push("");
461
+ return lines.join("\n");
462
+ }
463
+ function safeJson(value) {
464
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
465
+ }
466
+
467
+ // src/vite/index.ts
38
468
  var DEFAULT_FILE = ".iteration-feedback.json";
39
469
  var INJECTED = /* @__PURE__ */ new WeakSet();
40
470
  var BUNDLE_PATH = "/@isotone/panel.js";
41
471
  var MANIFEST = "variant.json";
42
472
  var SPECIAL_QUERY = /[?&](raw|url|inline|no-inline|worker|sharedworker|html-proxy)(?:[&=]|$)/;
473
+ var COMPARES_TYPES = /\.type\s*[!=]==?|[!=]==?\s*[\w$.]+\.type\b/;
474
+ function versionOfId(id) {
475
+ const at = id?.indexOf("?") ?? -1;
476
+ if (!id || at < 0) return null;
477
+ return new URLSearchParams(id.slice(at + 1)).get(VERSION_QUERY);
478
+ }
43
479
  function isInside(dir, path) {
44
480
  const rel = relative(dir, path);
45
481
  return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
@@ -65,7 +501,133 @@ function iteration(options = {}) {
65
501
  let activeFile = join(vopts.root, ACTIVE_FILE);
66
502
  let activeIds;
67
503
  const overrides = /* @__PURE__ */ new Map();
68
- const swapping = /* @__PURE__ */ new Set();
504
+ const swapping = /* @__PURE__ */ new Map();
505
+ let live = false;
506
+ let hasReact = false;
507
+ let ssrHost = false;
508
+ let manifests = null;
509
+ let byFile = null;
510
+ let addedFiles = null;
511
+ const dispatched = /* @__PURE__ */ new Map();
512
+ let reloadTimer = null;
513
+ const readManifests = () => {
514
+ manifests ??= readVariants(vopts);
515
+ return manifests;
516
+ };
517
+ const versionsByFile = () => {
518
+ if (byFile) return byFile;
519
+ byFile = /* @__PURE__ */ new Map();
520
+ for (const variant of readManifests()) {
521
+ for (const key of Object.keys(variant.files)) {
522
+ const path = normalizePath(join(vopts.root, key));
523
+ byFile.set(path, [...byFile.get(path) ?? [], variant.id]);
524
+ }
525
+ }
526
+ return byFile;
527
+ };
528
+ const filesOnlyInVariants = () => {
529
+ if (addedFiles) return addedFiles;
530
+ addedFiles = /* @__PURE__ */ new Map();
531
+ for (const path of versionsByFile().keys()) {
532
+ if (existsSync(path)) continue;
533
+ const stem = path.replace(/\.[cm]?[jt]sx?$/, "");
534
+ addedFiles.set(stem, [...addedFiles.get(stem) ?? [], path]);
535
+ }
536
+ return addedFiles;
537
+ };
538
+ const forgetManifests = () => {
539
+ manifests = null;
540
+ byFile = null;
541
+ addedFiles = null;
542
+ overrides.clear();
543
+ };
544
+ const groupsOf = (list) => overlayGroups(list).map((group) => ({ id: group.id, members: group.members.map((m) => m.id) }));
545
+ const liveConfig = () => ({
546
+ groups: groupsOf(readManifests()),
547
+ active: currentActive(),
548
+ ssr: ssrHost,
549
+ endpoint: VARIANTS_ENDPOINT
550
+ });
551
+ const overrideOf = (id, path) => {
552
+ const key = normalizePath(relative(vopts.root, path));
553
+ const manifest = readManifests().find((v) => v.id === id);
554
+ if (!manifest || !Object.hasOwn(manifest.files, key)) return null;
555
+ const file2 = join(variantDir(vopts, id), key);
556
+ return existsSync(file2) ? file2 : null;
557
+ };
558
+ const dispatchPlan = (path) => {
559
+ if (!/\.[cm]?[jt]sx?$/.test(path)) return null;
560
+ const ids = versionsByFile().get(path);
561
+ if (!ids?.length) return null;
562
+ const versions = [];
563
+ const names = /* @__PURE__ */ new Set();
564
+ const sources = [];
565
+ if (existsSync(path)) sources.push([null, path]);
566
+ for (const id of ids) {
567
+ const file2 = overrideOf(id, path);
568
+ if (file2) sources.push([id, file2]);
569
+ }
570
+ for (const [id, file2] of sources) {
571
+ const code = readFileSync(file2, "utf8");
572
+ if (COMPARES_TYPES.test(code)) return null;
573
+ const found = scanExports(code);
574
+ if (!found || isRouteModule(found)) return null;
575
+ for (const name of found) names.add(name);
576
+ versions.push(id);
577
+ }
578
+ if (versions.length === 0 || names.size === 0) return null;
579
+ return { key: normalizePath(relative(vopts.root, path)), versions, names: [...names].sort() };
580
+ };
581
+ const namesChanged = (path) => {
582
+ const plan = dispatchPlan(path);
583
+ return !plan || plan.names.join(",") !== dispatched.get(path);
584
+ };
585
+ let reloadPage = () => {
586
+ };
587
+ const swapWaiters = /* @__PURE__ */ new Set();
588
+ const hotModules = (changed, modulesOf, env) => {
589
+ if (!variantsOptions) return void 0;
590
+ const client = env === null || env === "client";
591
+ if (isInside(vDir, changed)) {
592
+ overrides.clear();
593
+ const hit = overrideSourcePath(vopts, changed);
594
+ if (!hit || hit.file === MANIFEST) return [];
595
+ const source = normalizePath(join(vopts.root, hit.file));
596
+ if (live && client && dispatched.has(source)) {
597
+ if (namesChanged(source)) {
598
+ reloadPage();
599
+ return [];
600
+ }
601
+ return [...modulesOf(source) ?? []].filter((m) => versionOfId(m.id) === hit.id);
602
+ }
603
+ if (!currentActive().includes(hit.id)) return [];
604
+ return [...modulesOf(source) ?? []];
605
+ }
606
+ const seen = swapping.get(changed);
607
+ let swap = false;
608
+ if (seen) {
609
+ if (env === null) {
610
+ swapping.delete(changed);
611
+ swap = true;
612
+ } else if (!seen.has(env)) {
613
+ seen.add(env);
614
+ swap = true;
615
+ for (const check of [...swapWaiters]) check();
616
+ }
617
+ }
618
+ const path = normalizePath(changed);
619
+ if (live && client && dispatched.has(path)) {
620
+ if (swap) return [];
621
+ if (namesChanged(path)) {
622
+ reloadPage();
623
+ return [];
624
+ }
625
+ return [...modulesOf(path) ?? []].filter((m) => versionOfId(m.id) === BASE_VERSION);
626
+ }
627
+ if (swap) return void 0;
628
+ if (overridable(changed) && overrideFor(currentActive(), changed)) return [];
629
+ return void 0;
630
+ };
69
631
  const currentActive = () => {
70
632
  if (isBuild) return parseVariantList(process.env.ISOTONE_VARIANT);
71
633
  if (activeIds === void 0) activeIds = readActive(vopts);
@@ -96,29 +658,67 @@ function iteration(options = {}) {
96
658
  };
97
659
  vDir = variantsDir(vopts);
98
660
  activeFile = join(vopts.root, ACTIVE_FILE);
661
+ hasReact = (config.plugins ?? []).some((p) => /react/i.test(p.name));
662
+ ssrHost = config.appType !== "spa" && config.appType !== "mpa";
663
+ forgetManifests();
664
+ },
665
+ async resolveId(source, importer, opts = {}) {
666
+ if (!variantsOptions) return null;
667
+ if (source === LIVE_MODULE) return RESOLVED_LIVE_MODULE;
668
+ if (!importer) return null;
669
+ const marker = source.indexOf(`?${VERSION_QUERY}=`);
670
+ if (live && marker > 0 && source.startsWith("./")) {
671
+ const dir = dirname(importer.replace(/[?#].*$/, ""));
672
+ return normalizePath(resolve(dir, source.slice(0, marker))) + source.slice(marker);
673
+ }
674
+ const added = filesOnlyInVariants();
675
+ if (added.size === 0 || source.startsWith("\0")) return null;
676
+ const bare = source.replace(/[?#].*$/, "");
677
+ const leaf = bare.slice(bare.lastIndexOf("/") + 1).replace(/\.[cm]?[jt]sx?$/, "");
678
+ const candidates = [...added.entries()].filter(([stem]) => stem.endsWith(`/${leaf}`) || stem.endsWith(`/${leaf}/index`));
679
+ if (candidates.length === 0) return null;
680
+ const resolved = await this.resolve(source, importer, { ...opts, skipSelf: true });
681
+ if (resolved) return resolved;
682
+ const importerPath = importer.replace(/[?#].*$/, "");
683
+ const target = bare.startsWith(".") ? normalizePath(resolve(dirname(importerPath), bare)).replace(/\.[cm]?[jt]sx?$/, "") : null;
684
+ const tail = bare.split("/").slice(1).join("/").replace(/\.[cm]?[jt]sx?$/, "");
685
+ const hits = candidates.filter(([stem]) => target ? stem === target || stem === `${target}/index` : tail !== "" && (stem.endsWith(`/${tail}`) || stem.endsWith(`/${tail}/index`)));
686
+ if (hits.length !== 1) return null;
687
+ const paths = hits[0]?.[1] ?? [];
688
+ return paths.length === 1 ? paths[0] : null;
99
689
  },
100
- load(id) {
690
+ load(id, opts) {
101
691
  if (!variantsOptions || SPECIAL_QUERY.test(id)) return null;
692
+ if (id === RESOLVED_LIVE_MODULE) return { code: liveRuntimeModule(liveConfig()), map: null };
102
693
  const path = id.replace(/[?#].*$/, "");
103
694
  if (!overridable(path)) return null;
695
+ const query = id.slice(path.length).replace(/^\?/, "").replace(/#.*$/, "");
696
+ const version = live ? new URLSearchParams(query).get(VERSION_QUERY) : null;
697
+ if (version !== null) {
698
+ const from = version === BASE_VERSION ? existsSync(path) ? path : null : overrideOf(version, path);
699
+ return { code: from ? readFileSync(from, "utf8") : "export {};\n", map: null };
700
+ }
701
+ if (live && !opts?.ssr && query === "") {
702
+ const plan = dispatchPlan(path);
703
+ if (plan) {
704
+ dispatched.set(path, plan.names.join(","));
705
+ const name = path.slice(path.lastIndexOf("/") + 1);
706
+ return { code: dispatcherModule(plan.key, name, plan.versions, plan.names), map: null };
707
+ }
708
+ }
104
709
  const override = overrideFor(currentActive(), path);
105
710
  if (!override) return null;
106
711
  return { code: readFileSync(override, "utf8"), map: null };
107
712
  },
713
+ // Vite 6 and later call this once per environment and skip
714
+ // handleHotUpdate; Vite 5 only knows handleHotUpdate.
715
+ hotUpdate(ctx) {
716
+ const env = this.environment;
717
+ return hotModules(ctx.file, (f) => env.moduleGraph.getModulesByFile(f), env.name);
718
+ },
108
719
  handleHotUpdate(ctx) {
109
- if (!variantsOptions) return;
110
- const { file: changed, server } = ctx;
111
- if (isInside(vDir, changed)) {
112
- overrides.clear();
113
- const hit = overrideSourcePath(vopts, changed);
114
- if (!hit || hit.file === MANIFEST) return [];
115
- if (!currentActive().includes(hit.id)) return [];
116
- const source = normalizePath(join(vopts.root, hit.file));
117
- return [...server.moduleGraph.getModulesByFile(source) ?? []];
118
- }
119
- if (swapping.delete(changed)) return;
120
- if (overridable(changed) && overrideFor(currentActive(), changed)) return [];
121
- return;
720
+ const { server } = ctx;
721
+ return hotModules(ctx.file, (f) => server.moduleGraph.getModulesByFile(f), null);
122
722
  },
123
723
  configureServer(server) {
124
724
  if (!isDev) return;
@@ -167,13 +767,32 @@ function iteration(options = {}) {
167
767
  });
168
768
  if (variantsOptions) {
169
769
  activeIds = readActive(vopts);
770
+ live = (variantsOptions.live ?? hasReact) && server.environments !== void 0;
771
+ const clientGraph = () => server.environments?.client?.moduleGraph ?? server.moduleGraph;
772
+ const signature = () => JSON.stringify(readManifests().map((v) => [v.id, v.group ?? "", Object.keys(v.files).sort()]));
773
+ let listed = live ? signature() : "";
774
+ reloadPage = () => {
775
+ if (reloadTimer) clearTimeout(reloadTimer);
776
+ reloadTimer = setTimeout(() => {
777
+ reloadTimer = null;
778
+ const graph = clientGraph();
779
+ const paths = /* @__PURE__ */ new Set([...dispatched.keys(), ...versionsByFile().keys()]);
780
+ for (const path of paths) {
781
+ for (const mod of graph.getModulesByFile(path) ?? []) graph.invalidateModule(mod);
782
+ }
783
+ const runtime = graph.getModuleById(RESOLVED_LIVE_MODULE);
784
+ if (runtime) graph.invalidateModule(runtime);
785
+ dispatched.clear();
786
+ server.ws.send({ type: "full-reload", path: "*" });
787
+ }, 50);
788
+ };
170
789
  const activate = (next, removedFiles = []) => {
171
790
  const prev = currentActive();
172
791
  const changed = [
173
792
  ...prev.filter((id) => !next.includes(id)),
174
793
  ...next.filter((id) => !prev.includes(id))
175
794
  ];
176
- if (changed.length === 0 && removedFiles.length === 0) return;
795
+ if (changed.length === 0 && removedFiles.length === 0) return Promise.resolve();
177
796
  activeIds = next;
178
797
  overrides.clear();
179
798
  const sources = new Set(removedFiles.map((file2) => join(vopts.root, file2)));
@@ -183,12 +802,39 @@ function iteration(options = {}) {
183
802
  sources.add(join(vopts.root, key));
184
803
  }
185
804
  }
805
+ const emitted = [];
186
806
  for (const source of sources) {
187
807
  const loaded = server.moduleGraph.getModulesByFile(normalizePath(source));
188
808
  if (!loaded || loaded.size === 0) continue;
189
- swapping.add(source);
809
+ swapping.set(source, /* @__PURE__ */ new Set());
810
+ emitted.push(source);
190
811
  server.watcher.emit("change", source);
191
812
  }
813
+ const settled = serverSawSwap(emitted);
814
+ if (live) void settled.then(() => server.ws.send({ type: "custom", event: ACTIVE_EVENT, data: { active: next } }));
815
+ return settled;
816
+ };
817
+ const serverSawSwap = (files) => {
818
+ const envs = Object.keys(server.environments ?? {}).filter((name) => name !== "client");
819
+ if (!live || files.length === 0 || envs.length === 0) return Promise.resolve();
820
+ return new Promise((done) => {
821
+ const check = () => {
822
+ const seen = files.every((file2) => {
823
+ const by = swapping.get(file2);
824
+ return !by || envs.every((env) => by.has(env));
825
+ });
826
+ if (!seen) return;
827
+ finish();
828
+ };
829
+ const timer = setTimeout(() => finish(), 1e3);
830
+ const finish = () => {
831
+ clearTimeout(timer);
832
+ swapWaiters.delete(check);
833
+ setTimeout(done, 30);
834
+ };
835
+ swapWaiters.add(check);
836
+ check();
837
+ });
192
838
  };
193
839
  mkdirSync(dirname(activeFile), { recursive: true });
194
840
  ensureFileIgnored(activeFile);
@@ -210,13 +856,21 @@ function iteration(options = {}) {
210
856
  return;
211
857
  }
212
858
  (method === "POST" ? readNodeJson(req) : Promise.resolve(void 0)).then((body) => {
213
- const result = answer(
214
- res,
215
- () => variantsResponse(vopts, { method, url: req.url, body })
216
- );
217
- if (result?.status === 200 && method === "POST") {
859
+ let result;
860
+ try {
861
+ result = variantsResponse(vopts, { method, url: req.url, body });
862
+ } catch (error) {
863
+ sendFailure(res, error);
864
+ return;
865
+ }
866
+ const send = () => {
867
+ answer(res, () => result);
868
+ };
869
+ if (result.status === 200 && method === "POST") {
218
870
  const updated = result.body;
219
- activate(updated.active, updated.changedFiles);
871
+ activate(updated.active, updated.changedFiles).then(send, send);
872
+ } else {
873
+ send();
220
874
  }
221
875
  }).catch((error) => sendFailure(res, error));
222
876
  });
@@ -225,13 +879,34 @@ function iteration(options = {}) {
225
879
  server.watcher.on("all", (_event, changed) => {
226
880
  const path = resolve(changed);
227
881
  if (path === activeFile) {
228
- activate(readActive(vopts));
882
+ void activate(readActive(vopts));
229
883
  } else if (isInside(vDir, path)) {
230
- overrides.clear();
884
+ forgetManifests();
885
+ if (live) {
886
+ const now = signature();
887
+ if (now !== listed) {
888
+ listed = now;
889
+ reloadPage();
890
+ }
891
+ }
231
892
  }
232
893
  });
233
894
  }
234
- if (options.panel === false) return;
895
+ const liveScript = () => live ? liveStoreScript(liveConfig()) : null;
896
+ if (options.panel === false) {
897
+ if (!live) return;
898
+ server.middlewares.use((req, res, next) => {
899
+ if (!(req.headers.accept ?? "").includes("text/html")) {
900
+ next();
901
+ return;
902
+ }
903
+ delete req.headers["if-none-match"];
904
+ delete req.headers["if-modified-since"];
905
+ injectHtmlResponse(res, null, liveScript);
906
+ next();
907
+ });
908
+ return;
909
+ }
235
910
  server.middlewares.use(BUNDLE_PATH, (_req, res) => {
236
911
  res.setHeader("content-type", "text/javascript; charset=utf-8");
237
912
  res.setHeader("cache-control", "no-store");
@@ -245,7 +920,7 @@ function iteration(options = {}) {
245
920
  }
246
921
  delete req.headers["if-none-match"];
247
922
  delete req.headers["if-modified-since"];
248
- injectHtmlResponse(res, mountCode);
923
+ injectHtmlResponse(res, mountCode, liveScript);
249
924
  next();
250
925
  });
251
926
  }
@@ -286,7 +961,7 @@ function mountModuleSource(rule, key, variants, shortcut, theme) {
286
961
  else window.addEventListener('load', start, { once: true });
287
962
  `;
288
963
  }
289
- function injectHtmlResponse(res, code) {
964
+ function injectHtmlResponse(res, code, head = () => null) {
290
965
  if (INJECTED.has(res)) return;
291
966
  INJECTED.add(res);
292
967
  const originalWrite = res.write.bind(res);
@@ -336,9 +1011,11 @@ function injectHtmlResponse(res, code) {
336
1011
  buffering = false;
337
1012
  const body = Buffer.concat(chunks).toString("utf8");
338
1013
  if (body.includes("<html") && (body.includes("<head") || body.includes("</body>"))) {
339
- const script = `<script type="module" data-isotone-inject>${code}</script>`;
1014
+ const script = code === null ? "" : `<script type="module" data-isotone-inject>${code}</script>`;
1015
+ const store = head();
1016
+ const early = store ? `<script data-isotone-live>${store.replace(/<\/script/gi, "<\\/script")}</script>` : "";
340
1017
  if (!res.headersSent) originalSetHeader("Cache-Control", "no-store");
341
- return originalEnd(withNativeFetch(injectIntoHead(body, script)), cb);
1018
+ return originalEnd(withNativeFetch(injectIntoHead(body, `${early}${script}`)), cb);
342
1019
  }
343
1020
  return originalEnd(Buffer.concat(chunks), cb);
344
1021
  });