uidex 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.cjs +1112 -1054
- package/dist/cli/cli.cjs.map +1 -1
- package/dist/headless/index.cjs +4 -448
- package/dist/headless/index.cjs.map +1 -1
- package/dist/headless/index.d.cts +41 -11
- package/dist/headless/index.d.ts +41 -11
- package/dist/headless/index.js +4 -450
- package/dist/headless/index.js.map +1 -1
- package/dist/index.cjs +147 -3252
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -316
- package/dist/index.d.ts +43 -316
- package/dist/index.js +133 -3254
- package/dist/index.js.map +1 -1
- package/dist/playwright/index.cjs +175 -0
- package/dist/playwright/index.cjs.map +1 -1
- package/dist/playwright/index.d.cts +2 -0
- package/dist/playwright/index.d.ts +2 -0
- package/dist/playwright/index.js +167 -0
- package/dist/playwright/index.js.map +1 -1
- package/dist/playwright/states-reporter.cjs +123 -0
- package/dist/playwright/states-reporter.cjs.map +1 -0
- package/dist/playwright/states-reporter.d.cts +46 -0
- package/dist/playwright/states-reporter.d.ts +46 -0
- package/dist/playwright/states-reporter.js +88 -0
- package/dist/playwright/states-reporter.js.map +1 -0
- package/dist/playwright/states.cjs +118 -0
- package/dist/playwright/states.cjs.map +1 -0
- package/dist/playwright/states.d.cts +120 -0
- package/dist/playwright/states.d.ts +120 -0
- package/dist/playwright/states.js +88 -0
- package/dist/playwright/states.js.map +1 -0
- package/dist/react/index.cjs +163 -3255
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +62 -275
- package/dist/react/index.d.ts +62 -275
- package/dist/react/index.js +151 -3268
- package/dist/react/index.js.map +1 -1
- package/dist/scan/index.cjs +1292 -345
- package/dist/scan/index.cjs.map +1 -1
- package/dist/scan/index.d.cts +305 -12
- package/dist/scan/index.d.ts +305 -12
- package/dist/scan/index.js +1283 -345
- package/dist/scan/index.js.map +1 -1
- package/package.json +12 -16
- package/dist/cloud/index.cjs +0 -682
- package/dist/cloud/index.cjs.map +0 -1
- package/dist/cloud/index.d.cts +0 -270
- package/dist/cloud/index.d.ts +0 -270
- package/dist/cloud/index.js +0 -645
- package/dist/cloud/index.js.map +0 -1
package/dist/scan/index.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)) {
|
|
@@ -178,7 +202,13 @@ function validateConfig(raw) {
|
|
|
178
202
|
fail(`conventions.features must be a string or false`);
|
|
179
203
|
}
|
|
180
204
|
if (c.pages !== void 0 && c.pages !== false && c.pages !== "auto") {
|
|
181
|
-
|
|
205
|
+
const p2 = c.pages;
|
|
206
|
+
const routesDir = typeof p2 === "object" && p2 !== null && !Array.isArray(p2) ? p2.routesDir : void 0;
|
|
207
|
+
if (typeof p2 !== "object" || p2 === null || Array.isArray(p2) || Object.keys(p2).some((k) => k !== "routesDir") || typeof routesDir !== "string" || routesDir.length === 0) {
|
|
208
|
+
fail(
|
|
209
|
+
`conventions.pages must be "auto", false, or { routesDir: string }`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
182
212
|
}
|
|
183
213
|
if (c.flows !== void 0 && c.flows !== false) {
|
|
184
214
|
assertStringArray(c.flows, "conventions.flows");
|
|
@@ -194,7 +224,9 @@ function validateConfig(raw) {
|
|
|
194
224
|
output: raw.output,
|
|
195
225
|
flows: raw.flows,
|
|
196
226
|
audit: raw.audit,
|
|
197
|
-
conventions: raw.conventions
|
|
227
|
+
conventions: raw.conventions,
|
|
228
|
+
statesManifest: raw.statesManifest,
|
|
229
|
+
coverageBaseline: raw.coverageBaseline
|
|
198
230
|
};
|
|
199
231
|
}
|
|
200
232
|
function parseConfig(json) {
|
|
@@ -283,7 +315,12 @@ var BASE_EXCLUDES = [
|
|
|
283
315
|
"**/dist/**",
|
|
284
316
|
"**/build/**",
|
|
285
317
|
"**/.next/**",
|
|
286
|
-
|
|
318
|
+
// Exclude the scanner's own generated siblings so a scan never re-ingests its
|
|
319
|
+
// output. Enumerated (not `*.gen.*`) so a hand-written file that merely contains
|
|
320
|
+
// a `.gen.` segment, e.g. `foo.gen.helpers.ts`, is still scanned.
|
|
321
|
+
"**/*.gen.ts",
|
|
322
|
+
"**/*.gen.data.ts",
|
|
323
|
+
"**/*.gen.locs.ts"
|
|
287
324
|
];
|
|
288
325
|
var TEST_EXCLUDES = [
|
|
289
326
|
"**/*.test.ts",
|
|
@@ -483,6 +520,201 @@ function unwrapTsExpression(expr) {
|
|
|
483
520
|
}
|
|
484
521
|
}
|
|
485
522
|
|
|
523
|
+
// src/shared/entities/types.ts
|
|
524
|
+
var ENTITY_KINDS = [
|
|
525
|
+
"route",
|
|
526
|
+
"page",
|
|
527
|
+
"feature",
|
|
528
|
+
"widget",
|
|
529
|
+
"region",
|
|
530
|
+
"element",
|
|
531
|
+
"primitive",
|
|
532
|
+
"flow"
|
|
533
|
+
];
|
|
534
|
+
var CORE_STATE_KINDS = [
|
|
535
|
+
"loading",
|
|
536
|
+
"empty",
|
|
537
|
+
"populated",
|
|
538
|
+
"error"
|
|
539
|
+
];
|
|
540
|
+
function isMetaKind(kind) {
|
|
541
|
+
return kind !== "route" && kind !== "flow";
|
|
542
|
+
}
|
|
543
|
+
function isMetaEntity(entity) {
|
|
544
|
+
return isMetaKind(entity.kind);
|
|
545
|
+
}
|
|
546
|
+
function entityKey(entity) {
|
|
547
|
+
return entity.kind === "route" ? entity.path : entity.id;
|
|
548
|
+
}
|
|
549
|
+
var UnknownEntityKindError = class extends Error {
|
|
550
|
+
kind;
|
|
551
|
+
constructor(kind) {
|
|
552
|
+
super(`Unknown entity kind: ${kind}`);
|
|
553
|
+
this.name = "UnknownEntityKindError";
|
|
554
|
+
this.kind = kind;
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
var KIND_SET = new Set(ENTITY_KINDS);
|
|
558
|
+
function assertEntityKind(kind) {
|
|
559
|
+
if (!KIND_SET.has(kind)) throw new UnknownEntityKindError(kind);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/shared/entities/registry.ts
|
|
563
|
+
function emptyStore() {
|
|
564
|
+
return {
|
|
565
|
+
route: /* @__PURE__ */ new Map(),
|
|
566
|
+
page: /* @__PURE__ */ new Map(),
|
|
567
|
+
feature: /* @__PURE__ */ new Map(),
|
|
568
|
+
widget: /* @__PURE__ */ new Map(),
|
|
569
|
+
region: /* @__PURE__ */ new Map(),
|
|
570
|
+
element: /* @__PURE__ */ new Map(),
|
|
571
|
+
primitive: /* @__PURE__ */ new Map(),
|
|
572
|
+
flow: /* @__PURE__ */ new Map()
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
function computeFlowIds(flows, targetId) {
|
|
576
|
+
const ids = [];
|
|
577
|
+
for (const flow of flows) {
|
|
578
|
+
if (flow.touches.includes(targetId)) ids.push(flow.id);
|
|
579
|
+
}
|
|
580
|
+
return ids;
|
|
581
|
+
}
|
|
582
|
+
function freezeEntity(entity, flows) {
|
|
583
|
+
if (!isMetaKind(entity.kind)) return entity;
|
|
584
|
+
const withMeta = entity;
|
|
585
|
+
if (withMeta.meta === void 0) return entity;
|
|
586
|
+
const computedFlows = Object.freeze(computeFlowIds(flows, withMeta.id));
|
|
587
|
+
const mergedMeta = { ...withMeta.meta, flows: computedFlows };
|
|
588
|
+
return { ...entity, meta: Object.freeze(mergedMeta) };
|
|
589
|
+
}
|
|
590
|
+
function createRegistry() {
|
|
591
|
+
const store = emptyStore();
|
|
592
|
+
let flowsCache = null;
|
|
593
|
+
const patternCache = /* @__PURE__ */ new Map();
|
|
594
|
+
const getFlows = () => {
|
|
595
|
+
if (flowsCache === null) flowsCache = Array.from(store.flow.values());
|
|
596
|
+
return flowsCache;
|
|
597
|
+
};
|
|
598
|
+
const add = (entity) => {
|
|
599
|
+
assertEntityKind(entity.kind);
|
|
600
|
+
const key = entityKey(entity);
|
|
601
|
+
store[entity.kind].set(key, entity);
|
|
602
|
+
flowsCache = null;
|
|
603
|
+
patternCache.delete(entity.kind);
|
|
604
|
+
};
|
|
605
|
+
const get = (kind, id) => {
|
|
606
|
+
assertEntityKind(kind);
|
|
607
|
+
const raw = store[kind].get(id);
|
|
608
|
+
if (raw === void 0) return void 0;
|
|
609
|
+
return freezeEntity(raw, getFlows());
|
|
610
|
+
};
|
|
611
|
+
const getPatternsForKind = (kind) => {
|
|
612
|
+
const cached = patternCache.get(kind);
|
|
613
|
+
if (cached !== void 0) return cached;
|
|
614
|
+
const patterns = [];
|
|
615
|
+
for (const [key, entity] of store[kind]) {
|
|
616
|
+
if (key.includes("*")) {
|
|
617
|
+
const segments = key.split("*");
|
|
618
|
+
patterns.push({
|
|
619
|
+
segments,
|
|
620
|
+
staticLength: segments.reduce((n, s) => n + s.length, 0),
|
|
621
|
+
entity
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
patternCache.set(
|
|
626
|
+
kind,
|
|
627
|
+
patterns
|
|
628
|
+
);
|
|
629
|
+
return patterns;
|
|
630
|
+
};
|
|
631
|
+
const matchesSegments = (segments, id) => {
|
|
632
|
+
const first = segments[0];
|
|
633
|
+
const last = segments[segments.length - 1];
|
|
634
|
+
if (!id.startsWith(first)) return false;
|
|
635
|
+
let pos = first.length;
|
|
636
|
+
for (let i = 1; i < segments.length - 1; i++) {
|
|
637
|
+
const idx = id.indexOf(segments[i], pos);
|
|
638
|
+
if (idx === -1) return false;
|
|
639
|
+
pos = idx + segments[i].length;
|
|
640
|
+
}
|
|
641
|
+
return id.endsWith(last) && id.length - last.length >= pos;
|
|
642
|
+
};
|
|
643
|
+
const matchPattern = (kind, id) => {
|
|
644
|
+
assertEntityKind(kind);
|
|
645
|
+
const patterns = getPatternsForKind(kind);
|
|
646
|
+
if (patterns.length === 0) return void 0;
|
|
647
|
+
let best;
|
|
648
|
+
for (const entry of patterns) {
|
|
649
|
+
if (matchesSegments(entry.segments, id) && (best === void 0 || entry.staticLength > best.staticLength)) {
|
|
650
|
+
best = entry;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (best === void 0) return void 0;
|
|
654
|
+
return freezeEntity(best.entity, getFlows());
|
|
655
|
+
};
|
|
656
|
+
const list = (kind) => {
|
|
657
|
+
assertEntityKind(kind);
|
|
658
|
+
const flows = getFlows();
|
|
659
|
+
return Array.from(
|
|
660
|
+
store[kind].values(),
|
|
661
|
+
(e) => freezeEntity(e, flows)
|
|
662
|
+
);
|
|
663
|
+
};
|
|
664
|
+
const allEntities = function* () {
|
|
665
|
+
for (const kind of Object.keys(store)) {
|
|
666
|
+
for (const entity of store[kind].values()) {
|
|
667
|
+
yield entity;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
const query = (predicate) => {
|
|
672
|
+
const flows = getFlows();
|
|
673
|
+
const result = [];
|
|
674
|
+
for (const entity of allEntities()) {
|
|
675
|
+
if (predicate(entity)) result.push(freezeEntity(entity, flows));
|
|
676
|
+
}
|
|
677
|
+
return result;
|
|
678
|
+
};
|
|
679
|
+
const byScope = (scope) => query(
|
|
680
|
+
(entity) => "scopes" in entity && Array.isArray(entity.scopes) && entity.scopes.includes(scope)
|
|
681
|
+
);
|
|
682
|
+
const touchedBy = (flowId) => {
|
|
683
|
+
const flow = store.flow.get(flowId);
|
|
684
|
+
if (flow === void 0) return [];
|
|
685
|
+
const ids = new Set(flow.touches);
|
|
686
|
+
return query((entity) => {
|
|
687
|
+
if (!isMetaEntity(entity)) return false;
|
|
688
|
+
return ids.has(entity.id);
|
|
689
|
+
});
|
|
690
|
+
};
|
|
691
|
+
const reports = /* @__PURE__ */ new Map();
|
|
692
|
+
const reportsCbs = /* @__PURE__ */ new Set();
|
|
693
|
+
const setReports = (kind, id, records) => {
|
|
694
|
+
reports.set(`${kind}:${id}`, records);
|
|
695
|
+
for (const cb of reportsCbs) cb();
|
|
696
|
+
};
|
|
697
|
+
const getReports = (kind, id) => reports.get(`${kind}:${id}`) ?? [];
|
|
698
|
+
const listReportKeys = () => Array.from(reports.keys());
|
|
699
|
+
const onReportsChange = (cb) => {
|
|
700
|
+
reportsCbs.add(cb);
|
|
701
|
+
return () => reportsCbs.delete(cb);
|
|
702
|
+
};
|
|
703
|
+
return {
|
|
704
|
+
add,
|
|
705
|
+
get,
|
|
706
|
+
matchPattern,
|
|
707
|
+
list,
|
|
708
|
+
query,
|
|
709
|
+
byScope,
|
|
710
|
+
touchedBy,
|
|
711
|
+
setReports,
|
|
712
|
+
getReports,
|
|
713
|
+
listReportKeys,
|
|
714
|
+
onReportsChange
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
486
718
|
// src/scanner/scan/extract-uidex-export.ts
|
|
487
719
|
var KIND_DISCRIMINATORS = [
|
|
488
720
|
"page",
|
|
@@ -500,20 +732,35 @@ var ALLOWED_FIELDS = {
|
|
|
500
732
|
"features",
|
|
501
733
|
"widgets",
|
|
502
734
|
"acceptance",
|
|
735
|
+
"states",
|
|
736
|
+
"capture",
|
|
737
|
+
"waivers",
|
|
503
738
|
"description"
|
|
504
739
|
]),
|
|
740
|
+
// `capture` / `waivers` are route-scoped, and only a page maps to a route — so
|
|
741
|
+
// they are page-only (the gates read them off the page). A feature/widget that
|
|
742
|
+
// sets them gets an `uidex-export-unknown-field` error rather than a silent no-op.
|
|
505
743
|
feature: /* @__PURE__ */ new Set([
|
|
506
744
|
"feature",
|
|
507
745
|
"name",
|
|
508
746
|
"features",
|
|
509
747
|
"acceptance",
|
|
748
|
+
"states",
|
|
510
749
|
"description"
|
|
511
750
|
]),
|
|
512
751
|
primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
|
|
513
|
-
widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "description"]),
|
|
752
|
+
widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "states", "description"]),
|
|
514
753
|
region: /* @__PURE__ */ new Set(["region", "name", "description"]),
|
|
515
754
|
flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
|
|
516
755
|
};
|
|
756
|
+
var STATE_FIELDS = /* @__PURE__ */ new Set([
|
|
757
|
+
"name",
|
|
758
|
+
"kind",
|
|
759
|
+
"acceptance",
|
|
760
|
+
"description"
|
|
761
|
+
]);
|
|
762
|
+
var CORE_STATE_KINDS2 = new Set(CORE_STATE_KINDS);
|
|
763
|
+
var STATE_KINDS = /* @__PURE__ */ new Set([...CORE_STATE_KINDS, "variant"]);
|
|
517
764
|
var FALSEABLE = /* @__PURE__ */ new Set([
|
|
518
765
|
"page",
|
|
519
766
|
"feature",
|
|
@@ -903,6 +1150,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
903
1150
|
}
|
|
904
1151
|
const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
905
1152
|
const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
1153
|
+
const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey) : void 0;
|
|
1154
|
+
const capture = kind === "page" ? readBooleanField(byKey, "capture") : void 0;
|
|
1155
|
+
const waivers = kind === "page" ? readWaiversField(byKey) : void 0;
|
|
906
1156
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
907
1157
|
const metadata = {
|
|
908
1158
|
source: "ts-export",
|
|
@@ -925,6 +1175,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
925
1175
|
metadata.widgets = widgetsField.values;
|
|
926
1176
|
metadata.widgetSpans = widgetsField.spans;
|
|
927
1177
|
}
|
|
1178
|
+
if (states?.length) metadata.states = states;
|
|
1179
|
+
if (capture !== void 0) metadata.capture = capture;
|
|
1180
|
+
if (waivers && Object.keys(waivers).length > 0) metadata.waivers = waivers;
|
|
928
1181
|
if (notFlow) metadata.notFlow = true;
|
|
929
1182
|
if (typeof id === "string" && idValue.kind === "string") {
|
|
930
1183
|
metadata.idSpan = idValue.span;
|
|
@@ -961,6 +1214,48 @@ function readIdField(value, kind, fieldName) {
|
|
|
961
1214
|
value.pos
|
|
962
1215
|
);
|
|
963
1216
|
}
|
|
1217
|
+
function readWaiversField(byKey) {
|
|
1218
|
+
const entry = byKey.get("waivers");
|
|
1219
|
+
if (!entry) return void 0;
|
|
1220
|
+
if (entry.value.kind !== "object") {
|
|
1221
|
+
throw new ExtractError(
|
|
1222
|
+
"uidex-export-invalid-field",
|
|
1223
|
+
"`waivers` must be an object mapping a core kind to a reason.",
|
|
1224
|
+
entry.value.pos
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
const waivers = {};
|
|
1228
|
+
for (const e of entry.value.entries) {
|
|
1229
|
+
if (!CORE_STATE_KINDS2.has(e.key)) {
|
|
1230
|
+
throw new ExtractError(
|
|
1231
|
+
"uidex-export-invalid-field",
|
|
1232
|
+
`waiver key "${e.key}" is not a core kind; only ${[...CORE_STATE_KINDS2].join(", ")} participate in the matrix.`,
|
|
1233
|
+
e.keyPos
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
if (e.value.kind !== "string" || e.value.value.length === 0) {
|
|
1237
|
+
throw new ExtractError(
|
|
1238
|
+
"uidex-export-invalid-field",
|
|
1239
|
+
`waiver "${e.key}" needs a non-empty reason (why the route cannot render this kind).`,
|
|
1240
|
+
e.value.pos
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
waivers[e.key] = e.value.value;
|
|
1244
|
+
}
|
|
1245
|
+
return waivers;
|
|
1246
|
+
}
|
|
1247
|
+
function readBooleanField(byKey, name) {
|
|
1248
|
+
const entry = byKey.get(name);
|
|
1249
|
+
if (!entry) return void 0;
|
|
1250
|
+
if (entry.value.kind !== "boolean") {
|
|
1251
|
+
throw new ExtractError(
|
|
1252
|
+
"uidex-export-invalid-field",
|
|
1253
|
+
`\`${name}\` must be a boolean.`,
|
|
1254
|
+
entry.value.pos
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
return entry.value.value;
|
|
1258
|
+
}
|
|
964
1259
|
function readStringField(byKey, name) {
|
|
965
1260
|
const entry = byKey.get(name);
|
|
966
1261
|
if (!entry) return void 0;
|
|
@@ -998,6 +1293,86 @@ function readStringArrayField(byKey, name) {
|
|
|
998
1293
|
}
|
|
999
1294
|
return { values, spans };
|
|
1000
1295
|
}
|
|
1296
|
+
function readStatesField(byKey) {
|
|
1297
|
+
const entry = byKey.get("states");
|
|
1298
|
+
if (!entry) return void 0;
|
|
1299
|
+
if (entry.value.kind !== "array") {
|
|
1300
|
+
throw new ExtractError(
|
|
1301
|
+
"uidex-export-invalid-field",
|
|
1302
|
+
"`states` must be an array of state names or `{ name, acceptance?, description? }` objects.",
|
|
1303
|
+
entry.value.pos
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
const states = [];
|
|
1307
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1308
|
+
for (const item of entry.value.items) {
|
|
1309
|
+
let state;
|
|
1310
|
+
if (item.kind === "string") {
|
|
1311
|
+
state = { name: item.value, nameSpan: item.span };
|
|
1312
|
+
} else if (item.kind === "object") {
|
|
1313
|
+
const fieldByKey = /* @__PURE__ */ new Map();
|
|
1314
|
+
for (const e of item.entries) fieldByKey.set(e.key, e);
|
|
1315
|
+
for (const e of item.entries) {
|
|
1316
|
+
if (!STATE_FIELDS.has(e.key)) {
|
|
1317
|
+
throw new ExtractError(
|
|
1318
|
+
"uidex-export-invalid-field",
|
|
1319
|
+
`Unknown field "${e.key}" in a \`states\` entry. Allowed: ${Array.from(
|
|
1320
|
+
STATE_FIELDS
|
|
1321
|
+
).sort().join(", ")}.`,
|
|
1322
|
+
e.value.pos
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
const nameEntry = fieldByKey.get("name");
|
|
1327
|
+
if (!nameEntry || nameEntry.value.kind !== "string") {
|
|
1328
|
+
throw new ExtractError(
|
|
1329
|
+
"uidex-export-invalid-field",
|
|
1330
|
+
"Each object `states` entry must have a string `name`.",
|
|
1331
|
+
item.pos
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
state = { name: nameEntry.value.value, nameSpan: nameEntry.value.span };
|
|
1335
|
+
const kind = readStringField(fieldByKey, "kind");
|
|
1336
|
+
if (kind !== void 0) {
|
|
1337
|
+
if (!STATE_KINDS.has(kind)) {
|
|
1338
|
+
throw new ExtractError(
|
|
1339
|
+
"uidex-export-invalid-field",
|
|
1340
|
+
`state \`kind\` must be one of ${[...STATE_KINDS].join(", ")} (got "${kind}").`,
|
|
1341
|
+
fieldByKey.get("kind").value.pos
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
state.kind = kind;
|
|
1345
|
+
}
|
|
1346
|
+
const acceptance = readStringArrayField(fieldByKey, "acceptance")?.values;
|
|
1347
|
+
if (acceptance) state.acceptance = acceptance;
|
|
1348
|
+
const description = readStringField(fieldByKey, "description");
|
|
1349
|
+
if (description) state.description = description;
|
|
1350
|
+
} else {
|
|
1351
|
+
throw new ExtractError(
|
|
1352
|
+
"uidex-export-invalid-field",
|
|
1353
|
+
"`states` items must be strings or `{ name, ... }` objects.",
|
|
1354
|
+
item.pos
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
if (state.name.length === 0) {
|
|
1358
|
+
throw new ExtractError(
|
|
1359
|
+
"uidex-export-invalid-field",
|
|
1360
|
+
"A `states` entry has an empty `name`.",
|
|
1361
|
+
item.pos
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
if (seen.has(state.name)) {
|
|
1365
|
+
throw new ExtractError(
|
|
1366
|
+
"uidex-export-duplicate-state",
|
|
1367
|
+
`Duplicate state "${state.name}" in \`states\`; each state name must be unique within the entity.`,
|
|
1368
|
+
item.pos
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
seen.add(state.name);
|
|
1372
|
+
states.push(state);
|
|
1373
|
+
}
|
|
1374
|
+
return states;
|
|
1375
|
+
}
|
|
1001
1376
|
function posAt(content, offset, p2) {
|
|
1002
1377
|
const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
|
|
1003
1378
|
return { offset, line: p2.lineAt(offset), column: offset - lineStart + 1 };
|
|
@@ -1489,231 +1864,55 @@ function collectImportFacts(parsed) {
|
|
|
1489
1864
|
for (const spec of stmt.specifiers ?? []) {
|
|
1490
1865
|
const local = spec.local;
|
|
1491
1866
|
if (local && local.type === "Identifier") {
|
|
1492
|
-
names.push(String(local.name));
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
} else if ((stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportAllDeclaration") && stmt.source) {
|
|
1496
|
-
source = stmt.source;
|
|
1497
|
-
isTypeOnly = stmt.exportKind === "type";
|
|
1498
|
-
} else {
|
|
1499
|
-
continue;
|
|
1500
|
-
}
|
|
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);
|
|
1677
|
-
});
|
|
1678
|
-
};
|
|
1679
|
-
const reports = /* @__PURE__ */ new Map();
|
|
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
|
-
};
|
|
1867
|
+
names.push(String(local.name));
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
} else if ((stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportAllDeclaration") && stmt.source) {
|
|
1871
|
+
source = stmt.source;
|
|
1872
|
+
isTypeOnly = stmt.exportKind === "type";
|
|
1873
|
+
} else {
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
if (!source || source.type !== "Literal") continue;
|
|
1877
|
+
if (typeof source.value !== "string") continue;
|
|
1878
|
+
out2.push({
|
|
1879
|
+
specifier: source.value,
|
|
1880
|
+
line: parsed.lineAt(stmt.start),
|
|
1881
|
+
span: { start: stmt.start, end: stmt.end },
|
|
1882
|
+
isTypeOnly,
|
|
1883
|
+
names
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
return out2;
|
|
1704
1887
|
}
|
|
1705
1888
|
|
|
1889
|
+
// src/scanner/scan/resolve.ts
|
|
1890
|
+
var path4 = __toESM(require("path"), 1);
|
|
1891
|
+
|
|
1706
1892
|
// src/scanner/scan/routes.ts
|
|
1707
1893
|
var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1708
1894
|
var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1709
1895
|
var ROUTE_BASENAME = /^route\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1710
|
-
function detectRoutes(files) {
|
|
1896
|
+
function detectRoutes(files, options = {}) {
|
|
1711
1897
|
const out2 = [];
|
|
1712
1898
|
const seen = /* @__PURE__ */ new Set();
|
|
1899
|
+
const routesDir = options.routesDir ? options.routesDir.replace(/^\.?\/+/, "").replace(/\/+$/, "") : null;
|
|
1900
|
+
const routesDirParts = routesDir ? routesDir.split("/") : null;
|
|
1713
1901
|
for (const f of files) {
|
|
1714
1902
|
const rel = f.displayPath;
|
|
1715
1903
|
const parts = rel.split("/");
|
|
1716
1904
|
const base = parts[parts.length - 1];
|
|
1905
|
+
if (routesDirParts) {
|
|
1906
|
+
const end = dirEndIndex(parts, routesDirParts);
|
|
1907
|
+
if (end === -1 || end > parts.length - 1) continue;
|
|
1908
|
+
if (!PAGES_ROUTER_BASENAME.test(base)) continue;
|
|
1909
|
+
const routePath = routesStylePath(
|
|
1910
|
+
parts.slice(end, parts.length - 1),
|
|
1911
|
+
base
|
|
1912
|
+
);
|
|
1913
|
+
if (routePath !== null) push(out2, seen, routePath, f.displayPath);
|
|
1914
|
+
continue;
|
|
1915
|
+
}
|
|
1717
1916
|
const appIdx = parts.indexOf("app");
|
|
1718
1917
|
if (appIdx !== -1 && PAGE_BASENAME.test(base)) {
|
|
1719
1918
|
const routeSegments = parts.slice(appIdx + 1, parts.length - 1);
|
|
@@ -1740,15 +1939,11 @@ function detectRoutes(files) {
|
|
|
1740
1939
|
}
|
|
1741
1940
|
const routesIdx = parts.indexOf("routes");
|
|
1742
1941
|
if (routesIdx !== -1 && PAGES_ROUTER_BASENAME.test(base)) {
|
|
1743
|
-
const
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
base.replace(/\.[^.]+$/, "")
|
|
1749
|
-
].filter((s) => s !== "index" && s !== "__root");
|
|
1750
|
-
const routePath = formatNextAppPath(normalized);
|
|
1751
|
-
push(out2, seen, routePath, f.displayPath);
|
|
1942
|
+
const routePath = routesStylePath(
|
|
1943
|
+
parts.slice(routesIdx + 1, parts.length - 1),
|
|
1944
|
+
base
|
|
1945
|
+
);
|
|
1946
|
+
if (routePath !== null) push(out2, seen, routePath, f.displayPath);
|
|
1752
1947
|
continue;
|
|
1753
1948
|
}
|
|
1754
1949
|
}
|
|
@@ -1764,6 +1959,63 @@ function formatNextAppPath(segments) {
|
|
|
1764
1959
|
if (kept.length === 0) return "/";
|
|
1765
1960
|
return "/" + kept.join("/");
|
|
1766
1961
|
}
|
|
1962
|
+
function dirEndIndex(parts, dirParts) {
|
|
1963
|
+
if (dirParts.length === 0) return 0;
|
|
1964
|
+
for (let i = 0; i + dirParts.length <= parts.length; i++) {
|
|
1965
|
+
let match = true;
|
|
1966
|
+
for (let j = 0; j < dirParts.length; j++) {
|
|
1967
|
+
if (parts[i + j] !== dirParts[j]) {
|
|
1968
|
+
match = false;
|
|
1969
|
+
break;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
if (match) return i + dirParts.length;
|
|
1973
|
+
}
|
|
1974
|
+
return -1;
|
|
1975
|
+
}
|
|
1976
|
+
function routesStylePath(dirSegs, base) {
|
|
1977
|
+
const fileSegs = splitFlatSegments(base.replace(/\.[^.]+$/, ""));
|
|
1978
|
+
const leaf = fileSegs[fileSegs.length - 1];
|
|
1979
|
+
if (leaf.startsWith("_")) return null;
|
|
1980
|
+
const rawSegs = [...dirSegs, ...fileSegs];
|
|
1981
|
+
if (rawSegs.some((s) => s.startsWith("-"))) return null;
|
|
1982
|
+
if (rawSegs[0] === "api") return null;
|
|
1983
|
+
return formatTanStackPath(rawSegs);
|
|
1984
|
+
}
|
|
1985
|
+
function splitFlatSegments(name) {
|
|
1986
|
+
const segs = [];
|
|
1987
|
+
let cur = "";
|
|
1988
|
+
let depth = 0;
|
|
1989
|
+
for (const ch of name) {
|
|
1990
|
+
if (ch === "[") depth++;
|
|
1991
|
+
else if (ch === "]") depth = Math.max(0, depth - 1);
|
|
1992
|
+
if (ch === "." && depth === 0) {
|
|
1993
|
+
segs.push(cur);
|
|
1994
|
+
cur = "";
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1997
|
+
cur += ch;
|
|
1998
|
+
}
|
|
1999
|
+
segs.push(cur);
|
|
2000
|
+
return segs;
|
|
2001
|
+
}
|
|
2002
|
+
function formatTanStackPath(segments) {
|
|
2003
|
+
const kept = [];
|
|
2004
|
+
for (const seg of segments) {
|
|
2005
|
+
if (!seg) continue;
|
|
2006
|
+
if (seg === "index" || seg === "route" || seg === "__root") continue;
|
|
2007
|
+
if (seg.startsWith("_")) continue;
|
|
2008
|
+
if (seg.startsWith("(") && seg.endsWith(")")) continue;
|
|
2009
|
+
kept.push(normalizeTanStackSegment(seg.replace(/_$/, "")));
|
|
2010
|
+
}
|
|
2011
|
+
if (kept.length === 0) return "/";
|
|
2012
|
+
return "/" + kept.join("/");
|
|
2013
|
+
}
|
|
2014
|
+
function normalizeTanStackSegment(seg) {
|
|
2015
|
+
if (seg === "$") return "[...splat]";
|
|
2016
|
+
if (seg.startsWith("$")) return `[${seg.slice(1)}]`;
|
|
2017
|
+
return seg.replace(/\[([^\]]*)\]/g, "$1");
|
|
2018
|
+
}
|
|
1767
2019
|
function pathToId(routePath) {
|
|
1768
2020
|
if (routePath === "/") return "root";
|
|
1769
2021
|
return routePath.replace(/^\/+/, "").replace(/\[\.{3}([^\]]+)\]/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -1802,6 +2054,18 @@ function buildMetaFromExport(exp) {
|
|
|
1802
2054
|
if (exp.acceptance?.length) meta.acceptance = exp.acceptance;
|
|
1803
2055
|
if (exp.features?.length) meta.features = exp.features;
|
|
1804
2056
|
if (exp.widgets?.length) meta.widgets = exp.widgets;
|
|
2057
|
+
if (exp.states?.length) {
|
|
2058
|
+
meta.states = exp.states.map((s) => ({
|
|
2059
|
+
name: s.name,
|
|
2060
|
+
...s.kind ? { kind: s.kind } : {},
|
|
2061
|
+
...s.acceptance?.length ? { acceptance: s.acceptance } : {},
|
|
2062
|
+
...s.description ? { description: s.description } : {}
|
|
2063
|
+
}));
|
|
2064
|
+
}
|
|
2065
|
+
if (exp.capture === false) meta.capture = false;
|
|
2066
|
+
if (exp.waivers && Object.keys(exp.waivers).length > 0) {
|
|
2067
|
+
meta.waivers = exp.waivers;
|
|
2068
|
+
}
|
|
1805
2069
|
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
1806
2070
|
}
|
|
1807
2071
|
function resolve2(ctx) {
|
|
@@ -1847,9 +2111,12 @@ function resolve2(ctx) {
|
|
|
1847
2111
|
function metaWithComposes(kind, id, base) {
|
|
1848
2112
|
const composes = directChildren.get(`${kind}:${id}`);
|
|
1849
2113
|
if (!composes || composes.length === 0) return base;
|
|
1850
|
-
return { ...base
|
|
2114
|
+
return { ...base, composes };
|
|
1851
2115
|
}
|
|
1852
|
-
const routes = conventions.pages ===
|
|
2116
|
+
const routes = conventions.pages === false ? [] : detectRoutes(
|
|
2117
|
+
ctx.extracted.map((e) => e.file),
|
|
2118
|
+
conventions.pages === "auto" ? {} : { routesDir: conventions.pages.routesDir }
|
|
2119
|
+
);
|
|
1853
2120
|
const handledPageFiles = /* @__PURE__ */ new Set();
|
|
1854
2121
|
for (const route of routes) {
|
|
1855
2122
|
const routeDir = path4.posix.dirname(route.file);
|
|
@@ -2221,6 +2488,297 @@ function dedupe(arr) {
|
|
|
2221
2488
|
|
|
2222
2489
|
// src/scanner/scan/audit.ts
|
|
2223
2490
|
var path5 = __toESM(require("path"), 1);
|
|
2491
|
+
|
|
2492
|
+
// src/scanner/scan/page-coverage.ts
|
|
2493
|
+
function compileRoute(pattern) {
|
|
2494
|
+
const segs = pattern.split("/").filter(Boolean);
|
|
2495
|
+
let specificity = 0;
|
|
2496
|
+
const optionalTail = segs.length > 0 && /^\[\[\.{3}.+\]\]$/.test(segs[segs.length - 1]);
|
|
2497
|
+
const effective = optionalTail ? segs.slice(0, -1) : segs;
|
|
2498
|
+
const parts = effective.map((seg) => {
|
|
2499
|
+
const catchAll = /^\[\.{3}.+\]$/.test(seg);
|
|
2500
|
+
const dynamic = /^\[.+\]$/.test(seg);
|
|
2501
|
+
if (catchAll) return "(?:[^/]+)(?:/[^/]+)*";
|
|
2502
|
+
if (dynamic) return "[^/]+";
|
|
2503
|
+
specificity++;
|
|
2504
|
+
return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2505
|
+
});
|
|
2506
|
+
let body = parts.length === 0 ? "" : "/" + parts.join("/");
|
|
2507
|
+
if (optionalTail) body += "(?:/[^/]+)*";
|
|
2508
|
+
if (body === "") body = "/";
|
|
2509
|
+
const re = new RegExp(`^${body}/?$`);
|
|
2510
|
+
return { test: (url) => re.test(url), specificity };
|
|
2511
|
+
}
|
|
2512
|
+
function matchUrlToRoute(url, routePaths) {
|
|
2513
|
+
const path12 = stripQuery(url);
|
|
2514
|
+
let best = null;
|
|
2515
|
+
for (const rp of routePaths) {
|
|
2516
|
+
const { test, specificity } = compileRoute(rp);
|
|
2517
|
+
if (!test(path12)) continue;
|
|
2518
|
+
if (!best || specificity > best.specificity)
|
|
2519
|
+
best = { path: rp, specificity };
|
|
2520
|
+
}
|
|
2521
|
+
return best?.path ?? null;
|
|
2522
|
+
}
|
|
2523
|
+
function stripQuery(url) {
|
|
2524
|
+
const q = url.indexOf("?");
|
|
2525
|
+
const h = url.indexOf("#");
|
|
2526
|
+
let end = url.length;
|
|
2527
|
+
if (q !== -1) end = Math.min(end, q);
|
|
2528
|
+
if (h !== -1) end = Math.min(end, h);
|
|
2529
|
+
return url.slice(0, end);
|
|
2530
|
+
}
|
|
2531
|
+
function routeForCapture(c, routes, routePaths) {
|
|
2532
|
+
if (c.route && routePaths.includes(c.route)) return c.route;
|
|
2533
|
+
if (c.url) {
|
|
2534
|
+
const m = matchUrlToRoute(c.url, routePaths);
|
|
2535
|
+
if (m) return m;
|
|
2536
|
+
}
|
|
2537
|
+
if (c.kind === "page") {
|
|
2538
|
+
const r = routes.find((r2) => r2.page === c.entity);
|
|
2539
|
+
if (r) return r.path;
|
|
2540
|
+
}
|
|
2541
|
+
return null;
|
|
2542
|
+
}
|
|
2543
|
+
function checkPageCoverage(registry, manifest, baseline) {
|
|
2544
|
+
const routes = registry.list("route");
|
|
2545
|
+
if (routes.length === 0) return [];
|
|
2546
|
+
const routePaths = routes.map((r) => r.path);
|
|
2547
|
+
const captured = manifest.captured ?? [];
|
|
2548
|
+
const optedOut = /* @__PURE__ */ new Set();
|
|
2549
|
+
for (const r of routes) {
|
|
2550
|
+
const page = registry.get("page", r.page);
|
|
2551
|
+
if (page?.meta?.capture === false) optedOut.add(r.path);
|
|
2552
|
+
}
|
|
2553
|
+
const covered = /* @__PURE__ */ new Set();
|
|
2554
|
+
for (const c of captured) {
|
|
2555
|
+
const route = routeForCapture(c, routes, routePaths);
|
|
2556
|
+
if (route) covered.add(route);
|
|
2557
|
+
}
|
|
2558
|
+
const baselineSet = new Set(baseline?.uncapturedPages ?? []);
|
|
2559
|
+
const diagnostics = [];
|
|
2560
|
+
for (const r of routes) {
|
|
2561
|
+
if (covered.has(r.path) || optedOut.has(r.path)) continue;
|
|
2562
|
+
const loc = registry.get("page", r.page)?.loc;
|
|
2563
|
+
if (baselineSet.has(r.path)) {
|
|
2564
|
+
diagnostics.push({
|
|
2565
|
+
code: "page-backlog",
|
|
2566
|
+
severity: "info",
|
|
2567
|
+
message: `route "${r.path}" has no visual-states capture (acknowledged backlog)`,
|
|
2568
|
+
file: loc?.file,
|
|
2569
|
+
line: loc?.line,
|
|
2570
|
+
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 }\`.`
|
|
2571
|
+
});
|
|
2572
|
+
continue;
|
|
2573
|
+
}
|
|
2574
|
+
diagnostics.push({
|
|
2575
|
+
code: "page-no-capture",
|
|
2576
|
+
severity: "error",
|
|
2577
|
+
message: `route "${r.path}" has no visual-states capture and is not accounted for`,
|
|
2578
|
+
file: loc?.file,
|
|
2579
|
+
line: loc?.line,
|
|
2580
|
+
entity: { kind: "route", id: r.path },
|
|
2581
|
+
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).`
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
for (const b of baselineSet) {
|
|
2585
|
+
if (!routePaths.includes(b)) {
|
|
2586
|
+
diagnostics.push({
|
|
2587
|
+
code: "stale-page-baseline",
|
|
2588
|
+
severity: "warning",
|
|
2589
|
+
message: `coverage baseline lists "${b}", which is not a route in the registry`,
|
|
2590
|
+
hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
|
|
2591
|
+
});
|
|
2592
|
+
} else if (covered.has(b) || optedOut.has(b)) {
|
|
2593
|
+
diagnostics.push({
|
|
2594
|
+
code: "page-captured",
|
|
2595
|
+
severity: "warning",
|
|
2596
|
+
message: `route "${b}" is now captured but is still in the coverage baseline`,
|
|
2597
|
+
hint: `Prune it with \`uidex scan --update-baseline\` so the backlog reflects reality.`
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
return diagnostics;
|
|
2602
|
+
}
|
|
2603
|
+
function computePageBaseline(registry, manifest) {
|
|
2604
|
+
const uncaptured = [];
|
|
2605
|
+
for (const d of checkPageCoverage(registry, manifest, {
|
|
2606
|
+
uncapturedPages: []
|
|
2607
|
+
})) {
|
|
2608
|
+
if (d.code === "page-no-capture" && d.entity) uncaptured.push(d.entity.id);
|
|
2609
|
+
}
|
|
2610
|
+
return { uncapturedPages: uncaptured.sort() };
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
// src/scanner/scan/state-coverage.ts
|
|
2614
|
+
var STATE_KINDS2 = ["page", "feature", "widget"];
|
|
2615
|
+
function declaredStateEntities(registry) {
|
|
2616
|
+
const out2 = [];
|
|
2617
|
+
for (const kind of STATE_KINDS2) {
|
|
2618
|
+
for (const e of registry.list(kind)) {
|
|
2619
|
+
const states = e.meta?.states;
|
|
2620
|
+
if (!states || states.length === 0) continue;
|
|
2621
|
+
out2.push({
|
|
2622
|
+
kind,
|
|
2623
|
+
id: e.id,
|
|
2624
|
+
states: states.map((s) => s.name),
|
|
2625
|
+
loc: e.loc
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
return out2;
|
|
2630
|
+
}
|
|
2631
|
+
function captureMatchesEntity(cap, ent) {
|
|
2632
|
+
if (cap.entity !== ent.id) return false;
|
|
2633
|
+
return cap.kind === void 0 || cap.kind === ent.kind;
|
|
2634
|
+
}
|
|
2635
|
+
function checkStateCoverage(registry, manifest) {
|
|
2636
|
+
const diagnostics = [];
|
|
2637
|
+
const declared = declaredStateEntities(registry);
|
|
2638
|
+
const captured = manifest.captured ?? [];
|
|
2639
|
+
for (const ent of declared) {
|
|
2640
|
+
for (const state of ent.states) {
|
|
2641
|
+
const hit = captured.some(
|
|
2642
|
+
(c) => captureMatchesEntity(c, ent) && c.state === state
|
|
2643
|
+
);
|
|
2644
|
+
if (hit) continue;
|
|
2645
|
+
diagnostics.push({
|
|
2646
|
+
code: "missing-state-capture",
|
|
2647
|
+
severity: "warning",
|
|
2648
|
+
message: `${ent.kind} "${ent.id}" declares state "${state}" but no capture produced it`,
|
|
2649
|
+
file: ent.loc?.file,
|
|
2650
|
+
line: ent.loc?.line,
|
|
2651
|
+
entity: { kind: ent.kind, id: ent.id },
|
|
2652
|
+
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.`
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
for (const cap of captured) {
|
|
2657
|
+
const candidates = declared.filter((ent2) => captureMatchesEntity(cap, ent2));
|
|
2658
|
+
if (candidates.length === 0) continue;
|
|
2659
|
+
const declaredSomewhere = candidates.some(
|
|
2660
|
+
(ent2) => ent2.states.includes(cap.state)
|
|
2661
|
+
);
|
|
2662
|
+
if (declaredSomewhere) continue;
|
|
2663
|
+
const ent = candidates[0];
|
|
2664
|
+
diagnostics.push({
|
|
2665
|
+
code: "orphan-state-capture",
|
|
2666
|
+
severity: "warning",
|
|
2667
|
+
message: `Capture produced state "${cap.state}" for ${ent.kind} "${ent.id}", which is not in its declared states [${ent.states.join(", ")}]`,
|
|
2668
|
+
file: ent.loc?.file,
|
|
2669
|
+
line: ent.loc?.line,
|
|
2670
|
+
entity: { kind: ent.kind, id: ent.id },
|
|
2671
|
+
hint: `Declare "${cap.state}" in \`export const uidex = { states: [...] }\` for "${ent.id}", or rename the capture to a declared state.`
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
2674
|
+
return diagnostics;
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
// src/scanner/scan/state-matrix.ts
|
|
2678
|
+
var CORE_KINDS = CORE_STATE_KINDS;
|
|
2679
|
+
var STATE_ENTITY_KINDS = ["page", "feature", "widget"];
|
|
2680
|
+
function resolveMatrixKind(registry, c) {
|
|
2681
|
+
if (c.stateKind) return c.stateKind;
|
|
2682
|
+
for (const kind of STATE_ENTITY_KINDS) {
|
|
2683
|
+
const declared = registry.get(kind, c.entity)?.meta?.states?.find((s) => s.name === c.state)?.kind;
|
|
2684
|
+
if (declared) return declared;
|
|
2685
|
+
}
|
|
2686
|
+
return CORE_KINDS.includes(c.state) ? c.state : "variant";
|
|
2687
|
+
}
|
|
2688
|
+
function coreKindsByRoute(registry, manifest) {
|
|
2689
|
+
const routes = registry.list("route");
|
|
2690
|
+
const routePaths = routes.map((r) => r.path);
|
|
2691
|
+
const byRoute = /* @__PURE__ */ new Map();
|
|
2692
|
+
for (const c of manifest.captured ?? []) {
|
|
2693
|
+
const route = routeForCapture(c, routes, routePaths);
|
|
2694
|
+
if (!route) continue;
|
|
2695
|
+
if (!byRoute.has(route)) byRoute.set(route, /* @__PURE__ */ new Set());
|
|
2696
|
+
const kind = resolveMatrixKind(registry, c);
|
|
2697
|
+
if (CORE_KINDS.includes(kind)) byRoute.get(route).add(kind);
|
|
2698
|
+
}
|
|
2699
|
+
return byRoute;
|
|
2700
|
+
}
|
|
2701
|
+
function pageWaivers(registry, routePage) {
|
|
2702
|
+
return registry.get("page", routePage)?.meta?.waivers ?? {};
|
|
2703
|
+
}
|
|
2704
|
+
function isOptedOut(registry, page) {
|
|
2705
|
+
return page ? registry.get("page", page)?.meta?.capture === false : false;
|
|
2706
|
+
}
|
|
2707
|
+
function checkStateMatrix(registry, manifest, baseline) {
|
|
2708
|
+
const routes = registry.list("route");
|
|
2709
|
+
if (routes.length === 0) return [];
|
|
2710
|
+
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
2711
|
+
const captured = coreKindsByRoute(registry, manifest);
|
|
2712
|
+
const baselined = baseline?.missingKinds ?? {};
|
|
2713
|
+
const diagnostics = [];
|
|
2714
|
+
for (const [route, kinds] of captured) {
|
|
2715
|
+
const page = pageOf.get(route);
|
|
2716
|
+
if (isOptedOut(registry, page)) continue;
|
|
2717
|
+
const waivers = page ? pageWaivers(registry, page) : {};
|
|
2718
|
+
const loc = page ? registry.get("page", page)?.loc : void 0;
|
|
2719
|
+
const baselinedKinds = new Set(baselined[route] ?? []);
|
|
2720
|
+
for (const kind of CORE_KINDS) {
|
|
2721
|
+
if (kinds.has(kind)) continue;
|
|
2722
|
+
if (waivers[kind]) continue;
|
|
2723
|
+
if (baselinedKinds.has(kind)) {
|
|
2724
|
+
diagnostics.push({
|
|
2725
|
+
code: "matrix-backlog",
|
|
2726
|
+
severity: "info",
|
|
2727
|
+
message: `route "${route}" does not render core kind "${kind}" (acknowledged MISSING_KINDS backlog)`,
|
|
2728
|
+
file: loc?.file,
|
|
2729
|
+
line: loc?.line,
|
|
2730
|
+
hint: `Capture the "${kind}" state for "${route}", waive it (\`waivers: { ${kind}: "why it can't" }\` on the page), or leave it in the baseline.`
|
|
2731
|
+
});
|
|
2732
|
+
continue;
|
|
2733
|
+
}
|
|
2734
|
+
diagnostics.push({
|
|
2735
|
+
code: "core-kind",
|
|
2736
|
+
severity: "error",
|
|
2737
|
+
message: `route "${route}" is captured but never renders core kind "${kind}"`,
|
|
2738
|
+
file: loc?.file,
|
|
2739
|
+
line: loc?.line,
|
|
2740
|
+
entity: { kind: "route", id: route },
|
|
2741
|
+
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).`
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
for (const [route, kinds] of captured) {
|
|
2746
|
+
const page = pageOf.get(route);
|
|
2747
|
+
if (!page || isOptedOut(registry, page)) continue;
|
|
2748
|
+
const waivers = pageWaivers(registry, page);
|
|
2749
|
+
const loc = registry.get("page", page)?.loc;
|
|
2750
|
+
for (const kind of Object.keys(waivers)) {
|
|
2751
|
+
if (kinds.has(kind)) {
|
|
2752
|
+
diagnostics.push({
|
|
2753
|
+
code: "stale-waiver",
|
|
2754
|
+
severity: "warning",
|
|
2755
|
+
message: `page "${page}" waives core kind "${kind}", but route "${route}" does render it`,
|
|
2756
|
+
file: loc?.file,
|
|
2757
|
+
line: loc?.line,
|
|
2758
|
+
entity: { kind: "page", id: page },
|
|
2759
|
+
hint: `Remove the "${kind}" waiver \u2014 the route renders that kind, so the waiver's reason is stale.`
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
return diagnostics;
|
|
2765
|
+
}
|
|
2766
|
+
function computeMissingKinds(registry, manifest) {
|
|
2767
|
+
const routes = registry.list("route");
|
|
2768
|
+
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
2769
|
+
const captured = coreKindsByRoute(registry, manifest);
|
|
2770
|
+
const out2 = {};
|
|
2771
|
+
for (const [route, kinds] of captured) {
|
|
2772
|
+
const page = pageOf.get(route);
|
|
2773
|
+
if (isOptedOut(registry, page)) continue;
|
|
2774
|
+
const waivers = page ? pageWaivers(registry, page) : {};
|
|
2775
|
+
const missing = CORE_KINDS.filter((k) => !kinds.has(k) && !waivers[k]);
|
|
2776
|
+
if (missing.length > 0) out2[route] = missing;
|
|
2777
|
+
}
|
|
2778
|
+
return out2;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
// src/scanner/scan/audit.ts
|
|
2224
2782
|
function audit(opts) {
|
|
2225
2783
|
const diagnostics = [];
|
|
2226
2784
|
const { registry, extracted, files, config } = opts;
|
|
@@ -2229,6 +2787,9 @@ function audit(opts) {
|
|
|
2229
2787
|
const acceptanceEnabled = config.audit?.acceptance ?? true;
|
|
2230
2788
|
const scopeLeakEnabled = config.audit?.scopeLeak ?? true;
|
|
2231
2789
|
const coverageEnabled = config.audit?.coverage ?? true;
|
|
2790
|
+
const statesEnabled = config.audit?.states ?? true;
|
|
2791
|
+
const pageCoverageEnabled = config.audit?.pageCoverage ?? true;
|
|
2792
|
+
const stateMatrixEnabled = config.audit?.stateMatrix ?? true;
|
|
2232
2793
|
if (opts.resolveDiagnostics) diagnostics.push(...opts.resolveDiagnostics);
|
|
2233
2794
|
for (const ef of extracted) {
|
|
2234
2795
|
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
@@ -2262,6 +2823,48 @@ function audit(opts) {
|
|
|
2262
2823
|
}
|
|
2263
2824
|
}
|
|
2264
2825
|
}
|
|
2826
|
+
if (check && opts.typesGenerated !== void 0) {
|
|
2827
|
+
const outRel = opts.typesOutputPath ?? config.output;
|
|
2828
|
+
const fresh = normalizeForCheck(opts.typesGenerated);
|
|
2829
|
+
if (opts.typesOnDisk === null || opts.typesOnDisk === void 0) {
|
|
2830
|
+
diagnostics.push({
|
|
2831
|
+
code: "gen-missing",
|
|
2832
|
+
severity: "error",
|
|
2833
|
+
message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
|
|
2834
|
+
file: outRel,
|
|
2835
|
+
hint: "Run `uidex scan` (without --check) to regenerate"
|
|
2836
|
+
});
|
|
2837
|
+
} else if (normalizeForCheck(opts.typesOnDisk) !== fresh) {
|
|
2838
|
+
diagnostics.push({
|
|
2839
|
+
code: "gen-stale",
|
|
2840
|
+
severity: "error",
|
|
2841
|
+
message: `Generated file "${outRel}" is stale`,
|
|
2842
|
+
file: outRel,
|
|
2843
|
+
hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
if (check && opts.locsGenerated !== void 0) {
|
|
2848
|
+
const outRel = opts.locsOutputPath ?? config.output;
|
|
2849
|
+
const fresh = normalizeForCheck(opts.locsGenerated);
|
|
2850
|
+
if (opts.locsOnDisk === null || opts.locsOnDisk === void 0) {
|
|
2851
|
+
diagnostics.push({
|
|
2852
|
+
code: "gen-missing",
|
|
2853
|
+
severity: "error",
|
|
2854
|
+
message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
|
|
2855
|
+
file: outRel,
|
|
2856
|
+
hint: "Run `uidex scan` (without --check) to regenerate"
|
|
2857
|
+
});
|
|
2858
|
+
} else if (normalizeForCheck(opts.locsOnDisk) !== fresh) {
|
|
2859
|
+
diagnostics.push({
|
|
2860
|
+
code: "gen-stale",
|
|
2861
|
+
severity: "error",
|
|
2862
|
+
message: `Generated file "${outRel}" is stale`,
|
|
2863
|
+
file: outRel,
|
|
2864
|
+
hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2265
2868
|
if (lint && acceptanceEnabled) {
|
|
2266
2869
|
for (const kind of ["widget", "feature", "page"]) {
|
|
2267
2870
|
for (const e of registry.list(kind)) {
|
|
@@ -2521,6 +3124,21 @@ function audit(opts) {
|
|
|
2521
3124
|
}
|
|
2522
3125
|
}
|
|
2523
3126
|
}
|
|
3127
|
+
if (lint && statesEnabled && opts.capturedStates) {
|
|
3128
|
+
diagnostics.push(...checkStateCoverage(registry, opts.capturedStates));
|
|
3129
|
+
}
|
|
3130
|
+
if (lint && pageCoverageEnabled && opts.capturedStates) {
|
|
3131
|
+
diagnostics.push(
|
|
3132
|
+
...checkPageCoverage(registry, opts.capturedStates, opts.pageBaseline)
|
|
3133
|
+
);
|
|
3134
|
+
}
|
|
3135
|
+
if (lint && stateMatrixEnabled && opts.capturedStates) {
|
|
3136
|
+
diagnostics.push(
|
|
3137
|
+
...checkStateMatrix(registry, opts.capturedStates, {
|
|
3138
|
+
missingKinds: opts.pageBaseline?.missingKinds ?? {}
|
|
3139
|
+
})
|
|
3140
|
+
);
|
|
3141
|
+
}
|
|
2524
3142
|
const summary = {
|
|
2525
3143
|
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
2526
3144
|
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
@@ -2685,6 +3303,148 @@ function stableReplacer(_key, value) {
|
|
|
2685
3303
|
}
|
|
2686
3304
|
|
|
2687
3305
|
// src/scanner/scan/emit.ts
|
|
3306
|
+
var AUTHORED_KINDS = /* @__PURE__ */ new Set([
|
|
3307
|
+
"route",
|
|
3308
|
+
"page",
|
|
3309
|
+
"feature",
|
|
3310
|
+
"widget",
|
|
3311
|
+
"primitive",
|
|
3312
|
+
"flow"
|
|
3313
|
+
]);
|
|
3314
|
+
function internedPayload(entities) {
|
|
3315
|
+
const fileIndex = /* @__PURE__ */ new Map();
|
|
3316
|
+
const files = [];
|
|
3317
|
+
const compact = entities.map((e) => {
|
|
3318
|
+
const loc = e.loc;
|
|
3319
|
+
if (!loc || typeof loc.file !== "string") return e;
|
|
3320
|
+
let idx = fileIndex.get(loc.file);
|
|
3321
|
+
if (idx === void 0) {
|
|
3322
|
+
idx = files.length;
|
|
3323
|
+
files.push(loc.file);
|
|
3324
|
+
fileIndex.set(loc.file, idx);
|
|
3325
|
+
}
|
|
3326
|
+
const rest = { ...loc };
|
|
3327
|
+
delete rest.file;
|
|
3328
|
+
return { ...e, loc: { f: idx, ...rest } };
|
|
3329
|
+
});
|
|
3330
|
+
return {
|
|
3331
|
+
filesDecl: `const files: readonly string[] = ${jsonStable(files)}`,
|
|
3332
|
+
parseExpr: `(JSON.parse(${JSON.stringify(
|
|
3333
|
+
JSON.stringify(compact, replacerSorted)
|
|
3334
|
+
)}) as any[]).map(hydrate)`
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
3337
|
+
var HYDRATE_FN = [
|
|
3338
|
+
"function hydrate(e: any): any {",
|
|
3339
|
+
' if (e.loc && typeof e.loc.f === "number") {',
|
|
3340
|
+
" const { f, ...rest } = e.loc",
|
|
3341
|
+
" e.loc = { file: files[f], ...rest }",
|
|
3342
|
+
" }",
|
|
3343
|
+
" return e",
|
|
3344
|
+
"}"
|
|
3345
|
+
].join("\n");
|
|
3346
|
+
function compactMeta(meta) {
|
|
3347
|
+
if (!meta || typeof meta !== "object") return meta;
|
|
3348
|
+
const composes = meta.composes;
|
|
3349
|
+
if (!Array.isArray(composes)) return meta;
|
|
3350
|
+
return {
|
|
3351
|
+
...meta,
|
|
3352
|
+
composes: composes.map(
|
|
3353
|
+
(c) => c && typeof c === "object" && c.kind === "element" && Object.keys(c).length === 2 && typeof c.id === "string" ? c.id : c
|
|
3354
|
+
)
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
3357
|
+
function emitLocsModule(thin, uidexImport) {
|
|
3358
|
+
const kinds = [...new Set(thin.map((e) => e.kind))].sort();
|
|
3359
|
+
const kindIdx = new Map(kinds.map((k, i) => [k, i]));
|
|
3360
|
+
const fileIndex = /* @__PURE__ */ new Map();
|
|
3361
|
+
const files = [];
|
|
3362
|
+
const fi = (f) => {
|
|
3363
|
+
let i = fileIndex.get(f);
|
|
3364
|
+
if (i === void 0) {
|
|
3365
|
+
i = files.length;
|
|
3366
|
+
files.push(f);
|
|
3367
|
+
fileIndex.set(f, i);
|
|
3368
|
+
}
|
|
3369
|
+
return i;
|
|
3370
|
+
};
|
|
3371
|
+
const bare = [];
|
|
3372
|
+
const rich = [];
|
|
3373
|
+
for (const e of thin) {
|
|
3374
|
+
const loc = e.loc;
|
|
3375
|
+
const topOnlyKindIdLoc = Object.keys(e).every(
|
|
3376
|
+
(k) => k === "kind" || k === "id" || k === "loc"
|
|
3377
|
+
);
|
|
3378
|
+
const locOnlyKnown = !!loc && typeof loc.file === "string" && Object.keys(loc).every(
|
|
3379
|
+
(k) => k === "file" || k === "line" || k === "column"
|
|
3380
|
+
);
|
|
3381
|
+
if (topOnlyKindIdLoc && locOnlyKnown) {
|
|
3382
|
+
const row = [
|
|
3383
|
+
kindIdx.get(e.kind),
|
|
3384
|
+
e.id,
|
|
3385
|
+
fi(loc.file)
|
|
3386
|
+
];
|
|
3387
|
+
if (loc.line != null || loc.column != null)
|
|
3388
|
+
row.push(Number(loc.line ?? 0));
|
|
3389
|
+
if (loc.column != null) row.push(Number(loc.column));
|
|
3390
|
+
bare.push(row);
|
|
3391
|
+
} else {
|
|
3392
|
+
const richEntity = { ...e };
|
|
3393
|
+
if (loc && typeof loc.file === "string") {
|
|
3394
|
+
const { file, ...rest } = loc;
|
|
3395
|
+
richEntity.loc = { f: fi(file), ...rest };
|
|
3396
|
+
}
|
|
3397
|
+
if (richEntity.meta) richEntity.meta = compactMeta(richEntity.meta);
|
|
3398
|
+
rich.push(richEntity);
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
const str = (v) => JSON.stringify(JSON.stringify(v, replacerSorted));
|
|
3402
|
+
const l = [];
|
|
3403
|
+
l.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
3404
|
+
l.push(
|
|
3405
|
+
"// Deferred element/region source locations for the inspector's copy-path."
|
|
3406
|
+
);
|
|
3407
|
+
l.push("// @ts-nocheck");
|
|
3408
|
+
l.push("/* eslint-disable */");
|
|
3409
|
+
l.push(`import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`);
|
|
3410
|
+
l.push("");
|
|
3411
|
+
l.push(`const files: readonly string[] = ${jsonStable(files)}`);
|
|
3412
|
+
l.push(`const kinds: readonly string[] = ${jsonStable(kinds)}`);
|
|
3413
|
+
l.push("function loc(fi: number, line?: number, col?: number) {");
|
|
3414
|
+
l.push(" const o: any = { file: files[fi] }");
|
|
3415
|
+
l.push(" if (line) o.line = line");
|
|
3416
|
+
l.push(" if (col) o.column = col");
|
|
3417
|
+
l.push(" return o");
|
|
3418
|
+
l.push("}");
|
|
3419
|
+
l.push(`const bare: any[] = JSON.parse(${str(bare)})`);
|
|
3420
|
+
l.push(`const rich: any[] = JSON.parse(${str(rich)})`);
|
|
3421
|
+
l.push("");
|
|
3422
|
+
l.push(
|
|
3423
|
+
"/** Add the thin element/region entities (source locations) to a registry. */"
|
|
3424
|
+
);
|
|
3425
|
+
l.push("export function loadLocs(target: Registry): Registry {");
|
|
3426
|
+
l.push(" for (const r of bare)");
|
|
3427
|
+
l.push(
|
|
3428
|
+
" target.add({ kind: kinds[r[0]], id: r[1], loc: loc(r[2], r[3], r[4]) } as Entity)"
|
|
3429
|
+
);
|
|
3430
|
+
l.push(" for (const e of rich) {");
|
|
3431
|
+
l.push(" const out: any = { ...e }");
|
|
3432
|
+
l.push(' if (e.loc && typeof e.loc.f === "number") {');
|
|
3433
|
+
l.push(" const { f, ...rest } = e.loc");
|
|
3434
|
+
l.push(" out.loc = { file: files[f], ...rest }");
|
|
3435
|
+
l.push(" }");
|
|
3436
|
+
l.push(" if (e.meta && Array.isArray(e.meta.composes)) {");
|
|
3437
|
+
l.push(
|
|
3438
|
+
' out.meta = { ...e.meta, composes: e.meta.composes.map((c: any) => typeof c === "string" ? { id: c, kind: "element" } : c) }'
|
|
3439
|
+
);
|
|
3440
|
+
l.push(" }");
|
|
3441
|
+
l.push(" target.add(out)");
|
|
3442
|
+
l.push(" }");
|
|
3443
|
+
l.push(" return target");
|
|
3444
|
+
l.push("}");
|
|
3445
|
+
l.push("");
|
|
3446
|
+
return l.join("\n");
|
|
3447
|
+
}
|
|
2688
3448
|
function sortById(arr) {
|
|
2689
3449
|
return [...arr].sort((a, b) => a.id.localeCompare(b.id));
|
|
2690
3450
|
}
|
|
@@ -2710,6 +3470,20 @@ function emitIdUnion(name, ids) {
|
|
|
2710
3470
|
${body}
|
|
2711
3471
|
`;
|
|
2712
3472
|
}
|
|
3473
|
+
var OPAQUE_ID_UNIONS = /* @__PURE__ */ new Set(["ElementId"]);
|
|
3474
|
+
function emitId(name, ids) {
|
|
3475
|
+
if (OPAQUE_ID_UNIONS.has(name)) {
|
|
3476
|
+
return `export type ${name} = string & { readonly __uidex?: ${JSON.stringify(name)} }
|
|
3477
|
+
`;
|
|
3478
|
+
}
|
|
3479
|
+
return emitIdUnion(name, ids);
|
|
3480
|
+
}
|
|
3481
|
+
function dataOutputPath(output) {
|
|
3482
|
+
return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".data.$1");
|
|
3483
|
+
}
|
|
3484
|
+
function locsOutputPath(output) {
|
|
3485
|
+
return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".locs.$1");
|
|
3486
|
+
}
|
|
2713
3487
|
function emit(opts) {
|
|
2714
3488
|
const { registry, gitContext, uidexImport = "uidex" } = opts;
|
|
2715
3489
|
const routes = [...registry.list("route")].sort(
|
|
@@ -2718,112 +3492,131 @@ function emit(opts) {
|
|
|
2718
3492
|
const pages = sortById(registry.list("page"));
|
|
2719
3493
|
const features = sortById(registry.list("feature"));
|
|
2720
3494
|
const widgets = sortById(registry.list("widget"));
|
|
3495
|
+
const stateNames = /* @__PURE__ */ new Set();
|
|
3496
|
+
for (const e of [...pages, ...features, ...widgets]) {
|
|
3497
|
+
for (const s of e.meta?.states ?? []) stateNames.add(s.name);
|
|
3498
|
+
}
|
|
2721
3499
|
const regions = sortById(registry.list("region"));
|
|
2722
3500
|
const elements = sortById(registry.list("element"));
|
|
2723
3501
|
const primitives = sortById(registry.list("primitive"));
|
|
2724
3502
|
const flows = sortById(registry.list("flow"));
|
|
2725
|
-
const
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
lines.push(
|
|
2730
|
-
`import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`
|
|
3503
|
+
const t = [];
|
|
3504
|
+
t.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
3505
|
+
t.push(
|
|
3506
|
+
"// Types only \u2014 the runtime registry lives in the sibling `.data` module."
|
|
2731
3507
|
);
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
3508
|
+
t.push("/* eslint-disable */");
|
|
3509
|
+
t.push("");
|
|
3510
|
+
t.push("// ---- id unions ----");
|
|
3511
|
+
t.push(
|
|
3512
|
+
emitId(
|
|
2736
3513
|
"PageId",
|
|
2737
3514
|
pages.map((e) => e.id)
|
|
2738
3515
|
)
|
|
2739
3516
|
);
|
|
2740
|
-
|
|
2741
|
-
|
|
3517
|
+
t.push(
|
|
3518
|
+
emitId(
|
|
2742
3519
|
"FeatureId",
|
|
2743
3520
|
features.map((e) => e.id)
|
|
2744
3521
|
)
|
|
2745
3522
|
);
|
|
2746
|
-
|
|
2747
|
-
|
|
3523
|
+
t.push(
|
|
3524
|
+
emitId(
|
|
2748
3525
|
"WidgetId",
|
|
2749
3526
|
widgets.map((e) => e.id)
|
|
2750
3527
|
)
|
|
2751
3528
|
);
|
|
2752
|
-
|
|
2753
|
-
|
|
3529
|
+
t.push(
|
|
3530
|
+
emitId(
|
|
2754
3531
|
"RegionId",
|
|
2755
3532
|
regions.map((e) => e.id)
|
|
2756
3533
|
)
|
|
2757
3534
|
);
|
|
2758
|
-
|
|
2759
|
-
|
|
3535
|
+
t.push(
|
|
3536
|
+
emitId(
|
|
2760
3537
|
"ElementId",
|
|
2761
3538
|
elements.map((e) => e.id)
|
|
2762
3539
|
)
|
|
2763
3540
|
);
|
|
2764
|
-
|
|
2765
|
-
|
|
3541
|
+
t.push(
|
|
3542
|
+
emitId(
|
|
2766
3543
|
"PrimitiveId",
|
|
2767
3544
|
primitives.map((e) => e.id)
|
|
2768
3545
|
)
|
|
2769
3546
|
);
|
|
2770
|
-
|
|
2771
|
-
|
|
3547
|
+
t.push(
|
|
3548
|
+
emitId(
|
|
2772
3549
|
"FlowId",
|
|
2773
3550
|
flows.map((e) => e.id)
|
|
2774
3551
|
)
|
|
2775
3552
|
);
|
|
2776
|
-
|
|
2777
|
-
|
|
3553
|
+
t.push(
|
|
3554
|
+
emitId(
|
|
2778
3555
|
"RouteId",
|
|
2779
3556
|
routes.map((e) => e.path)
|
|
2780
3557
|
)
|
|
2781
3558
|
);
|
|
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
|
-
|
|
3559
|
+
t.push(emitId("StateId", [...stateNames]));
|
|
3560
|
+
t.push("");
|
|
3561
|
+
t.push("// ---- authoring-surface shape types ----");
|
|
3562
|
+
t.push("export namespace Uidex {");
|
|
3563
|
+
t.push(
|
|
3564
|
+
' export type CoreStateKind = "loading" | "empty" | "populated" | "error"'
|
|
3565
|
+
);
|
|
3566
|
+
t.push(' export type StateKind = CoreStateKind | "variant"');
|
|
3567
|
+
t.push(" export interface State {");
|
|
3568
|
+
t.push(" name: string");
|
|
3569
|
+
t.push(" kind?: StateKind");
|
|
3570
|
+
t.push(" acceptance?: readonly string[]");
|
|
3571
|
+
t.push(" description?: string");
|
|
3572
|
+
t.push(" }");
|
|
3573
|
+
t.push(" export type Waivers = Partial<Record<CoreStateKind, string>>");
|
|
3574
|
+
t.push(" export interface Page {");
|
|
3575
|
+
t.push(" page: PageId | false");
|
|
3576
|
+
t.push(" name?: string");
|
|
3577
|
+
t.push(" features?: readonly FeatureId[]");
|
|
3578
|
+
t.push(" widgets?: readonly WidgetId[]");
|
|
3579
|
+
t.push(" acceptance?: readonly string[]");
|
|
3580
|
+
t.push(" states?: readonly (string | State)[]");
|
|
3581
|
+
t.push(" capture?: boolean");
|
|
3582
|
+
t.push(" waivers?: Waivers");
|
|
3583
|
+
t.push(" description?: string");
|
|
3584
|
+
t.push(" }");
|
|
3585
|
+
t.push(" export interface Feature {");
|
|
3586
|
+
t.push(" feature: FeatureId | false");
|
|
3587
|
+
t.push(" name?: string");
|
|
3588
|
+
t.push(" features?: readonly FeatureId[]");
|
|
3589
|
+
t.push(" acceptance?: readonly string[]");
|
|
3590
|
+
t.push(" states?: readonly (string | State)[]");
|
|
3591
|
+
t.push(" description?: string");
|
|
3592
|
+
t.push(" }");
|
|
3593
|
+
t.push(" export interface Primitive {");
|
|
3594
|
+
t.push(" primitive: PrimitiveId");
|
|
3595
|
+
t.push(" name?: string");
|
|
3596
|
+
t.push(" description?: string");
|
|
3597
|
+
t.push(" }");
|
|
3598
|
+
t.push(" export interface Widget {");
|
|
3599
|
+
t.push(" widget: WidgetId");
|
|
3600
|
+
t.push(" name?: string");
|
|
3601
|
+
t.push(" acceptance?: readonly string[]");
|
|
3602
|
+
t.push(" states?: readonly (string | State)[]");
|
|
3603
|
+
t.push(" description?: string");
|
|
3604
|
+
t.push(" }");
|
|
3605
|
+
t.push(" export interface Region {");
|
|
3606
|
+
t.push(" region: RegionId | false");
|
|
3607
|
+
t.push(" name?: string");
|
|
3608
|
+
t.push(" description?: string");
|
|
3609
|
+
t.push(" }");
|
|
3610
|
+
t.push(" export interface Flow {");
|
|
3611
|
+
t.push(" flow: FlowId");
|
|
3612
|
+
t.push(" name?: string");
|
|
3613
|
+
t.push(" description?: string");
|
|
3614
|
+
t.push(" }");
|
|
3615
|
+
t.push(" export interface NotFlow {");
|
|
3616
|
+
t.push(" notFlow: true");
|
|
3617
|
+
t.push(" }");
|
|
3618
|
+
t.push("}");
|
|
3619
|
+
t.push("");
|
|
2827
3620
|
const allEntities = [
|
|
2828
3621
|
...routes,
|
|
2829
3622
|
...pages,
|
|
@@ -2834,25 +3627,39 @@ function emit(opts) {
|
|
|
2834
3627
|
...primitives,
|
|
2835
3628
|
...flows
|
|
2836
3629
|
];
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
);
|
|
2840
|
-
lines.push("");
|
|
2841
|
-
lines.push("// ---- git context ----");
|
|
3630
|
+
const authored = allEntities.filter((e) => AUTHORED_KINDS.has(e.kind));
|
|
3631
|
+
const thin = allEntities.filter((e) => !AUTHORED_KINDS.has(e.kind));
|
|
2842
3632
|
const gc = gitContext ?? { branch: null, commit: null, pr: null };
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
3633
|
+
const authoredPayload = internedPayload(authored);
|
|
3634
|
+
const d = [];
|
|
3635
|
+
d.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
3636
|
+
d.push(
|
|
3637
|
+
"// Authored-metadata registry. The thin element/region entities (source"
|
|
3638
|
+
);
|
|
3639
|
+
d.push(
|
|
3640
|
+
"// locations only) live in the sibling `.locs` module, loaded lazily."
|
|
3641
|
+
);
|
|
3642
|
+
d.push("// @ts-nocheck");
|
|
3643
|
+
d.push("/* eslint-disable */");
|
|
3644
|
+
d.push(`import { createUidex } from ${JSON.stringify(uidexImport)}`);
|
|
3645
|
+
d.push(`import type { Registry } from ${JSON.stringify(uidexImport)}`);
|
|
3646
|
+
d.push("");
|
|
3647
|
+
d.push(authoredPayload.filesDecl);
|
|
3648
|
+
d.push(HYDRATE_FN);
|
|
3649
|
+
d.push(`export const entities = ${authoredPayload.parseExpr}`);
|
|
3650
|
+
d.push("");
|
|
3651
|
+
d.push(`export const gitContext = ${jsonStable(gc)} as const`);
|
|
3652
|
+
d.push("");
|
|
3653
|
+
d.push("export function loadRegistry(target: Registry): Registry {");
|
|
3654
|
+
d.push(" for (const entity of entities) target.add(entity)");
|
|
3655
|
+
d.push(" return target");
|
|
3656
|
+
d.push("}");
|
|
3657
|
+
d.push("");
|
|
3658
|
+
d.push("export const uidex = createUidex()");
|
|
3659
|
+
d.push("for (const entity of entities) uidex.registry.add(entity)");
|
|
3660
|
+
d.push("");
|
|
3661
|
+
const locs = emitLocsModule(thin, uidexImport);
|
|
3662
|
+
return { types: t.join("\n"), data: d.join("\n"), locs };
|
|
2856
3663
|
}
|
|
2857
3664
|
|
|
2858
3665
|
// src/scanner/scan/git.ts
|
|
@@ -2963,6 +3770,68 @@ function renderSpec(args) {
|
|
|
2963
3770
|
// src/scanner/scan/pipeline.ts
|
|
2964
3771
|
var fs4 = __toESM(require("fs"), 1);
|
|
2965
3772
|
var path7 = __toESM(require("path"), 1);
|
|
3773
|
+
var DEFAULT_STATES_MANIFEST = "uidex-states.json";
|
|
3774
|
+
var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
|
|
3775
|
+
function loadPageBaseline(configDir, config) {
|
|
3776
|
+
const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
3777
|
+
const abs = path7.resolve(configDir, rel);
|
|
3778
|
+
let raw;
|
|
3779
|
+
try {
|
|
3780
|
+
raw = fs4.readFileSync(abs, "utf8");
|
|
3781
|
+
} catch {
|
|
3782
|
+
return void 0;
|
|
3783
|
+
}
|
|
3784
|
+
try {
|
|
3785
|
+
const parsed = JSON.parse(raw);
|
|
3786
|
+
if (!parsed || !Array.isArray(parsed.uncapturedPages)) return void 0;
|
|
3787
|
+
const uncapturedPages = parsed.uncapturedPages.filter(
|
|
3788
|
+
(p2) => typeof p2 === "string"
|
|
3789
|
+
);
|
|
3790
|
+
let missingKinds;
|
|
3791
|
+
const mkRaw = parsed.missingKinds;
|
|
3792
|
+
if (mkRaw && typeof mkRaw === "object" && !Array.isArray(mkRaw)) {
|
|
3793
|
+
const mk = {};
|
|
3794
|
+
for (const [route, kinds] of Object.entries(mkRaw)) {
|
|
3795
|
+
if (Array.isArray(kinds)) {
|
|
3796
|
+
mk[route] = kinds.filter((k) => typeof k === "string");
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
missingKinds = mk;
|
|
3800
|
+
}
|
|
3801
|
+
return { uncapturedPages, ...missingKinds ? { missingKinds } : {} };
|
|
3802
|
+
} catch {
|
|
3803
|
+
return void 0;
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
function loadCapturedStates(configDir, config) {
|
|
3807
|
+
const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
|
|
3808
|
+
const abs = path7.resolve(configDir, rel);
|
|
3809
|
+
let raw;
|
|
3810
|
+
try {
|
|
3811
|
+
raw = fs4.readFileSync(abs, "utf8");
|
|
3812
|
+
} catch {
|
|
3813
|
+
return void 0;
|
|
3814
|
+
}
|
|
3815
|
+
try {
|
|
3816
|
+
const parsed = JSON.parse(raw);
|
|
3817
|
+
if (!parsed || !Array.isArray(parsed.captured)) return void 0;
|
|
3818
|
+
const captured = [];
|
|
3819
|
+
for (const entry of parsed.captured) {
|
|
3820
|
+
if (!entry || typeof entry !== "object") continue;
|
|
3821
|
+
const c = entry;
|
|
3822
|
+
if (typeof c.entity !== "string" || typeof c.state !== "string") continue;
|
|
3823
|
+
const rec = { entity: c.entity, state: c.state };
|
|
3824
|
+
if (typeof c.kind === "string") rec.kind = c.kind;
|
|
3825
|
+
if (typeof c.stateKind === "string") rec.stateKind = c.stateKind;
|
|
3826
|
+
if (typeof c.url === "string") rec.url = c.url;
|
|
3827
|
+
if (typeof c.route === "string") rec.route = c.route;
|
|
3828
|
+
captured.push(rec);
|
|
3829
|
+
}
|
|
3830
|
+
return { captured };
|
|
3831
|
+
} catch {
|
|
3832
|
+
return void 0;
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
2966
3835
|
function runScan(opts = {}) {
|
|
2967
3836
|
const cwd = opts.cwd ?? process.cwd();
|
|
2968
3837
|
const configs = opts.configs ?? discover({ cwd });
|
|
@@ -2989,23 +3858,29 @@ function runOne(dc, opts) {
|
|
|
2989
3858
|
flowFiles: extractedFlows
|
|
2990
3859
|
});
|
|
2991
3860
|
const gitContext = resolveGitContext({ cwd: configDir });
|
|
2992
|
-
const
|
|
3861
|
+
const emitted = emit({
|
|
2993
3862
|
registry: resolved.registry,
|
|
2994
3863
|
gitContext
|
|
2995
3864
|
});
|
|
2996
|
-
const
|
|
2997
|
-
const
|
|
2998
|
-
|
|
3865
|
+
const typesRel = config.output;
|
|
3866
|
+
const typesPath = path7.resolve(configDir, typesRel);
|
|
3867
|
+
const dataRel = dataOutputPath(config.output);
|
|
3868
|
+
const dataPath = path7.resolve(configDir, dataRel);
|
|
3869
|
+
const locsRel = locsOutputPath(config.output);
|
|
3870
|
+
const locsPath = path7.resolve(configDir, locsRel);
|
|
3871
|
+
let typesOnDisk = null;
|
|
3872
|
+
let dataOnDisk = null;
|
|
3873
|
+
let locsOnDisk = null;
|
|
2999
3874
|
if (opts.check) {
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
existingOnDisk = null;
|
|
3004
|
-
}
|
|
3875
|
+
typesOnDisk = readOrNull(typesPath);
|
|
3876
|
+
dataOnDisk = readOrNull(dataPath);
|
|
3877
|
+
locsOnDisk = readOrNull(locsPath);
|
|
3005
3878
|
}
|
|
3006
3879
|
const hasExtractDiagnostics = [...extracted, ...extractedFlows].some(
|
|
3007
3880
|
(ef) => (ef.diagnostics?.length ?? 0) > 0
|
|
3008
3881
|
);
|
|
3882
|
+
const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
|
|
3883
|
+
const pageBaseline = opts.pageBaseline ?? loadPageBaseline(configDir, config);
|
|
3009
3884
|
let auditResult;
|
|
3010
3885
|
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
|
|
3011
3886
|
auditResult = audit({
|
|
@@ -3017,9 +3892,20 @@ function runOne(dc, opts) {
|
|
|
3017
3892
|
check: opts.check,
|
|
3018
3893
|
lint: opts.lint,
|
|
3019
3894
|
resolveDiagnostics: resolved.diagnostics,
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3895
|
+
// The entity array (and thus the meaningful drift) lives in the data file,
|
|
3896
|
+
// so the entity-aware gen-stale diff runs against it; the types file gets a
|
|
3897
|
+
// plain content compare (see audit()).
|
|
3898
|
+
generated: emitted.data,
|
|
3899
|
+
existingOnDisk: dataOnDisk,
|
|
3900
|
+
outputPath: dataRel,
|
|
3901
|
+
typesGenerated: emitted.types,
|
|
3902
|
+
typesOnDisk,
|
|
3903
|
+
typesOutputPath: typesRel,
|
|
3904
|
+
locsGenerated: emitted.locs,
|
|
3905
|
+
locsOnDisk,
|
|
3906
|
+
locsOutputPath: locsRel,
|
|
3907
|
+
capturedStates,
|
|
3908
|
+
pageBaseline
|
|
3023
3909
|
});
|
|
3024
3910
|
}
|
|
3025
3911
|
return {
|
|
@@ -3028,13 +3914,38 @@ function runOne(dc, opts) {
|
|
|
3028
3914
|
registry: resolved.registry,
|
|
3029
3915
|
gitContext,
|
|
3030
3916
|
audit: auditResult,
|
|
3031
|
-
|
|
3032
|
-
|
|
3917
|
+
types: {
|
|
3918
|
+
generated: emitted.types,
|
|
3919
|
+
outputPath: typesPath,
|
|
3920
|
+
outputRel: typesRel
|
|
3921
|
+
},
|
|
3922
|
+
data: { generated: emitted.data, outputPath: dataPath, outputRel: dataRel },
|
|
3923
|
+
locs: { generated: emitted.locs, outputPath: locsPath, outputRel: locsRel },
|
|
3924
|
+
capturedStates
|
|
3033
3925
|
};
|
|
3034
3926
|
}
|
|
3927
|
+
function readOrNull(p2) {
|
|
3928
|
+
try {
|
|
3929
|
+
return fs4.readFileSync(p2, "utf8");
|
|
3930
|
+
} catch {
|
|
3931
|
+
return null;
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
function writeArtifact(artifact) {
|
|
3935
|
+
if (readOrNull(artifact.outputPath) === artifact.generated) return false;
|
|
3936
|
+
fs4.mkdirSync(path7.dirname(artifact.outputPath), { recursive: true });
|
|
3937
|
+
fs4.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
|
|
3938
|
+
return true;
|
|
3939
|
+
}
|
|
3035
3940
|
function writeScanResult(result) {
|
|
3036
|
-
|
|
3037
|
-
|
|
3941
|
+
const wroteTypes = writeArtifact(result.types);
|
|
3942
|
+
const wroteData = writeArtifact(result.data);
|
|
3943
|
+
const wroteLocs = writeArtifact(result.locs);
|
|
3944
|
+
return wroteTypes || wroteData || wroteLocs;
|
|
3945
|
+
}
|
|
3946
|
+
function pageBaselinePath(result) {
|
|
3947
|
+
const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
3948
|
+
return path7.resolve(result.configDir, rel);
|
|
3038
3949
|
}
|
|
3039
3950
|
|
|
3040
3951
|
// src/scanner/scan/fix.ts
|
|
@@ -3607,16 +4518,13 @@ function helpText2() {
|
|
|
3607
4518
|
" scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
|
|
3608
4519
|
" rename <element|widget|region> <old-id> <new-id> Rename an id everywhere (DOM attr, flows, exports)",
|
|
3609
4520
|
" 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
4521
|
"",
|
|
3615
4522
|
"Flags:",
|
|
3616
4523
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
3617
4524
|
" --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
|
|
3618
4525
|
" --audit Equivalent to --check --lint (read-only)",
|
|
3619
4526
|
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
|
|
4527
|
+
" --update-baseline Regenerate the page-coverage baseline (uidex-coverage-baseline.json) from the current uncaptured routes",
|
|
3620
4528
|
" --json Emit JSON diagnostics on stdout",
|
|
3621
4529
|
" --force (scaffold) overwrite existing spec",
|
|
3622
4530
|
""
|
|
@@ -3636,7 +4544,7 @@ function runInit(cwd, w) {
|
|
|
3636
4544
|
fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3637
4545
|
w.out(`Created ${configPath}`);
|
|
3638
4546
|
const gitignorePath = path11.join(cwd, ".gitignore");
|
|
3639
|
-
const entry = "*.gen
|
|
4547
|
+
const entry = "*.gen.*";
|
|
3640
4548
|
if (fs8.existsSync(gitignorePath)) {
|
|
3641
4549
|
const existing = fs8.readFileSync(gitignorePath, "utf8");
|
|
3642
4550
|
const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
|
|
@@ -3659,8 +4567,9 @@ function runInit(cwd, w) {
|
|
|
3659
4567
|
}
|
|
3660
4568
|
function runScanCommand(cwd, flags, w) {
|
|
3661
4569
|
const fix = Boolean(flags.fix);
|
|
3662
|
-
const
|
|
3663
|
-
const
|
|
4570
|
+
const updateBaseline = Boolean(flags["update-baseline"]);
|
|
4571
|
+
const check = !fix && !updateBaseline && Boolean(flags.check || flags.audit);
|
|
4572
|
+
const lint = Boolean(flags.lint || flags.audit || fix || updateBaseline);
|
|
3664
4573
|
const asJson = Boolean(flags.json);
|
|
3665
4574
|
let configs = discover({ cwd });
|
|
3666
4575
|
if (configs.length === 0) {
|
|
@@ -3679,8 +4588,33 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3679
4588
|
configs = discover({ cwd });
|
|
3680
4589
|
}
|
|
3681
4590
|
const results = runScan({ cwd, check, lint, configs });
|
|
4591
|
+
const artifacts = results.flatMap((r) => [r.types, r.data, r.locs]);
|
|
4592
|
+
const wrote = /* @__PURE__ */ new Set();
|
|
3682
4593
|
if (!check) {
|
|
3683
|
-
for (const
|
|
4594
|
+
for (const a of artifacts) {
|
|
4595
|
+
if (writeArtifact(a)) wrote.add(a);
|
|
4596
|
+
}
|
|
4597
|
+
}
|
|
4598
|
+
if (updateBaseline) {
|
|
4599
|
+
for (const r of results) {
|
|
4600
|
+
const manifest = r.capturedStates ?? { captured: [] };
|
|
4601
|
+
const pageBaseline = computePageBaseline(r.registry, manifest);
|
|
4602
|
+
const missingKinds = computeMissingKinds(r.registry, manifest);
|
|
4603
|
+
const baseline = {
|
|
4604
|
+
...pageBaseline,
|
|
4605
|
+
...Object.keys(missingKinds).length > 0 ? { missingKinds } : {}
|
|
4606
|
+
};
|
|
4607
|
+
const p2 = pageBaselinePath(r);
|
|
4608
|
+
fs8.writeFileSync(p2, JSON.stringify(baseline, null, 2) + "\n");
|
|
4609
|
+
const kindGaps = Object.values(missingKinds).reduce(
|
|
4610
|
+
(n, ks) => n + ks.length,
|
|
4611
|
+
0
|
|
4612
|
+
);
|
|
4613
|
+
w.out(
|
|
4614
|
+
`Wrote ${p2} \u2014 ${baseline.uncapturedPages.length} uncaptured page(s), ${kindGaps} missing core-kind(s) baselined`
|
|
4615
|
+
);
|
|
4616
|
+
}
|
|
4617
|
+
return w.result(0);
|
|
3684
4618
|
}
|
|
3685
4619
|
const allDiagnostics = results.flatMap((r) => r.audit?.diagnostics ?? []);
|
|
3686
4620
|
const summary = results.reduce(
|
|
@@ -3708,10 +4642,14 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3708
4642
|
);
|
|
3709
4643
|
}
|
|
3710
4644
|
for (const r of results) {
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
4645
|
+
for (const art of [r.types, r.data, r.locs]) {
|
|
4646
|
+
if (check) {
|
|
4647
|
+
w.out(`Checked ${art.outputPath}`);
|
|
4648
|
+
} else if (wrote.has(art)) {
|
|
4649
|
+
w.out(`Wrote ${art.outputPath}`);
|
|
4650
|
+
} else {
|
|
4651
|
+
w.out(`Unchanged ${art.outputPath}`);
|
|
4652
|
+
}
|
|
3715
4653
|
}
|
|
3716
4654
|
for (const d of r.audit?.diagnostics ?? []) {
|
|
3717
4655
|
const loc = d.file ? `${d.file}${d.line ? `:${d.line}` : ""}` : "";
|
|
@@ -3820,12 +4758,21 @@ function createWriter() {
|
|
|
3820
4758
|
DEFAULT_CONVENTIONS,
|
|
3821
4759
|
applyFixes,
|
|
3822
4760
|
audit,
|
|
4761
|
+
checkPageCoverage,
|
|
4762
|
+
checkStateCoverage,
|
|
4763
|
+
checkStateMatrix,
|
|
4764
|
+
compileRoute,
|
|
4765
|
+
computeMissingKinds,
|
|
4766
|
+
computePageBaseline,
|
|
3823
4767
|
detectRoutes,
|
|
3824
4768
|
discover,
|
|
3825
4769
|
emit,
|
|
3826
4770
|
extract,
|
|
3827
4771
|
extractUidexExports,
|
|
3828
4772
|
globToRegExp,
|
|
4773
|
+
loadPageBaseline,
|
|
4774
|
+
matchUrlToRoute,
|
|
4775
|
+
pageBaselinePath,
|
|
3829
4776
|
parseConfig,
|
|
3830
4777
|
pathToId,
|
|
3831
4778
|
renameEntity,
|