isotone 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/AGENTS.md +79 -5
  2. package/CHANGELOG.md +34 -0
  3. package/README.md +64 -14
  4. package/dist/{IterationPanel-phJRYtUO.d.ts → IterationPanel-Bq77d5NT.d.ts} +3 -1
  5. package/dist/{chunk-K5RAC2BT.js → chunk-2FY2JNPG.js} +3 -5
  6. package/dist/chunk-2FY2JNPG.js.map +1 -0
  7. package/dist/{chunk-RKZWO37H.js → chunk-BTJHQXF7.js} +85 -1
  8. package/dist/chunk-BTJHQXF7.js.map +1 -0
  9. package/dist/chunk-JCTWHR7U.js +13 -0
  10. package/dist/chunk-JCTWHR7U.js.map +1 -0
  11. package/dist/{chunk-YTEUMO3O.js → chunk-KRS57POF.js} +17 -33
  12. package/dist/chunk-KRS57POF.js.map +1 -0
  13. package/dist/chunk-L6O4HYGP.js +1150 -0
  14. package/dist/chunk-L6O4HYGP.js.map +1 -0
  15. package/dist/chunk-MX4RQJFV.js +9 -0
  16. package/dist/chunk-MX4RQJFV.js.map +1 -0
  17. package/dist/chunk-YPDV3PX4.js +142 -0
  18. package/dist/chunk-YPDV3PX4.js.map +1 -0
  19. package/dist/cli.js +1040 -148
  20. package/dist/cli.js.map +1 -1
  21. package/dist/core.d.ts +92 -2
  22. package/dist/core.js +10 -3
  23. package/dist/index.d.ts +13 -28
  24. package/dist/index.js +586 -257
  25. package/dist/index.js.map +1 -1
  26. package/dist/isotone.global.js +98 -42
  27. package/dist/serve.d.ts +5 -3
  28. package/dist/serve.js +60 -23
  29. package/dist/serve.js.map +1 -1
  30. package/dist/server.d.ts +7 -0
  31. package/dist/server.js +24 -11
  32. package/dist/server.js.map +1 -1
  33. package/dist/vite-plugin.d.ts +4 -3
  34. package/dist/vite-plugin.js +23 -19
  35. package/dist/vite-plugin.js.map +1 -1
  36. package/docs/blast.md +164 -0
  37. package/docs/integrations.md +75 -15
  38. package/docs/migration.md +19 -0
  39. package/docs/updates.md +110 -4
  40. package/docs/variants.md +65 -7
  41. package/package.json +2 -1
  42. package/dist/chunk-A4DX2V4C.js +0 -7
  43. package/dist/chunk-A4DX2V4C.js.map +0 -1
  44. package/dist/chunk-BRBXIGZP.js +0 -593
  45. package/dist/chunk-BRBXIGZP.js.map +0 -1
  46. package/dist/chunk-K5RAC2BT.js.map +0 -1
  47. package/dist/chunk-LXW6WFOC.js +0 -402
  48. package/dist/chunk-LXW6WFOC.js.map +0 -1
  49. package/dist/chunk-RKZWO37H.js.map +0 -1
  50. package/dist/chunk-RQ2YI5KS.js +0 -24
  51. package/dist/chunk-RQ2YI5KS.js.map +0 -1
  52. package/dist/chunk-YTEUMO3O.js.map +0 -1
