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.cjs
CHANGED
|
@@ -35,12 +35,21 @@ __export(scan_exports, {
|
|
|
35
35
|
DEFAULT_CONVENTIONS: () => DEFAULT_CONVENTIONS,
|
|
36
36
|
applyFixes: () => applyFixes,
|
|
37
37
|
audit: () => audit,
|
|
38
|
+
checkPageCoverage: () => checkPageCoverage,
|
|
39
|
+
checkStateCoverage: () => checkStateCoverage,
|
|
40
|
+
checkStateMatrix: () => checkStateMatrix,
|
|
41
|
+
compileRoute: () => compileRoute,
|
|
42
|
+
computeMissingKinds: () => computeMissingKinds,
|
|
43
|
+
computePageBaseline: () => computePageBaseline,
|
|
38
44
|
detectRoutes: () => detectRoutes,
|
|
39
45
|
discover: () => discover,
|
|
40
46
|
emit: () => emit,
|
|
41
47
|
extract: () => extract,
|
|
42
48
|
extractUidexExports: () => extractUidexExports,
|
|
43
49
|
globToRegExp: () => globToRegExp,
|
|
50
|
+
loadPageBaseline: () => loadPageBaseline,
|
|
51
|
+
matchUrlToRoute: () => matchUrlToRoute,
|
|
52
|
+
pageBaselinePath: () => pageBaselinePath,
|
|
44
53
|
parseConfig: () => parseConfig,
|
|
45
54
|
pathToId: () => pathToId,
|
|
46
55
|
renameEntity: () => renameEntity,
|
|
@@ -86,7 +95,9 @@ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
86
95
|
"output",
|
|
87
96
|
"flows",
|
|
88
97
|
"audit",
|
|
89
|
-
"conventions"
|
|
98
|
+
"conventions",
|
|
99
|
+
"statesManifest",
|
|
100
|
+
"coverageBaseline"
|
|
90
101
|
]);
|
|
91
102
|
var ALLOWED_SOURCE_KEYS = /* @__PURE__ */ new Set(["rootDir", "include", "exclude", "prefix"]);
|
|
92
103
|
var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -96,7 +107,14 @@ var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
|
|
|
96
107
|
"flows",
|
|
97
108
|
"regions"
|
|
98
109
|
]);
|
|
99
|
-
var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
|
|
110
|
+
var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
|
|
111
|
+
"scopeLeak",
|
|
112
|
+
"coverage",
|
|
113
|
+
"acceptance",
|
|
114
|
+
"states",
|
|
115
|
+
"pageCoverage",
|
|
116
|
+
"stateMatrix"
|
|
117
|
+
]);
|
|
100
118
|
function fail(msg) {
|
|
101
119
|
throw new ConfigError(`Invalid .uidex.json: ${msg}`);
|
|
102
120
|
}
|
|
@@ -155,6 +173,12 @@ function validateConfig(raw) {
|
|
|
155
173
|
}
|
|
156
174
|
if (raw.exclude !== void 0) assertStringArray(raw.exclude, `exclude`);
|
|
157
175
|
if (raw.flows !== void 0) assertStringArray(raw.flows, `flows`);
|
|
176
|
+
if (raw.statesManifest !== void 0 && typeof raw.statesManifest !== "string") {
|
|
177
|
+
fail(`"statesManifest" must be a string`);
|
|
178
|
+
}
|
|
179
|
+
if (raw.coverageBaseline !== void 0 && typeof raw.coverageBaseline !== "string") {
|
|
180
|
+
fail(`"coverageBaseline" must be a string`);
|
|
181
|
+
}
|
|
158
182
|
if (raw.audit !== void 0) {
|
|
159
183
|
assertObject(raw.audit, "audit");
|
|
160
184
|
for (const key of Object.keys(raw.audit)) {
|
|
@@ -194,7 +218,9 @@ function validateConfig(raw) {
|
|
|
194
218
|
output: raw.output,
|
|
195
219
|
flows: raw.flows,
|
|
196
220
|
audit: raw.audit,
|
|
197
|
-
conventions: raw.conventions
|
|
221
|
+
conventions: raw.conventions,
|
|
222
|
+
statesManifest: raw.statesManifest,
|
|
223
|
+
coverageBaseline: raw.coverageBaseline
|
|
198
224
|
};
|
|
199
225
|
}
|
|
200
226
|
function parseConfig(json) {
|
|
@@ -283,7 +309,12 @@ var BASE_EXCLUDES = [
|
|
|
283
309
|
"**/dist/**",
|
|
284
310
|
"**/build/**",
|
|
285
311
|
"**/.next/**",
|
|
286
|
-
|
|
312
|
+
// Exclude the scanner's own generated siblings so a scan never re-ingests its
|
|
313
|
+
// output. Enumerated (not `*.gen.*`) so a hand-written file that merely contains
|
|
314
|
+
// a `.gen.` segment, e.g. `foo.gen.helpers.ts`, is still scanned.
|
|
315
|
+
"**/*.gen.ts",
|
|
316
|
+
"**/*.gen.data.ts",
|
|
317
|
+
"**/*.gen.locs.ts"
|
|
287
318
|
];
|
|
288
319
|
var TEST_EXCLUDES = [
|
|
289
320
|
"**/*.test.ts",
|
|
@@ -483,6 +514,201 @@ function unwrapTsExpression(expr) {
|
|
|
483
514
|
}
|
|
484
515
|
}
|
|
485
516
|
|
|
517
|
+
// src/shared/entities/types.ts
|
|
518
|
+
var ENTITY_KINDS = [
|
|
519
|
+
"route",
|
|
520
|
+
"page",
|
|
521
|
+
"feature",
|
|
522
|
+
"widget",
|
|
523
|
+
"region",
|
|
524
|
+
"element",
|
|
525
|
+
"primitive",
|
|
526
|
+
"flow"
|
|
527
|
+
];
|
|
528
|
+
var CORE_STATE_KINDS = [
|
|
529
|
+
"loading",
|
|
530
|
+
"empty",
|
|
531
|
+
"populated",
|
|
532
|
+
"error"
|
|
533
|
+
];
|
|
534
|
+
function isMetaKind(kind) {
|
|
535
|
+
return kind !== "route" && kind !== "flow";
|
|
536
|
+
}
|
|
537
|
+
function isMetaEntity(entity) {
|
|
538
|
+
return isMetaKind(entity.kind);
|
|
539
|
+
}
|
|
540
|
+
function entityKey(entity) {
|
|
541
|
+
return entity.kind === "route" ? entity.path : entity.id;
|
|
542
|
+
}
|
|
543
|
+
var UnknownEntityKindError = class extends Error {
|
|
544
|
+
kind;
|
|
545
|
+
constructor(kind) {
|
|
546
|
+
super(`Unknown entity kind: ${kind}`);
|
|
547
|
+
this.name = "UnknownEntityKindError";
|
|
548
|
+
this.kind = kind;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
var KIND_SET = new Set(ENTITY_KINDS);
|
|
552
|
+
function assertEntityKind(kind) {
|
|
553
|
+
if (!KIND_SET.has(kind)) throw new UnknownEntityKindError(kind);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/shared/entities/registry.ts
|
|
557
|
+
function emptyStore() {
|
|
558
|
+
return {
|
|
559
|
+
route: /* @__PURE__ */ new Map(),
|
|
560
|
+
page: /* @__PURE__ */ new Map(),
|
|
561
|
+
feature: /* @__PURE__ */ new Map(),
|
|
562
|
+
widget: /* @__PURE__ */ new Map(),
|
|
563
|
+
region: /* @__PURE__ */ new Map(),
|
|
564
|
+
element: /* @__PURE__ */ new Map(),
|
|
565
|
+
primitive: /* @__PURE__ */ new Map(),
|
|
566
|
+
flow: /* @__PURE__ */ new Map()
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function computeFlowIds(flows, targetId) {
|
|
570
|
+
const ids = [];
|
|
571
|
+
for (const flow of flows) {
|
|
572
|
+
if (flow.touches.includes(targetId)) ids.push(flow.id);
|
|
573
|
+
}
|
|
574
|
+
return ids;
|
|
575
|
+
}
|
|
576
|
+
function freezeEntity(entity, flows) {
|
|
577
|
+
if (!isMetaKind(entity.kind)) return entity;
|
|
578
|
+
const withMeta = entity;
|
|
579
|
+
if (withMeta.meta === void 0) return entity;
|
|
580
|
+
const computedFlows = Object.freeze(computeFlowIds(flows, withMeta.id));
|
|
581
|
+
const mergedMeta = { ...withMeta.meta, flows: computedFlows };
|
|
582
|
+
return { ...entity, meta: Object.freeze(mergedMeta) };
|
|
583
|
+
}
|
|
584
|
+
function createRegistry() {
|
|
585
|
+
const store = emptyStore();
|
|
586
|
+
let flowsCache = null;
|
|
587
|
+
const patternCache = /* @__PURE__ */ new Map();
|
|
588
|
+
const getFlows = () => {
|
|
589
|
+
if (flowsCache === null) flowsCache = Array.from(store.flow.values());
|
|
590
|
+
return flowsCache;
|
|
591
|
+
};
|
|
592
|
+
const add = (entity) => {
|
|
593
|
+
assertEntityKind(entity.kind);
|
|
594
|
+
const key = entityKey(entity);
|
|
595
|
+
store[entity.kind].set(key, entity);
|
|
596
|
+
flowsCache = null;
|
|
597
|
+
patternCache.delete(entity.kind);
|
|
598
|
+
};
|
|
599
|
+
const get = (kind, id) => {
|
|
600
|
+
assertEntityKind(kind);
|
|
601
|
+
const raw = store[kind].get(id);
|
|
602
|
+
if (raw === void 0) return void 0;
|
|
603
|
+
return freezeEntity(raw, getFlows());
|
|
604
|
+
};
|
|
605
|
+
const getPatternsForKind = (kind) => {
|
|
606
|
+
const cached = patternCache.get(kind);
|
|
607
|
+
if (cached !== void 0) return cached;
|
|
608
|
+
const patterns = [];
|
|
609
|
+
for (const [key, entity] of store[kind]) {
|
|
610
|
+
if (key.includes("*")) {
|
|
611
|
+
const segments = key.split("*");
|
|
612
|
+
patterns.push({
|
|
613
|
+
segments,
|
|
614
|
+
staticLength: segments.reduce((n, s) => n + s.length, 0),
|
|
615
|
+
entity
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
patternCache.set(
|
|
620
|
+
kind,
|
|
621
|
+
patterns
|
|
622
|
+
);
|
|
623
|
+
return patterns;
|
|
624
|
+
};
|
|
625
|
+
const matchesSegments = (segments, id) => {
|
|
626
|
+
const first = segments[0];
|
|
627
|
+
const last = segments[segments.length - 1];
|
|
628
|
+
if (!id.startsWith(first)) return false;
|
|
629
|
+
let pos = first.length;
|
|
630
|
+
for (let i = 1; i < segments.length - 1; i++) {
|
|
631
|
+
const idx = id.indexOf(segments[i], pos);
|
|
632
|
+
if (idx === -1) return false;
|
|
633
|
+
pos = idx + segments[i].length;
|
|
634
|
+
}
|
|
635
|
+
return id.endsWith(last) && id.length - last.length >= pos;
|
|
636
|
+
};
|
|
637
|
+
const matchPattern = (kind, id) => {
|
|
638
|
+
assertEntityKind(kind);
|
|
639
|
+
const patterns = getPatternsForKind(kind);
|
|
640
|
+
if (patterns.length === 0) return void 0;
|
|
641
|
+
let best;
|
|
642
|
+
for (const entry of patterns) {
|
|
643
|
+
if (matchesSegments(entry.segments, id) && (best === void 0 || entry.staticLength > best.staticLength)) {
|
|
644
|
+
best = entry;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (best === void 0) return void 0;
|
|
648
|
+
return freezeEntity(best.entity, getFlows());
|
|
649
|
+
};
|
|
650
|
+
const list = (kind) => {
|
|
651
|
+
assertEntityKind(kind);
|
|
652
|
+
const flows = getFlows();
|
|
653
|
+
return Array.from(
|
|
654
|
+
store[kind].values(),
|
|
655
|
+
(e) => freezeEntity(e, flows)
|
|
656
|
+
);
|
|
657
|
+
};
|
|
658
|
+
const allEntities = function* () {
|
|
659
|
+
for (const kind of Object.keys(store)) {
|
|
660
|
+
for (const entity of store[kind].values()) {
|
|
661
|
+
yield entity;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
const query = (predicate) => {
|
|
666
|
+
const flows = getFlows();
|
|
667
|
+
const result = [];
|
|
668
|
+
for (const entity of allEntities()) {
|
|
669
|
+
if (predicate(entity)) result.push(freezeEntity(entity, flows));
|
|
670
|
+
}
|
|
671
|
+
return result;
|
|
672
|
+
};
|
|
673
|
+
const byScope = (scope) => query(
|
|
674
|
+
(entity) => "scopes" in entity && Array.isArray(entity.scopes) && entity.scopes.includes(scope)
|
|
675
|
+
);
|
|
676
|
+
const touchedBy = (flowId) => {
|
|
677
|
+
const flow = store.flow.get(flowId);
|
|
678
|
+
if (flow === void 0) return [];
|
|
679
|
+
const ids = new Set(flow.touches);
|
|
680
|
+
return query((entity) => {
|
|
681
|
+
if (!isMetaEntity(entity)) return false;
|
|
682
|
+
return ids.has(entity.id);
|
|
683
|
+
});
|
|
684
|
+
};
|
|
685
|
+
const reports = /* @__PURE__ */ new Map();
|
|
686
|
+
const reportsCbs = /* @__PURE__ */ new Set();
|
|
687
|
+
const setReports = (kind, id, records) => {
|
|
688
|
+
reports.set(`${kind}:${id}`, records);
|
|
689
|
+
for (const cb of reportsCbs) cb();
|
|
690
|
+
};
|
|
691
|
+
const getReports = (kind, id) => reports.get(`${kind}:${id}`) ?? [];
|
|
692
|
+
const listReportKeys = () => Array.from(reports.keys());
|
|
693
|
+
const onReportsChange = (cb) => {
|
|
694
|
+
reportsCbs.add(cb);
|
|
695
|
+
return () => reportsCbs.delete(cb);
|
|
696
|
+
};
|
|
697
|
+
return {
|
|
698
|
+
add,
|
|
699
|
+
get,
|
|
700
|
+
matchPattern,
|
|
701
|
+
list,
|
|
702
|
+
query,
|
|
703
|
+
byScope,
|
|
704
|
+
touchedBy,
|
|
705
|
+
setReports,
|
|
706
|
+
getReports,
|
|
707
|
+
listReportKeys,
|
|
708
|
+
onReportsChange
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
486
712
|
// src/scanner/scan/extract-uidex-export.ts
|
|
487
713
|
var KIND_DISCRIMINATORS = [
|
|
488
714
|
"page",
|
|
@@ -500,20 +726,35 @@ var ALLOWED_FIELDS = {
|
|
|
500
726
|
"features",
|
|
501
727
|
"widgets",
|
|
502
728
|
"acceptance",
|
|
729
|
+
"states",
|
|
730
|
+
"capture",
|
|
731
|
+
"waivers",
|
|
503
732
|
"description"
|
|
504
733
|
]),
|
|
734
|
+
// `capture` / `waivers` are route-scoped, and only a page maps to a route — so
|
|
735
|
+
// they are page-only (the gates read them off the page). A feature/widget that
|
|
736
|
+
// sets them gets an `uidex-export-unknown-field` error rather than a silent no-op.
|
|
505
737
|
feature: /* @__PURE__ */ new Set([
|
|
506
738
|
"feature",
|
|
507
739
|
"name",
|
|
508
740
|
"features",
|
|
509
741
|
"acceptance",
|
|
742
|
+
"states",
|
|
510
743
|
"description"
|
|
511
744
|
]),
|
|
512
745
|
primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
|
|
513
|
-
widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "description"]),
|
|
746
|
+
widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "states", "description"]),
|
|
514
747
|
region: /* @__PURE__ */ new Set(["region", "name", "description"]),
|
|
515
748
|
flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
|
|
516
749
|
};
|
|
750
|
+
var STATE_FIELDS = /* @__PURE__ */ new Set([
|
|
751
|
+
"name",
|
|
752
|
+
"kind",
|
|
753
|
+
"acceptance",
|
|
754
|
+
"description"
|
|
755
|
+
]);
|
|
756
|
+
var CORE_STATE_KINDS2 = new Set(CORE_STATE_KINDS);
|
|
757
|
+
var STATE_KINDS = /* @__PURE__ */ new Set([...CORE_STATE_KINDS, "variant"]);
|
|
517
758
|
var FALSEABLE = /* @__PURE__ */ new Set([
|
|
518
759
|
"page",
|
|
519
760
|
"feature",
|
|
@@ -903,6 +1144,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
903
1144
|
}
|
|
904
1145
|
const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
905
1146
|
const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
1147
|
+
const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey) : void 0;
|
|
1148
|
+
const capture = kind === "page" ? readBooleanField(byKey, "capture") : void 0;
|
|
1149
|
+
const waivers = kind === "page" ? readWaiversField(byKey) : void 0;
|
|
906
1150
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
907
1151
|
const metadata = {
|
|
908
1152
|
source: "ts-export",
|
|
@@ -925,6 +1169,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
925
1169
|
metadata.widgets = widgetsField.values;
|
|
926
1170
|
metadata.widgetSpans = widgetsField.spans;
|
|
927
1171
|
}
|
|
1172
|
+
if (states?.length) metadata.states = states;
|
|
1173
|
+
if (capture !== void 0) metadata.capture = capture;
|
|
1174
|
+
if (waivers && Object.keys(waivers).length > 0) metadata.waivers = waivers;
|
|
928
1175
|
if (notFlow) metadata.notFlow = true;
|
|
929
1176
|
if (typeof id === "string" && idValue.kind === "string") {
|
|
930
1177
|
metadata.idSpan = idValue.span;
|
|
@@ -961,6 +1208,48 @@ function readIdField(value, kind, fieldName) {
|
|
|
961
1208
|
value.pos
|
|
962
1209
|
);
|
|
963
1210
|
}
|
|
1211
|
+
function readWaiversField(byKey) {
|
|
1212
|
+
const entry = byKey.get("waivers");
|
|
1213
|
+
if (!entry) return void 0;
|
|
1214
|
+
if (entry.value.kind !== "object") {
|
|
1215
|
+
throw new ExtractError(
|
|
1216
|
+
"uidex-export-invalid-field",
|
|
1217
|
+
"`waivers` must be an object mapping a core kind to a reason.",
|
|
1218
|
+
entry.value.pos
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
const waivers = {};
|
|
1222
|
+
for (const e of entry.value.entries) {
|
|
1223
|
+
if (!CORE_STATE_KINDS2.has(e.key)) {
|
|
1224
|
+
throw new ExtractError(
|
|
1225
|
+
"uidex-export-invalid-field",
|
|
1226
|
+
`waiver key "${e.key}" is not a core kind; only ${[...CORE_STATE_KINDS2].join(", ")} participate in the matrix.`,
|
|
1227
|
+
e.keyPos
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
if (e.value.kind !== "string" || e.value.value.length === 0) {
|
|
1231
|
+
throw new ExtractError(
|
|
1232
|
+
"uidex-export-invalid-field",
|
|
1233
|
+
`waiver "${e.key}" needs a non-empty reason (why the route cannot render this kind).`,
|
|
1234
|
+
e.value.pos
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
waivers[e.key] = e.value.value;
|
|
1238
|
+
}
|
|
1239
|
+
return waivers;
|
|
1240
|
+
}
|
|
1241
|
+
function readBooleanField(byKey, name) {
|
|
1242
|
+
const entry = byKey.get(name);
|
|
1243
|
+
if (!entry) return void 0;
|
|
1244
|
+
if (entry.value.kind !== "boolean") {
|
|
1245
|
+
throw new ExtractError(
|
|
1246
|
+
"uidex-export-invalid-field",
|
|
1247
|
+
`\`${name}\` must be a boolean.`,
|
|
1248
|
+
entry.value.pos
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
return entry.value.value;
|
|
1252
|
+
}
|
|
964
1253
|
function readStringField(byKey, name) {
|
|
965
1254
|
const entry = byKey.get(name);
|
|
966
1255
|
if (!entry) return void 0;
|
|
@@ -998,6 +1287,86 @@ function readStringArrayField(byKey, name) {
|
|
|
998
1287
|
}
|
|
999
1288
|
return { values, spans };
|
|
1000
1289
|
}
|
|
1290
|
+
function readStatesField(byKey) {
|
|
1291
|
+
const entry = byKey.get("states");
|
|
1292
|
+
if (!entry) return void 0;
|
|
1293
|
+
if (entry.value.kind !== "array") {
|
|
1294
|
+
throw new ExtractError(
|
|
1295
|
+
"uidex-export-invalid-field",
|
|
1296
|
+
"`states` must be an array of state names or `{ name, acceptance?, description? }` objects.",
|
|
1297
|
+
entry.value.pos
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
const states = [];
|
|
1301
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1302
|
+
for (const item of entry.value.items) {
|
|
1303
|
+
let state;
|
|
1304
|
+
if (item.kind === "string") {
|
|
1305
|
+
state = { name: item.value, nameSpan: item.span };
|
|
1306
|
+
} else if (item.kind === "object") {
|
|
1307
|
+
const fieldByKey = /* @__PURE__ */ new Map();
|
|
1308
|
+
for (const e of item.entries) fieldByKey.set(e.key, e);
|
|
1309
|
+
for (const e of item.entries) {
|
|
1310
|
+
if (!STATE_FIELDS.has(e.key)) {
|
|
1311
|
+
throw new ExtractError(
|
|
1312
|
+
"uidex-export-invalid-field",
|
|
1313
|
+
`Unknown field "${e.key}" in a \`states\` entry. Allowed: ${Array.from(
|
|
1314
|
+
STATE_FIELDS
|
|
1315
|
+
).sort().join(", ")}.`,
|
|
1316
|
+
e.value.pos
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
const nameEntry = fieldByKey.get("name");
|
|
1321
|
+
if (!nameEntry || nameEntry.value.kind !== "string") {
|
|
1322
|
+
throw new ExtractError(
|
|
1323
|
+
"uidex-export-invalid-field",
|
|
1324
|
+
"Each object `states` entry must have a string `name`.",
|
|
1325
|
+
item.pos
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
state = { name: nameEntry.value.value, nameSpan: nameEntry.value.span };
|
|
1329
|
+
const kind = readStringField(fieldByKey, "kind");
|
|
1330
|
+
if (kind !== void 0) {
|
|
1331
|
+
if (!STATE_KINDS.has(kind)) {
|
|
1332
|
+
throw new ExtractError(
|
|
1333
|
+
"uidex-export-invalid-field",
|
|
1334
|
+
`state \`kind\` must be one of ${[...STATE_KINDS].join(", ")} (got "${kind}").`,
|
|
1335
|
+
fieldByKey.get("kind").value.pos
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
state.kind = kind;
|
|
1339
|
+
}
|
|
1340
|
+
const acceptance = readStringArrayField(fieldByKey, "acceptance")?.values;
|
|
1341
|
+
if (acceptance) state.acceptance = acceptance;
|
|
1342
|
+
const description = readStringField(fieldByKey, "description");
|
|
1343
|
+
if (description) state.description = description;
|
|
1344
|
+
} else {
|
|
1345
|
+
throw new ExtractError(
|
|
1346
|
+
"uidex-export-invalid-field",
|
|
1347
|
+
"`states` items must be strings or `{ name, ... }` objects.",
|
|
1348
|
+
item.pos
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
if (state.name.length === 0) {
|
|
1352
|
+
throw new ExtractError(
|
|
1353
|
+
"uidex-export-invalid-field",
|
|
1354
|
+
"A `states` entry has an empty `name`.",
|
|
1355
|
+
item.pos
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
if (seen.has(state.name)) {
|
|
1359
|
+
throw new ExtractError(
|
|
1360
|
+
"uidex-export-duplicate-state",
|
|
1361
|
+
`Duplicate state "${state.name}" in \`states\`; each state name must be unique within the entity.`,
|
|
1362
|
+
item.pos
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
seen.add(state.name);
|
|
1366
|
+
states.push(state);
|
|
1367
|
+
}
|
|
1368
|
+
return states;
|
|
1369
|
+
}
|
|
1001
1370
|
function posAt(content, offset, p2) {
|
|
1002
1371
|
const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
|
|
1003
1372
|
return { offset, line: p2.lineAt(offset), column: offset - lineStart + 1 };
|
|
@@ -1498,211 +1867,22 @@ function collectImportFacts(parsed) {
|
|
|
1498
1867
|
} else {
|
|
1499
1868
|
continue;
|
|
1500
1869
|
}
|
|
1501
|
-
if (!source || source.type !== "Literal") continue;
|
|
1502
|
-
if (typeof source.value !== "string") continue;
|
|
1503
|
-
out2.push({
|
|
1504
|
-
specifier: source.value,
|
|
1505
|
-
line: parsed.lineAt(stmt.start),
|
|
1506
|
-
span: { start: stmt.start, end: stmt.end },
|
|
1507
|
-
isTypeOnly,
|
|
1508
|
-
names
|
|
1509
|
-
});
|
|
1510
|
-
}
|
|
1511
|
-
return out2;
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
// src/scanner/scan/resolve.ts
|
|
1515
|
-
var path4 = __toESM(require("path"), 1);
|
|
1516
|
-
|
|
1517
|
-
// src/shared/entities/types.ts
|
|
1518
|
-
var ENTITY_KINDS = [
|
|
1519
|
-
"route",
|
|
1520
|
-
"page",
|
|
1521
|
-
"feature",
|
|
1522
|
-
"widget",
|
|
1523
|
-
"region",
|
|
1524
|
-
"element",
|
|
1525
|
-
"primitive",
|
|
1526
|
-
"flow"
|
|
1527
|
-
];
|
|
1528
|
-
function isMetaKind(kind) {
|
|
1529
|
-
return kind !== "route" && kind !== "flow";
|
|
1530
|
-
}
|
|
1531
|
-
function isMetaEntity(entity) {
|
|
1532
|
-
return isMetaKind(entity.kind);
|
|
1533
|
-
}
|
|
1534
|
-
function entityKey(entity) {
|
|
1535
|
-
return entity.kind === "route" ? entity.path : entity.id;
|
|
1536
|
-
}
|
|
1537
|
-
var UnknownEntityKindError = class extends Error {
|
|
1538
|
-
kind;
|
|
1539
|
-
constructor(kind) {
|
|
1540
|
-
super(`Unknown entity kind: ${kind}`);
|
|
1541
|
-
this.name = "UnknownEntityKindError";
|
|
1542
|
-
this.kind = kind;
|
|
1543
|
-
}
|
|
1544
|
-
};
|
|
1545
|
-
var KIND_SET = new Set(ENTITY_KINDS);
|
|
1546
|
-
function assertEntityKind(kind) {
|
|
1547
|
-
if (!KIND_SET.has(kind)) throw new UnknownEntityKindError(kind);
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
// src/shared/entities/registry.ts
|
|
1551
|
-
function emptyStore() {
|
|
1552
|
-
return {
|
|
1553
|
-
route: /* @__PURE__ */ new Map(),
|
|
1554
|
-
page: /* @__PURE__ */ new Map(),
|
|
1555
|
-
feature: /* @__PURE__ */ new Map(),
|
|
1556
|
-
widget: /* @__PURE__ */ new Map(),
|
|
1557
|
-
region: /* @__PURE__ */ new Map(),
|
|
1558
|
-
element: /* @__PURE__ */ new Map(),
|
|
1559
|
-
primitive: /* @__PURE__ */ new Map(),
|
|
1560
|
-
flow: /* @__PURE__ */ new Map()
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
|
-
function computeFlowIds(flows, targetId) {
|
|
1564
|
-
const ids = [];
|
|
1565
|
-
for (const flow of flows) {
|
|
1566
|
-
if (flow.touches.includes(targetId)) ids.push(flow.id);
|
|
1567
|
-
}
|
|
1568
|
-
return ids;
|
|
1569
|
-
}
|
|
1570
|
-
function freezeEntity(entity, flows) {
|
|
1571
|
-
if (!isMetaKind(entity.kind)) return entity;
|
|
1572
|
-
const withMeta = entity;
|
|
1573
|
-
if (withMeta.meta === void 0) return entity;
|
|
1574
|
-
const computedFlows = Object.freeze(computeFlowIds(flows, withMeta.id));
|
|
1575
|
-
const mergedMeta = { ...withMeta.meta, flows: computedFlows };
|
|
1576
|
-
return { ...entity, meta: Object.freeze(mergedMeta) };
|
|
1577
|
-
}
|
|
1578
|
-
function createRegistry() {
|
|
1579
|
-
const store = emptyStore();
|
|
1580
|
-
let flowsCache = null;
|
|
1581
|
-
const patternCache = /* @__PURE__ */ new Map();
|
|
1582
|
-
const getFlows = () => {
|
|
1583
|
-
if (flowsCache === null) flowsCache = Array.from(store.flow.values());
|
|
1584
|
-
return flowsCache;
|
|
1585
|
-
};
|
|
1586
|
-
const add = (entity) => {
|
|
1587
|
-
assertEntityKind(entity.kind);
|
|
1588
|
-
const key = entityKey(entity);
|
|
1589
|
-
store[entity.kind].set(key, entity);
|
|
1590
|
-
flowsCache = null;
|
|
1591
|
-
patternCache.delete(entity.kind);
|
|
1592
|
-
};
|
|
1593
|
-
const get = (kind, id) => {
|
|
1594
|
-
assertEntityKind(kind);
|
|
1595
|
-
const raw = store[kind].get(id);
|
|
1596
|
-
if (raw === void 0) return void 0;
|
|
1597
|
-
return freezeEntity(raw, getFlows());
|
|
1598
|
-
};
|
|
1599
|
-
const getPatternsForKind = (kind) => {
|
|
1600
|
-
const cached = patternCache.get(kind);
|
|
1601
|
-
if (cached !== void 0) return cached;
|
|
1602
|
-
const patterns = [];
|
|
1603
|
-
for (const [key, entity] of store[kind]) {
|
|
1604
|
-
if (key.includes("*")) {
|
|
1605
|
-
const segments = key.split("*");
|
|
1606
|
-
patterns.push({
|
|
1607
|
-
segments,
|
|
1608
|
-
staticLength: segments.reduce((n, s) => n + s.length, 0),
|
|
1609
|
-
entity
|
|
1610
|
-
});
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
patternCache.set(
|
|
1614
|
-
kind,
|
|
1615
|
-
patterns
|
|
1616
|
-
);
|
|
1617
|
-
return patterns;
|
|
1618
|
-
};
|
|
1619
|
-
const matchesSegments = (segments, id) => {
|
|
1620
|
-
const first = segments[0];
|
|
1621
|
-
const last = segments[segments.length - 1];
|
|
1622
|
-
if (!id.startsWith(first)) return false;
|
|
1623
|
-
let pos = first.length;
|
|
1624
|
-
for (let i = 1; i < segments.length - 1; i++) {
|
|
1625
|
-
const idx = id.indexOf(segments[i], pos);
|
|
1626
|
-
if (idx === -1) return false;
|
|
1627
|
-
pos = idx + segments[i].length;
|
|
1628
|
-
}
|
|
1629
|
-
return id.endsWith(last) && id.length - last.length >= pos;
|
|
1630
|
-
};
|
|
1631
|
-
const matchPattern = (kind, id) => {
|
|
1632
|
-
assertEntityKind(kind);
|
|
1633
|
-
const patterns = getPatternsForKind(kind);
|
|
1634
|
-
if (patterns.length === 0) return void 0;
|
|
1635
|
-
let best;
|
|
1636
|
-
for (const entry of patterns) {
|
|
1637
|
-
if (matchesSegments(entry.segments, id) && (best === void 0 || entry.staticLength > best.staticLength)) {
|
|
1638
|
-
best = entry;
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
if (best === void 0) return void 0;
|
|
1642
|
-
return freezeEntity(best.entity, getFlows());
|
|
1643
|
-
};
|
|
1644
|
-
const list = (kind) => {
|
|
1645
|
-
assertEntityKind(kind);
|
|
1646
|
-
const flows = getFlows();
|
|
1647
|
-
return Array.from(
|
|
1648
|
-
store[kind].values(),
|
|
1649
|
-
(e) => freezeEntity(e, flows)
|
|
1650
|
-
);
|
|
1651
|
-
};
|
|
1652
|
-
const allEntities = function* () {
|
|
1653
|
-
for (const kind of Object.keys(store)) {
|
|
1654
|
-
for (const entity of store[kind].values()) {
|
|
1655
|
-
yield entity;
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
};
|
|
1659
|
-
const query = (predicate) => {
|
|
1660
|
-
const flows = getFlows();
|
|
1661
|
-
const result = [];
|
|
1662
|
-
for (const entity of allEntities()) {
|
|
1663
|
-
if (predicate(entity)) result.push(freezeEntity(entity, flows));
|
|
1664
|
-
}
|
|
1665
|
-
return result;
|
|
1666
|
-
};
|
|
1667
|
-
const byScope = (scope) => query(
|
|
1668
|
-
(entity) => "scopes" in entity && Array.isArray(entity.scopes) && entity.scopes.includes(scope)
|
|
1669
|
-
);
|
|
1670
|
-
const touchedBy = (flowId) => {
|
|
1671
|
-
const flow = store.flow.get(flowId);
|
|
1672
|
-
if (flow === void 0) return [];
|
|
1673
|
-
const ids = new Set(flow.touches);
|
|
1674
|
-
return query((entity) => {
|
|
1675
|
-
if (!isMetaEntity(entity)) return false;
|
|
1676
|
-
return ids.has(entity.id);
|
|
1870
|
+
if (!source || source.type !== "Literal") continue;
|
|
1871
|
+
if (typeof source.value !== "string") continue;
|
|
1872
|
+
out2.push({
|
|
1873
|
+
specifier: source.value,
|
|
1874
|
+
line: parsed.lineAt(stmt.start),
|
|
1875
|
+
span: { start: stmt.start, end: stmt.end },
|
|
1876
|
+
isTypeOnly,
|
|
1877
|
+
names
|
|
1677
1878
|
});
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
const reportsCbs = /* @__PURE__ */ new Set();
|
|
1681
|
-
const setReports = (kind, id, records) => {
|
|
1682
|
-
reports.set(`${kind}:${id}`, records);
|
|
1683
|
-
for (const cb of reportsCbs) cb();
|
|
1684
|
-
};
|
|
1685
|
-
const getReports = (kind, id) => reports.get(`${kind}:${id}`) ?? [];
|
|
1686
|
-
const listReportKeys = () => Array.from(reports.keys());
|
|
1687
|
-
const onReportsChange = (cb) => {
|
|
1688
|
-
reportsCbs.add(cb);
|
|
1689
|
-
return () => reportsCbs.delete(cb);
|
|
1690
|
-
};
|
|
1691
|
-
return {
|
|
1692
|
-
add,
|
|
1693
|
-
get,
|
|
1694
|
-
matchPattern,
|
|
1695
|
-
list,
|
|
1696
|
-
query,
|
|
1697
|
-
byScope,
|
|
1698
|
-
touchedBy,
|
|
1699
|
-
setReports,
|
|
1700
|
-
getReports,
|
|
1701
|
-
listReportKeys,
|
|
1702
|
-
onReportsChange
|
|
1703
|
-
};
|
|
1879
|
+
}
|
|
1880
|
+
return out2;
|
|
1704
1881
|
}
|
|
1705
1882
|
|
|
1883
|
+
// src/scanner/scan/resolve.ts
|
|
1884
|
+
var path4 = __toESM(require("path"), 1);
|
|
1885
|
+
|
|
1706
1886
|
// src/scanner/scan/routes.ts
|
|
1707
1887
|
var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1708
1888
|
var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
@@ -1802,6 +1982,18 @@ function buildMetaFromExport(exp) {
|
|
|
1802
1982
|
if (exp.acceptance?.length) meta.acceptance = exp.acceptance;
|
|
1803
1983
|
if (exp.features?.length) meta.features = exp.features;
|
|
1804
1984
|
if (exp.widgets?.length) meta.widgets = exp.widgets;
|
|
1985
|
+
if (exp.states?.length) {
|
|
1986
|
+
meta.states = exp.states.map((s) => ({
|
|
1987
|
+
name: s.name,
|
|
1988
|
+
...s.kind ? { kind: s.kind } : {},
|
|
1989
|
+
...s.acceptance?.length ? { acceptance: s.acceptance } : {},
|
|
1990
|
+
...s.description ? { description: s.description } : {}
|
|
1991
|
+
}));
|
|
1992
|
+
}
|
|
1993
|
+
if (exp.capture === false) meta.capture = false;
|
|
1994
|
+
if (exp.waivers && Object.keys(exp.waivers).length > 0) {
|
|
1995
|
+
meta.waivers = exp.waivers;
|
|
1996
|
+
}
|
|
1805
1997
|
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
1806
1998
|
}
|
|
1807
1999
|
function resolve2(ctx) {
|
|
@@ -2221,6 +2413,297 @@ function dedupe(arr) {
|
|
|
2221
2413
|
|
|
2222
2414
|
// src/scanner/scan/audit.ts
|
|
2223
2415
|
var path5 = __toESM(require("path"), 1);
|
|
2416
|
+
|
|
2417
|
+
// src/scanner/scan/page-coverage.ts
|
|
2418
|
+
function compileRoute(pattern) {
|
|
2419
|
+
const segs = pattern.split("/").filter(Boolean);
|
|
2420
|
+
let specificity = 0;
|
|
2421
|
+
const optionalTail = segs.length > 0 && /^\[\[\.{3}.+\]\]$/.test(segs[segs.length - 1]);
|
|
2422
|
+
const effective = optionalTail ? segs.slice(0, -1) : segs;
|
|
2423
|
+
const parts = effective.map((seg) => {
|
|
2424
|
+
const catchAll = /^\[\.{3}.+\]$/.test(seg);
|
|
2425
|
+
const dynamic = /^\[.+\]$/.test(seg);
|
|
2426
|
+
if (catchAll) return "(?:[^/]+)(?:/[^/]+)*";
|
|
2427
|
+
if (dynamic) return "[^/]+";
|
|
2428
|
+
specificity++;
|
|
2429
|
+
return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2430
|
+
});
|
|
2431
|
+
let body = parts.length === 0 ? "" : "/" + parts.join("/");
|
|
2432
|
+
if (optionalTail) body += "(?:/[^/]+)*";
|
|
2433
|
+
if (body === "") body = "/";
|
|
2434
|
+
const re = new RegExp(`^${body}/?$`);
|
|
2435
|
+
return { test: (url) => re.test(url), specificity };
|
|
2436
|
+
}
|
|
2437
|
+
function matchUrlToRoute(url, routePaths) {
|
|
2438
|
+
const path12 = stripQuery(url);
|
|
2439
|
+
let best = null;
|
|
2440
|
+
for (const rp of routePaths) {
|
|
2441
|
+
const { test, specificity } = compileRoute(rp);
|
|
2442
|
+
if (!test(path12)) continue;
|
|
2443
|
+
if (!best || specificity > best.specificity)
|
|
2444
|
+
best = { path: rp, specificity };
|
|
2445
|
+
}
|
|
2446
|
+
return best?.path ?? null;
|
|
2447
|
+
}
|
|
2448
|
+
function stripQuery(url) {
|
|
2449
|
+
const q = url.indexOf("?");
|
|
2450
|
+
const h = url.indexOf("#");
|
|
2451
|
+
let end = url.length;
|
|
2452
|
+
if (q !== -1) end = Math.min(end, q);
|
|
2453
|
+
if (h !== -1) end = Math.min(end, h);
|
|
2454
|
+
return url.slice(0, end);
|
|
2455
|
+
}
|
|
2456
|
+
function routeForCapture(c, routes, routePaths) {
|
|
2457
|
+
if (c.route && routePaths.includes(c.route)) return c.route;
|
|
2458
|
+
if (c.url) {
|
|
2459
|
+
const m = matchUrlToRoute(c.url, routePaths);
|
|
2460
|
+
if (m) return m;
|
|
2461
|
+
}
|
|
2462
|
+
if (c.kind === "page") {
|
|
2463
|
+
const r = routes.find((r2) => r2.page === c.entity);
|
|
2464
|
+
if (r) return r.path;
|
|
2465
|
+
}
|
|
2466
|
+
return null;
|
|
2467
|
+
}
|
|
2468
|
+
function checkPageCoverage(registry, manifest, baseline) {
|
|
2469
|
+
const routes = registry.list("route");
|
|
2470
|
+
if (routes.length === 0) return [];
|
|
2471
|
+
const routePaths = routes.map((r) => r.path);
|
|
2472
|
+
const captured = manifest.captured ?? [];
|
|
2473
|
+
const optedOut = /* @__PURE__ */ new Set();
|
|
2474
|
+
for (const r of routes) {
|
|
2475
|
+
const page = registry.get("page", r.page);
|
|
2476
|
+
if (page?.meta?.capture === false) optedOut.add(r.path);
|
|
2477
|
+
}
|
|
2478
|
+
const covered = /* @__PURE__ */ new Set();
|
|
2479
|
+
for (const c of captured) {
|
|
2480
|
+
const route = routeForCapture(c, routes, routePaths);
|
|
2481
|
+
if (route) covered.add(route);
|
|
2482
|
+
}
|
|
2483
|
+
const baselineSet = new Set(baseline?.uncapturedPages ?? []);
|
|
2484
|
+
const diagnostics = [];
|
|
2485
|
+
for (const r of routes) {
|
|
2486
|
+
if (covered.has(r.path) || optedOut.has(r.path)) continue;
|
|
2487
|
+
const loc = registry.get("page", r.page)?.loc;
|
|
2488
|
+
if (baselineSet.has(r.path)) {
|
|
2489
|
+
diagnostics.push({
|
|
2490
|
+
code: "page-backlog",
|
|
2491
|
+
severity: "info",
|
|
2492
|
+
message: `route "${r.path}" has no visual-states capture (acknowledged backlog)`,
|
|
2493
|
+
file: loc?.file,
|
|
2494
|
+
line: loc?.line,
|
|
2495
|
+
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 }\`.`
|
|
2496
|
+
});
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
diagnostics.push({
|
|
2500
|
+
code: "page-no-capture",
|
|
2501
|
+
severity: "error",
|
|
2502
|
+
message: `route "${r.path}" has no visual-states capture and is not accounted for`,
|
|
2503
|
+
file: loc?.file,
|
|
2504
|
+
line: loc?.line,
|
|
2505
|
+
entity: { kind: "route", id: r.path },
|
|
2506
|
+
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).`
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
for (const b of baselineSet) {
|
|
2510
|
+
if (!routePaths.includes(b)) {
|
|
2511
|
+
diagnostics.push({
|
|
2512
|
+
code: "stale-page-baseline",
|
|
2513
|
+
severity: "warning",
|
|
2514
|
+
message: `coverage baseline lists "${b}", which is not a route in the registry`,
|
|
2515
|
+
hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
|
|
2516
|
+
});
|
|
2517
|
+
} else if (covered.has(b) || optedOut.has(b)) {
|
|
2518
|
+
diagnostics.push({
|
|
2519
|
+
code: "page-captured",
|
|
2520
|
+
severity: "warning",
|
|
2521
|
+
message: `route "${b}" is now captured but is still in the coverage baseline`,
|
|
2522
|
+
hint: `Prune it with \`uidex scan --update-baseline\` so the backlog reflects reality.`
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
return diagnostics;
|
|
2527
|
+
}
|
|
2528
|
+
function computePageBaseline(registry, manifest) {
|
|
2529
|
+
const uncaptured = [];
|
|
2530
|
+
for (const d of checkPageCoverage(registry, manifest, {
|
|
2531
|
+
uncapturedPages: []
|
|
2532
|
+
})) {
|
|
2533
|
+
if (d.code === "page-no-capture" && d.entity) uncaptured.push(d.entity.id);
|
|
2534
|
+
}
|
|
2535
|
+
return { uncapturedPages: uncaptured.sort() };
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
// src/scanner/scan/state-coverage.ts
|
|
2539
|
+
var STATE_KINDS2 = ["page", "feature", "widget"];
|
|
2540
|
+
function declaredStateEntities(registry) {
|
|
2541
|
+
const out2 = [];
|
|
2542
|
+
for (const kind of STATE_KINDS2) {
|
|
2543
|
+
for (const e of registry.list(kind)) {
|
|
2544
|
+
const states = e.meta?.states;
|
|
2545
|
+
if (!states || states.length === 0) continue;
|
|
2546
|
+
out2.push({
|
|
2547
|
+
kind,
|
|
2548
|
+
id: e.id,
|
|
2549
|
+
states: states.map((s) => s.name),
|
|
2550
|
+
loc: e.loc
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
return out2;
|
|
2555
|
+
}
|
|
2556
|
+
function captureMatchesEntity(cap, ent) {
|
|
2557
|
+
if (cap.entity !== ent.id) return false;
|
|
2558
|
+
return cap.kind === void 0 || cap.kind === ent.kind;
|
|
2559
|
+
}
|
|
2560
|
+
function checkStateCoverage(registry, manifest) {
|
|
2561
|
+
const diagnostics = [];
|
|
2562
|
+
const declared = declaredStateEntities(registry);
|
|
2563
|
+
const captured = manifest.captured ?? [];
|
|
2564
|
+
for (const ent of declared) {
|
|
2565
|
+
for (const state of ent.states) {
|
|
2566
|
+
const hit = captured.some(
|
|
2567
|
+
(c) => captureMatchesEntity(c, ent) && c.state === state
|
|
2568
|
+
);
|
|
2569
|
+
if (hit) continue;
|
|
2570
|
+
diagnostics.push({
|
|
2571
|
+
code: "missing-state-capture",
|
|
2572
|
+
severity: "warning",
|
|
2573
|
+
message: `${ent.kind} "${ent.id}" declares state "${state}" but no capture produced it`,
|
|
2574
|
+
file: ent.loc?.file,
|
|
2575
|
+
line: ent.loc?.line,
|
|
2576
|
+
entity: { kind: ent.kind, id: ent.id },
|
|
2577
|
+
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.`
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
for (const cap of captured) {
|
|
2582
|
+
const candidates = declared.filter((ent2) => captureMatchesEntity(cap, ent2));
|
|
2583
|
+
if (candidates.length === 0) continue;
|
|
2584
|
+
const declaredSomewhere = candidates.some(
|
|
2585
|
+
(ent2) => ent2.states.includes(cap.state)
|
|
2586
|
+
);
|
|
2587
|
+
if (declaredSomewhere) continue;
|
|
2588
|
+
const ent = candidates[0];
|
|
2589
|
+
diagnostics.push({
|
|
2590
|
+
code: "orphan-state-capture",
|
|
2591
|
+
severity: "warning",
|
|
2592
|
+
message: `Capture produced state "${cap.state}" for ${ent.kind} "${ent.id}", which is not in its declared states [${ent.states.join(", ")}]`,
|
|
2593
|
+
file: ent.loc?.file,
|
|
2594
|
+
line: ent.loc?.line,
|
|
2595
|
+
entity: { kind: ent.kind, id: ent.id },
|
|
2596
|
+
hint: `Declare "${cap.state}" in \`export const uidex = { states: [...] }\` for "${ent.id}", or rename the capture to a declared state.`
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2599
|
+
return diagnostics;
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
// src/scanner/scan/state-matrix.ts
|
|
2603
|
+
var CORE_KINDS = CORE_STATE_KINDS;
|
|
2604
|
+
var STATE_ENTITY_KINDS = ["page", "feature", "widget"];
|
|
2605
|
+
function resolveMatrixKind(registry, c) {
|
|
2606
|
+
if (c.stateKind) return c.stateKind;
|
|
2607
|
+
for (const kind of STATE_ENTITY_KINDS) {
|
|
2608
|
+
const declared = registry.get(kind, c.entity)?.meta?.states?.find((s) => s.name === c.state)?.kind;
|
|
2609
|
+
if (declared) return declared;
|
|
2610
|
+
}
|
|
2611
|
+
return CORE_KINDS.includes(c.state) ? c.state : "variant";
|
|
2612
|
+
}
|
|
2613
|
+
function coreKindsByRoute(registry, manifest) {
|
|
2614
|
+
const routes = registry.list("route");
|
|
2615
|
+
const routePaths = routes.map((r) => r.path);
|
|
2616
|
+
const byRoute = /* @__PURE__ */ new Map();
|
|
2617
|
+
for (const c of manifest.captured ?? []) {
|
|
2618
|
+
const route = routeForCapture(c, routes, routePaths);
|
|
2619
|
+
if (!route) continue;
|
|
2620
|
+
if (!byRoute.has(route)) byRoute.set(route, /* @__PURE__ */ new Set());
|
|
2621
|
+
const kind = resolveMatrixKind(registry, c);
|
|
2622
|
+
if (CORE_KINDS.includes(kind)) byRoute.get(route).add(kind);
|
|
2623
|
+
}
|
|
2624
|
+
return byRoute;
|
|
2625
|
+
}
|
|
2626
|
+
function pageWaivers(registry, routePage) {
|
|
2627
|
+
return registry.get("page", routePage)?.meta?.waivers ?? {};
|
|
2628
|
+
}
|
|
2629
|
+
function isOptedOut(registry, page) {
|
|
2630
|
+
return page ? registry.get("page", page)?.meta?.capture === false : false;
|
|
2631
|
+
}
|
|
2632
|
+
function checkStateMatrix(registry, manifest, baseline) {
|
|
2633
|
+
const routes = registry.list("route");
|
|
2634
|
+
if (routes.length === 0) return [];
|
|
2635
|
+
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
2636
|
+
const captured = coreKindsByRoute(registry, manifest);
|
|
2637
|
+
const baselined = baseline?.missingKinds ?? {};
|
|
2638
|
+
const diagnostics = [];
|
|
2639
|
+
for (const [route, kinds] of captured) {
|
|
2640
|
+
const page = pageOf.get(route);
|
|
2641
|
+
if (isOptedOut(registry, page)) continue;
|
|
2642
|
+
const waivers = page ? pageWaivers(registry, page) : {};
|
|
2643
|
+
const loc = page ? registry.get("page", page)?.loc : void 0;
|
|
2644
|
+
const baselinedKinds = new Set(baselined[route] ?? []);
|
|
2645
|
+
for (const kind of CORE_KINDS) {
|
|
2646
|
+
if (kinds.has(kind)) continue;
|
|
2647
|
+
if (waivers[kind]) continue;
|
|
2648
|
+
if (baselinedKinds.has(kind)) {
|
|
2649
|
+
diagnostics.push({
|
|
2650
|
+
code: "matrix-backlog",
|
|
2651
|
+
severity: "info",
|
|
2652
|
+
message: `route "${route}" does not render core kind "${kind}" (acknowledged MISSING_KINDS backlog)`,
|
|
2653
|
+
file: loc?.file,
|
|
2654
|
+
line: loc?.line,
|
|
2655
|
+
hint: `Capture the "${kind}" state for "${route}", waive it (\`waivers: { ${kind}: "why it can't" }\` on the page), or leave it in the baseline.`
|
|
2656
|
+
});
|
|
2657
|
+
continue;
|
|
2658
|
+
}
|
|
2659
|
+
diagnostics.push({
|
|
2660
|
+
code: "core-kind",
|
|
2661
|
+
severity: "error",
|
|
2662
|
+
message: `route "${route}" is captured but never renders core kind "${kind}"`,
|
|
2663
|
+
file: loc?.file,
|
|
2664
|
+
line: loc?.line,
|
|
2665
|
+
entity: { kind: "route", id: route },
|
|
2666
|
+
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).`
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
for (const [route, kinds] of captured) {
|
|
2671
|
+
const page = pageOf.get(route);
|
|
2672
|
+
if (!page || isOptedOut(registry, page)) continue;
|
|
2673
|
+
const waivers = pageWaivers(registry, page);
|
|
2674
|
+
const loc = registry.get("page", page)?.loc;
|
|
2675
|
+
for (const kind of Object.keys(waivers)) {
|
|
2676
|
+
if (kinds.has(kind)) {
|
|
2677
|
+
diagnostics.push({
|
|
2678
|
+
code: "stale-waiver",
|
|
2679
|
+
severity: "warning",
|
|
2680
|
+
message: `page "${page}" waives core kind "${kind}", but route "${route}" does render it`,
|
|
2681
|
+
file: loc?.file,
|
|
2682
|
+
line: loc?.line,
|
|
2683
|
+
entity: { kind: "page", id: page },
|
|
2684
|
+
hint: `Remove the "${kind}" waiver \u2014 the route renders that kind, so the waiver's reason is stale.`
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
return diagnostics;
|
|
2690
|
+
}
|
|
2691
|
+
function computeMissingKinds(registry, manifest) {
|
|
2692
|
+
const routes = registry.list("route");
|
|
2693
|
+
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
2694
|
+
const captured = coreKindsByRoute(registry, manifest);
|
|
2695
|
+
const out2 = {};
|
|
2696
|
+
for (const [route, kinds] of captured) {
|
|
2697
|
+
const page = pageOf.get(route);
|
|
2698
|
+
if (isOptedOut(registry, page)) continue;
|
|
2699
|
+
const waivers = page ? pageWaivers(registry, page) : {};
|
|
2700
|
+
const missing = CORE_KINDS.filter((k) => !kinds.has(k) && !waivers[k]);
|
|
2701
|
+
if (missing.length > 0) out2[route] = missing;
|
|
2702
|
+
}
|
|
2703
|
+
return out2;
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
// src/scanner/scan/audit.ts
|
|
2224
2707
|
function audit(opts) {
|
|
2225
2708
|
const diagnostics = [];
|
|
2226
2709
|
const { registry, extracted, files, config } = opts;
|
|
@@ -2229,6 +2712,9 @@ function audit(opts) {
|
|
|
2229
2712
|
const acceptanceEnabled = config.audit?.acceptance ?? true;
|
|
2230
2713
|
const scopeLeakEnabled = config.audit?.scopeLeak ?? true;
|
|
2231
2714
|
const coverageEnabled = config.audit?.coverage ?? true;
|
|
2715
|
+
const statesEnabled = config.audit?.states ?? true;
|
|
2716
|
+
const pageCoverageEnabled = config.audit?.pageCoverage ?? true;
|
|
2717
|
+
const stateMatrixEnabled = config.audit?.stateMatrix ?? true;
|
|
2232
2718
|
if (opts.resolveDiagnostics) diagnostics.push(...opts.resolveDiagnostics);
|
|
2233
2719
|
for (const ef of extracted) {
|
|
2234
2720
|
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
@@ -2262,6 +2748,48 @@ function audit(opts) {
|
|
|
2262
2748
|
}
|
|
2263
2749
|
}
|
|
2264
2750
|
}
|
|
2751
|
+
if (check && opts.typesGenerated !== void 0) {
|
|
2752
|
+
const outRel = opts.typesOutputPath ?? config.output;
|
|
2753
|
+
const fresh = normalizeForCheck(opts.typesGenerated);
|
|
2754
|
+
if (opts.typesOnDisk === null || opts.typesOnDisk === void 0) {
|
|
2755
|
+
diagnostics.push({
|
|
2756
|
+
code: "gen-missing",
|
|
2757
|
+
severity: "error",
|
|
2758
|
+
message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
|
|
2759
|
+
file: outRel,
|
|
2760
|
+
hint: "Run `uidex scan` (without --check) to regenerate"
|
|
2761
|
+
});
|
|
2762
|
+
} else if (normalizeForCheck(opts.typesOnDisk) !== fresh) {
|
|
2763
|
+
diagnostics.push({
|
|
2764
|
+
code: "gen-stale",
|
|
2765
|
+
severity: "error",
|
|
2766
|
+
message: `Generated file "${outRel}" is stale`,
|
|
2767
|
+
file: outRel,
|
|
2768
|
+
hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
if (check && opts.locsGenerated !== void 0) {
|
|
2773
|
+
const outRel = opts.locsOutputPath ?? config.output;
|
|
2774
|
+
const fresh = normalizeForCheck(opts.locsGenerated);
|
|
2775
|
+
if (opts.locsOnDisk === null || opts.locsOnDisk === void 0) {
|
|
2776
|
+
diagnostics.push({
|
|
2777
|
+
code: "gen-missing",
|
|
2778
|
+
severity: "error",
|
|
2779
|
+
message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
|
|
2780
|
+
file: outRel,
|
|
2781
|
+
hint: "Run `uidex scan` (without --check) to regenerate"
|
|
2782
|
+
});
|
|
2783
|
+
} else if (normalizeForCheck(opts.locsOnDisk) !== fresh) {
|
|
2784
|
+
diagnostics.push({
|
|
2785
|
+
code: "gen-stale",
|
|
2786
|
+
severity: "error",
|
|
2787
|
+
message: `Generated file "${outRel}" is stale`,
|
|
2788
|
+
file: outRel,
|
|
2789
|
+
hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2265
2793
|
if (lint && acceptanceEnabled) {
|
|
2266
2794
|
for (const kind of ["widget", "feature", "page"]) {
|
|
2267
2795
|
for (const e of registry.list(kind)) {
|
|
@@ -2521,6 +3049,21 @@ function audit(opts) {
|
|
|
2521
3049
|
}
|
|
2522
3050
|
}
|
|
2523
3051
|
}
|
|
3052
|
+
if (lint && statesEnabled && opts.capturedStates) {
|
|
3053
|
+
diagnostics.push(...checkStateCoverage(registry, opts.capturedStates));
|
|
3054
|
+
}
|
|
3055
|
+
if (lint && pageCoverageEnabled && opts.capturedStates) {
|
|
3056
|
+
diagnostics.push(
|
|
3057
|
+
...checkPageCoverage(registry, opts.capturedStates, opts.pageBaseline)
|
|
3058
|
+
);
|
|
3059
|
+
}
|
|
3060
|
+
if (lint && stateMatrixEnabled && opts.capturedStates) {
|
|
3061
|
+
diagnostics.push(
|
|
3062
|
+
...checkStateMatrix(registry, opts.capturedStates, {
|
|
3063
|
+
missingKinds: opts.pageBaseline?.missingKinds ?? {}
|
|
3064
|
+
})
|
|
3065
|
+
);
|
|
3066
|
+
}
|
|
2524
3067
|
const summary = {
|
|
2525
3068
|
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
2526
3069
|
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
@@ -2685,6 +3228,148 @@ function stableReplacer(_key, value) {
|
|
|
2685
3228
|
}
|
|
2686
3229
|
|
|
2687
3230
|
// src/scanner/scan/emit.ts
|
|
3231
|
+
var AUTHORED_KINDS = /* @__PURE__ */ new Set([
|
|
3232
|
+
"route",
|
|
3233
|
+
"page",
|
|
3234
|
+
"feature",
|
|
3235
|
+
"widget",
|
|
3236
|
+
"primitive",
|
|
3237
|
+
"flow"
|
|
3238
|
+
]);
|
|
3239
|
+
function internedPayload(entities) {
|
|
3240
|
+
const fileIndex = /* @__PURE__ */ new Map();
|
|
3241
|
+
const files = [];
|
|
3242
|
+
const compact = entities.map((e) => {
|
|
3243
|
+
const loc = e.loc;
|
|
3244
|
+
if (!loc || typeof loc.file !== "string") return e;
|
|
3245
|
+
let idx = fileIndex.get(loc.file);
|
|
3246
|
+
if (idx === void 0) {
|
|
3247
|
+
idx = files.length;
|
|
3248
|
+
files.push(loc.file);
|
|
3249
|
+
fileIndex.set(loc.file, idx);
|
|
3250
|
+
}
|
|
3251
|
+
const rest = { ...loc };
|
|
3252
|
+
delete rest.file;
|
|
3253
|
+
return { ...e, loc: { f: idx, ...rest } };
|
|
3254
|
+
});
|
|
3255
|
+
return {
|
|
3256
|
+
filesDecl: `const files: readonly string[] = ${jsonStable(files)}`,
|
|
3257
|
+
parseExpr: `(JSON.parse(${JSON.stringify(
|
|
3258
|
+
JSON.stringify(compact, replacerSorted)
|
|
3259
|
+
)}) as any[]).map(hydrate)`
|
|
3260
|
+
};
|
|
3261
|
+
}
|
|
3262
|
+
var HYDRATE_FN = [
|
|
3263
|
+
"function hydrate(e: any): any {",
|
|
3264
|
+
' if (e.loc && typeof e.loc.f === "number") {',
|
|
3265
|
+
" const { f, ...rest } = e.loc",
|
|
3266
|
+
" e.loc = { file: files[f], ...rest }",
|
|
3267
|
+
" }",
|
|
3268
|
+
" return e",
|
|
3269
|
+
"}"
|
|
3270
|
+
].join("\n");
|
|
3271
|
+
function compactMeta(meta) {
|
|
3272
|
+
if (!meta || typeof meta !== "object") return meta;
|
|
3273
|
+
const composes = meta.composes;
|
|
3274
|
+
if (!Array.isArray(composes)) return meta;
|
|
3275
|
+
return {
|
|
3276
|
+
...meta,
|
|
3277
|
+
composes: composes.map(
|
|
3278
|
+
(c) => c && typeof c === "object" && c.kind === "element" && Object.keys(c).length === 2 && typeof c.id === "string" ? c.id : c
|
|
3279
|
+
)
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
function emitLocsModule(thin, uidexImport) {
|
|
3283
|
+
const kinds = [...new Set(thin.map((e) => e.kind))].sort();
|
|
3284
|
+
const kindIdx = new Map(kinds.map((k, i) => [k, i]));
|
|
3285
|
+
const fileIndex = /* @__PURE__ */ new Map();
|
|
3286
|
+
const files = [];
|
|
3287
|
+
const fi = (f) => {
|
|
3288
|
+
let i = fileIndex.get(f);
|
|
3289
|
+
if (i === void 0) {
|
|
3290
|
+
i = files.length;
|
|
3291
|
+
files.push(f);
|
|
3292
|
+
fileIndex.set(f, i);
|
|
3293
|
+
}
|
|
3294
|
+
return i;
|
|
3295
|
+
};
|
|
3296
|
+
const bare = [];
|
|
3297
|
+
const rich = [];
|
|
3298
|
+
for (const e of thin) {
|
|
3299
|
+
const loc = e.loc;
|
|
3300
|
+
const topOnlyKindIdLoc = Object.keys(e).every(
|
|
3301
|
+
(k) => k === "kind" || k === "id" || k === "loc"
|
|
3302
|
+
);
|
|
3303
|
+
const locOnlyKnown = !!loc && typeof loc.file === "string" && Object.keys(loc).every(
|
|
3304
|
+
(k) => k === "file" || k === "line" || k === "column"
|
|
3305
|
+
);
|
|
3306
|
+
if (topOnlyKindIdLoc && locOnlyKnown) {
|
|
3307
|
+
const row = [
|
|
3308
|
+
kindIdx.get(e.kind),
|
|
3309
|
+
e.id,
|
|
3310
|
+
fi(loc.file)
|
|
3311
|
+
];
|
|
3312
|
+
if (loc.line != null || loc.column != null)
|
|
3313
|
+
row.push(Number(loc.line ?? 0));
|
|
3314
|
+
if (loc.column != null) row.push(Number(loc.column));
|
|
3315
|
+
bare.push(row);
|
|
3316
|
+
} else {
|
|
3317
|
+
const richEntity = { ...e };
|
|
3318
|
+
if (loc && typeof loc.file === "string") {
|
|
3319
|
+
const { file, ...rest } = loc;
|
|
3320
|
+
richEntity.loc = { f: fi(file), ...rest };
|
|
3321
|
+
}
|
|
3322
|
+
if (richEntity.meta) richEntity.meta = compactMeta(richEntity.meta);
|
|
3323
|
+
rich.push(richEntity);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
const str = (v) => JSON.stringify(JSON.stringify(v, replacerSorted));
|
|
3327
|
+
const l = [];
|
|
3328
|
+
l.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
3329
|
+
l.push(
|
|
3330
|
+
"// Deferred element/region source locations for the inspector's copy-path."
|
|
3331
|
+
);
|
|
3332
|
+
l.push("// @ts-nocheck");
|
|
3333
|
+
l.push("/* eslint-disable */");
|
|
3334
|
+
l.push(`import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`);
|
|
3335
|
+
l.push("");
|
|
3336
|
+
l.push(`const files: readonly string[] = ${jsonStable(files)}`);
|
|
3337
|
+
l.push(`const kinds: readonly string[] = ${jsonStable(kinds)}`);
|
|
3338
|
+
l.push("function loc(fi: number, line?: number, col?: number) {");
|
|
3339
|
+
l.push(" const o: any = { file: files[fi] }");
|
|
3340
|
+
l.push(" if (line) o.line = line");
|
|
3341
|
+
l.push(" if (col) o.column = col");
|
|
3342
|
+
l.push(" return o");
|
|
3343
|
+
l.push("}");
|
|
3344
|
+
l.push(`const bare: any[] = JSON.parse(${str(bare)})`);
|
|
3345
|
+
l.push(`const rich: any[] = JSON.parse(${str(rich)})`);
|
|
3346
|
+
l.push("");
|
|
3347
|
+
l.push(
|
|
3348
|
+
"/** Add the thin element/region entities (source locations) to a registry. */"
|
|
3349
|
+
);
|
|
3350
|
+
l.push("export function loadLocs(target: Registry): Registry {");
|
|
3351
|
+
l.push(" for (const r of bare)");
|
|
3352
|
+
l.push(
|
|
3353
|
+
" target.add({ kind: kinds[r[0]], id: r[1], loc: loc(r[2], r[3], r[4]) } as Entity)"
|
|
3354
|
+
);
|
|
3355
|
+
l.push(" for (const e of rich) {");
|
|
3356
|
+
l.push(" const out: any = { ...e }");
|
|
3357
|
+
l.push(' if (e.loc && typeof e.loc.f === "number") {');
|
|
3358
|
+
l.push(" const { f, ...rest } = e.loc");
|
|
3359
|
+
l.push(" out.loc = { file: files[f], ...rest }");
|
|
3360
|
+
l.push(" }");
|
|
3361
|
+
l.push(" if (e.meta && Array.isArray(e.meta.composes)) {");
|
|
3362
|
+
l.push(
|
|
3363
|
+
' out.meta = { ...e.meta, composes: e.meta.composes.map((c: any) => typeof c === "string" ? { id: c, kind: "element" } : c) }'
|
|
3364
|
+
);
|
|
3365
|
+
l.push(" }");
|
|
3366
|
+
l.push(" target.add(out)");
|
|
3367
|
+
l.push(" }");
|
|
3368
|
+
l.push(" return target");
|
|
3369
|
+
l.push("}");
|
|
3370
|
+
l.push("");
|
|
3371
|
+
return l.join("\n");
|
|
3372
|
+
}
|
|
2688
3373
|
function sortById(arr) {
|
|
2689
3374
|
return [...arr].sort((a, b) => a.id.localeCompare(b.id));
|
|
2690
3375
|
}
|
|
@@ -2710,6 +3395,20 @@ function emitIdUnion(name, ids) {
|
|
|
2710
3395
|
${body}
|
|
2711
3396
|
`;
|
|
2712
3397
|
}
|
|
3398
|
+
var OPAQUE_ID_UNIONS = /* @__PURE__ */ new Set(["ElementId"]);
|
|
3399
|
+
function emitId(name, ids) {
|
|
3400
|
+
if (OPAQUE_ID_UNIONS.has(name)) {
|
|
3401
|
+
return `export type ${name} = string & { readonly __uidex?: ${JSON.stringify(name)} }
|
|
3402
|
+
`;
|
|
3403
|
+
}
|
|
3404
|
+
return emitIdUnion(name, ids);
|
|
3405
|
+
}
|
|
3406
|
+
function dataOutputPath(output) {
|
|
3407
|
+
return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".data.$1");
|
|
3408
|
+
}
|
|
3409
|
+
function locsOutputPath(output) {
|
|
3410
|
+
return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".locs.$1");
|
|
3411
|
+
}
|
|
2713
3412
|
function emit(opts) {
|
|
2714
3413
|
const { registry, gitContext, uidexImport = "uidex" } = opts;
|
|
2715
3414
|
const routes = [...registry.list("route")].sort(
|
|
@@ -2718,112 +3417,131 @@ function emit(opts) {
|
|
|
2718
3417
|
const pages = sortById(registry.list("page"));
|
|
2719
3418
|
const features = sortById(registry.list("feature"));
|
|
2720
3419
|
const widgets = sortById(registry.list("widget"));
|
|
3420
|
+
const stateNames = /* @__PURE__ */ new Set();
|
|
3421
|
+
for (const e of [...pages, ...features, ...widgets]) {
|
|
3422
|
+
for (const s of e.meta?.states ?? []) stateNames.add(s.name);
|
|
3423
|
+
}
|
|
2721
3424
|
const regions = sortById(registry.list("region"));
|
|
2722
3425
|
const elements = sortById(registry.list("element"));
|
|
2723
3426
|
const primitives = sortById(registry.list("primitive"));
|
|
2724
3427
|
const flows = sortById(registry.list("flow"));
|
|
2725
|
-
const
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
lines.push(
|
|
2730
|
-
`import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`
|
|
3428
|
+
const t = [];
|
|
3429
|
+
t.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
3430
|
+
t.push(
|
|
3431
|
+
"// Types only \u2014 the runtime registry lives in the sibling `.data` module."
|
|
2731
3432
|
);
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
3433
|
+
t.push("/* eslint-disable */");
|
|
3434
|
+
t.push("");
|
|
3435
|
+
t.push("// ---- id unions ----");
|
|
3436
|
+
t.push(
|
|
3437
|
+
emitId(
|
|
2736
3438
|
"PageId",
|
|
2737
3439
|
pages.map((e) => e.id)
|
|
2738
3440
|
)
|
|
2739
3441
|
);
|
|
2740
|
-
|
|
2741
|
-
|
|
3442
|
+
t.push(
|
|
3443
|
+
emitId(
|
|
2742
3444
|
"FeatureId",
|
|
2743
3445
|
features.map((e) => e.id)
|
|
2744
3446
|
)
|
|
2745
3447
|
);
|
|
2746
|
-
|
|
2747
|
-
|
|
3448
|
+
t.push(
|
|
3449
|
+
emitId(
|
|
2748
3450
|
"WidgetId",
|
|
2749
3451
|
widgets.map((e) => e.id)
|
|
2750
3452
|
)
|
|
2751
3453
|
);
|
|
2752
|
-
|
|
2753
|
-
|
|
3454
|
+
t.push(
|
|
3455
|
+
emitId(
|
|
2754
3456
|
"RegionId",
|
|
2755
3457
|
regions.map((e) => e.id)
|
|
2756
3458
|
)
|
|
2757
3459
|
);
|
|
2758
|
-
|
|
2759
|
-
|
|
3460
|
+
t.push(
|
|
3461
|
+
emitId(
|
|
2760
3462
|
"ElementId",
|
|
2761
3463
|
elements.map((e) => e.id)
|
|
2762
3464
|
)
|
|
2763
3465
|
);
|
|
2764
|
-
|
|
2765
|
-
|
|
3466
|
+
t.push(
|
|
3467
|
+
emitId(
|
|
2766
3468
|
"PrimitiveId",
|
|
2767
3469
|
primitives.map((e) => e.id)
|
|
2768
3470
|
)
|
|
2769
3471
|
);
|
|
2770
|
-
|
|
2771
|
-
|
|
3472
|
+
t.push(
|
|
3473
|
+
emitId(
|
|
2772
3474
|
"FlowId",
|
|
2773
3475
|
flows.map((e) => e.id)
|
|
2774
3476
|
)
|
|
2775
3477
|
);
|
|
2776
|
-
|
|
2777
|
-
|
|
3478
|
+
t.push(
|
|
3479
|
+
emitId(
|
|
2778
3480
|
"RouteId",
|
|
2779
3481
|
routes.map((e) => e.path)
|
|
2780
3482
|
)
|
|
2781
3483
|
);
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
3484
|
+
t.push(emitId("StateId", [...stateNames]));
|
|
3485
|
+
t.push("");
|
|
3486
|
+
t.push("// ---- authoring-surface shape types ----");
|
|
3487
|
+
t.push("export namespace Uidex {");
|
|
3488
|
+
t.push(
|
|
3489
|
+
' export type CoreStateKind = "loading" | "empty" | "populated" | "error"'
|
|
3490
|
+
);
|
|
3491
|
+
t.push(' export type StateKind = CoreStateKind | "variant"');
|
|
3492
|
+
t.push(" export interface State {");
|
|
3493
|
+
t.push(" name: string");
|
|
3494
|
+
t.push(" kind?: StateKind");
|
|
3495
|
+
t.push(" acceptance?: readonly string[]");
|
|
3496
|
+
t.push(" description?: string");
|
|
3497
|
+
t.push(" }");
|
|
3498
|
+
t.push(" export type Waivers = Partial<Record<CoreStateKind, string>>");
|
|
3499
|
+
t.push(" export interface Page {");
|
|
3500
|
+
t.push(" page: PageId | false");
|
|
3501
|
+
t.push(" name?: string");
|
|
3502
|
+
t.push(" features?: readonly FeatureId[]");
|
|
3503
|
+
t.push(" widgets?: readonly WidgetId[]");
|
|
3504
|
+
t.push(" acceptance?: readonly string[]");
|
|
3505
|
+
t.push(" states?: readonly (string | State)[]");
|
|
3506
|
+
t.push(" capture?: boolean");
|
|
3507
|
+
t.push(" waivers?: Waivers");
|
|
3508
|
+
t.push(" description?: string");
|
|
3509
|
+
t.push(" }");
|
|
3510
|
+
t.push(" export interface Feature {");
|
|
3511
|
+
t.push(" feature: FeatureId | false");
|
|
3512
|
+
t.push(" name?: string");
|
|
3513
|
+
t.push(" features?: readonly FeatureId[]");
|
|
3514
|
+
t.push(" acceptance?: readonly string[]");
|
|
3515
|
+
t.push(" states?: readonly (string | State)[]");
|
|
3516
|
+
t.push(" description?: string");
|
|
3517
|
+
t.push(" }");
|
|
3518
|
+
t.push(" export interface Primitive {");
|
|
3519
|
+
t.push(" primitive: PrimitiveId");
|
|
3520
|
+
t.push(" name?: string");
|
|
3521
|
+
t.push(" description?: string");
|
|
3522
|
+
t.push(" }");
|
|
3523
|
+
t.push(" export interface Widget {");
|
|
3524
|
+
t.push(" widget: WidgetId");
|
|
3525
|
+
t.push(" name?: string");
|
|
3526
|
+
t.push(" acceptance?: readonly string[]");
|
|
3527
|
+
t.push(" states?: readonly (string | State)[]");
|
|
3528
|
+
t.push(" description?: string");
|
|
3529
|
+
t.push(" }");
|
|
3530
|
+
t.push(" export interface Region {");
|
|
3531
|
+
t.push(" region: RegionId | false");
|
|
3532
|
+
t.push(" name?: string");
|
|
3533
|
+
t.push(" description?: string");
|
|
3534
|
+
t.push(" }");
|
|
3535
|
+
t.push(" export interface Flow {");
|
|
3536
|
+
t.push(" flow: FlowId");
|
|
3537
|
+
t.push(" name?: string");
|
|
3538
|
+
t.push(" description?: string");
|
|
3539
|
+
t.push(" }");
|
|
3540
|
+
t.push(" export interface NotFlow {");
|
|
3541
|
+
t.push(" notFlow: true");
|
|
3542
|
+
t.push(" }");
|
|
3543
|
+
t.push("}");
|
|
3544
|
+
t.push("");
|
|
2827
3545
|
const allEntities = [
|
|
2828
3546
|
...routes,
|
|
2829
3547
|
...pages,
|
|
@@ -2834,25 +3552,39 @@ function emit(opts) {
|
|
|
2834
3552
|
...primitives,
|
|
2835
3553
|
...flows
|
|
2836
3554
|
];
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
);
|
|
2840
|
-
lines.push("");
|
|
2841
|
-
lines.push("// ---- git context ----");
|
|
3555
|
+
const authored = allEntities.filter((e) => AUTHORED_KINDS.has(e.kind));
|
|
3556
|
+
const thin = allEntities.filter((e) => !AUTHORED_KINDS.has(e.kind));
|
|
2842
3557
|
const gc = gitContext ?? { branch: null, commit: null, pr: null };
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
3558
|
+
const authoredPayload = internedPayload(authored);
|
|
3559
|
+
const d = [];
|
|
3560
|
+
d.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
3561
|
+
d.push(
|
|
3562
|
+
"// Authored-metadata registry. The thin element/region entities (source"
|
|
3563
|
+
);
|
|
3564
|
+
d.push(
|
|
3565
|
+
"// locations only) live in the sibling `.locs` module, loaded lazily."
|
|
3566
|
+
);
|
|
3567
|
+
d.push("// @ts-nocheck");
|
|
3568
|
+
d.push("/* eslint-disable */");
|
|
3569
|
+
d.push(`import { createUidex } from ${JSON.stringify(uidexImport)}`);
|
|
3570
|
+
d.push(`import type { Registry } from ${JSON.stringify(uidexImport)}`);
|
|
3571
|
+
d.push("");
|
|
3572
|
+
d.push(authoredPayload.filesDecl);
|
|
3573
|
+
d.push(HYDRATE_FN);
|
|
3574
|
+
d.push(`export const entities = ${authoredPayload.parseExpr}`);
|
|
3575
|
+
d.push("");
|
|
3576
|
+
d.push(`export const gitContext = ${jsonStable(gc)} as const`);
|
|
3577
|
+
d.push("");
|
|
3578
|
+
d.push("export function loadRegistry(target: Registry): Registry {");
|
|
3579
|
+
d.push(" for (const entity of entities) target.add(entity)");
|
|
3580
|
+
d.push(" return target");
|
|
3581
|
+
d.push("}");
|
|
3582
|
+
d.push("");
|
|
3583
|
+
d.push("export const uidex = createUidex()");
|
|
3584
|
+
d.push("for (const entity of entities) uidex.registry.add(entity)");
|
|
3585
|
+
d.push("");
|
|
3586
|
+
const locs = emitLocsModule(thin, uidexImport);
|
|
3587
|
+
return { types: t.join("\n"), data: d.join("\n"), locs };
|
|
2856
3588
|
}
|
|
2857
3589
|
|
|
2858
3590
|
// src/scanner/scan/git.ts
|
|
@@ -2963,6 +3695,68 @@ function renderSpec(args) {
|
|
|
2963
3695
|
// src/scanner/scan/pipeline.ts
|
|
2964
3696
|
var fs4 = __toESM(require("fs"), 1);
|
|
2965
3697
|
var path7 = __toESM(require("path"), 1);
|
|
3698
|
+
var DEFAULT_STATES_MANIFEST = "uidex-states.json";
|
|
3699
|
+
var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
|
|
3700
|
+
function loadPageBaseline(configDir, config) {
|
|
3701
|
+
const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
3702
|
+
const abs = path7.resolve(configDir, rel);
|
|
3703
|
+
let raw;
|
|
3704
|
+
try {
|
|
3705
|
+
raw = fs4.readFileSync(abs, "utf8");
|
|
3706
|
+
} catch {
|
|
3707
|
+
return void 0;
|
|
3708
|
+
}
|
|
3709
|
+
try {
|
|
3710
|
+
const parsed = JSON.parse(raw);
|
|
3711
|
+
if (!parsed || !Array.isArray(parsed.uncapturedPages)) return void 0;
|
|
3712
|
+
const uncapturedPages = parsed.uncapturedPages.filter(
|
|
3713
|
+
(p2) => typeof p2 === "string"
|
|
3714
|
+
);
|
|
3715
|
+
let missingKinds;
|
|
3716
|
+
const mkRaw = parsed.missingKinds;
|
|
3717
|
+
if (mkRaw && typeof mkRaw === "object" && !Array.isArray(mkRaw)) {
|
|
3718
|
+
const mk = {};
|
|
3719
|
+
for (const [route, kinds] of Object.entries(mkRaw)) {
|
|
3720
|
+
if (Array.isArray(kinds)) {
|
|
3721
|
+
mk[route] = kinds.filter((k) => typeof k === "string");
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
missingKinds = mk;
|
|
3725
|
+
}
|
|
3726
|
+
return { uncapturedPages, ...missingKinds ? { missingKinds } : {} };
|
|
3727
|
+
} catch {
|
|
3728
|
+
return void 0;
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
function loadCapturedStates(configDir, config) {
|
|
3732
|
+
const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
|
|
3733
|
+
const abs = path7.resolve(configDir, rel);
|
|
3734
|
+
let raw;
|
|
3735
|
+
try {
|
|
3736
|
+
raw = fs4.readFileSync(abs, "utf8");
|
|
3737
|
+
} catch {
|
|
3738
|
+
return void 0;
|
|
3739
|
+
}
|
|
3740
|
+
try {
|
|
3741
|
+
const parsed = JSON.parse(raw);
|
|
3742
|
+
if (!parsed || !Array.isArray(parsed.captured)) return void 0;
|
|
3743
|
+
const captured = [];
|
|
3744
|
+
for (const entry of parsed.captured) {
|
|
3745
|
+
if (!entry || typeof entry !== "object") continue;
|
|
3746
|
+
const c = entry;
|
|
3747
|
+
if (typeof c.entity !== "string" || typeof c.state !== "string") continue;
|
|
3748
|
+
const rec = { entity: c.entity, state: c.state };
|
|
3749
|
+
if (typeof c.kind === "string") rec.kind = c.kind;
|
|
3750
|
+
if (typeof c.stateKind === "string") rec.stateKind = c.stateKind;
|
|
3751
|
+
if (typeof c.url === "string") rec.url = c.url;
|
|
3752
|
+
if (typeof c.route === "string") rec.route = c.route;
|
|
3753
|
+
captured.push(rec);
|
|
3754
|
+
}
|
|
3755
|
+
return { captured };
|
|
3756
|
+
} catch {
|
|
3757
|
+
return void 0;
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
2966
3760
|
function runScan(opts = {}) {
|
|
2967
3761
|
const cwd = opts.cwd ?? process.cwd();
|
|
2968
3762
|
const configs = opts.configs ?? discover({ cwd });
|
|
@@ -2989,23 +3783,29 @@ function runOne(dc, opts) {
|
|
|
2989
3783
|
flowFiles: extractedFlows
|
|
2990
3784
|
});
|
|
2991
3785
|
const gitContext = resolveGitContext({ cwd: configDir });
|
|
2992
|
-
const
|
|
3786
|
+
const emitted = emit({
|
|
2993
3787
|
registry: resolved.registry,
|
|
2994
3788
|
gitContext
|
|
2995
3789
|
});
|
|
2996
|
-
const
|
|
2997
|
-
const
|
|
2998
|
-
|
|
3790
|
+
const typesRel = config.output;
|
|
3791
|
+
const typesPath = path7.resolve(configDir, typesRel);
|
|
3792
|
+
const dataRel = dataOutputPath(config.output);
|
|
3793
|
+
const dataPath = path7.resolve(configDir, dataRel);
|
|
3794
|
+
const locsRel = locsOutputPath(config.output);
|
|
3795
|
+
const locsPath = path7.resolve(configDir, locsRel);
|
|
3796
|
+
let typesOnDisk = null;
|
|
3797
|
+
let dataOnDisk = null;
|
|
3798
|
+
let locsOnDisk = null;
|
|
2999
3799
|
if (opts.check) {
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
existingOnDisk = null;
|
|
3004
|
-
}
|
|
3800
|
+
typesOnDisk = readOrNull(typesPath);
|
|
3801
|
+
dataOnDisk = readOrNull(dataPath);
|
|
3802
|
+
locsOnDisk = readOrNull(locsPath);
|
|
3005
3803
|
}
|
|
3006
3804
|
const hasExtractDiagnostics = [...extracted, ...extractedFlows].some(
|
|
3007
3805
|
(ef) => (ef.diagnostics?.length ?? 0) > 0
|
|
3008
3806
|
);
|
|
3807
|
+
const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
|
|
3808
|
+
const pageBaseline = opts.pageBaseline ?? loadPageBaseline(configDir, config);
|
|
3009
3809
|
let auditResult;
|
|
3010
3810
|
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
|
|
3011
3811
|
auditResult = audit({
|
|
@@ -3017,9 +3817,20 @@ function runOne(dc, opts) {
|
|
|
3017
3817
|
check: opts.check,
|
|
3018
3818
|
lint: opts.lint,
|
|
3019
3819
|
resolveDiagnostics: resolved.diagnostics,
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3820
|
+
// The entity array (and thus the meaningful drift) lives in the data file,
|
|
3821
|
+
// so the entity-aware gen-stale diff runs against it; the types file gets a
|
|
3822
|
+
// plain content compare (see audit()).
|
|
3823
|
+
generated: emitted.data,
|
|
3824
|
+
existingOnDisk: dataOnDisk,
|
|
3825
|
+
outputPath: dataRel,
|
|
3826
|
+
typesGenerated: emitted.types,
|
|
3827
|
+
typesOnDisk,
|
|
3828
|
+
typesOutputPath: typesRel,
|
|
3829
|
+
locsGenerated: emitted.locs,
|
|
3830
|
+
locsOnDisk,
|
|
3831
|
+
locsOutputPath: locsRel,
|
|
3832
|
+
capturedStates,
|
|
3833
|
+
pageBaseline
|
|
3023
3834
|
});
|
|
3024
3835
|
}
|
|
3025
3836
|
return {
|
|
@@ -3028,13 +3839,38 @@ function runOne(dc, opts) {
|
|
|
3028
3839
|
registry: resolved.registry,
|
|
3029
3840
|
gitContext,
|
|
3030
3841
|
audit: auditResult,
|
|
3031
|
-
|
|
3032
|
-
|
|
3842
|
+
types: {
|
|
3843
|
+
generated: emitted.types,
|
|
3844
|
+
outputPath: typesPath,
|
|
3845
|
+
outputRel: typesRel
|
|
3846
|
+
},
|
|
3847
|
+
data: { generated: emitted.data, outputPath: dataPath, outputRel: dataRel },
|
|
3848
|
+
locs: { generated: emitted.locs, outputPath: locsPath, outputRel: locsRel },
|
|
3849
|
+
capturedStates
|
|
3033
3850
|
};
|
|
3034
3851
|
}
|
|
3852
|
+
function readOrNull(p2) {
|
|
3853
|
+
try {
|
|
3854
|
+
return fs4.readFileSync(p2, "utf8");
|
|
3855
|
+
} catch {
|
|
3856
|
+
return null;
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
function writeArtifact(artifact) {
|
|
3860
|
+
if (readOrNull(artifact.outputPath) === artifact.generated) return false;
|
|
3861
|
+
fs4.mkdirSync(path7.dirname(artifact.outputPath), { recursive: true });
|
|
3862
|
+
fs4.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
|
|
3863
|
+
return true;
|
|
3864
|
+
}
|
|
3035
3865
|
function writeScanResult(result) {
|
|
3036
|
-
|
|
3037
|
-
|
|
3866
|
+
const wroteTypes = writeArtifact(result.types);
|
|
3867
|
+
const wroteData = writeArtifact(result.data);
|
|
3868
|
+
const wroteLocs = writeArtifact(result.locs);
|
|
3869
|
+
return wroteTypes || wroteData || wroteLocs;
|
|
3870
|
+
}
|
|
3871
|
+
function pageBaselinePath(result) {
|
|
3872
|
+
const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
3873
|
+
return path7.resolve(result.configDir, rel);
|
|
3038
3874
|
}
|
|
3039
3875
|
|
|
3040
3876
|
// src/scanner/scan/fix.ts
|
|
@@ -3607,16 +4443,13 @@ function helpText2() {
|
|
|
3607
4443
|
" scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
|
|
3608
4444
|
" rename <element|widget|region> <old-id> <new-id> Rename an id everywhere (DOM attr, flows, exports)",
|
|
3609
4445
|
" ai <install|uninstall|providers> Manage AI assistant integrations",
|
|
3610
|
-
" api <METHOD> <PATH> Call the uidex API",
|
|
3611
|
-
" api --list Show available API routes",
|
|
3612
|
-
" api login Authenticate via browser",
|
|
3613
|
-
" api login --token <tok> Store an auth token directly",
|
|
3614
4446
|
"",
|
|
3615
4447
|
"Flags:",
|
|
3616
4448
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
3617
4449
|
" --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
|
|
3618
4450
|
" --audit Equivalent to --check --lint (read-only)",
|
|
3619
4451
|
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
|
|
4452
|
+
" --update-baseline Regenerate the page-coverage baseline (uidex-coverage-baseline.json) from the current uncaptured routes",
|
|
3620
4453
|
" --json Emit JSON diagnostics on stdout",
|
|
3621
4454
|
" --force (scaffold) overwrite existing spec",
|
|
3622
4455
|
""
|
|
@@ -3636,7 +4469,7 @@ function runInit(cwd, w) {
|
|
|
3636
4469
|
fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3637
4470
|
w.out(`Created ${configPath}`);
|
|
3638
4471
|
const gitignorePath = path11.join(cwd, ".gitignore");
|
|
3639
|
-
const entry = "*.gen
|
|
4472
|
+
const entry = "*.gen.*";
|
|
3640
4473
|
if (fs8.existsSync(gitignorePath)) {
|
|
3641
4474
|
const existing = fs8.readFileSync(gitignorePath, "utf8");
|
|
3642
4475
|
const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
|
|
@@ -3659,8 +4492,9 @@ function runInit(cwd, w) {
|
|
|
3659
4492
|
}
|
|
3660
4493
|
function runScanCommand(cwd, flags, w) {
|
|
3661
4494
|
const fix = Boolean(flags.fix);
|
|
3662
|
-
const
|
|
3663
|
-
const
|
|
4495
|
+
const updateBaseline = Boolean(flags["update-baseline"]);
|
|
4496
|
+
const check = !fix && !updateBaseline && Boolean(flags.check || flags.audit);
|
|
4497
|
+
const lint = Boolean(flags.lint || flags.audit || fix || updateBaseline);
|
|
3664
4498
|
const asJson = Boolean(flags.json);
|
|
3665
4499
|
let configs = discover({ cwd });
|
|
3666
4500
|
if (configs.length === 0) {
|
|
@@ -3679,8 +4513,33 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3679
4513
|
configs = discover({ cwd });
|
|
3680
4514
|
}
|
|
3681
4515
|
const results = runScan({ cwd, check, lint, configs });
|
|
4516
|
+
const artifacts = results.flatMap((r) => [r.types, r.data, r.locs]);
|
|
4517
|
+
const wrote = /* @__PURE__ */ new Set();
|
|
3682
4518
|
if (!check) {
|
|
3683
|
-
for (const
|
|
4519
|
+
for (const a of artifacts) {
|
|
4520
|
+
if (writeArtifact(a)) wrote.add(a);
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4523
|
+
if (updateBaseline) {
|
|
4524
|
+
for (const r of results) {
|
|
4525
|
+
const manifest = r.capturedStates ?? { captured: [] };
|
|
4526
|
+
const pageBaseline = computePageBaseline(r.registry, manifest);
|
|
4527
|
+
const missingKinds = computeMissingKinds(r.registry, manifest);
|
|
4528
|
+
const baseline = {
|
|
4529
|
+
...pageBaseline,
|
|
4530
|
+
...Object.keys(missingKinds).length > 0 ? { missingKinds } : {}
|
|
4531
|
+
};
|
|
4532
|
+
const p2 = pageBaselinePath(r);
|
|
4533
|
+
fs8.writeFileSync(p2, JSON.stringify(baseline, null, 2) + "\n");
|
|
4534
|
+
const kindGaps = Object.values(missingKinds).reduce(
|
|
4535
|
+
(n, ks) => n + ks.length,
|
|
4536
|
+
0
|
|
4537
|
+
);
|
|
4538
|
+
w.out(
|
|
4539
|
+
`Wrote ${p2} \u2014 ${baseline.uncapturedPages.length} uncaptured page(s), ${kindGaps} missing core-kind(s) baselined`
|
|
4540
|
+
);
|
|
4541
|
+
}
|
|
4542
|
+
return w.result(0);
|
|
3684
4543
|
}
|
|
3685
4544
|
const allDiagnostics = results.flatMap((r) => r.audit?.diagnostics ?? []);
|
|
3686
4545
|
const summary = results.reduce(
|
|
@@ -3708,10 +4567,14 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3708
4567
|
);
|
|
3709
4568
|
}
|
|
3710
4569
|
for (const r of results) {
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
4570
|
+
for (const art of [r.types, r.data, r.locs]) {
|
|
4571
|
+
if (check) {
|
|
4572
|
+
w.out(`Checked ${art.outputPath}`);
|
|
4573
|
+
} else if (wrote.has(art)) {
|
|
4574
|
+
w.out(`Wrote ${art.outputPath}`);
|
|
4575
|
+
} else {
|
|
4576
|
+
w.out(`Unchanged ${art.outputPath}`);
|
|
4577
|
+
}
|
|
3715
4578
|
}
|
|
3716
4579
|
for (const d of r.audit?.diagnostics ?? []) {
|
|
3717
4580
|
const loc = d.file ? `${d.file}${d.line ? `:${d.line}` : ""}` : "";
|
|
@@ -3820,12 +4683,21 @@ function createWriter() {
|
|
|
3820
4683
|
DEFAULT_CONVENTIONS,
|
|
3821
4684
|
applyFixes,
|
|
3822
4685
|
audit,
|
|
4686
|
+
checkPageCoverage,
|
|
4687
|
+
checkStateCoverage,
|
|
4688
|
+
checkStateMatrix,
|
|
4689
|
+
compileRoute,
|
|
4690
|
+
computeMissingKinds,
|
|
4691
|
+
computePageBaseline,
|
|
3823
4692
|
detectRoutes,
|
|
3824
4693
|
discover,
|
|
3825
4694
|
emit,
|
|
3826
4695
|
extract,
|
|
3827
4696
|
extractUidexExports,
|
|
3828
4697
|
globToRegExp,
|
|
4698
|
+
loadPageBaseline,
|
|
4699
|
+
matchUrlToRoute,
|
|
4700
|
+
pageBaselinePath,
|
|
3829
4701
|
parseConfig,
|
|
3830
4702
|
pathToId,
|
|
3831
4703
|
renameEntity,
|