uidex 0.2.4 → 0.3.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 (67) hide show
  1. package/README.md +253 -353
  2. package/dist/cli/cli.cjs +3243 -0
  3. package/dist/cli/cli.cjs.map +1 -0
  4. package/dist/cloud/index.cjs +149 -0
  5. package/dist/cloud/index.cjs.map +1 -0
  6. package/dist/cloud/index.d.cts +108 -0
  7. package/dist/cloud/index.d.ts +108 -0
  8. package/dist/cloud/index.js +120 -0
  9. package/dist/cloud/index.js.map +1 -0
  10. package/dist/headless/index.cjs +3580 -0
  11. package/dist/headless/index.cjs.map +1 -0
  12. package/dist/headless/index.d.cts +214 -0
  13. package/dist/headless/index.d.ts +214 -0
  14. package/dist/headless/index.js +3562 -0
  15. package/dist/headless/index.js.map +1 -0
  16. package/dist/index.cjs +6902 -9801
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +901 -146
  19. package/dist/index.d.ts +901 -146
  20. package/dist/index.js +6896 -9805
  21. package/dist/index.js.map +1 -1
  22. package/dist/playwright/index.cjs +164 -24
  23. package/dist/playwright/index.cjs.map +1 -1
  24. package/dist/playwright/index.d.cts +30 -53
  25. package/dist/playwright/index.d.ts +30 -53
  26. package/dist/playwright/index.js +148 -21
  27. package/dist/playwright/index.js.map +1 -1
  28. package/dist/playwright/reporter.cjs +62 -28
  29. package/dist/playwright/reporter.cjs.map +1 -1
  30. package/dist/playwright/reporter.d.cts +24 -12
  31. package/dist/playwright/reporter.d.ts +24 -12
  32. package/dist/playwright/reporter.js +62 -28
  33. package/dist/playwright/reporter.js.map +1 -1
  34. package/dist/react/index.cjs +6936 -9808
  35. package/dist/react/index.cjs.map +1 -1
  36. package/dist/react/index.d.cts +673 -146
  37. package/dist/react/index.d.ts +673 -146
  38. package/dist/react/index.js +6980 -9811
  39. package/dist/react/index.js.map +1 -1
  40. package/dist/scan/index.cjs +3281 -0
  41. package/dist/scan/index.cjs.map +1 -0
  42. package/dist/scan/index.d.cts +373 -0
  43. package/dist/scan/index.d.ts +373 -0
  44. package/dist/scan/index.js +3224 -0
  45. package/dist/scan/index.js.map +1 -0
  46. package/package.json +71 -65
  47. package/templates/claude/audit.md +37 -0
  48. package/templates/claude/rules.md +212 -0
  49. package/claude/audit-command.md +0 -46
  50. package/claude/rules.md +0 -167
  51. package/dist/api/index.cjs +0 -254
  52. package/dist/api/index.cjs.map +0 -1
  53. package/dist/api/index.d.cts +0 -236
  54. package/dist/api/index.d.ts +0 -236
  55. package/dist/api/index.js +0 -226
  56. package/dist/api/index.js.map +0 -1
  57. package/dist/core/index.cjs +0 -11045
  58. package/dist/core/index.cjs.map +0 -1
  59. package/dist/core/index.d.cts +0 -424
  60. package/dist/core/index.d.ts +0 -424
  61. package/dist/core/index.global.js +0 -66516
  62. package/dist/core/index.global.js.map +0 -1
  63. package/dist/core/index.js +0 -10995
  64. package/dist/core/index.js.map +0 -1
  65. package/dist/core/style.css +0 -1529
  66. package/dist/scripts/cli.cjs +0 -3904
  67. package/uidex.schema.json +0 -93