@@ -1,593 +0,0 @@
1
- import {
2
- OVERLAY_GROUP_ID,
3
- overlayGroups
4
- } from "./chunk-RKZWO37H.js";
5
- import {
6
- ensureFileIgnored,
7
- withFileLock,
8
- writeJson
9
- } from "./chunk-LXW6WFOC.js";
10
-
11
- // src/variants-store.ts
12
- import { execFileSync } from "child_process";
13
- import { createHash, randomUUID } from "crypto";
14
- import {
15
- existsSync,
16
- mkdirSync,
17
- lstatSync,
18
- readdirSync,
19
- readFileSync,
20
- realpathSync,
21
- rmSync,
22
- writeFileSync
23
- } from "fs";
24
- import { tmpdir } from "os";
25
- import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
26
- var DEFAULT_VARIANTS_DIR = ".isotone/variants";
27
- var ACTIVE_FILE = ".isotone/active.json";
28
- var KNOBS_FILE = ".isotone/knobs.json";
29
- function mutateVariants(opts, change) {
30
- return withFileLock(join(variantsDir(opts), ".mutation"), change);
31
- }
32
- function variantsDir(opts) {
33
- const dir = opts.dir ?? DEFAULT_VARIANTS_DIR;
34
- return isAbsolute(dir) ? dir : resolve(opts.root, dir);
35
- }
36
- function variantDir(opts, id) {
37
- if (!isValidVariantId(id)) throw new Error(`Invalid variant id "${id}"`);
38
- return join(variantsDir(opts), id);
39
- }
40
- function isValidVariantId(id) {
41
- return /^[a-z0-9][a-z0-9-]{0,63}$/.test(id);
42
- }
43
- function toPosixPath(p) {
44
- return p.split(sep).join("/");
45
- }
46
- function isEscaping(rel) {
47
- return rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel);
48
- }
49
- function staysInside(base, file) {
50
- const canonical = (path) => {
51
- let existing = resolve(path);
52
- while (dirname(existing) !== existing) {
53
- try {
54
- lstatSync(existing);
55
- break;
56
- } catch (error) {
57
- if (error.code !== "ENOENT") throw error;
58
- existing = dirname(existing);
59
- }
60
- }
61
- return resolve(realpathSync(existing), relative(existing, resolve(path)));
62
- };
63
- return !isEscaping(relative(canonical(base), canonical(file)));
64
- }
65
- function repoRelativeKey(root, file) {
66
- const abs = isAbsolute(file) ? resolve(file) : resolve(root, file);
67
- const rel = relative(root, abs);
68
- if (!rel || isEscaping(rel) || !isSafeManifestKey(toPosixPath(rel)) || !staysInside(root, abs)) {
69
- throw new Error(`"${file}" escapes the project root`);
70
- }
71
- return { abs, key: toPosixPath(rel) };
72
- }
73
- function isSafeManifestKey(key) {
74
- if (!key || isAbsolute(key) || /[\\:\0]/.test(key) || key === "variant.json") return false;
75
- return !key.split("/").some((segment) => !segment || segment === "." || segment === ".." || segment === ".git");
76
- }
77
- function isPlausibleVariant(v) {
78
- if (!v || typeof v !== "object" || Array.isArray(v)) return false;
79
- const o = v;
80
- return typeof o.id === "string" && typeof o.label === "string" && typeof o.createdAt === "string" && (o.route === void 0 || typeof o.route === "string") && (o.description === void 0 || typeof o.description === "string") && (o.group === void 0 || typeof o.group === "string" && OVERLAY_GROUP_ID.test(o.group)) && (o.groupLabel === void 0 || typeof o.groupLabel === "string") && !!o.files && typeof o.files === "object" && !Array.isArray(o.files) && Object.entries(o.files).every(([key, entry]) => isSafeManifestKey(key) && entry && typeof entry === "object" && "base" in entry && (entry.base === null || typeof entry.base === "string" && /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(entry.base)) && (!("added" in entry) || entry.added === true));
81
- }
82
- function readVariant(opts, id) {
83
- if (!isValidVariantId(id)) return null;
84
- const file = join(variantDir(opts, id), "variant.json");
85
- if (!existsSync(file)) return null;
86
- try {
87
- const data = JSON.parse(readFileSync(file, "utf8"));
88
- const dir = variantDir(opts, id);
89
- return isPlausibleVariant(data) && data.id === id && staysInside(variantsDir(opts), file) && Object.keys(data.files).every((key) => staysInside(opts.root, join(opts.root, key)) && staysInside(dir, join(dir, key))) ? data : null;
90
- } catch {
91
- return null;
92
- }
93
- }
94
- function writeVariant(opts, v) {
95
- if (!isPlausibleVariant(v)) throw new Error("Invalid variant manifest");
96
- const dir = variantDir(opts, v.id);
97
- if (!staysInside(variantsDir(opts), dir)) throw new Error("Variant directory escapes the variants tree");
98
- mkdirSync(dir, { recursive: true });
99
- writeJson(join(dir, "variant.json"), v);
100
- }
101
- function isStale(base, currentHash) {
102
- return base === null || currentHash === null || currentHash !== base;
103
- }
104
- function readVariants(opts) {
105
- const dir = variantsDir(opts);
106
- if (!existsSync(dir)) return [];
107
- return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => readVariant(opts, e.name)).filter((v) => v !== null).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
108
- }
109
- function listVariants(opts) {
110
- const dir = variantsDir(opts);
111
- if (!existsSync(dir)) return [];
112
- const root = resolve(opts.root);
113
- const ids = readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
114
- const statuses = [];
115
- const hashes = /* @__PURE__ */ new Map();
116
- for (const id of ids) {
117
- const manifest = readVariant(opts, id);
118
- if (!manifest) continue;
119
- const stale = [];
120
- const missing = [];
121
- for (const [file, entry] of Object.entries(manifest.files)) {
122
- const overrideAbs = join(dir, id, file);
123
- if (!existsSync(overrideAbs)) {
124
- missing.push(file);
125
- continue;
126
- }
127
- if (entry.added) {
128
- if (existsSync(join(root, file))) stale.push(file);
129
- continue;
130
- }
131
- const hashKey = `${entry.base?.length}:${file}`;
132
- if (!hashes.has(hashKey)) hashes.set(hashKey, hashFile(join(root, file), entry.base));
133
- if (isStale(entry.base, hashes.get(hashKey) ?? null)) stale.push(file);
134
- }
135
- statuses.push({ ...manifest, stale, missing });
136
- }
137
- statuses.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
138
- return statuses;
139
- }
140
- function createVariant(opts, input) {
141
- return mutateVariants(opts, () => {
142
- if (!isValidVariantId(input.id)) {
143
- throw new Error(`Invalid variant id "${input.id}"`);
144
- }
145
- if (input.group !== void 0) assertGroupId(input.group);
146
- if (readVariant(opts, input.id)) {
147
- throw new Error(`Variant "${input.id}" already exists`);
148
- }
149
- const variant = {
150
- id: input.id,
151
- label: input.label || input.id,
152
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
153
- files: {},
154
- ...input.route !== void 0 ? { route: input.route } : {},
155
- ...input.element !== void 0 ? { element: input.element } : {},
156
- ...input.note !== void 0 ? { note: input.note } : {},
157
- ...input.description !== void 0 ? { description: input.description } : {},
158
- ...input.group !== void 0 ? { group: input.group } : {},
159
- ...input.groupLabel !== void 0 ? { groupLabel: input.groupLabel } : {}
160
- };
161
- writeVariant(opts, variant);
162
- return variant;
163
- });
164
- }
165
- function assertGroupId(group) {
166
- if (!OVERLAY_GROUP_ID.test(group)) {
167
- throw new Error(`Invalid group id "${group}" (lowercase letters, digits and hyphens)`);
168
- }
169
- }
170
- function setVariantGroup(opts, id, group, groupLabel) {
171
- return mutateVariants(opts, () => {
172
- const manifest = readVariant(opts, id);
173
- if (!manifest) throw new Error(`No variant "${id}"`);
174
- assertGroupId(group);
175
- manifest.group = group;
176
- if (groupLabel !== void 0) manifest.groupLabel = groupLabel;
177
- writeVariant(opts, manifest);
178
- return manifest;
179
- });
180
- }
181
- function addVariantFile(opts, id, file, { create = false } = {}) {
182
- return mutateVariants(opts, () => {
183
- const manifest = readVariant(opts, id);
184
- if (!manifest) throw new Error(`No variant "${id}"`);
185
- const root = resolve(opts.root);
186
- const { abs: sourceAbs, key } = repoRelativeKey(root, file);
187
- if (Object.hasOwn(manifest.files, key)) return manifest;
188
- const destAbs = join(variantDir(opts, id), key);
189
- if (!staysInside(variantDir(opts, id), destAbs)) throw new Error("Override path escapes the variant directory");
190
- if (create) {
191
- if (existsSync(sourceAbs)) throw new Error(`"${file}" already exists; add it without --new`);
192
- mkdirSync(dirname(destAbs), { recursive: true });
193
- if (!existsSync(destAbs)) writeFileSync(destAbs, "");
194
- manifest.files = { ...manifest.files, [key]: { base: null, added: true } };
195
- writeVariant(opts, manifest);
196
- return manifest;
197
- }
198
- if (!existsSync(sourceAbs)) throw new Error(`"${file}" does not exist. Use --new for a file the variant adds.`);
199
- mkdirSync(dirname(destAbs), { recursive: true });
200
- writeFileSync(destAbs, readFileSync(sourceAbs));
201
- manifest.files = { ...manifest.files, [key]: { base: blobOf(sourceAbs) } };
202
- writeVariant(opts, manifest);
203
- return manifest;
204
- });
205
- }
206
- function removeVariantFile(opts, id, file) {
207
- return mutateVariants(opts, () => {
208
- const manifest = readVariant(opts, id);
209
- if (!manifest) throw new Error(`No variant "${id}"`);
210
- const root = resolve(opts.root);
211
- const { key } = repoRelativeKey(root, file);
212
- if (Object.hasOwn(manifest.files, key)) {
213
- delete manifest.files[key];
214
- rmSync(join(variantDir(opts, id), key), { force: true });
215
- writeVariant(opts, manifest);
216
- }
217
- return manifest;
218
- });
219
- }
220
- var CONFLICT_MARKER = "<<<<<<< variant";
221
- function promoteVariant(opts, id, { force = false } = {}) {
222
- return mutateVariants(opts, () => {
223
- if (!isValidVariantId(id)) throw new Error(`Invalid variant id "${id}"`);
224
- const manifest = readVariant(opts, id);
225
- if (!manifest) throw new Error(`No variant "${id}"`);
226
- const active = readActive(opts);
227
- const root = resolve(opts.root);
228
- const vDir = variantDir(opts, id);
229
- const missing = Object.keys(manifest.files).filter((key) => !existsSync(join(vDir, key)));
230
- if (missing.length) throw new Error(`"${id}" has missing overrides: ${missing.join(", ")}. Restore or remove them before promoting.`);
231
- if (!force) {
232
- const status = listVariants(opts).find((v) => v.id === id);
233
- if (status?.stale.length) {
234
- const reasons = status.stale.map(
235
- (file) => manifest.files[file]?.base === null ? `no base recorded for ${file} (not a git repo when the override was made)` : `${file} changed since the override was made`
236
- );
237
- throw new Error(
238
- `"${id}" is stale (${reasons.join("; ")}). Run \`variant sync ${id}\` first, or promote with --force.`
239
- );
240
- }
241
- const conflicted = Object.keys(manifest.files).filter((key) => {
242
- const overrideAbs = join(vDir, key);
243
- return existsSync(overrideAbs) && readFileSync(overrideAbs, "utf8").includes(CONFLICT_MARKER);
244
- });
245
- if (conflicted.length) {
246
- throw new Error(
247
- `"${id}" has unresolved conflict markers in ${conflicted.join(", ")}. Resolve them, or promote with --force.`
248
- );
249
- }
250
- }
251
- const siblings = overlayGroups(readVariants(opts)).find((g) => g.members.some((m) => m.id === id))?.members.filter((m) => m.id !== id) ?? [];
252
- const files = [];
253
- const landed = /* @__PURE__ */ new Map();
254
- for (const key of Object.keys(manifest.files)) {
255
- const overrideAbs = join(vDir, key);
256
- const destAbs = join(root, key);
257
- const before = existsSync(destAbs) ? currentBlob(destAbs) : null;
258
- mkdirSync(dirname(destAbs), { recursive: true });
259
- writeFileSync(destAbs, readFileSync(overrideAbs));
260
- landed.set(key, { before, after: blobOf(destAbs) });
261
- files.push(key);
262
- }
263
- rmSync(vDir, { recursive: true, force: true });
264
- if (active.includes(id)) writeActive(opts, active.filter((a) => a !== id));
265
- return { files, rebased: rebaseSiblings(opts, siblings, landed) };
266
- });
267
- }
268
- function rebaseSiblings(opts, siblings, landed) {
269
- const rebased = [];
270
- for (const sibling of siblings) {
271
- let moved = false;
272
- for (const [key, entry] of Object.entries(sibling.files)) {
273
- const change = landed.get(key);
274
- if (!change?.after || !(entry.added || entry.base !== null && entry.base === change.before)) continue;
275
- sibling.files[key] = { base: change.after };
276
- moved = true;
277
- }
278
- if (!moved) continue;
279
- writeVariant(opts, sibling);
280
- rebased.push(sibling.id);
281
- }
282
- return rebased;
283
- }
284
- function dropVariant(opts, id) {
285
- mutateVariants(opts, () => {
286
- if (!isValidVariantId(id)) throw new Error(`Invalid variant id "${id}"`);
287
- const active = readActive(opts);
288
- rmSync(variantDir(opts, id), { recursive: true, force: true });
289
- if (active.includes(id)) writeActive(opts, active.filter((a) => a !== id));
290
- });
291
- }
292
- var GIT_QUIET = ["ignore", "pipe", "ignore"];
293
- function blobOf(absPath) {
294
- try {
295
- return execFileSync("git", ["hash-object", "--no-filters", "-w", absPath], {
296
- cwd: dirname(absPath),
297
- encoding: "utf8",
298
- stdio: GIT_QUIET
299
- }).trim();
300
- } catch {
301
- return null;
302
- }
303
- }
304
- function currentBlob(absPath) {
305
- try {
306
- return execFileSync("git", ["hash-object", "--no-filters", absPath], {
307
- cwd: dirname(absPath),
308
- encoding: "utf8",
309
- stdio: GIT_QUIET
310
- }).trim();
311
- } catch {
312
- return null;
313
- }
314
- }
315
- function hashFile(file, base) {
316
- if (base === null) return null;
317
- try {
318
- const contents = readFileSync(file);
319
- return createHash(base.length === 64 ? "sha256" : "sha1").update(`blob ${contents.length}\0`).update(contents).digest("hex");
320
- } catch {
321
- return null;
322
- }
323
- }
324
- function catFile(cwd, hash) {
325
- try {
326
- return execFileSync("git", ["cat-file", "-p", hash], { cwd, encoding: "utf8", stdio: GIT_QUIET });
327
- } catch {
328
- return null;
329
- }
330
- }
331
- function mergeFile(overrideAbs, baseAbs, currentAbs) {
332
- try {
333
- const stdout = execFileSync(
334
- "git",
335
- [
336
- "merge-file",
337
- "-p",
338
- "-L",
339
- "variant",
340
- "-L",
341
- "base",
342
- "-L",
343
- "default",
344
- overrideAbs,
345
- baseAbs,
346
- currentAbs
347
- ],
348
- { encoding: "utf8", stdio: GIT_QUIET }
349
- );
350
- return { status: 0, stdout };
351
- } catch (err) {
352
- const e = err;
353
- return { status: e.status ?? null, stdout: e.stdout ?? "" };
354
- }
355
- }
356
- function isConflictStatus(status) {
357
- return status !== null && status >= 1 && status <= 127;
358
- }
359
- function recordBase(absPath, hash) {
360
- return blobOf(absPath) ?? hash;
361
- }
362
- function syncVariant(opts, id) {
363
- return mutateVariants(opts, () => {
364
- const manifest = readVariant(opts, id);
365
- if (!manifest) throw new Error(`No variant "${id}"`);
366
- const root = resolve(opts.root);
367
- const vDir = variantDir(opts, id);
368
- const merged = [];
369
- const conflicts = [];
370
- const unchanged = [];
371
- let dirty = false;
372
- for (const [file, entry] of Object.entries(manifest.files)) {
373
- const currentAbs = join(root, file);
374
- const overrideAbs = join(vDir, file);
375
- if (entry.added) {
376
- (existsSync(currentAbs) ? conflicts : unchanged).push(file);
377
- continue;
378
- }
379
- const currentHash = currentBlob(currentAbs);
380
- if (!isStale(entry.base, currentHash)) {
381
- unchanged.push(file);
382
- continue;
383
- }
384
- const baseContent = entry.base !== null ? catFile(dirname(currentAbs), entry.base) : null;
385
- if (baseContent === null || currentHash === null) {
386
- conflicts.push(file);
387
- continue;
388
- }
389
- const baseTmp = join(tmpdir(), `isotone-base-${randomUUID()}`);
390
- writeFileSync(baseTmp, baseContent);
391
- try {
392
- const result = mergeFile(overrideAbs, baseTmp, currentAbs);
393
- if (result.status === 0) {
394
- writeFileSync(overrideAbs, result.stdout);
395
- entry.base = recordBase(currentAbs, currentHash);
396
- dirty = true;
397
- merged.push(file);
398
- } else if (isConflictStatus(result.status)) {
399
- writeFileSync(overrideAbs, result.stdout);
400
- entry.base = recordBase(currentAbs, currentHash);
401
- dirty = true;
402
- conflicts.push(file);
403
- } else {
404
- conflicts.push(file);
405
- }
406
- } finally {
407
- rmSync(baseTmp, { force: true });
408
- }
409
- }
410
- if (dirty) writeVariant(opts, manifest);
411
- return { merged, conflicts, unchanged };
412
- });
413
- }
414
- function resolveOverride(opts, active, absSourcePath) {
415
- if (active.length === 0) return null;
416
- const root = resolve(opts.root);
417
- const relToRoot = relative(root, resolve(absSourcePath));
418
- if (isEscaping(relToRoot)) return null;
419
- const key = toPosixPath(relToRoot);
420
- for (const id of active) {
421
- const manifest = readVariant(opts, id);
422
- if (!manifest || !Object.hasOwn(manifest.files, key)) continue;
423
- const overrideAbs = join(variantDir(opts, id), key);
424
- if (existsSync(overrideAbs)) return overrideAbs;
425
- }
426
- return null;
427
- }
428
- function overrideSourcePath(opts, absOverridePath) {
429
- const rel = relative(variantsDir(opts), resolve(absOverridePath));
430
- if (!rel || isEscaping(rel)) return null;
431
- const [id, ...rest] = toPosixPath(rel).split("/");
432
- if (!id || rest.length === 0) return null;
433
- return { id, file: rest.join("/") };
434
- }
435
- function activeConflict(opts, ids) {
436
- const owner = /* @__PURE__ */ new Map();
437
- for (const id of ids) {
438
- const manifest = readVariant(opts, id);
439
- for (const file of Object.keys(manifest?.files ?? {})) {
440
- const first = owner.get(file);
441
- if (first !== void 0 && first !== id) return [first, id, file];
442
- owner.set(file, id);
443
- }
444
- }
445
- return null;
446
- }
447
- function crossGroupOverlaps(opts, id) {
448
- const variants = readVariants(opts);
449
- const self = variants.find((v) => v.id === id);
450
- if (!self) return [];
451
- const siblings = overlayGroups(variants).find((g) => g.members.some((m) => m.id === id))?.members ?? [];
452
- return variants.flatMap((other) => other.id === id || siblings.includes(other) ? [] : Object.keys(other.files).filter((file) => Object.hasOwn(self.files, file)).map((file) => ({ file, other: other.id })));
453
- }
454
- function readKnobs(opts) {
455
- const file = resolve(opts.root, KNOBS_FILE);
456
- if (!existsSync(file)) return [];
457
- let parsed;
458
- try {
459
- parsed = JSON.parse(readFileSync(file, "utf8"));
460
- } catch {
461
- return [];
462
- }
463
- if (!Array.isArray(parsed)) return [];
464
- const seen = /* @__PURE__ */ new Set();
465
- return parsed.filter((k) => {
466
- if (!k || typeof k !== "object") return false;
467
- const a = k;
468
- if (typeof a.id !== "string" || !isValidVariantId(a.id) || typeof a.label !== "string") return false;
469
- if (a.route !== void 0 && typeof a.route !== "string" && (!Array.isArray(a.route) || !a.route.every((route) => typeof route === "string"))) return false;
470
- if (a.showWhen !== void 0 && (!a.showWhen || typeof a.showWhen.axis !== "string" || a.showWhen.equals !== void 0 && typeof a.showWhen.equals !== "string" || a.showWhen.not !== void 0 && typeof a.showWhen.not !== "string")) return false;
471
- if (a.section !== void 0 && typeof a.section !== "string") return false;
472
- if (a.kind === "range") {
473
- const { min, max, step, defaultValue } = a;
474
- if (typeof min !== "number" || typeof max !== "number" || typeof step !== "number" || typeof defaultValue !== "number") return false;
475
- return [min, max, step, defaultValue, max - min].every(Number.isFinite) && min < max && step > 0 && defaultValue >= min && defaultValue <= max && (a.unit === void 0 || typeof a.unit === "string");
476
- }
477
- if (a.kind === "color") {
478
- const c = a;
479
- const hex = (value) => typeof value === "string" && /^#(?:[a-f0-9]{3}|[a-f0-9]{6})$/i.test(value);
480
- return hex(c.defaultColor) && Array.isArray(c.swatches) && c.swatches.every((s) => s && typeof s.id === "string" && typeof s.label === "string" && hex(s.value));
481
- }
482
- if (a.kind === "select") {
483
- const s = a;
484
- return Array.isArray(s.options) && s.options.length > 0 && s.options.every((o) => o && typeof o.id === "string" && typeof o.label === "string") && s.options.some((o) => o.id === s.defaultOption);
485
- }
486
- return false;
487
- }).filter((axis) => {
488
- if (seen.has(axis.id)) return false;
489
- seen.add(axis.id);
490
- return true;
491
- });
492
- }
493
- function readActive(opts) {
494
- const file = join(resolve(opts.root), ACTIVE_FILE);
495
- if (!existsSync(file)) return [];
496
- let raw;
497
- try {
498
- raw = JSON.parse(readFileSync(file, "utf8")).active;
499
- } catch {
500
- return [];
501
- }
502
- const ids = Array.isArray(raw) ? raw : typeof raw === "string" ? [raw] : [];
503
- const kept = [];
504
- const seen = /* @__PURE__ */ new Set();
505
- const owner = /* @__PURE__ */ new Set();
506
- for (const id of ids) {
507
- if (typeof id !== "string" || seen.has(id)) continue;
508
- seen.add(id);
509
- const manifest = readVariant(opts, id);
510
- if (!manifest) continue;
511
- const files = Object.keys(manifest.files);
512
- if (files.some((f) => owner.has(f))) continue;
513
- for (const f of files) owner.add(f);
514
- kept.push(id);
515
- }
516
- return kept;
517
- }
518
- function writeActive(opts, ids) {
519
- const file = join(resolve(opts.root), ACTIVE_FILE);
520
- mkdirSync(dirname(file), { recursive: true });
521
- const active = Array.from(new Set(ids));
522
- writeJson(file, { active });
523
- ensureFileIgnored(file);
524
- }
525
- function variantsResponse(opts, req) {
526
- const method = req.method ?? "";
527
- const path = (req.url ?? "/").split("?")[0] || "/";
528
- const json = (status, body) => ({
529
- status,
530
- headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" },
531
- body
532
- });
533
- if (method === "GET" && path === "/") {
534
- return json(200, { variants: listVariants(opts), active: readActive(opts), knobs: readKnobs(opts) });
535
- }
536
- if (method === "POST" && path === "/active") {
537
- const body = req.body;
538
- if (body === null || typeof body !== "object" || Array.isArray(body)) {
539
- return json(400, { error: "body must be a JSON object" });
540
- }
541
- const raw = body.active;
542
- if (!Array.isArray(raw) || !raw.every((id) => typeof id === "string")) {
543
- return json(400, { error: "active must be an array of variant ids" });
544
- }
545
- const active = Array.from(new Set(raw));
546
- for (const id of active) {
547
- if (!isValidVariantId(id)) return json(400, { error: `invalid variant id "${id}"` });
548
- }
549
- for (const id of active) {
550
- if (!readVariant(opts, id)) return json(404, { error: `unknown variant "${id}"` });
551
- }
552
- const conflict = activeConflict(opts, active);
553
- if (conflict) {
554
- const [a, b, file] = conflict;
555
- const groups = overlayGroups(readVariants(opts));
556
- const named = (id) => {
557
- const label = groups.find((group) => group.members.some((member) => member.id === id))?.label;
558
- return label ? `"${id}" (${label})` : `"${id}"`;
559
- };
560
- return json(409, {
561
- error: `${named(a)} and ${named(b)} both override ${file}; only one can be active`,
562
- conflict
563
- });
564
- }
565
- writeActive(opts, active);
566
- return json(200, { active });
567
- }
568
- return json(405, { error: "method not allowed" });
569
- }
570
-
571
- export {
572
- DEFAULT_VARIANTS_DIR,
573
- ACTIVE_FILE,
574
- variantsDir,
575
- variantDir,
576
- readVariant,
577
- readVariants,
578
- listVariants,
579
- createVariant,
580
- setVariantGroup,
581
- addVariantFile,
582
- removeVariantFile,
583
- promoteVariant,
584
- dropVariant,
585
- syncVariant,
586
- resolveOverride,
587
- overrideSourcePath,
588
- crossGroupOverlaps,
589
- readActive,
590
- writeActive,
591
- variantsResponse
592
- };
593
- //# sourceMappingURL=chunk-BRBXIGZP.js.map