uidex 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.cjs +1112 -1054
- package/dist/cli/cli.cjs.map +1 -1
- package/dist/headless/index.cjs +4 -448
- package/dist/headless/index.cjs.map +1 -1
- package/dist/headless/index.d.cts +41 -11
- package/dist/headless/index.d.ts +41 -11
- package/dist/headless/index.js +4 -450
- package/dist/headless/index.js.map +1 -1
- package/dist/index.cjs +147 -3252
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -316
- package/dist/index.d.ts +43 -316
- package/dist/index.js +133 -3254
- package/dist/index.js.map +1 -1
- package/dist/playwright/index.cjs +175 -0
- package/dist/playwright/index.cjs.map +1 -1
- package/dist/playwright/index.d.cts +2 -0
- package/dist/playwright/index.d.ts +2 -0
- package/dist/playwright/index.js +167 -0
- package/dist/playwright/index.js.map +1 -1
- package/dist/playwright/states-reporter.cjs +123 -0
- package/dist/playwright/states-reporter.cjs.map +1 -0
- package/dist/playwright/states-reporter.d.cts +46 -0
- package/dist/playwright/states-reporter.d.ts +46 -0
- package/dist/playwright/states-reporter.js +88 -0
- package/dist/playwright/states-reporter.js.map +1 -0
- package/dist/playwright/states.cjs +118 -0
- package/dist/playwright/states.cjs.map +1 -0
- package/dist/playwright/states.d.cts +120 -0
- package/dist/playwright/states.d.ts +120 -0
- package/dist/playwright/states.js +88 -0
- package/dist/playwright/states.js.map +1 -0
- package/dist/react/index.cjs +163 -3255
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +62 -275
- package/dist/react/index.d.ts +62 -275
- package/dist/react/index.js +151 -3268
- package/dist/react/index.js.map +1 -1
- package/dist/scan/index.cjs +1292 -345
- package/dist/scan/index.cjs.map +1 -1
- package/dist/scan/index.d.cts +305 -12
- package/dist/scan/index.d.ts +305 -12
- package/dist/scan/index.js +1283 -345
- package/dist/scan/index.js.map +1 -1
- package/package.json +12 -16
- package/dist/cloud/index.cjs +0 -682
- package/dist/cloud/index.cjs.map +0 -1
- package/dist/cloud/index.d.cts +0 -270
- package/dist/cloud/index.d.ts +0 -270
- package/dist/cloud/index.js +0 -645
- package/dist/cloud/index.js.map +0 -1
package/dist/scan/index.js
CHANGED
|
@@ -28,7 +28,9 @@ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
28
28
|
"output",
|
|
29
29
|
"flows",
|
|
30
30
|
"audit",
|
|
31
|
-
"conventions"
|
|
31
|
+
"conventions",
|
|
32
|
+
"statesManifest",
|
|
33
|
+
"coverageBaseline"
|
|
32
34
|
]);
|
|
33
35
|
var ALLOWED_SOURCE_KEYS = /* @__PURE__ */ new Set(["rootDir", "include", "exclude", "prefix"]);
|
|
34
36
|
var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -38,7 +40,14 @@ var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
|
|
|
38
40
|
"flows",
|
|
39
41
|
"regions"
|
|
40
42
|
]);
|
|
41
|
-
var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
|
|
43
|
+
var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
|
|
44
|
+
"scopeLeak",
|
|
45
|
+
"coverage",
|
|
46
|
+
"acceptance",
|
|
47
|
+
"states",
|
|
48
|
+
"pageCoverage",
|
|
49
|
+
"stateMatrix"
|
|
50
|
+
]);
|
|
42
51
|
function fail(msg) {
|
|
43
52
|
throw new ConfigError(`Invalid .uidex.json: ${msg}`);
|
|
44
53
|
}
|
|
@@ -97,6 +106,12 @@ function validateConfig(raw) {
|
|
|
97
106
|
}
|
|
98
107
|
if (raw.exclude !== void 0) assertStringArray(raw.exclude, `exclude`);
|
|
99
108
|
if (raw.flows !== void 0) assertStringArray(raw.flows, `flows`);
|
|
109
|
+
if (raw.statesManifest !== void 0 && typeof raw.statesManifest !== "string") {
|
|
110
|
+
fail(`"statesManifest" must be a string`);
|
|
111
|
+
}
|
|
112
|
+
if (raw.coverageBaseline !== void 0 && typeof raw.coverageBaseline !== "string") {
|
|
113
|
+
fail(`"coverageBaseline" must be a string`);
|
|
114
|
+
}
|
|
100
115
|
if (raw.audit !== void 0) {
|
|
101
116
|
assertObject(raw.audit, "audit");
|
|
102
117
|
for (const key of Object.keys(raw.audit)) {
|
|
@@ -120,7 +135,13 @@ function validateConfig(raw) {
|
|
|
120
135
|
fail(`conventions.features must be a string or false`);
|
|
121
136
|
}
|
|
122
137
|
if (c.pages !== void 0 && c.pages !== false && c.pages !== "auto") {
|
|
123
|
-
|
|
138
|
+
const p2 = c.pages;
|
|
139
|
+
const routesDir = typeof p2 === "object" && p2 !== null && !Array.isArray(p2) ? p2.routesDir : void 0;
|
|
140
|
+
if (typeof p2 !== "object" || p2 === null || Array.isArray(p2) || Object.keys(p2).some((k) => k !== "routesDir") || typeof routesDir !== "string" || routesDir.length === 0) {
|
|
141
|
+
fail(
|
|
142
|
+
`conventions.pages must be "auto", false, or { routesDir: string }`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
124
145
|
}
|
|
125
146
|
if (c.flows !== void 0 && c.flows !== false) {
|
|
126
147
|
assertStringArray(c.flows, "conventions.flows");
|
|
@@ -136,7 +157,9 @@ function validateConfig(raw) {
|
|
|
136
157
|
output: raw.output,
|
|
137
158
|
flows: raw.flows,
|
|
138
159
|
audit: raw.audit,
|
|
139
|
-
conventions: raw.conventions
|
|
160
|
+
conventions: raw.conventions,
|
|
161
|
+
statesManifest: raw.statesManifest,
|
|
162
|
+
coverageBaseline: raw.coverageBaseline
|
|
140
163
|
};
|
|
141
164
|
}
|
|
142
165
|
function parseConfig(json) {
|
|
@@ -225,7 +248,12 @@ var BASE_EXCLUDES = [
|
|
|
225
248
|
"**/dist/**",
|
|
226
249
|
"**/build/**",
|
|
227
250
|
"**/.next/**",
|
|
228
|
-
|
|
251
|
+
// Exclude the scanner's own generated siblings so a scan never re-ingests its
|
|
252
|
+
// output. Enumerated (not `*.gen.*`) so a hand-written file that merely contains
|
|
253
|
+
// a `.gen.` segment, e.g. `foo.gen.helpers.ts`, is still scanned.
|
|
254
|
+
"**/*.gen.ts",
|
|
255
|
+
"**/*.gen.data.ts",
|
|
256
|
+
"**/*.gen.locs.ts"
|
|
229
257
|
];
|
|
230
258
|
var TEST_EXCLUDES = [
|
|
231
259
|
"**/*.test.ts",
|
|
@@ -425,6 +453,201 @@ function unwrapTsExpression(expr) {
|
|
|
425
453
|
}
|
|
426
454
|
}
|
|
427
455
|
|
|
456
|
+
// src/shared/entities/types.ts
|
|
457
|
+
var ENTITY_KINDS = [
|
|
458
|
+
"route",
|
|
459
|
+
"page",
|
|
460
|
+
"feature",
|
|
461
|
+
"widget",
|
|
462
|
+
"region",
|
|
463
|
+
"element",
|
|
464
|
+
"primitive",
|
|
465
|
+
"flow"
|
|
466
|
+
];
|
|
467
|
+
var CORE_STATE_KINDS = [
|
|
468
|
+
"loading",
|
|
469
|
+
"empty",
|
|
470
|
+
"populated",
|
|
471
|
+
"error"
|
|
472
|
+
];
|
|
473
|
+
function isMetaKind(kind) {
|
|
474
|
+
return kind !== "route" && kind !== "flow";
|
|
475
|
+
}
|
|
476
|
+
function isMetaEntity(entity) {
|
|
477
|
+
return isMetaKind(entity.kind);
|
|
478
|
+
}
|
|
479
|
+
function entityKey(entity) {
|
|
480
|
+
return entity.kind === "route" ? entity.path : entity.id;
|
|
481
|
+
}
|
|
482
|
+
var UnknownEntityKindError = class extends Error {
|
|
483
|
+
kind;
|
|
484
|
+
constructor(kind) {
|
|
485
|
+
super(`Unknown entity kind: ${kind}`);
|
|
486
|
+
this.name = "UnknownEntityKindError";
|
|
487
|
+
this.kind = kind;
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
var KIND_SET = new Set(ENTITY_KINDS);
|
|
491
|
+
function assertEntityKind(kind) {
|
|
492
|
+
if (!KIND_SET.has(kind)) throw new UnknownEntityKindError(kind);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/shared/entities/registry.ts
|
|
496
|
+
function emptyStore() {
|
|
497
|
+
return {
|
|
498
|
+
route: /* @__PURE__ */ new Map(),
|
|
499
|
+
page: /* @__PURE__ */ new Map(),
|
|
500
|
+
feature: /* @__PURE__ */ new Map(),
|
|
501
|
+
widget: /* @__PURE__ */ new Map(),
|
|
502
|
+
region: /* @__PURE__ */ new Map(),
|
|
503
|
+
element: /* @__PURE__ */ new Map(),
|
|
504
|
+
primitive: /* @__PURE__ */ new Map(),
|
|
505
|
+
flow: /* @__PURE__ */ new Map()
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function computeFlowIds(flows, targetId) {
|
|
509
|
+
const ids = [];
|
|
510
|
+
for (const flow of flows) {
|
|
511
|
+
if (flow.touches.includes(targetId)) ids.push(flow.id);
|
|
512
|
+
}
|
|
513
|
+
return ids;
|
|
514
|
+
}
|
|
515
|
+
function freezeEntity(entity, flows) {
|
|
516
|
+
if (!isMetaKind(entity.kind)) return entity;
|
|
517
|
+
const withMeta = entity;
|
|
518
|
+
if (withMeta.meta === void 0) return entity;
|
|
519
|
+
const computedFlows = Object.freeze(computeFlowIds(flows, withMeta.id));
|
|
520
|
+
const mergedMeta = { ...withMeta.meta, flows: computedFlows };
|
|
521
|
+
return { ...entity, meta: Object.freeze(mergedMeta) };
|
|
522
|
+
}
|
|
523
|
+
function createRegistry() {
|
|
524
|
+
const store = emptyStore();
|
|
525
|
+
let flowsCache = null;
|
|
526
|
+
const patternCache = /* @__PURE__ */ new Map();
|
|
527
|
+
const getFlows = () => {
|
|
528
|
+
if (flowsCache === null) flowsCache = Array.from(store.flow.values());
|
|
529
|
+
return flowsCache;
|
|
530
|
+
};
|
|
531
|
+
const add = (entity) => {
|
|
532
|
+
assertEntityKind(entity.kind);
|
|
533
|
+
const key = entityKey(entity);
|
|
534
|
+
store[entity.kind].set(key, entity);
|
|
535
|
+
flowsCache = null;
|
|
536
|
+
patternCache.delete(entity.kind);
|
|
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 getPatternsForKind = (kind) => {
|
|
545
|
+
const cached = patternCache.get(kind);
|
|
546
|
+
if (cached !== void 0) return cached;
|
|
547
|
+
const patterns = [];
|
|
548
|
+
for (const [key, entity] of store[kind]) {
|
|
549
|
+
if (key.includes("*")) {
|
|
550
|
+
const segments = key.split("*");
|
|
551
|
+
patterns.push({
|
|
552
|
+
segments,
|
|
553
|
+
staticLength: segments.reduce((n, s) => n + s.length, 0),
|
|
554
|
+
entity
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
patternCache.set(
|
|
559
|
+
kind,
|
|
560
|
+
patterns
|
|
561
|
+
);
|
|
562
|
+
return patterns;
|
|
563
|
+
};
|
|
564
|
+
const matchesSegments = (segments, id) => {
|
|
565
|
+
const first = segments[0];
|
|
566
|
+
const last = segments[segments.length - 1];
|
|
567
|
+
if (!id.startsWith(first)) return false;
|
|
568
|
+
let pos = first.length;
|
|
569
|
+
for (let i = 1; i < segments.length - 1; i++) {
|
|
570
|
+
const idx = id.indexOf(segments[i], pos);
|
|
571
|
+
if (idx === -1) return false;
|
|
572
|
+
pos = idx + segments[i].length;
|
|
573
|
+
}
|
|
574
|
+
return id.endsWith(last) && id.length - last.length >= pos;
|
|
575
|
+
};
|
|
576
|
+
const matchPattern = (kind, id) => {
|
|
577
|
+
assertEntityKind(kind);
|
|
578
|
+
const patterns = getPatternsForKind(kind);
|
|
579
|
+
if (patterns.length === 0) return void 0;
|
|
580
|
+
let best;
|
|
581
|
+
for (const entry of patterns) {
|
|
582
|
+
if (matchesSegments(entry.segments, id) && (best === void 0 || entry.staticLength > best.staticLength)) {
|
|
583
|
+
best = entry;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (best === void 0) return void 0;
|
|
587
|
+
return freezeEntity(best.entity, getFlows());
|
|
588
|
+
};
|
|
589
|
+
const list = (kind) => {
|
|
590
|
+
assertEntityKind(kind);
|
|
591
|
+
const flows = getFlows();
|
|
592
|
+
return Array.from(
|
|
593
|
+
store[kind].values(),
|
|
594
|
+
(e) => freezeEntity(e, flows)
|
|
595
|
+
);
|
|
596
|
+
};
|
|
597
|
+
const allEntities = function* () {
|
|
598
|
+
for (const kind of Object.keys(store)) {
|
|
599
|
+
for (const entity of store[kind].values()) {
|
|
600
|
+
yield entity;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
const query = (predicate) => {
|
|
605
|
+
const flows = getFlows();
|
|
606
|
+
const result = [];
|
|
607
|
+
for (const entity of allEntities()) {
|
|
608
|
+
if (predicate(entity)) result.push(freezeEntity(entity, flows));
|
|
609
|
+
}
|
|
610
|
+
return result;
|
|
611
|
+
};
|
|
612
|
+
const byScope = (scope) => query(
|
|
613
|
+
(entity) => "scopes" in entity && Array.isArray(entity.scopes) && entity.scopes.includes(scope)
|
|
614
|
+
);
|
|
615
|
+
const touchedBy = (flowId) => {
|
|
616
|
+
const flow = store.flow.get(flowId);
|
|
617
|
+
if (flow === void 0) return [];
|
|
618
|
+
const ids = new Set(flow.touches);
|
|
619
|
+
return query((entity) => {
|
|
620
|
+
if (!isMetaEntity(entity)) return false;
|
|
621
|
+
return ids.has(entity.id);
|
|
622
|
+
});
|
|
623
|
+
};
|
|
624
|
+
const reports = /* @__PURE__ */ new Map();
|
|
625
|
+
const reportsCbs = /* @__PURE__ */ new Set();
|
|
626
|
+
const setReports = (kind, id, records) => {
|
|
627
|
+
reports.set(`${kind}:${id}`, records);
|
|
628
|
+
for (const cb of reportsCbs) cb();
|
|
629
|
+
};
|
|
630
|
+
const getReports = (kind, id) => reports.get(`${kind}:${id}`) ?? [];
|
|
631
|
+
const listReportKeys = () => Array.from(reports.keys());
|
|
632
|
+
const onReportsChange = (cb) => {
|
|
633
|
+
reportsCbs.add(cb);
|
|
634
|
+
return () => reportsCbs.delete(cb);
|
|
635
|
+
};
|
|
636
|
+
return {
|
|
637
|
+
add,
|
|
638
|
+
get,
|
|
639
|
+
matchPattern,
|
|
640
|
+
list,
|
|
641
|
+
query,
|
|
642
|
+
byScope,
|
|
643
|
+
touchedBy,
|
|
644
|
+
setReports,
|
|
645
|
+
getReports,
|
|
646
|
+
listReportKeys,
|
|
647
|
+
onReportsChange
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
428
651
|
// src/scanner/scan/extract-uidex-export.ts
|
|
429
652
|
var KIND_DISCRIMINATORS = [
|
|
430
653
|
"page",
|
|
@@ -442,20 +665,35 @@ var ALLOWED_FIELDS = {
|
|
|
442
665
|
"features",
|
|
443
666
|
"widgets",
|
|
444
667
|
"acceptance",
|
|
668
|
+
"states",
|
|
669
|
+
"capture",
|
|
670
|
+
"waivers",
|
|
445
671
|
"description"
|
|
446
672
|
]),
|
|
673
|
+
// `capture` / `waivers` are route-scoped, and only a page maps to a route — so
|
|
674
|
+
// they are page-only (the gates read them off the page). A feature/widget that
|
|
675
|
+
// sets them gets an `uidex-export-unknown-field` error rather than a silent no-op.
|
|
447
676
|
feature: /* @__PURE__ */ new Set([
|
|
448
677
|
"feature",
|
|
449
678
|
"name",
|
|
450
679
|
"features",
|
|
451
680
|
"acceptance",
|
|
681
|
+
"states",
|
|
452
682
|
"description"
|
|
453
683
|
]),
|
|
454
684
|
primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
|
|
455
|
-
widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "description"]),
|
|
685
|
+
widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "states", "description"]),
|
|
456
686
|
region: /* @__PURE__ */ new Set(["region", "name", "description"]),
|
|
457
687
|
flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
|
|
458
688
|
};
|
|
689
|
+
var STATE_FIELDS = /* @__PURE__ */ new Set([
|
|
690
|
+
"name",
|
|
691
|
+
"kind",
|
|
692
|
+
"acceptance",
|
|
693
|
+
"description"
|
|
694
|
+
]);
|
|
695
|
+
var CORE_STATE_KINDS2 = new Set(CORE_STATE_KINDS);
|
|
696
|
+
var STATE_KINDS = /* @__PURE__ */ new Set([...CORE_STATE_KINDS, "variant"]);
|
|
459
697
|
var FALSEABLE = /* @__PURE__ */ new Set([
|
|
460
698
|
"page",
|
|
461
699
|
"feature",
|
|
@@ -845,6 +1083,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
845
1083
|
}
|
|
846
1084
|
const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
847
1085
|
const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
1086
|
+
const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey) : void 0;
|
|
1087
|
+
const capture = kind === "page" ? readBooleanField(byKey, "capture") : void 0;
|
|
1088
|
+
const waivers = kind === "page" ? readWaiversField(byKey) : void 0;
|
|
848
1089
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
849
1090
|
const metadata = {
|
|
850
1091
|
source: "ts-export",
|
|
@@ -867,6 +1108,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
867
1108
|
metadata.widgets = widgetsField.values;
|
|
868
1109
|
metadata.widgetSpans = widgetsField.spans;
|
|
869
1110
|
}
|
|
1111
|
+
if (states?.length) metadata.states = states;
|
|
1112
|
+
if (capture !== void 0) metadata.capture = capture;
|
|
1113
|
+
if (waivers && Object.keys(waivers).length > 0) metadata.waivers = waivers;
|
|
870
1114
|
if (notFlow) metadata.notFlow = true;
|
|
871
1115
|
if (typeof id === "string" && idValue.kind === "string") {
|
|
872
1116
|
metadata.idSpan = idValue.span;
|
|
@@ -903,6 +1147,48 @@ function readIdField(value, kind, fieldName) {
|
|
|
903
1147
|
value.pos
|
|
904
1148
|
);
|
|
905
1149
|
}
|
|
1150
|
+
function readWaiversField(byKey) {
|
|
1151
|
+
const entry = byKey.get("waivers");
|
|
1152
|
+
if (!entry) return void 0;
|
|
1153
|
+
if (entry.value.kind !== "object") {
|
|
1154
|
+
throw new ExtractError(
|
|
1155
|
+
"uidex-export-invalid-field",
|
|
1156
|
+
"`waivers` must be an object mapping a core kind to a reason.",
|
|
1157
|
+
entry.value.pos
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
const waivers = {};
|
|
1161
|
+
for (const e of entry.value.entries) {
|
|
1162
|
+
if (!CORE_STATE_KINDS2.has(e.key)) {
|
|
1163
|
+
throw new ExtractError(
|
|
1164
|
+
"uidex-export-invalid-field",
|
|
1165
|
+
`waiver key "${e.key}" is not a core kind; only ${[...CORE_STATE_KINDS2].join(", ")} participate in the matrix.`,
|
|
1166
|
+
e.keyPos
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
if (e.value.kind !== "string" || e.value.value.length === 0) {
|
|
1170
|
+
throw new ExtractError(
|
|
1171
|
+
"uidex-export-invalid-field",
|
|
1172
|
+
`waiver "${e.key}" needs a non-empty reason (why the route cannot render this kind).`,
|
|
1173
|
+
e.value.pos
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
waivers[e.key] = e.value.value;
|
|
1177
|
+
}
|
|
1178
|
+
return waivers;
|
|
1179
|
+
}
|
|
1180
|
+
function readBooleanField(byKey, name) {
|
|
1181
|
+
const entry = byKey.get(name);
|
|
1182
|
+
if (!entry) return void 0;
|
|
1183
|
+
if (entry.value.kind !== "boolean") {
|
|
1184
|
+
throw new ExtractError(
|
|
1185
|
+
"uidex-export-invalid-field",
|
|
1186
|
+
`\`${name}\` must be a boolean.`,
|
|
1187
|
+
entry.value.pos
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
return entry.value.value;
|
|
1191
|
+
}
|
|
906
1192
|
function readStringField(byKey, name) {
|
|
907
1193
|
const entry = byKey.get(name);
|
|
908
1194
|
if (!entry) return void 0;
|
|
@@ -940,6 +1226,86 @@ function readStringArrayField(byKey, name) {
|
|
|
940
1226
|
}
|
|
941
1227
|
return { values, spans };
|
|
942
1228
|
}
|
|
1229
|
+
function readStatesField(byKey) {
|
|
1230
|
+
const entry = byKey.get("states");
|
|
1231
|
+
if (!entry) return void 0;
|
|
1232
|
+
if (entry.value.kind !== "array") {
|
|
1233
|
+
throw new ExtractError(
|
|
1234
|
+
"uidex-export-invalid-field",
|
|
1235
|
+
"`states` must be an array of state names or `{ name, acceptance?, description? }` objects.",
|
|
1236
|
+
entry.value.pos
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
const states = [];
|
|
1240
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1241
|
+
for (const item of entry.value.items) {
|
|
1242
|
+
let state;
|
|
1243
|
+
if (item.kind === "string") {
|
|
1244
|
+
state = { name: item.value, nameSpan: item.span };
|
|
1245
|
+
} else if (item.kind === "object") {
|
|
1246
|
+
const fieldByKey = /* @__PURE__ */ new Map();
|
|
1247
|
+
for (const e of item.entries) fieldByKey.set(e.key, e);
|
|
1248
|
+
for (const e of item.entries) {
|
|
1249
|
+
if (!STATE_FIELDS.has(e.key)) {
|
|
1250
|
+
throw new ExtractError(
|
|
1251
|
+
"uidex-export-invalid-field",
|
|
1252
|
+
`Unknown field "${e.key}" in a \`states\` entry. Allowed: ${Array.from(
|
|
1253
|
+
STATE_FIELDS
|
|
1254
|
+
).sort().join(", ")}.`,
|
|
1255
|
+
e.value.pos
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
const nameEntry = fieldByKey.get("name");
|
|
1260
|
+
if (!nameEntry || nameEntry.value.kind !== "string") {
|
|
1261
|
+
throw new ExtractError(
|
|
1262
|
+
"uidex-export-invalid-field",
|
|
1263
|
+
"Each object `states` entry must have a string `name`.",
|
|
1264
|
+
item.pos
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
state = { name: nameEntry.value.value, nameSpan: nameEntry.value.span };
|
|
1268
|
+
const kind = readStringField(fieldByKey, "kind");
|
|
1269
|
+
if (kind !== void 0) {
|
|
1270
|
+
if (!STATE_KINDS.has(kind)) {
|
|
1271
|
+
throw new ExtractError(
|
|
1272
|
+
"uidex-export-invalid-field",
|
|
1273
|
+
`state \`kind\` must be one of ${[...STATE_KINDS].join(", ")} (got "${kind}").`,
|
|
1274
|
+
fieldByKey.get("kind").value.pos
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
state.kind = kind;
|
|
1278
|
+
}
|
|
1279
|
+
const acceptance = readStringArrayField(fieldByKey, "acceptance")?.values;
|
|
1280
|
+
if (acceptance) state.acceptance = acceptance;
|
|
1281
|
+
const description = readStringField(fieldByKey, "description");
|
|
1282
|
+
if (description) state.description = description;
|
|
1283
|
+
} else {
|
|
1284
|
+
throw new ExtractError(
|
|
1285
|
+
"uidex-export-invalid-field",
|
|
1286
|
+
"`states` items must be strings or `{ name, ... }` objects.",
|
|
1287
|
+
item.pos
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
if (state.name.length === 0) {
|
|
1291
|
+
throw new ExtractError(
|
|
1292
|
+
"uidex-export-invalid-field",
|
|
1293
|
+
"A `states` entry has an empty `name`.",
|
|
1294
|
+
item.pos
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
if (seen.has(state.name)) {
|
|
1298
|
+
throw new ExtractError(
|
|
1299
|
+
"uidex-export-duplicate-state",
|
|
1300
|
+
`Duplicate state "${state.name}" in \`states\`; each state name must be unique within the entity.`,
|
|
1301
|
+
item.pos
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
seen.add(state.name);
|
|
1305
|
+
states.push(state);
|
|
1306
|
+
}
|
|
1307
|
+
return states;
|
|
1308
|
+
}
|
|
943
1309
|
function posAt(content, offset, p2) {
|
|
944
1310
|
const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
|
|
945
1311
|
return { offset, line: p2.lineAt(offset), column: offset - lineStart + 1 };
|
|
@@ -1431,231 +1797,55 @@ function collectImportFacts(parsed) {
|
|
|
1431
1797
|
for (const spec of stmt.specifiers ?? []) {
|
|
1432
1798
|
const local = spec.local;
|
|
1433
1799
|
if (local && local.type === "Identifier") {
|
|
1434
|
-
names.push(String(local.name));
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
} else if ((stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportAllDeclaration") && stmt.source) {
|
|
1438
|
-
source = stmt.source;
|
|
1439
|
-
isTypeOnly = stmt.exportKind === "type";
|
|
1440
|
-
} else {
|
|
1441
|
-
continue;
|
|
1442
|
-
}
|
|
1443
|
-
if (!source || source.type !== "Literal") continue;
|
|
1444
|
-
if (typeof source.value !== "string") continue;
|
|
1445
|
-
out2.push({
|
|
1446
|
-
specifier: source.value,
|
|
1447
|
-
line: parsed.lineAt(stmt.start),
|
|
1448
|
-
span: { start: stmt.start, end: stmt.end },
|
|
1449
|
-
isTypeOnly,
|
|
1450
|
-
names
|
|
1451
|
-
});
|
|
1452
|
-
}
|
|
1453
|
-
return out2;
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
// src/scanner/scan/resolve.ts
|
|
1457
|
-
import * as path4 from "path";
|
|
1458
|
-
|
|
1459
|
-
// src/shared/entities/types.ts
|
|
1460
|
-
var ENTITY_KINDS = [
|
|
1461
|
-
"route",
|
|
1462
|
-
"page",
|
|
1463
|
-
"feature",
|
|
1464
|
-
"widget",
|
|
1465
|
-
"region",
|
|
1466
|
-
"element",
|
|
1467
|
-
"primitive",
|
|
1468
|
-
"flow"
|
|
1469
|
-
];
|
|
1470
|
-
function isMetaKind(kind) {
|
|
1471
|
-
return kind !== "route" && kind !== "flow";
|
|
1472
|
-
}
|
|
1473
|
-
function isMetaEntity(entity) {
|
|
1474
|
-
return isMetaKind(entity.kind);
|
|
1475
|
-
}
|
|
1476
|
-
function entityKey(entity) {
|
|
1477
|
-
return entity.kind === "route" ? entity.path : entity.id;
|
|
1478
|
-
}
|
|
1479
|
-
var UnknownEntityKindError = class extends Error {
|
|
1480
|
-
kind;
|
|
1481
|
-
constructor(kind) {
|
|
1482
|
-
super(`Unknown entity kind: ${kind}`);
|
|
1483
|
-
this.name = "UnknownEntityKindError";
|
|
1484
|
-
this.kind = kind;
|
|
1485
|
-
}
|
|
1486
|
-
};
|
|
1487
|
-
var KIND_SET = new Set(ENTITY_KINDS);
|
|
1488
|
-
function assertEntityKind(kind) {
|
|
1489
|
-
if (!KIND_SET.has(kind)) throw new UnknownEntityKindError(kind);
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
// src/shared/entities/registry.ts
|
|
1493
|
-
function emptyStore() {
|
|
1494
|
-
return {
|
|
1495
|
-
route: /* @__PURE__ */ new Map(),
|
|
1496
|
-
page: /* @__PURE__ */ new Map(),
|
|
1497
|
-
feature: /* @__PURE__ */ new Map(),
|
|
1498
|
-
widget: /* @__PURE__ */ new Map(),
|
|
1499
|
-
region: /* @__PURE__ */ new Map(),
|
|
1500
|
-
element: /* @__PURE__ */ new Map(),
|
|
1501
|
-
primitive: /* @__PURE__ */ new Map(),
|
|
1502
|
-
flow: /* @__PURE__ */ new Map()
|
|
1503
|
-
};
|
|
1504
|
-
}
|
|
1505
|
-
function computeFlowIds(flows, targetId) {
|
|
1506
|
-
const ids = [];
|
|
1507
|
-
for (const flow of flows) {
|
|
1508
|
-
if (flow.touches.includes(targetId)) ids.push(flow.id);
|
|
1509
|
-
}
|
|
1510
|
-
return ids;
|
|
1511
|
-
}
|
|
1512
|
-
function freezeEntity(entity, flows) {
|
|
1513
|
-
if (!isMetaKind(entity.kind)) return entity;
|
|
1514
|
-
const withMeta = entity;
|
|
1515
|
-
if (withMeta.meta === void 0) return entity;
|
|
1516
|
-
const computedFlows = Object.freeze(computeFlowIds(flows, withMeta.id));
|
|
1517
|
-
const mergedMeta = { ...withMeta.meta, flows: computedFlows };
|
|
1518
|
-
return { ...entity, meta: Object.freeze(mergedMeta) };
|
|
1519
|
-
}
|
|
1520
|
-
function createRegistry() {
|
|
1521
|
-
const store = emptyStore();
|
|
1522
|
-
let flowsCache = null;
|
|
1523
|
-
const patternCache = /* @__PURE__ */ new Map();
|
|
1524
|
-
const getFlows = () => {
|
|
1525
|
-
if (flowsCache === null) flowsCache = Array.from(store.flow.values());
|
|
1526
|
-
return flowsCache;
|
|
1527
|
-
};
|
|
1528
|
-
const add = (entity) => {
|
|
1529
|
-
assertEntityKind(entity.kind);
|
|
1530
|
-
const key = entityKey(entity);
|
|
1531
|
-
store[entity.kind].set(key, entity);
|
|
1532
|
-
flowsCache = null;
|
|
1533
|
-
patternCache.delete(entity.kind);
|
|
1534
|
-
};
|
|
1535
|
-
const get = (kind, id) => {
|
|
1536
|
-
assertEntityKind(kind);
|
|
1537
|
-
const raw = store[kind].get(id);
|
|
1538
|
-
if (raw === void 0) return void 0;
|
|
1539
|
-
return freezeEntity(raw, getFlows());
|
|
1540
|
-
};
|
|
1541
|
-
const getPatternsForKind = (kind) => {
|
|
1542
|
-
const cached = patternCache.get(kind);
|
|
1543
|
-
if (cached !== void 0) return cached;
|
|
1544
|
-
const patterns = [];
|
|
1545
|
-
for (const [key, entity] of store[kind]) {
|
|
1546
|
-
if (key.includes("*")) {
|
|
1547
|
-
const segments = key.split("*");
|
|
1548
|
-
patterns.push({
|
|
1549
|
-
segments,
|
|
1550
|
-
staticLength: segments.reduce((n, s) => n + s.length, 0),
|
|
1551
|
-
entity
|
|
1552
|
-
});
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
patternCache.set(
|
|
1556
|
-
kind,
|
|
1557
|
-
patterns
|
|
1558
|
-
);
|
|
1559
|
-
return patterns;
|
|
1560
|
-
};
|
|
1561
|
-
const matchesSegments = (segments, id) => {
|
|
1562
|
-
const first = segments[0];
|
|
1563
|
-
const last = segments[segments.length - 1];
|
|
1564
|
-
if (!id.startsWith(first)) return false;
|
|
1565
|
-
let pos = first.length;
|
|
1566
|
-
for (let i = 1; i < segments.length - 1; i++) {
|
|
1567
|
-
const idx = id.indexOf(segments[i], pos);
|
|
1568
|
-
if (idx === -1) return false;
|
|
1569
|
-
pos = idx + segments[i].length;
|
|
1570
|
-
}
|
|
1571
|
-
return id.endsWith(last) && id.length - last.length >= pos;
|
|
1572
|
-
};
|
|
1573
|
-
const matchPattern = (kind, id) => {
|
|
1574
|
-
assertEntityKind(kind);
|
|
1575
|
-
const patterns = getPatternsForKind(kind);
|
|
1576
|
-
if (patterns.length === 0) return void 0;
|
|
1577
|
-
let best;
|
|
1578
|
-
for (const entry of patterns) {
|
|
1579
|
-
if (matchesSegments(entry.segments, id) && (best === void 0 || entry.staticLength > best.staticLength)) {
|
|
1580
|
-
best = entry;
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
if (best === void 0) return void 0;
|
|
1584
|
-
return freezeEntity(best.entity, getFlows());
|
|
1585
|
-
};
|
|
1586
|
-
const list = (kind) => {
|
|
1587
|
-
assertEntityKind(kind);
|
|
1588
|
-
const flows = getFlows();
|
|
1589
|
-
return Array.from(
|
|
1590
|
-
store[kind].values(),
|
|
1591
|
-
(e) => freezeEntity(e, flows)
|
|
1592
|
-
);
|
|
1593
|
-
};
|
|
1594
|
-
const allEntities = function* () {
|
|
1595
|
-
for (const kind of Object.keys(store)) {
|
|
1596
|
-
for (const entity of store[kind].values()) {
|
|
1597
|
-
yield entity;
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
};
|
|
1601
|
-
const query = (predicate) => {
|
|
1602
|
-
const flows = getFlows();
|
|
1603
|
-
const result = [];
|
|
1604
|
-
for (const entity of allEntities()) {
|
|
1605
|
-
if (predicate(entity)) result.push(freezeEntity(entity, flows));
|
|
1606
|
-
}
|
|
1607
|
-
return result;
|
|
1608
|
-
};
|
|
1609
|
-
const byScope = (scope) => query(
|
|
1610
|
-
(entity) => "scopes" in entity && Array.isArray(entity.scopes) && entity.scopes.includes(scope)
|
|
1611
|
-
);
|
|
1612
|
-
const touchedBy = (flowId) => {
|
|
1613
|
-
const flow = store.flow.get(flowId);
|
|
1614
|
-
if (flow === void 0) return [];
|
|
1615
|
-
const ids = new Set(flow.touches);
|
|
1616
|
-
return query((entity) => {
|
|
1617
|
-
if (!isMetaEntity(entity)) return false;
|
|
1618
|
-
return ids.has(entity.id);
|
|
1619
|
-
});
|
|
1620
|
-
};
|
|
1621
|
-
const reports = /* @__PURE__ */ new Map();
|
|
1622
|
-
const reportsCbs = /* @__PURE__ */ new Set();
|
|
1623
|
-
const setReports = (kind, id, records) => {
|
|
1624
|
-
reports.set(`${kind}:${id}`, records);
|
|
1625
|
-
for (const cb of reportsCbs) cb();
|
|
1626
|
-
};
|
|
1627
|
-
const getReports = (kind, id) => reports.get(`${kind}:${id}`) ?? [];
|
|
1628
|
-
const listReportKeys = () => Array.from(reports.keys());
|
|
1629
|
-
const onReportsChange = (cb) => {
|
|
1630
|
-
reportsCbs.add(cb);
|
|
1631
|
-
return () => reportsCbs.delete(cb);
|
|
1632
|
-
};
|
|
1633
|
-
return {
|
|
1634
|
-
add,
|
|
1635
|
-
get,
|
|
1636
|
-
matchPattern,
|
|
1637
|
-
list,
|
|
1638
|
-
query,
|
|
1639
|
-
byScope,
|
|
1640
|
-
touchedBy,
|
|
1641
|
-
setReports,
|
|
1642
|
-
getReports,
|
|
1643
|
-
listReportKeys,
|
|
1644
|
-
onReportsChange
|
|
1645
|
-
};
|
|
1800
|
+
names.push(String(local.name));
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
} else if ((stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportAllDeclaration") && stmt.source) {
|
|
1804
|
+
source = stmt.source;
|
|
1805
|
+
isTypeOnly = stmt.exportKind === "type";
|
|
1806
|
+
} else {
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
if (!source || source.type !== "Literal") continue;
|
|
1810
|
+
if (typeof source.value !== "string") continue;
|
|
1811
|
+
out2.push({
|
|
1812
|
+
specifier: source.value,
|
|
1813
|
+
line: parsed.lineAt(stmt.start),
|
|
1814
|
+
span: { start: stmt.start, end: stmt.end },
|
|
1815
|
+
isTypeOnly,
|
|
1816
|
+
names
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
return out2;
|
|
1646
1820
|
}
|
|
1647
1821
|
|
|
1822
|
+
// src/scanner/scan/resolve.ts
|
|
1823
|
+
import * as path4 from "path";
|
|
1824
|
+
|
|
1648
1825
|
// src/scanner/scan/routes.ts
|
|
1649
1826
|
var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1650
1827
|
var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1651
1828
|
var ROUTE_BASENAME = /^route\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1652
|
-
function detectRoutes(files) {
|
|
1829
|
+
function detectRoutes(files, options = {}) {
|
|
1653
1830
|
const out2 = [];
|
|
1654
1831
|
const seen = /* @__PURE__ */ new Set();
|
|
1832
|
+
const routesDir = options.routesDir ? options.routesDir.replace(/^\.?\/+/, "").replace(/\/+$/, "") : null;
|
|
1833
|
+
const routesDirParts = routesDir ? routesDir.split("/") : null;
|
|
1655
1834
|
for (const f of files) {
|
|
1656
1835
|
const rel = f.displayPath;
|
|
1657
1836
|
const parts = rel.split("/");
|
|
1658
1837
|
const base = parts[parts.length - 1];
|
|
1838
|
+
if (routesDirParts) {
|
|
1839
|
+
const end = dirEndIndex(parts, routesDirParts);
|
|
1840
|
+
if (end === -1 || end > parts.length - 1) continue;
|
|
1841
|
+
if (!PAGES_ROUTER_BASENAME.test(base)) continue;
|
|
1842
|
+
const routePath = routesStylePath(
|
|
1843
|
+
parts.slice(end, parts.length - 1),
|
|
1844
|
+
base
|
|
1845
|
+
);
|
|
1846
|
+
if (routePath !== null) push(out2, seen, routePath, f.displayPath);
|
|
1847
|
+
continue;
|
|
1848
|
+
}
|
|
1659
1849
|
const appIdx = parts.indexOf("app");
|
|
1660
1850
|
if (appIdx !== -1 && PAGE_BASENAME.test(base)) {
|
|
1661
1851
|
const routeSegments = parts.slice(appIdx + 1, parts.length - 1);
|
|
@@ -1682,15 +1872,11 @@ function detectRoutes(files) {
|
|
|
1682
1872
|
}
|
|
1683
1873
|
const routesIdx = parts.indexOf("routes");
|
|
1684
1874
|
if (routesIdx !== -1 && PAGES_ROUTER_BASENAME.test(base)) {
|
|
1685
|
-
const
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
base.replace(/\.[^.]+$/, "")
|
|
1691
|
-
].filter((s) => s !== "index" && s !== "__root");
|
|
1692
|
-
const routePath = formatNextAppPath(normalized);
|
|
1693
|
-
push(out2, seen, routePath, f.displayPath);
|
|
1875
|
+
const routePath = routesStylePath(
|
|
1876
|
+
parts.slice(routesIdx + 1, parts.length - 1),
|
|
1877
|
+
base
|
|
1878
|
+
);
|
|
1879
|
+
if (routePath !== null) push(out2, seen, routePath, f.displayPath);
|
|
1694
1880
|
continue;
|
|
1695
1881
|
}
|
|
1696
1882
|
}
|
|
@@ -1706,6 +1892,63 @@ function formatNextAppPath(segments) {
|
|
|
1706
1892
|
if (kept.length === 0) return "/";
|
|
1707
1893
|
return "/" + kept.join("/");
|
|
1708
1894
|
}
|
|
1895
|
+
function dirEndIndex(parts, dirParts) {
|
|
1896
|
+
if (dirParts.length === 0) return 0;
|
|
1897
|
+
for (let i = 0; i + dirParts.length <= parts.length; i++) {
|
|
1898
|
+
let match = true;
|
|
1899
|
+
for (let j = 0; j < dirParts.length; j++) {
|
|
1900
|
+
if (parts[i + j] !== dirParts[j]) {
|
|
1901
|
+
match = false;
|
|
1902
|
+
break;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
if (match) return i + dirParts.length;
|
|
1906
|
+
}
|
|
1907
|
+
return -1;
|
|
1908
|
+
}
|
|
1909
|
+
function routesStylePath(dirSegs, base) {
|
|
1910
|
+
const fileSegs = splitFlatSegments(base.replace(/\.[^.]+$/, ""));
|
|
1911
|
+
const leaf = fileSegs[fileSegs.length - 1];
|
|
1912
|
+
if (leaf.startsWith("_")) return null;
|
|
1913
|
+
const rawSegs = [...dirSegs, ...fileSegs];
|
|
1914
|
+
if (rawSegs.some((s) => s.startsWith("-"))) return null;
|
|
1915
|
+
if (rawSegs[0] === "api") return null;
|
|
1916
|
+
return formatTanStackPath(rawSegs);
|
|
1917
|
+
}
|
|
1918
|
+
function splitFlatSegments(name) {
|
|
1919
|
+
const segs = [];
|
|
1920
|
+
let cur = "";
|
|
1921
|
+
let depth = 0;
|
|
1922
|
+
for (const ch of name) {
|
|
1923
|
+
if (ch === "[") depth++;
|
|
1924
|
+
else if (ch === "]") depth = Math.max(0, depth - 1);
|
|
1925
|
+
if (ch === "." && depth === 0) {
|
|
1926
|
+
segs.push(cur);
|
|
1927
|
+
cur = "";
|
|
1928
|
+
continue;
|
|
1929
|
+
}
|
|
1930
|
+
cur += ch;
|
|
1931
|
+
}
|
|
1932
|
+
segs.push(cur);
|
|
1933
|
+
return segs;
|
|
1934
|
+
}
|
|
1935
|
+
function formatTanStackPath(segments) {
|
|
1936
|
+
const kept = [];
|
|
1937
|
+
for (const seg of segments) {
|
|
1938
|
+
if (!seg) continue;
|
|
1939
|
+
if (seg === "index" || seg === "route" || seg === "__root") continue;
|
|
1940
|
+
if (seg.startsWith("_")) continue;
|
|
1941
|
+
if (seg.startsWith("(") && seg.endsWith(")")) continue;
|
|
1942
|
+
kept.push(normalizeTanStackSegment(seg.replace(/_$/, "")));
|
|
1943
|
+
}
|
|
1944
|
+
if (kept.length === 0) return "/";
|
|
1945
|
+
return "/" + kept.join("/");
|
|
1946
|
+
}
|
|
1947
|
+
function normalizeTanStackSegment(seg) {
|
|
1948
|
+
if (seg === "$") return "[...splat]";
|
|
1949
|
+
if (seg.startsWith("$")) return `[${seg.slice(1)}]`;
|
|
1950
|
+
return seg.replace(/\[([^\]]*)\]/g, "$1");
|
|
1951
|
+
}
|
|
1709
1952
|
function pathToId(routePath) {
|
|
1710
1953
|
if (routePath === "/") return "root";
|
|
1711
1954
|
return routePath.replace(/^\/+/, "").replace(/\[\.{3}([^\]]+)\]/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -1744,6 +1987,18 @@ function buildMetaFromExport(exp) {
|
|
|
1744
1987
|
if (exp.acceptance?.length) meta.acceptance = exp.acceptance;
|
|
1745
1988
|
if (exp.features?.length) meta.features = exp.features;
|
|
1746
1989
|
if (exp.widgets?.length) meta.widgets = exp.widgets;
|
|
1990
|
+
if (exp.states?.length) {
|
|
1991
|
+
meta.states = exp.states.map((s) => ({
|
|
1992
|
+
name: s.name,
|
|
1993
|
+
...s.kind ? { kind: s.kind } : {},
|
|
1994
|
+
...s.acceptance?.length ? { acceptance: s.acceptance } : {},
|
|
1995
|
+
...s.description ? { description: s.description } : {}
|
|
1996
|
+
}));
|
|
1997
|
+
}
|
|
1998
|
+
if (exp.capture === false) meta.capture = false;
|
|
1999
|
+
if (exp.waivers && Object.keys(exp.waivers).length > 0) {
|
|
2000
|
+
meta.waivers = exp.waivers;
|
|
2001
|
+
}
|
|
1747
2002
|
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
1748
2003
|
}
|
|
1749
2004
|
function resolve2(ctx) {
|
|
@@ -1789,9 +2044,12 @@ function resolve2(ctx) {
|
|
|
1789
2044
|
function metaWithComposes(kind, id, base) {
|
|
1790
2045
|
const composes = directChildren.get(`${kind}:${id}`);
|
|
1791
2046
|
if (!composes || composes.length === 0) return base;
|
|
1792
|
-
return { ...base
|
|
2047
|
+
return { ...base, composes };
|
|
1793
2048
|
}
|
|
1794
|
-
const routes = conventions.pages ===
|
|
2049
|
+
const routes = conventions.pages === false ? [] : detectRoutes(
|
|
2050
|
+
ctx.extracted.map((e) => e.file),
|
|
2051
|
+
conventions.pages === "auto" ? {} : { routesDir: conventions.pages.routesDir }
|
|
2052
|
+
);
|
|
1795
2053
|
const handledPageFiles = /* @__PURE__ */ new Set();
|
|
1796
2054
|
for (const route of routes) {
|
|
1797
2055
|
const routeDir = path4.posix.dirname(route.file);
|
|
@@ -2163,6 +2421,297 @@ function dedupe(arr) {
|
|
|
2163
2421
|
|
|
2164
2422
|
// src/scanner/scan/audit.ts
|
|
2165
2423
|
import * as path5 from "path";
|
|
2424
|
+
|
|
2425
|
+
// src/scanner/scan/page-coverage.ts
|
|
2426
|
+
function compileRoute(pattern) {
|
|
2427
|
+
const segs = pattern.split("/").filter(Boolean);
|
|
2428
|
+
let specificity = 0;
|
|
2429
|
+
const optionalTail = segs.length > 0 && /^\[\[\.{3}.+\]\]$/.test(segs[segs.length - 1]);
|
|
2430
|
+
const effective = optionalTail ? segs.slice(0, -1) : segs;
|
|
2431
|
+
const parts = effective.map((seg) => {
|
|
2432
|
+
const catchAll = /^\[\.{3}.+\]$/.test(seg);
|
|
2433
|
+
const dynamic = /^\[.+\]$/.test(seg);
|
|
2434
|
+
if (catchAll) return "(?:[^/]+)(?:/[^/]+)*";
|
|
2435
|
+
if (dynamic) return "[^/]+";
|
|
2436
|
+
specificity++;
|
|
2437
|
+
return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2438
|
+
});
|
|
2439
|
+
let body = parts.length === 0 ? "" : "/" + parts.join("/");
|
|
2440
|
+
if (optionalTail) body += "(?:/[^/]+)*";
|
|
2441
|
+
if (body === "") body = "/";
|
|
2442
|
+
const re = new RegExp(`^${body}/?$`);
|
|
2443
|
+
return { test: (url) => re.test(url), specificity };
|
|
2444
|
+
}
|
|
2445
|
+
function matchUrlToRoute(url, routePaths) {
|
|
2446
|
+
const path12 = stripQuery(url);
|
|
2447
|
+
let best = null;
|
|
2448
|
+
for (const rp of routePaths) {
|
|
2449
|
+
const { test, specificity } = compileRoute(rp);
|
|
2450
|
+
if (!test(path12)) continue;
|
|
2451
|
+
if (!best || specificity > best.specificity)
|
|
2452
|
+
best = { path: rp, specificity };
|
|
2453
|
+
}
|
|
2454
|
+
return best?.path ?? null;
|
|
2455
|
+
}
|
|
2456
|
+
function stripQuery(url) {
|
|
2457
|
+
const q = url.indexOf("?");
|
|
2458
|
+
const h = url.indexOf("#");
|
|
2459
|
+
let end = url.length;
|
|
2460
|
+
if (q !== -1) end = Math.min(end, q);
|
|
2461
|
+
if (h !== -1) end = Math.min(end, h);
|
|
2462
|
+
return url.slice(0, end);
|
|
2463
|
+
}
|
|
2464
|
+
function routeForCapture(c, routes, routePaths) {
|
|
2465
|
+
if (c.route && routePaths.includes(c.route)) return c.route;
|
|
2466
|
+
if (c.url) {
|
|
2467
|
+
const m = matchUrlToRoute(c.url, routePaths);
|
|
2468
|
+
if (m) return m;
|
|
2469
|
+
}
|
|
2470
|
+
if (c.kind === "page") {
|
|
2471
|
+
const r = routes.find((r2) => r2.page === c.entity);
|
|
2472
|
+
if (r) return r.path;
|
|
2473
|
+
}
|
|
2474
|
+
return null;
|
|
2475
|
+
}
|
|
2476
|
+
function checkPageCoverage(registry, manifest, baseline) {
|
|
2477
|
+
const routes = registry.list("route");
|
|
2478
|
+
if (routes.length === 0) return [];
|
|
2479
|
+
const routePaths = routes.map((r) => r.path);
|
|
2480
|
+
const captured = manifest.captured ?? [];
|
|
2481
|
+
const optedOut = /* @__PURE__ */ new Set();
|
|
2482
|
+
for (const r of routes) {
|
|
2483
|
+
const page = registry.get("page", r.page);
|
|
2484
|
+
if (page?.meta?.capture === false) optedOut.add(r.path);
|
|
2485
|
+
}
|
|
2486
|
+
const covered = /* @__PURE__ */ new Set();
|
|
2487
|
+
for (const c of captured) {
|
|
2488
|
+
const route = routeForCapture(c, routes, routePaths);
|
|
2489
|
+
if (route) covered.add(route);
|
|
2490
|
+
}
|
|
2491
|
+
const baselineSet = new Set(baseline?.uncapturedPages ?? []);
|
|
2492
|
+
const diagnostics = [];
|
|
2493
|
+
for (const r of routes) {
|
|
2494
|
+
if (covered.has(r.path) || optedOut.has(r.path)) continue;
|
|
2495
|
+
const loc = registry.get("page", r.page)?.loc;
|
|
2496
|
+
if (baselineSet.has(r.path)) {
|
|
2497
|
+
diagnostics.push({
|
|
2498
|
+
code: "page-backlog",
|
|
2499
|
+
severity: "info",
|
|
2500
|
+
message: `route "${r.path}" has no visual-states capture (acknowledged backlog)`,
|
|
2501
|
+
file: loc?.file,
|
|
2502
|
+
line: loc?.line,
|
|
2503
|
+
hint: `Capture it (drive the route in a states spec) and it drops off the baseline, or opt it out with \`export const uidex = { page: "${r.page}", capture: false }\`.`
|
|
2504
|
+
});
|
|
2505
|
+
continue;
|
|
2506
|
+
}
|
|
2507
|
+
diagnostics.push({
|
|
2508
|
+
code: "page-no-capture",
|
|
2509
|
+
severity: "error",
|
|
2510
|
+
message: `route "${r.path}" has no visual-states capture and is not accounted for`,
|
|
2511
|
+
file: loc?.file,
|
|
2512
|
+
line: loc?.line,
|
|
2513
|
+
entity: { kind: "route", id: r.path },
|
|
2514
|
+
hint: `Capture the route in a states spec, opt it out with \`export const uidex = { page: "${r.page}", capture: false }\`, or grandfather it as acknowledged backlog with \`uidex scan --update-baseline\` (the baseline change lands in the diff for review).`
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
for (const b of baselineSet) {
|
|
2518
|
+
if (!routePaths.includes(b)) {
|
|
2519
|
+
diagnostics.push({
|
|
2520
|
+
code: "stale-page-baseline",
|
|
2521
|
+
severity: "warning",
|
|
2522
|
+
message: `coverage baseline lists "${b}", which is not a route in the registry`,
|
|
2523
|
+
hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
|
|
2524
|
+
});
|
|
2525
|
+
} else if (covered.has(b) || optedOut.has(b)) {
|
|
2526
|
+
diagnostics.push({
|
|
2527
|
+
code: "page-captured",
|
|
2528
|
+
severity: "warning",
|
|
2529
|
+
message: `route "${b}" is now captured but is still in the coverage baseline`,
|
|
2530
|
+
hint: `Prune it with \`uidex scan --update-baseline\` so the backlog reflects reality.`
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
return diagnostics;
|
|
2535
|
+
}
|
|
2536
|
+
function computePageBaseline(registry, manifest) {
|
|
2537
|
+
const uncaptured = [];
|
|
2538
|
+
for (const d of checkPageCoverage(registry, manifest, {
|
|
2539
|
+
uncapturedPages: []
|
|
2540
|
+
})) {
|
|
2541
|
+
if (d.code === "page-no-capture" && d.entity) uncaptured.push(d.entity.id);
|
|
2542
|
+
}
|
|
2543
|
+
return { uncapturedPages: uncaptured.sort() };
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
// src/scanner/scan/state-coverage.ts
|
|
2547
|
+
var STATE_KINDS2 = ["page", "feature", "widget"];
|
|
2548
|
+
function declaredStateEntities(registry) {
|
|
2549
|
+
const out2 = [];
|
|
2550
|
+
for (const kind of STATE_KINDS2) {
|
|
2551
|
+
for (const e of registry.list(kind)) {
|
|
2552
|
+
const states = e.meta?.states;
|
|
2553
|
+
if (!states || states.length === 0) continue;
|
|
2554
|
+
out2.push({
|
|
2555
|
+
kind,
|
|
2556
|
+
id: e.id,
|
|
2557
|
+
states: states.map((s) => s.name),
|
|
2558
|
+
loc: e.loc
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
return out2;
|
|
2563
|
+
}
|
|
2564
|
+
function captureMatchesEntity(cap, ent) {
|
|
2565
|
+
if (cap.entity !== ent.id) return false;
|
|
2566
|
+
return cap.kind === void 0 || cap.kind === ent.kind;
|
|
2567
|
+
}
|
|
2568
|
+
function checkStateCoverage(registry, manifest) {
|
|
2569
|
+
const diagnostics = [];
|
|
2570
|
+
const declared = declaredStateEntities(registry);
|
|
2571
|
+
const captured = manifest.captured ?? [];
|
|
2572
|
+
for (const ent of declared) {
|
|
2573
|
+
for (const state of ent.states) {
|
|
2574
|
+
const hit = captured.some(
|
|
2575
|
+
(c) => captureMatchesEntity(c, ent) && c.state === state
|
|
2576
|
+
);
|
|
2577
|
+
if (hit) continue;
|
|
2578
|
+
diagnostics.push({
|
|
2579
|
+
code: "missing-state-capture",
|
|
2580
|
+
severity: "warning",
|
|
2581
|
+
message: `${ent.kind} "${ent.id}" declares state "${state}" but no capture produced it`,
|
|
2582
|
+
file: ent.loc?.file,
|
|
2583
|
+
line: ent.loc?.line,
|
|
2584
|
+
entity: { kind: ent.kind, id: ent.id },
|
|
2585
|
+
hint: `Add a capture for "${ent.id}/${state}" to the states matrix, or remove the state from \`export const uidex = { states: [...] }\` if it no longer exists.`
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
for (const cap of captured) {
|
|
2590
|
+
const candidates = declared.filter((ent2) => captureMatchesEntity(cap, ent2));
|
|
2591
|
+
if (candidates.length === 0) continue;
|
|
2592
|
+
const declaredSomewhere = candidates.some(
|
|
2593
|
+
(ent2) => ent2.states.includes(cap.state)
|
|
2594
|
+
);
|
|
2595
|
+
if (declaredSomewhere) continue;
|
|
2596
|
+
const ent = candidates[0];
|
|
2597
|
+
diagnostics.push({
|
|
2598
|
+
code: "orphan-state-capture",
|
|
2599
|
+
severity: "warning",
|
|
2600
|
+
message: `Capture produced state "${cap.state}" for ${ent.kind} "${ent.id}", which is not in its declared states [${ent.states.join(", ")}]`,
|
|
2601
|
+
file: ent.loc?.file,
|
|
2602
|
+
line: ent.loc?.line,
|
|
2603
|
+
entity: { kind: ent.kind, id: ent.id },
|
|
2604
|
+
hint: `Declare "${cap.state}" in \`export const uidex = { states: [...] }\` for "${ent.id}", or rename the capture to a declared state.`
|
|
2605
|
+
});
|
|
2606
|
+
}
|
|
2607
|
+
return diagnostics;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
// src/scanner/scan/state-matrix.ts
|
|
2611
|
+
var CORE_KINDS = CORE_STATE_KINDS;
|
|
2612
|
+
var STATE_ENTITY_KINDS = ["page", "feature", "widget"];
|
|
2613
|
+
function resolveMatrixKind(registry, c) {
|
|
2614
|
+
if (c.stateKind) return c.stateKind;
|
|
2615
|
+
for (const kind of STATE_ENTITY_KINDS) {
|
|
2616
|
+
const declared = registry.get(kind, c.entity)?.meta?.states?.find((s) => s.name === c.state)?.kind;
|
|
2617
|
+
if (declared) return declared;
|
|
2618
|
+
}
|
|
2619
|
+
return CORE_KINDS.includes(c.state) ? c.state : "variant";
|
|
2620
|
+
}
|
|
2621
|
+
function coreKindsByRoute(registry, manifest) {
|
|
2622
|
+
const routes = registry.list("route");
|
|
2623
|
+
const routePaths = routes.map((r) => r.path);
|
|
2624
|
+
const byRoute = /* @__PURE__ */ new Map();
|
|
2625
|
+
for (const c of manifest.captured ?? []) {
|
|
2626
|
+
const route = routeForCapture(c, routes, routePaths);
|
|
2627
|
+
if (!route) continue;
|
|
2628
|
+
if (!byRoute.has(route)) byRoute.set(route, /* @__PURE__ */ new Set());
|
|
2629
|
+
const kind = resolveMatrixKind(registry, c);
|
|
2630
|
+
if (CORE_KINDS.includes(kind)) byRoute.get(route).add(kind);
|
|
2631
|
+
}
|
|
2632
|
+
return byRoute;
|
|
2633
|
+
}
|
|
2634
|
+
function pageWaivers(registry, routePage) {
|
|
2635
|
+
return registry.get("page", routePage)?.meta?.waivers ?? {};
|
|
2636
|
+
}
|
|
2637
|
+
function isOptedOut(registry, page) {
|
|
2638
|
+
return page ? registry.get("page", page)?.meta?.capture === false : false;
|
|
2639
|
+
}
|
|
2640
|
+
function checkStateMatrix(registry, manifest, baseline) {
|
|
2641
|
+
const routes = registry.list("route");
|
|
2642
|
+
if (routes.length === 0) return [];
|
|
2643
|
+
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
2644
|
+
const captured = coreKindsByRoute(registry, manifest);
|
|
2645
|
+
const baselined = baseline?.missingKinds ?? {};
|
|
2646
|
+
const diagnostics = [];
|
|
2647
|
+
for (const [route, kinds] of captured) {
|
|
2648
|
+
const page = pageOf.get(route);
|
|
2649
|
+
if (isOptedOut(registry, page)) continue;
|
|
2650
|
+
const waivers = page ? pageWaivers(registry, page) : {};
|
|
2651
|
+
const loc = page ? registry.get("page", page)?.loc : void 0;
|
|
2652
|
+
const baselinedKinds = new Set(baselined[route] ?? []);
|
|
2653
|
+
for (const kind of CORE_KINDS) {
|
|
2654
|
+
if (kinds.has(kind)) continue;
|
|
2655
|
+
if (waivers[kind]) continue;
|
|
2656
|
+
if (baselinedKinds.has(kind)) {
|
|
2657
|
+
diagnostics.push({
|
|
2658
|
+
code: "matrix-backlog",
|
|
2659
|
+
severity: "info",
|
|
2660
|
+
message: `route "${route}" does not render core kind "${kind}" (acknowledged MISSING_KINDS backlog)`,
|
|
2661
|
+
file: loc?.file,
|
|
2662
|
+
line: loc?.line,
|
|
2663
|
+
hint: `Capture the "${kind}" state for "${route}", waive it (\`waivers: { ${kind}: "why it can't" }\` on the page), or leave it in the baseline.`
|
|
2664
|
+
});
|
|
2665
|
+
continue;
|
|
2666
|
+
}
|
|
2667
|
+
diagnostics.push({
|
|
2668
|
+
code: "core-kind",
|
|
2669
|
+
severity: "error",
|
|
2670
|
+
message: `route "${route}" is captured but never renders core kind "${kind}"`,
|
|
2671
|
+
file: loc?.file,
|
|
2672
|
+
line: loc?.line,
|
|
2673
|
+
entity: { kind: "route", id: route },
|
|
2674
|
+
hint: `Capture the "${kind}" state, or if the route cannot render it add \`export const uidex = { page: "${page}", waivers: { ${kind}: "why it can't" } }\`, or grandfather it via \`uidex scan --update-baseline\` (the baseline change lands in the diff for review).`
|
|
2675
|
+
});
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
for (const [route, kinds] of captured) {
|
|
2679
|
+
const page = pageOf.get(route);
|
|
2680
|
+
if (!page || isOptedOut(registry, page)) continue;
|
|
2681
|
+
const waivers = pageWaivers(registry, page);
|
|
2682
|
+
const loc = registry.get("page", page)?.loc;
|
|
2683
|
+
for (const kind of Object.keys(waivers)) {
|
|
2684
|
+
if (kinds.has(kind)) {
|
|
2685
|
+
diagnostics.push({
|
|
2686
|
+
code: "stale-waiver",
|
|
2687
|
+
severity: "warning",
|
|
2688
|
+
message: `page "${page}" waives core kind "${kind}", but route "${route}" does render it`,
|
|
2689
|
+
file: loc?.file,
|
|
2690
|
+
line: loc?.line,
|
|
2691
|
+
entity: { kind: "page", id: page },
|
|
2692
|
+
hint: `Remove the "${kind}" waiver \u2014 the route renders that kind, so the waiver's reason is stale.`
|
|
2693
|
+
});
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
return diagnostics;
|
|
2698
|
+
}
|
|
2699
|
+
function computeMissingKinds(registry, manifest) {
|
|
2700
|
+
const routes = registry.list("route");
|
|
2701
|
+
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
2702
|
+
const captured = coreKindsByRoute(registry, manifest);
|
|
2703
|
+
const out2 = {};
|
|
2704
|
+
for (const [route, kinds] of captured) {
|
|
2705
|
+
const page = pageOf.get(route);
|
|
2706
|
+
if (isOptedOut(registry, page)) continue;
|
|
2707
|
+
const waivers = page ? pageWaivers(registry, page) : {};
|
|
2708
|
+
const missing = CORE_KINDS.filter((k) => !kinds.has(k) && !waivers[k]);
|
|
2709
|
+
if (missing.length > 0) out2[route] = missing;
|
|
2710
|
+
}
|
|
2711
|
+
return out2;
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
// src/scanner/scan/audit.ts
|
|
2166
2715
|
function audit(opts) {
|
|
2167
2716
|
const diagnostics = [];
|
|
2168
2717
|
const { registry, extracted, files, config } = opts;
|
|
@@ -2171,6 +2720,9 @@ function audit(opts) {
|
|
|
2171
2720
|
const acceptanceEnabled = config.audit?.acceptance ?? true;
|
|
2172
2721
|
const scopeLeakEnabled = config.audit?.scopeLeak ?? true;
|
|
2173
2722
|
const coverageEnabled = config.audit?.coverage ?? true;
|
|
2723
|
+
const statesEnabled = config.audit?.states ?? true;
|
|
2724
|
+
const pageCoverageEnabled = config.audit?.pageCoverage ?? true;
|
|
2725
|
+
const stateMatrixEnabled = config.audit?.stateMatrix ?? true;
|
|
2174
2726
|
if (opts.resolveDiagnostics) diagnostics.push(...opts.resolveDiagnostics);
|
|
2175
2727
|
for (const ef of extracted) {
|
|
2176
2728
|
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
@@ -2204,6 +2756,48 @@ function audit(opts) {
|
|
|
2204
2756
|
}
|
|
2205
2757
|
}
|
|
2206
2758
|
}
|
|
2759
|
+
if (check && opts.typesGenerated !== void 0) {
|
|
2760
|
+
const outRel = opts.typesOutputPath ?? config.output;
|
|
2761
|
+
const fresh = normalizeForCheck(opts.typesGenerated);
|
|
2762
|
+
if (opts.typesOnDisk === null || opts.typesOnDisk === void 0) {
|
|
2763
|
+
diagnostics.push({
|
|
2764
|
+
code: "gen-missing",
|
|
2765
|
+
severity: "error",
|
|
2766
|
+
message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
|
|
2767
|
+
file: outRel,
|
|
2768
|
+
hint: "Run `uidex scan` (without --check) to regenerate"
|
|
2769
|
+
});
|
|
2770
|
+
} else if (normalizeForCheck(opts.typesOnDisk) !== fresh) {
|
|
2771
|
+
diagnostics.push({
|
|
2772
|
+
code: "gen-stale",
|
|
2773
|
+
severity: "error",
|
|
2774
|
+
message: `Generated file "${outRel}" is stale`,
|
|
2775
|
+
file: outRel,
|
|
2776
|
+
hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
|
|
2777
|
+
});
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
if (check && opts.locsGenerated !== void 0) {
|
|
2781
|
+
const outRel = opts.locsOutputPath ?? config.output;
|
|
2782
|
+
const fresh = normalizeForCheck(opts.locsGenerated);
|
|
2783
|
+
if (opts.locsOnDisk === null || opts.locsOnDisk === void 0) {
|
|
2784
|
+
diagnostics.push({
|
|
2785
|
+
code: "gen-missing",
|
|
2786
|
+
severity: "error",
|
|
2787
|
+
message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
|
|
2788
|
+
file: outRel,
|
|
2789
|
+
hint: "Run `uidex scan` (without --check) to regenerate"
|
|
2790
|
+
});
|
|
2791
|
+
} else if (normalizeForCheck(opts.locsOnDisk) !== fresh) {
|
|
2792
|
+
diagnostics.push({
|
|
2793
|
+
code: "gen-stale",
|
|
2794
|
+
severity: "error",
|
|
2795
|
+
message: `Generated file "${outRel}" is stale`,
|
|
2796
|
+
file: outRel,
|
|
2797
|
+
hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2207
2801
|
if (lint && acceptanceEnabled) {
|
|
2208
2802
|
for (const kind of ["widget", "feature", "page"]) {
|
|
2209
2803
|
for (const e of registry.list(kind)) {
|
|
@@ -2463,6 +3057,21 @@ function audit(opts) {
|
|
|
2463
3057
|
}
|
|
2464
3058
|
}
|
|
2465
3059
|
}
|
|
3060
|
+
if (lint && statesEnabled && opts.capturedStates) {
|
|
3061
|
+
diagnostics.push(...checkStateCoverage(registry, opts.capturedStates));
|
|
3062
|
+
}
|
|
3063
|
+
if (lint && pageCoverageEnabled && opts.capturedStates) {
|
|
3064
|
+
diagnostics.push(
|
|
3065
|
+
...checkPageCoverage(registry, opts.capturedStates, opts.pageBaseline)
|
|
3066
|
+
);
|
|
3067
|
+
}
|
|
3068
|
+
if (lint && stateMatrixEnabled && opts.capturedStates) {
|
|
3069
|
+
diagnostics.push(
|
|
3070
|
+
...checkStateMatrix(registry, opts.capturedStates, {
|
|
3071
|
+
missingKinds: opts.pageBaseline?.missingKinds ?? {}
|
|
3072
|
+
})
|
|
3073
|
+
);
|
|
3074
|
+
}
|
|
2466
3075
|
const summary = {
|
|
2467
3076
|
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
2468
3077
|
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
@@ -2627,6 +3236,148 @@ function stableReplacer(_key, value) {
|
|
|
2627
3236
|
}
|
|
2628
3237
|
|
|
2629
3238
|
// src/scanner/scan/emit.ts
|
|
3239
|
+
var AUTHORED_KINDS = /* @__PURE__ */ new Set([
|
|
3240
|
+
"route",
|
|
3241
|
+
"page",
|
|
3242
|
+
"feature",
|
|
3243
|
+
"widget",
|
|
3244
|
+
"primitive",
|
|
3245
|
+
"flow"
|
|
3246
|
+
]);
|
|
3247
|
+
function internedPayload(entities) {
|
|
3248
|
+
const fileIndex = /* @__PURE__ */ new Map();
|
|
3249
|
+
const files = [];
|
|
3250
|
+
const compact = entities.map((e) => {
|
|
3251
|
+
const loc = e.loc;
|
|
3252
|
+
if (!loc || typeof loc.file !== "string") return e;
|
|
3253
|
+
let idx = fileIndex.get(loc.file);
|
|
3254
|
+
if (idx === void 0) {
|
|
3255
|
+
idx = files.length;
|
|
3256
|
+
files.push(loc.file);
|
|
3257
|
+
fileIndex.set(loc.file, idx);
|
|
3258
|
+
}
|
|
3259
|
+
const rest = { ...loc };
|
|
3260
|
+
delete rest.file;
|
|
3261
|
+
return { ...e, loc: { f: idx, ...rest } };
|
|
3262
|
+
});
|
|
3263
|
+
return {
|
|
3264
|
+
filesDecl: `const files: readonly string[] = ${jsonStable(files)}`,
|
|
3265
|
+
parseExpr: `(JSON.parse(${JSON.stringify(
|
|
3266
|
+
JSON.stringify(compact, replacerSorted)
|
|
3267
|
+
)}) as any[]).map(hydrate)`
|
|
3268
|
+
};
|
|
3269
|
+
}
|
|
3270
|
+
var HYDRATE_FN = [
|
|
3271
|
+
"function hydrate(e: any): any {",
|
|
3272
|
+
' if (e.loc && typeof e.loc.f === "number") {',
|
|
3273
|
+
" const { f, ...rest } = e.loc",
|
|
3274
|
+
" e.loc = { file: files[f], ...rest }",
|
|
3275
|
+
" }",
|
|
3276
|
+
" return e",
|
|
3277
|
+
"}"
|
|
3278
|
+
].join("\n");
|
|
3279
|
+
function compactMeta(meta) {
|
|
3280
|
+
if (!meta || typeof meta !== "object") return meta;
|
|
3281
|
+
const composes = meta.composes;
|
|
3282
|
+
if (!Array.isArray(composes)) return meta;
|
|
3283
|
+
return {
|
|
3284
|
+
...meta,
|
|
3285
|
+
composes: composes.map(
|
|
3286
|
+
(c) => c && typeof c === "object" && c.kind === "element" && Object.keys(c).length === 2 && typeof c.id === "string" ? c.id : c
|
|
3287
|
+
)
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
function emitLocsModule(thin, uidexImport) {
|
|
3291
|
+
const kinds = [...new Set(thin.map((e) => e.kind))].sort();
|
|
3292
|
+
const kindIdx = new Map(kinds.map((k, i) => [k, i]));
|
|
3293
|
+
const fileIndex = /* @__PURE__ */ new Map();
|
|
3294
|
+
const files = [];
|
|
3295
|
+
const fi = (f) => {
|
|
3296
|
+
let i = fileIndex.get(f);
|
|
3297
|
+
if (i === void 0) {
|
|
3298
|
+
i = files.length;
|
|
3299
|
+
files.push(f);
|
|
3300
|
+
fileIndex.set(f, i);
|
|
3301
|
+
}
|
|
3302
|
+
return i;
|
|
3303
|
+
};
|
|
3304
|
+
const bare = [];
|
|
3305
|
+
const rich = [];
|
|
3306
|
+
for (const e of thin) {
|
|
3307
|
+
const loc = e.loc;
|
|
3308
|
+
const topOnlyKindIdLoc = Object.keys(e).every(
|
|
3309
|
+
(k) => k === "kind" || k === "id" || k === "loc"
|
|
3310
|
+
);
|
|
3311
|
+
const locOnlyKnown = !!loc && typeof loc.file === "string" && Object.keys(loc).every(
|
|
3312
|
+
(k) => k === "file" || k === "line" || k === "column"
|
|
3313
|
+
);
|
|
3314
|
+
if (topOnlyKindIdLoc && locOnlyKnown) {
|
|
3315
|
+
const row = [
|
|
3316
|
+
kindIdx.get(e.kind),
|
|
3317
|
+
e.id,
|
|
3318
|
+
fi(loc.file)
|
|
3319
|
+
];
|
|
3320
|
+
if (loc.line != null || loc.column != null)
|
|
3321
|
+
row.push(Number(loc.line ?? 0));
|
|
3322
|
+
if (loc.column != null) row.push(Number(loc.column));
|
|
3323
|
+
bare.push(row);
|
|
3324
|
+
} else {
|
|
3325
|
+
const richEntity = { ...e };
|
|
3326
|
+
if (loc && typeof loc.file === "string") {
|
|
3327
|
+
const { file, ...rest } = loc;
|
|
3328
|
+
richEntity.loc = { f: fi(file), ...rest };
|
|
3329
|
+
}
|
|
3330
|
+
if (richEntity.meta) richEntity.meta = compactMeta(richEntity.meta);
|
|
3331
|
+
rich.push(richEntity);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
const str = (v) => JSON.stringify(JSON.stringify(v, replacerSorted));
|
|
3335
|
+
const l = [];
|
|
3336
|
+
l.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
3337
|
+
l.push(
|
|
3338
|
+
"// Deferred element/region source locations for the inspector's copy-path."
|
|
3339
|
+
);
|
|
3340
|
+
l.push("// @ts-nocheck");
|
|
3341
|
+
l.push("/* eslint-disable */");
|
|
3342
|
+
l.push(`import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`);
|
|
3343
|
+
l.push("");
|
|
3344
|
+
l.push(`const files: readonly string[] = ${jsonStable(files)}`);
|
|
3345
|
+
l.push(`const kinds: readonly string[] = ${jsonStable(kinds)}`);
|
|
3346
|
+
l.push("function loc(fi: number, line?: number, col?: number) {");
|
|
3347
|
+
l.push(" const o: any = { file: files[fi] }");
|
|
3348
|
+
l.push(" if (line) o.line = line");
|
|
3349
|
+
l.push(" if (col) o.column = col");
|
|
3350
|
+
l.push(" return o");
|
|
3351
|
+
l.push("}");
|
|
3352
|
+
l.push(`const bare: any[] = JSON.parse(${str(bare)})`);
|
|
3353
|
+
l.push(`const rich: any[] = JSON.parse(${str(rich)})`);
|
|
3354
|
+
l.push("");
|
|
3355
|
+
l.push(
|
|
3356
|
+
"/** Add the thin element/region entities (source locations) to a registry. */"
|
|
3357
|
+
);
|
|
3358
|
+
l.push("export function loadLocs(target: Registry): Registry {");
|
|
3359
|
+
l.push(" for (const r of bare)");
|
|
3360
|
+
l.push(
|
|
3361
|
+
" target.add({ kind: kinds[r[0]], id: r[1], loc: loc(r[2], r[3], r[4]) } as Entity)"
|
|
3362
|
+
);
|
|
3363
|
+
l.push(" for (const e of rich) {");
|
|
3364
|
+
l.push(" const out: any = { ...e }");
|
|
3365
|
+
l.push(' if (e.loc && typeof e.loc.f === "number") {');
|
|
3366
|
+
l.push(" const { f, ...rest } = e.loc");
|
|
3367
|
+
l.push(" out.loc = { file: files[f], ...rest }");
|
|
3368
|
+
l.push(" }");
|
|
3369
|
+
l.push(" if (e.meta && Array.isArray(e.meta.composes)) {");
|
|
3370
|
+
l.push(
|
|
3371
|
+
' out.meta = { ...e.meta, composes: e.meta.composes.map((c: any) => typeof c === "string" ? { id: c, kind: "element" } : c) }'
|
|
3372
|
+
);
|
|
3373
|
+
l.push(" }");
|
|
3374
|
+
l.push(" target.add(out)");
|
|
3375
|
+
l.push(" }");
|
|
3376
|
+
l.push(" return target");
|
|
3377
|
+
l.push("}");
|
|
3378
|
+
l.push("");
|
|
3379
|
+
return l.join("\n");
|
|
3380
|
+
}
|
|
2630
3381
|
function sortById(arr) {
|
|
2631
3382
|
return [...arr].sort((a, b) => a.id.localeCompare(b.id));
|
|
2632
3383
|
}
|
|
@@ -2652,6 +3403,20 @@ function emitIdUnion(name, ids) {
|
|
|
2652
3403
|
${body}
|
|
2653
3404
|
`;
|
|
2654
3405
|
}
|
|
3406
|
+
var OPAQUE_ID_UNIONS = /* @__PURE__ */ new Set(["ElementId"]);
|
|
3407
|
+
function emitId(name, ids) {
|
|
3408
|
+
if (OPAQUE_ID_UNIONS.has(name)) {
|
|
3409
|
+
return `export type ${name} = string & { readonly __uidex?: ${JSON.stringify(name)} }
|
|
3410
|
+
`;
|
|
3411
|
+
}
|
|
3412
|
+
return emitIdUnion(name, ids);
|
|
3413
|
+
}
|
|
3414
|
+
function dataOutputPath(output) {
|
|
3415
|
+
return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".data.$1");
|
|
3416
|
+
}
|
|
3417
|
+
function locsOutputPath(output) {
|
|
3418
|
+
return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".locs.$1");
|
|
3419
|
+
}
|
|
2655
3420
|
function emit(opts) {
|
|
2656
3421
|
const { registry, gitContext, uidexImport = "uidex" } = opts;
|
|
2657
3422
|
const routes = [...registry.list("route")].sort(
|
|
@@ -2660,112 +3425,131 @@ function emit(opts) {
|
|
|
2660
3425
|
const pages = sortById(registry.list("page"));
|
|
2661
3426
|
const features = sortById(registry.list("feature"));
|
|
2662
3427
|
const widgets = sortById(registry.list("widget"));
|
|
3428
|
+
const stateNames = /* @__PURE__ */ new Set();
|
|
3429
|
+
for (const e of [...pages, ...features, ...widgets]) {
|
|
3430
|
+
for (const s of e.meta?.states ?? []) stateNames.add(s.name);
|
|
3431
|
+
}
|
|
2663
3432
|
const regions = sortById(registry.list("region"));
|
|
2664
3433
|
const elements = sortById(registry.list("element"));
|
|
2665
3434
|
const primitives = sortById(registry.list("primitive"));
|
|
2666
3435
|
const flows = sortById(registry.list("flow"));
|
|
2667
|
-
const
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
lines.push(
|
|
2672
|
-
`import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`
|
|
3436
|
+
const t = [];
|
|
3437
|
+
t.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
3438
|
+
t.push(
|
|
3439
|
+
"// Types only \u2014 the runtime registry lives in the sibling `.data` module."
|
|
2673
3440
|
);
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
3441
|
+
t.push("/* eslint-disable */");
|
|
3442
|
+
t.push("");
|
|
3443
|
+
t.push("// ---- id unions ----");
|
|
3444
|
+
t.push(
|
|
3445
|
+
emitId(
|
|
2678
3446
|
"PageId",
|
|
2679
3447
|
pages.map((e) => e.id)
|
|
2680
3448
|
)
|
|
2681
3449
|
);
|
|
2682
|
-
|
|
2683
|
-
|
|
3450
|
+
t.push(
|
|
3451
|
+
emitId(
|
|
2684
3452
|
"FeatureId",
|
|
2685
3453
|
features.map((e) => e.id)
|
|
2686
3454
|
)
|
|
2687
3455
|
);
|
|
2688
|
-
|
|
2689
|
-
|
|
3456
|
+
t.push(
|
|
3457
|
+
emitId(
|
|
2690
3458
|
"WidgetId",
|
|
2691
3459
|
widgets.map((e) => e.id)
|
|
2692
3460
|
)
|
|
2693
3461
|
);
|
|
2694
|
-
|
|
2695
|
-
|
|
3462
|
+
t.push(
|
|
3463
|
+
emitId(
|
|
2696
3464
|
"RegionId",
|
|
2697
3465
|
regions.map((e) => e.id)
|
|
2698
3466
|
)
|
|
2699
3467
|
);
|
|
2700
|
-
|
|
2701
|
-
|
|
3468
|
+
t.push(
|
|
3469
|
+
emitId(
|
|
2702
3470
|
"ElementId",
|
|
2703
3471
|
elements.map((e) => e.id)
|
|
2704
3472
|
)
|
|
2705
3473
|
);
|
|
2706
|
-
|
|
2707
|
-
|
|
3474
|
+
t.push(
|
|
3475
|
+
emitId(
|
|
2708
3476
|
"PrimitiveId",
|
|
2709
3477
|
primitives.map((e) => e.id)
|
|
2710
3478
|
)
|
|
2711
3479
|
);
|
|
2712
|
-
|
|
2713
|
-
|
|
3480
|
+
t.push(
|
|
3481
|
+
emitId(
|
|
2714
3482
|
"FlowId",
|
|
2715
3483
|
flows.map((e) => e.id)
|
|
2716
3484
|
)
|
|
2717
3485
|
);
|
|
2718
|
-
|
|
2719
|
-
|
|
3486
|
+
t.push(
|
|
3487
|
+
emitId(
|
|
2720
3488
|
"RouteId",
|
|
2721
3489
|
routes.map((e) => e.path)
|
|
2722
3490
|
)
|
|
2723
3491
|
);
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
3492
|
+
t.push(emitId("StateId", [...stateNames]));
|
|
3493
|
+
t.push("");
|
|
3494
|
+
t.push("// ---- authoring-surface shape types ----");
|
|
3495
|
+
t.push("export namespace Uidex {");
|
|
3496
|
+
t.push(
|
|
3497
|
+
' export type CoreStateKind = "loading" | "empty" | "populated" | "error"'
|
|
3498
|
+
);
|
|
3499
|
+
t.push(' export type StateKind = CoreStateKind | "variant"');
|
|
3500
|
+
t.push(" export interface State {");
|
|
3501
|
+
t.push(" name: string");
|
|
3502
|
+
t.push(" kind?: StateKind");
|
|
3503
|
+
t.push(" acceptance?: readonly string[]");
|
|
3504
|
+
t.push(" description?: string");
|
|
3505
|
+
t.push(" }");
|
|
3506
|
+
t.push(" export type Waivers = Partial<Record<CoreStateKind, string>>");
|
|
3507
|
+
t.push(" export interface Page {");
|
|
3508
|
+
t.push(" page: PageId | false");
|
|
3509
|
+
t.push(" name?: string");
|
|
3510
|
+
t.push(" features?: readonly FeatureId[]");
|
|
3511
|
+
t.push(" widgets?: readonly WidgetId[]");
|
|
3512
|
+
t.push(" acceptance?: readonly string[]");
|
|
3513
|
+
t.push(" states?: readonly (string | State)[]");
|
|
3514
|
+
t.push(" capture?: boolean");
|
|
3515
|
+
t.push(" waivers?: Waivers");
|
|
3516
|
+
t.push(" description?: string");
|
|
3517
|
+
t.push(" }");
|
|
3518
|
+
t.push(" export interface Feature {");
|
|
3519
|
+
t.push(" feature: FeatureId | false");
|
|
3520
|
+
t.push(" name?: string");
|
|
3521
|
+
t.push(" features?: readonly FeatureId[]");
|
|
3522
|
+
t.push(" acceptance?: readonly string[]");
|
|
3523
|
+
t.push(" states?: readonly (string | State)[]");
|
|
3524
|
+
t.push(" description?: string");
|
|
3525
|
+
t.push(" }");
|
|
3526
|
+
t.push(" export interface Primitive {");
|
|
3527
|
+
t.push(" primitive: PrimitiveId");
|
|
3528
|
+
t.push(" name?: string");
|
|
3529
|
+
t.push(" description?: string");
|
|
3530
|
+
t.push(" }");
|
|
3531
|
+
t.push(" export interface Widget {");
|
|
3532
|
+
t.push(" widget: WidgetId");
|
|
3533
|
+
t.push(" name?: string");
|
|
3534
|
+
t.push(" acceptance?: readonly string[]");
|
|
3535
|
+
t.push(" states?: readonly (string | State)[]");
|
|
3536
|
+
t.push(" description?: string");
|
|
3537
|
+
t.push(" }");
|
|
3538
|
+
t.push(" export interface Region {");
|
|
3539
|
+
t.push(" region: RegionId | false");
|
|
3540
|
+
t.push(" name?: string");
|
|
3541
|
+
t.push(" description?: string");
|
|
3542
|
+
t.push(" }");
|
|
3543
|
+
t.push(" export interface Flow {");
|
|
3544
|
+
t.push(" flow: FlowId");
|
|
3545
|
+
t.push(" name?: string");
|
|
3546
|
+
t.push(" description?: string");
|
|
3547
|
+
t.push(" }");
|
|
3548
|
+
t.push(" export interface NotFlow {");
|
|
3549
|
+
t.push(" notFlow: true");
|
|
3550
|
+
t.push(" }");
|
|
3551
|
+
t.push("}");
|
|
3552
|
+
t.push("");
|
|
2769
3553
|
const allEntities = [
|
|
2770
3554
|
...routes,
|
|
2771
3555
|
...pages,
|
|
@@ -2776,25 +3560,39 @@ function emit(opts) {
|
|
|
2776
3560
|
...primitives,
|
|
2777
3561
|
...flows
|
|
2778
3562
|
];
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
);
|
|
2782
|
-
lines.push("");
|
|
2783
|
-
lines.push("// ---- git context ----");
|
|
3563
|
+
const authored = allEntities.filter((e) => AUTHORED_KINDS.has(e.kind));
|
|
3564
|
+
const thin = allEntities.filter((e) => !AUTHORED_KINDS.has(e.kind));
|
|
2784
3565
|
const gc = gitContext ?? { branch: null, commit: null, pr: null };
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
3566
|
+
const authoredPayload = internedPayload(authored);
|
|
3567
|
+
const d = [];
|
|
3568
|
+
d.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
3569
|
+
d.push(
|
|
3570
|
+
"// Authored-metadata registry. The thin element/region entities (source"
|
|
3571
|
+
);
|
|
3572
|
+
d.push(
|
|
3573
|
+
"// locations only) live in the sibling `.locs` module, loaded lazily."
|
|
3574
|
+
);
|
|
3575
|
+
d.push("// @ts-nocheck");
|
|
3576
|
+
d.push("/* eslint-disable */");
|
|
3577
|
+
d.push(`import { createUidex } from ${JSON.stringify(uidexImport)}`);
|
|
3578
|
+
d.push(`import type { Registry } from ${JSON.stringify(uidexImport)}`);
|
|
3579
|
+
d.push("");
|
|
3580
|
+
d.push(authoredPayload.filesDecl);
|
|
3581
|
+
d.push(HYDRATE_FN);
|
|
3582
|
+
d.push(`export const entities = ${authoredPayload.parseExpr}`);
|
|
3583
|
+
d.push("");
|
|
3584
|
+
d.push(`export const gitContext = ${jsonStable(gc)} as const`);
|
|
3585
|
+
d.push("");
|
|
3586
|
+
d.push("export function loadRegistry(target: Registry): Registry {");
|
|
3587
|
+
d.push(" for (const entity of entities) target.add(entity)");
|
|
3588
|
+
d.push(" return target");
|
|
3589
|
+
d.push("}");
|
|
3590
|
+
d.push("");
|
|
3591
|
+
d.push("export const uidex = createUidex()");
|
|
3592
|
+
d.push("for (const entity of entities) uidex.registry.add(entity)");
|
|
3593
|
+
d.push("");
|
|
3594
|
+
const locs = emitLocsModule(thin, uidexImport);
|
|
3595
|
+
return { types: t.join("\n"), data: d.join("\n"), locs };
|
|
2798
3596
|
}
|
|
2799
3597
|
|
|
2800
3598
|
// src/scanner/scan/git.ts
|
|
@@ -2905,6 +3703,68 @@ function renderSpec(args) {
|
|
|
2905
3703
|
// src/scanner/scan/pipeline.ts
|
|
2906
3704
|
import * as fs4 from "fs";
|
|
2907
3705
|
import * as path7 from "path";
|
|
3706
|
+
var DEFAULT_STATES_MANIFEST = "uidex-states.json";
|
|
3707
|
+
var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
|
|
3708
|
+
function loadPageBaseline(configDir, config) {
|
|
3709
|
+
const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
3710
|
+
const abs = path7.resolve(configDir, rel);
|
|
3711
|
+
let raw;
|
|
3712
|
+
try {
|
|
3713
|
+
raw = fs4.readFileSync(abs, "utf8");
|
|
3714
|
+
} catch {
|
|
3715
|
+
return void 0;
|
|
3716
|
+
}
|
|
3717
|
+
try {
|
|
3718
|
+
const parsed = JSON.parse(raw);
|
|
3719
|
+
if (!parsed || !Array.isArray(parsed.uncapturedPages)) return void 0;
|
|
3720
|
+
const uncapturedPages = parsed.uncapturedPages.filter(
|
|
3721
|
+
(p2) => typeof p2 === "string"
|
|
3722
|
+
);
|
|
3723
|
+
let missingKinds;
|
|
3724
|
+
const mkRaw = parsed.missingKinds;
|
|
3725
|
+
if (mkRaw && typeof mkRaw === "object" && !Array.isArray(mkRaw)) {
|
|
3726
|
+
const mk = {};
|
|
3727
|
+
for (const [route, kinds] of Object.entries(mkRaw)) {
|
|
3728
|
+
if (Array.isArray(kinds)) {
|
|
3729
|
+
mk[route] = kinds.filter((k) => typeof k === "string");
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
missingKinds = mk;
|
|
3733
|
+
}
|
|
3734
|
+
return { uncapturedPages, ...missingKinds ? { missingKinds } : {} };
|
|
3735
|
+
} catch {
|
|
3736
|
+
return void 0;
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
function loadCapturedStates(configDir, config) {
|
|
3740
|
+
const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
|
|
3741
|
+
const abs = path7.resolve(configDir, rel);
|
|
3742
|
+
let raw;
|
|
3743
|
+
try {
|
|
3744
|
+
raw = fs4.readFileSync(abs, "utf8");
|
|
3745
|
+
} catch {
|
|
3746
|
+
return void 0;
|
|
3747
|
+
}
|
|
3748
|
+
try {
|
|
3749
|
+
const parsed = JSON.parse(raw);
|
|
3750
|
+
if (!parsed || !Array.isArray(parsed.captured)) return void 0;
|
|
3751
|
+
const captured = [];
|
|
3752
|
+
for (const entry of parsed.captured) {
|
|
3753
|
+
if (!entry || typeof entry !== "object") continue;
|
|
3754
|
+
const c = entry;
|
|
3755
|
+
if (typeof c.entity !== "string" || typeof c.state !== "string") continue;
|
|
3756
|
+
const rec = { entity: c.entity, state: c.state };
|
|
3757
|
+
if (typeof c.kind === "string") rec.kind = c.kind;
|
|
3758
|
+
if (typeof c.stateKind === "string") rec.stateKind = c.stateKind;
|
|
3759
|
+
if (typeof c.url === "string") rec.url = c.url;
|
|
3760
|
+
if (typeof c.route === "string") rec.route = c.route;
|
|
3761
|
+
captured.push(rec);
|
|
3762
|
+
}
|
|
3763
|
+
return { captured };
|
|
3764
|
+
} catch {
|
|
3765
|
+
return void 0;
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
2908
3768
|
function runScan(opts = {}) {
|
|
2909
3769
|
const cwd = opts.cwd ?? process.cwd();
|
|
2910
3770
|
const configs = opts.configs ?? discover({ cwd });
|
|
@@ -2931,23 +3791,29 @@ function runOne(dc, opts) {
|
|
|
2931
3791
|
flowFiles: extractedFlows
|
|
2932
3792
|
});
|
|
2933
3793
|
const gitContext = resolveGitContext({ cwd: configDir });
|
|
2934
|
-
const
|
|
3794
|
+
const emitted = emit({
|
|
2935
3795
|
registry: resolved.registry,
|
|
2936
3796
|
gitContext
|
|
2937
3797
|
});
|
|
2938
|
-
const
|
|
2939
|
-
const
|
|
2940
|
-
|
|
3798
|
+
const typesRel = config.output;
|
|
3799
|
+
const typesPath = path7.resolve(configDir, typesRel);
|
|
3800
|
+
const dataRel = dataOutputPath(config.output);
|
|
3801
|
+
const dataPath = path7.resolve(configDir, dataRel);
|
|
3802
|
+
const locsRel = locsOutputPath(config.output);
|
|
3803
|
+
const locsPath = path7.resolve(configDir, locsRel);
|
|
3804
|
+
let typesOnDisk = null;
|
|
3805
|
+
let dataOnDisk = null;
|
|
3806
|
+
let locsOnDisk = null;
|
|
2941
3807
|
if (opts.check) {
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
existingOnDisk = null;
|
|
2946
|
-
}
|
|
3808
|
+
typesOnDisk = readOrNull(typesPath);
|
|
3809
|
+
dataOnDisk = readOrNull(dataPath);
|
|
3810
|
+
locsOnDisk = readOrNull(locsPath);
|
|
2947
3811
|
}
|
|
2948
3812
|
const hasExtractDiagnostics = [...extracted, ...extractedFlows].some(
|
|
2949
3813
|
(ef) => (ef.diagnostics?.length ?? 0) > 0
|
|
2950
3814
|
);
|
|
3815
|
+
const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
|
|
3816
|
+
const pageBaseline = opts.pageBaseline ?? loadPageBaseline(configDir, config);
|
|
2951
3817
|
let auditResult;
|
|
2952
3818
|
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
|
|
2953
3819
|
auditResult = audit({
|
|
@@ -2959,9 +3825,20 @@ function runOne(dc, opts) {
|
|
|
2959
3825
|
check: opts.check,
|
|
2960
3826
|
lint: opts.lint,
|
|
2961
3827
|
resolveDiagnostics: resolved.diagnostics,
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
3828
|
+
// The entity array (and thus the meaningful drift) lives in the data file,
|
|
3829
|
+
// so the entity-aware gen-stale diff runs against it; the types file gets a
|
|
3830
|
+
// plain content compare (see audit()).
|
|
3831
|
+
generated: emitted.data,
|
|
3832
|
+
existingOnDisk: dataOnDisk,
|
|
3833
|
+
outputPath: dataRel,
|
|
3834
|
+
typesGenerated: emitted.types,
|
|
3835
|
+
typesOnDisk,
|
|
3836
|
+
typesOutputPath: typesRel,
|
|
3837
|
+
locsGenerated: emitted.locs,
|
|
3838
|
+
locsOnDisk,
|
|
3839
|
+
locsOutputPath: locsRel,
|
|
3840
|
+
capturedStates,
|
|
3841
|
+
pageBaseline
|
|
2965
3842
|
});
|
|
2966
3843
|
}
|
|
2967
3844
|
return {
|
|
@@ -2970,13 +3847,38 @@ function runOne(dc, opts) {
|
|
|
2970
3847
|
registry: resolved.registry,
|
|
2971
3848
|
gitContext,
|
|
2972
3849
|
audit: auditResult,
|
|
2973
|
-
|
|
2974
|
-
|
|
3850
|
+
types: {
|
|
3851
|
+
generated: emitted.types,
|
|
3852
|
+
outputPath: typesPath,
|
|
3853
|
+
outputRel: typesRel
|
|
3854
|
+
},
|
|
3855
|
+
data: { generated: emitted.data, outputPath: dataPath, outputRel: dataRel },
|
|
3856
|
+
locs: { generated: emitted.locs, outputPath: locsPath, outputRel: locsRel },
|
|
3857
|
+
capturedStates
|
|
2975
3858
|
};
|
|
2976
3859
|
}
|
|
3860
|
+
function readOrNull(p2) {
|
|
3861
|
+
try {
|
|
3862
|
+
return fs4.readFileSync(p2, "utf8");
|
|
3863
|
+
} catch {
|
|
3864
|
+
return null;
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
function writeArtifact(artifact) {
|
|
3868
|
+
if (readOrNull(artifact.outputPath) === artifact.generated) return false;
|
|
3869
|
+
fs4.mkdirSync(path7.dirname(artifact.outputPath), { recursive: true });
|
|
3870
|
+
fs4.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
|
|
3871
|
+
return true;
|
|
3872
|
+
}
|
|
2977
3873
|
function writeScanResult(result) {
|
|
2978
|
-
|
|
2979
|
-
|
|
3874
|
+
const wroteTypes = writeArtifact(result.types);
|
|
3875
|
+
const wroteData = writeArtifact(result.data);
|
|
3876
|
+
const wroteLocs = writeArtifact(result.locs);
|
|
3877
|
+
return wroteTypes || wroteData || wroteLocs;
|
|
3878
|
+
}
|
|
3879
|
+
function pageBaselinePath(result) {
|
|
3880
|
+
const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
3881
|
+
return path7.resolve(result.configDir, rel);
|
|
2980
3882
|
}
|
|
2981
3883
|
|
|
2982
3884
|
// src/scanner/scan/fix.ts
|
|
@@ -3549,16 +4451,13 @@ function helpText2() {
|
|
|
3549
4451
|
" scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
|
|
3550
4452
|
" rename <element|widget|region> <old-id> <new-id> Rename an id everywhere (DOM attr, flows, exports)",
|
|
3551
4453
|
" ai <install|uninstall|providers> Manage AI assistant integrations",
|
|
3552
|
-
" api <METHOD> <PATH> Call the uidex API",
|
|
3553
|
-
" api --list Show available API routes",
|
|
3554
|
-
" api login Authenticate via browser",
|
|
3555
|
-
" api login --token <tok> Store an auth token directly",
|
|
3556
4454
|
"",
|
|
3557
4455
|
"Flags:",
|
|
3558
4456
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
3559
4457
|
" --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
|
|
3560
4458
|
" --audit Equivalent to --check --lint (read-only)",
|
|
3561
4459
|
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
|
|
4460
|
+
" --update-baseline Regenerate the page-coverage baseline (uidex-coverage-baseline.json) from the current uncaptured routes",
|
|
3562
4461
|
" --json Emit JSON diagnostics on stdout",
|
|
3563
4462
|
" --force (scaffold) overwrite existing spec",
|
|
3564
4463
|
""
|
|
@@ -3578,7 +4477,7 @@ function runInit(cwd, w) {
|
|
|
3578
4477
|
fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3579
4478
|
w.out(`Created ${configPath}`);
|
|
3580
4479
|
const gitignorePath = path11.join(cwd, ".gitignore");
|
|
3581
|
-
const entry = "*.gen
|
|
4480
|
+
const entry = "*.gen.*";
|
|
3582
4481
|
if (fs8.existsSync(gitignorePath)) {
|
|
3583
4482
|
const existing = fs8.readFileSync(gitignorePath, "utf8");
|
|
3584
4483
|
const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
|
|
@@ -3601,8 +4500,9 @@ function runInit(cwd, w) {
|
|
|
3601
4500
|
}
|
|
3602
4501
|
function runScanCommand(cwd, flags, w) {
|
|
3603
4502
|
const fix = Boolean(flags.fix);
|
|
3604
|
-
const
|
|
3605
|
-
const
|
|
4503
|
+
const updateBaseline = Boolean(flags["update-baseline"]);
|
|
4504
|
+
const check = !fix && !updateBaseline && Boolean(flags.check || flags.audit);
|
|
4505
|
+
const lint = Boolean(flags.lint || flags.audit || fix || updateBaseline);
|
|
3606
4506
|
const asJson = Boolean(flags.json);
|
|
3607
4507
|
let configs = discover({ cwd });
|
|
3608
4508
|
if (configs.length === 0) {
|
|
@@ -3621,8 +4521,33 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3621
4521
|
configs = discover({ cwd });
|
|
3622
4522
|
}
|
|
3623
4523
|
const results = runScan({ cwd, check, lint, configs });
|
|
4524
|
+
const artifacts = results.flatMap((r) => [r.types, r.data, r.locs]);
|
|
4525
|
+
const wrote = /* @__PURE__ */ new Set();
|
|
3624
4526
|
if (!check) {
|
|
3625
|
-
for (const
|
|
4527
|
+
for (const a of artifacts) {
|
|
4528
|
+
if (writeArtifact(a)) wrote.add(a);
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
4531
|
+
if (updateBaseline) {
|
|
4532
|
+
for (const r of results) {
|
|
4533
|
+
const manifest = r.capturedStates ?? { captured: [] };
|
|
4534
|
+
const pageBaseline = computePageBaseline(r.registry, manifest);
|
|
4535
|
+
const missingKinds = computeMissingKinds(r.registry, manifest);
|
|
4536
|
+
const baseline = {
|
|
4537
|
+
...pageBaseline,
|
|
4538
|
+
...Object.keys(missingKinds).length > 0 ? { missingKinds } : {}
|
|
4539
|
+
};
|
|
4540
|
+
const p2 = pageBaselinePath(r);
|
|
4541
|
+
fs8.writeFileSync(p2, JSON.stringify(baseline, null, 2) + "\n");
|
|
4542
|
+
const kindGaps = Object.values(missingKinds).reduce(
|
|
4543
|
+
(n, ks) => n + ks.length,
|
|
4544
|
+
0
|
|
4545
|
+
);
|
|
4546
|
+
w.out(
|
|
4547
|
+
`Wrote ${p2} \u2014 ${baseline.uncapturedPages.length} uncaptured page(s), ${kindGaps} missing core-kind(s) baselined`
|
|
4548
|
+
);
|
|
4549
|
+
}
|
|
4550
|
+
return w.result(0);
|
|
3626
4551
|
}
|
|
3627
4552
|
const allDiagnostics = results.flatMap((r) => r.audit?.diagnostics ?? []);
|
|
3628
4553
|
const summary = results.reduce(
|
|
@@ -3650,10 +4575,14 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3650
4575
|
);
|
|
3651
4576
|
}
|
|
3652
4577
|
for (const r of results) {
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
4578
|
+
for (const art of [r.types, r.data, r.locs]) {
|
|
4579
|
+
if (check) {
|
|
4580
|
+
w.out(`Checked ${art.outputPath}`);
|
|
4581
|
+
} else if (wrote.has(art)) {
|
|
4582
|
+
w.out(`Wrote ${art.outputPath}`);
|
|
4583
|
+
} else {
|
|
4584
|
+
w.out(`Unchanged ${art.outputPath}`);
|
|
4585
|
+
}
|
|
3657
4586
|
}
|
|
3658
4587
|
for (const d of r.audit?.diagnostics ?? []) {
|
|
3659
4588
|
const loc = d.file ? `${d.file}${d.line ? `:${d.line}` : ""}` : "";
|
|
@@ -3761,12 +4690,21 @@ export {
|
|
|
3761
4690
|
DEFAULT_CONVENTIONS,
|
|
3762
4691
|
applyFixes,
|
|
3763
4692
|
audit,
|
|
4693
|
+
checkPageCoverage,
|
|
4694
|
+
checkStateCoverage,
|
|
4695
|
+
checkStateMatrix,
|
|
4696
|
+
compileRoute,
|
|
4697
|
+
computeMissingKinds,
|
|
4698
|
+
computePageBaseline,
|
|
3764
4699
|
detectRoutes,
|
|
3765
4700
|
discover,
|
|
3766
4701
|
emit,
|
|
3767
4702
|
extract,
|
|
3768
4703
|
extractUidexExports,
|
|
3769
4704
|
globToRegExp,
|
|
4705
|
+
loadPageBaseline,
|
|
4706
|
+
matchUrlToRoute,
|
|
4707
|
+
pageBaselinePath,
|
|
3770
4708
|
parseConfig,
|
|
3771
4709
|
pathToId,
|
|
3772
4710
|
renameEntity,
|