uidex 0.7.0 → 0.8.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 +1024 -1041
- 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 +1194 -322
- package/dist/scan/index.cjs.map +1 -1
- package/dist/scan/index.d.cts +274 -8
- package/dist/scan/index.d.ts +274 -8
- package/dist/scan/index.js +1185 -322
- package/dist/scan/index.js.map +1 -1
- package/package.json +27 -31
- 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)) {
|
|
@@ -136,7 +151,9 @@ function validateConfig(raw) {
|
|
|
136
151
|
output: raw.output,
|
|
137
152
|
flows: raw.flows,
|
|
138
153
|
audit: raw.audit,
|
|
139
|
-
conventions: raw.conventions
|
|
154
|
+
conventions: raw.conventions,
|
|
155
|
+
statesManifest: raw.statesManifest,
|
|
156
|
+
coverageBaseline: raw.coverageBaseline
|
|
140
157
|
};
|
|
141
158
|
}
|
|
142
159
|
function parseConfig(json) {
|
|
@@ -225,7 +242,12 @@ var BASE_EXCLUDES = [
|
|
|
225
242
|
"**/dist/**",
|
|
226
243
|
"**/build/**",
|
|
227
244
|
"**/.next/**",
|
|
228
|
-
|
|
245
|
+
// Exclude the scanner's own generated siblings so a scan never re-ingests its
|
|
246
|
+
// output. Enumerated (not `*.gen.*`) so a hand-written file that merely contains
|
|
247
|
+
// a `.gen.` segment, e.g. `foo.gen.helpers.ts`, is still scanned.
|
|
248
|
+
"**/*.gen.ts",
|
|
249
|
+
"**/*.gen.data.ts",
|
|
250
|
+
"**/*.gen.locs.ts"
|
|
229
251
|
];
|
|
230
252
|
var TEST_EXCLUDES = [
|
|
231
253
|
"**/*.test.ts",
|
|
@@ -425,6 +447,201 @@ function unwrapTsExpression(expr) {
|
|
|
425
447
|
}
|
|
426
448
|
}
|
|
427
449
|
|
|
450
|
+
// src/shared/entities/types.ts
|
|
451
|
+
var ENTITY_KINDS = [
|
|
452
|
+
"route",
|
|
453
|
+
"page",
|
|
454
|
+
"feature",
|
|
455
|
+
"widget",
|
|
456
|
+
"region",
|
|
457
|
+
"element",
|
|
458
|
+
"primitive",
|
|
459
|
+
"flow"
|
|
460
|
+
];
|
|
461
|
+
var CORE_STATE_KINDS = [
|
|
462
|
+
"loading",
|
|
463
|
+
"empty",
|
|
464
|
+
"populated",
|
|
465
|
+
"error"
|
|
466
|
+
];
|
|
467
|
+
function isMetaKind(kind) {
|
|
468
|
+
return kind !== "route" && kind !== "flow";
|
|
469
|
+
}
|
|
470
|
+
function isMetaEntity(entity) {
|
|
471
|
+
return isMetaKind(entity.kind);
|
|
472
|
+
}
|
|
473
|
+
function entityKey(entity) {
|
|
474
|
+
return entity.kind === "route" ? entity.path : entity.id;
|
|
475
|
+
}
|
|
476
|
+
var UnknownEntityKindError = class extends Error {
|
|
477
|
+
kind;
|
|
478
|
+
constructor(kind) {
|
|
479
|
+
super(`Unknown entity kind: ${kind}`);
|
|
480
|
+
this.name = "UnknownEntityKindError";
|
|
481
|
+
this.kind = kind;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
var KIND_SET = new Set(ENTITY_KINDS);
|
|
485
|
+
function assertEntityKind(kind) {
|
|
486
|
+
if (!KIND_SET.has(kind)) throw new UnknownEntityKindError(kind);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/shared/entities/registry.ts
|
|
490
|
+
function emptyStore() {
|
|
491
|
+
return {
|
|
492
|
+
route: /* @__PURE__ */ new Map(),
|
|
493
|
+
page: /* @__PURE__ */ new Map(),
|
|
494
|
+
feature: /* @__PURE__ */ new Map(),
|
|
495
|
+
widget: /* @__PURE__ */ new Map(),
|
|
496
|
+
region: /* @__PURE__ */ new Map(),
|
|
497
|
+
element: /* @__PURE__ */ new Map(),
|
|
498
|
+
primitive: /* @__PURE__ */ new Map(),
|
|
499
|
+
flow: /* @__PURE__ */ new Map()
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function computeFlowIds(flows, targetId) {
|
|
503
|
+
const ids = [];
|
|
504
|
+
for (const flow of flows) {
|
|
505
|
+
if (flow.touches.includes(targetId)) ids.push(flow.id);
|
|
506
|
+
}
|
|
507
|
+
return ids;
|
|
508
|
+
}
|
|
509
|
+
function freezeEntity(entity, flows) {
|
|
510
|
+
if (!isMetaKind(entity.kind)) return entity;
|
|
511
|
+
const withMeta = entity;
|
|
512
|
+
if (withMeta.meta === void 0) return entity;
|
|
513
|
+
const computedFlows = Object.freeze(computeFlowIds(flows, withMeta.id));
|
|
514
|
+
const mergedMeta = { ...withMeta.meta, flows: computedFlows };
|
|
515
|
+
return { ...entity, meta: Object.freeze(mergedMeta) };
|
|
516
|
+
}
|
|
517
|
+
function createRegistry() {
|
|
518
|
+
const store = emptyStore();
|
|
519
|
+
let flowsCache = null;
|
|
520
|
+
const patternCache = /* @__PURE__ */ new Map();
|
|
521
|
+
const getFlows = () => {
|
|
522
|
+
if (flowsCache === null) flowsCache = Array.from(store.flow.values());
|
|
523
|
+
return flowsCache;
|
|
524
|
+
};
|
|
525
|
+
const add = (entity) => {
|
|
526
|
+
assertEntityKind(entity.kind);
|
|
527
|
+
const key = entityKey(entity);
|
|
528
|
+
store[entity.kind].set(key, entity);
|
|
529
|
+
flowsCache = null;
|
|
530
|
+
patternCache.delete(entity.kind);
|
|
531
|
+
};
|
|
532
|
+
const get = (kind, id) => {
|
|
533
|
+
assertEntityKind(kind);
|
|
534
|
+
const raw = store[kind].get(id);
|
|
535
|
+
if (raw === void 0) return void 0;
|
|
536
|
+
return freezeEntity(raw, getFlows());
|
|
537
|
+
};
|
|
538
|
+
const getPatternsForKind = (kind) => {
|
|
539
|
+
const cached = patternCache.get(kind);
|
|
540
|
+
if (cached !== void 0) return cached;
|
|
541
|
+
const patterns = [];
|
|
542
|
+
for (const [key, entity] of store[kind]) {
|
|
543
|
+
if (key.includes("*")) {
|
|
544
|
+
const segments = key.split("*");
|
|
545
|
+
patterns.push({
|
|
546
|
+
segments,
|
|
547
|
+
staticLength: segments.reduce((n, s) => n + s.length, 0),
|
|
548
|
+
entity
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
patternCache.set(
|
|
553
|
+
kind,
|
|
554
|
+
patterns
|
|
555
|
+
);
|
|
556
|
+
return patterns;
|
|
557
|
+
};
|
|
558
|
+
const matchesSegments = (segments, id) => {
|
|
559
|
+
const first = segments[0];
|
|
560
|
+
const last = segments[segments.length - 1];
|
|
561
|
+
if (!id.startsWith(first)) return false;
|
|
562
|
+
let pos = first.length;
|
|
563
|
+
for (let i = 1; i < segments.length - 1; i++) {
|
|
564
|
+
const idx = id.indexOf(segments[i], pos);
|
|
565
|
+
if (idx === -1) return false;
|
|
566
|
+
pos = idx + segments[i].length;
|
|
567
|
+
}
|
|
568
|
+
return id.endsWith(last) && id.length - last.length >= pos;
|
|
569
|
+
};
|
|
570
|
+
const matchPattern = (kind, id) => {
|
|
571
|
+
assertEntityKind(kind);
|
|
572
|
+
const patterns = getPatternsForKind(kind);
|
|
573
|
+
if (patterns.length === 0) return void 0;
|
|
574
|
+
let best;
|
|
575
|
+
for (const entry of patterns) {
|
|
576
|
+
if (matchesSegments(entry.segments, id) && (best === void 0 || entry.staticLength > best.staticLength)) {
|
|
577
|
+
best = entry;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (best === void 0) return void 0;
|
|
581
|
+
return freezeEntity(best.entity, getFlows());
|
|
582
|
+
};
|
|
583
|
+
const list = (kind) => {
|
|
584
|
+
assertEntityKind(kind);
|
|
585
|
+
const flows = getFlows();
|
|
586
|
+
return Array.from(
|
|
587
|
+
store[kind].values(),
|
|
588
|
+
(e) => freezeEntity(e, flows)
|
|
589
|
+
);
|
|
590
|
+
};
|
|
591
|
+
const allEntities = function* () {
|
|
592
|
+
for (const kind of Object.keys(store)) {
|
|
593
|
+
for (const entity of store[kind].values()) {
|
|
594
|
+
yield entity;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
const query = (predicate) => {
|
|
599
|
+
const flows = getFlows();
|
|
600
|
+
const result = [];
|
|
601
|
+
for (const entity of allEntities()) {
|
|
602
|
+
if (predicate(entity)) result.push(freezeEntity(entity, flows));
|
|
603
|
+
}
|
|
604
|
+
return result;
|
|
605
|
+
};
|
|
606
|
+
const byScope = (scope) => query(
|
|
607
|
+
(entity) => "scopes" in entity && Array.isArray(entity.scopes) && entity.scopes.includes(scope)
|
|
608
|
+
);
|
|
609
|
+
const touchedBy = (flowId) => {
|
|
610
|
+
const flow = store.flow.get(flowId);
|
|
611
|
+
if (flow === void 0) return [];
|
|
612
|
+
const ids = new Set(flow.touches);
|
|
613
|
+
return query((entity) => {
|
|
614
|
+
if (!isMetaEntity(entity)) return false;
|
|
615
|
+
return ids.has(entity.id);
|
|
616
|
+
});
|
|
617
|
+
};
|
|
618
|
+
const reports = /* @__PURE__ */ new Map();
|
|
619
|
+
const reportsCbs = /* @__PURE__ */ new Set();
|
|
620
|
+
const setReports = (kind, id, records) => {
|
|
621
|
+
reports.set(`${kind}:${id}`, records);
|
|
622
|
+
for (const cb of reportsCbs) cb();
|
|
623
|
+
};
|
|
624
|
+
const getReports = (kind, id) => reports.get(`${kind}:${id}`) ?? [];
|
|
625
|
+
const listReportKeys = () => Array.from(reports.keys());
|
|
626
|
+
const onReportsChange = (cb) => {
|
|
627
|
+
reportsCbs.add(cb);
|
|
628
|
+
return () => reportsCbs.delete(cb);
|
|
629
|
+
};
|
|
630
|
+
return {
|
|
631
|
+
add,
|
|
632
|
+
get,
|
|
633
|
+
matchPattern,
|
|
634
|
+
list,
|
|
635
|
+
query,
|
|
636
|
+
byScope,
|
|
637
|
+
touchedBy,
|
|
638
|
+
setReports,
|
|
639
|
+
getReports,
|
|
640
|
+
listReportKeys,
|
|
641
|
+
onReportsChange
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
428
645
|
// src/scanner/scan/extract-uidex-export.ts
|
|
429
646
|
var KIND_DISCRIMINATORS = [
|
|
430
647
|
"page",
|
|
@@ -442,20 +659,35 @@ var ALLOWED_FIELDS = {
|
|
|
442
659
|
"features",
|
|
443
660
|
"widgets",
|
|
444
661
|
"acceptance",
|
|
662
|
+
"states",
|
|
663
|
+
"capture",
|
|
664
|
+
"waivers",
|
|
445
665
|
"description"
|
|
446
666
|
]),
|
|
667
|
+
// `capture` / `waivers` are route-scoped, and only a page maps to a route — so
|
|
668
|
+
// they are page-only (the gates read them off the page). A feature/widget that
|
|
669
|
+
// sets them gets an `uidex-export-unknown-field` error rather than a silent no-op.
|
|
447
670
|
feature: /* @__PURE__ */ new Set([
|
|
448
671
|
"feature",
|
|
449
672
|
"name",
|
|
450
673
|
"features",
|
|
451
674
|
"acceptance",
|
|
675
|
+
"states",
|
|
452
676
|
"description"
|
|
453
677
|
]),
|
|
454
678
|
primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
|
|
455
|
-
widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "description"]),
|
|
679
|
+
widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "states", "description"]),
|
|
456
680
|
region: /* @__PURE__ */ new Set(["region", "name", "description"]),
|
|
457
681
|
flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
|
|
458
682
|
};
|
|
683
|
+
var STATE_FIELDS = /* @__PURE__ */ new Set([
|
|
684
|
+
"name",
|
|
685
|
+
"kind",
|
|
686
|
+
"acceptance",
|
|
687
|
+
"description"
|
|
688
|
+
]);
|
|
689
|
+
var CORE_STATE_KINDS2 = new Set(CORE_STATE_KINDS);
|
|
690
|
+
var STATE_KINDS = /* @__PURE__ */ new Set([...CORE_STATE_KINDS, "variant"]);
|
|
459
691
|
var FALSEABLE = /* @__PURE__ */ new Set([
|
|
460
692
|
"page",
|
|
461
693
|
"feature",
|
|
@@ -845,6 +1077,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
845
1077
|
}
|
|
846
1078
|
const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
847
1079
|
const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
1080
|
+
const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey) : void 0;
|
|
1081
|
+
const capture = kind === "page" ? readBooleanField(byKey, "capture") : void 0;
|
|
1082
|
+
const waivers = kind === "page" ? readWaiversField(byKey) : void 0;
|
|
848
1083
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
849
1084
|
const metadata = {
|
|
850
1085
|
source: "ts-export",
|
|
@@ -867,6 +1102,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
867
1102
|
metadata.widgets = widgetsField.values;
|
|
868
1103
|
metadata.widgetSpans = widgetsField.spans;
|
|
869
1104
|
}
|
|
1105
|
+
if (states?.length) metadata.states = states;
|
|
1106
|
+
if (capture !== void 0) metadata.capture = capture;
|
|
1107
|
+
if (waivers && Object.keys(waivers).length > 0) metadata.waivers = waivers;
|
|
870
1108
|
if (notFlow) metadata.notFlow = true;
|
|
871
1109
|
if (typeof id === "string" && idValue.kind === "string") {
|
|
872
1110
|
metadata.idSpan = idValue.span;
|
|
@@ -903,6 +1141,48 @@ function readIdField(value, kind, fieldName) {
|
|
|
903
1141
|
value.pos
|
|
904
1142
|
);
|
|
905
1143
|
}
|
|
1144
|
+
function readWaiversField(byKey) {
|
|
1145
|
+
const entry = byKey.get("waivers");
|
|
1146
|
+
if (!entry) return void 0;
|
|
1147
|
+
if (entry.value.kind !== "object") {
|
|
1148
|
+
throw new ExtractError(
|
|
1149
|
+
"uidex-export-invalid-field",
|
|
1150
|
+
"`waivers` must be an object mapping a core kind to a reason.",
|
|
1151
|
+
entry.value.pos
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
const waivers = {};
|
|
1155
|
+
for (const e of entry.value.entries) {
|
|
1156
|
+
if (!CORE_STATE_KINDS2.has(e.key)) {
|
|
1157
|
+
throw new ExtractError(
|
|
1158
|
+
"uidex-export-invalid-field",
|
|
1159
|
+
`waiver key "${e.key}" is not a core kind; only ${[...CORE_STATE_KINDS2].join(", ")} participate in the matrix.`,
|
|
1160
|
+
e.keyPos
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
if (e.value.kind !== "string" || e.value.value.length === 0) {
|
|
1164
|
+
throw new ExtractError(
|
|
1165
|
+
"uidex-export-invalid-field",
|
|
1166
|
+
`waiver "${e.key}" needs a non-empty reason (why the route cannot render this kind).`,
|
|
1167
|
+
e.value.pos
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
waivers[e.key] = e.value.value;
|
|
1171
|
+
}
|
|
1172
|
+
return waivers;
|
|
1173
|
+
}
|
|
1174
|
+
function readBooleanField(byKey, name) {
|
|
1175
|
+
const entry = byKey.get(name);
|
|
1176
|
+
if (!entry) return void 0;
|
|
1177
|
+
if (entry.value.kind !== "boolean") {
|
|
1178
|
+
throw new ExtractError(
|
|
1179
|
+
"uidex-export-invalid-field",
|
|
1180
|
+
`\`${name}\` must be a boolean.`,
|
|
1181
|
+
entry.value.pos
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
return entry.value.value;
|
|
1185
|
+
}
|
|
906
1186
|
function readStringField(byKey, name) {
|
|
907
1187
|
const entry = byKey.get(name);
|
|
908
1188
|
if (!entry) return void 0;
|
|
@@ -940,6 +1220,86 @@ function readStringArrayField(byKey, name) {
|
|
|
940
1220
|
}
|
|
941
1221
|
return { values, spans };
|
|
942
1222
|
}
|
|
1223
|
+
function readStatesField(byKey) {
|
|
1224
|
+
const entry = byKey.get("states");
|
|
1225
|
+
if (!entry) return void 0;
|
|
1226
|
+
if (entry.value.kind !== "array") {
|
|
1227
|
+
throw new ExtractError(
|
|
1228
|
+
"uidex-export-invalid-field",
|
|
1229
|
+
"`states` must be an array of state names or `{ name, acceptance?, description? }` objects.",
|
|
1230
|
+
entry.value.pos
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
const states = [];
|
|
1234
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1235
|
+
for (const item of entry.value.items) {
|
|
1236
|
+
let state;
|
|
1237
|
+
if (item.kind === "string") {
|
|
1238
|
+
state = { name: item.value, nameSpan: item.span };
|
|
1239
|
+
} else if (item.kind === "object") {
|
|
1240
|
+
const fieldByKey = /* @__PURE__ */ new Map();
|
|
1241
|
+
for (const e of item.entries) fieldByKey.set(e.key, e);
|
|
1242
|
+
for (const e of item.entries) {
|
|
1243
|
+
if (!STATE_FIELDS.has(e.key)) {
|
|
1244
|
+
throw new ExtractError(
|
|
1245
|
+
"uidex-export-invalid-field",
|
|
1246
|
+
`Unknown field "${e.key}" in a \`states\` entry. Allowed: ${Array.from(
|
|
1247
|
+
STATE_FIELDS
|
|
1248
|
+
).sort().join(", ")}.`,
|
|
1249
|
+
e.value.pos
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
const nameEntry = fieldByKey.get("name");
|
|
1254
|
+
if (!nameEntry || nameEntry.value.kind !== "string") {
|
|
1255
|
+
throw new ExtractError(
|
|
1256
|
+
"uidex-export-invalid-field",
|
|
1257
|
+
"Each object `states` entry must have a string `name`.",
|
|
1258
|
+
item.pos
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
state = { name: nameEntry.value.value, nameSpan: nameEntry.value.span };
|
|
1262
|
+
const kind = readStringField(fieldByKey, "kind");
|
|
1263
|
+
if (kind !== void 0) {
|
|
1264
|
+
if (!STATE_KINDS.has(kind)) {
|
|
1265
|
+
throw new ExtractError(
|
|
1266
|
+
"uidex-export-invalid-field",
|
|
1267
|
+
`state \`kind\` must be one of ${[...STATE_KINDS].join(", ")} (got "${kind}").`,
|
|
1268
|
+
fieldByKey.get("kind").value.pos
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
state.kind = kind;
|
|
1272
|
+
}
|
|
1273
|
+
const acceptance = readStringArrayField(fieldByKey, "acceptance")?.values;
|
|
1274
|
+
if (acceptance) state.acceptance = acceptance;
|
|
1275
|
+
const description = readStringField(fieldByKey, "description");
|
|
1276
|
+
if (description) state.description = description;
|
|
1277
|
+
} else {
|
|
1278
|
+
throw new ExtractError(
|
|
1279
|
+
"uidex-export-invalid-field",
|
|
1280
|
+
"`states` items must be strings or `{ name, ... }` objects.",
|
|
1281
|
+
item.pos
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
if (state.name.length === 0) {
|
|
1285
|
+
throw new ExtractError(
|
|
1286
|
+
"uidex-export-invalid-field",
|
|
1287
|
+
"A `states` entry has an empty `name`.",
|
|
1288
|
+
item.pos
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
if (seen.has(state.name)) {
|
|
1292
|
+
throw new ExtractError(
|
|
1293
|
+
"uidex-export-duplicate-state",
|
|
1294
|
+
`Duplicate state "${state.name}" in \`states\`; each state name must be unique within the entity.`,
|
|
1295
|
+
item.pos
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
seen.add(state.name);
|
|
1299
|
+
states.push(state);
|
|
1300
|
+
}
|
|
1301
|
+
return states;
|
|
1302
|
+
}
|
|
943
1303
|
function posAt(content, offset, p2) {
|
|
944
1304
|
const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
|
|
945
1305
|
return { offset, line: p2.lineAt(offset), column: offset - lineStart + 1 };
|
|
@@ -1440,211 +1800,22 @@ function collectImportFacts(parsed) {
|
|
|
1440
1800
|
} else {
|
|
1441
1801
|
continue;
|
|
1442
1802
|
}
|
|
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);
|
|
1803
|
+
if (!source || source.type !== "Literal") continue;
|
|
1804
|
+
if (typeof source.value !== "string") continue;
|
|
1805
|
+
out2.push({
|
|
1806
|
+
specifier: source.value,
|
|
1807
|
+
line: parsed.lineAt(stmt.start),
|
|
1808
|
+
span: { start: stmt.start, end: stmt.end },
|
|
1809
|
+
isTypeOnly,
|
|
1810
|
+
names
|
|
1619
1811
|
});
|
|
1620
|
-
}
|
|
1621
|
-
|
|
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
|
-
};
|
|
1812
|
+
}
|
|
1813
|
+
return out2;
|
|
1646
1814
|
}
|
|
1647
1815
|
|
|
1816
|
+
// src/scanner/scan/resolve.ts
|
|
1817
|
+
import * as path4 from "path";
|
|
1818
|
+
|
|
1648
1819
|
// src/scanner/scan/routes.ts
|
|
1649
1820
|
var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1650
1821
|
var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
@@ -1744,6 +1915,18 @@ function buildMetaFromExport(exp) {
|
|
|
1744
1915
|
if (exp.acceptance?.length) meta.acceptance = exp.acceptance;
|
|
1745
1916
|
if (exp.features?.length) meta.features = exp.features;
|
|
1746
1917
|
if (exp.widgets?.length) meta.widgets = exp.widgets;
|
|
1918
|
+
if (exp.states?.length) {
|
|
1919
|
+
meta.states = exp.states.map((s) => ({
|
|
1920
|
+
name: s.name,
|
|
1921
|
+
...s.kind ? { kind: s.kind } : {},
|
|
1922
|
+
...s.acceptance?.length ? { acceptance: s.acceptance } : {},
|
|
1923
|
+
...s.description ? { description: s.description } : {}
|
|
1924
|
+
}));
|
|
1925
|
+
}
|
|
1926
|
+
if (exp.capture === false) meta.capture = false;
|
|
1927
|
+
if (exp.waivers && Object.keys(exp.waivers).length > 0) {
|
|
1928
|
+
meta.waivers = exp.waivers;
|
|
1929
|
+
}
|
|
1747
1930
|
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
1748
1931
|
}
|
|
1749
1932
|
function resolve2(ctx) {
|
|
@@ -2163,6 +2346,297 @@ function dedupe(arr) {
|
|
|
2163
2346
|
|
|
2164
2347
|
// src/scanner/scan/audit.ts
|
|
2165
2348
|
import * as path5 from "path";
|
|
2349
|
+
|
|
2350
|
+
// src/scanner/scan/page-coverage.ts
|
|
2351
|
+
function compileRoute(pattern) {
|
|
2352
|
+
const segs = pattern.split("/").filter(Boolean);
|
|
2353
|
+
let specificity = 0;
|
|
2354
|
+
const optionalTail = segs.length > 0 && /^\[\[\.{3}.+\]\]$/.test(segs[segs.length - 1]);
|
|
2355
|
+
const effective = optionalTail ? segs.slice(0, -1) : segs;
|
|
2356
|
+
const parts = effective.map((seg) => {
|
|
2357
|
+
const catchAll = /^\[\.{3}.+\]$/.test(seg);
|
|
2358
|
+
const dynamic = /^\[.+\]$/.test(seg);
|
|
2359
|
+
if (catchAll) return "(?:[^/]+)(?:/[^/]+)*";
|
|
2360
|
+
if (dynamic) return "[^/]+";
|
|
2361
|
+
specificity++;
|
|
2362
|
+
return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2363
|
+
});
|
|
2364
|
+
let body = parts.length === 0 ? "" : "/" + parts.join("/");
|
|
2365
|
+
if (optionalTail) body += "(?:/[^/]+)*";
|
|
2366
|
+
if (body === "") body = "/";
|
|
2367
|
+
const re = new RegExp(`^${body}/?$`);
|
|
2368
|
+
return { test: (url) => re.test(url), specificity };
|
|
2369
|
+
}
|
|
2370
|
+
function matchUrlToRoute(url, routePaths) {
|
|
2371
|
+
const path12 = stripQuery(url);
|
|
2372
|
+
let best = null;
|
|
2373
|
+
for (const rp of routePaths) {
|
|
2374
|
+
const { test, specificity } = compileRoute(rp);
|
|
2375
|
+
if (!test(path12)) continue;
|
|
2376
|
+
if (!best || specificity > best.specificity)
|
|
2377
|
+
best = { path: rp, specificity };
|
|
2378
|
+
}
|
|
2379
|
+
return best?.path ?? null;
|
|
2380
|
+
}
|
|
2381
|
+
function stripQuery(url) {
|
|
2382
|
+
const q = url.indexOf("?");
|
|
2383
|
+
const h = url.indexOf("#");
|
|
2384
|
+
let end = url.length;
|
|
2385
|
+
if (q !== -1) end = Math.min(end, q);
|
|
2386
|
+
if (h !== -1) end = Math.min(end, h);
|
|
2387
|
+
return url.slice(0, end);
|
|
2388
|
+
}
|
|
2389
|
+
function routeForCapture(c, routes, routePaths) {
|
|
2390
|
+
if (c.route && routePaths.includes(c.route)) return c.route;
|
|
2391
|
+
if (c.url) {
|
|
2392
|
+
const m = matchUrlToRoute(c.url, routePaths);
|
|
2393
|
+
if (m) return m;
|
|
2394
|
+
}
|
|
2395
|
+
if (c.kind === "page") {
|
|
2396
|
+
const r = routes.find((r2) => r2.page === c.entity);
|
|
2397
|
+
if (r) return r.path;
|
|
2398
|
+
}
|
|
2399
|
+
return null;
|
|
2400
|
+
}
|
|
2401
|
+
function checkPageCoverage(registry, manifest, baseline) {
|
|
2402
|
+
const routes = registry.list("route");
|
|
2403
|
+
if (routes.length === 0) return [];
|
|
2404
|
+
const routePaths = routes.map((r) => r.path);
|
|
2405
|
+
const captured = manifest.captured ?? [];
|
|
2406
|
+
const optedOut = /* @__PURE__ */ new Set();
|
|
2407
|
+
for (const r of routes) {
|
|
2408
|
+
const page = registry.get("page", r.page);
|
|
2409
|
+
if (page?.meta?.capture === false) optedOut.add(r.path);
|
|
2410
|
+
}
|
|
2411
|
+
const covered = /* @__PURE__ */ new Set();
|
|
2412
|
+
for (const c of captured) {
|
|
2413
|
+
const route = routeForCapture(c, routes, routePaths);
|
|
2414
|
+
if (route) covered.add(route);
|
|
2415
|
+
}
|
|
2416
|
+
const baselineSet = new Set(baseline?.uncapturedPages ?? []);
|
|
2417
|
+
const diagnostics = [];
|
|
2418
|
+
for (const r of routes) {
|
|
2419
|
+
if (covered.has(r.path) || optedOut.has(r.path)) continue;
|
|
2420
|
+
const loc = registry.get("page", r.page)?.loc;
|
|
2421
|
+
if (baselineSet.has(r.path)) {
|
|
2422
|
+
diagnostics.push({
|
|
2423
|
+
code: "page-backlog",
|
|
2424
|
+
severity: "info",
|
|
2425
|
+
message: `route "${r.path}" has no visual-states capture (acknowledged backlog)`,
|
|
2426
|
+
file: loc?.file,
|
|
2427
|
+
line: loc?.line,
|
|
2428
|
+
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 }\`.`
|
|
2429
|
+
});
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
diagnostics.push({
|
|
2433
|
+
code: "page-no-capture",
|
|
2434
|
+
severity: "error",
|
|
2435
|
+
message: `route "${r.path}" has no visual-states capture and is not accounted for`,
|
|
2436
|
+
file: loc?.file,
|
|
2437
|
+
line: loc?.line,
|
|
2438
|
+
entity: { kind: "route", id: r.path },
|
|
2439
|
+
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).`
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
for (const b of baselineSet) {
|
|
2443
|
+
if (!routePaths.includes(b)) {
|
|
2444
|
+
diagnostics.push({
|
|
2445
|
+
code: "stale-page-baseline",
|
|
2446
|
+
severity: "warning",
|
|
2447
|
+
message: `coverage baseline lists "${b}", which is not a route in the registry`,
|
|
2448
|
+
hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
|
|
2449
|
+
});
|
|
2450
|
+
} else if (covered.has(b) || optedOut.has(b)) {
|
|
2451
|
+
diagnostics.push({
|
|
2452
|
+
code: "page-captured",
|
|
2453
|
+
severity: "warning",
|
|
2454
|
+
message: `route "${b}" is now captured but is still in the coverage baseline`,
|
|
2455
|
+
hint: `Prune it with \`uidex scan --update-baseline\` so the backlog reflects reality.`
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
return diagnostics;
|
|
2460
|
+
}
|
|
2461
|
+
function computePageBaseline(registry, manifest) {
|
|
2462
|
+
const uncaptured = [];
|
|
2463
|
+
for (const d of checkPageCoverage(registry, manifest, {
|
|
2464
|
+
uncapturedPages: []
|
|
2465
|
+
})) {
|
|
2466
|
+
if (d.code === "page-no-capture" && d.entity) uncaptured.push(d.entity.id);
|
|
2467
|
+
}
|
|
2468
|
+
return { uncapturedPages: uncaptured.sort() };
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// src/scanner/scan/state-coverage.ts
|
|
2472
|
+
var STATE_KINDS2 = ["page", "feature", "widget"];
|
|
2473
|
+
function declaredStateEntities(registry) {
|
|
2474
|
+
const out2 = [];
|
|
2475
|
+
for (const kind of STATE_KINDS2) {
|
|
2476
|
+
for (const e of registry.list(kind)) {
|
|
2477
|
+
const states = e.meta?.states;
|
|
2478
|
+
if (!states || states.length === 0) continue;
|
|
2479
|
+
out2.push({
|
|
2480
|
+
kind,
|
|
2481
|
+
id: e.id,
|
|
2482
|
+
states: states.map((s) => s.name),
|
|
2483
|
+
loc: e.loc
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
return out2;
|
|
2488
|
+
}
|
|
2489
|
+
function captureMatchesEntity(cap, ent) {
|
|
2490
|
+
if (cap.entity !== ent.id) return false;
|
|
2491
|
+
return cap.kind === void 0 || cap.kind === ent.kind;
|
|
2492
|
+
}
|
|
2493
|
+
function checkStateCoverage(registry, manifest) {
|
|
2494
|
+
const diagnostics = [];
|
|
2495
|
+
const declared = declaredStateEntities(registry);
|
|
2496
|
+
const captured = manifest.captured ?? [];
|
|
2497
|
+
for (const ent of declared) {
|
|
2498
|
+
for (const state of ent.states) {
|
|
2499
|
+
const hit = captured.some(
|
|
2500
|
+
(c) => captureMatchesEntity(c, ent) && c.state === state
|
|
2501
|
+
);
|
|
2502
|
+
if (hit) continue;
|
|
2503
|
+
diagnostics.push({
|
|
2504
|
+
code: "missing-state-capture",
|
|
2505
|
+
severity: "warning",
|
|
2506
|
+
message: `${ent.kind} "${ent.id}" declares state "${state}" but no capture produced it`,
|
|
2507
|
+
file: ent.loc?.file,
|
|
2508
|
+
line: ent.loc?.line,
|
|
2509
|
+
entity: { kind: ent.kind, id: ent.id },
|
|
2510
|
+
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.`
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
for (const cap of captured) {
|
|
2515
|
+
const candidates = declared.filter((ent2) => captureMatchesEntity(cap, ent2));
|
|
2516
|
+
if (candidates.length === 0) continue;
|
|
2517
|
+
const declaredSomewhere = candidates.some(
|
|
2518
|
+
(ent2) => ent2.states.includes(cap.state)
|
|
2519
|
+
);
|
|
2520
|
+
if (declaredSomewhere) continue;
|
|
2521
|
+
const ent = candidates[0];
|
|
2522
|
+
diagnostics.push({
|
|
2523
|
+
code: "orphan-state-capture",
|
|
2524
|
+
severity: "warning",
|
|
2525
|
+
message: `Capture produced state "${cap.state}" for ${ent.kind} "${ent.id}", which is not in its declared states [${ent.states.join(", ")}]`,
|
|
2526
|
+
file: ent.loc?.file,
|
|
2527
|
+
line: ent.loc?.line,
|
|
2528
|
+
entity: { kind: ent.kind, id: ent.id },
|
|
2529
|
+
hint: `Declare "${cap.state}" in \`export const uidex = { states: [...] }\` for "${ent.id}", or rename the capture to a declared state.`
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
return diagnostics;
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
// src/scanner/scan/state-matrix.ts
|
|
2536
|
+
var CORE_KINDS = CORE_STATE_KINDS;
|
|
2537
|
+
var STATE_ENTITY_KINDS = ["page", "feature", "widget"];
|
|
2538
|
+
function resolveMatrixKind(registry, c) {
|
|
2539
|
+
if (c.stateKind) return c.stateKind;
|
|
2540
|
+
for (const kind of STATE_ENTITY_KINDS) {
|
|
2541
|
+
const declared = registry.get(kind, c.entity)?.meta?.states?.find((s) => s.name === c.state)?.kind;
|
|
2542
|
+
if (declared) return declared;
|
|
2543
|
+
}
|
|
2544
|
+
return CORE_KINDS.includes(c.state) ? c.state : "variant";
|
|
2545
|
+
}
|
|
2546
|
+
function coreKindsByRoute(registry, manifest) {
|
|
2547
|
+
const routes = registry.list("route");
|
|
2548
|
+
const routePaths = routes.map((r) => r.path);
|
|
2549
|
+
const byRoute = /* @__PURE__ */ new Map();
|
|
2550
|
+
for (const c of manifest.captured ?? []) {
|
|
2551
|
+
const route = routeForCapture(c, routes, routePaths);
|
|
2552
|
+
if (!route) continue;
|
|
2553
|
+
if (!byRoute.has(route)) byRoute.set(route, /* @__PURE__ */ new Set());
|
|
2554
|
+
const kind = resolveMatrixKind(registry, c);
|
|
2555
|
+
if (CORE_KINDS.includes(kind)) byRoute.get(route).add(kind);
|
|
2556
|
+
}
|
|
2557
|
+
return byRoute;
|
|
2558
|
+
}
|
|
2559
|
+
function pageWaivers(registry, routePage) {
|
|
2560
|
+
return registry.get("page", routePage)?.meta?.waivers ?? {};
|
|
2561
|
+
}
|
|
2562
|
+
function isOptedOut(registry, page) {
|
|
2563
|
+
return page ? registry.get("page", page)?.meta?.capture === false : false;
|
|
2564
|
+
}
|
|
2565
|
+
function checkStateMatrix(registry, manifest, baseline) {
|
|
2566
|
+
const routes = registry.list("route");
|
|
2567
|
+
if (routes.length === 0) return [];
|
|
2568
|
+
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
2569
|
+
const captured = coreKindsByRoute(registry, manifest);
|
|
2570
|
+
const baselined = baseline?.missingKinds ?? {};
|
|
2571
|
+
const diagnostics = [];
|
|
2572
|
+
for (const [route, kinds] of captured) {
|
|
2573
|
+
const page = pageOf.get(route);
|
|
2574
|
+
if (isOptedOut(registry, page)) continue;
|
|
2575
|
+
const waivers = page ? pageWaivers(registry, page) : {};
|
|
2576
|
+
const loc = page ? registry.get("page", page)?.loc : void 0;
|
|
2577
|
+
const baselinedKinds = new Set(baselined[route] ?? []);
|
|
2578
|
+
for (const kind of CORE_KINDS) {
|
|
2579
|
+
if (kinds.has(kind)) continue;
|
|
2580
|
+
if (waivers[kind]) continue;
|
|
2581
|
+
if (baselinedKinds.has(kind)) {
|
|
2582
|
+
diagnostics.push({
|
|
2583
|
+
code: "matrix-backlog",
|
|
2584
|
+
severity: "info",
|
|
2585
|
+
message: `route "${route}" does not render core kind "${kind}" (acknowledged MISSING_KINDS backlog)`,
|
|
2586
|
+
file: loc?.file,
|
|
2587
|
+
line: loc?.line,
|
|
2588
|
+
hint: `Capture the "${kind}" state for "${route}", waive it (\`waivers: { ${kind}: "why it can't" }\` on the page), or leave it in the baseline.`
|
|
2589
|
+
});
|
|
2590
|
+
continue;
|
|
2591
|
+
}
|
|
2592
|
+
diagnostics.push({
|
|
2593
|
+
code: "core-kind",
|
|
2594
|
+
severity: "error",
|
|
2595
|
+
message: `route "${route}" is captured but never renders core kind "${kind}"`,
|
|
2596
|
+
file: loc?.file,
|
|
2597
|
+
line: loc?.line,
|
|
2598
|
+
entity: { kind: "route", id: route },
|
|
2599
|
+
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).`
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
for (const [route, kinds] of captured) {
|
|
2604
|
+
const page = pageOf.get(route);
|
|
2605
|
+
if (!page || isOptedOut(registry, page)) continue;
|
|
2606
|
+
const waivers = pageWaivers(registry, page);
|
|
2607
|
+
const loc = registry.get("page", page)?.loc;
|
|
2608
|
+
for (const kind of Object.keys(waivers)) {
|
|
2609
|
+
if (kinds.has(kind)) {
|
|
2610
|
+
diagnostics.push({
|
|
2611
|
+
code: "stale-waiver",
|
|
2612
|
+
severity: "warning",
|
|
2613
|
+
message: `page "${page}" waives core kind "${kind}", but route "${route}" does render it`,
|
|
2614
|
+
file: loc?.file,
|
|
2615
|
+
line: loc?.line,
|
|
2616
|
+
entity: { kind: "page", id: page },
|
|
2617
|
+
hint: `Remove the "${kind}" waiver \u2014 the route renders that kind, so the waiver's reason is stale.`
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
return diagnostics;
|
|
2623
|
+
}
|
|
2624
|
+
function computeMissingKinds(registry, manifest) {
|
|
2625
|
+
const routes = registry.list("route");
|
|
2626
|
+
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
2627
|
+
const captured = coreKindsByRoute(registry, manifest);
|
|
2628
|
+
const out2 = {};
|
|
2629
|
+
for (const [route, kinds] of captured) {
|
|
2630
|
+
const page = pageOf.get(route);
|
|
2631
|
+
if (isOptedOut(registry, page)) continue;
|
|
2632
|
+
const waivers = page ? pageWaivers(registry, page) : {};
|
|
2633
|
+
const missing = CORE_KINDS.filter((k) => !kinds.has(k) && !waivers[k]);
|
|
2634
|
+
if (missing.length > 0) out2[route] = missing;
|
|
2635
|
+
}
|
|
2636
|
+
return out2;
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
// src/scanner/scan/audit.ts
|
|
2166
2640
|
function audit(opts) {
|
|
2167
2641
|
const diagnostics = [];
|
|
2168
2642
|
const { registry, extracted, files, config } = opts;
|
|
@@ -2171,6 +2645,9 @@ function audit(opts) {
|
|
|
2171
2645
|
const acceptanceEnabled = config.audit?.acceptance ?? true;
|
|
2172
2646
|
const scopeLeakEnabled = config.audit?.scopeLeak ?? true;
|
|
2173
2647
|
const coverageEnabled = config.audit?.coverage ?? true;
|
|
2648
|
+
const statesEnabled = config.audit?.states ?? true;
|
|
2649
|
+
const pageCoverageEnabled = config.audit?.pageCoverage ?? true;
|
|
2650
|
+
const stateMatrixEnabled = config.audit?.stateMatrix ?? true;
|
|
2174
2651
|
if (opts.resolveDiagnostics) diagnostics.push(...opts.resolveDiagnostics);
|
|
2175
2652
|
for (const ef of extracted) {
|
|
2176
2653
|
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
@@ -2204,6 +2681,48 @@ function audit(opts) {
|
|
|
2204
2681
|
}
|
|
2205
2682
|
}
|
|
2206
2683
|
}
|
|
2684
|
+
if (check && opts.typesGenerated !== void 0) {
|
|
2685
|
+
const outRel = opts.typesOutputPath ?? config.output;
|
|
2686
|
+
const fresh = normalizeForCheck(opts.typesGenerated);
|
|
2687
|
+
if (opts.typesOnDisk === null || opts.typesOnDisk === void 0) {
|
|
2688
|
+
diagnostics.push({
|
|
2689
|
+
code: "gen-missing",
|
|
2690
|
+
severity: "error",
|
|
2691
|
+
message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
|
|
2692
|
+
file: outRel,
|
|
2693
|
+
hint: "Run `uidex scan` (without --check) to regenerate"
|
|
2694
|
+
});
|
|
2695
|
+
} else if (normalizeForCheck(opts.typesOnDisk) !== fresh) {
|
|
2696
|
+
diagnostics.push({
|
|
2697
|
+
code: "gen-stale",
|
|
2698
|
+
severity: "error",
|
|
2699
|
+
message: `Generated file "${outRel}" is stale`,
|
|
2700
|
+
file: outRel,
|
|
2701
|
+
hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
if (check && opts.locsGenerated !== void 0) {
|
|
2706
|
+
const outRel = opts.locsOutputPath ?? config.output;
|
|
2707
|
+
const fresh = normalizeForCheck(opts.locsGenerated);
|
|
2708
|
+
if (opts.locsOnDisk === null || opts.locsOnDisk === void 0) {
|
|
2709
|
+
diagnostics.push({
|
|
2710
|
+
code: "gen-missing",
|
|
2711
|
+
severity: "error",
|
|
2712
|
+
message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
|
|
2713
|
+
file: outRel,
|
|
2714
|
+
hint: "Run `uidex scan` (without --check) to regenerate"
|
|
2715
|
+
});
|
|
2716
|
+
} else if (normalizeForCheck(opts.locsOnDisk) !== fresh) {
|
|
2717
|
+
diagnostics.push({
|
|
2718
|
+
code: "gen-stale",
|
|
2719
|
+
severity: "error",
|
|
2720
|
+
message: `Generated file "${outRel}" is stale`,
|
|
2721
|
+
file: outRel,
|
|
2722
|
+
hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2207
2726
|
if (lint && acceptanceEnabled) {
|
|
2208
2727
|
for (const kind of ["widget", "feature", "page"]) {
|
|
2209
2728
|
for (const e of registry.list(kind)) {
|
|
@@ -2463,6 +2982,21 @@ function audit(opts) {
|
|
|
2463
2982
|
}
|
|
2464
2983
|
}
|
|
2465
2984
|
}
|
|
2985
|
+
if (lint && statesEnabled && opts.capturedStates) {
|
|
2986
|
+
diagnostics.push(...checkStateCoverage(registry, opts.capturedStates));
|
|
2987
|
+
}
|
|
2988
|
+
if (lint && pageCoverageEnabled && opts.capturedStates) {
|
|
2989
|
+
diagnostics.push(
|
|
2990
|
+
...checkPageCoverage(registry, opts.capturedStates, opts.pageBaseline)
|
|
2991
|
+
);
|
|
2992
|
+
}
|
|
2993
|
+
if (lint && stateMatrixEnabled && opts.capturedStates) {
|
|
2994
|
+
diagnostics.push(
|
|
2995
|
+
...checkStateMatrix(registry, opts.capturedStates, {
|
|
2996
|
+
missingKinds: opts.pageBaseline?.missingKinds ?? {}
|
|
2997
|
+
})
|
|
2998
|
+
);
|
|
2999
|
+
}
|
|
2466
3000
|
const summary = {
|
|
2467
3001
|
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
2468
3002
|
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
@@ -2627,6 +3161,148 @@ function stableReplacer(_key, value) {
|
|
|
2627
3161
|
}
|
|
2628
3162
|
|
|
2629
3163
|
// src/scanner/scan/emit.ts
|
|
3164
|
+
var AUTHORED_KINDS = /* @__PURE__ */ new Set([
|
|
3165
|
+
"route",
|
|
3166
|
+
"page",
|
|
3167
|
+
"feature",
|
|
3168
|
+
"widget",
|
|
3169
|
+
"primitive",
|
|
3170
|
+
"flow"
|
|
3171
|
+
]);
|
|
3172
|
+
function internedPayload(entities) {
|
|
3173
|
+
const fileIndex = /* @__PURE__ */ new Map();
|
|
3174
|
+
const files = [];
|
|
3175
|
+
const compact = entities.map((e) => {
|
|
3176
|
+
const loc = e.loc;
|
|
3177
|
+
if (!loc || typeof loc.file !== "string") return e;
|
|
3178
|
+
let idx = fileIndex.get(loc.file);
|
|
3179
|
+
if (idx === void 0) {
|
|
3180
|
+
idx = files.length;
|
|
3181
|
+
files.push(loc.file);
|
|
3182
|
+
fileIndex.set(loc.file, idx);
|
|
3183
|
+
}
|
|
3184
|
+
const rest = { ...loc };
|
|
3185
|
+
delete rest.file;
|
|
3186
|
+
return { ...e, loc: { f: idx, ...rest } };
|
|
3187
|
+
});
|
|
3188
|
+
return {
|
|
3189
|
+
filesDecl: `const files: readonly string[] = ${jsonStable(files)}`,
|
|
3190
|
+
parseExpr: `(JSON.parse(${JSON.stringify(
|
|
3191
|
+
JSON.stringify(compact, replacerSorted)
|
|
3192
|
+
)}) as any[]).map(hydrate)`
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
var HYDRATE_FN = [
|
|
3196
|
+
"function hydrate(e: any): any {",
|
|
3197
|
+
' if (e.loc && typeof e.loc.f === "number") {',
|
|
3198
|
+
" const { f, ...rest } = e.loc",
|
|
3199
|
+
" e.loc = { file: files[f], ...rest }",
|
|
3200
|
+
" }",
|
|
3201
|
+
" return e",
|
|
3202
|
+
"}"
|
|
3203
|
+
].join("\n");
|
|
3204
|
+
function compactMeta(meta) {
|
|
3205
|
+
if (!meta || typeof meta !== "object") return meta;
|
|
3206
|
+
const composes = meta.composes;
|
|
3207
|
+
if (!Array.isArray(composes)) return meta;
|
|
3208
|
+
return {
|
|
3209
|
+
...meta,
|
|
3210
|
+
composes: composes.map(
|
|
3211
|
+
(c) => c && typeof c === "object" && c.kind === "element" && Object.keys(c).length === 2 && typeof c.id === "string" ? c.id : c
|
|
3212
|
+
)
|
|
3213
|
+
};
|
|
3214
|
+
}
|
|
3215
|
+
function emitLocsModule(thin, uidexImport) {
|
|
3216
|
+
const kinds = [...new Set(thin.map((e) => e.kind))].sort();
|
|
3217
|
+
const kindIdx = new Map(kinds.map((k, i) => [k, i]));
|
|
3218
|
+
const fileIndex = /* @__PURE__ */ new Map();
|
|
3219
|
+
const files = [];
|
|
3220
|
+
const fi = (f) => {
|
|
3221
|
+
let i = fileIndex.get(f);
|
|
3222
|
+
if (i === void 0) {
|
|
3223
|
+
i = files.length;
|
|
3224
|
+
files.push(f);
|
|
3225
|
+
fileIndex.set(f, i);
|
|
3226
|
+
}
|
|
3227
|
+
return i;
|
|
3228
|
+
};
|
|
3229
|
+
const bare = [];
|
|
3230
|
+
const rich = [];
|
|
3231
|
+
for (const e of thin) {
|
|
3232
|
+
const loc = e.loc;
|
|
3233
|
+
const topOnlyKindIdLoc = Object.keys(e).every(
|
|
3234
|
+
(k) => k === "kind" || k === "id" || k === "loc"
|
|
3235
|
+
);
|
|
3236
|
+
const locOnlyKnown = !!loc && typeof loc.file === "string" && Object.keys(loc).every(
|
|
3237
|
+
(k) => k === "file" || k === "line" || k === "column"
|
|
3238
|
+
);
|
|
3239
|
+
if (topOnlyKindIdLoc && locOnlyKnown) {
|
|
3240
|
+
const row = [
|
|
3241
|
+
kindIdx.get(e.kind),
|
|
3242
|
+
e.id,
|
|
3243
|
+
fi(loc.file)
|
|
3244
|
+
];
|
|
3245
|
+
if (loc.line != null || loc.column != null)
|
|
3246
|
+
row.push(Number(loc.line ?? 0));
|
|
3247
|
+
if (loc.column != null) row.push(Number(loc.column));
|
|
3248
|
+
bare.push(row);
|
|
3249
|
+
} else {
|
|
3250
|
+
const richEntity = { ...e };
|
|
3251
|
+
if (loc && typeof loc.file === "string") {
|
|
3252
|
+
const { file, ...rest } = loc;
|
|
3253
|
+
richEntity.loc = { f: fi(file), ...rest };
|
|
3254
|
+
}
|
|
3255
|
+
if (richEntity.meta) richEntity.meta = compactMeta(richEntity.meta);
|
|
3256
|
+
rich.push(richEntity);
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
const str = (v) => JSON.stringify(JSON.stringify(v, replacerSorted));
|
|
3260
|
+
const l = [];
|
|
3261
|
+
l.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
3262
|
+
l.push(
|
|
3263
|
+
"// Deferred element/region source locations for the inspector's copy-path."
|
|
3264
|
+
);
|
|
3265
|
+
l.push("// @ts-nocheck");
|
|
3266
|
+
l.push("/* eslint-disable */");
|
|
3267
|
+
l.push(`import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`);
|
|
3268
|
+
l.push("");
|
|
3269
|
+
l.push(`const files: readonly string[] = ${jsonStable(files)}`);
|
|
3270
|
+
l.push(`const kinds: readonly string[] = ${jsonStable(kinds)}`);
|
|
3271
|
+
l.push("function loc(fi: number, line?: number, col?: number) {");
|
|
3272
|
+
l.push(" const o: any = { file: files[fi] }");
|
|
3273
|
+
l.push(" if (line) o.line = line");
|
|
3274
|
+
l.push(" if (col) o.column = col");
|
|
3275
|
+
l.push(" return o");
|
|
3276
|
+
l.push("}");
|
|
3277
|
+
l.push(`const bare: any[] = JSON.parse(${str(bare)})`);
|
|
3278
|
+
l.push(`const rich: any[] = JSON.parse(${str(rich)})`);
|
|
3279
|
+
l.push("");
|
|
3280
|
+
l.push(
|
|
3281
|
+
"/** Add the thin element/region entities (source locations) to a registry. */"
|
|
3282
|
+
);
|
|
3283
|
+
l.push("export function loadLocs(target: Registry): Registry {");
|
|
3284
|
+
l.push(" for (const r of bare)");
|
|
3285
|
+
l.push(
|
|
3286
|
+
" target.add({ kind: kinds[r[0]], id: r[1], loc: loc(r[2], r[3], r[4]) } as Entity)"
|
|
3287
|
+
);
|
|
3288
|
+
l.push(" for (const e of rich) {");
|
|
3289
|
+
l.push(" const out: any = { ...e }");
|
|
3290
|
+
l.push(' if (e.loc && typeof e.loc.f === "number") {');
|
|
3291
|
+
l.push(" const { f, ...rest } = e.loc");
|
|
3292
|
+
l.push(" out.loc = { file: files[f], ...rest }");
|
|
3293
|
+
l.push(" }");
|
|
3294
|
+
l.push(" if (e.meta && Array.isArray(e.meta.composes)) {");
|
|
3295
|
+
l.push(
|
|
3296
|
+
' out.meta = { ...e.meta, composes: e.meta.composes.map((c: any) => typeof c === "string" ? { id: c, kind: "element" } : c) }'
|
|
3297
|
+
);
|
|
3298
|
+
l.push(" }");
|
|
3299
|
+
l.push(" target.add(out)");
|
|
3300
|
+
l.push(" }");
|
|
3301
|
+
l.push(" return target");
|
|
3302
|
+
l.push("}");
|
|
3303
|
+
l.push("");
|
|
3304
|
+
return l.join("\n");
|
|
3305
|
+
}
|
|
2630
3306
|
function sortById(arr) {
|
|
2631
3307
|
return [...arr].sort((a, b) => a.id.localeCompare(b.id));
|
|
2632
3308
|
}
|
|
@@ -2652,6 +3328,20 @@ function emitIdUnion(name, ids) {
|
|
|
2652
3328
|
${body}
|
|
2653
3329
|
`;
|
|
2654
3330
|
}
|
|
3331
|
+
var OPAQUE_ID_UNIONS = /* @__PURE__ */ new Set(["ElementId"]);
|
|
3332
|
+
function emitId(name, ids) {
|
|
3333
|
+
if (OPAQUE_ID_UNIONS.has(name)) {
|
|
3334
|
+
return `export type ${name} = string & { readonly __uidex?: ${JSON.stringify(name)} }
|
|
3335
|
+
`;
|
|
3336
|
+
}
|
|
3337
|
+
return emitIdUnion(name, ids);
|
|
3338
|
+
}
|
|
3339
|
+
function dataOutputPath(output) {
|
|
3340
|
+
return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".data.$1");
|
|
3341
|
+
}
|
|
3342
|
+
function locsOutputPath(output) {
|
|
3343
|
+
return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".locs.$1");
|
|
3344
|
+
}
|
|
2655
3345
|
function emit(opts) {
|
|
2656
3346
|
const { registry, gitContext, uidexImport = "uidex" } = opts;
|
|
2657
3347
|
const routes = [...registry.list("route")].sort(
|
|
@@ -2660,112 +3350,131 @@ function emit(opts) {
|
|
|
2660
3350
|
const pages = sortById(registry.list("page"));
|
|
2661
3351
|
const features = sortById(registry.list("feature"));
|
|
2662
3352
|
const widgets = sortById(registry.list("widget"));
|
|
3353
|
+
const stateNames = /* @__PURE__ */ new Set();
|
|
3354
|
+
for (const e of [...pages, ...features, ...widgets]) {
|
|
3355
|
+
for (const s of e.meta?.states ?? []) stateNames.add(s.name);
|
|
3356
|
+
}
|
|
2663
3357
|
const regions = sortById(registry.list("region"));
|
|
2664
3358
|
const elements = sortById(registry.list("element"));
|
|
2665
3359
|
const primitives = sortById(registry.list("primitive"));
|
|
2666
3360
|
const flows = sortById(registry.list("flow"));
|
|
2667
|
-
const
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
lines.push(
|
|
2672
|
-
`import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`
|
|
3361
|
+
const t = [];
|
|
3362
|
+
t.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
3363
|
+
t.push(
|
|
3364
|
+
"// Types only \u2014 the runtime registry lives in the sibling `.data` module."
|
|
2673
3365
|
);
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
3366
|
+
t.push("/* eslint-disable */");
|
|
3367
|
+
t.push("");
|
|
3368
|
+
t.push("// ---- id unions ----");
|
|
3369
|
+
t.push(
|
|
3370
|
+
emitId(
|
|
2678
3371
|
"PageId",
|
|
2679
3372
|
pages.map((e) => e.id)
|
|
2680
3373
|
)
|
|
2681
3374
|
);
|
|
2682
|
-
|
|
2683
|
-
|
|
3375
|
+
t.push(
|
|
3376
|
+
emitId(
|
|
2684
3377
|
"FeatureId",
|
|
2685
3378
|
features.map((e) => e.id)
|
|
2686
3379
|
)
|
|
2687
3380
|
);
|
|
2688
|
-
|
|
2689
|
-
|
|
3381
|
+
t.push(
|
|
3382
|
+
emitId(
|
|
2690
3383
|
"WidgetId",
|
|
2691
3384
|
widgets.map((e) => e.id)
|
|
2692
3385
|
)
|
|
2693
3386
|
);
|
|
2694
|
-
|
|
2695
|
-
|
|
3387
|
+
t.push(
|
|
3388
|
+
emitId(
|
|
2696
3389
|
"RegionId",
|
|
2697
3390
|
regions.map((e) => e.id)
|
|
2698
3391
|
)
|
|
2699
3392
|
);
|
|
2700
|
-
|
|
2701
|
-
|
|
3393
|
+
t.push(
|
|
3394
|
+
emitId(
|
|
2702
3395
|
"ElementId",
|
|
2703
3396
|
elements.map((e) => e.id)
|
|
2704
3397
|
)
|
|
2705
3398
|
);
|
|
2706
|
-
|
|
2707
|
-
|
|
3399
|
+
t.push(
|
|
3400
|
+
emitId(
|
|
2708
3401
|
"PrimitiveId",
|
|
2709
3402
|
primitives.map((e) => e.id)
|
|
2710
3403
|
)
|
|
2711
3404
|
);
|
|
2712
|
-
|
|
2713
|
-
|
|
3405
|
+
t.push(
|
|
3406
|
+
emitId(
|
|
2714
3407
|
"FlowId",
|
|
2715
3408
|
flows.map((e) => e.id)
|
|
2716
3409
|
)
|
|
2717
3410
|
);
|
|
2718
|
-
|
|
2719
|
-
|
|
3411
|
+
t.push(
|
|
3412
|
+
emitId(
|
|
2720
3413
|
"RouteId",
|
|
2721
3414
|
routes.map((e) => e.path)
|
|
2722
3415
|
)
|
|
2723
3416
|
);
|
|
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
|
-
|
|
3417
|
+
t.push(emitId("StateId", [...stateNames]));
|
|
3418
|
+
t.push("");
|
|
3419
|
+
t.push("// ---- authoring-surface shape types ----");
|
|
3420
|
+
t.push("export namespace Uidex {");
|
|
3421
|
+
t.push(
|
|
3422
|
+
' export type CoreStateKind = "loading" | "empty" | "populated" | "error"'
|
|
3423
|
+
);
|
|
3424
|
+
t.push(' export type StateKind = CoreStateKind | "variant"');
|
|
3425
|
+
t.push(" export interface State {");
|
|
3426
|
+
t.push(" name: string");
|
|
3427
|
+
t.push(" kind?: StateKind");
|
|
3428
|
+
t.push(" acceptance?: readonly string[]");
|
|
3429
|
+
t.push(" description?: string");
|
|
3430
|
+
t.push(" }");
|
|
3431
|
+
t.push(" export type Waivers = Partial<Record<CoreStateKind, string>>");
|
|
3432
|
+
t.push(" export interface Page {");
|
|
3433
|
+
t.push(" page: PageId | false");
|
|
3434
|
+
t.push(" name?: string");
|
|
3435
|
+
t.push(" features?: readonly FeatureId[]");
|
|
3436
|
+
t.push(" widgets?: readonly WidgetId[]");
|
|
3437
|
+
t.push(" acceptance?: readonly string[]");
|
|
3438
|
+
t.push(" states?: readonly (string | State)[]");
|
|
3439
|
+
t.push(" capture?: boolean");
|
|
3440
|
+
t.push(" waivers?: Waivers");
|
|
3441
|
+
t.push(" description?: string");
|
|
3442
|
+
t.push(" }");
|
|
3443
|
+
t.push(" export interface Feature {");
|
|
3444
|
+
t.push(" feature: FeatureId | false");
|
|
3445
|
+
t.push(" name?: string");
|
|
3446
|
+
t.push(" features?: readonly FeatureId[]");
|
|
3447
|
+
t.push(" acceptance?: readonly string[]");
|
|
3448
|
+
t.push(" states?: readonly (string | State)[]");
|
|
3449
|
+
t.push(" description?: string");
|
|
3450
|
+
t.push(" }");
|
|
3451
|
+
t.push(" export interface Primitive {");
|
|
3452
|
+
t.push(" primitive: PrimitiveId");
|
|
3453
|
+
t.push(" name?: string");
|
|
3454
|
+
t.push(" description?: string");
|
|
3455
|
+
t.push(" }");
|
|
3456
|
+
t.push(" export interface Widget {");
|
|
3457
|
+
t.push(" widget: WidgetId");
|
|
3458
|
+
t.push(" name?: string");
|
|
3459
|
+
t.push(" acceptance?: readonly string[]");
|
|
3460
|
+
t.push(" states?: readonly (string | State)[]");
|
|
3461
|
+
t.push(" description?: string");
|
|
3462
|
+
t.push(" }");
|
|
3463
|
+
t.push(" export interface Region {");
|
|
3464
|
+
t.push(" region: RegionId | false");
|
|
3465
|
+
t.push(" name?: string");
|
|
3466
|
+
t.push(" description?: string");
|
|
3467
|
+
t.push(" }");
|
|
3468
|
+
t.push(" export interface Flow {");
|
|
3469
|
+
t.push(" flow: FlowId");
|
|
3470
|
+
t.push(" name?: string");
|
|
3471
|
+
t.push(" description?: string");
|
|
3472
|
+
t.push(" }");
|
|
3473
|
+
t.push(" export interface NotFlow {");
|
|
3474
|
+
t.push(" notFlow: true");
|
|
3475
|
+
t.push(" }");
|
|
3476
|
+
t.push("}");
|
|
3477
|
+
t.push("");
|
|
2769
3478
|
const allEntities = [
|
|
2770
3479
|
...routes,
|
|
2771
3480
|
...pages,
|
|
@@ -2776,25 +3485,39 @@ function emit(opts) {
|
|
|
2776
3485
|
...primitives,
|
|
2777
3486
|
...flows
|
|
2778
3487
|
];
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
);
|
|
2782
|
-
lines.push("");
|
|
2783
|
-
lines.push("// ---- git context ----");
|
|
3488
|
+
const authored = allEntities.filter((e) => AUTHORED_KINDS.has(e.kind));
|
|
3489
|
+
const thin = allEntities.filter((e) => !AUTHORED_KINDS.has(e.kind));
|
|
2784
3490
|
const gc = gitContext ?? { branch: null, commit: null, pr: null };
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
3491
|
+
const authoredPayload = internedPayload(authored);
|
|
3492
|
+
const d = [];
|
|
3493
|
+
d.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
3494
|
+
d.push(
|
|
3495
|
+
"// Authored-metadata registry. The thin element/region entities (source"
|
|
3496
|
+
);
|
|
3497
|
+
d.push(
|
|
3498
|
+
"// locations only) live in the sibling `.locs` module, loaded lazily."
|
|
3499
|
+
);
|
|
3500
|
+
d.push("// @ts-nocheck");
|
|
3501
|
+
d.push("/* eslint-disable */");
|
|
3502
|
+
d.push(`import { createUidex } from ${JSON.stringify(uidexImport)}`);
|
|
3503
|
+
d.push(`import type { Registry } from ${JSON.stringify(uidexImport)}`);
|
|
3504
|
+
d.push("");
|
|
3505
|
+
d.push(authoredPayload.filesDecl);
|
|
3506
|
+
d.push(HYDRATE_FN);
|
|
3507
|
+
d.push(`export const entities = ${authoredPayload.parseExpr}`);
|
|
3508
|
+
d.push("");
|
|
3509
|
+
d.push(`export const gitContext = ${jsonStable(gc)} as const`);
|
|
3510
|
+
d.push("");
|
|
3511
|
+
d.push("export function loadRegistry(target: Registry): Registry {");
|
|
3512
|
+
d.push(" for (const entity of entities) target.add(entity)");
|
|
3513
|
+
d.push(" return target");
|
|
3514
|
+
d.push("}");
|
|
3515
|
+
d.push("");
|
|
3516
|
+
d.push("export const uidex = createUidex()");
|
|
3517
|
+
d.push("for (const entity of entities) uidex.registry.add(entity)");
|
|
3518
|
+
d.push("");
|
|
3519
|
+
const locs = emitLocsModule(thin, uidexImport);
|
|
3520
|
+
return { types: t.join("\n"), data: d.join("\n"), locs };
|
|
2798
3521
|
}
|
|
2799
3522
|
|
|
2800
3523
|
// src/scanner/scan/git.ts
|
|
@@ -2905,6 +3628,68 @@ function renderSpec(args) {
|
|
|
2905
3628
|
// src/scanner/scan/pipeline.ts
|
|
2906
3629
|
import * as fs4 from "fs";
|
|
2907
3630
|
import * as path7 from "path";
|
|
3631
|
+
var DEFAULT_STATES_MANIFEST = "uidex-states.json";
|
|
3632
|
+
var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
|
|
3633
|
+
function loadPageBaseline(configDir, config) {
|
|
3634
|
+
const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
3635
|
+
const abs = path7.resolve(configDir, rel);
|
|
3636
|
+
let raw;
|
|
3637
|
+
try {
|
|
3638
|
+
raw = fs4.readFileSync(abs, "utf8");
|
|
3639
|
+
} catch {
|
|
3640
|
+
return void 0;
|
|
3641
|
+
}
|
|
3642
|
+
try {
|
|
3643
|
+
const parsed = JSON.parse(raw);
|
|
3644
|
+
if (!parsed || !Array.isArray(parsed.uncapturedPages)) return void 0;
|
|
3645
|
+
const uncapturedPages = parsed.uncapturedPages.filter(
|
|
3646
|
+
(p2) => typeof p2 === "string"
|
|
3647
|
+
);
|
|
3648
|
+
let missingKinds;
|
|
3649
|
+
const mkRaw = parsed.missingKinds;
|
|
3650
|
+
if (mkRaw && typeof mkRaw === "object" && !Array.isArray(mkRaw)) {
|
|
3651
|
+
const mk = {};
|
|
3652
|
+
for (const [route, kinds] of Object.entries(mkRaw)) {
|
|
3653
|
+
if (Array.isArray(kinds)) {
|
|
3654
|
+
mk[route] = kinds.filter((k) => typeof k === "string");
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
missingKinds = mk;
|
|
3658
|
+
}
|
|
3659
|
+
return { uncapturedPages, ...missingKinds ? { missingKinds } : {} };
|
|
3660
|
+
} catch {
|
|
3661
|
+
return void 0;
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
function loadCapturedStates(configDir, config) {
|
|
3665
|
+
const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
|
|
3666
|
+
const abs = path7.resolve(configDir, rel);
|
|
3667
|
+
let raw;
|
|
3668
|
+
try {
|
|
3669
|
+
raw = fs4.readFileSync(abs, "utf8");
|
|
3670
|
+
} catch {
|
|
3671
|
+
return void 0;
|
|
3672
|
+
}
|
|
3673
|
+
try {
|
|
3674
|
+
const parsed = JSON.parse(raw);
|
|
3675
|
+
if (!parsed || !Array.isArray(parsed.captured)) return void 0;
|
|
3676
|
+
const captured = [];
|
|
3677
|
+
for (const entry of parsed.captured) {
|
|
3678
|
+
if (!entry || typeof entry !== "object") continue;
|
|
3679
|
+
const c = entry;
|
|
3680
|
+
if (typeof c.entity !== "string" || typeof c.state !== "string") continue;
|
|
3681
|
+
const rec = { entity: c.entity, state: c.state };
|
|
3682
|
+
if (typeof c.kind === "string") rec.kind = c.kind;
|
|
3683
|
+
if (typeof c.stateKind === "string") rec.stateKind = c.stateKind;
|
|
3684
|
+
if (typeof c.url === "string") rec.url = c.url;
|
|
3685
|
+
if (typeof c.route === "string") rec.route = c.route;
|
|
3686
|
+
captured.push(rec);
|
|
3687
|
+
}
|
|
3688
|
+
return { captured };
|
|
3689
|
+
} catch {
|
|
3690
|
+
return void 0;
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
2908
3693
|
function runScan(opts = {}) {
|
|
2909
3694
|
const cwd = opts.cwd ?? process.cwd();
|
|
2910
3695
|
const configs = opts.configs ?? discover({ cwd });
|
|
@@ -2931,23 +3716,29 @@ function runOne(dc, opts) {
|
|
|
2931
3716
|
flowFiles: extractedFlows
|
|
2932
3717
|
});
|
|
2933
3718
|
const gitContext = resolveGitContext({ cwd: configDir });
|
|
2934
|
-
const
|
|
3719
|
+
const emitted = emit({
|
|
2935
3720
|
registry: resolved.registry,
|
|
2936
3721
|
gitContext
|
|
2937
3722
|
});
|
|
2938
|
-
const
|
|
2939
|
-
const
|
|
2940
|
-
|
|
3723
|
+
const typesRel = config.output;
|
|
3724
|
+
const typesPath = path7.resolve(configDir, typesRel);
|
|
3725
|
+
const dataRel = dataOutputPath(config.output);
|
|
3726
|
+
const dataPath = path7.resolve(configDir, dataRel);
|
|
3727
|
+
const locsRel = locsOutputPath(config.output);
|
|
3728
|
+
const locsPath = path7.resolve(configDir, locsRel);
|
|
3729
|
+
let typesOnDisk = null;
|
|
3730
|
+
let dataOnDisk = null;
|
|
3731
|
+
let locsOnDisk = null;
|
|
2941
3732
|
if (opts.check) {
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
existingOnDisk = null;
|
|
2946
|
-
}
|
|
3733
|
+
typesOnDisk = readOrNull(typesPath);
|
|
3734
|
+
dataOnDisk = readOrNull(dataPath);
|
|
3735
|
+
locsOnDisk = readOrNull(locsPath);
|
|
2947
3736
|
}
|
|
2948
3737
|
const hasExtractDiagnostics = [...extracted, ...extractedFlows].some(
|
|
2949
3738
|
(ef) => (ef.diagnostics?.length ?? 0) > 0
|
|
2950
3739
|
);
|
|
3740
|
+
const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
|
|
3741
|
+
const pageBaseline = opts.pageBaseline ?? loadPageBaseline(configDir, config);
|
|
2951
3742
|
let auditResult;
|
|
2952
3743
|
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
|
|
2953
3744
|
auditResult = audit({
|
|
@@ -2959,9 +3750,20 @@ function runOne(dc, opts) {
|
|
|
2959
3750
|
check: opts.check,
|
|
2960
3751
|
lint: opts.lint,
|
|
2961
3752
|
resolveDiagnostics: resolved.diagnostics,
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
3753
|
+
// The entity array (and thus the meaningful drift) lives in the data file,
|
|
3754
|
+
// so the entity-aware gen-stale diff runs against it; the types file gets a
|
|
3755
|
+
// plain content compare (see audit()).
|
|
3756
|
+
generated: emitted.data,
|
|
3757
|
+
existingOnDisk: dataOnDisk,
|
|
3758
|
+
outputPath: dataRel,
|
|
3759
|
+
typesGenerated: emitted.types,
|
|
3760
|
+
typesOnDisk,
|
|
3761
|
+
typesOutputPath: typesRel,
|
|
3762
|
+
locsGenerated: emitted.locs,
|
|
3763
|
+
locsOnDisk,
|
|
3764
|
+
locsOutputPath: locsRel,
|
|
3765
|
+
capturedStates,
|
|
3766
|
+
pageBaseline
|
|
2965
3767
|
});
|
|
2966
3768
|
}
|
|
2967
3769
|
return {
|
|
@@ -2970,13 +3772,38 @@ function runOne(dc, opts) {
|
|
|
2970
3772
|
registry: resolved.registry,
|
|
2971
3773
|
gitContext,
|
|
2972
3774
|
audit: auditResult,
|
|
2973
|
-
|
|
2974
|
-
|
|
3775
|
+
types: {
|
|
3776
|
+
generated: emitted.types,
|
|
3777
|
+
outputPath: typesPath,
|
|
3778
|
+
outputRel: typesRel
|
|
3779
|
+
},
|
|
3780
|
+
data: { generated: emitted.data, outputPath: dataPath, outputRel: dataRel },
|
|
3781
|
+
locs: { generated: emitted.locs, outputPath: locsPath, outputRel: locsRel },
|
|
3782
|
+
capturedStates
|
|
2975
3783
|
};
|
|
2976
3784
|
}
|
|
3785
|
+
function readOrNull(p2) {
|
|
3786
|
+
try {
|
|
3787
|
+
return fs4.readFileSync(p2, "utf8");
|
|
3788
|
+
} catch {
|
|
3789
|
+
return null;
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
function writeArtifact(artifact) {
|
|
3793
|
+
if (readOrNull(artifact.outputPath) === artifact.generated) return false;
|
|
3794
|
+
fs4.mkdirSync(path7.dirname(artifact.outputPath), { recursive: true });
|
|
3795
|
+
fs4.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
|
|
3796
|
+
return true;
|
|
3797
|
+
}
|
|
2977
3798
|
function writeScanResult(result) {
|
|
2978
|
-
|
|
2979
|
-
|
|
3799
|
+
const wroteTypes = writeArtifact(result.types);
|
|
3800
|
+
const wroteData = writeArtifact(result.data);
|
|
3801
|
+
const wroteLocs = writeArtifact(result.locs);
|
|
3802
|
+
return wroteTypes || wroteData || wroteLocs;
|
|
3803
|
+
}
|
|
3804
|
+
function pageBaselinePath(result) {
|
|
3805
|
+
const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
3806
|
+
return path7.resolve(result.configDir, rel);
|
|
2980
3807
|
}
|
|
2981
3808
|
|
|
2982
3809
|
// src/scanner/scan/fix.ts
|
|
@@ -3549,16 +4376,13 @@ function helpText2() {
|
|
|
3549
4376
|
" scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
|
|
3550
4377
|
" rename <element|widget|region> <old-id> <new-id> Rename an id everywhere (DOM attr, flows, exports)",
|
|
3551
4378
|
" 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
4379
|
"",
|
|
3557
4380
|
"Flags:",
|
|
3558
4381
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
3559
4382
|
" --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
|
|
3560
4383
|
" --audit Equivalent to --check --lint (read-only)",
|
|
3561
4384
|
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
|
|
4385
|
+
" --update-baseline Regenerate the page-coverage baseline (uidex-coverage-baseline.json) from the current uncaptured routes",
|
|
3562
4386
|
" --json Emit JSON diagnostics on stdout",
|
|
3563
4387
|
" --force (scaffold) overwrite existing spec",
|
|
3564
4388
|
""
|
|
@@ -3578,7 +4402,7 @@ function runInit(cwd, w) {
|
|
|
3578
4402
|
fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3579
4403
|
w.out(`Created ${configPath}`);
|
|
3580
4404
|
const gitignorePath = path11.join(cwd, ".gitignore");
|
|
3581
|
-
const entry = "*.gen
|
|
4405
|
+
const entry = "*.gen.*";
|
|
3582
4406
|
if (fs8.existsSync(gitignorePath)) {
|
|
3583
4407
|
const existing = fs8.readFileSync(gitignorePath, "utf8");
|
|
3584
4408
|
const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
|
|
@@ -3601,8 +4425,9 @@ function runInit(cwd, w) {
|
|
|
3601
4425
|
}
|
|
3602
4426
|
function runScanCommand(cwd, flags, w) {
|
|
3603
4427
|
const fix = Boolean(flags.fix);
|
|
3604
|
-
const
|
|
3605
|
-
const
|
|
4428
|
+
const updateBaseline = Boolean(flags["update-baseline"]);
|
|
4429
|
+
const check = !fix && !updateBaseline && Boolean(flags.check || flags.audit);
|
|
4430
|
+
const lint = Boolean(flags.lint || flags.audit || fix || updateBaseline);
|
|
3606
4431
|
const asJson = Boolean(flags.json);
|
|
3607
4432
|
let configs = discover({ cwd });
|
|
3608
4433
|
if (configs.length === 0) {
|
|
@@ -3621,8 +4446,33 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3621
4446
|
configs = discover({ cwd });
|
|
3622
4447
|
}
|
|
3623
4448
|
const results = runScan({ cwd, check, lint, configs });
|
|
4449
|
+
const artifacts = results.flatMap((r) => [r.types, r.data, r.locs]);
|
|
4450
|
+
const wrote = /* @__PURE__ */ new Set();
|
|
3624
4451
|
if (!check) {
|
|
3625
|
-
for (const
|
|
4452
|
+
for (const a of artifacts) {
|
|
4453
|
+
if (writeArtifact(a)) wrote.add(a);
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
if (updateBaseline) {
|
|
4457
|
+
for (const r of results) {
|
|
4458
|
+
const manifest = r.capturedStates ?? { captured: [] };
|
|
4459
|
+
const pageBaseline = computePageBaseline(r.registry, manifest);
|
|
4460
|
+
const missingKinds = computeMissingKinds(r.registry, manifest);
|
|
4461
|
+
const baseline = {
|
|
4462
|
+
...pageBaseline,
|
|
4463
|
+
...Object.keys(missingKinds).length > 0 ? { missingKinds } : {}
|
|
4464
|
+
};
|
|
4465
|
+
const p2 = pageBaselinePath(r);
|
|
4466
|
+
fs8.writeFileSync(p2, JSON.stringify(baseline, null, 2) + "\n");
|
|
4467
|
+
const kindGaps = Object.values(missingKinds).reduce(
|
|
4468
|
+
(n, ks) => n + ks.length,
|
|
4469
|
+
0
|
|
4470
|
+
);
|
|
4471
|
+
w.out(
|
|
4472
|
+
`Wrote ${p2} \u2014 ${baseline.uncapturedPages.length} uncaptured page(s), ${kindGaps} missing core-kind(s) baselined`
|
|
4473
|
+
);
|
|
4474
|
+
}
|
|
4475
|
+
return w.result(0);
|
|
3626
4476
|
}
|
|
3627
4477
|
const allDiagnostics = results.flatMap((r) => r.audit?.diagnostics ?? []);
|
|
3628
4478
|
const summary = results.reduce(
|
|
@@ -3650,10 +4500,14 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3650
4500
|
);
|
|
3651
4501
|
}
|
|
3652
4502
|
for (const r of results) {
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
4503
|
+
for (const art of [r.types, r.data, r.locs]) {
|
|
4504
|
+
if (check) {
|
|
4505
|
+
w.out(`Checked ${art.outputPath}`);
|
|
4506
|
+
} else if (wrote.has(art)) {
|
|
4507
|
+
w.out(`Wrote ${art.outputPath}`);
|
|
4508
|
+
} else {
|
|
4509
|
+
w.out(`Unchanged ${art.outputPath}`);
|
|
4510
|
+
}
|
|
3657
4511
|
}
|
|
3658
4512
|
for (const d of r.audit?.diagnostics ?? []) {
|
|
3659
4513
|
const loc = d.file ? `${d.file}${d.line ? `:${d.line}` : ""}` : "";
|
|
@@ -3761,12 +4615,21 @@ export {
|
|
|
3761
4615
|
DEFAULT_CONVENTIONS,
|
|
3762
4616
|
applyFixes,
|
|
3763
4617
|
audit,
|
|
4618
|
+
checkPageCoverage,
|
|
4619
|
+
checkStateCoverage,
|
|
4620
|
+
checkStateMatrix,
|
|
4621
|
+
compileRoute,
|
|
4622
|
+
computeMissingKinds,
|
|
4623
|
+
computePageBaseline,
|
|
3764
4624
|
detectRoutes,
|
|
3765
4625
|
discover,
|
|
3766
4626
|
emit,
|
|
3767
4627
|
extract,
|
|
3768
4628
|
extractUidexExports,
|
|
3769
4629
|
globToRegExp,
|
|
4630
|
+
loadPageBaseline,
|
|
4631
|
+
matchUrlToRoute,
|
|
4632
|
+
pageBaselinePath,
|
|
3770
4633
|
parseConfig,
|
|
3771
4634
|
pathToId,
|
|
3772
4635
|
renameEntity,
|