@@ -0,0 +1,3243 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/scan/cli.ts
27
+ var fs7 = __toESM(require("fs"), 1);
28
+ var path8 = __toESM(require("path"), 1);
29
+
30
+ // src/scan/ai/index.ts
31
+ var p = __toESM(require("@clack/prompts"), 1);
32
+
33
+ // src/scan/ai/providers/claude.ts
34
+ var fs2 = __toESM(require("fs"), 1);
35
+ var path2 = __toESM(require("path"), 1);
36
+
37
+ // src/scan/ai/templates.ts
38
+ var fs = __toESM(require("fs"), 1);
39
+ var path = __toESM(require("path"), 1);
40
+ function templatePath(rel) {
41
+ const candidates = [
42
+ path.resolve(__dirname, "../../templates", rel),
43
+ // dist/cli/cli.cjs → ../../templates
44
+ path.resolve(__dirname, "../../../templates", rel)
45
+ // src/scan/ai/foo.ts → ../../../templates
46
+ ];
47
+ for (const c of candidates) {
48
+ try {
49
+ fs.accessSync(c, fs.constants.R_OK);
50
+ return c;
51
+ } catch {
52
+ continue;
53
+ }
54
+ }
55
+ throw new Error(
56
+ `uidex: template not found: ${rel}. Looked in:
57
+ ${candidates.join("\n ")}`
58
+ );
59
+ }
60
+ function readTemplate(rel) {
61
+ return fs.readFileSync(templatePath(rel), "utf8");
62
+ }
63
+
64
+ // src/scan/ai/providers/claude.ts
65
+ var CLAUDE_FILES = [
66
+ { dest: ".claude/rules/uidex.md", template: "claude/rules.md" },
67
+ { dest: ".claude/commands/uidex/audit.md", template: "claude/audit.md" }
68
+ ];
69
+ var claudeProvider = {
70
+ id: "claude",
71
+ label: "Claude Code",
72
+ description: "Adds .claude/rules/uidex.md and the /uidex:audit slash command.",
73
+ async install({ cwd, force }) {
74
+ const changes = [];
75
+ for (const file of CLAUDE_FILES) {
76
+ const dest = path2.join(cwd, file.dest);
77
+ const exists = fs2.existsSync(dest);
78
+ if (exists && !force) {
79
+ changes.push({
80
+ path: file.dest,
81
+ action: "skipped",
82
+ reason: "exists (use --force to overwrite)"
83
+ });
84
+ continue;
85
+ }
86
+ fs2.mkdirSync(path2.dirname(dest), { recursive: true });
87
+ fs2.writeFileSync(dest, readTemplate(file.template));
88
+ changes.push({
89
+ path: file.dest,
90
+ action: exists ? "overwritten" : "created"
91
+ });
92
+ }
93
+ return { changes };
94
+ },
95
+ async uninstall({ cwd }) {
96
+ const changes = [];
97
+ for (const file of CLAUDE_FILES) {
98
+ const dest = path2.join(cwd, file.dest);
99
+ if (!fs2.existsSync(dest)) {
100
+ changes.push({ path: file.dest, action: "skipped", reason: "absent" });
101
+ continue;
102
+ }
103
+ fs2.unlinkSync(dest);
104
+ changes.push({ path: file.dest, action: "removed" });
105
+ }
106
+ cleanupEmpty(path2.join(cwd, ".claude/commands/uidex"));
107
+ cleanupEmpty(path2.join(cwd, ".claude/commands"));
108
+ cleanupEmpty(path2.join(cwd, ".claude/rules"));
109
+ return { changes };
110
+ }
111
+ };
112
+ function cleanupEmpty(dir) {
113
+ try {
114
+ const entries = fs2.readdirSync(dir);
115
+ if (entries.length === 0) fs2.rmdirSync(dir);
116
+ } catch {
117
+ }
118
+ }
119
+
120
+ // src/scan/ai/providers/index.ts
121
+ var PROVIDERS = [claudeProvider];
122
+ function getProvider(id) {
123
+ return PROVIDERS.find((p2) => p2.id === id);
124
+ }
125
+
126
+ // src/scan/ai/index.ts
127
+ async function runAiCommand(opts) {
128
+ const { cwd, argv } = opts;
129
+ const sub = argv[0];
130
+ if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
131
+ return out(0, helpText());
132
+ }
133
+ if (sub === "providers") {
134
+ return out(
135
+ 0,
136
+ PROVIDERS.map((pr) => ` ${pr.id.padEnd(10)} ${pr.label}`).join("\n") + "\n"
137
+ );
138
+ }
139
+ if (sub !== "install" && sub !== "uninstall") {
140
+ return err(1, `Unknown ai subcommand: ${sub}
141
+
142
+ ${helpText()}`);
143
+ }
144
+ const flags = parseFlags(argv.slice(1));
145
+ const provider = await selectProvider(opts, flags.provider);
146
+ if (!provider) return out(0, "Cancelled.\n");
147
+ if (sub === "install") {
148
+ const result2 = await provider.install({
149
+ cwd,
150
+ force: flags.force === true
151
+ });
152
+ return out(0, formatChanges(provider, "Installed", result2));
153
+ }
154
+ const result = await provider.uninstall({ cwd });
155
+ return out(0, formatChanges(provider, "Uninstalled", result));
156
+ }
157
+ async function selectProvider(opts, explicit) {
158
+ if (explicit) {
159
+ const found = getProvider(explicit);
160
+ if (!found) {
161
+ throw new Error(
162
+ `Unknown provider: ${explicit}. Run 'uidex ai providers' to list.`
163
+ );
164
+ }
165
+ return found;
166
+ }
167
+ if (PROVIDERS.length === 1) return PROVIDERS[0];
168
+ if (opts.nonInteractive) {
169
+ throw new Error(
170
+ "Multiple providers available; pass --provider <id> in non-interactive mode."
171
+ );
172
+ }
173
+ if (opts.prompt?.selectProvider) {
174
+ const id = await opts.prompt.selectProvider(PROVIDERS);
175
+ return id ? getProvider(id) ?? null : null;
176
+ }
177
+ const choice = await p.select({
178
+ message: "Which AI assistant?",
179
+ options: PROVIDERS.map((pr) => ({
180
+ value: pr.id,
181
+ label: pr.label,
182
+ hint: pr.description
183
+ }))
184
+ });
185
+ if (p.isCancel(choice)) return null;
186
+ return getProvider(choice) ?? null;
187
+ }
188
+ function parseFlags(args) {
189
+ const flags = {};
190
+ for (let i = 0; i < args.length; i++) {
191
+ const a = args[i];
192
+ if (a === "--force") flags.force = true;
193
+ else if (a === "--provider" || a === "-p") flags.provider = args[++i];
194
+ else if (a.startsWith("--provider=")) flags.provider = a.slice(11);
195
+ }
196
+ return flags;
197
+ }
198
+ function formatChanges(provider, verb, result) {
199
+ const lines = [`${verb} ${provider.label}:`];
200
+ for (const c of result.changes) lines.push(` ${describe(c)}`);
201
+ return lines.join("\n") + "\n";
202
+ }
203
+ function describe(c) {
204
+ switch (c.action) {
205
+ case "created":
206
+ return `+ ${c.path}`;
207
+ case "overwritten":
208
+ return `~ ${c.path}`;
209
+ case "removed":
210
+ return `- ${c.path}`;
211
+ case "skipped":
212
+ return `\xB7 ${c.path}${c.reason ? ` (${c.reason})` : ""}`;
213
+ }
214
+ }
215
+ function helpText() {
216
+ return [
217
+ "uidex ai \u2014 install AI assistant integrations",
218
+ "",
219
+ "Subcommands:",
220
+ " install Install an AI integration into the current repo",
221
+ " uninstall Remove an AI integration from the current repo",
222
+ " providers List available providers",
223
+ "",
224
+ "Flags:",
225
+ " --provider <id>, -p Skip the prompt and pick a provider by id",
226
+ " --force (install) overwrite existing files",
227
+ ""
228
+ ].join("\n");
229
+ }
230
+ function out(exitCode, stdout) {
231
+ return { exitCode, stdout, stderr: "" };
232
+ }
233
+ function err(exitCode, stderr) {
234
+ return { exitCode, stdout: "", stderr };
235
+ }
236
+
237
+ // src/scan/discover.ts
238
+ var fs3 = __toESM(require("fs"), 1);
239
+ var path3 = __toESM(require("path"), 1);
240
+
241
+ // src/scan/config.ts
242
+ var DEFAULT_TYPE_MODE = "strict";
243
+ var ConfigError = class extends Error {
244
+ constructor(message) {
245
+ super(message);
246
+ this.name = "ConfigError";
247
+ }
248
+ };
249
+ var LEGACY_KEYS = /* @__PURE__ */ new Set([
250
+ "scanner",
251
+ "defaults",
252
+ "colors",
253
+ "components",
254
+ "triggerElement",
255
+ "devtools"
256
+ ]);
257
+ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
258
+ "$schema",
259
+ "sources",
260
+ "exclude",
261
+ "output",
262
+ "flows",
263
+ "typeMode",
264
+ "audit",
265
+ "conventions"
266
+ ]);
267
+ var ALLOWED_TYPE_MODES = /* @__PURE__ */ new Set(["strict", "loose"]);
268
+ var ALLOWED_SOURCE_KEYS = /* @__PURE__ */ new Set(["rootDir", "include", "exclude", "prefix"]);
269
+ var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
270
+ "primitives",
271
+ "features",
272
+ "pages",
273
+ "flows",
274
+ "regions"
275
+ ]);
276
+ var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set(["scopeLeak", "coverage", "acceptance"]);
277
+ function fail(msg) {
278
+ throw new ConfigError(`Invalid .uidex.json: ${msg}`);
279
+ }
280
+ function assertObject(value, path9) {
281
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
282
+ fail(`${path9} must be an object`);
283
+ }
284
+ }
285
+ function assertStringArray(value, path9) {
286
+ if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
287
+ fail(`${path9} must be a string[]`);
288
+ }
289
+ }
290
+ function validateConfig(raw) {
291
+ assertObject(raw, "root");
292
+ for (const key of Object.keys(raw)) {
293
+ if (LEGACY_KEYS.has(key)) {
294
+ fail(
295
+ `legacy nested key "${key}" is not supported; v2 uses a flat schema (sources[], output, flows, audit, conventions)`
296
+ );
297
+ }
298
+ if (!ALLOWED_TOP_LEVEL_KEYS.has(key)) {
299
+ fail(`unknown top-level key "${key}"`);
300
+ }
301
+ }
302
+ if (!("sources" in raw)) {
303
+ fail(
304
+ `required key "sources" is missing (must be an array with at least one entry)`
305
+ );
306
+ }
307
+ const sources = raw.sources;
308
+ if (!Array.isArray(sources) || sources.length === 0) {
309
+ fail(`"sources" must be a non-empty array`);
310
+ }
311
+ const validatedSources = sources.map((src, i) => {
312
+ assertObject(src, `sources[${i}]`);
313
+ for (const key of Object.keys(src)) {
314
+ if (!ALLOWED_SOURCE_KEYS.has(key)) {
315
+ fail(`unknown key "${key}" in sources[${i}]`);
316
+ }
317
+ }
318
+ if (typeof src.rootDir !== "string" || src.rootDir.length === 0) {
319
+ fail(`sources[${i}].rootDir must be a non-empty string`);
320
+ }
321
+ if (src.include !== void 0)
322
+ assertStringArray(src.include, `sources[${i}].include`);
323
+ if (src.exclude !== void 0)
324
+ assertStringArray(src.exclude, `sources[${i}].exclude`);
325
+ if (src.prefix !== void 0 && typeof src.prefix !== "string") {
326
+ fail(`sources[${i}].prefix must be a string`);
327
+ }
328
+ return src;
329
+ });
330
+ if (typeof raw.output !== "string" || raw.output.length === 0) {
331
+ fail(`"output" must be a non-empty string`);
332
+ }
333
+ if (raw.exclude !== void 0) assertStringArray(raw.exclude, `exclude`);
334
+ if (raw.flows !== void 0) assertStringArray(raw.flows, `flows`);
335
+ if (raw.typeMode !== void 0) {
336
+ if (typeof raw.typeMode !== "string" || !ALLOWED_TYPE_MODES.has(raw.typeMode)) {
337
+ fail(`"typeMode" must be "strict" or "loose"`);
338
+ }
339
+ }
340
+ if (raw.audit !== void 0) {
341
+ assertObject(raw.audit, "audit");
342
+ for (const key of Object.keys(raw.audit)) {
343
+ if (!ALLOWED_AUDIT_KEYS.has(key)) fail(`unknown key "${key}" in audit`);
344
+ const v = raw.audit[key];
345
+ if (typeof v !== "boolean") fail(`audit.${key} must be a boolean`);
346
+ }
347
+ }
348
+ if (raw.conventions !== void 0) {
349
+ assertObject(raw.conventions, "conventions");
350
+ const c = raw.conventions;
351
+ for (const key of Object.keys(c)) {
352
+ if (!ALLOWED_CONVENTIONS_KEYS.has(key)) {
353
+ fail(`unknown key "${key}" in conventions`);
354
+ }
355
+ }
356
+ if (c.primitives !== void 0 && c.primitives !== false) {
357
+ assertStringArray(c.primitives, "conventions.primitives");
358
+ }
359
+ if (c.features !== void 0 && c.features !== false && typeof c.features !== "string") {
360
+ fail(`conventions.features must be a string or false`);
361
+ }
362
+ if (c.pages !== void 0 && c.pages !== false && c.pages !== "auto") {
363
+ fail(`conventions.pages must be "auto" or false`);
364
+ }
365
+ if (c.flows !== void 0 && c.flows !== false) {
366
+ assertStringArray(c.flows, "conventions.flows");
367
+ }
368
+ if (c.regions !== void 0 && c.regions !== false && c.regions !== "landmarks") {
369
+ fail(`conventions.regions must be "landmarks" or false`);
370
+ }
371
+ }
372
+ return {
373
+ $schema: typeof raw.$schema === "string" ? raw.$schema : void 0,
374
+ sources: validatedSources,
375
+ exclude: raw.exclude,
376
+ output: raw.output,
377
+ flows: raw.flows,
378
+ typeMode: raw.typeMode ?? DEFAULT_TYPE_MODE,
379
+ audit: raw.audit,
380
+ conventions: raw.conventions
381
+ };
382
+ }
383
+ function parseConfig(json) {
384
+ let raw;
385
+ try {
386
+ raw = JSON.parse(json);
387
+ } catch (e) {
388
+ const msg = e instanceof Error ? e.message : String(e);
389
+ throw new ConfigError(`Invalid .uidex.json: JSON parse error: ${msg}`);
390
+ }
391
+ return validateConfig(raw);
392
+ }
393
+ var DEFAULT_CONVENTIONS = {
394
+ primitives: ["src/ui/**", "src/components/ui/**"],
395
+ features: "src/features/*",
396
+ pages: "auto",
397
+ flows: ["e2e/**/*.spec.ts"],
398
+ regions: "landmarks"
399
+ };
400
+
401
+ // src/scan/discover.ts
402
+ var CONFIG_FILENAME = ".uidex.json";
403
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
404
+ "node_modules",
405
+ ".git",
406
+ "dist",
407
+ "build",
408
+ ".next",
409
+ ".turbo",
410
+ ".cache",
411
+ "coverage"
412
+ ]);
413
+ var MAX_DEPTH = 4;
414
+ function tryReadConfig(configPath) {
415
+ let source;
416
+ try {
417
+ source = fs3.readFileSync(configPath, "utf8");
418
+ } catch {
419
+ return null;
420
+ }
421
+ return {
422
+ configPath,
423
+ configDir: path3.dirname(configPath),
424
+ config: parseConfig(source)
425
+ };
426
+ }
427
+ function discover(options = {}) {
428
+ const cwd = options.cwd ?? process.cwd();
429
+ const maxDepth = options.maxDepth ?? MAX_DEPTH;
430
+ const rootMatch = tryReadConfig(path3.join(cwd, CONFIG_FILENAME));
431
+ if (rootMatch) return [rootMatch];
432
+ const results = [];
433
+ const queue = [[cwd, 0]];
434
+ while (queue.length > 0) {
435
+ const [dir, depth] = queue.shift();
436
+ let entries;
437
+ try {
438
+ entries = fs3.readdirSync(dir, { withFileTypes: true });
439
+ } catch {
440
+ continue;
441
+ }
442
+ for (const entry of entries) {
443
+ if (!entry.isDirectory()) continue;
444
+ if (SKIP_DIRS.has(entry.name)) continue;
445
+ if (entry.name.startsWith(".")) continue;
446
+ const childDir = path3.join(dir, entry.name);
447
+ const candidate = tryReadConfig(path3.join(childDir, CONFIG_FILENAME));
448
+ if (candidate) {
449
+ results.push(candidate);
450
+ continue;
451
+ }
452
+ if (depth + 1 < maxDepth) {
453
+ queue.push([childDir, depth + 1]);
454
+ }
455
+ }
456
+ }
457
+ return results.sort((a, b) => a.configPath.localeCompare(b.configPath));
458
+ }
459
+
460
+ // src/scan/pipeline.ts
461
+ var fs5 = __toESM(require("fs"), 1);
462
+ var path6 = __toESM(require("path"), 1);
463
+
464
+ // src/entities/types.ts
465
+ var ENTITY_KINDS = [
466
+ "route",
467
+ "page",
468
+ "feature",
469
+ "widget",
470
+ "region",
471
+ "element",
472
+ "primitive",
473
+ "flow"
474
+ ];
475
+ function isMetaKind(kind) {
476
+ return kind !== "route" && kind !== "flow";
477
+ }
478
+ function isMetaEntity(entity) {
479
+ return isMetaKind(entity.kind);
480
+ }
481
+ function entityKey(entity) {
482
+ return entity.kind === "route" ? entity.path : entity.id;
483
+ }
484
+ var UnknownEntityKindError = class extends Error {
485
+ kind;
486
+ constructor(kind) {
487
+ super(`Unknown entity kind: ${kind}`);
488
+ this.name = "UnknownEntityKindError";
489
+ this.kind = kind;
490
+ }
491
+ };
492
+ var KIND_SET = new Set(ENTITY_KINDS);
493
+ function assertEntityKind(kind) {
494
+ if (!KIND_SET.has(kind)) throw new UnknownEntityKindError(kind);
495
+ }
496
+
497
+ // src/entities/registry.ts
498
+ function emptyStore() {
499
+ return {
500
+ route: /* @__PURE__ */ new Map(),
501
+ page: /* @__PURE__ */ new Map(),
502
+ feature: /* @__PURE__ */ new Map(),
503
+ widget: /* @__PURE__ */ new Map(),
504
+ region: /* @__PURE__ */ new Map(),
505
+ element: /* @__PURE__ */ new Map(),
506
+ primitive: /* @__PURE__ */ new Map(),
507
+ flow: /* @__PURE__ */ new Map()
508
+ };
509
+ }
510
+ function computeFlowIds(flows, targetId) {
511
+ const ids = [];
512
+ for (const flow of flows) {
513
+ if (flow.touches.includes(targetId)) ids.push(flow.id);
514
+ }
515
+ return ids;
516
+ }
517
+ function freezeEntity(entity, flows) {
518
+ if (!isMetaKind(entity.kind)) return entity;
519
+ const withMeta = entity;
520
+ if (withMeta.meta === void 0) return entity;
521
+ const computedFlows = Object.freeze(computeFlowIds(flows, withMeta.id));
522
+ const mergedMeta = { ...withMeta.meta, flows: computedFlows };
523
+ return { ...entity, meta: Object.freeze(mergedMeta) };
524
+ }
525
+ function createRegistry() {
526
+ const store = emptyStore();
527
+ let flowsCache = null;
528
+ const getFlows = () => {
529
+ if (flowsCache === null) flowsCache = Array.from(store.flow.values());
530
+ return flowsCache;
531
+ };
532
+ const add = (entity) => {
533
+ assertEntityKind(entity.kind);
534
+ const key = entityKey(entity);
535
+ store[entity.kind].set(key, entity);
536
+ flowsCache = null;
537
+ };
538
+ const get = (kind, id) => {
539
+ assertEntityKind(kind);
540
+ const raw = store[kind].get(id);
541
+ if (raw === void 0) return void 0;
542
+ return freezeEntity(raw, getFlows());
543
+ };
544
+ const list = (kind) => {
545
+ assertEntityKind(kind);
546
+ const flows = getFlows();
547
+ return Array.from(
548
+ store[kind].values(),
549
+ (e) => freezeEntity(e, flows)
550
+ );
551
+ };
552
+ const allEntities = function* () {
553
+ for (const kind of Object.keys(store)) {
554
+ for (const entity of store[kind].values()) {
555
+ yield entity;
556
+ }
557
+ }
558
+ };
559
+ const query = (predicate) => {
560
+ const flows = getFlows();
561
+ const result = [];
562
+ for (const entity of allEntities()) {
563
+ if (predicate(entity)) result.push(freezeEntity(entity, flows));
564
+ }
565
+ return result;
566
+ };
567
+ const byScope = (scope) => query(
568
+ (entity) => "scopes" in entity && Array.isArray(entity.scopes) && entity.scopes.includes(scope)
569
+ );
570
+ const touchedBy = (flowId) => {
571
+ const flow = store.flow.get(flowId);
572
+ if (flow === void 0) return [];
573
+ const ids = new Set(flow.touches);
574
+ return query((entity) => {
575
+ if (!isMetaEntity(entity)) return false;
576
+ return ids.has(entity.id);
577
+ });
578
+ };
579
+ return { add, get, list, query, byScope, touchedBy };
580
+ }
581
+
582
+ // src/scan/audit.ts
583
+ var MARKER_FILENAMES = ["UIDEX_PAGE.md", "UIDEX_FEATURE.md"];
584
+ function audit(opts) {
585
+ const diagnostics = [];
586
+ const { registry, extracted, files, config } = opts;
587
+ const check = opts.check ?? false;
588
+ const lint = opts.lint ?? false;
589
+ const acceptanceEnabled = config.audit?.acceptance ?? true;
590
+ const scopeLeakEnabled = config.audit?.scopeLeak ?? true;
591
+ const coverageEnabled = config.audit?.coverage ?? true;
592
+ if (opts.resolveDiagnostics) diagnostics.push(...opts.resolveDiagnostics);
593
+ if (check) {
594
+ for (const f of files) {
595
+ const base = f.displayPath.split("/").pop() ?? "";
596
+ if (MARKER_FILENAMES.includes(base)) {
597
+ diagnostics.push({
598
+ code: "marker-md-ignored",
599
+ severity: "warning",
600
+ message: `Marker file "${base}" is ignored in v2; migrate to \`export const uidex\``,
601
+ file: f.displayPath
602
+ });
603
+ }
604
+ }
605
+ }
606
+ if (check && opts.generated !== void 0) {
607
+ const outRel = opts.outputPath ?? config.output;
608
+ const fresh = normalizeLineEndings(opts.generated);
609
+ if (opts.existingOnDisk === null || opts.existingOnDisk === void 0) {
610
+ diagnostics.push({
611
+ code: "gen-missing",
612
+ severity: "error",
613
+ message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
614
+ file: outRel,
615
+ hint: "Run `uidex scan` (without --check) to regenerate"
616
+ });
617
+ } else {
618
+ const existing = normalizeLineEndings(opts.existingOnDisk);
619
+ if (existing !== fresh) {
620
+ const changed = diffEntities(existing, opts.generated, registry);
621
+ const summary2 = formatChangedSummary(changed);
622
+ diagnostics.push({
623
+ code: "gen-stale",
624
+ severity: "error",
625
+ message: `Generated file "${outRel}" is stale${summary2 ? `; ${summary2}` : ""}`,
626
+ file: outRel,
627
+ hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
628
+ });
629
+ }
630
+ }
631
+ }
632
+ if (lint) {
633
+ for (const ef of extracted) {
634
+ for (const a of ef.annotations) {
635
+ const migration = legacyJsdocMigration(a);
636
+ if (!migration) continue;
637
+ diagnostics.push({
638
+ code: "legacy-jsdoc",
639
+ severity: "warning",
640
+ message: migration.message,
641
+ file: a.file,
642
+ line: a.line,
643
+ hint: migration.hint
644
+ });
645
+ }
646
+ }
647
+ }
648
+ if (lint && acceptanceEnabled) {
649
+ for (const kind of ["widget", "feature", "page"]) {
650
+ for (const e of registry.list(kind)) {
651
+ const criteria = e.meta?.acceptance ?? [];
652
+ if (criteria.length === 0) {
653
+ if (kind === "widget") {
654
+ diagnostics.push({
655
+ code: "widget-missing-acceptance",
656
+ severity: "warning",
657
+ message: `Widget "${e.id}" has no acceptance metadata; add \`export const uidex = { widget: "${e.id}", acceptance: [...] }\` at the widget's definition site`,
658
+ file: e.loc?.file,
659
+ line: e.loc?.line,
660
+ entity: { kind, id: e.id },
661
+ hint: `Adding acceptance criteria allows \`uidex scaffold widget ${e.id}\` to generate a Playwright spec`
662
+ });
663
+ }
664
+ continue;
665
+ }
666
+ const flowIds = e.meta?.flows ?? [];
667
+ if (flowIds.length === 0) {
668
+ const hint = kind === "widget" ? `uidex scaffold widget ${e.id}` : `Add a Playwright flow under e2e/** that touches ${kind} "${e.id}" (tag the describe with @uidex:flow)`;
669
+ for (const c of criteria) {
670
+ diagnostics.push({
671
+ code: "acceptance-uncovered",
672
+ severity: "warning",
673
+ message: `${kind} "${e.id}" has acceptance criterion not covered by any flow: "${c}"`,
674
+ file: e.loc?.file,
675
+ line: e.loc?.line,
676
+ entity: { kind, id: e.id },
677
+ hint
678
+ });
679
+ }
680
+ }
681
+ }
682
+ }
683
+ }
684
+ if (lint) {
685
+ for (const f of files) {
686
+ const lines = f.content.split("\n");
687
+ const candidateRe = /<(button|a|input|select|textarea)(?=[\s/>])([^>]*)>/g;
688
+ let m;
689
+ while ((m = candidateRe.exec(f.content)) !== null) {
690
+ const attrs = m[2];
691
+ if (attrs && attrs.includes("data-uidex")) continue;
692
+ const idx = m.index;
693
+ let line = 1;
694
+ for (let i = 0; i < idx; i++) if (f.content[i] === "\n") line++;
695
+ void lines;
696
+ diagnostics.push({
697
+ code: "missing-element-annotation",
698
+ severity: "info",
699
+ message: `Interactive <${m[1].toLowerCase()}> without data-uidex annotation`,
700
+ file: f.displayPath,
701
+ line
702
+ });
703
+ }
704
+ }
705
+ }
706
+ if (lint && scopeLeakEnabled) {
707
+ const primitives = registry.list("primitive");
708
+ const byName = /* @__PURE__ */ new Map();
709
+ for (const p2 of primitives) byName.set(p2.id, p2);
710
+ for (const f of files) {
711
+ const importRe = /import\s+(?:[^'"]+)\s+from\s+['"]([^'"]+)['"]/g;
712
+ let m;
713
+ while ((m = importRe.exec(f.content)) !== null) {
714
+ const spec = m[1];
715
+ const baseName2 = spec.split("/").pop() ?? "";
716
+ const primitive = byName.get(
717
+ baseName2.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
718
+ );
719
+ if (!primitive) continue;
720
+ const scope = primitive.scopes?.[0] ?? "global";
721
+ if (scope === "global") continue;
722
+ const [kind, id] = scope.split(":");
723
+ const importerSegments = f.displayPath.split("/");
724
+ if (!importerSegments.includes(id) || !importerSegments.includes(kind + "s")) {
725
+ diagnostics.push({
726
+ code: "scope-leak",
727
+ severity: "warning",
728
+ message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${f.displayPath}`,
729
+ file: f.displayPath
730
+ });
731
+ }
732
+ }
733
+ }
734
+ }
735
+ if (lint && coverageEnabled) {
736
+ for (const flow of registry.list("flow")) {
737
+ for (const touchedId of flow.touches) {
738
+ const found = registry.get("element", touchedId) ?? registry.get("widget", touchedId) ?? registry.get("region", touchedId);
739
+ if (!found) {
740
+ diagnostics.push({
741
+ code: "unknown-reference",
742
+ severity: "warning",
743
+ message: `Flow "${flow.id}" references unknown entity "${touchedId}"`,
744
+ file: flow.loc.file,
745
+ line: flow.loc.line
746
+ });
747
+ }
748
+ }
749
+ }
750
+ }
751
+ const summary = {
752
+ errors: diagnostics.filter((d) => d.severity === "error").length,
753
+ warnings: diagnostics.filter((d) => d.severity === "warning").length
754
+ };
755
+ return { diagnostics, summary };
756
+ }
757
+ function legacyJsdocMigration(a) {
758
+ const quote = (s) => JSON.stringify(s);
759
+ const arr = (xs) => xs && xs.length > 0 ? `[${xs.map(quote).join(", ")}]` : "";
760
+ const entityHint = (kind) => {
761
+ const uidexKind = kind.charAt(0).toUpperCase() + kind.slice(1);
762
+ const parts = [`${kind}: ${quote(a.id)}`];
763
+ if (a.acceptance?.length) parts.push(`acceptance: ${arr(a.acceptance)}`);
764
+ return {
765
+ message: `Legacy JSDoc tag \`@uidex ${kind} ${a.id}\` is no longer recognised; migrate to \`export const uidex\``,
766
+ hint: `Replace with: export const uidex = { ${parts.join(", ")} } as const satisfies Uidex.${uidexKind}`
767
+ };
768
+ };
769
+ switch (a.kind) {
770
+ case "page-doc":
771
+ return entityHint("page");
772
+ case "feature-doc":
773
+ return entityHint("feature");
774
+ case "widget-doc":
775
+ return entityHint("widget");
776
+ case "not-flow":
777
+ return {
778
+ message: `Legacy JSDoc tag \`@uidex:not-flow\` is no longer recognised; migrate to \`export const uidex\``,
779
+ hint: `Replace with: export const uidex = { notFlow: true } as const satisfies Uidex.NotFlow`
780
+ };
781
+ case "orphan-acceptance":
782
+ return {
783
+ message: `Legacy JSDoc tag \`@acceptance\` is no longer recognised; migrate to the \`acceptance\` field on \`export const uidex\``,
784
+ hint: `Replace with: export const uidex = { /* kind */, acceptance: ${arr(a.acceptance)} } as const`
785
+ };
786
+ default:
787
+ return null;
788
+ }
789
+ }
790
+ function normalizeLineEndings(s) {
791
+ return s.replace(/\r\n/g, "\n");
792
+ }
793
+ function formatChangedSummary(change) {
794
+ const parts = [];
795
+ const fmt = (kind, names) => {
796
+ if (names.length === 0) return;
797
+ const preview = names.slice(0, 3).join(", ");
798
+ const suffix = names.length > 3 ? ` (+${names.length - 3} more)` : "";
799
+ parts.push(`${kind}: ${preview}${suffix}`);
800
+ };
801
+ fmt("added", change.added);
802
+ fmt("removed", change.removed);
803
+ fmt("modified", change.modified);
804
+ return parts.join("; ");
805
+ }
806
+ function diffEntities(existing, generated, registry) {
807
+ const oldEntities = extractEntitiesArray(existing);
808
+ const newEntities = extractEntitiesArray(generated) ?? freshEntities(registry);
809
+ const added = [];
810
+ const removed = [];
811
+ const modified = [];
812
+ if (!oldEntities) {
813
+ for (const e of newEntities) added.push(labelFor(e));
814
+ return { added, removed, modified };
815
+ }
816
+ const oldByKey = /* @__PURE__ */ new Map();
817
+ for (const e of oldEntities) oldByKey.set(entityKey(e) + "@" + e.kind, e);
818
+ const newByKey = /* @__PURE__ */ new Map();
819
+ for (const e of newEntities) newByKey.set(entityKey(e) + "@" + e.kind, e);
820
+ for (const [k, e] of newByKey) {
821
+ const prev = oldByKey.get(k);
822
+ if (!prev) {
823
+ added.push(labelFor(e));
824
+ } else if (stableStringify(prev) !== stableStringify(e)) {
825
+ modified.push(labelFor(e));
826
+ }
827
+ }
828
+ for (const [k, e] of oldByKey) {
829
+ if (!newByKey.has(k)) removed.push(labelFor(e));
830
+ }
831
+ return { added, removed, modified };
832
+ }
833
+ function labelFor(e) {
834
+ return `${e.kind} "${entityKey(e)}"`;
835
+ }
836
+ function freshEntities(registry) {
837
+ const kinds = [
838
+ "route",
839
+ "page",
840
+ "feature",
841
+ "widget",
842
+ "region",
843
+ "element",
844
+ "primitive",
845
+ "flow"
846
+ ];
847
+ const out2 = [];
848
+ for (const k of kinds) out2.push(...registry.list(k));
849
+ return out2;
850
+ }
851
+ function extractEntitiesArray(source) {
852
+ const marker = "export const entities: ReadonlyArray<Entity> = ";
853
+ const idx = source.indexOf(marker);
854
+ if (idx === -1) return null;
855
+ const start = source.indexOf("[", idx);
856
+ if (start === -1) return null;
857
+ let depth = 0;
858
+ let inStr = null;
859
+ let escaped = false;
860
+ for (let i = start; i < source.length; i++) {
861
+ const c = source[i];
862
+ if (inStr) {
863
+ if (escaped) {
864
+ escaped = false;
865
+ } else if (c === "\\") {
866
+ escaped = true;
867
+ } else if (c === inStr) {
868
+ inStr = null;
869
+ }
870
+ continue;
871
+ }
872
+ if (c === '"' || c === "'") {
873
+ inStr = c;
874
+ continue;
875
+ }
876
+ if (c === "[") depth++;
877
+ else if (c === "]") {
878
+ depth--;
879
+ if (depth === 0) {
880
+ const json = source.slice(start, i + 1);
881
+ try {
882
+ return JSON.parse(json);
883
+ } catch {
884
+ return null;
885
+ }
886
+ }
887
+ }
888
+ }
889
+ return null;
890
+ }
891
+ function stableStringify(value) {
892
+ return JSON.stringify(value, stableReplacer);
893
+ }
894
+ function stableReplacer(_key, value) {
895
+ if (value && typeof value === "object" && !Array.isArray(value)) {
896
+ const sorted = {};
897
+ for (const k of Object.keys(value).sort()) {
898
+ sorted[k] = value[k];
899
+ }
900
+ return sorted;
901
+ }
902
+ return value;
903
+ }
904
+
905
+ // src/scan/emit.ts
906
+ function sortById(arr) {
907
+ return [...arr].sort((a, b) => a.id.localeCompare(b.id));
908
+ }
909
+ function jsonStable(value, indent = 2) {
910
+ return JSON.stringify(value, replacerSorted, indent);
911
+ }
912
+ function replacerSorted(_key, value) {
913
+ if (value && typeof value === "object" && !Array.isArray(value)) {
914
+ const sorted = {};
915
+ for (const k of Object.keys(value).sort()) {
916
+ sorted[k] = value[k];
917
+ }
918
+ return sorted;
919
+ }
920
+ return value;
921
+ }
922
+ function emitIdUnion(name, ids, typeMode) {
923
+ if (typeMode === "loose") return `export type ${name} = string
924
+ `;
925
+ if (ids.length === 0) return `export type ${name} = never
926
+ `;
927
+ const sorted = [...ids].sort();
928
+ const body = sorted.map((id) => ` | ${JSON.stringify(id)}`).join("\n");
929
+ return `export type ${name} =
930
+ ${body}
931
+ `;
932
+ }
933
+ function emit(opts) {
934
+ const {
935
+ registry,
936
+ gitContext,
937
+ uidexImport = "uidex",
938
+ typeMode = "strict"
939
+ } = opts;
940
+ const routes = [...registry.list("route")].sort(
941
+ (a, b) => a.path.localeCompare(b.path)
942
+ );
943
+ const pages = sortById(registry.list("page"));
944
+ const features = sortById(registry.list("feature"));
945
+ const widgets = sortById(registry.list("widget"));
946
+ const regions = sortById(registry.list("region"));
947
+ const elements = sortById(registry.list("element"));
948
+ const primitives = sortById(registry.list("primitive"));
949
+ const flows = sortById(registry.list("flow"));
950
+ const lines = [];
951
+ lines.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
952
+ lines.push("/* eslint-disable */");
953
+ lines.push(`import { createUidex } from ${JSON.stringify(uidexImport)}`);
954
+ lines.push(
955
+ `import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`
956
+ );
957
+ lines.push("");
958
+ lines.push("// ---- id unions ----");
959
+ lines.push(
960
+ emitIdUnion(
961
+ "PageId",
962
+ pages.map((e) => e.id),
963
+ typeMode
964
+ )
965
+ );
966
+ lines.push(
967
+ emitIdUnion(
968
+ "FeatureId",
969
+ features.map((e) => e.id),
970
+ typeMode
971
+ )
972
+ );
973
+ lines.push(
974
+ emitIdUnion(
975
+ "WidgetId",
976
+ widgets.map((e) => e.id),
977
+ typeMode
978
+ )
979
+ );
980
+ lines.push(
981
+ emitIdUnion(
982
+ "RegionId",
983
+ regions.map((e) => e.id),
984
+ typeMode
985
+ )
986
+ );
987
+ lines.push(
988
+ emitIdUnion(
989
+ "ElementId",
990
+ elements.map((e) => e.id),
991
+ typeMode
992
+ )
993
+ );
994
+ lines.push(
995
+ emitIdUnion(
996
+ "PrimitiveId",
997
+ primitives.map((e) => e.id),
998
+ typeMode
999
+ )
1000
+ );
1001
+ lines.push(
1002
+ emitIdUnion(
1003
+ "FlowId",
1004
+ flows.map((e) => e.id),
1005
+ typeMode
1006
+ )
1007
+ );
1008
+ lines.push(
1009
+ emitIdUnion(
1010
+ "RouteId",
1011
+ routes.map((e) => e.path),
1012
+ typeMode
1013
+ )
1014
+ );
1015
+ lines.push("");
1016
+ lines.push("// ---- authoring-surface shape types ----");
1017
+ lines.push("export namespace Uidex {");
1018
+ lines.push(" export interface Page {");
1019
+ lines.push(" page: PageId | false");
1020
+ lines.push(" name?: string");
1021
+ lines.push(" features?: readonly FeatureId[]");
1022
+ lines.push(" widgets?: readonly WidgetId[]");
1023
+ lines.push(" acceptance?: readonly string[]");
1024
+ lines.push(" description?: string");
1025
+ lines.push(" }");
1026
+ lines.push(" export interface Feature {");
1027
+ lines.push(" feature: FeatureId | false");
1028
+ lines.push(" name?: string");
1029
+ lines.push(" acceptance?: readonly string[]");
1030
+ lines.push(" description?: string");
1031
+ lines.push(" }");
1032
+ lines.push(" export interface Primitive {");
1033
+ lines.push(" primitive: PrimitiveId");
1034
+ lines.push(" name?: string");
1035
+ lines.push(" description?: string");
1036
+ lines.push(" }");
1037
+ lines.push(" export interface Widget {");
1038
+ lines.push(" widget: WidgetId");
1039
+ lines.push(" name?: string");
1040
+ lines.push(" acceptance?: readonly string[]");
1041
+ lines.push(" description?: string");
1042
+ lines.push(" }");
1043
+ lines.push(" export interface Flow {");
1044
+ lines.push(" flow: FlowId");
1045
+ lines.push(" name?: string");
1046
+ lines.push(" description?: string");
1047
+ lines.push(" }");
1048
+ lines.push(" export interface NotFlow {");
1049
+ lines.push(" notFlow: true");
1050
+ lines.push(" }");
1051
+ lines.push("}");
1052
+ lines.push("");
1053
+ lines.push("// ---- entities ----");
1054
+ const allEntities = [
1055
+ ...routes,
1056
+ ...pages,
1057
+ ...features,
1058
+ ...widgets,
1059
+ ...regions,
1060
+ ...elements,
1061
+ ...primitives,
1062
+ ...flows
1063
+ ];
1064
+ lines.push(
1065
+ `export const entities: ReadonlyArray<Entity> = ${jsonStable(allEntities)}`
1066
+ );
1067
+ lines.push("");
1068
+ lines.push("// ---- git context ----");
1069
+ const gc = gitContext ?? { branch: null, commit: null, pr: null };
1070
+ lines.push(`export const gitContext = ${jsonStable(gc)} as const`);
1071
+ lines.push("");
1072
+ lines.push("// ---- registry factory ----");
1073
+ lines.push("export function loadRegistry(target: Registry): Registry {");
1074
+ lines.push(" for (const entity of entities) target.add(entity)");
1075
+ lines.push(" return target");
1076
+ lines.push("}");
1077
+ lines.push("");
1078
+ lines.push("// ---- preconfigured uidex instance ----");
1079
+ lines.push("export const uidex = createUidex()");
1080
+ lines.push("for (const entity of entities) uidex.registry.add(entity)");
1081
+ lines.push("");
1082
+ return lines.join("\n");
1083
+ }
1084
+
1085
+ // src/scan/extract-uidex-export.ts
1086
+ var KIND_DISCRIMINATORS = [
1087
+ "page",
1088
+ "feature",
1089
+ "primitive",
1090
+ "widget",
1091
+ "flow",
1092
+ "notFlow"
1093
+ ];
1094
+ var ALLOWED_FIELDS = {
1095
+ page: /* @__PURE__ */ new Set([
1096
+ "page",
1097
+ "name",
1098
+ "features",
1099
+ "widgets",
1100
+ "acceptance",
1101
+ "description"
1102
+ ]),
1103
+ feature: /* @__PURE__ */ new Set(["feature", "name", "acceptance", "description"]),
1104
+ primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
1105
+ widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "description"]),
1106
+ flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
1107
+ };
1108
+ var FALSEABLE = /* @__PURE__ */ new Set([
1109
+ "page",
1110
+ "feature",
1111
+ "primitive"
1112
+ ]);
1113
+ var ExtractError = class extends Error {
1114
+ code;
1115
+ hint;
1116
+ pos;
1117
+ constructor(code, message, pos, hint) {
1118
+ super(message);
1119
+ this.code = code;
1120
+ this.pos = pos;
1121
+ this.hint = hint;
1122
+ }
1123
+ };
1124
+ function extractUidexExports(file) {
1125
+ const exports2 = [];
1126
+ const diagnostics = [];
1127
+ const { content, displayPath } = file;
1128
+ for (const header of findExportHeaders(content)) {
1129
+ try {
1130
+ const value = parseExpression(content, header.exprStart);
1131
+ const metadata = buildMetadata(
1132
+ value,
1133
+ displayPath,
1134
+ header.headerPos,
1135
+ diagnostics
1136
+ );
1137
+ exports2.push(metadata);
1138
+ } catch (e) {
1139
+ if (e instanceof ExtractError) {
1140
+ diagnostics.push({
1141
+ code: e.code,
1142
+ severity: "error",
1143
+ message: e.message,
1144
+ file: displayPath,
1145
+ line: e.pos.line,
1146
+ hint: e.hint
1147
+ });
1148
+ } else {
1149
+ throw e;
1150
+ }
1151
+ }
1152
+ }
1153
+ return { exports: exports2, diagnostics };
1154
+ }
1155
+ var HEADER_RE = /(?:^|\n)[\t ]*export\s+const\s+uidex\b(?:\s*:\s*[^=\n]+?)?\s*=\s*/g;
1156
+ function findExportHeaders(content) {
1157
+ const out2 = [];
1158
+ HEADER_RE.lastIndex = 0;
1159
+ let m;
1160
+ while ((m = HEADER_RE.exec(content)) !== null) {
1161
+ const leadingNewline = m[0].startsWith("\n") ? 1 : 0;
1162
+ const headerOffset = m.index + leadingNewline;
1163
+ const exprStart = m.index + m[0].length;
1164
+ if (isInsideCommentOrString(content, headerOffset)) continue;
1165
+ out2.push({
1166
+ headerPos: posAt(content, headerOffset),
1167
+ exprStart
1168
+ });
1169
+ }
1170
+ return out2;
1171
+ }
1172
+ function isInsideCommentOrString(content, target) {
1173
+ let i = 0;
1174
+ let inLineComment = false;
1175
+ let inBlockComment = false;
1176
+ let stringDelim = null;
1177
+ let inTemplate = false;
1178
+ let templateDepth = 0;
1179
+ while (i < target) {
1180
+ const c = content[i];
1181
+ const n = content[i + 1];
1182
+ if (inLineComment) {
1183
+ if (c === "\n") inLineComment = false;
1184
+ i++;
1185
+ continue;
1186
+ }
1187
+ if (inBlockComment) {
1188
+ if (c === "*" && n === "/") {
1189
+ inBlockComment = false;
1190
+ i += 2;
1191
+ continue;
1192
+ }
1193
+ i++;
1194
+ continue;
1195
+ }
1196
+ if (stringDelim !== null) {
1197
+ if (c === "\\") {
1198
+ i += 2;
1199
+ continue;
1200
+ }
1201
+ if (c === stringDelim) stringDelim = null;
1202
+ i++;
1203
+ continue;
1204
+ }
1205
+ if (inTemplate) {
1206
+ if (c === "\\") {
1207
+ i += 2;
1208
+ continue;
1209
+ }
1210
+ if (c === "$" && n === "{") {
1211
+ templateDepth++;
1212
+ i += 2;
1213
+ continue;
1214
+ }
1215
+ if (c === "`" && templateDepth === 0) {
1216
+ inTemplate = false;
1217
+ i++;
1218
+ continue;
1219
+ }
1220
+ if (templateDepth > 0 && c === "}") {
1221
+ templateDepth--;
1222
+ i++;
1223
+ continue;
1224
+ }
1225
+ i++;
1226
+ continue;
1227
+ }
1228
+ if (c === "/" && n === "/") {
1229
+ inLineComment = true;
1230
+ i += 2;
1231
+ continue;
1232
+ }
1233
+ if (c === "/" && n === "*") {
1234
+ inBlockComment = true;
1235
+ i += 2;
1236
+ continue;
1237
+ }
1238
+ if (c === '"' || c === "'") {
1239
+ stringDelim = c;
1240
+ i++;
1241
+ continue;
1242
+ }
1243
+ if (c === "`") {
1244
+ inTemplate = true;
1245
+ i++;
1246
+ continue;
1247
+ }
1248
+ i++;
1249
+ }
1250
+ return inLineComment || inBlockComment || stringDelim !== null || inTemplate;
1251
+ }
1252
+ var Tokenizer = class {
1253
+ constructor(src, start) {
1254
+ this.src = src;
1255
+ this.pos = start;
1256
+ let line = 1;
1257
+ let lineStart = 0;
1258
+ for (let i = 0; i < start; i++) {
1259
+ if (src[i] === "\n") {
1260
+ line++;
1261
+ lineStart = i + 1;
1262
+ }
1263
+ }
1264
+ this.line = line;
1265
+ this.lineStart = lineStart;
1266
+ }
1267
+ src;
1268
+ pos;
1269
+ line;
1270
+ lineStart;
1271
+ currentPos() {
1272
+ return {
1273
+ offset: this.pos,
1274
+ line: this.line,
1275
+ column: this.pos - this.lineStart + 1
1276
+ };
1277
+ }
1278
+ advance(n = 1) {
1279
+ for (let i = 0; i < n; i++) {
1280
+ if (this.pos < this.src.length && this.src[this.pos] === "\n") {
1281
+ this.line++;
1282
+ this.lineStart = this.pos + 1;
1283
+ }
1284
+ this.pos++;
1285
+ }
1286
+ }
1287
+ skipTrivia() {
1288
+ while (this.pos < this.src.length) {
1289
+ const c = this.src[this.pos];
1290
+ const n = this.src[this.pos + 1];
1291
+ if (c === " " || c === " " || c === "\r" || c === "\n") {
1292
+ this.advance();
1293
+ continue;
1294
+ }
1295
+ if (c === "/" && n === "/") {
1296
+ while (this.pos < this.src.length && this.src[this.pos] !== "\n") {
1297
+ this.advance();
1298
+ }
1299
+ continue;
1300
+ }
1301
+ if (c === "/" && n === "*") {
1302
+ this.advance(2);
1303
+ while (this.pos < this.src.length) {
1304
+ if (this.src[this.pos] === "*" && this.src[this.pos + 1] === "/") {
1305
+ this.advance(2);
1306
+ break;
1307
+ }
1308
+ this.advance();
1309
+ }
1310
+ continue;
1311
+ }
1312
+ break;
1313
+ }
1314
+ }
1315
+ next() {
1316
+ this.skipTrivia();
1317
+ if (this.pos >= this.src.length) {
1318
+ return { kind: "eof", value: "", pos: this.currentPos(), end: this.pos };
1319
+ }
1320
+ const pos = this.currentPos();
1321
+ const c = this.src[this.pos];
1322
+ switch (c) {
1323
+ case "{":
1324
+ this.advance();
1325
+ return { kind: "lbrace", value: c, pos, end: this.pos };
1326
+ case "}":
1327
+ this.advance();
1328
+ return { kind: "rbrace", value: c, pos, end: this.pos };
1329
+ case "[":
1330
+ this.advance();
1331
+ return { kind: "lbracket", value: c, pos, end: this.pos };
1332
+ case "]":
1333
+ this.advance();
1334
+ return { kind: "rbracket", value: c, pos, end: this.pos };
1335
+ case "(":
1336
+ this.advance();
1337
+ return { kind: "lparen", value: c, pos, end: this.pos };
1338
+ case ")":
1339
+ this.advance();
1340
+ return { kind: "rparen", value: c, pos, end: this.pos };
1341
+ case ",":
1342
+ this.advance();
1343
+ return { kind: "comma", value: c, pos, end: this.pos };
1344
+ case ":":
1345
+ this.advance();
1346
+ return { kind: "colon", value: c, pos, end: this.pos };
1347
+ }
1348
+ if (c === "." && this.src[this.pos + 1] === "." && this.src[this.pos + 2] === ".") {
1349
+ this.advance(3);
1350
+ return { kind: "spread", value: "...", pos, end: this.pos };
1351
+ }
1352
+ if (c === '"' || c === "'") {
1353
+ return this.readString(pos, c);
1354
+ }
1355
+ if (c === "`") {
1356
+ return this.readTemplate(pos);
1357
+ }
1358
+ if (isDigit(c) || c === "-" && isDigit(this.src[this.pos + 1])) {
1359
+ return this.readNumber(pos);
1360
+ }
1361
+ if (isIdentStart(c)) {
1362
+ return this.readIdent(pos);
1363
+ }
1364
+ this.advance();
1365
+ return { kind: "punct", value: c, pos, end: this.pos };
1366
+ }
1367
+ readString(pos, delim) {
1368
+ this.advance();
1369
+ let value = "";
1370
+ while (this.pos < this.src.length) {
1371
+ const c = this.src[this.pos];
1372
+ if (c === "\\") {
1373
+ const esc = this.src[this.pos + 1];
1374
+ this.advance(2);
1375
+ value += decodeEscape(esc);
1376
+ continue;
1377
+ }
1378
+ if (c === delim) {
1379
+ this.advance();
1380
+ return { kind: "string", value, pos, end: this.pos };
1381
+ }
1382
+ if (c === "\n") {
1383
+ return { kind: "punct", value: delim, pos, end: this.pos };
1384
+ }
1385
+ value += c;
1386
+ this.advance();
1387
+ }
1388
+ return { kind: "punct", value: delim, pos, end: this.pos };
1389
+ }
1390
+ readTemplate(pos) {
1391
+ this.advance();
1392
+ let value = "";
1393
+ let hasExpression = false;
1394
+ while (this.pos < this.src.length) {
1395
+ const c = this.src[this.pos];
1396
+ const n = this.src[this.pos + 1];
1397
+ if (c === "\\") {
1398
+ const esc = this.src[this.pos + 1];
1399
+ this.advance(2);
1400
+ value += decodeEscape(esc);
1401
+ continue;
1402
+ }
1403
+ if (c === "$" && n === "{") {
1404
+ hasExpression = true;
1405
+ this.advance(2);
1406
+ let depth = 1;
1407
+ while (this.pos < this.src.length && depth > 0) {
1408
+ const ch = this.src[this.pos];
1409
+ if (ch === "{") depth++;
1410
+ else if (ch === "}") depth--;
1411
+ this.advance();
1412
+ }
1413
+ continue;
1414
+ }
1415
+ if (c === "`") {
1416
+ this.advance();
1417
+ if (hasExpression) {
1418
+ return { kind: "template", value, pos, end: this.pos };
1419
+ }
1420
+ return { kind: "string", value, pos, end: this.pos };
1421
+ }
1422
+ value += c;
1423
+ this.advance();
1424
+ }
1425
+ return { kind: "template", value, pos, end: this.pos };
1426
+ }
1427
+ readNumber(pos) {
1428
+ const start = this.pos;
1429
+ if (this.src[this.pos] === "-") this.advance();
1430
+ while (this.pos < this.src.length && isDigit(this.src[this.pos])) {
1431
+ this.advance();
1432
+ }
1433
+ if (this.src[this.pos] === ".") {
1434
+ this.advance();
1435
+ while (this.pos < this.src.length && isDigit(this.src[this.pos])) {
1436
+ this.advance();
1437
+ }
1438
+ }
1439
+ if (this.src[this.pos] === "e" || this.src[this.pos] === "E") {
1440
+ this.advance();
1441
+ if (this.src[this.pos] === "+" || this.src[this.pos] === "-") {
1442
+ this.advance();
1443
+ }
1444
+ while (this.pos < this.src.length && isDigit(this.src[this.pos])) {
1445
+ this.advance();
1446
+ }
1447
+ }
1448
+ const value = this.src.slice(start, this.pos);
1449
+ return { kind: "number", value, pos, end: this.pos };
1450
+ }
1451
+ readIdent(pos) {
1452
+ const start = this.pos;
1453
+ while (this.pos < this.src.length && isIdentPart(this.src[this.pos])) {
1454
+ this.advance();
1455
+ }
1456
+ const value = this.src.slice(start, this.pos);
1457
+ return { kind: "ident", value, pos, end: this.pos };
1458
+ }
1459
+ };
1460
+ function isDigit(c) {
1461
+ return c !== void 0 && c >= "0" && c <= "9";
1462
+ }
1463
+ function isIdentStart(c) {
1464
+ if (c === void 0) return false;
1465
+ return c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_" || c === "$";
1466
+ }
1467
+ function isIdentPart(c) {
1468
+ return isIdentStart(c) || isDigit(c);
1469
+ }
1470
+ function decodeEscape(esc) {
1471
+ switch (esc) {
1472
+ case "n":
1473
+ return "\n";
1474
+ case "t":
1475
+ return " ";
1476
+ case "r":
1477
+ return "\r";
1478
+ case "\\":
1479
+ return "\\";
1480
+ case "'":
1481
+ return "'";
1482
+ case '"':
1483
+ return '"';
1484
+ case "`":
1485
+ return "`";
1486
+ case "0":
1487
+ return "\0";
1488
+ case "b":
1489
+ return "\b";
1490
+ case "f":
1491
+ return "\f";
1492
+ case "v":
1493
+ return "\v";
1494
+ default:
1495
+ return esc ?? "";
1496
+ }
1497
+ }
1498
+ function parseExpression(content, start) {
1499
+ const tokenizer = new Tokenizer(content, start);
1500
+ const parser = new Parser(tokenizer);
1501
+ const value = parser.parseValue();
1502
+ parser.consumeTrailingAssertions();
1503
+ return value;
1504
+ }
1505
+ var Parser = class {
1506
+ constructor(tok) {
1507
+ this.tok = tok;
1508
+ }
1509
+ tok;
1510
+ lookahead = null;
1511
+ peek() {
1512
+ if (this.lookahead === null) this.lookahead = this.tok.next();
1513
+ return this.lookahead;
1514
+ }
1515
+ consume() {
1516
+ const t = this.peek();
1517
+ this.lookahead = null;
1518
+ return t;
1519
+ }
1520
+ parseValue() {
1521
+ const t = this.peek();
1522
+ switch (t.kind) {
1523
+ case "lbrace":
1524
+ return this.parseObject();
1525
+ case "lbracket":
1526
+ return this.parseArray();
1527
+ case "string":
1528
+ this.consume();
1529
+ return { kind: "string", value: t.value, pos: t.pos };
1530
+ case "template":
1531
+ throw new ExtractError(
1532
+ "uidex-export-invalid-literal",
1533
+ "Template literal with expression parts is not allowed in `export const uidex`; use a plain string literal.",
1534
+ t.pos
1535
+ );
1536
+ case "number": {
1537
+ this.consume();
1538
+ const n = Number(t.value);
1539
+ if (!Number.isFinite(n)) {
1540
+ throw new ExtractError(
1541
+ "uidex-export-invalid-literal",
1542
+ `Invalid numeric literal "${t.value}" in \`export const uidex\`.`,
1543
+ t.pos
1544
+ );
1545
+ }
1546
+ return { kind: "number", value: n, pos: t.pos };
1547
+ }
1548
+ case "ident":
1549
+ if (t.value === "true" || t.value === "false") {
1550
+ this.consume();
1551
+ return {
1552
+ kind: "boolean",
1553
+ value: t.value === "true",
1554
+ pos: t.pos
1555
+ };
1556
+ }
1557
+ if (t.value === "null") {
1558
+ this.consume();
1559
+ return { kind: "null", pos: t.pos };
1560
+ }
1561
+ if (t.value === "undefined") {
1562
+ throw new ExtractError(
1563
+ "uidex-export-invalid-literal",
1564
+ "`undefined` is not allowed as a value in `export const uidex`; omit the field instead.",
1565
+ t.pos
1566
+ );
1567
+ }
1568
+ throw new ExtractError(
1569
+ "uidex-export-invalid-literal",
1570
+ `Identifier reference "${t.value}" is not allowed in \`export const uidex\`; the right-hand side must be a plain literal.`,
1571
+ t.pos
1572
+ );
1573
+ case "spread":
1574
+ throw new ExtractError(
1575
+ "uidex-export-invalid-literal",
1576
+ "Spread (`...`) is not allowed in `export const uidex`; the right-hand side must be a plain literal.",
1577
+ t.pos
1578
+ );
1579
+ case "lparen":
1580
+ throw new ExtractError(
1581
+ "uidex-export-invalid-literal",
1582
+ "Parenthesised or grouped expressions are not allowed in `export const uidex`.",
1583
+ t.pos
1584
+ );
1585
+ case "punct":
1586
+ throw new ExtractError(
1587
+ "uidex-export-invalid-literal",
1588
+ `Unexpected token "${t.value}" in \`export const uidex\`.`,
1589
+ t.pos
1590
+ );
1591
+ case "eof":
1592
+ throw new ExtractError(
1593
+ "uidex-export-invalid-literal",
1594
+ "Expected a value for `export const uidex` but reached end of file.",
1595
+ t.pos
1596
+ );
1597
+ default:
1598
+ throw new ExtractError(
1599
+ "uidex-export-invalid-literal",
1600
+ `Unexpected token in \`export const uidex\`.`,
1601
+ t.pos
1602
+ );
1603
+ }
1604
+ }
1605
+ parseObject() {
1606
+ const open = this.consume();
1607
+ const entries = [];
1608
+ const seen = /* @__PURE__ */ new Set();
1609
+ while (true) {
1610
+ const t = this.peek();
1611
+ if (t.kind === "rbrace") {
1612
+ this.consume();
1613
+ break;
1614
+ }
1615
+ if (t.kind === "spread") {
1616
+ throw new ExtractError(
1617
+ "uidex-export-invalid-literal",
1618
+ "Spread (`...`) is not allowed inside `export const uidex`.",
1619
+ t.pos
1620
+ );
1621
+ }
1622
+ if (t.kind === "lbracket") {
1623
+ this.consume();
1624
+ const keyTok = this.peek();
1625
+ if (keyTok.kind !== "string") {
1626
+ throw new ExtractError(
1627
+ "uidex-export-invalid-literal",
1628
+ "Computed property keys must be string literals in `export const uidex`.",
1629
+ keyTok.pos
1630
+ );
1631
+ }
1632
+ this.consume();
1633
+ const close = this.peek();
1634
+ if (close.kind !== "rbracket") {
1635
+ throw new ExtractError(
1636
+ "uidex-export-invalid-literal",
1637
+ "Expected `]` after computed property key.",
1638
+ close.pos
1639
+ );
1640
+ }
1641
+ this.consume();
1642
+ const colon = this.peek();
1643
+ if (colon.kind !== "colon") {
1644
+ throw new ExtractError(
1645
+ "uidex-export-invalid-literal",
1646
+ "Expected `:` after computed property key.",
1647
+ colon.pos
1648
+ );
1649
+ }
1650
+ this.consume();
1651
+ const value = this.parseValue();
1652
+ this.recordEntry(entries, seen, keyTok.value, value, keyTok.pos);
1653
+ } else if (t.kind === "ident" || t.kind === "string") {
1654
+ const keyTok = this.consume();
1655
+ const next = this.peek();
1656
+ if (next.kind === "colon") {
1657
+ this.consume();
1658
+ const value = this.parseValue();
1659
+ this.recordEntry(entries, seen, keyTok.value, value, keyTok.pos);
1660
+ } else {
1661
+ throw new ExtractError(
1662
+ "uidex-export-invalid-literal",
1663
+ keyTok.kind === "ident" ? `Shorthand property "${keyTok.value}" is not allowed; write "${keyTok.value}: ..." with a literal value.` : "Expected `:` after property key.",
1664
+ keyTok.pos
1665
+ );
1666
+ }
1667
+ } else if (t.kind === "number") {
1668
+ throw new ExtractError(
1669
+ "uidex-export-invalid-literal",
1670
+ "Numeric property keys are not allowed in `export const uidex`.",
1671
+ t.pos
1672
+ );
1673
+ } else {
1674
+ throw new ExtractError(
1675
+ "uidex-export-invalid-literal",
1676
+ `Unexpected token "${t.value}" inside object.`,
1677
+ t.pos
1678
+ );
1679
+ }
1680
+ const after = this.peek();
1681
+ if (after.kind === "comma") {
1682
+ this.consume();
1683
+ continue;
1684
+ }
1685
+ if (after.kind === "rbrace") {
1686
+ this.consume();
1687
+ break;
1688
+ }
1689
+ throw new ExtractError(
1690
+ "uidex-export-invalid-literal",
1691
+ `Expected \`,\` or \`}\`, got "${after.value}".`,
1692
+ after.pos
1693
+ );
1694
+ }
1695
+ return { kind: "object", entries, pos: open.pos };
1696
+ }
1697
+ recordEntry(entries, seen, key, value, pos) {
1698
+ if (seen.has(key)) {
1699
+ throw new ExtractError(
1700
+ "uidex-export-duplicate-field",
1701
+ `Duplicate field "${key}" in \`export const uidex\`.`,
1702
+ pos
1703
+ );
1704
+ }
1705
+ seen.add(key);
1706
+ entries.push([key, value]);
1707
+ }
1708
+ parseArray() {
1709
+ const open = this.consume();
1710
+ const items = [];
1711
+ while (true) {
1712
+ const t = this.peek();
1713
+ if (t.kind === "rbracket") {
1714
+ this.consume();
1715
+ break;
1716
+ }
1717
+ if (t.kind === "spread") {
1718
+ throw new ExtractError(
1719
+ "uidex-export-invalid-literal",
1720
+ "Spread (`...`) is not allowed inside `export const uidex`.",
1721
+ t.pos
1722
+ );
1723
+ }
1724
+ const value = this.parseValue();
1725
+ if (value.kind === "object") {
1726
+ }
1727
+ items.push(value);
1728
+ const after = this.peek();
1729
+ if (after.kind === "comma") {
1730
+ this.consume();
1731
+ continue;
1732
+ }
1733
+ if (after.kind === "rbracket") {
1734
+ this.consume();
1735
+ break;
1736
+ }
1737
+ throw new ExtractError(
1738
+ "uidex-export-invalid-literal",
1739
+ `Expected \`,\` or \`]\`, got "${after.value}".`,
1740
+ after.pos
1741
+ );
1742
+ }
1743
+ return { kind: "array", items, pos: open.pos };
1744
+ }
1745
+ consumeTrailingAssertions() {
1746
+ const first = this.peek();
1747
+ if (first.kind === "ident" && first.value === "as") {
1748
+ this.consume();
1749
+ const next = this.peek();
1750
+ if (next.kind === "ident" && next.value === "const") {
1751
+ this.consume();
1752
+ } else {
1753
+ throw new ExtractError(
1754
+ "uidex-export-invalid-literal",
1755
+ "Only `as const` is allowed after the `export const uidex` value.",
1756
+ next.pos
1757
+ );
1758
+ }
1759
+ }
1760
+ const maybeSatisfies = this.peek();
1761
+ if (maybeSatisfies.kind === "ident" && maybeSatisfies.value === "satisfies") {
1762
+ return;
1763
+ }
1764
+ }
1765
+ };
1766
+ function buildMetadata(value, file, headerPos, diagnostics) {
1767
+ if (value.kind !== "object") {
1768
+ throw new ExtractError(
1769
+ "uidex-export-invalid-literal",
1770
+ "`export const uidex` must be assigned an object literal.",
1771
+ value.pos
1772
+ );
1773
+ }
1774
+ const byKey = /* @__PURE__ */ new Map();
1775
+ for (const [k, v] of value.entries) byKey.set(k, v);
1776
+ const presentKinds = KIND_DISCRIMINATORS.filter(
1777
+ (k) => byKey.has(k)
1778
+ );
1779
+ if (presentKinds.length === 0) {
1780
+ throw new ExtractError(
1781
+ "uidex-export-missing-kind",
1782
+ "`export const uidex` must declare one of: " + KIND_DISCRIMINATORS.join(", ") + ".",
1783
+ value.pos
1784
+ );
1785
+ }
1786
+ if (presentKinds.length > 1) {
1787
+ throw new ExtractError(
1788
+ "uidex-export-ambiguous-kind",
1789
+ `\`export const uidex\` declares multiple kinds (${presentKinds.join(
1790
+ ", "
1791
+ )}); exactly one kind discriminator is allowed per module.`,
1792
+ value.pos
1793
+ );
1794
+ }
1795
+ const discriminator = presentKinds[0];
1796
+ const kind = discriminator === "notFlow" ? "flow" : discriminator;
1797
+ const allowed = ALLOWED_FIELDS[kind];
1798
+ for (const [k] of value.entries) {
1799
+ if (!allowed.has(k)) {
1800
+ const fieldVal = byKey.get(k);
1801
+ throw new ExtractError(
1802
+ "uidex-export-unknown-field",
1803
+ `Unknown field "${k}" in \`export const uidex\` for kind "${kind}". Allowed: ${Array.from(
1804
+ allowed
1805
+ ).sort().join(", ")}.`,
1806
+ fieldVal.pos
1807
+ );
1808
+ }
1809
+ }
1810
+ const idField = discriminator === "notFlow" ? "flow" : discriminator;
1811
+ const idValue = byKey.get(discriminator);
1812
+ let id;
1813
+ if (discriminator === "notFlow") {
1814
+ const v = idValue;
1815
+ if (v.kind !== "boolean" || v.value !== true) {
1816
+ throw new ExtractError(
1817
+ "uidex-export-invalid-field",
1818
+ "`notFlow` must be `true`.",
1819
+ v.pos
1820
+ );
1821
+ }
1822
+ id = false;
1823
+ } else {
1824
+ id = readIdField(idValue, kind, idField);
1825
+ }
1826
+ const acceptance = readStringArrayField(byKey, "acceptance");
1827
+ const description = readStringField(byKey, "description");
1828
+ const name = readStringField(byKey, "name");
1829
+ if (name === "") {
1830
+ const pos = byKey.get("name").pos;
1831
+ diagnostics.push({
1832
+ code: "uidex-export-empty-name",
1833
+ severity: "info",
1834
+ message: "`name` is an empty string; treating as unset.",
1835
+ file,
1836
+ line: pos.line
1837
+ });
1838
+ }
1839
+ const features = kind === "page" ? readStringArrayField(byKey, "features") : void 0;
1840
+ const widgets = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
1841
+ const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
1842
+ const metadata = {
1843
+ source: "ts-export",
1844
+ kind,
1845
+ id,
1846
+ loc: {
1847
+ file,
1848
+ line: headerPos.line,
1849
+ column: headerPos.column
1850
+ }
1851
+ };
1852
+ if (name) metadata.name = name;
1853
+ if (acceptance) metadata.acceptance = acceptance;
1854
+ if (description) metadata.description = description;
1855
+ if (features) metadata.features = features;
1856
+ if (widgets) metadata.widgets = widgets;
1857
+ if (notFlow) metadata.notFlow = true;
1858
+ return metadata;
1859
+ }
1860
+ function readIdField(value, kind, fieldName) {
1861
+ if (value.kind === "string") {
1862
+ if (value.value.length === 0) {
1863
+ throw new ExtractError(
1864
+ "uidex-export-invalid-field",
1865
+ `\`${fieldName}\` must be a non-empty string.`,
1866
+ value.pos
1867
+ );
1868
+ }
1869
+ return value.value;
1870
+ }
1871
+ if (value.kind === "boolean" && value.value === false) {
1872
+ if (!FALSEABLE.has(kind)) {
1873
+ throw new ExtractError(
1874
+ "uidex-export-invalid-field",
1875
+ `\`${fieldName}: false\` is only valid for page, feature, and primitive kinds.`,
1876
+ value.pos
1877
+ );
1878
+ }
1879
+ return false;
1880
+ }
1881
+ throw new ExtractError(
1882
+ "uidex-export-invalid-field",
1883
+ `\`${fieldName}\` must be a string${FALSEABLE.has(kind) ? " or `false`" : ""}.`,
1884
+ value.pos
1885
+ );
1886
+ }
1887
+ function readStringField(byKey, name) {
1888
+ const v = byKey.get(name);
1889
+ if (!v) return void 0;
1890
+ if (v.kind !== "string") {
1891
+ throw new ExtractError(
1892
+ "uidex-export-invalid-field",
1893
+ `\`${name}\` must be a string.`,
1894
+ v.pos
1895
+ );
1896
+ }
1897
+ return v.value;
1898
+ }
1899
+ function readStringArrayField(byKey, name) {
1900
+ const v = byKey.get(name);
1901
+ if (!v) return void 0;
1902
+ if (v.kind !== "array") {
1903
+ throw new ExtractError(
1904
+ "uidex-export-invalid-field",
1905
+ `\`${name}\` must be an array of strings.`,
1906
+ v.pos
1907
+ );
1908
+ }
1909
+ const out2 = [];
1910
+ for (const item of v.items) {
1911
+ if (item.kind !== "string") {
1912
+ throw new ExtractError(
1913
+ "uidex-export-invalid-field",
1914
+ `\`${name}\` must contain only string literals.`,
1915
+ item.pos
1916
+ );
1917
+ }
1918
+ out2.push(item.value);
1919
+ }
1920
+ return out2;
1921
+ }
1922
+ function posAt(content, offset) {
1923
+ let line = 1;
1924
+ let lineStart = 0;
1925
+ for (let i = 0; i < offset && i < content.length; i++) {
1926
+ if (content[i] === "\n") {
1927
+ line++;
1928
+ lineStart = i + 1;
1929
+ }
1930
+ }
1931
+ return { offset, line, column: offset - lineStart + 1 };
1932
+ }
1933
+
1934
+ // src/scan/jsx-ancestry.ts
1935
+ var DATA_ATTR_RE = /\bdata-uidex(?:-(region|widget|primitive))?\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
1936
+ function parseDataAttrs(tagSource) {
1937
+ if (!tagSource.includes("data-uidex")) return [];
1938
+ const out2 = [];
1939
+ for (const m of tagSource.matchAll(DATA_ATTR_RE)) {
1940
+ const kind = m[1] ?? "element";
1941
+ const id = m[2] ?? m[3];
1942
+ if (id) out2.push({ kind, id });
1943
+ }
1944
+ return out2;
1945
+ }
1946
+ function collectJSXAncestry(content) {
1947
+ if (!content.includes("data-uidex")) return [];
1948
+ const out2 = [];
1949
+ const ancestors = [];
1950
+ const stack = [];
1951
+ const N = content.length;
1952
+ let i = 0;
1953
+ let line = 1;
1954
+ const advanceLines = (from, to) => {
1955
+ for (let k = from; k < to; k++) {
1956
+ if (content.charCodeAt(k) === 10) line++;
1957
+ }
1958
+ };
1959
+ while (i < N) {
1960
+ const c = content[i];
1961
+ if (c === "\n") {
1962
+ line++;
1963
+ i++;
1964
+ continue;
1965
+ }
1966
+ if (c === "/" && content[i + 1] === "/") {
1967
+ while (i < N && content[i] !== "\n") i++;
1968
+ continue;
1969
+ }
1970
+ if (c === "/" && content[i + 1] === "*") {
1971
+ const end = content.indexOf("*/", i + 2);
1972
+ const next = end === -1 ? N : end + 2;
1973
+ advanceLines(i, next);
1974
+ i = next;
1975
+ continue;
1976
+ }
1977
+ if (c === '"' || c === "'") {
1978
+ const next = skipString(content, i, c);
1979
+ advanceLines(i, next);
1980
+ i = next;
1981
+ continue;
1982
+ }
1983
+ if (c === "`") {
1984
+ const next = skipTemplate(content, i);
1985
+ advanceLines(i, next);
1986
+ i = next;
1987
+ continue;
1988
+ }
1989
+ if (c === "<") {
1990
+ const nextCh = content[i + 1];
1991
+ if (nextCh === "/") {
1992
+ const end = content.indexOf(">", i);
1993
+ if (end === -1) break;
1994
+ const tagName = content.slice(i + 2, end).match(/^\s*([\w.-]*)/)?.[1] ?? "";
1995
+ if (tagName) {
1996
+ for (let k = stack.length - 1; k >= 0; k--) {
1997
+ if (stack[k].tagName === tagName) {
1998
+ for (let j = stack.length - 1; j >= k; j--) {
1999
+ ancestors.length -= stack[j].pushed;
2000
+ }
2001
+ stack.length = k;
2002
+ break;
2003
+ }
2004
+ }
2005
+ }
2006
+ advanceLines(i, end + 1);
2007
+ i = end + 1;
2008
+ continue;
2009
+ }
2010
+ if (nextCh && /[A-Za-z_]/.test(nextCh)) {
2011
+ const end = findTagEnd(content, i + 1);
2012
+ if (end === -1) break;
2013
+ const tagSource = content.slice(i, end + 1);
2014
+ const tagName = tagSource.match(/^<\s*([\w.-]*)/)?.[1] ?? "";
2015
+ const isSelf = content[end - 1] === "/";
2016
+ if (tagName) {
2017
+ const attrs = parseDataAttrs(tagSource);
2018
+ if (attrs.length > 0) {
2019
+ const snapshot = ancestors.slice();
2020
+ for (const a of attrs) {
2021
+ out2.push({ kind: a.kind, id: a.id, line, ancestors: snapshot });
2022
+ }
2023
+ }
2024
+ if (!isSelf) {
2025
+ for (const a of attrs) ancestors.push(a);
2026
+ stack.push({ tagName, pushed: attrs.length });
2027
+ }
2028
+ }
2029
+ advanceLines(i, end + 1);
2030
+ i = end + 1;
2031
+ continue;
2032
+ }
2033
+ }
2034
+ i++;
2035
+ }
2036
+ return out2;
2037
+ }
2038
+ function skipString(content, start, quote) {
2039
+ const N = content.length;
2040
+ let i = start + 1;
2041
+ while (i < N) {
2042
+ const c = content[i];
2043
+ if (c === "\\") {
2044
+ i += 2;
2045
+ continue;
2046
+ }
2047
+ if (c === quote) return i + 1;
2048
+ i++;
2049
+ }
2050
+ return N;
2051
+ }
2052
+ function skipTemplate(content, start) {
2053
+ const N = content.length;
2054
+ let i = start + 1;
2055
+ while (i < N) {
2056
+ const c = content[i];
2057
+ if (c === "\\") {
2058
+ i += 2;
2059
+ continue;
2060
+ }
2061
+ if (c === "`") return i + 1;
2062
+ if (c === "$" && content[i + 1] === "{") {
2063
+ i += 2;
2064
+ let depth = 1;
2065
+ while (i < N && depth > 0) {
2066
+ const cj = content[i];
2067
+ if (cj === '"' || cj === "'") {
2068
+ i = skipString(content, i, cj);
2069
+ continue;
2070
+ }
2071
+ if (cj === "`") {
2072
+ i = skipTemplate(content, i);
2073
+ continue;
2074
+ }
2075
+ if (cj === "{") depth++;
2076
+ else if (cj === "}") depth--;
2077
+ i++;
2078
+ }
2079
+ continue;
2080
+ }
2081
+ i++;
2082
+ }
2083
+ return N;
2084
+ }
2085
+ function findTagEnd(content, start) {
2086
+ const N = content.length;
2087
+ let i = start;
2088
+ while (i < N) {
2089
+ const c = content[i];
2090
+ if (c === '"' || c === "'") {
2091
+ i = skipString(content, i, c);
2092
+ continue;
2093
+ }
2094
+ if (c === "`") {
2095
+ i = skipTemplate(content, i);
2096
+ continue;
2097
+ }
2098
+ if (c === "{") {
2099
+ let depth = 1;
2100
+ i++;
2101
+ while (i < N && depth > 0) {
2102
+ const cj = content[i];
2103
+ if (cj === '"' || cj === "'") {
2104
+ i = skipString(content, i, cj);
2105
+ continue;
2106
+ }
2107
+ if (cj === "`") {
2108
+ i = skipTemplate(content, i);
2109
+ continue;
2110
+ }
2111
+ if (cj === "{") depth++;
2112
+ else if (cj === "}") depth--;
2113
+ i++;
2114
+ }
2115
+ continue;
2116
+ }
2117
+ if (c === ">") return i;
2118
+ i++;
2119
+ }
2120
+ return -1;
2121
+ }
2122
+
2123
+ // src/scan/extract.ts
2124
+ var JSDOC_BLOCK = /\/\*\*([\s\S]*?)\*\//g;
2125
+ function lineAt(content, index) {
2126
+ let line = 1;
2127
+ for (let i = 0; i < index && i < content.length; i++) {
2128
+ if (content[i] === "\n") line++;
2129
+ }
2130
+ return line;
2131
+ }
2132
+ function parseJSDoc(block) {
2133
+ const lines = block.split("\n").map((l) => l.replace(/^\s*\*\s?/, "").replace(/^\s*\/?\*+/, ""));
2134
+ let kind = null;
2135
+ let id = null;
2136
+ const acceptance = [];
2137
+ const desc = [];
2138
+ let notFlow = false;
2139
+ for (const raw of lines) {
2140
+ const line = raw.trim();
2141
+ if (!line) continue;
2142
+ const uidex = line.match(
2143
+ /^@uidex\s+(page|feature|widget)\s+(\S+)(?:\s+-\s+(.+))?/
2144
+ );
2145
+ if (uidex) {
2146
+ kind = uidex[1];
2147
+ id = uidex[2];
2148
+ if (uidex[3]) desc.push(uidex[3].trim());
2149
+ continue;
2150
+ }
2151
+ if (/^@uidex:not-flow\b/.test(line)) {
2152
+ notFlow = true;
2153
+ continue;
2154
+ }
2155
+ const accept = line.match(/^@acceptance\s+(.+)$/);
2156
+ if (accept) {
2157
+ acceptance.push(accept[1].trim());
2158
+ continue;
2159
+ }
2160
+ if (line.startsWith("@")) continue;
2161
+ desc.push(line);
2162
+ }
2163
+ return {
2164
+ kind,
2165
+ id,
2166
+ description: desc.join(" ").trim(),
2167
+ acceptance,
2168
+ notFlow
2169
+ };
2170
+ }
2171
+ function extract(files) {
2172
+ return files.map((file) => {
2173
+ const { exports: exports2, diagnostics } = extractUidexExports(file);
2174
+ const out2 = {
2175
+ file,
2176
+ annotations: extractOne(file)
2177
+ };
2178
+ if (exports2.length > 0) out2.metadata = exports2;
2179
+ if (diagnostics.length > 0) out2.diagnostics = diagnostics;
2180
+ return out2;
2181
+ });
2182
+ }
2183
+ function extractOne(file) {
2184
+ const annotations = [];
2185
+ const { content, displayPath } = file;
2186
+ for (const occ of collectJSXAncestry(content)) {
2187
+ annotations.push({
2188
+ kind: occ.kind,
2189
+ id: occ.id,
2190
+ file: displayPath,
2191
+ line: occ.line,
2192
+ ...occ.ancestors.length > 0 ? { ancestors: occ.ancestors } : {}
2193
+ });
2194
+ }
2195
+ JSDOC_BLOCK.lastIndex = 0;
2196
+ let jm;
2197
+ while ((jm = JSDOC_BLOCK.exec(content)) !== null) {
2198
+ const parsed = parseJSDoc(jm[1]);
2199
+ const line = lineAt(content, jm.index);
2200
+ if (parsed.notFlow) {
2201
+ annotations.push({ kind: "not-flow", id: "", file: displayPath, line });
2202
+ }
2203
+ if (parsed.kind && parsed.id) {
2204
+ const kind = parsed.kind === "page" ? "page-doc" : parsed.kind === "feature" ? "feature-doc" : "widget-doc";
2205
+ annotations.push({
2206
+ kind,
2207
+ id: parsed.id,
2208
+ file: displayPath,
2209
+ line,
2210
+ description: parsed.description || void 0,
2211
+ acceptance: parsed.acceptance.length ? parsed.acceptance : void 0
2212
+ });
2213
+ } else if (parsed.acceptance.length > 0) {
2214
+ annotations.push({
2215
+ kind: "orphan-acceptance",
2216
+ id: "",
2217
+ file: displayPath,
2218
+ line,
2219
+ acceptance: parsed.acceptance
2220
+ });
2221
+ }
2222
+ }
2223
+ return annotations;
2224
+ }
2225
+
2226
+ // src/scan/git.ts
2227
+ var import_node_child_process = require("child_process");
2228
+ function runGit(args, cwd) {
2229
+ try {
2230
+ const out2 = (0, import_node_child_process.execSync)(`git ${args.join(" ")}`, {
2231
+ cwd,
2232
+ stdio: ["ignore", "pipe", "ignore"],
2233
+ encoding: "utf8"
2234
+ });
2235
+ return out2.trim() || null;
2236
+ } catch {
2237
+ return null;
2238
+ }
2239
+ }
2240
+ function resolveGitContext(opts = {}) {
2241
+ const cwd = opts.cwd ?? process.cwd();
2242
+ const env = opts.env ?? process.env;
2243
+ const branch = env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME || env.BUILDKITE_BRANCH || env.CI_COMMIT_REF_NAME || env.BITBUCKET_BRANCH || runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd) || null;
2244
+ const commit = env.GITHUB_SHA || env.BUILDKITE_COMMIT || env.CI_COMMIT_SHA || env.BITBUCKET_COMMIT || runGit(["rev-parse", "HEAD"], cwd) || null;
2245
+ const pr = env.PR_NUMBER || env.GITHUB_PR_NUMBER || parseGitHubRef(env.GITHUB_REF) || env.BUILDKITE_PULL_REQUEST || env.CI_MERGE_REQUEST_IID || env.BITBUCKET_PR_ID || null;
2246
+ return {
2247
+ branch: branch || null,
2248
+ commit: commit || null,
2249
+ pr: pr && pr !== "false" ? String(pr) : null
2250
+ };
2251
+ }
2252
+ function parseGitHubRef(ref) {
2253
+ if (!ref) return null;
2254
+ const m = ref.match(/^refs\/pull\/(\d+)\/merge$/);
2255
+ return m ? m[1] : null;
2256
+ }
2257
+
2258
+ // src/scan/resolve.ts
2259
+ var path5 = __toESM(require("path"), 1);
2260
+
2261
+ // src/scan/routes.ts
2262
+ var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
2263
+ var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
2264
+ var ROUTE_BASENAME = /^route\.(tsx|ts|jsx|js|mjs|cjs)$/;
2265
+ function detectRoutes(files) {
2266
+ const out2 = [];
2267
+ const seen = /* @__PURE__ */ new Set();
2268
+ for (const f of files) {
2269
+ const rel = f.displayPath;
2270
+ const parts = rel.split("/");
2271
+ const base = parts[parts.length - 1];
2272
+ const appIdx = parts.indexOf("app");
2273
+ if (appIdx !== -1 && PAGE_BASENAME.test(base)) {
2274
+ const routeSegments = parts.slice(appIdx + 1, parts.length - 1);
2275
+ const routePath = formatNextAppPath(routeSegments);
2276
+ push(out2, seen, routePath, f.displayPath);
2277
+ continue;
2278
+ }
2279
+ if (appIdx !== -1 && ROUTE_BASENAME.test(base)) {
2280
+ continue;
2281
+ }
2282
+ const pagesIdx = parts.indexOf("pages");
2283
+ if (pagesIdx !== -1 && PAGES_ROUTER_BASENAME.test(base)) {
2284
+ const segs = parts.slice(pagesIdx + 1);
2285
+ if (segs[0] === "api") continue;
2286
+ const last = segs[segs.length - 1];
2287
+ if (last.startsWith("_")) continue;
2288
+ const normalized = [
2289
+ ...segs.slice(0, -1),
2290
+ base.replace(/\.[^.]+$/, "")
2291
+ ].filter((s) => s !== "index");
2292
+ const routePath = formatNextAppPath(normalized);
2293
+ push(out2, seen, routePath, f.displayPath);
2294
+ continue;
2295
+ }
2296
+ const routesIdx = parts.indexOf("routes");
2297
+ if (routesIdx !== -1 && PAGES_ROUTER_BASENAME.test(base)) {
2298
+ const segs = parts.slice(routesIdx + 1);
2299
+ const last = segs[segs.length - 1];
2300
+ if (last.startsWith("_")) continue;
2301
+ const normalized = [
2302
+ ...segs.slice(0, -1),
2303
+ base.replace(/\.[^.]+$/, "")
2304
+ ].filter((s) => s !== "index" && s !== "__root");
2305
+ const routePath = formatNextAppPath(normalized);
2306
+ push(out2, seen, routePath, f.displayPath);
2307
+ continue;
2308
+ }
2309
+ }
2310
+ return out2.sort((a, b) => a.path.localeCompare(b.path));
2311
+ }
2312
+ function push(out2, seen, routePath, file) {
2313
+ if (seen.has(routePath)) return;
2314
+ seen.add(routePath);
2315
+ out2.push({ id: pathToId(routePath), path: routePath, file });
2316
+ }
2317
+ function formatNextAppPath(segments) {
2318
+ const kept = segments.filter((s) => !(s.startsWith("(") && s.endsWith(")")));
2319
+ if (kept.length === 0) return "/";
2320
+ return "/" + kept.join("/");
2321
+ }
2322
+ function pathToId(routePath) {
2323
+ if (routePath === "/") return "root";
2324
+ return routePath.replace(/^\/+/, "").replace(/\[\.{3}([^\]]+)\]/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2325
+ }
2326
+
2327
+ // src/scan/walk.ts
2328
+ var fs4 = __toESM(require("fs"), 1);
2329
+ var path4 = __toESM(require("path"), 1);
2330
+ var DEFAULT_INCLUDES = ["**/*.{ts,tsx,js,jsx,mjs,cjs}"];
2331
+ var BASE_EXCLUDES = [
2332
+ "**/node_modules/**",
2333
+ "**/dist/**",
2334
+ "**/build/**",
2335
+ "**/.next/**",
2336
+ "**/*.gen.ts"
2337
+ ];
2338
+ var TEST_EXCLUDES = [
2339
+ "**/*.test.ts",
2340
+ "**/*.test.tsx",
2341
+ "**/*.spec.ts",
2342
+ "**/*.spec.tsx"
2343
+ ];
2344
+ var DEFAULT_EXCLUDES = [...BASE_EXCLUDES, ...TEST_EXCLUDES];
2345
+ function globToRegExp(glob) {
2346
+ let i = 0;
2347
+ let out2 = "";
2348
+ while (i < glob.length) {
2349
+ const c = glob[i];
2350
+ if (c === "*" && glob[i + 1] === "*") {
2351
+ if (glob[i + 2] === "/") {
2352
+ out2 += "(?:.*/)?";
2353
+ i += 3;
2354
+ continue;
2355
+ }
2356
+ out2 += ".*";
2357
+ i += 2;
2358
+ continue;
2359
+ }
2360
+ if (c === "*") {
2361
+ out2 += "[^/]*";
2362
+ i++;
2363
+ continue;
2364
+ }
2365
+ if (c === "?") {
2366
+ out2 += "[^/]";
2367
+ i++;
2368
+ continue;
2369
+ }
2370
+ if (c === "{") {
2371
+ const end = glob.indexOf("}", i);
2372
+ if (end === -1) {
2373
+ out2 += "\\{";
2374
+ i++;
2375
+ continue;
2376
+ }
2377
+ const parts = glob.slice(i + 1, end).split(",").map((p2) => p2.replace(/[.+^$()|\\]/g, "\\$&"));
2378
+ out2 += `(?:${parts.join("|")})`;
2379
+ i = end + 1;
2380
+ continue;
2381
+ }
2382
+ if (/[.+^$()|\\]/.test(c)) {
2383
+ out2 += "\\" + c;
2384
+ i++;
2385
+ continue;
2386
+ }
2387
+ out2 += c;
2388
+ i++;
2389
+ }
2390
+ return new RegExp(`^${out2}$`);
2391
+ }
2392
+ function toPosix(p2) {
2393
+ return p2.split(path4.sep).join("/");
2394
+ }
2395
+ function matchesAny(rel, patterns) {
2396
+ return patterns.some((g) => globToRegExp(g).test(rel));
2397
+ }
2398
+ function walk(sources, options) {
2399
+ const { cwd, globalExcludes = [], includeTests = false } = options;
2400
+ const out2 = [];
2401
+ const baseDefaults = includeTests ? BASE_EXCLUDES : DEFAULT_EXCLUDES;
2402
+ for (const source of sources) {
2403
+ const includes = source.include?.length ? source.include : DEFAULT_INCLUDES;
2404
+ const excludes = [
2405
+ ...baseDefaults,
2406
+ ...globalExcludes,
2407
+ ...source.exclude ?? []
2408
+ ];
2409
+ const absRoot = path4.resolve(cwd, source.rootDir);
2410
+ for (const filePath of walkDir(absRoot, absRoot)) {
2411
+ const rel = toPosix(path4.relative(absRoot, filePath));
2412
+ if (matchesAny(rel, excludes)) continue;
2413
+ if (!matchesAny(rel, includes)) continue;
2414
+ let content;
2415
+ try {
2416
+ content = fs4.readFileSync(filePath, "utf8");
2417
+ } catch {
2418
+ continue;
2419
+ }
2420
+ const relFromCwd = toPosix(path4.relative(cwd, filePath));
2421
+ const displayPath = source.prefix ? `${source.prefix.replace(/\/$/, "")}/${rel}` : relFromCwd;
2422
+ out2.push({
2423
+ sourcePath: filePath,
2424
+ relativePath: rel,
2425
+ displayPath,
2426
+ content
2427
+ });
2428
+ }
2429
+ }
2430
+ return out2.sort((a, b) => a.displayPath.localeCompare(b.displayPath));
2431
+ }
2432
+ function* walkDir(root, dir) {
2433
+ let entries;
2434
+ try {
2435
+ entries = fs4.readdirSync(dir, { withFileTypes: true });
2436
+ } catch {
2437
+ return;
2438
+ }
2439
+ for (const entry of entries) {
2440
+ const full = path4.join(dir, entry.name);
2441
+ if (entry.isDirectory()) {
2442
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git" || entry.name === "build" || entry.name === ".next") {
2443
+ continue;
2444
+ }
2445
+ yield* walkDir(root, full);
2446
+ } else if (entry.isFile()) {
2447
+ yield full;
2448
+ }
2449
+ }
2450
+ }
2451
+
2452
+ // src/scan/resolve.ts
2453
+ var DOM_ATTR_KINDS = /* @__PURE__ */ new Set([
2454
+ "element",
2455
+ "region",
2456
+ "widget",
2457
+ "primitive"
2458
+ ]);
2459
+ function resolveConventions(c) {
2460
+ return {
2461
+ primitives: c?.primitives === void 0 ? DEFAULT_CONVENTIONS.primitives : c.primitives,
2462
+ features: c?.features === void 0 ? DEFAULT_CONVENTIONS.features : c.features,
2463
+ pages: c?.pages === void 0 ? DEFAULT_CONVENTIONS.pages : c.pages,
2464
+ flows: c?.flows === void 0 ? DEFAULT_CONVENTIONS.flows : c.flows,
2465
+ regions: c?.regions === void 0 ? DEFAULT_CONVENTIONS.regions : c.regions
2466
+ };
2467
+ }
2468
+ function kebab(str) {
2469
+ return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").replace(/[^a-zA-Z0-9-]/g, "").toLowerCase();
2470
+ }
2471
+ function baseName(file) {
2472
+ const b = path5.posix.basename(file);
2473
+ return b.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "");
2474
+ }
2475
+ var LANDMARK_RE = /<(header|nav|main|aside|footer)(\s[^>]*)?>|role=["']region["']/gi;
2476
+ function extractLandmarks(file) {
2477
+ const out2 = [];
2478
+ LANDMARK_RE.lastIndex = 0;
2479
+ let m;
2480
+ while ((m = LANDMARK_RE.exec(file.content)) !== null) {
2481
+ const tag = m[1] ?? "region";
2482
+ const line = 1 + file.content.slice(0, m.index).split("\n").length - 1;
2483
+ out2.push({ tag, line });
2484
+ }
2485
+ return out2;
2486
+ }
2487
+ function fileMatchesAny(displayPath, patterns) {
2488
+ return patterns.some((g) => globToRegExp(g).test(displayPath));
2489
+ }
2490
+ function buildMetaFromExport(exp) {
2491
+ const meta = {};
2492
+ if (exp.name) meta.name = exp.name;
2493
+ if (exp.description) meta.description = exp.description;
2494
+ if (exp.acceptance?.length) meta.acceptance = exp.acceptance;
2495
+ if (exp.features?.length) meta.features = exp.features;
2496
+ if (exp.widgets?.length) meta.widgets = exp.widgets;
2497
+ return Object.keys(meta).length > 0 ? meta : void 0;
2498
+ }
2499
+ function resolve3(ctx) {
2500
+ const conventions = resolveConventions(ctx.config.conventions);
2501
+ const registry = createRegistry();
2502
+ const diagnostics = [];
2503
+ const exportsByFile = /* @__PURE__ */ new Map();
2504
+ for (const ef of ctx.extracted) {
2505
+ if (!ef.metadata) continue;
2506
+ const map = /* @__PURE__ */ new Map();
2507
+ for (const m of ef.metadata) map.set(m.kind, m);
2508
+ exportsByFile.set(ef.file.displayPath, map);
2509
+ }
2510
+ function exportFor(file, kind) {
2511
+ return exportsByFile.get(file)?.get(kind);
2512
+ }
2513
+ const allAnnotations = [];
2514
+ for (const ef of ctx.extracted) {
2515
+ for (const a of ef.annotations) allAnnotations.push(a);
2516
+ }
2517
+ const directChildren = /* @__PURE__ */ new Map();
2518
+ const seenPerParent = /* @__PURE__ */ new Map();
2519
+ for (const ann of allAnnotations) {
2520
+ if (!DOM_ATTR_KINDS.has(ann.kind)) continue;
2521
+ const parent = ann.ancestors?.[ann.ancestors.length - 1];
2522
+ if (!parent) continue;
2523
+ const pk = `${parent.kind}:${parent.id}`;
2524
+ const childKey = `${ann.kind}:${ann.id}`;
2525
+ let seen = seenPerParent.get(pk);
2526
+ if (!seen) {
2527
+ seen = /* @__PURE__ */ new Set();
2528
+ seenPerParent.set(pk, seen);
2529
+ }
2530
+ if (seen.has(childKey)) continue;
2531
+ seen.add(childKey);
2532
+ let arr = directChildren.get(pk);
2533
+ if (!arr) {
2534
+ arr = [];
2535
+ directChildren.set(pk, arr);
2536
+ }
2537
+ arr.push({ kind: ann.kind, id: ann.id });
2538
+ }
2539
+ function metaWithComposes(kind, id, base) {
2540
+ const composes = directChildren.get(`${kind}:${id}`);
2541
+ if (!composes || composes.length === 0) return base;
2542
+ return { ...base ?? {}, composes };
2543
+ }
2544
+ const routes = conventions.pages === "auto" ? detectRoutes(ctx.extracted.map((e) => e.file)) : [];
2545
+ const handledPageFiles = /* @__PURE__ */ new Set();
2546
+ for (const route of routes) {
2547
+ const exp = exportFor(route.file, "page");
2548
+ if (exp && exp.id === false) continue;
2549
+ const effectiveId = exp && typeof exp.id === "string" ? exp.id : route.id;
2550
+ const meta = exp ? buildMetaFromExport(exp) : void 0;
2551
+ registry.add({ kind: "route", path: route.path, page: effectiveId });
2552
+ const page = {
2553
+ kind: "page",
2554
+ id: effectiveId,
2555
+ loc: { file: route.file, line: exp?.loc.line },
2556
+ ...meta ? { meta } : {}
2557
+ };
2558
+ registry.add(page);
2559
+ handledPageFiles.add(route.file);
2560
+ }
2561
+ for (const ef of ctx.extracted) {
2562
+ const exp = exportFor(ef.file.displayPath, "page");
2563
+ if (!exp || typeof exp.id !== "string") continue;
2564
+ if (handledPageFiles.has(ef.file.displayPath)) continue;
2565
+ const meta = buildMetaFromExport(exp);
2566
+ registry.add({
2567
+ kind: "page",
2568
+ id: exp.id,
2569
+ loc: { file: ef.file.displayPath, line: exp.loc.line },
2570
+ ...meta ? { meta } : {}
2571
+ });
2572
+ }
2573
+ const featureGlob = typeof conventions.features === "string" ? conventions.features : null;
2574
+ const conventionalFeatureDirs = /* @__PURE__ */ new Set();
2575
+ const featureExportsByDir = /* @__PURE__ */ new Map();
2576
+ const suppressedFeatureDirs = /* @__PURE__ */ new Set();
2577
+ if (featureGlob) {
2578
+ const re = globToRegExp(featureGlob + "/**");
2579
+ for (const ef of ctx.extracted) {
2580
+ if (!re.test(ef.file.displayPath)) continue;
2581
+ const dir = extractFeatureDir(ef.file.displayPath, featureGlob);
2582
+ if (!dir) continue;
2583
+ conventionalFeatureDirs.add(dir);
2584
+ const exp = exportFor(ef.file.displayPath, "feature");
2585
+ if (exp) {
2586
+ if (exp.id === false) suppressedFeatureDirs.add(dir);
2587
+ else if (!featureExportsByDir.has(dir))
2588
+ featureExportsByDir.set(dir, exp);
2589
+ }
2590
+ }
2591
+ for (const dir of conventionalFeatureDirs) {
2592
+ if (suppressedFeatureDirs.has(dir)) continue;
2593
+ const exp = featureExportsByDir.get(dir);
2594
+ const id = exp && typeof exp.id === "string" ? exp.id : path5.posix.basename(dir);
2595
+ const meta = exp ? buildMetaFromExport(exp) : void 0;
2596
+ const feature = {
2597
+ kind: "feature",
2598
+ id,
2599
+ loc: exp ? { file: exp.loc.file, line: exp.loc.line } : { file: dir },
2600
+ ...meta ? { meta } : {}
2601
+ };
2602
+ registry.add(feature);
2603
+ }
2604
+ }
2605
+ for (const ef of ctx.extracted) {
2606
+ const exp = exportFor(ef.file.displayPath, "feature");
2607
+ if (!exp || typeof exp.id !== "string") continue;
2608
+ if (featureGlob) {
2609
+ const dir = extractFeatureDir(ef.file.displayPath, featureGlob);
2610
+ if (dir && conventionalFeatureDirs.has(dir)) continue;
2611
+ }
2612
+ const meta = buildMetaFromExport(exp);
2613
+ registry.add({
2614
+ kind: "feature",
2615
+ id: exp.id,
2616
+ loc: { file: ef.file.displayPath, line: exp.loc.line },
2617
+ ...meta ? { meta } : {}
2618
+ });
2619
+ }
2620
+ const widgetDomByFile = /* @__PURE__ */ new Map();
2621
+ for (const a of allAnnotations) {
2622
+ if (a.kind !== "widget") continue;
2623
+ const list = widgetDomByFile.get(a.file) ?? [];
2624
+ list.push(a);
2625
+ widgetDomByFile.set(a.file, list);
2626
+ }
2627
+ const widgetFiles = /* @__PURE__ */ new Set([
2628
+ ...widgetDomByFile.keys(),
2629
+ ...[...exportsByFile.entries()].filter(([, kinds]) => kinds.has("widget")).map(([file]) => file)
2630
+ ]);
2631
+ for (const file of widgetFiles) {
2632
+ const domAnnotations = widgetDomByFile.get(file) ?? [];
2633
+ const exp = exportFor(file, "widget");
2634
+ if (exp && typeof exp.id === "string") {
2635
+ const exportId = exp.id;
2636
+ const domIds = domAnnotations.map((a) => a.id);
2637
+ if (domAnnotations.length === 0) {
2638
+ diagnostics.push({
2639
+ code: "widget-missing-dom",
2640
+ severity: "error",
2641
+ message: `Widget "${exportId}" declared via export const uidex but no data-uidex-widget="${exportId}" attribute found in the same file`,
2642
+ file,
2643
+ line: exp.loc.line,
2644
+ entity: { kind: "widget", id: exportId },
2645
+ hint: `Add \`<... data-uidex-widget="${exportId}">\` at the widget's DOM root`
2646
+ });
2647
+ continue;
2648
+ }
2649
+ if (!domIds.includes(exportId)) {
2650
+ const domList = domIds.map((i) => `"${i}"`).join(", ");
2651
+ diagnostics.push({
2652
+ code: "widget-id-mismatch",
2653
+ severity: "error",
2654
+ message: `Widget id mismatch in ${file}: export declares "${exportId}" but data-uidex-widget attribute(s) are [${domList}]`,
2655
+ file,
2656
+ line: exp.loc.line,
2657
+ entity: { kind: "widget", id: exportId },
2658
+ hint: `Update either the export or the DOM attribute so both sides use the same id`
2659
+ });
2660
+ continue;
2661
+ }
2662
+ const match = domAnnotations.find((a) => a.id === exportId);
2663
+ const meta = metaWithComposes(
2664
+ "widget",
2665
+ exportId,
2666
+ buildMetaFromExport(exp)
2667
+ );
2668
+ const widget = {
2669
+ kind: "widget",
2670
+ id: exportId,
2671
+ loc: { file, line: match.line },
2672
+ ...meta ? { meta } : {}
2673
+ };
2674
+ registry.add(widget);
2675
+ } else {
2676
+ for (const a of domAnnotations) {
2677
+ const meta = metaWithComposes("widget", a.id);
2678
+ const widget = {
2679
+ kind: "widget",
2680
+ id: a.id,
2681
+ loc: { file: a.file, line: a.line },
2682
+ ...meta ? { meta } : {}
2683
+ };
2684
+ registry.add(widget);
2685
+ }
2686
+ }
2687
+ }
2688
+ if (conventions.regions === "landmarks") {
2689
+ for (const ef of ctx.extracted) {
2690
+ for (const lm of extractLandmarks(ef.file)) {
2691
+ const id = kebab(`${lm.tag}`);
2692
+ if (!registry.get("region", id)) {
2693
+ const meta = metaWithComposes("region", id);
2694
+ const region = {
2695
+ kind: "region",
2696
+ id,
2697
+ loc: { file: ef.file.displayPath, line: lm.line },
2698
+ ...meta ? { meta } : {}
2699
+ };
2700
+ registry.add(region);
2701
+ }
2702
+ }
2703
+ }
2704
+ }
2705
+ for (const r of allAnnotations.filter((a) => a.kind === "region")) {
2706
+ const meta = metaWithComposes("region", r.id);
2707
+ const region = {
2708
+ kind: "region",
2709
+ id: r.id,
2710
+ loc: { file: r.file, line: r.line },
2711
+ ...meta ? { meta } : {}
2712
+ };
2713
+ registry.add(region);
2714
+ }
2715
+ const primitiveConventions = conventions.primitives;
2716
+ for (const ef of ctx.extracted) {
2717
+ const file = ef.file.displayPath;
2718
+ const exp = exportFor(file, "primitive");
2719
+ if (exp && exp.id === false) continue;
2720
+ if (exp && typeof exp.id === "string") {
2721
+ const meta = metaWithComposes(
2722
+ "primitive",
2723
+ exp.id,
2724
+ buildMetaFromExport(exp)
2725
+ );
2726
+ const primitive = {
2727
+ kind: "primitive",
2728
+ id: exp.id,
2729
+ loc: { file, line: exp.loc.line },
2730
+ scopes: [computeScope(file)],
2731
+ ...meta ? { meta } : {}
2732
+ };
2733
+ registry.add(primitive);
2734
+ continue;
2735
+ }
2736
+ const domPrimitives = allAnnotations.filter(
2737
+ (a) => a.kind === "primitive" && a.file === file
2738
+ );
2739
+ if (domPrimitives.length > 0) {
2740
+ for (const p2 of domPrimitives) {
2741
+ const meta = metaWithComposes("primitive", p2.id);
2742
+ const primitive = {
2743
+ kind: "primitive",
2744
+ id: p2.id,
2745
+ loc: { file: p2.file, line: p2.line },
2746
+ scopes: [computeScope(p2.file)],
2747
+ ...meta ? { meta } : {}
2748
+ };
2749
+ registry.add(primitive);
2750
+ }
2751
+ continue;
2752
+ }
2753
+ if (primitiveConventions && fileMatchesAny(file, primitiveConventions)) {
2754
+ const name = kebab(baseName(file));
2755
+ if (!name) continue;
2756
+ const scope = computeScope(file);
2757
+ const meta = metaWithComposes("primitive", name);
2758
+ const primitive = {
2759
+ kind: "primitive",
2760
+ id: name,
2761
+ loc: { file },
2762
+ scopes: [scope],
2763
+ ...meta ? { meta } : {}
2764
+ };
2765
+ registry.add(primitive);
2766
+ }
2767
+ }
2768
+ for (const e of allAnnotations.filter((a) => a.kind === "element")) {
2769
+ const element = {
2770
+ kind: "element",
2771
+ id: e.id,
2772
+ loc: { file: e.file, line: e.line }
2773
+ };
2774
+ registry.add(element);
2775
+ }
2776
+ if (ctx.flowFiles && conventions.flows) {
2777
+ for (const ff of ctx.flowFiles) {
2778
+ const notFlowExport = (ff.metadata ?? []).find(
2779
+ (m) => m.kind === "flow" && m.notFlow === true
2780
+ );
2781
+ if (notFlowExport) continue;
2782
+ const flowExport = (ff.metadata ?? []).find(
2783
+ (m) => m.kind === "flow" && typeof m.id === "string"
2784
+ );
2785
+ const derived = extractFlowsFromSource(ff.file);
2786
+ if (flowExport && typeof flowExport.id === "string" && derived.length === 1) {
2787
+ const base = derived[0];
2788
+ const flow = {
2789
+ kind: "flow",
2790
+ id: flowExport.id,
2791
+ loc: base.loc,
2792
+ touches: base.touches
2793
+ };
2794
+ registry.add(flow);
2795
+ } else {
2796
+ for (const flow of derived) registry.add(flow);
2797
+ }
2798
+ }
2799
+ }
2800
+ const entities = [];
2801
+ for (const kind of [
2802
+ "route",
2803
+ "page",
2804
+ "feature",
2805
+ "widget",
2806
+ "region",
2807
+ "element",
2808
+ "primitive",
2809
+ "flow"
2810
+ ]) {
2811
+ entities.push(...registry.list(kind));
2812
+ }
2813
+ return { registry, entities, routes, diagnostics };
2814
+ }
2815
+ function extractFeatureDir(displayPath, featureGlob) {
2816
+ const parts = featureGlob.split("/");
2817
+ const starIdx = parts.indexOf("*");
2818
+ if (starIdx === -1) return null;
2819
+ const prefixParts = parts.slice(0, starIdx);
2820
+ const pathParts = displayPath.split("/");
2821
+ for (let i = 0; i < prefixParts.length; i++) {
2822
+ if (pathParts[i] !== prefixParts[i]) return null;
2823
+ }
2824
+ if (pathParts.length <= starIdx) return null;
2825
+ return pathParts.slice(0, starIdx + 1).join("/");
2826
+ }
2827
+ function computeScope(displayPath) {
2828
+ const parts = displayPath.split("/");
2829
+ const featureIdx = parts.indexOf("features");
2830
+ if (featureIdx !== -1 && parts[featureIdx + 1]) {
2831
+ return `feature:${parts[featureIdx + 1]}`;
2832
+ }
2833
+ const pagesIdx = parts.indexOf("pages");
2834
+ if (pagesIdx !== -1 && parts[pagesIdx + 1]) {
2835
+ return `page:${parts[pagesIdx + 1]}`;
2836
+ }
2837
+ return "global";
2838
+ }
2839
+ function extractFlowsFromSource(file) {
2840
+ const flows = [];
2841
+ const source = file.content;
2842
+ const describeRe = /test\.describe\(\s*(?:'([^']*)'|"([^"]*)")\s*,\s*\{[^}]*tag:\s*(?:'@uidex:flow'|"@uidex:flow"|\[[^\]]*@uidex:flow[^\]]*\])[^}]*\}/g;
2843
+ let m;
2844
+ while ((m = describeRe.exec(source)) !== null) {
2845
+ const title = m[1] ?? m[2];
2846
+ const id = kebab(title);
2847
+ const line = 1 + source.slice(0, m.index).split("\n").length - 1;
2848
+ const after = source.slice(m.index + m[0].length);
2849
+ const arrow = after.match(/=>\s*\{/);
2850
+ if (!arrow || arrow.index === void 0) continue;
2851
+ const bodyStart = m.index + m[0].length + arrow.index + arrow[0].length;
2852
+ let depth = 1;
2853
+ let bodyEnd = -1;
2854
+ for (let i = bodyStart; i < source.length; i++) {
2855
+ if (source[i] === "{") depth++;
2856
+ else if (source[i] === "}") {
2857
+ depth--;
2858
+ if (depth === 0) {
2859
+ bodyEnd = i;
2860
+ break;
2861
+ }
2862
+ }
2863
+ }
2864
+ if (bodyEnd === -1) continue;
2865
+ const body = source.slice(bodyStart, bodyEnd);
2866
+ const touches = captureUidexIds(body);
2867
+ flows.push({
2868
+ kind: "flow",
2869
+ id,
2870
+ loc: { file: file.displayPath, line },
2871
+ touches: dedupe(touches.map((t) => t.id))
2872
+ });
2873
+ }
2874
+ return flows;
2875
+ }
2876
+ function captureUidexIds(body) {
2877
+ const out2 = [];
2878
+ const re = /uidex\(\s*(?:'([^']+)'|"([^"]+)"|`([^`$]+)`)\s*\)/g;
2879
+ let m;
2880
+ while ((m = re.exec(body)) !== null) {
2881
+ out2.push({ id: m[1] || m[2] || m[3] });
2882
+ }
2883
+ return out2;
2884
+ }
2885
+ function dedupe(arr) {
2886
+ return Array.from(new Set(arr));
2887
+ }
2888
+
2889
+ // src/scan/pipeline.ts
2890
+ function runScan(opts = {}) {
2891
+ const cwd = opts.cwd ?? process.cwd();
2892
+ const configs = opts.configs ?? discover({ cwd });
2893
+ if (configs.length === 0) {
2894
+ throw new Error(`No .uidex.json found starting from ${cwd}`);
2895
+ }
2896
+ return configs.map((dc) => runOne(dc, opts));
2897
+ }
2898
+ function runOne(dc, opts) {
2899
+ const { config, configDir } = dc;
2900
+ const sourceFiles = walk(config.sources, {
2901
+ cwd: configDir,
2902
+ globalExcludes: config.exclude
2903
+ });
2904
+ const extracted = extract(sourceFiles);
2905
+ const flowFiles = config.flows ? walk(
2906
+ config.flows.map((glob) => ({ rootDir: ".", include: [glob] })),
2907
+ { cwd: configDir, includeTests: true }
2908
+ ) : [];
2909
+ const extractedFlows = extract(flowFiles);
2910
+ const resolved = resolve3({
2911
+ config,
2912
+ extracted,
2913
+ flowFiles: extractedFlows
2914
+ });
2915
+ const gitContext = resolveGitContext({ cwd: configDir });
2916
+ const generated = emit({
2917
+ registry: resolved.registry,
2918
+ gitContext,
2919
+ typeMode: config.typeMode
2920
+ });
2921
+ const outputPath = path6.resolve(configDir, config.output);
2922
+ const outputRel = config.output;
2923
+ let existingOnDisk = null;
2924
+ if (opts.check) {
2925
+ try {
2926
+ existingOnDisk = fs5.readFileSync(outputPath, "utf8");
2927
+ } catch {
2928
+ existingOnDisk = null;
2929
+ }
2930
+ }
2931
+ let auditResult;
2932
+ if (opts.check || opts.lint || resolved.diagnostics.length > 0) {
2933
+ auditResult = audit({
2934
+ registry: resolved.registry,
2935
+ extracted,
2936
+ files: sourceFiles,
2937
+ config,
2938
+ check: opts.check,
2939
+ lint: opts.lint,
2940
+ resolveDiagnostics: resolved.diagnostics,
2941
+ generated,
2942
+ existingOnDisk,
2943
+ outputPath: outputRel
2944
+ });
2945
+ }
2946
+ return {
2947
+ config,
2948
+ configDir,
2949
+ registry: resolved.registry,
2950
+ gitContext,
2951
+ audit: auditResult,
2952
+ generated,
2953
+ outputPath
2954
+ };
2955
+ }
2956
+ function writeScanResult(result) {
2957
+ fs5.mkdirSync(path6.dirname(result.outputPath), { recursive: true });
2958
+ fs5.writeFileSync(result.outputPath, result.generated, "utf8");
2959
+ }
2960
+
2961
+ // src/scan/scaffold.ts
2962
+ var fs6 = __toESM(require("fs"), 1);
2963
+ var path7 = __toESM(require("path"), 1);
2964
+ function scaffoldWidgetSpec(opts) {
2965
+ const {
2966
+ registry,
2967
+ widgetId,
2968
+ outDir,
2969
+ force = false,
2970
+ fixtureImport = "./fixtures"
2971
+ } = opts;
2972
+ const widget = registry.get("widget", widgetId);
2973
+ if (!widget) {
2974
+ throw new Error(`Widget "${widgetId}" not found in registry`);
2975
+ }
2976
+ const criteria = widget.meta?.acceptance ?? [];
2977
+ const filename = `widget-${widgetId}.spec.ts`;
2978
+ const outputPath = path7.resolve(outDir, filename);
2979
+ if (fs6.existsSync(outputPath) && !force) {
2980
+ return {
2981
+ outputPath,
2982
+ written: false,
2983
+ skipped: true,
2984
+ reason: `spec already exists at ${outputPath}; pass --force to overwrite`
2985
+ };
2986
+ }
2987
+ const content = renderSpec({
2988
+ widgetId,
2989
+ criteria,
2990
+ fixtureImport
2991
+ });
2992
+ fs6.mkdirSync(path7.dirname(outputPath), { recursive: true });
2993
+ fs6.writeFileSync(outputPath, content, "utf8");
2994
+ return { outputPath, written: true, skipped: false };
2995
+ }
2996
+ function renderSpec(args) {
2997
+ const lines = [];
2998
+ lines.push(
2999
+ `import { test, expect } from ${JSON.stringify(args.fixtureImport)}`
3000
+ );
3001
+ lines.push("");
3002
+ lines.push(
3003
+ `test.describe(${JSON.stringify(args.widgetId)}, { tag: "@uidex:flow" }, () => {`
3004
+ );
3005
+ if (args.criteria.length === 0) {
3006
+ lines.push(` test("TODO: add acceptance criteria", async () => {`);
3007
+ lines.push(` // TODO`);
3008
+ lines.push(` })`);
3009
+ } else {
3010
+ for (const criterion of args.criteria) {
3011
+ lines.push(` test(${JSON.stringify(criterion)}, async ({ uidex }) => {`);
3012
+ lines.push(` // TODO: implement criterion`);
3013
+ lines.push(` void uidex`);
3014
+ lines.push(` expect(true).toBe(true)`);
3015
+ lines.push(` })`);
3016
+ lines.push("");
3017
+ }
3018
+ }
3019
+ lines.push("})");
3020
+ lines.push("");
3021
+ return lines.join("\n");
3022
+ }
3023
+
3024
+ // src/scan/cli.ts
3025
+ function parseFlags2(args) {
3026
+ const positional = [];
3027
+ const flags = {};
3028
+ for (let i = 0; i < args.length; i++) {
3029
+ const a = args[i];
3030
+ if (a.startsWith("--")) {
3031
+ const eq = a.indexOf("=");
3032
+ if (eq !== -1) {
3033
+ flags[a.slice(2, eq)] = a.slice(eq + 1);
3034
+ } else {
3035
+ const next = args[i + 1];
3036
+ if (next && !next.startsWith("--")) {
3037
+ flags[a.slice(2)] = next;
3038
+ i++;
3039
+ } else {
3040
+ flags[a.slice(2)] = true;
3041
+ }
3042
+ }
3043
+ } else {
3044
+ positional.push(a);
3045
+ }
3046
+ }
3047
+ return { positional, flags };
3048
+ }
3049
+ async function run(opts) {
3050
+ const cwd = opts.cwd ?? process.cwd();
3051
+ const { positional, flags } = parseFlags2(opts.argv);
3052
+ const command = positional[0] ?? "help";
3053
+ const writer = createWriter();
3054
+ try {
3055
+ switch (command) {
3056
+ case "help":
3057
+ case "--help":
3058
+ case "-h":
3059
+ writer.out(helpText2());
3060
+ return writer.result(0);
3061
+ case "init":
3062
+ return runInit(cwd, writer);
3063
+ case "scan":
3064
+ return runScanCommand(cwd, flags, writer);
3065
+ case "scaffold":
3066
+ return runScaffold(cwd, positional.slice(1), flags, writer);
3067
+ case "ai": {
3068
+ const result = await runAiCommand({
3069
+ cwd,
3070
+ argv: opts.argv.slice(1)
3071
+ });
3072
+ if (result.stdout) writer.out(result.stdout.replace(/\n$/, ""));
3073
+ if (result.stderr) writer.err(result.stderr.replace(/\n$/, ""));
3074
+ return writer.result(result.exitCode);
3075
+ }
3076
+ default:
3077
+ writer.err(`Unknown command: ${command}`);
3078
+ writer.err(helpText2());
3079
+ return writer.result(1);
3080
+ }
3081
+ } catch (e) {
3082
+ writer.err(e instanceof Error ? e.message : String(e));
3083
+ return writer.result(1);
3084
+ }
3085
+ }
3086
+ function helpText2() {
3087
+ return [
3088
+ "uidex \u2014 scanner CLI",
3089
+ "",
3090
+ "Commands:",
3091
+ " init Create a .uidex.json",
3092
+ " scan [flags] Run the scanner pipeline",
3093
+ " scaffold widget <id> Emit a Playwright spec from a widget's acceptance",
3094
+ " ai <install|uninstall|providers> Manage AI assistant integrations",
3095
+ "",
3096
+ "Flags:",
3097
+ " --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
3098
+ " --lint Run lint diagnostics (missing annotations, scope leak, legacy JSDoc)",
3099
+ " --audit Equivalent to --check --lint (read-only)",
3100
+ " --json Emit JSON diagnostics on stdout",
3101
+ " --force (scaffold) overwrite existing spec",
3102
+ ""
3103
+ ].join("\n");
3104
+ }
3105
+ function runInit(cwd, w) {
3106
+ const configPath = path8.join(cwd, CONFIG_FILENAME);
3107
+ if (fs7.existsSync(configPath)) {
3108
+ w.err(`.uidex.json already exists at ${configPath}`);
3109
+ return w.result(1);
3110
+ }
3111
+ const config = {
3112
+ $schema: "https://uidex.dev/schema/v2.json",
3113
+ sources: [{ rootDir: "src" }],
3114
+ output: "src/uidex.gen.ts"
3115
+ };
3116
+ fs7.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
3117
+ w.out(`Created ${configPath}`);
3118
+ const gitignorePath = path8.join(cwd, ".gitignore");
3119
+ const entry = "*.gen.ts";
3120
+ if (fs7.existsSync(gitignorePath)) {
3121
+ const existing = fs7.readFileSync(gitignorePath, "utf8");
3122
+ const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
3123
+ if (!hasEntry) {
3124
+ const needsNewline = existing.length > 0 && !existing.endsWith("\n");
3125
+ fs7.appendFileSync(
3126
+ gitignorePath,
3127
+ `${needsNewline ? "\n" : ""}${entry}
3128
+ `,
3129
+ "utf8"
3130
+ );
3131
+ w.out(`Appended ${entry} to ${gitignorePath}`);
3132
+ }
3133
+ } else {
3134
+ fs7.writeFileSync(gitignorePath, `${entry}
3135
+ `, "utf8");
3136
+ w.out(`Created ${gitignorePath} with ${entry}`);
3137
+ }
3138
+ return w.result(0);
3139
+ }
3140
+ function runScanCommand(cwd, flags, w) {
3141
+ const check = Boolean(flags.check || flags.audit);
3142
+ const lint = Boolean(flags.lint || flags.audit);
3143
+ const asJson = Boolean(flags.json);
3144
+ const configs = discover({ cwd });
3145
+ if (configs.length === 0) {
3146
+ w.err(`No ${CONFIG_FILENAME} found under ${cwd}`);
3147
+ return w.result(1);
3148
+ }
3149
+ const results = runScan({ cwd, check, lint, configs });
3150
+ if (!check) {
3151
+ for (const r of results) writeScanResult(r);
3152
+ }
3153
+ const allDiagnostics = results.flatMap((r) => r.audit?.diagnostics ?? []);
3154
+ const summary = results.reduce(
3155
+ (acc, r) => {
3156
+ acc.errors += r.audit?.summary.errors ?? 0;
3157
+ acc.warnings += r.audit?.summary.warnings ?? 0;
3158
+ return acc;
3159
+ },
3160
+ { errors: 0, warnings: 0 }
3161
+ );
3162
+ if (asJson) {
3163
+ const out2 = { diagnostics: allDiagnostics, summary };
3164
+ w.out(JSON.stringify(out2, null, 2));
3165
+ } else {
3166
+ for (const r of results) {
3167
+ if (check) {
3168
+ w.out(`Checked ${r.outputPath}`);
3169
+ } else {
3170
+ w.out(`Wrote ${r.outputPath}`);
3171
+ }
3172
+ for (const d of r.audit?.diagnostics ?? []) {
3173
+ const loc = d.file ? `${d.file}${d.line ? `:${d.line}` : ""}` : "";
3174
+ const stream = d.severity === "error" ? w.err : w.out;
3175
+ stream(`${d.severity.toUpperCase()} [${d.code}] ${loc} ${d.message}`);
3176
+ if (d.hint) stream(` hint: ${d.hint}`);
3177
+ }
3178
+ }
3179
+ if (check || lint) {
3180
+ w.out(`${summary.errors} error(s), ${summary.warnings} warning(s)`);
3181
+ }
3182
+ }
3183
+ const exit = summary.errors > 0 ? 1 : 0;
3184
+ return w.result(exit);
3185
+ }
3186
+ function runScaffold(cwd, args, flags, w) {
3187
+ const [kind, id] = args;
3188
+ if (kind !== "widget" || !id) {
3189
+ w.err("Usage: uidex scaffold widget <id> [--force]");
3190
+ return w.result(1);
3191
+ }
3192
+ const results = runScan({ cwd });
3193
+ for (const r of results) {
3194
+ const widget = r.registry.get("widget", id);
3195
+ if (!widget) continue;
3196
+ const outDir = path8.resolve(r.configDir, "e2e");
3197
+ const result = scaffoldWidgetSpec({
3198
+ registry: r.registry,
3199
+ widgetId: id,
3200
+ outDir,
3201
+ force: Boolean(flags.force)
3202
+ });
3203
+ if (result.skipped) {
3204
+ w.err(result.reason ?? "skipped");
3205
+ return w.result(1);
3206
+ }
3207
+ w.out(`Wrote ${result.outputPath}`);
3208
+ return w.result(0);
3209
+ }
3210
+ w.err(`Widget "${id}" not found in registry`);
3211
+ return w.result(1);
3212
+ }
3213
+ function createWriter() {
3214
+ let stdout = "";
3215
+ let stderr = "";
3216
+ return {
3217
+ out(msg) {
3218
+ stdout += msg + "\n";
3219
+ },
3220
+ err(msg) {
3221
+ stderr += msg + "\n";
3222
+ },
3223
+ result(exitCode) {
3224
+ return { exitCode, stdout, stderr };
3225
+ }
3226
+ };
3227
+ }
3228
+
3229
+ // src/cli/cli.ts
3230
+ run({ argv: process.argv.slice(2) }).then(
3231
+ (result) => {
3232
+ if (result.stdout) process.stdout.write(result.stdout);
3233
+ if (result.stderr) process.stderr.write(result.stderr);
3234
+ process.exit(result.exitCode);
3235
+ },
3236
+ (error) => {
3237
+ process.stderr.write(
3238
+ (error instanceof Error ? error.message : String(error)) + "\n"
3239
+ );
3240
+ process.exit(1);
3241
+ }
3242
+ );
3243
+ //# sourceMappingURL=cli.cjs.map