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/cli/cli.cjs
CHANGED
|
@@ -181,14 +181,14 @@ ${helpText()}`);
|
|
|
181
181
|
const provider = await selectProvider(opts, flags.provider);
|
|
182
182
|
if (!provider) return out(0, "Cancelled.\n");
|
|
183
183
|
if (sub === "install") {
|
|
184
|
-
const
|
|
184
|
+
const result2 = await provider.install({
|
|
185
185
|
cwd,
|
|
186
186
|
force: flags.force === true
|
|
187
187
|
});
|
|
188
|
-
return out(0, formatChanges(provider, "Installed",
|
|
188
|
+
return out(0, formatChanges(provider, "Installed", result2));
|
|
189
189
|
}
|
|
190
|
-
const
|
|
191
|
-
return out(0, formatChanges(provider, "Uninstalled",
|
|
190
|
+
const result = await provider.uninstall({ cwd });
|
|
191
|
+
return out(0, formatChanges(provider, "Uninstalled", result));
|
|
192
192
|
}
|
|
193
193
|
async function selectProvider(opts, explicit) {
|
|
194
194
|
if (explicit) {
|
|
@@ -231,9 +231,9 @@ function parseFlags(args) {
|
|
|
231
231
|
}
|
|
232
232
|
return flags;
|
|
233
233
|
}
|
|
234
|
-
function formatChanges(provider, verb,
|
|
234
|
+
function formatChanges(provider, verb, result) {
|
|
235
235
|
const lines = [`${verb} ${provider.label}:`];
|
|
236
|
-
for (const c of
|
|
236
|
+
for (const c of result.changes) lines.push(` ${describe(c)}`);
|
|
237
237
|
return lines.join("\n") + "\n";
|
|
238
238
|
}
|
|
239
239
|
function describe(c) {
|
|
@@ -300,7 +300,9 @@ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
300
300
|
"output",
|
|
301
301
|
"flows",
|
|
302
302
|
"audit",
|
|
303
|
-
"conventions"
|
|
303
|
+
"conventions",
|
|
304
|
+
"statesManifest",
|
|
305
|
+
"coverageBaseline"
|
|
304
306
|
]);
|
|
305
307
|
var ALLOWED_SOURCE_KEYS = /* @__PURE__ */ new Set(["rootDir", "include", "exclude", "prefix"]);
|
|
306
308
|
var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -310,7 +312,14 @@ var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
|
|
|
310
312
|
"flows",
|
|
311
313
|
"regions"
|
|
312
314
|
]);
|
|
313
|
-
var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
|
|
315
|
+
var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
|
|
316
|
+
"scopeLeak",
|
|
317
|
+
"coverage",
|
|
318
|
+
"acceptance",
|
|
319
|
+
"states",
|
|
320
|
+
"pageCoverage",
|
|
321
|
+
"stateMatrix"
|
|
322
|
+
]);
|
|
314
323
|
function fail(msg) {
|
|
315
324
|
throw new ConfigError(`Invalid .uidex.json: ${msg}`);
|
|
316
325
|
}
|
|
@@ -369,6 +378,12 @@ function validateConfig(raw) {
|
|
|
369
378
|
}
|
|
370
379
|
if (raw.exclude !== void 0) assertStringArray(raw.exclude, `exclude`);
|
|
371
380
|
if (raw.flows !== void 0) assertStringArray(raw.flows, `flows`);
|
|
381
|
+
if (raw.statesManifest !== void 0 && typeof raw.statesManifest !== "string") {
|
|
382
|
+
fail(`"statesManifest" must be a string`);
|
|
383
|
+
}
|
|
384
|
+
if (raw.coverageBaseline !== void 0 && typeof raw.coverageBaseline !== "string") {
|
|
385
|
+
fail(`"coverageBaseline" must be a string`);
|
|
386
|
+
}
|
|
372
387
|
if (raw.audit !== void 0) {
|
|
373
388
|
assertObject(raw.audit, "audit");
|
|
374
389
|
for (const key of Object.keys(raw.audit)) {
|
|
@@ -392,7 +407,13 @@ function validateConfig(raw) {
|
|
|
392
407
|
fail(`conventions.features must be a string or false`);
|
|
393
408
|
}
|
|
394
409
|
if (c.pages !== void 0 && c.pages !== false && c.pages !== "auto") {
|
|
395
|
-
|
|
410
|
+
const p2 = c.pages;
|
|
411
|
+
const routesDir = typeof p2 === "object" && p2 !== null && !Array.isArray(p2) ? p2.routesDir : void 0;
|
|
412
|
+
if (typeof p2 !== "object" || p2 === null || Array.isArray(p2) || Object.keys(p2).some((k) => k !== "routesDir") || typeof routesDir !== "string" || routesDir.length === 0) {
|
|
413
|
+
fail(
|
|
414
|
+
`conventions.pages must be "auto", false, or { routesDir: string }`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
396
417
|
}
|
|
397
418
|
if (c.flows !== void 0 && c.flows !== false) {
|
|
398
419
|
assertStringArray(c.flows, "conventions.flows");
|
|
@@ -408,7 +429,9 @@ function validateConfig(raw) {
|
|
|
408
429
|
output: raw.output,
|
|
409
430
|
flows: raw.flows,
|
|
410
431
|
audit: raw.audit,
|
|
411
|
-
conventions: raw.conventions
|
|
432
|
+
conventions: raw.conventions,
|
|
433
|
+
statesManifest: raw.statesManifest,
|
|
434
|
+
coverageBaseline: raw.coverageBaseline
|
|
412
435
|
};
|
|
413
436
|
}
|
|
414
437
|
function parseConfig(json) {
|
|
@@ -598,6 +621,12 @@ var ENTITY_KINDS = [
|
|
|
598
621
|
"primitive",
|
|
599
622
|
"flow"
|
|
600
623
|
];
|
|
624
|
+
var CORE_STATE_KINDS = [
|
|
625
|
+
"loading",
|
|
626
|
+
"empty",
|
|
627
|
+
"populated",
|
|
628
|
+
"error"
|
|
629
|
+
];
|
|
601
630
|
function isMetaKind(kind) {
|
|
602
631
|
return kind !== "route" && kind !== "flow";
|
|
603
632
|
}
|
|
@@ -731,11 +760,11 @@ function createRegistry() {
|
|
|
731
760
|
};
|
|
732
761
|
const query = (predicate) => {
|
|
733
762
|
const flows = getFlows();
|
|
734
|
-
const
|
|
763
|
+
const result = [];
|
|
735
764
|
for (const entity of allEntities()) {
|
|
736
|
-
if (predicate(entity))
|
|
765
|
+
if (predicate(entity)) result.push(freezeEntity(entity, flows));
|
|
737
766
|
}
|
|
738
|
-
return
|
|
767
|
+
return result;
|
|
739
768
|
};
|
|
740
769
|
const byScope = (scope) => query(
|
|
741
770
|
(entity) => "scopes" in entity && Array.isArray(entity.scopes) && entity.scopes.includes(scope)
|
|
@@ -776,6 +805,295 @@ function createRegistry() {
|
|
|
776
805
|
};
|
|
777
806
|
}
|
|
778
807
|
|
|
808
|
+
// src/scanner/scan/page-coverage.ts
|
|
809
|
+
function compileRoute(pattern) {
|
|
810
|
+
const segs = pattern.split("/").filter(Boolean);
|
|
811
|
+
let specificity = 0;
|
|
812
|
+
const optionalTail = segs.length > 0 && /^\[\[\.{3}.+\]\]$/.test(segs[segs.length - 1]);
|
|
813
|
+
const effective = optionalTail ? segs.slice(0, -1) : segs;
|
|
814
|
+
const parts = effective.map((seg) => {
|
|
815
|
+
const catchAll = /^\[\.{3}.+\]$/.test(seg);
|
|
816
|
+
const dynamic = /^\[.+\]$/.test(seg);
|
|
817
|
+
if (catchAll) return "(?:[^/]+)(?:/[^/]+)*";
|
|
818
|
+
if (dynamic) return "[^/]+";
|
|
819
|
+
specificity++;
|
|
820
|
+
return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
821
|
+
});
|
|
822
|
+
let body = parts.length === 0 ? "" : "/" + parts.join("/");
|
|
823
|
+
if (optionalTail) body += "(?:/[^/]+)*";
|
|
824
|
+
if (body === "") body = "/";
|
|
825
|
+
const re = new RegExp(`^${body}/?$`);
|
|
826
|
+
return { test: (url) => re.test(url), specificity };
|
|
827
|
+
}
|
|
828
|
+
function matchUrlToRoute(url, routePaths) {
|
|
829
|
+
const path12 = stripQuery(url);
|
|
830
|
+
let best = null;
|
|
831
|
+
for (const rp of routePaths) {
|
|
832
|
+
const { test, specificity } = compileRoute(rp);
|
|
833
|
+
if (!test(path12)) continue;
|
|
834
|
+
if (!best || specificity > best.specificity)
|
|
835
|
+
best = { path: rp, specificity };
|
|
836
|
+
}
|
|
837
|
+
return best?.path ?? null;
|
|
838
|
+
}
|
|
839
|
+
function stripQuery(url) {
|
|
840
|
+
const q = url.indexOf("?");
|
|
841
|
+
const h = url.indexOf("#");
|
|
842
|
+
let end = url.length;
|
|
843
|
+
if (q !== -1) end = Math.min(end, q);
|
|
844
|
+
if (h !== -1) end = Math.min(end, h);
|
|
845
|
+
return url.slice(0, end);
|
|
846
|
+
}
|
|
847
|
+
function routeForCapture(c, routes, routePaths) {
|
|
848
|
+
if (c.route && routePaths.includes(c.route)) return c.route;
|
|
849
|
+
if (c.url) {
|
|
850
|
+
const m = matchUrlToRoute(c.url, routePaths);
|
|
851
|
+
if (m) return m;
|
|
852
|
+
}
|
|
853
|
+
if (c.kind === "page") {
|
|
854
|
+
const r = routes.find((r2) => r2.page === c.entity);
|
|
855
|
+
if (r) return r.path;
|
|
856
|
+
}
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
function checkPageCoverage(registry, manifest, baseline) {
|
|
860
|
+
const routes = registry.list("route");
|
|
861
|
+
if (routes.length === 0) return [];
|
|
862
|
+
const routePaths = routes.map((r) => r.path);
|
|
863
|
+
const captured = manifest.captured ?? [];
|
|
864
|
+
const optedOut = /* @__PURE__ */ new Set();
|
|
865
|
+
for (const r of routes) {
|
|
866
|
+
const page = registry.get("page", r.page);
|
|
867
|
+
if (page?.meta?.capture === false) optedOut.add(r.path);
|
|
868
|
+
}
|
|
869
|
+
const covered = /* @__PURE__ */ new Set();
|
|
870
|
+
for (const c of captured) {
|
|
871
|
+
const route = routeForCapture(c, routes, routePaths);
|
|
872
|
+
if (route) covered.add(route);
|
|
873
|
+
}
|
|
874
|
+
const baselineSet = new Set(baseline?.uncapturedPages ?? []);
|
|
875
|
+
const diagnostics = [];
|
|
876
|
+
for (const r of routes) {
|
|
877
|
+
if (covered.has(r.path) || optedOut.has(r.path)) continue;
|
|
878
|
+
const loc = registry.get("page", r.page)?.loc;
|
|
879
|
+
if (baselineSet.has(r.path)) {
|
|
880
|
+
diagnostics.push({
|
|
881
|
+
code: "page-backlog",
|
|
882
|
+
severity: "info",
|
|
883
|
+
message: `route "${r.path}" has no visual-states capture (acknowledged backlog)`,
|
|
884
|
+
file: loc?.file,
|
|
885
|
+
line: loc?.line,
|
|
886
|
+
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 }\`.`
|
|
887
|
+
});
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
diagnostics.push({
|
|
891
|
+
code: "page-no-capture",
|
|
892
|
+
severity: "error",
|
|
893
|
+
message: `route "${r.path}" has no visual-states capture and is not accounted for`,
|
|
894
|
+
file: loc?.file,
|
|
895
|
+
line: loc?.line,
|
|
896
|
+
entity: { kind: "route", id: r.path },
|
|
897
|
+
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).`
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
for (const b of baselineSet) {
|
|
901
|
+
if (!routePaths.includes(b)) {
|
|
902
|
+
diagnostics.push({
|
|
903
|
+
code: "stale-page-baseline",
|
|
904
|
+
severity: "warning",
|
|
905
|
+
message: `coverage baseline lists "${b}", which is not a route in the registry`,
|
|
906
|
+
hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
|
|
907
|
+
});
|
|
908
|
+
} else if (covered.has(b) || optedOut.has(b)) {
|
|
909
|
+
diagnostics.push({
|
|
910
|
+
code: "page-captured",
|
|
911
|
+
severity: "warning",
|
|
912
|
+
message: `route "${b}" is now captured but is still in the coverage baseline`,
|
|
913
|
+
hint: `Prune it with \`uidex scan --update-baseline\` so the backlog reflects reality.`
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return diagnostics;
|
|
918
|
+
}
|
|
919
|
+
function computePageBaseline(registry, manifest) {
|
|
920
|
+
const uncaptured = [];
|
|
921
|
+
for (const d of checkPageCoverage(registry, manifest, {
|
|
922
|
+
uncapturedPages: []
|
|
923
|
+
})) {
|
|
924
|
+
if (d.code === "page-no-capture" && d.entity) uncaptured.push(d.entity.id);
|
|
925
|
+
}
|
|
926
|
+
return { uncapturedPages: uncaptured.sort() };
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// src/scanner/scan/state-coverage.ts
|
|
930
|
+
var STATE_KINDS = ["page", "feature", "widget"];
|
|
931
|
+
function declaredStateEntities(registry) {
|
|
932
|
+
const out2 = [];
|
|
933
|
+
for (const kind of STATE_KINDS) {
|
|
934
|
+
for (const e of registry.list(kind)) {
|
|
935
|
+
const states = e.meta?.states;
|
|
936
|
+
if (!states || states.length === 0) continue;
|
|
937
|
+
out2.push({
|
|
938
|
+
kind,
|
|
939
|
+
id: e.id,
|
|
940
|
+
states: states.map((s) => s.name),
|
|
941
|
+
loc: e.loc
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return out2;
|
|
946
|
+
}
|
|
947
|
+
function captureMatchesEntity(cap, ent) {
|
|
948
|
+
if (cap.entity !== ent.id) return false;
|
|
949
|
+
return cap.kind === void 0 || cap.kind === ent.kind;
|
|
950
|
+
}
|
|
951
|
+
function checkStateCoverage(registry, manifest) {
|
|
952
|
+
const diagnostics = [];
|
|
953
|
+
const declared = declaredStateEntities(registry);
|
|
954
|
+
const captured = manifest.captured ?? [];
|
|
955
|
+
for (const ent of declared) {
|
|
956
|
+
for (const state of ent.states) {
|
|
957
|
+
const hit = captured.some(
|
|
958
|
+
(c) => captureMatchesEntity(c, ent) && c.state === state
|
|
959
|
+
);
|
|
960
|
+
if (hit) continue;
|
|
961
|
+
diagnostics.push({
|
|
962
|
+
code: "missing-state-capture",
|
|
963
|
+
severity: "warning",
|
|
964
|
+
message: `${ent.kind} "${ent.id}" declares state "${state}" but no capture produced it`,
|
|
965
|
+
file: ent.loc?.file,
|
|
966
|
+
line: ent.loc?.line,
|
|
967
|
+
entity: { kind: ent.kind, id: ent.id },
|
|
968
|
+
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.`
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
for (const cap of captured) {
|
|
973
|
+
const candidates = declared.filter((ent2) => captureMatchesEntity(cap, ent2));
|
|
974
|
+
if (candidates.length === 0) continue;
|
|
975
|
+
const declaredSomewhere = candidates.some(
|
|
976
|
+
(ent2) => ent2.states.includes(cap.state)
|
|
977
|
+
);
|
|
978
|
+
if (declaredSomewhere) continue;
|
|
979
|
+
const ent = candidates[0];
|
|
980
|
+
diagnostics.push({
|
|
981
|
+
code: "orphan-state-capture",
|
|
982
|
+
severity: "warning",
|
|
983
|
+
message: `Capture produced state "${cap.state}" for ${ent.kind} "${ent.id}", which is not in its declared states [${ent.states.join(", ")}]`,
|
|
984
|
+
file: ent.loc?.file,
|
|
985
|
+
line: ent.loc?.line,
|
|
986
|
+
entity: { kind: ent.kind, id: ent.id },
|
|
987
|
+
hint: `Declare "${cap.state}" in \`export const uidex = { states: [...] }\` for "${ent.id}", or rename the capture to a declared state.`
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
return diagnostics;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// src/scanner/scan/state-matrix.ts
|
|
994
|
+
var CORE_KINDS = CORE_STATE_KINDS;
|
|
995
|
+
var STATE_ENTITY_KINDS = ["page", "feature", "widget"];
|
|
996
|
+
function resolveMatrixKind(registry, c) {
|
|
997
|
+
if (c.stateKind) return c.stateKind;
|
|
998
|
+
for (const kind of STATE_ENTITY_KINDS) {
|
|
999
|
+
const declared = registry.get(kind, c.entity)?.meta?.states?.find((s) => s.name === c.state)?.kind;
|
|
1000
|
+
if (declared) return declared;
|
|
1001
|
+
}
|
|
1002
|
+
return CORE_KINDS.includes(c.state) ? c.state : "variant";
|
|
1003
|
+
}
|
|
1004
|
+
function coreKindsByRoute(registry, manifest) {
|
|
1005
|
+
const routes = registry.list("route");
|
|
1006
|
+
const routePaths = routes.map((r) => r.path);
|
|
1007
|
+
const byRoute = /* @__PURE__ */ new Map();
|
|
1008
|
+
for (const c of manifest.captured ?? []) {
|
|
1009
|
+
const route = routeForCapture(c, routes, routePaths);
|
|
1010
|
+
if (!route) continue;
|
|
1011
|
+
if (!byRoute.has(route)) byRoute.set(route, /* @__PURE__ */ new Set());
|
|
1012
|
+
const kind = resolveMatrixKind(registry, c);
|
|
1013
|
+
if (CORE_KINDS.includes(kind)) byRoute.get(route).add(kind);
|
|
1014
|
+
}
|
|
1015
|
+
return byRoute;
|
|
1016
|
+
}
|
|
1017
|
+
function pageWaivers(registry, routePage) {
|
|
1018
|
+
return registry.get("page", routePage)?.meta?.waivers ?? {};
|
|
1019
|
+
}
|
|
1020
|
+
function isOptedOut(registry, page) {
|
|
1021
|
+
return page ? registry.get("page", page)?.meta?.capture === false : false;
|
|
1022
|
+
}
|
|
1023
|
+
function checkStateMatrix(registry, manifest, baseline) {
|
|
1024
|
+
const routes = registry.list("route");
|
|
1025
|
+
if (routes.length === 0) return [];
|
|
1026
|
+
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
1027
|
+
const captured = coreKindsByRoute(registry, manifest);
|
|
1028
|
+
const baselined = baseline?.missingKinds ?? {};
|
|
1029
|
+
const diagnostics = [];
|
|
1030
|
+
for (const [route, kinds] of captured) {
|
|
1031
|
+
const page = pageOf.get(route);
|
|
1032
|
+
if (isOptedOut(registry, page)) continue;
|
|
1033
|
+
const waivers = page ? pageWaivers(registry, page) : {};
|
|
1034
|
+
const loc = page ? registry.get("page", page)?.loc : void 0;
|
|
1035
|
+
const baselinedKinds = new Set(baselined[route] ?? []);
|
|
1036
|
+
for (const kind of CORE_KINDS) {
|
|
1037
|
+
if (kinds.has(kind)) continue;
|
|
1038
|
+
if (waivers[kind]) continue;
|
|
1039
|
+
if (baselinedKinds.has(kind)) {
|
|
1040
|
+
diagnostics.push({
|
|
1041
|
+
code: "matrix-backlog",
|
|
1042
|
+
severity: "info",
|
|
1043
|
+
message: `route "${route}" does not render core kind "${kind}" (acknowledged MISSING_KINDS backlog)`,
|
|
1044
|
+
file: loc?.file,
|
|
1045
|
+
line: loc?.line,
|
|
1046
|
+
hint: `Capture the "${kind}" state for "${route}", waive it (\`waivers: { ${kind}: "why it can't" }\` on the page), or leave it in the baseline.`
|
|
1047
|
+
});
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
diagnostics.push({
|
|
1051
|
+
code: "core-kind",
|
|
1052
|
+
severity: "error",
|
|
1053
|
+
message: `route "${route}" is captured but never renders core kind "${kind}"`,
|
|
1054
|
+
file: loc?.file,
|
|
1055
|
+
line: loc?.line,
|
|
1056
|
+
entity: { kind: "route", id: route },
|
|
1057
|
+
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).`
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
for (const [route, kinds] of captured) {
|
|
1062
|
+
const page = pageOf.get(route);
|
|
1063
|
+
if (!page || isOptedOut(registry, page)) continue;
|
|
1064
|
+
const waivers = pageWaivers(registry, page);
|
|
1065
|
+
const loc = registry.get("page", page)?.loc;
|
|
1066
|
+
for (const kind of Object.keys(waivers)) {
|
|
1067
|
+
if (kinds.has(kind)) {
|
|
1068
|
+
diagnostics.push({
|
|
1069
|
+
code: "stale-waiver",
|
|
1070
|
+
severity: "warning",
|
|
1071
|
+
message: `page "${page}" waives core kind "${kind}", but route "${route}" does render it`,
|
|
1072
|
+
file: loc?.file,
|
|
1073
|
+
line: loc?.line,
|
|
1074
|
+
entity: { kind: "page", id: page },
|
|
1075
|
+
hint: `Remove the "${kind}" waiver \u2014 the route renders that kind, so the waiver's reason is stale.`
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
return diagnostics;
|
|
1081
|
+
}
|
|
1082
|
+
function computeMissingKinds(registry, manifest) {
|
|
1083
|
+
const routes = registry.list("route");
|
|
1084
|
+
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
1085
|
+
const captured = coreKindsByRoute(registry, manifest);
|
|
1086
|
+
const out2 = {};
|
|
1087
|
+
for (const [route, kinds] of captured) {
|
|
1088
|
+
const page = pageOf.get(route);
|
|
1089
|
+
if (isOptedOut(registry, page)) continue;
|
|
1090
|
+
const waivers = page ? pageWaivers(registry, page) : {};
|
|
1091
|
+
const missing = CORE_KINDS.filter((k) => !kinds.has(k) && !waivers[k]);
|
|
1092
|
+
if (missing.length > 0) out2[route] = missing;
|
|
1093
|
+
}
|
|
1094
|
+
return out2;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
779
1097
|
// src/scanner/scan/audit.ts
|
|
780
1098
|
function audit(opts) {
|
|
781
1099
|
const diagnostics = [];
|
|
@@ -785,6 +1103,9 @@ function audit(opts) {
|
|
|
785
1103
|
const acceptanceEnabled = config.audit?.acceptance ?? true;
|
|
786
1104
|
const scopeLeakEnabled = config.audit?.scopeLeak ?? true;
|
|
787
1105
|
const coverageEnabled = config.audit?.coverage ?? true;
|
|
1106
|
+
const statesEnabled = config.audit?.states ?? true;
|
|
1107
|
+
const pageCoverageEnabled = config.audit?.pageCoverage ?? true;
|
|
1108
|
+
const stateMatrixEnabled = config.audit?.stateMatrix ?? true;
|
|
788
1109
|
if (opts.resolveDiagnostics) diagnostics.push(...opts.resolveDiagnostics);
|
|
789
1110
|
for (const ef of extracted) {
|
|
790
1111
|
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
@@ -818,6 +1139,48 @@ function audit(opts) {
|
|
|
818
1139
|
}
|
|
819
1140
|
}
|
|
820
1141
|
}
|
|
1142
|
+
if (check && opts.typesGenerated !== void 0) {
|
|
1143
|
+
const outRel = opts.typesOutputPath ?? config.output;
|
|
1144
|
+
const fresh = normalizeForCheck(opts.typesGenerated);
|
|
1145
|
+
if (opts.typesOnDisk === null || opts.typesOnDisk === void 0) {
|
|
1146
|
+
diagnostics.push({
|
|
1147
|
+
code: "gen-missing",
|
|
1148
|
+
severity: "error",
|
|
1149
|
+
message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
|
|
1150
|
+
file: outRel,
|
|
1151
|
+
hint: "Run `uidex scan` (without --check) to regenerate"
|
|
1152
|
+
});
|
|
1153
|
+
} else if (normalizeForCheck(opts.typesOnDisk) !== fresh) {
|
|
1154
|
+
diagnostics.push({
|
|
1155
|
+
code: "gen-stale",
|
|
1156
|
+
severity: "error",
|
|
1157
|
+
message: `Generated file "${outRel}" is stale`,
|
|
1158
|
+
file: outRel,
|
|
1159
|
+
hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if (check && opts.locsGenerated !== void 0) {
|
|
1164
|
+
const outRel = opts.locsOutputPath ?? config.output;
|
|
1165
|
+
const fresh = normalizeForCheck(opts.locsGenerated);
|
|
1166
|
+
if (opts.locsOnDisk === null || opts.locsOnDisk === void 0) {
|
|
1167
|
+
diagnostics.push({
|
|
1168
|
+
code: "gen-missing",
|
|
1169
|
+
severity: "error",
|
|
1170
|
+
message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
|
|
1171
|
+
file: outRel,
|
|
1172
|
+
hint: "Run `uidex scan` (without --check) to regenerate"
|
|
1173
|
+
});
|
|
1174
|
+
} else if (normalizeForCheck(opts.locsOnDisk) !== fresh) {
|
|
1175
|
+
diagnostics.push({
|
|
1176
|
+
code: "gen-stale",
|
|
1177
|
+
severity: "error",
|
|
1178
|
+
message: `Generated file "${outRel}" is stale`,
|
|
1179
|
+
file: outRel,
|
|
1180
|
+
hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
821
1184
|
if (lint && acceptanceEnabled) {
|
|
822
1185
|
for (const kind of ["widget", "feature", "page"]) {
|
|
823
1186
|
for (const e of registry.list(kind)) {
|
|
@@ -1077,6 +1440,21 @@ function audit(opts) {
|
|
|
1077
1440
|
}
|
|
1078
1441
|
}
|
|
1079
1442
|
}
|
|
1443
|
+
if (lint && statesEnabled && opts.capturedStates) {
|
|
1444
|
+
diagnostics.push(...checkStateCoverage(registry, opts.capturedStates));
|
|
1445
|
+
}
|
|
1446
|
+
if (lint && pageCoverageEnabled && opts.capturedStates) {
|
|
1447
|
+
diagnostics.push(
|
|
1448
|
+
...checkPageCoverage(registry, opts.capturedStates, opts.pageBaseline)
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
if (lint && stateMatrixEnabled && opts.capturedStates) {
|
|
1452
|
+
diagnostics.push(
|
|
1453
|
+
...checkStateMatrix(registry, opts.capturedStates, {
|
|
1454
|
+
missingKinds: opts.pageBaseline?.missingKinds ?? {}
|
|
1455
|
+
})
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1080
1458
|
const summary = {
|
|
1081
1459
|
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
1082
1460
|
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
@@ -1241,6 +1619,148 @@ function stableReplacer(_key, value) {
|
|
|
1241
1619
|
}
|
|
1242
1620
|
|
|
1243
1621
|
// src/scanner/scan/emit.ts
|
|
1622
|
+
var AUTHORED_KINDS = /* @__PURE__ */ new Set([
|
|
1623
|
+
"route",
|
|
1624
|
+
"page",
|
|
1625
|
+
"feature",
|
|
1626
|
+
"widget",
|
|
1627
|
+
"primitive",
|
|
1628
|
+
"flow"
|
|
1629
|
+
]);
|
|
1630
|
+
function internedPayload(entities) {
|
|
1631
|
+
const fileIndex = /* @__PURE__ */ new Map();
|
|
1632
|
+
const files = [];
|
|
1633
|
+
const compact = entities.map((e) => {
|
|
1634
|
+
const loc = e.loc;
|
|
1635
|
+
if (!loc || typeof loc.file !== "string") return e;
|
|
1636
|
+
let idx = fileIndex.get(loc.file);
|
|
1637
|
+
if (idx === void 0) {
|
|
1638
|
+
idx = files.length;
|
|
1639
|
+
files.push(loc.file);
|
|
1640
|
+
fileIndex.set(loc.file, idx);
|
|
1641
|
+
}
|
|
1642
|
+
const rest = { ...loc };
|
|
1643
|
+
delete rest.file;
|
|
1644
|
+
return { ...e, loc: { f: idx, ...rest } };
|
|
1645
|
+
});
|
|
1646
|
+
return {
|
|
1647
|
+
filesDecl: `const files: readonly string[] = ${jsonStable(files)}`,
|
|
1648
|
+
parseExpr: `(JSON.parse(${JSON.stringify(
|
|
1649
|
+
JSON.stringify(compact, replacerSorted)
|
|
1650
|
+
)}) as any[]).map(hydrate)`
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
var HYDRATE_FN = [
|
|
1654
|
+
"function hydrate(e: any): any {",
|
|
1655
|
+
' if (e.loc && typeof e.loc.f === "number") {',
|
|
1656
|
+
" const { f, ...rest } = e.loc",
|
|
1657
|
+
" e.loc = { file: files[f], ...rest }",
|
|
1658
|
+
" }",
|
|
1659
|
+
" return e",
|
|
1660
|
+
"}"
|
|
1661
|
+
].join("\n");
|
|
1662
|
+
function compactMeta(meta) {
|
|
1663
|
+
if (!meta || typeof meta !== "object") return meta;
|
|
1664
|
+
const composes = meta.composes;
|
|
1665
|
+
if (!Array.isArray(composes)) return meta;
|
|
1666
|
+
return {
|
|
1667
|
+
...meta,
|
|
1668
|
+
composes: composes.map(
|
|
1669
|
+
(c) => c && typeof c === "object" && c.kind === "element" && Object.keys(c).length === 2 && typeof c.id === "string" ? c.id : c
|
|
1670
|
+
)
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
function emitLocsModule(thin, uidexImport) {
|
|
1674
|
+
const kinds = [...new Set(thin.map((e) => e.kind))].sort();
|
|
1675
|
+
const kindIdx = new Map(kinds.map((k, i) => [k, i]));
|
|
1676
|
+
const fileIndex = /* @__PURE__ */ new Map();
|
|
1677
|
+
const files = [];
|
|
1678
|
+
const fi = (f) => {
|
|
1679
|
+
let i = fileIndex.get(f);
|
|
1680
|
+
if (i === void 0) {
|
|
1681
|
+
i = files.length;
|
|
1682
|
+
files.push(f);
|
|
1683
|
+
fileIndex.set(f, i);
|
|
1684
|
+
}
|
|
1685
|
+
return i;
|
|
1686
|
+
};
|
|
1687
|
+
const bare = [];
|
|
1688
|
+
const rich = [];
|
|
1689
|
+
for (const e of thin) {
|
|
1690
|
+
const loc = e.loc;
|
|
1691
|
+
const topOnlyKindIdLoc = Object.keys(e).every(
|
|
1692
|
+
(k) => k === "kind" || k === "id" || k === "loc"
|
|
1693
|
+
);
|
|
1694
|
+
const locOnlyKnown = !!loc && typeof loc.file === "string" && Object.keys(loc).every(
|
|
1695
|
+
(k) => k === "file" || k === "line" || k === "column"
|
|
1696
|
+
);
|
|
1697
|
+
if (topOnlyKindIdLoc && locOnlyKnown) {
|
|
1698
|
+
const row = [
|
|
1699
|
+
kindIdx.get(e.kind),
|
|
1700
|
+
e.id,
|
|
1701
|
+
fi(loc.file)
|
|
1702
|
+
];
|
|
1703
|
+
if (loc.line != null || loc.column != null)
|
|
1704
|
+
row.push(Number(loc.line ?? 0));
|
|
1705
|
+
if (loc.column != null) row.push(Number(loc.column));
|
|
1706
|
+
bare.push(row);
|
|
1707
|
+
} else {
|
|
1708
|
+
const richEntity = { ...e };
|
|
1709
|
+
if (loc && typeof loc.file === "string") {
|
|
1710
|
+
const { file, ...rest } = loc;
|
|
1711
|
+
richEntity.loc = { f: fi(file), ...rest };
|
|
1712
|
+
}
|
|
1713
|
+
if (richEntity.meta) richEntity.meta = compactMeta(richEntity.meta);
|
|
1714
|
+
rich.push(richEntity);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
const str = (v) => JSON.stringify(JSON.stringify(v, replacerSorted));
|
|
1718
|
+
const l = [];
|
|
1719
|
+
l.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
1720
|
+
l.push(
|
|
1721
|
+
"// Deferred element/region source locations for the inspector's copy-path."
|
|
1722
|
+
);
|
|
1723
|
+
l.push("// @ts-nocheck");
|
|
1724
|
+
l.push("/* eslint-disable */");
|
|
1725
|
+
l.push(`import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`);
|
|
1726
|
+
l.push("");
|
|
1727
|
+
l.push(`const files: readonly string[] = ${jsonStable(files)}`);
|
|
1728
|
+
l.push(`const kinds: readonly string[] = ${jsonStable(kinds)}`);
|
|
1729
|
+
l.push("function loc(fi: number, line?: number, col?: number) {");
|
|
1730
|
+
l.push(" const o: any = { file: files[fi] }");
|
|
1731
|
+
l.push(" if (line) o.line = line");
|
|
1732
|
+
l.push(" if (col) o.column = col");
|
|
1733
|
+
l.push(" return o");
|
|
1734
|
+
l.push("}");
|
|
1735
|
+
l.push(`const bare: any[] = JSON.parse(${str(bare)})`);
|
|
1736
|
+
l.push(`const rich: any[] = JSON.parse(${str(rich)})`);
|
|
1737
|
+
l.push("");
|
|
1738
|
+
l.push(
|
|
1739
|
+
"/** Add the thin element/region entities (source locations) to a registry. */"
|
|
1740
|
+
);
|
|
1741
|
+
l.push("export function loadLocs(target: Registry): Registry {");
|
|
1742
|
+
l.push(" for (const r of bare)");
|
|
1743
|
+
l.push(
|
|
1744
|
+
" target.add({ kind: kinds[r[0]], id: r[1], loc: loc(r[2], r[3], r[4]) } as Entity)"
|
|
1745
|
+
);
|
|
1746
|
+
l.push(" for (const e of rich) {");
|
|
1747
|
+
l.push(" const out: any = { ...e }");
|
|
1748
|
+
l.push(' if (e.loc && typeof e.loc.f === "number") {');
|
|
1749
|
+
l.push(" const { f, ...rest } = e.loc");
|
|
1750
|
+
l.push(" out.loc = { file: files[f], ...rest }");
|
|
1751
|
+
l.push(" }");
|
|
1752
|
+
l.push(" if (e.meta && Array.isArray(e.meta.composes)) {");
|
|
1753
|
+
l.push(
|
|
1754
|
+
' out.meta = { ...e.meta, composes: e.meta.composes.map((c: any) => typeof c === "string" ? { id: c, kind: "element" } : c) }'
|
|
1755
|
+
);
|
|
1756
|
+
l.push(" }");
|
|
1757
|
+
l.push(" target.add(out)");
|
|
1758
|
+
l.push(" }");
|
|
1759
|
+
l.push(" return target");
|
|
1760
|
+
l.push("}");
|
|
1761
|
+
l.push("");
|
|
1762
|
+
return l.join("\n");
|
|
1763
|
+
}
|
|
1244
1764
|
function sortById(arr) {
|
|
1245
1765
|
return [...arr].sort((a, b) => a.id.localeCompare(b.id));
|
|
1246
1766
|
}
|
|
@@ -1266,6 +1786,20 @@ function emitIdUnion(name, ids) {
|
|
|
1266
1786
|
${body}
|
|
1267
1787
|
`;
|
|
1268
1788
|
}
|
|
1789
|
+
var OPAQUE_ID_UNIONS = /* @__PURE__ */ new Set(["ElementId"]);
|
|
1790
|
+
function emitId(name, ids) {
|
|
1791
|
+
if (OPAQUE_ID_UNIONS.has(name)) {
|
|
1792
|
+
return `export type ${name} = string & { readonly __uidex?: ${JSON.stringify(name)} }
|
|
1793
|
+
`;
|
|
1794
|
+
}
|
|
1795
|
+
return emitIdUnion(name, ids);
|
|
1796
|
+
}
|
|
1797
|
+
function dataOutputPath(output) {
|
|
1798
|
+
return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".data.$1");
|
|
1799
|
+
}
|
|
1800
|
+
function locsOutputPath(output) {
|
|
1801
|
+
return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".locs.$1");
|
|
1802
|
+
}
|
|
1269
1803
|
function emit(opts) {
|
|
1270
1804
|
const { registry, gitContext, uidexImport = "uidex" } = opts;
|
|
1271
1805
|
const routes = [...registry.list("route")].sort(
|
|
@@ -1274,112 +1808,131 @@ function emit(opts) {
|
|
|
1274
1808
|
const pages = sortById(registry.list("page"));
|
|
1275
1809
|
const features = sortById(registry.list("feature"));
|
|
1276
1810
|
const widgets = sortById(registry.list("widget"));
|
|
1811
|
+
const stateNames = /* @__PURE__ */ new Set();
|
|
1812
|
+
for (const e of [...pages, ...features, ...widgets]) {
|
|
1813
|
+
for (const s of e.meta?.states ?? []) stateNames.add(s.name);
|
|
1814
|
+
}
|
|
1277
1815
|
const regions = sortById(registry.list("region"));
|
|
1278
1816
|
const elements = sortById(registry.list("element"));
|
|
1279
1817
|
const primitives = sortById(registry.list("primitive"));
|
|
1280
1818
|
const flows = sortById(registry.list("flow"));
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
lines.push(
|
|
1286
|
-
`import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`
|
|
1819
|
+
const t = [];
|
|
1820
|
+
t.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
1821
|
+
t.push(
|
|
1822
|
+
"// Types only \u2014 the runtime registry lives in the sibling `.data` module."
|
|
1287
1823
|
);
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1824
|
+
t.push("/* eslint-disable */");
|
|
1825
|
+
t.push("");
|
|
1826
|
+
t.push("// ---- id unions ----");
|
|
1827
|
+
t.push(
|
|
1828
|
+
emitId(
|
|
1292
1829
|
"PageId",
|
|
1293
1830
|
pages.map((e) => e.id)
|
|
1294
1831
|
)
|
|
1295
1832
|
);
|
|
1296
|
-
|
|
1297
|
-
|
|
1833
|
+
t.push(
|
|
1834
|
+
emitId(
|
|
1298
1835
|
"FeatureId",
|
|
1299
1836
|
features.map((e) => e.id)
|
|
1300
1837
|
)
|
|
1301
1838
|
);
|
|
1302
|
-
|
|
1303
|
-
|
|
1839
|
+
t.push(
|
|
1840
|
+
emitId(
|
|
1304
1841
|
"WidgetId",
|
|
1305
1842
|
widgets.map((e) => e.id)
|
|
1306
1843
|
)
|
|
1307
1844
|
);
|
|
1308
|
-
|
|
1309
|
-
|
|
1845
|
+
t.push(
|
|
1846
|
+
emitId(
|
|
1310
1847
|
"RegionId",
|
|
1311
1848
|
regions.map((e) => e.id)
|
|
1312
1849
|
)
|
|
1313
1850
|
);
|
|
1314
|
-
|
|
1315
|
-
|
|
1851
|
+
t.push(
|
|
1852
|
+
emitId(
|
|
1316
1853
|
"ElementId",
|
|
1317
1854
|
elements.map((e) => e.id)
|
|
1318
1855
|
)
|
|
1319
1856
|
);
|
|
1320
|
-
|
|
1321
|
-
|
|
1857
|
+
t.push(
|
|
1858
|
+
emitId(
|
|
1322
1859
|
"PrimitiveId",
|
|
1323
1860
|
primitives.map((e) => e.id)
|
|
1324
1861
|
)
|
|
1325
1862
|
);
|
|
1326
|
-
|
|
1327
|
-
|
|
1863
|
+
t.push(
|
|
1864
|
+
emitId(
|
|
1328
1865
|
"FlowId",
|
|
1329
1866
|
flows.map((e) => e.id)
|
|
1330
1867
|
)
|
|
1331
1868
|
);
|
|
1332
|
-
|
|
1333
|
-
|
|
1869
|
+
t.push(
|
|
1870
|
+
emitId(
|
|
1334
1871
|
"RouteId",
|
|
1335
1872
|
routes.map((e) => e.path)
|
|
1336
1873
|
)
|
|
1337
1874
|
);
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1875
|
+
t.push(emitId("StateId", [...stateNames]));
|
|
1876
|
+
t.push("");
|
|
1877
|
+
t.push("// ---- authoring-surface shape types ----");
|
|
1878
|
+
t.push("export namespace Uidex {");
|
|
1879
|
+
t.push(
|
|
1880
|
+
' export type CoreStateKind = "loading" | "empty" | "populated" | "error"'
|
|
1881
|
+
);
|
|
1882
|
+
t.push(' export type StateKind = CoreStateKind | "variant"');
|
|
1883
|
+
t.push(" export interface State {");
|
|
1884
|
+
t.push(" name: string");
|
|
1885
|
+
t.push(" kind?: StateKind");
|
|
1886
|
+
t.push(" acceptance?: readonly string[]");
|
|
1887
|
+
t.push(" description?: string");
|
|
1888
|
+
t.push(" }");
|
|
1889
|
+
t.push(" export type Waivers = Partial<Record<CoreStateKind, string>>");
|
|
1890
|
+
t.push(" export interface Page {");
|
|
1891
|
+
t.push(" page: PageId | false");
|
|
1892
|
+
t.push(" name?: string");
|
|
1893
|
+
t.push(" features?: readonly FeatureId[]");
|
|
1894
|
+
t.push(" widgets?: readonly WidgetId[]");
|
|
1895
|
+
t.push(" acceptance?: readonly string[]");
|
|
1896
|
+
t.push(" states?: readonly (string | State)[]");
|
|
1897
|
+
t.push(" capture?: boolean");
|
|
1898
|
+
t.push(" waivers?: Waivers");
|
|
1899
|
+
t.push(" description?: string");
|
|
1900
|
+
t.push(" }");
|
|
1901
|
+
t.push(" export interface Feature {");
|
|
1902
|
+
t.push(" feature: FeatureId | false");
|
|
1903
|
+
t.push(" name?: string");
|
|
1904
|
+
t.push(" features?: readonly FeatureId[]");
|
|
1905
|
+
t.push(" acceptance?: readonly string[]");
|
|
1906
|
+
t.push(" states?: readonly (string | State)[]");
|
|
1907
|
+
t.push(" description?: string");
|
|
1908
|
+
t.push(" }");
|
|
1909
|
+
t.push(" export interface Primitive {");
|
|
1910
|
+
t.push(" primitive: PrimitiveId");
|
|
1911
|
+
t.push(" name?: string");
|
|
1912
|
+
t.push(" description?: string");
|
|
1913
|
+
t.push(" }");
|
|
1914
|
+
t.push(" export interface Widget {");
|
|
1915
|
+
t.push(" widget: WidgetId");
|
|
1916
|
+
t.push(" name?: string");
|
|
1917
|
+
t.push(" acceptance?: readonly string[]");
|
|
1918
|
+
t.push(" states?: readonly (string | State)[]");
|
|
1919
|
+
t.push(" description?: string");
|
|
1920
|
+
t.push(" }");
|
|
1921
|
+
t.push(" export interface Region {");
|
|
1922
|
+
t.push(" region: RegionId | false");
|
|
1923
|
+
t.push(" name?: string");
|
|
1924
|
+
t.push(" description?: string");
|
|
1925
|
+
t.push(" }");
|
|
1926
|
+
t.push(" export interface Flow {");
|
|
1927
|
+
t.push(" flow: FlowId");
|
|
1928
|
+
t.push(" name?: string");
|
|
1929
|
+
t.push(" description?: string");
|
|
1930
|
+
t.push(" }");
|
|
1931
|
+
t.push(" export interface NotFlow {");
|
|
1932
|
+
t.push(" notFlow: true");
|
|
1933
|
+
t.push(" }");
|
|
1934
|
+
t.push("}");
|
|
1935
|
+
t.push("");
|
|
1383
1936
|
const allEntities = [
|
|
1384
1937
|
...routes,
|
|
1385
1938
|
...pages,
|
|
@@ -1390,25 +1943,39 @@ function emit(opts) {
|
|
|
1390
1943
|
...primitives,
|
|
1391
1944
|
...flows
|
|
1392
1945
|
];
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
);
|
|
1396
|
-
lines.push("");
|
|
1397
|
-
lines.push("// ---- git context ----");
|
|
1946
|
+
const authored = allEntities.filter((e) => AUTHORED_KINDS.has(e.kind));
|
|
1947
|
+
const thin = allEntities.filter((e) => !AUTHORED_KINDS.has(e.kind));
|
|
1398
1948
|
const gc = gitContext ?? { branch: null, commit: null, pr: null };
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1949
|
+
const authoredPayload = internedPayload(authored);
|
|
1950
|
+
const d = [];
|
|
1951
|
+
d.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
1952
|
+
d.push(
|
|
1953
|
+
"// Authored-metadata registry. The thin element/region entities (source"
|
|
1954
|
+
);
|
|
1955
|
+
d.push(
|
|
1956
|
+
"// locations only) live in the sibling `.locs` module, loaded lazily."
|
|
1957
|
+
);
|
|
1958
|
+
d.push("// @ts-nocheck");
|
|
1959
|
+
d.push("/* eslint-disable */");
|
|
1960
|
+
d.push(`import { createUidex } from ${JSON.stringify(uidexImport)}`);
|
|
1961
|
+
d.push(`import type { Registry } from ${JSON.stringify(uidexImport)}`);
|
|
1962
|
+
d.push("");
|
|
1963
|
+
d.push(authoredPayload.filesDecl);
|
|
1964
|
+
d.push(HYDRATE_FN);
|
|
1965
|
+
d.push(`export const entities = ${authoredPayload.parseExpr}`);
|
|
1966
|
+
d.push("");
|
|
1967
|
+
d.push(`export const gitContext = ${jsonStable(gc)} as const`);
|
|
1968
|
+
d.push("");
|
|
1969
|
+
d.push("export function loadRegistry(target: Registry): Registry {");
|
|
1970
|
+
d.push(" for (const entity of entities) target.add(entity)");
|
|
1971
|
+
d.push(" return target");
|
|
1972
|
+
d.push("}");
|
|
1973
|
+
d.push("");
|
|
1974
|
+
d.push("export const uidex = createUidex()");
|
|
1975
|
+
d.push("for (const entity of entities) uidex.registry.add(entity)");
|
|
1976
|
+
d.push("");
|
|
1977
|
+
const locs = emitLocsModule(thin, uidexImport);
|
|
1978
|
+
return { types: t.join("\n"), data: d.join("\n"), locs };
|
|
1412
1979
|
}
|
|
1413
1980
|
|
|
1414
1981
|
// src/scanner/scan/ast.ts
|
|
@@ -1445,14 +2012,14 @@ function makeLineAt(content) {
|
|
|
1445
2012
|
function parseSource(file) {
|
|
1446
2013
|
const lineAt = makeLineAt(file.content);
|
|
1447
2014
|
try {
|
|
1448
|
-
const
|
|
2015
|
+
const result = (0, import_oxc_parser.parseSync)(file.sourcePath, file.content, {
|
|
1449
2016
|
lang: langFor(file.sourcePath),
|
|
1450
2017
|
sourceType: "module"
|
|
1451
2018
|
});
|
|
1452
2019
|
return {
|
|
1453
|
-
program:
|
|
1454
|
-
hasErrors:
|
|
1455
|
-
comments:
|
|
2020
|
+
program: result.program,
|
|
2021
|
+
hasErrors: result.errors.length > 0,
|
|
2022
|
+
comments: result.comments ?? [],
|
|
1456
2023
|
lineAt
|
|
1457
2024
|
};
|
|
1458
2025
|
} catch {
|
|
@@ -1512,20 +2079,35 @@ var ALLOWED_FIELDS = {
|
|
|
1512
2079
|
"features",
|
|
1513
2080
|
"widgets",
|
|
1514
2081
|
"acceptance",
|
|
2082
|
+
"states",
|
|
2083
|
+
"capture",
|
|
2084
|
+
"waivers",
|
|
1515
2085
|
"description"
|
|
1516
2086
|
]),
|
|
2087
|
+
// `capture` / `waivers` are route-scoped, and only a page maps to a route — so
|
|
2088
|
+
// they are page-only (the gates read them off the page). A feature/widget that
|
|
2089
|
+
// sets them gets an `uidex-export-unknown-field` error rather than a silent no-op.
|
|
1517
2090
|
feature: /* @__PURE__ */ new Set([
|
|
1518
2091
|
"feature",
|
|
1519
2092
|
"name",
|
|
1520
2093
|
"features",
|
|
1521
2094
|
"acceptance",
|
|
2095
|
+
"states",
|
|
1522
2096
|
"description"
|
|
1523
2097
|
]),
|
|
1524
2098
|
primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
|
|
1525
|
-
widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "description"]),
|
|
2099
|
+
widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "states", "description"]),
|
|
1526
2100
|
region: /* @__PURE__ */ new Set(["region", "name", "description"]),
|
|
1527
2101
|
flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
|
|
1528
2102
|
};
|
|
2103
|
+
var STATE_FIELDS = /* @__PURE__ */ new Set([
|
|
2104
|
+
"name",
|
|
2105
|
+
"kind",
|
|
2106
|
+
"acceptance",
|
|
2107
|
+
"description"
|
|
2108
|
+
]);
|
|
2109
|
+
var CORE_STATE_KINDS2 = new Set(CORE_STATE_KINDS);
|
|
2110
|
+
var STATE_KINDS2 = /* @__PURE__ */ new Set([...CORE_STATE_KINDS, "variant"]);
|
|
1529
2111
|
var FALSEABLE = /* @__PURE__ */ new Set([
|
|
1530
2112
|
"page",
|
|
1531
2113
|
"feature",
|
|
@@ -1915,6 +2497,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
1915
2497
|
}
|
|
1916
2498
|
const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
1917
2499
|
const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
2500
|
+
const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey) : void 0;
|
|
2501
|
+
const capture = kind === "page" ? readBooleanField(byKey, "capture") : void 0;
|
|
2502
|
+
const waivers = kind === "page" ? readWaiversField(byKey) : void 0;
|
|
1918
2503
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
1919
2504
|
const metadata = {
|
|
1920
2505
|
source: "ts-export",
|
|
@@ -1937,6 +2522,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
1937
2522
|
metadata.widgets = widgetsField.values;
|
|
1938
2523
|
metadata.widgetSpans = widgetsField.spans;
|
|
1939
2524
|
}
|
|
2525
|
+
if (states?.length) metadata.states = states;
|
|
2526
|
+
if (capture !== void 0) metadata.capture = capture;
|
|
2527
|
+
if (waivers && Object.keys(waivers).length > 0) metadata.waivers = waivers;
|
|
1940
2528
|
if (notFlow) metadata.notFlow = true;
|
|
1941
2529
|
if (typeof id === "string" && idValue.kind === "string") {
|
|
1942
2530
|
metadata.idSpan = idValue.span;
|
|
@@ -1973,6 +2561,48 @@ function readIdField(value, kind, fieldName) {
|
|
|
1973
2561
|
value.pos
|
|
1974
2562
|
);
|
|
1975
2563
|
}
|
|
2564
|
+
function readWaiversField(byKey) {
|
|
2565
|
+
const entry = byKey.get("waivers");
|
|
2566
|
+
if (!entry) return void 0;
|
|
2567
|
+
if (entry.value.kind !== "object") {
|
|
2568
|
+
throw new ExtractError(
|
|
2569
|
+
"uidex-export-invalid-field",
|
|
2570
|
+
"`waivers` must be an object mapping a core kind to a reason.",
|
|
2571
|
+
entry.value.pos
|
|
2572
|
+
);
|
|
2573
|
+
}
|
|
2574
|
+
const waivers = {};
|
|
2575
|
+
for (const e of entry.value.entries) {
|
|
2576
|
+
if (!CORE_STATE_KINDS2.has(e.key)) {
|
|
2577
|
+
throw new ExtractError(
|
|
2578
|
+
"uidex-export-invalid-field",
|
|
2579
|
+
`waiver key "${e.key}" is not a core kind; only ${[...CORE_STATE_KINDS2].join(", ")} participate in the matrix.`,
|
|
2580
|
+
e.keyPos
|
|
2581
|
+
);
|
|
2582
|
+
}
|
|
2583
|
+
if (e.value.kind !== "string" || e.value.value.length === 0) {
|
|
2584
|
+
throw new ExtractError(
|
|
2585
|
+
"uidex-export-invalid-field",
|
|
2586
|
+
`waiver "${e.key}" needs a non-empty reason (why the route cannot render this kind).`,
|
|
2587
|
+
e.value.pos
|
|
2588
|
+
);
|
|
2589
|
+
}
|
|
2590
|
+
waivers[e.key] = e.value.value;
|
|
2591
|
+
}
|
|
2592
|
+
return waivers;
|
|
2593
|
+
}
|
|
2594
|
+
function readBooleanField(byKey, name) {
|
|
2595
|
+
const entry = byKey.get(name);
|
|
2596
|
+
if (!entry) return void 0;
|
|
2597
|
+
if (entry.value.kind !== "boolean") {
|
|
2598
|
+
throw new ExtractError(
|
|
2599
|
+
"uidex-export-invalid-field",
|
|
2600
|
+
`\`${name}\` must be a boolean.`,
|
|
2601
|
+
entry.value.pos
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
return entry.value.value;
|
|
2605
|
+
}
|
|
1976
2606
|
function readStringField(byKey, name) {
|
|
1977
2607
|
const entry = byKey.get(name);
|
|
1978
2608
|
if (!entry) return void 0;
|
|
@@ -2010,6 +2640,86 @@ function readStringArrayField(byKey, name) {
|
|
|
2010
2640
|
}
|
|
2011
2641
|
return { values, spans };
|
|
2012
2642
|
}
|
|
2643
|
+
function readStatesField(byKey) {
|
|
2644
|
+
const entry = byKey.get("states");
|
|
2645
|
+
if (!entry) return void 0;
|
|
2646
|
+
if (entry.value.kind !== "array") {
|
|
2647
|
+
throw new ExtractError(
|
|
2648
|
+
"uidex-export-invalid-field",
|
|
2649
|
+
"`states` must be an array of state names or `{ name, acceptance?, description? }` objects.",
|
|
2650
|
+
entry.value.pos
|
|
2651
|
+
);
|
|
2652
|
+
}
|
|
2653
|
+
const states = [];
|
|
2654
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2655
|
+
for (const item of entry.value.items) {
|
|
2656
|
+
let state;
|
|
2657
|
+
if (item.kind === "string") {
|
|
2658
|
+
state = { name: item.value, nameSpan: item.span };
|
|
2659
|
+
} else if (item.kind === "object") {
|
|
2660
|
+
const fieldByKey = /* @__PURE__ */ new Map();
|
|
2661
|
+
for (const e of item.entries) fieldByKey.set(e.key, e);
|
|
2662
|
+
for (const e of item.entries) {
|
|
2663
|
+
if (!STATE_FIELDS.has(e.key)) {
|
|
2664
|
+
throw new ExtractError(
|
|
2665
|
+
"uidex-export-invalid-field",
|
|
2666
|
+
`Unknown field "${e.key}" in a \`states\` entry. Allowed: ${Array.from(
|
|
2667
|
+
STATE_FIELDS
|
|
2668
|
+
).sort().join(", ")}.`,
|
|
2669
|
+
e.value.pos
|
|
2670
|
+
);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
const nameEntry = fieldByKey.get("name");
|
|
2674
|
+
if (!nameEntry || nameEntry.value.kind !== "string") {
|
|
2675
|
+
throw new ExtractError(
|
|
2676
|
+
"uidex-export-invalid-field",
|
|
2677
|
+
"Each object `states` entry must have a string `name`.",
|
|
2678
|
+
item.pos
|
|
2679
|
+
);
|
|
2680
|
+
}
|
|
2681
|
+
state = { name: nameEntry.value.value, nameSpan: nameEntry.value.span };
|
|
2682
|
+
const kind = readStringField(fieldByKey, "kind");
|
|
2683
|
+
if (kind !== void 0) {
|
|
2684
|
+
if (!STATE_KINDS2.has(kind)) {
|
|
2685
|
+
throw new ExtractError(
|
|
2686
|
+
"uidex-export-invalid-field",
|
|
2687
|
+
`state \`kind\` must be one of ${[...STATE_KINDS2].join(", ")} (got "${kind}").`,
|
|
2688
|
+
fieldByKey.get("kind").value.pos
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
state.kind = kind;
|
|
2692
|
+
}
|
|
2693
|
+
const acceptance = readStringArrayField(fieldByKey, "acceptance")?.values;
|
|
2694
|
+
if (acceptance) state.acceptance = acceptance;
|
|
2695
|
+
const description = readStringField(fieldByKey, "description");
|
|
2696
|
+
if (description) state.description = description;
|
|
2697
|
+
} else {
|
|
2698
|
+
throw new ExtractError(
|
|
2699
|
+
"uidex-export-invalid-field",
|
|
2700
|
+
"`states` items must be strings or `{ name, ... }` objects.",
|
|
2701
|
+
item.pos
|
|
2702
|
+
);
|
|
2703
|
+
}
|
|
2704
|
+
if (state.name.length === 0) {
|
|
2705
|
+
throw new ExtractError(
|
|
2706
|
+
"uidex-export-invalid-field",
|
|
2707
|
+
"A `states` entry has an empty `name`.",
|
|
2708
|
+
item.pos
|
|
2709
|
+
);
|
|
2710
|
+
}
|
|
2711
|
+
if (seen.has(state.name)) {
|
|
2712
|
+
throw new ExtractError(
|
|
2713
|
+
"uidex-export-duplicate-state",
|
|
2714
|
+
`Duplicate state "${state.name}" in \`states\`; each state name must be unique within the entity.`,
|
|
2715
|
+
item.pos
|
|
2716
|
+
);
|
|
2717
|
+
}
|
|
2718
|
+
seen.add(state.name);
|
|
2719
|
+
states.push(state);
|
|
2720
|
+
}
|
|
2721
|
+
return states;
|
|
2722
|
+
}
|
|
2013
2723
|
function posAt(content, offset, p2) {
|
|
2014
2724
|
const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
|
|
2015
2725
|
return { offset, line: p2.lineAt(offset), column: offset - lineStart + 1 };
|
|
@@ -2247,22 +2957,22 @@ function collectElementAttrs(opening, consts, dynamicAttrs, lineAt) {
|
|
|
2247
2957
|
if (!kind) continue;
|
|
2248
2958
|
const value = attr.value;
|
|
2249
2959
|
if (!value) continue;
|
|
2250
|
-
let
|
|
2960
|
+
let result = UNRESOLVED;
|
|
2251
2961
|
let valueSpan;
|
|
2252
2962
|
if (value.type === "Literal") {
|
|
2253
2963
|
const v = staticString(value);
|
|
2254
|
-
|
|
2255
|
-
if (
|
|
2964
|
+
result = v !== null && v.length > 0 ? { resolved: true, ids: [v] } : UNRESOLVED;
|
|
2965
|
+
if (result.resolved) valueSpan = { start: value.start, end: value.end };
|
|
2256
2966
|
} else if (value.type === "JSXExpressionContainer") {
|
|
2257
2967
|
const expr = value.expression;
|
|
2258
2968
|
if (expr && expr.type !== "JSXEmptyExpression") {
|
|
2259
|
-
|
|
2969
|
+
result = evalIdExpression(expr, consts);
|
|
2260
2970
|
const inner = unwrapTsExpression(expr);
|
|
2261
|
-
if (
|
|
2971
|
+
if (result.resolved && staticString(inner) !== null) {
|
|
2262
2972
|
valueSpan = { start: inner.start, end: inner.end };
|
|
2263
2973
|
}
|
|
2264
2974
|
}
|
|
2265
|
-
if (!
|
|
2975
|
+
if (!result.resolved) {
|
|
2266
2976
|
dynamicAttrs.push({
|
|
2267
2977
|
kind,
|
|
2268
2978
|
attrName: kind === "element" ? "data-uidex" : `data-uidex-${kind}`,
|
|
@@ -2271,15 +2981,15 @@ function collectElementAttrs(opening, consts, dynamicAttrs, lineAt) {
|
|
|
2271
2981
|
continue;
|
|
2272
2982
|
}
|
|
2273
2983
|
}
|
|
2274
|
-
if (!
|
|
2275
|
-
for (const id of
|
|
2984
|
+
if (!result.resolved) continue;
|
|
2985
|
+
for (const id of result.ids) {
|
|
2276
2986
|
const resolved = {
|
|
2277
2987
|
kind,
|
|
2278
2988
|
id,
|
|
2279
2989
|
start: attr.start,
|
|
2280
2990
|
isPattern: id.includes("*"),
|
|
2281
2991
|
// Only a single plain string literal is renameable in place.
|
|
2282
|
-
...
|
|
2992
|
+
...result.ids.length === 1 && valueSpan ? { span: valueSpan } : {}
|
|
2283
2993
|
};
|
|
2284
2994
|
if (resolved.isPattern) patterns.push(resolved);
|
|
2285
2995
|
else statics.push(resolved);
|
|
@@ -2562,13 +3272,26 @@ var path8 = __toESM(require("path"), 1);
|
|
|
2562
3272
|
var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
2563
3273
|
var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
2564
3274
|
var ROUTE_BASENAME = /^route\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
2565
|
-
function detectRoutes(files) {
|
|
3275
|
+
function detectRoutes(files, options = {}) {
|
|
2566
3276
|
const out2 = [];
|
|
2567
3277
|
const seen = /* @__PURE__ */ new Set();
|
|
3278
|
+
const routesDir = options.routesDir ? options.routesDir.replace(/^\.?\/+/, "").replace(/\/+$/, "") : null;
|
|
3279
|
+
const routesDirParts = routesDir ? routesDir.split("/") : null;
|
|
2568
3280
|
for (const f of files) {
|
|
2569
3281
|
const rel = f.displayPath;
|
|
2570
3282
|
const parts = rel.split("/");
|
|
2571
3283
|
const base = parts[parts.length - 1];
|
|
3284
|
+
if (routesDirParts) {
|
|
3285
|
+
const end = dirEndIndex(parts, routesDirParts);
|
|
3286
|
+
if (end === -1 || end > parts.length - 1) continue;
|
|
3287
|
+
if (!PAGES_ROUTER_BASENAME.test(base)) continue;
|
|
3288
|
+
const routePath = routesStylePath(
|
|
3289
|
+
parts.slice(end, parts.length - 1),
|
|
3290
|
+
base
|
|
3291
|
+
);
|
|
3292
|
+
if (routePath !== null) push(out2, seen, routePath, f.displayPath);
|
|
3293
|
+
continue;
|
|
3294
|
+
}
|
|
2572
3295
|
const appIdx = parts.indexOf("app");
|
|
2573
3296
|
if (appIdx !== -1 && PAGE_BASENAME.test(base)) {
|
|
2574
3297
|
const routeSegments = parts.slice(appIdx + 1, parts.length - 1);
|
|
@@ -2595,15 +3318,11 @@ function detectRoutes(files) {
|
|
|
2595
3318
|
}
|
|
2596
3319
|
const routesIdx = parts.indexOf("routes");
|
|
2597
3320
|
if (routesIdx !== -1 && PAGES_ROUTER_BASENAME.test(base)) {
|
|
2598
|
-
const
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
base.replace(/\.[^.]+$/, "")
|
|
2604
|
-
].filter((s) => s !== "index" && s !== "__root");
|
|
2605
|
-
const routePath = formatNextAppPath(normalized);
|
|
2606
|
-
push(out2, seen, routePath, f.displayPath);
|
|
3321
|
+
const routePath = routesStylePath(
|
|
3322
|
+
parts.slice(routesIdx + 1, parts.length - 1),
|
|
3323
|
+
base
|
|
3324
|
+
);
|
|
3325
|
+
if (routePath !== null) push(out2, seen, routePath, f.displayPath);
|
|
2607
3326
|
continue;
|
|
2608
3327
|
}
|
|
2609
3328
|
}
|
|
@@ -2619,6 +3338,63 @@ function formatNextAppPath(segments) {
|
|
|
2619
3338
|
if (kept.length === 0) return "/";
|
|
2620
3339
|
return "/" + kept.join("/");
|
|
2621
3340
|
}
|
|
3341
|
+
function dirEndIndex(parts, dirParts) {
|
|
3342
|
+
if (dirParts.length === 0) return 0;
|
|
3343
|
+
for (let i = 0; i + dirParts.length <= parts.length; i++) {
|
|
3344
|
+
let match = true;
|
|
3345
|
+
for (let j = 0; j < dirParts.length; j++) {
|
|
3346
|
+
if (parts[i + j] !== dirParts[j]) {
|
|
3347
|
+
match = false;
|
|
3348
|
+
break;
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
if (match) return i + dirParts.length;
|
|
3352
|
+
}
|
|
3353
|
+
return -1;
|
|
3354
|
+
}
|
|
3355
|
+
function routesStylePath(dirSegs, base) {
|
|
3356
|
+
const fileSegs = splitFlatSegments(base.replace(/\.[^.]+$/, ""));
|
|
3357
|
+
const leaf = fileSegs[fileSegs.length - 1];
|
|
3358
|
+
if (leaf.startsWith("_")) return null;
|
|
3359
|
+
const rawSegs = [...dirSegs, ...fileSegs];
|
|
3360
|
+
if (rawSegs.some((s) => s.startsWith("-"))) return null;
|
|
3361
|
+
if (rawSegs[0] === "api") return null;
|
|
3362
|
+
return formatTanStackPath(rawSegs);
|
|
3363
|
+
}
|
|
3364
|
+
function splitFlatSegments(name) {
|
|
3365
|
+
const segs = [];
|
|
3366
|
+
let cur = "";
|
|
3367
|
+
let depth = 0;
|
|
3368
|
+
for (const ch of name) {
|
|
3369
|
+
if (ch === "[") depth++;
|
|
3370
|
+
else if (ch === "]") depth = Math.max(0, depth - 1);
|
|
3371
|
+
if (ch === "." && depth === 0) {
|
|
3372
|
+
segs.push(cur);
|
|
3373
|
+
cur = "";
|
|
3374
|
+
continue;
|
|
3375
|
+
}
|
|
3376
|
+
cur += ch;
|
|
3377
|
+
}
|
|
3378
|
+
segs.push(cur);
|
|
3379
|
+
return segs;
|
|
3380
|
+
}
|
|
3381
|
+
function formatTanStackPath(segments) {
|
|
3382
|
+
const kept = [];
|
|
3383
|
+
for (const seg of segments) {
|
|
3384
|
+
if (!seg) continue;
|
|
3385
|
+
if (seg === "index" || seg === "route" || seg === "__root") continue;
|
|
3386
|
+
if (seg.startsWith("_")) continue;
|
|
3387
|
+
if (seg.startsWith("(") && seg.endsWith(")")) continue;
|
|
3388
|
+
kept.push(normalizeTanStackSegment(seg.replace(/_$/, "")));
|
|
3389
|
+
}
|
|
3390
|
+
if (kept.length === 0) return "/";
|
|
3391
|
+
return "/" + kept.join("/");
|
|
3392
|
+
}
|
|
3393
|
+
function normalizeTanStackSegment(seg) {
|
|
3394
|
+
if (seg === "$") return "[...splat]";
|
|
3395
|
+
if (seg.startsWith("$")) return `[${seg.slice(1)}]`;
|
|
3396
|
+
return seg.replace(/\[([^\]]*)\]/g, "$1");
|
|
3397
|
+
}
|
|
2622
3398
|
function pathToId(routePath) {
|
|
2623
3399
|
if (routePath === "/") return "root";
|
|
2624
3400
|
return routePath.replace(/^\/+/, "").replace(/\[\.{3}([^\]]+)\]/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -2633,7 +3409,12 @@ var BASE_EXCLUDES = [
|
|
|
2633
3409
|
"**/dist/**",
|
|
2634
3410
|
"**/build/**",
|
|
2635
3411
|
"**/.next/**",
|
|
2636
|
-
|
|
3412
|
+
// Exclude the scanner's own generated siblings so a scan never re-ingests its
|
|
3413
|
+
// output. Enumerated (not `*.gen.*`) so a hand-written file that merely contains
|
|
3414
|
+
// a `.gen.` segment, e.g. `foo.gen.helpers.ts`, is still scanned.
|
|
3415
|
+
"**/*.gen.ts",
|
|
3416
|
+
"**/*.gen.data.ts",
|
|
3417
|
+
"**/*.gen.locs.ts"
|
|
2637
3418
|
];
|
|
2638
3419
|
var TEST_EXCLUDES = [
|
|
2639
3420
|
"**/*.test.ts",
|
|
@@ -2782,6 +3563,18 @@ function buildMetaFromExport(exp) {
|
|
|
2782
3563
|
if (exp.acceptance?.length) meta.acceptance = exp.acceptance;
|
|
2783
3564
|
if (exp.features?.length) meta.features = exp.features;
|
|
2784
3565
|
if (exp.widgets?.length) meta.widgets = exp.widgets;
|
|
3566
|
+
if (exp.states?.length) {
|
|
3567
|
+
meta.states = exp.states.map((s) => ({
|
|
3568
|
+
name: s.name,
|
|
3569
|
+
...s.kind ? { kind: s.kind } : {},
|
|
3570
|
+
...s.acceptance?.length ? { acceptance: s.acceptance } : {},
|
|
3571
|
+
...s.description ? { description: s.description } : {}
|
|
3572
|
+
}));
|
|
3573
|
+
}
|
|
3574
|
+
if (exp.capture === false) meta.capture = false;
|
|
3575
|
+
if (exp.waivers && Object.keys(exp.waivers).length > 0) {
|
|
3576
|
+
meta.waivers = exp.waivers;
|
|
3577
|
+
}
|
|
2785
3578
|
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
2786
3579
|
}
|
|
2787
3580
|
function resolve3(ctx) {
|
|
@@ -2827,9 +3620,12 @@ function resolve3(ctx) {
|
|
|
2827
3620
|
function metaWithComposes(kind, id, base) {
|
|
2828
3621
|
const composes = directChildren.get(`${kind}:${id}`);
|
|
2829
3622
|
if (!composes || composes.length === 0) return base;
|
|
2830
|
-
return { ...base
|
|
3623
|
+
return { ...base, composes };
|
|
2831
3624
|
}
|
|
2832
|
-
const routes = conventions.pages ===
|
|
3625
|
+
const routes = conventions.pages === false ? [] : detectRoutes(
|
|
3626
|
+
ctx.extracted.map((e) => e.file),
|
|
3627
|
+
conventions.pages === "auto" ? {} : { routesDir: conventions.pages.routesDir }
|
|
3628
|
+
);
|
|
2833
3629
|
const handledPageFiles = /* @__PURE__ */ new Set();
|
|
2834
3630
|
for (const route of routes) {
|
|
2835
3631
|
const routeDir = path8.posix.dirname(route.file);
|
|
@@ -3200,6 +3996,68 @@ function dedupe(arr) {
|
|
|
3200
3996
|
}
|
|
3201
3997
|
|
|
3202
3998
|
// src/scanner/scan/pipeline.ts
|
|
3999
|
+
var DEFAULT_STATES_MANIFEST = "uidex-states.json";
|
|
4000
|
+
var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
|
|
4001
|
+
function loadPageBaseline(configDir, config) {
|
|
4002
|
+
const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
4003
|
+
const abs = path9.resolve(configDir, rel);
|
|
4004
|
+
let raw;
|
|
4005
|
+
try {
|
|
4006
|
+
raw = fs6.readFileSync(abs, "utf8");
|
|
4007
|
+
} catch {
|
|
4008
|
+
return void 0;
|
|
4009
|
+
}
|
|
4010
|
+
try {
|
|
4011
|
+
const parsed = JSON.parse(raw);
|
|
4012
|
+
if (!parsed || !Array.isArray(parsed.uncapturedPages)) return void 0;
|
|
4013
|
+
const uncapturedPages = parsed.uncapturedPages.filter(
|
|
4014
|
+
(p2) => typeof p2 === "string"
|
|
4015
|
+
);
|
|
4016
|
+
let missingKinds;
|
|
4017
|
+
const mkRaw = parsed.missingKinds;
|
|
4018
|
+
if (mkRaw && typeof mkRaw === "object" && !Array.isArray(mkRaw)) {
|
|
4019
|
+
const mk = {};
|
|
4020
|
+
for (const [route, kinds] of Object.entries(mkRaw)) {
|
|
4021
|
+
if (Array.isArray(kinds)) {
|
|
4022
|
+
mk[route] = kinds.filter((k) => typeof k === "string");
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
missingKinds = mk;
|
|
4026
|
+
}
|
|
4027
|
+
return { uncapturedPages, ...missingKinds ? { missingKinds } : {} };
|
|
4028
|
+
} catch {
|
|
4029
|
+
return void 0;
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
function loadCapturedStates(configDir, config) {
|
|
4033
|
+
const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
|
|
4034
|
+
const abs = path9.resolve(configDir, rel);
|
|
4035
|
+
let raw;
|
|
4036
|
+
try {
|
|
4037
|
+
raw = fs6.readFileSync(abs, "utf8");
|
|
4038
|
+
} catch {
|
|
4039
|
+
return void 0;
|
|
4040
|
+
}
|
|
4041
|
+
try {
|
|
4042
|
+
const parsed = JSON.parse(raw);
|
|
4043
|
+
if (!parsed || !Array.isArray(parsed.captured)) return void 0;
|
|
4044
|
+
const captured = [];
|
|
4045
|
+
for (const entry of parsed.captured) {
|
|
4046
|
+
if (!entry || typeof entry !== "object") continue;
|
|
4047
|
+
const c = entry;
|
|
4048
|
+
if (typeof c.entity !== "string" || typeof c.state !== "string") continue;
|
|
4049
|
+
const rec = { entity: c.entity, state: c.state };
|
|
4050
|
+
if (typeof c.kind === "string") rec.kind = c.kind;
|
|
4051
|
+
if (typeof c.stateKind === "string") rec.stateKind = c.stateKind;
|
|
4052
|
+
if (typeof c.url === "string") rec.url = c.url;
|
|
4053
|
+
if (typeof c.route === "string") rec.route = c.route;
|
|
4054
|
+
captured.push(rec);
|
|
4055
|
+
}
|
|
4056
|
+
return { captured };
|
|
4057
|
+
} catch {
|
|
4058
|
+
return void 0;
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
3203
4061
|
function runScan(opts = {}) {
|
|
3204
4062
|
const cwd = opts.cwd ?? process.cwd();
|
|
3205
4063
|
const configs = opts.configs ?? discover({ cwd });
|
|
@@ -3226,23 +4084,29 @@ function runOne(dc, opts) {
|
|
|
3226
4084
|
flowFiles: extractedFlows
|
|
3227
4085
|
});
|
|
3228
4086
|
const gitContext = resolveGitContext({ cwd: configDir });
|
|
3229
|
-
const
|
|
4087
|
+
const emitted = emit({
|
|
3230
4088
|
registry: resolved.registry,
|
|
3231
4089
|
gitContext
|
|
3232
4090
|
});
|
|
3233
|
-
const
|
|
3234
|
-
const
|
|
3235
|
-
|
|
4091
|
+
const typesRel = config.output;
|
|
4092
|
+
const typesPath = path9.resolve(configDir, typesRel);
|
|
4093
|
+
const dataRel = dataOutputPath(config.output);
|
|
4094
|
+
const dataPath = path9.resolve(configDir, dataRel);
|
|
4095
|
+
const locsRel = locsOutputPath(config.output);
|
|
4096
|
+
const locsPath = path9.resolve(configDir, locsRel);
|
|
4097
|
+
let typesOnDisk = null;
|
|
4098
|
+
let dataOnDisk = null;
|
|
4099
|
+
let locsOnDisk = null;
|
|
3236
4100
|
if (opts.check) {
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
existingOnDisk = null;
|
|
3241
|
-
}
|
|
4101
|
+
typesOnDisk = readOrNull(typesPath);
|
|
4102
|
+
dataOnDisk = readOrNull(dataPath);
|
|
4103
|
+
locsOnDisk = readOrNull(locsPath);
|
|
3242
4104
|
}
|
|
3243
4105
|
const hasExtractDiagnostics = [...extracted, ...extractedFlows].some(
|
|
3244
4106
|
(ef) => (ef.diagnostics?.length ?? 0) > 0
|
|
3245
4107
|
);
|
|
4108
|
+
const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
|
|
4109
|
+
const pageBaseline = opts.pageBaseline ?? loadPageBaseline(configDir, config);
|
|
3246
4110
|
let auditResult;
|
|
3247
4111
|
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
|
|
3248
4112
|
auditResult = audit({
|
|
@@ -3254,9 +4118,20 @@ function runOne(dc, opts) {
|
|
|
3254
4118
|
check: opts.check,
|
|
3255
4119
|
lint: opts.lint,
|
|
3256
4120
|
resolveDiagnostics: resolved.diagnostics,
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
4121
|
+
// The entity array (and thus the meaningful drift) lives in the data file,
|
|
4122
|
+
// so the entity-aware gen-stale diff runs against it; the types file gets a
|
|
4123
|
+
// plain content compare (see audit()).
|
|
4124
|
+
generated: emitted.data,
|
|
4125
|
+
existingOnDisk: dataOnDisk,
|
|
4126
|
+
outputPath: dataRel,
|
|
4127
|
+
typesGenerated: emitted.types,
|
|
4128
|
+
typesOnDisk,
|
|
4129
|
+
typesOutputPath: typesRel,
|
|
4130
|
+
locsGenerated: emitted.locs,
|
|
4131
|
+
locsOnDisk,
|
|
4132
|
+
locsOutputPath: locsRel,
|
|
4133
|
+
capturedStates,
|
|
4134
|
+
pageBaseline
|
|
3260
4135
|
});
|
|
3261
4136
|
}
|
|
3262
4137
|
return {
|
|
@@ -3265,13 +4140,38 @@ function runOne(dc, opts) {
|
|
|
3265
4140
|
registry: resolved.registry,
|
|
3266
4141
|
gitContext,
|
|
3267
4142
|
audit: auditResult,
|
|
3268
|
-
|
|
3269
|
-
|
|
4143
|
+
types: {
|
|
4144
|
+
generated: emitted.types,
|
|
4145
|
+
outputPath: typesPath,
|
|
4146
|
+
outputRel: typesRel
|
|
4147
|
+
},
|
|
4148
|
+
data: { generated: emitted.data, outputPath: dataPath, outputRel: dataRel },
|
|
4149
|
+
locs: { generated: emitted.locs, outputPath: locsPath, outputRel: locsRel },
|
|
4150
|
+
capturedStates
|
|
3270
4151
|
};
|
|
3271
4152
|
}
|
|
3272
|
-
function
|
|
3273
|
-
|
|
3274
|
-
|
|
4153
|
+
function readOrNull(p2) {
|
|
4154
|
+
try {
|
|
4155
|
+
return fs6.readFileSync(p2, "utf8");
|
|
4156
|
+
} catch {
|
|
4157
|
+
return null;
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
function writeArtifact(artifact) {
|
|
4161
|
+
if (readOrNull(artifact.outputPath) === artifact.generated) return false;
|
|
4162
|
+
fs6.mkdirSync(path9.dirname(artifact.outputPath), { recursive: true });
|
|
4163
|
+
fs6.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
|
|
4164
|
+
return true;
|
|
4165
|
+
}
|
|
4166
|
+
function writeScanResult(result) {
|
|
4167
|
+
const wroteTypes = writeArtifact(result.types);
|
|
4168
|
+
const wroteData = writeArtifact(result.data);
|
|
4169
|
+
const wroteLocs = writeArtifact(result.locs);
|
|
4170
|
+
return wroteTypes || wroteData || wroteLocs;
|
|
4171
|
+
}
|
|
4172
|
+
function pageBaselinePath(result) {
|
|
4173
|
+
const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
4174
|
+
return path9.resolve(result.configDir, rel);
|
|
3275
4175
|
}
|
|
3276
4176
|
|
|
3277
4177
|
// src/scanner/scan/rename.ts
|
|
@@ -3411,7 +4311,7 @@ function renameEntity(opts) {
|
|
|
3411
4311
|
]
|
|
3412
4312
|
};
|
|
3413
4313
|
}
|
|
3414
|
-
const
|
|
4314
|
+
const result = applyFixes([
|
|
3415
4315
|
{
|
|
3416
4316
|
code: "rename",
|
|
3417
4317
|
severity: "info",
|
|
@@ -3422,8 +4322,8 @@ function renameEntity(opts) {
|
|
|
3422
4322
|
}
|
|
3423
4323
|
}
|
|
3424
4324
|
]);
|
|
3425
|
-
if (
|
|
3426
|
-
errors.push(`Some edits were skipped: ${
|
|
4325
|
+
if (result.skipped.length > 0) {
|
|
4326
|
+
errors.push(`Some edits were skipped: ${result.skipped[0].reason}`);
|
|
3427
4327
|
}
|
|
3428
4328
|
for (const r of runScan({ cwd })) writeScanResult(r);
|
|
3429
4329
|
return { edits: edits.length, manual, errors };
|
|
@@ -3522,10 +4422,10 @@ function parseArgs(args) {
|
|
|
3522
4422
|
async function run(opts) {
|
|
3523
4423
|
const cwd = opts.cwd ?? process.cwd();
|
|
3524
4424
|
const { positional, flags } = parseArgs(opts.argv);
|
|
3525
|
-
const
|
|
4425
|
+
const command = positional[0] ?? "help";
|
|
3526
4426
|
const writer = createWriter();
|
|
3527
4427
|
try {
|
|
3528
|
-
switch (
|
|
4428
|
+
switch (command) {
|
|
3529
4429
|
case "help":
|
|
3530
4430
|
case "--help":
|
|
3531
4431
|
case "-h":
|
|
@@ -3540,16 +4440,16 @@ async function run(opts) {
|
|
|
3540
4440
|
case "rename":
|
|
3541
4441
|
return runRename(cwd, positional.slice(1), flags, writer);
|
|
3542
4442
|
case "ai": {
|
|
3543
|
-
const
|
|
4443
|
+
const result = await runAiCommand({
|
|
3544
4444
|
cwd,
|
|
3545
4445
|
argv: opts.argv.slice(1)
|
|
3546
4446
|
});
|
|
3547
|
-
if (
|
|
3548
|
-
if (
|
|
3549
|
-
return writer.result(
|
|
4447
|
+
if (result.stdout) writer.out(result.stdout.replace(/\n$/, ""));
|
|
4448
|
+
if (result.stderr) writer.err(result.stderr.replace(/\n$/, ""));
|
|
4449
|
+
return writer.result(result.exitCode);
|
|
3550
4450
|
}
|
|
3551
4451
|
default:
|
|
3552
|
-
writer.err(`Unknown command: ${
|
|
4452
|
+
writer.err(`Unknown command: ${command}`);
|
|
3553
4453
|
writer.err(helpText2());
|
|
3554
4454
|
return writer.result(1);
|
|
3555
4455
|
}
|
|
@@ -3568,16 +4468,13 @@ function helpText2() {
|
|
|
3568
4468
|
" scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
|
|
3569
4469
|
" rename <element|widget|region> <old-id> <new-id> Rename an id everywhere (DOM attr, flows, exports)",
|
|
3570
4470
|
" ai <install|uninstall|providers> Manage AI assistant integrations",
|
|
3571
|
-
" api <METHOD> <PATH> Call the uidex API",
|
|
3572
|
-
" api --list Show available API routes",
|
|
3573
|
-
" api login Authenticate via browser",
|
|
3574
|
-
" api login --token <tok> Store an auth token directly",
|
|
3575
4471
|
"",
|
|
3576
4472
|
"Flags:",
|
|
3577
4473
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
3578
4474
|
" --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
|
|
3579
4475
|
" --audit Equivalent to --check --lint (read-only)",
|
|
3580
4476
|
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
|
|
4477
|
+
" --update-baseline Regenerate the page-coverage baseline (uidex-coverage-baseline.json) from the current uncaptured routes",
|
|
3581
4478
|
" --json Emit JSON diagnostics on stdout",
|
|
3582
4479
|
" --force (scaffold) overwrite existing spec",
|
|
3583
4480
|
""
|
|
@@ -3597,7 +4494,7 @@ function runInit(cwd, w) {
|
|
|
3597
4494
|
fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3598
4495
|
w.out(`Created ${configPath}`);
|
|
3599
4496
|
const gitignorePath = path11.join(cwd, ".gitignore");
|
|
3600
|
-
const entry = "*.gen
|
|
4497
|
+
const entry = "*.gen.*";
|
|
3601
4498
|
if (fs8.existsSync(gitignorePath)) {
|
|
3602
4499
|
const existing = fs8.readFileSync(gitignorePath, "utf8");
|
|
3603
4500
|
const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
|
|
@@ -3620,8 +4517,9 @@ function runInit(cwd, w) {
|
|
|
3620
4517
|
}
|
|
3621
4518
|
function runScanCommand(cwd, flags, w) {
|
|
3622
4519
|
const fix = Boolean(flags.fix);
|
|
3623
|
-
const
|
|
3624
|
-
const
|
|
4520
|
+
const updateBaseline = Boolean(flags["update-baseline"]);
|
|
4521
|
+
const check = !fix && !updateBaseline && Boolean(flags.check || flags.audit);
|
|
4522
|
+
const lint = Boolean(flags.lint || flags.audit || fix || updateBaseline);
|
|
3625
4523
|
const asJson = Boolean(flags.json);
|
|
3626
4524
|
let configs = discover({ cwd });
|
|
3627
4525
|
if (configs.length === 0) {
|
|
@@ -3632,16 +4530,41 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3632
4530
|
let fixSkipped = [];
|
|
3633
4531
|
if (fix) {
|
|
3634
4532
|
const discovery = runScan({ cwd, check: true, lint: true, configs });
|
|
3635
|
-
const
|
|
4533
|
+
const result = applyFixes(
|
|
3636
4534
|
discovery.flatMap((r) => r.audit?.diagnostics ?? [])
|
|
3637
4535
|
);
|
|
3638
|
-
fixed =
|
|
3639
|
-
fixSkipped =
|
|
4536
|
+
fixed = result.applied;
|
|
4537
|
+
fixSkipped = result.skipped;
|
|
3640
4538
|
configs = discover({ cwd });
|
|
3641
4539
|
}
|
|
3642
4540
|
const results = runScan({ cwd, check, lint, configs });
|
|
4541
|
+
const artifacts = results.flatMap((r) => [r.types, r.data, r.locs]);
|
|
4542
|
+
const wrote = /* @__PURE__ */ new Set();
|
|
3643
4543
|
if (!check) {
|
|
3644
|
-
for (const
|
|
4544
|
+
for (const a of artifacts) {
|
|
4545
|
+
if (writeArtifact(a)) wrote.add(a);
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
if (updateBaseline) {
|
|
4549
|
+
for (const r of results) {
|
|
4550
|
+
const manifest = r.capturedStates ?? { captured: [] };
|
|
4551
|
+
const pageBaseline = computePageBaseline(r.registry, manifest);
|
|
4552
|
+
const missingKinds = computeMissingKinds(r.registry, manifest);
|
|
4553
|
+
const baseline = {
|
|
4554
|
+
...pageBaseline,
|
|
4555
|
+
...Object.keys(missingKinds).length > 0 ? { missingKinds } : {}
|
|
4556
|
+
};
|
|
4557
|
+
const p2 = pageBaselinePath(r);
|
|
4558
|
+
fs8.writeFileSync(p2, JSON.stringify(baseline, null, 2) + "\n");
|
|
4559
|
+
const kindGaps = Object.values(missingKinds).reduce(
|
|
4560
|
+
(n, ks) => n + ks.length,
|
|
4561
|
+
0
|
|
4562
|
+
);
|
|
4563
|
+
w.out(
|
|
4564
|
+
`Wrote ${p2} \u2014 ${baseline.uncapturedPages.length} uncaptured page(s), ${kindGaps} missing core-kind(s) baselined`
|
|
4565
|
+
);
|
|
4566
|
+
}
|
|
4567
|
+
return w.result(0);
|
|
3645
4568
|
}
|
|
3646
4569
|
const allDiagnostics = results.flatMap((r) => r.audit?.diagnostics ?? []);
|
|
3647
4570
|
const summary = results.reduce(
|
|
@@ -3669,10 +4592,14 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3669
4592
|
);
|
|
3670
4593
|
}
|
|
3671
4594
|
for (const r of results) {
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
4595
|
+
for (const art of [r.types, r.data, r.locs]) {
|
|
4596
|
+
if (check) {
|
|
4597
|
+
w.out(`Checked ${art.outputPath}`);
|
|
4598
|
+
} else if (wrote.has(art)) {
|
|
4599
|
+
w.out(`Wrote ${art.outputPath}`);
|
|
4600
|
+
} else {
|
|
4601
|
+
w.out(`Unchanged ${art.outputPath}`);
|
|
4602
|
+
}
|
|
3676
4603
|
}
|
|
3677
4604
|
for (const d of r.audit?.diagnostics ?? []) {
|
|
3678
4605
|
const loc = d.file ? `${d.file}${d.line ? `:${d.line}` : ""}` : "";
|
|
@@ -3708,18 +4635,18 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
3708
4635
|
const entity = r.registry.get(scaffoldKind, id);
|
|
3709
4636
|
if (!entity) continue;
|
|
3710
4637
|
const outDir = path11.resolve(r.configDir, "e2e");
|
|
3711
|
-
const
|
|
4638
|
+
const result = scaffoldSpec({
|
|
3712
4639
|
registry: r.registry,
|
|
3713
4640
|
kind: scaffoldKind,
|
|
3714
4641
|
id,
|
|
3715
4642
|
outDir,
|
|
3716
4643
|
force: Boolean(flags.force)
|
|
3717
4644
|
});
|
|
3718
|
-
if (
|
|
3719
|
-
w.err(
|
|
4645
|
+
if (result.skipped) {
|
|
4646
|
+
w.err(result.reason ?? "skipped");
|
|
3720
4647
|
return w.result(1);
|
|
3721
4648
|
}
|
|
3722
|
-
w.out(`Wrote ${
|
|
4649
|
+
w.out(`Wrote ${result.outputPath}`);
|
|
3723
4650
|
return w.result(0);
|
|
3724
4651
|
}
|
|
3725
4652
|
w.err(
|
|
@@ -3736,24 +4663,24 @@ function runRename(cwd, args, flags, w) {
|
|
|
3736
4663
|
);
|
|
3737
4664
|
return w.result(1);
|
|
3738
4665
|
}
|
|
3739
|
-
const
|
|
4666
|
+
const result = renameEntity({
|
|
3740
4667
|
cwd,
|
|
3741
4668
|
kind,
|
|
3742
4669
|
oldId,
|
|
3743
4670
|
newId,
|
|
3744
4671
|
force: Boolean(flags.force)
|
|
3745
4672
|
});
|
|
3746
|
-
for (const e of
|
|
3747
|
-
for (const m of
|
|
4673
|
+
for (const e of result.errors) w.err(e);
|
|
4674
|
+
for (const m of result.manual) {
|
|
3748
4675
|
w.err(`MANUAL ${m.file}:${m.line} \u2014 ${m.reason}`);
|
|
3749
4676
|
}
|
|
3750
|
-
if (
|
|
4677
|
+
if (result.errors.length > 0) return w.result(1);
|
|
3751
4678
|
w.out(
|
|
3752
|
-
`Renamed ${kind} "${oldId}" \u2192 "${newId}" (${
|
|
4679
|
+
`Renamed ${kind} "${oldId}" \u2192 "${newId}" (${result.edits} edit(s)); gen file regenerated`
|
|
3753
4680
|
);
|
|
3754
|
-
if (
|
|
4681
|
+
if (result.manual.length > 0) {
|
|
3755
4682
|
w.err(
|
|
3756
|
-
`${
|
|
4683
|
+
`${result.manual.length} occurrence(s) need manual follow-up (listed above)`
|
|
3757
4684
|
);
|
|
3758
4685
|
return w.result(1);
|
|
3759
4686
|
}
|
|
@@ -3775,883 +4702,14 @@ function createWriter() {
|
|
|
3775
4702
|
};
|
|
3776
4703
|
}
|
|
3777
4704
|
|
|
3778
|
-
// src/scanner/cli/auth.ts
|
|
3779
|
-
function createMemoryTokenStorage(initial) {
|
|
3780
|
-
let token = initial ?? null;
|
|
3781
|
-
return {
|
|
3782
|
-
get: () => token,
|
|
3783
|
-
set: (t) => {
|
|
3784
|
-
token = t;
|
|
3785
|
-
},
|
|
3786
|
-
clear: () => {
|
|
3787
|
-
token = null;
|
|
3788
|
-
}
|
|
3789
|
-
};
|
|
3790
|
-
}
|
|
3791
|
-
function createFileTokenStorage(options) {
|
|
3792
|
-
const nodeFs = require("fs");
|
|
3793
|
-
const nodePath = require("path");
|
|
3794
|
-
const file = options.path;
|
|
3795
|
-
function read() {
|
|
3796
|
-
if (!nodeFs.existsSync(file)) return null;
|
|
3797
|
-
try {
|
|
3798
|
-
const raw = nodeFs.readFileSync(file, "utf8");
|
|
3799
|
-
const parsed = JSON.parse(raw);
|
|
3800
|
-
if (parsed && typeof parsed === "object") {
|
|
3801
|
-
return parsed;
|
|
3802
|
-
}
|
|
3803
|
-
} catch {
|
|
3804
|
-
return null;
|
|
3805
|
-
}
|
|
3806
|
-
return null;
|
|
3807
|
-
}
|
|
3808
|
-
return {
|
|
3809
|
-
get: () => read()?.token ?? null,
|
|
3810
|
-
set: (token) => {
|
|
3811
|
-
nodeFs.mkdirSync(nodePath.dirname(file), { recursive: true });
|
|
3812
|
-
nodeFs.writeFileSync(file, JSON.stringify({ token }, null, 2));
|
|
3813
|
-
},
|
|
3814
|
-
clear: () => {
|
|
3815
|
-
if (nodeFs.existsSync(file)) nodeFs.unlinkSync(file);
|
|
3816
|
-
}
|
|
3817
|
-
};
|
|
3818
|
-
}
|
|
3819
|
-
function defaultTokenPath() {
|
|
3820
|
-
const os = require("os");
|
|
3821
|
-
const path12 = require("path");
|
|
3822
|
-
return path12.join(os.homedir(), ".uidex", "cloud-token.json");
|
|
3823
|
-
}
|
|
3824
|
-
|
|
3825
|
-
// src/scanner/cli/http.ts
|
|
3826
|
-
function createHttpClient(options) {
|
|
3827
|
-
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
3828
|
-
async function request(path12, opts = {}) {
|
|
3829
|
-
let url = `${baseUrl}${path12.startsWith("/") ? path12 : `/${path12}`}`;
|
|
3830
|
-
if (opts.query) {
|
|
3831
|
-
url += (url.includes("?") ? "&" : "?") + opts.query;
|
|
3832
|
-
}
|
|
3833
|
-
const headers = {
|
|
3834
|
-
Accept: "application/json",
|
|
3835
|
-
...opts.headers
|
|
3836
|
-
};
|
|
3837
|
-
const token = options.token();
|
|
3838
|
-
if (token) headers.Authorization = `Bearer ${token}`;
|
|
3839
|
-
if (opts.body !== void 0) {
|
|
3840
|
-
headers["Content-Type"] ??= "application/json";
|
|
3841
|
-
}
|
|
3842
|
-
const res = await fetch(url, {
|
|
3843
|
-
method: opts.method ?? (opts.body ? "POST" : "GET"),
|
|
3844
|
-
headers,
|
|
3845
|
-
body: opts.body
|
|
3846
|
-
});
|
|
3847
|
-
const raw = await res.text();
|
|
3848
|
-
let body = raw;
|
|
3849
|
-
try {
|
|
3850
|
-
body = JSON.parse(raw);
|
|
3851
|
-
} catch {
|
|
3852
|
-
}
|
|
3853
|
-
return { status: res.status, body, raw };
|
|
3854
|
-
}
|
|
3855
|
-
return { request };
|
|
3856
|
-
}
|
|
3857
|
-
|
|
3858
|
-
// src/scanner/cli/json-highlight.ts
|
|
3859
|
-
var RESET = "\x1B[0m";
|
|
3860
|
-
var BOLD = "\x1B[1m";
|
|
3861
|
-
var DIM = "\x1B[2m";
|
|
3862
|
-
var RED = "\x1B[31m";
|
|
3863
|
-
var CYAN = "\x1B[36m";
|
|
3864
|
-
var GREEN = "\x1B[32m";
|
|
3865
|
-
var YELLOW = "\x1B[33m";
|
|
3866
|
-
var MAGENTA = "\x1B[35m";
|
|
3867
|
-
function highlightJson(value, color) {
|
|
3868
|
-
const json = JSON.stringify(value, null, 2);
|
|
3869
|
-
if (!color) return json;
|
|
3870
|
-
return json.replace(
|
|
3871
|
-
/("(?:\\.|[^"\\])*")\s*:|("(?:\\.|[^"\\])*")|(\b(?:true|false|null)\b)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g,
|
|
3872
|
-
(match, key, str, literal, num) => {
|
|
3873
|
-
if (key) return `${CYAN}${key}${RESET}:`;
|
|
3874
|
-
if (str) return `${GREEN}${str}${RESET}`;
|
|
3875
|
-
if (literal) return `${MAGENTA}${literal}${RESET}`;
|
|
3876
|
-
if (num) return `${YELLOW}${num}${RESET}`;
|
|
3877
|
-
return match;
|
|
3878
|
-
}
|
|
3879
|
-
);
|
|
3880
|
-
}
|
|
3881
|
-
function formatStatus(status, color) {
|
|
3882
|
-
if (!color) return `HTTP ${status}`;
|
|
3883
|
-
const code = status >= 400 ? RED : status >= 300 ? YELLOW : GREEN;
|
|
3884
|
-
return `${code}HTTP ${status}${RESET}`;
|
|
3885
|
-
}
|
|
3886
|
-
function boldText(text, color) {
|
|
3887
|
-
if (!color) return text;
|
|
3888
|
-
return `${BOLD}${text}${RESET}`;
|
|
3889
|
-
}
|
|
3890
|
-
function dimText(text, color) {
|
|
3891
|
-
if (!color) return text;
|
|
3892
|
-
return `${DIM}${text}${RESET}`;
|
|
3893
|
-
}
|
|
3894
|
-
|
|
3895
|
-
// src/scanner/cli/api-routes.gen.ts
|
|
3896
|
-
var API_ROUTES = [
|
|
3897
|
-
{
|
|
3898
|
-
"method": "POST",
|
|
3899
|
-
"path": "/api/ingest",
|
|
3900
|
-
"operationId": "submitReport",
|
|
3901
|
-
"summary": "Submit feedback from the SDK.",
|
|
3902
|
-
"tag": "Ingest",
|
|
3903
|
-
"params": []
|
|
3904
|
-
},
|
|
3905
|
-
{
|
|
3906
|
-
"method": "GET",
|
|
3907
|
-
"path": "/api/ingest/config",
|
|
3908
|
-
"operationId": "getIngestConfig",
|
|
3909
|
-
"summary": "Get integration configuration for the project.",
|
|
3910
|
-
"tag": "Ingest",
|
|
3911
|
-
"params": []
|
|
3912
|
-
},
|
|
3913
|
-
{
|
|
3914
|
-
"method": "GET",
|
|
3915
|
-
"path": "/api/ingest/reports",
|
|
3916
|
-
"operationId": "listIngestReports",
|
|
3917
|
-
"summary": "List feedback for the project (SDK view).",
|
|
3918
|
-
"tag": "Ingest",
|
|
3919
|
-
"params": []
|
|
3920
|
-
},
|
|
3921
|
-
{
|
|
3922
|
-
"method": "GET",
|
|
3923
|
-
"path": "/api/ingest/pins",
|
|
3924
|
-
"operationId": "listPins",
|
|
3925
|
-
"summary": "List pins by route and/or entity.",
|
|
3926
|
-
"tag": "Ingest",
|
|
3927
|
-
"params": []
|
|
3928
|
-
},
|
|
3929
|
-
{
|
|
3930
|
-
"method": "POST",
|
|
3931
|
-
"path": "/api/ingest/pins/archive",
|
|
3932
|
-
"operationId": "archivePin",
|
|
3933
|
-
"summary": "Archive a pin.",
|
|
3934
|
-
"tag": "Ingest",
|
|
3935
|
-
"params": []
|
|
3936
|
-
},
|
|
3937
|
-
{
|
|
3938
|
-
"method": "POST",
|
|
3939
|
-
"path": "/api/cli-auth/authorize",
|
|
3940
|
-
"operationId": "authorizeCli",
|
|
3941
|
-
"summary": "Authorize a CLI session by delivering a token.",
|
|
3942
|
-
"tag": "CLI Auth",
|
|
3943
|
-
"params": []
|
|
3944
|
-
},
|
|
3945
|
-
{
|
|
3946
|
-
"method": "GET",
|
|
3947
|
-
"path": "/api/organizations",
|
|
3948
|
-
"operationId": "listOrganizations",
|
|
3949
|
-
"summary": "List organizations for the current user.",
|
|
3950
|
-
"tag": "Organizations",
|
|
3951
|
-
"params": []
|
|
3952
|
-
},
|
|
3953
|
-
{
|
|
3954
|
-
"method": "POST",
|
|
3955
|
-
"path": "/api/organizations",
|
|
3956
|
-
"operationId": "createOrganization",
|
|
3957
|
-
"summary": "Create an organization.",
|
|
3958
|
-
"tag": "Organizations",
|
|
3959
|
-
"params": []
|
|
3960
|
-
},
|
|
3961
|
-
{
|
|
3962
|
-
"method": "POST",
|
|
3963
|
-
"path": "/api/organizations/switch",
|
|
3964
|
-
"operationId": "switchOrganization",
|
|
3965
|
-
"summary": "Switch the active organization.",
|
|
3966
|
-
"tag": "Organizations",
|
|
3967
|
-
"params": []
|
|
3968
|
-
},
|
|
3969
|
-
{
|
|
3970
|
-
"method": "GET",
|
|
3971
|
-
"path": "/api/organizations/{orgId}",
|
|
3972
|
-
"operationId": "getOrganization",
|
|
3973
|
-
"summary": "Get organization details.",
|
|
3974
|
-
"tag": "Organizations",
|
|
3975
|
-
"params": [
|
|
3976
|
-
"orgId"
|
|
3977
|
-
]
|
|
3978
|
-
},
|
|
3979
|
-
{
|
|
3980
|
-
"method": "PATCH",
|
|
3981
|
-
"path": "/api/organizations/{orgId}",
|
|
3982
|
-
"operationId": "updateOrganization",
|
|
3983
|
-
"summary": "Update an organization.",
|
|
3984
|
-
"tag": "Organizations",
|
|
3985
|
-
"params": [
|
|
3986
|
-
"orgId"
|
|
3987
|
-
]
|
|
3988
|
-
},
|
|
3989
|
-
{
|
|
3990
|
-
"method": "DELETE",
|
|
3991
|
-
"path": "/api/organizations/{orgId}",
|
|
3992
|
-
"operationId": "deleteOrganization",
|
|
3993
|
-
"summary": "Delete an organization.",
|
|
3994
|
-
"tag": "Organizations",
|
|
3995
|
-
"params": [
|
|
3996
|
-
"orgId"
|
|
3997
|
-
]
|
|
3998
|
-
},
|
|
3999
|
-
{
|
|
4000
|
-
"method": "GET",
|
|
4001
|
-
"path": "/api/organizations/{orgId}/members",
|
|
4002
|
-
"operationId": "listMembers",
|
|
4003
|
-
"summary": "List organization members.",
|
|
4004
|
-
"tag": "Members",
|
|
4005
|
-
"params": [
|
|
4006
|
-
"orgId"
|
|
4007
|
-
]
|
|
4008
|
-
},
|
|
4009
|
-
{
|
|
4010
|
-
"method": "DELETE",
|
|
4011
|
-
"path": "/api/organizations/{orgId}/members",
|
|
4012
|
-
"operationId": "removeMember",
|
|
4013
|
-
"summary": "Remove a member from the organization.",
|
|
4014
|
-
"tag": "Members",
|
|
4015
|
-
"params": [
|
|
4016
|
-
"orgId"
|
|
4017
|
-
]
|
|
4018
|
-
},
|
|
4019
|
-
{
|
|
4020
|
-
"method": "PATCH",
|
|
4021
|
-
"path": "/api/organizations/{orgId}/members/{memberId}",
|
|
4022
|
-
"operationId": "updateMember",
|
|
4023
|
-
"summary": "Update a member's role.",
|
|
4024
|
-
"tag": "Members",
|
|
4025
|
-
"params": [
|
|
4026
|
-
"orgId",
|
|
4027
|
-
"memberId"
|
|
4028
|
-
]
|
|
4029
|
-
},
|
|
4030
|
-
{
|
|
4031
|
-
"method": "GET",
|
|
4032
|
-
"path": "/api/organizations/{orgId}/invitations",
|
|
4033
|
-
"operationId": "listInvitations",
|
|
4034
|
-
"summary": "List organization invitations.",
|
|
4035
|
-
"tag": "Invitations",
|
|
4036
|
-
"params": [
|
|
4037
|
-
"orgId"
|
|
4038
|
-
]
|
|
4039
|
-
},
|
|
4040
|
-
{
|
|
4041
|
-
"method": "POST",
|
|
4042
|
-
"path": "/api/organizations/{orgId}/invitations",
|
|
4043
|
-
"operationId": "createInvitation",
|
|
4044
|
-
"summary": "Create an invitation link.",
|
|
4045
|
-
"tag": "Invitations",
|
|
4046
|
-
"params": [
|
|
4047
|
-
"orgId"
|
|
4048
|
-
]
|
|
4049
|
-
},
|
|
4050
|
-
{
|
|
4051
|
-
"method": "DELETE",
|
|
4052
|
-
"path": "/api/organizations/{orgId}/invitations/{inviteId}",
|
|
4053
|
-
"operationId": "deleteInvitation",
|
|
4054
|
-
"summary": "Revoke an invitation.",
|
|
4055
|
-
"tag": "Invitations",
|
|
4056
|
-
"params": [
|
|
4057
|
-
"orgId",
|
|
4058
|
-
"inviteId"
|
|
4059
|
-
]
|
|
4060
|
-
},
|
|
4061
|
-
{
|
|
4062
|
-
"method": "GET",
|
|
4063
|
-
"path": "/api/organizations/{orgId}/projects",
|
|
4064
|
-
"operationId": "listProjects",
|
|
4065
|
-
"summary": "List projects in an organization.",
|
|
4066
|
-
"tag": "Projects",
|
|
4067
|
-
"params": [
|
|
4068
|
-
"orgId"
|
|
4069
|
-
]
|
|
4070
|
-
},
|
|
4071
|
-
{
|
|
4072
|
-
"method": "POST",
|
|
4073
|
-
"path": "/api/organizations/{orgId}/projects",
|
|
4074
|
-
"operationId": "createProject",
|
|
4075
|
-
"summary": "Create a project.",
|
|
4076
|
-
"tag": "Projects",
|
|
4077
|
-
"params": [
|
|
4078
|
-
"orgId"
|
|
4079
|
-
]
|
|
4080
|
-
},
|
|
4081
|
-
{
|
|
4082
|
-
"method": "GET",
|
|
4083
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}",
|
|
4084
|
-
"operationId": "getProject",
|
|
4085
|
-
"summary": "Get project details.",
|
|
4086
|
-
"tag": "Projects",
|
|
4087
|
-
"params": [
|
|
4088
|
-
"orgId",
|
|
4089
|
-
"projectId"
|
|
4090
|
-
]
|
|
4091
|
-
},
|
|
4092
|
-
{
|
|
4093
|
-
"method": "PATCH",
|
|
4094
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}",
|
|
4095
|
-
"operationId": "updateProject",
|
|
4096
|
-
"summary": "Update a project.",
|
|
4097
|
-
"tag": "Projects",
|
|
4098
|
-
"params": [
|
|
4099
|
-
"orgId",
|
|
4100
|
-
"projectId"
|
|
4101
|
-
]
|
|
4102
|
-
},
|
|
4103
|
-
{
|
|
4104
|
-
"method": "DELETE",
|
|
4105
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}",
|
|
4106
|
-
"operationId": "deleteProject",
|
|
4107
|
-
"summary": "Delete a project.",
|
|
4108
|
-
"tag": "Projects",
|
|
4109
|
-
"params": [
|
|
4110
|
-
"orgId",
|
|
4111
|
-
"projectId"
|
|
4112
|
-
]
|
|
4113
|
-
},
|
|
4114
|
-
{
|
|
4115
|
-
"method": "GET",
|
|
4116
|
-
"path": "/api/projects/resolve",
|
|
4117
|
-
"operationId": "resolveProject",
|
|
4118
|
-
"summary": "Resolve an API key to its project and organization.",
|
|
4119
|
-
"tag": "Projects",
|
|
4120
|
-
"params": []
|
|
4121
|
-
},
|
|
4122
|
-
{
|
|
4123
|
-
"method": "GET",
|
|
4124
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/api-keys",
|
|
4125
|
-
"operationId": "listApiKeys",
|
|
4126
|
-
"summary": "List API keys for a project.",
|
|
4127
|
-
"tag": "API Keys",
|
|
4128
|
-
"params": [
|
|
4129
|
-
"orgId",
|
|
4130
|
-
"projectId"
|
|
4131
|
-
]
|
|
4132
|
-
},
|
|
4133
|
-
{
|
|
4134
|
-
"method": "POST",
|
|
4135
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/api-keys",
|
|
4136
|
-
"operationId": "createApiKey",
|
|
4137
|
-
"summary": "Create an API key.",
|
|
4138
|
-
"tag": "API Keys",
|
|
4139
|
-
"params": [
|
|
4140
|
-
"orgId",
|
|
4141
|
-
"projectId"
|
|
4142
|
-
]
|
|
4143
|
-
},
|
|
4144
|
-
{
|
|
4145
|
-
"method": "PATCH",
|
|
4146
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/api-keys/{keyId}",
|
|
4147
|
-
"operationId": "revokeApiKey",
|
|
4148
|
-
"summary": "Revoke an API key.",
|
|
4149
|
-
"tag": "API Keys",
|
|
4150
|
-
"params": [
|
|
4151
|
-
"orgId",
|
|
4152
|
-
"projectId",
|
|
4153
|
-
"keyId"
|
|
4154
|
-
]
|
|
4155
|
-
},
|
|
4156
|
-
{
|
|
4157
|
-
"method": "DELETE",
|
|
4158
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/api-keys/{keyId}",
|
|
4159
|
-
"operationId": "deleteApiKey",
|
|
4160
|
-
"summary": "Permanently delete an API key.",
|
|
4161
|
-
"tag": "API Keys",
|
|
4162
|
-
"params": [
|
|
4163
|
-
"orgId",
|
|
4164
|
-
"projectId",
|
|
4165
|
-
"keyId"
|
|
4166
|
-
]
|
|
4167
|
-
},
|
|
4168
|
-
{
|
|
4169
|
-
"method": "GET",
|
|
4170
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/reports",
|
|
4171
|
-
"operationId": "listProjectReports",
|
|
4172
|
-
"summary": "List feedback for a project.",
|
|
4173
|
-
"tag": "Reports",
|
|
4174
|
-
"params": [
|
|
4175
|
-
"orgId",
|
|
4176
|
-
"projectId"
|
|
4177
|
-
]
|
|
4178
|
-
},
|
|
4179
|
-
{
|
|
4180
|
-
"method": "GET",
|
|
4181
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}",
|
|
4182
|
-
"operationId": "getReport",
|
|
4183
|
-
"summary": "Get feedback details.",
|
|
4184
|
-
"tag": "Reports",
|
|
4185
|
-
"params": [
|
|
4186
|
-
"orgId",
|
|
4187
|
-
"projectId",
|
|
4188
|
-
"reportId"
|
|
4189
|
-
]
|
|
4190
|
-
},
|
|
4191
|
-
{
|
|
4192
|
-
"method": "PATCH",
|
|
4193
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}",
|
|
4194
|
-
"operationId": "updateReport",
|
|
4195
|
-
"summary": "Update feedback status, priority, or assignment.",
|
|
4196
|
-
"tag": "Reports",
|
|
4197
|
-
"params": [
|
|
4198
|
-
"orgId",
|
|
4199
|
-
"projectId",
|
|
4200
|
-
"reportId"
|
|
4201
|
-
]
|
|
4202
|
-
},
|
|
4203
|
-
{
|
|
4204
|
-
"method": "DELETE",
|
|
4205
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}",
|
|
4206
|
-
"operationId": "deleteReport",
|
|
4207
|
-
"summary": "Delete a feedback record.",
|
|
4208
|
-
"tag": "Reports",
|
|
4209
|
-
"params": [
|
|
4210
|
-
"orgId",
|
|
4211
|
-
"projectId",
|
|
4212
|
-
"reportId"
|
|
4213
|
-
]
|
|
4214
|
-
},
|
|
4215
|
-
{
|
|
4216
|
-
"method": "POST",
|
|
4217
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/reports/bulk-status",
|
|
4218
|
-
"operationId": "bulkUpdateReportStatus",
|
|
4219
|
-
"summary": "Update status on multiple feedback records.",
|
|
4220
|
-
"tag": "Reports",
|
|
4221
|
-
"params": [
|
|
4222
|
-
"orgId",
|
|
4223
|
-
"projectId"
|
|
4224
|
-
]
|
|
4225
|
-
},
|
|
4226
|
-
{
|
|
4227
|
-
"method": "GET",
|
|
4228
|
-
"path": "/api/organizations/{orgId}/integrations",
|
|
4229
|
-
"operationId": "listIntegrations",
|
|
4230
|
-
"summary": "List integrations for an organization.",
|
|
4231
|
-
"tag": "Integrations",
|
|
4232
|
-
"params": [
|
|
4233
|
-
"orgId"
|
|
4234
|
-
]
|
|
4235
|
-
},
|
|
4236
|
-
{
|
|
4237
|
-
"method": "POST",
|
|
4238
|
-
"path": "/api/organizations/{orgId}/integrations",
|
|
4239
|
-
"operationId": "createIntegration",
|
|
4240
|
-
"summary": "Create an integration.",
|
|
4241
|
-
"tag": "Integrations",
|
|
4242
|
-
"params": [
|
|
4243
|
-
"orgId"
|
|
4244
|
-
]
|
|
4245
|
-
},
|
|
4246
|
-
{
|
|
4247
|
-
"method": "GET",
|
|
4248
|
-
"path": "/api/organizations/{orgId}/integrations/{integrationId}",
|
|
4249
|
-
"operationId": "getIntegration",
|
|
4250
|
-
"summary": "Get integration details.",
|
|
4251
|
-
"tag": "Integrations",
|
|
4252
|
-
"params": [
|
|
4253
|
-
"orgId",
|
|
4254
|
-
"integrationId"
|
|
4255
|
-
]
|
|
4256
|
-
},
|
|
4257
|
-
{
|
|
4258
|
-
"method": "DELETE",
|
|
4259
|
-
"path": "/api/organizations/{orgId}/integrations/{integrationId}",
|
|
4260
|
-
"operationId": "deleteIntegration",
|
|
4261
|
-
"summary": "Delete an integration.",
|
|
4262
|
-
"tag": "Integrations",
|
|
4263
|
-
"params": [
|
|
4264
|
-
"orgId",
|
|
4265
|
-
"integrationId"
|
|
4266
|
-
]
|
|
4267
|
-
},
|
|
4268
|
-
{
|
|
4269
|
-
"method": "GET",
|
|
4270
|
-
"path": "/api/organizations/{orgId}/integrations/{integrationId}/targets",
|
|
4271
|
-
"operationId": "listIntegrationTargets",
|
|
4272
|
-
"summary": "List available targets (e.g. Jira projects/boards).",
|
|
4273
|
-
"tag": "Integrations",
|
|
4274
|
-
"params": [
|
|
4275
|
-
"orgId",
|
|
4276
|
-
"integrationId"
|
|
4277
|
-
]
|
|
4278
|
-
},
|
|
4279
|
-
{
|
|
4280
|
-
"method": "POST",
|
|
4281
|
-
"path": "/api/organizations/{orgId}/integrations/{integrationId}/test",
|
|
4282
|
-
"operationId": "testIntegration",
|
|
4283
|
-
"summary": "Test integration connectivity.",
|
|
4284
|
-
"tag": "Integrations",
|
|
4285
|
-
"params": [
|
|
4286
|
-
"orgId",
|
|
4287
|
-
"integrationId"
|
|
4288
|
-
]
|
|
4289
|
-
},
|
|
4290
|
-
{
|
|
4291
|
-
"method": "GET",
|
|
4292
|
-
"path": "/api/organizations/{orgId}/external-issues",
|
|
4293
|
-
"operationId": "listExternalIssues",
|
|
4294
|
-
"summary": "List external issues, optionally filtered by feedback.",
|
|
4295
|
-
"tag": "External Issues",
|
|
4296
|
-
"params": [
|
|
4297
|
-
"orgId"
|
|
4298
|
-
]
|
|
4299
|
-
},
|
|
4300
|
-
{
|
|
4301
|
-
"method": "GET",
|
|
4302
|
-
"path": "/api/organizations/{orgId}/issue-drafts",
|
|
4303
|
-
"operationId": "listIssueDrafts",
|
|
4304
|
-
"summary": "List issue drafts for a project.",
|
|
4305
|
-
"tag": "Issue Drafts",
|
|
4306
|
-
"params": [
|
|
4307
|
-
"orgId"
|
|
4308
|
-
]
|
|
4309
|
-
},
|
|
4310
|
-
{
|
|
4311
|
-
"method": "POST",
|
|
4312
|
-
"path": "/api/organizations/{orgId}/issue-drafts",
|
|
4313
|
-
"operationId": "createIssueDraft",
|
|
4314
|
-
"summary": "Manually create an issue draft.",
|
|
4315
|
-
"tag": "Issue Drafts",
|
|
4316
|
-
"params": [
|
|
4317
|
-
"orgId"
|
|
4318
|
-
]
|
|
4319
|
-
},
|
|
4320
|
-
{
|
|
4321
|
-
"method": "PATCH",
|
|
4322
|
-
"path": "/api/organizations/{orgId}/issue-drafts/{draftId}",
|
|
4323
|
-
"operationId": "updateIssueDraft",
|
|
4324
|
-
"summary": "Update an issue draft.",
|
|
4325
|
-
"tag": "Issue Drafts",
|
|
4326
|
-
"params": [
|
|
4327
|
-
"orgId",
|
|
4328
|
-
"draftId"
|
|
4329
|
-
]
|
|
4330
|
-
},
|
|
4331
|
-
{
|
|
4332
|
-
"method": "DELETE",
|
|
4333
|
-
"path": "/api/organizations/{orgId}/issue-drafts/{draftId}",
|
|
4334
|
-
"operationId": "deleteIssueDraft",
|
|
4335
|
-
"summary": "Delete an issue draft.",
|
|
4336
|
-
"tag": "Issue Drafts",
|
|
4337
|
-
"params": [
|
|
4338
|
-
"orgId",
|
|
4339
|
-
"draftId"
|
|
4340
|
-
]
|
|
4341
|
-
},
|
|
4342
|
-
{
|
|
4343
|
-
"method": "POST",
|
|
4344
|
-
"path": "/api/organizations/{orgId}/issue-drafts/{draftId}/submit",
|
|
4345
|
-
"operationId": "submitIssueDraft",
|
|
4346
|
-
"summary": "Submit a draft to the configured integration.",
|
|
4347
|
-
"tag": "Issue Drafts",
|
|
4348
|
-
"params": [
|
|
4349
|
-
"orgId",
|
|
4350
|
-
"draftId"
|
|
4351
|
-
]
|
|
4352
|
-
},
|
|
4353
|
-
{
|
|
4354
|
-
"method": "POST",
|
|
4355
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/triage",
|
|
4356
|
-
"operationId": "runTriage",
|
|
4357
|
-
"summary": "Run LLM-powered triage on untriaged feedback.",
|
|
4358
|
-
"tag": "Triage",
|
|
4359
|
-
"params": [
|
|
4360
|
-
"orgId",
|
|
4361
|
-
"projectId"
|
|
4362
|
-
]
|
|
4363
|
-
},
|
|
4364
|
-
{
|
|
4365
|
-
"method": "GET",
|
|
4366
|
-
"path": "/api/user/tokens",
|
|
4367
|
-
"operationId": "listUserTokens",
|
|
4368
|
-
"summary": "List personal access tokens.",
|
|
4369
|
-
"tag": "User Tokens",
|
|
4370
|
-
"params": []
|
|
4371
|
-
},
|
|
4372
|
-
{
|
|
4373
|
-
"method": "POST",
|
|
4374
|
-
"path": "/api/user/tokens",
|
|
4375
|
-
"operationId": "createUserToken",
|
|
4376
|
-
"summary": "Create a personal access token.",
|
|
4377
|
-
"tag": "User Tokens",
|
|
4378
|
-
"params": []
|
|
4379
|
-
},
|
|
4380
|
-
{
|
|
4381
|
-
"method": "DELETE",
|
|
4382
|
-
"path": "/api/user/tokens/{tokenId}",
|
|
4383
|
-
"operationId": "deleteUserToken",
|
|
4384
|
-
"summary": "Revoke and delete a personal access token.",
|
|
4385
|
-
"tag": "User Tokens",
|
|
4386
|
-
"params": [
|
|
4387
|
-
"tokenId"
|
|
4388
|
-
]
|
|
4389
|
-
}
|
|
4390
|
-
];
|
|
4391
|
-
|
|
4392
|
-
// src/scanner/cli/api.ts
|
|
4393
|
-
var METHODS = /* @__PURE__ */ new Set([
|
|
4394
|
-
"GET",
|
|
4395
|
-
"POST",
|
|
4396
|
-
"PUT",
|
|
4397
|
-
"PATCH",
|
|
4398
|
-
"DELETE",
|
|
4399
|
-
"HEAD",
|
|
4400
|
-
"OPTIONS"
|
|
4401
|
-
]);
|
|
4402
|
-
var HELP = `uidex api \u2014 call the uidex API
|
|
4403
|
-
|
|
4404
|
-
Usage:
|
|
4405
|
-
uidex api <METHOD> <PATH> [flags] Make an API request
|
|
4406
|
-
uidex api --list [--tag <tag>] List available routes
|
|
4407
|
-
uidex api login Authenticate via browser
|
|
4408
|
-
uidex api login --token <tok> Store an auth token directly
|
|
4409
|
-
uidex api status Check auth state
|
|
4410
|
-
|
|
4411
|
-
Flags:
|
|
4412
|
-
--body <json> Request body (JSON string)
|
|
4413
|
-
--query <params> Query string (key=val&key2=val2)
|
|
4414
|
-
--base-url <url> Override the API base URL
|
|
4415
|
-
--token <tok> Use a specific token (instead of stored)
|
|
4416
|
-
--raw Disable JSON pretty-printing
|
|
4417
|
-
--no-color Disable colored output
|
|
4418
|
-
--list List available API routes
|
|
4419
|
-
--tag <tag> Filter --list by tag
|
|
4420
|
-
--json Emit --list as JSON
|
|
4421
|
-
`;
|
|
4422
|
-
async function runApiCommand(opts) {
|
|
4423
|
-
const { positional, flags } = parseArgs(opts.argv);
|
|
4424
|
-
const stdout = [];
|
|
4425
|
-
const stderr = [];
|
|
4426
|
-
const color = opts.color ?? (flags["no-color"] ? false : process.stdout.isTTY ?? false);
|
|
4427
|
-
const ctx = { flags, opts, color, stdout, stderr };
|
|
4428
|
-
const sub = positional[0];
|
|
4429
|
-
if (!sub && !flags.list) {
|
|
4430
|
-
stdout.push(HELP);
|
|
4431
|
-
return result(0, stdout, stderr);
|
|
4432
|
-
}
|
|
4433
|
-
if (flags.list) return listRoutes(ctx);
|
|
4434
|
-
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
4435
|
-
stdout.push(HELP);
|
|
4436
|
-
return result(0, stdout, stderr);
|
|
4437
|
-
}
|
|
4438
|
-
if (sub === "login") return runLogin(ctx);
|
|
4439
|
-
if (sub === "status") return runStatus(ctx);
|
|
4440
|
-
const method = sub?.toUpperCase();
|
|
4441
|
-
const path12 = positional[1];
|
|
4442
|
-
if (!method || !METHODS.has(method)) {
|
|
4443
|
-
stderr.push(`Unknown command or method: ${sub}`);
|
|
4444
|
-
stderr.push(HELP);
|
|
4445
|
-
return result(1, stdout, stderr);
|
|
4446
|
-
}
|
|
4447
|
-
if (!path12) {
|
|
4448
|
-
stderr.push("Missing path. Usage: uidex api GET /api/organizations");
|
|
4449
|
-
return result(1, stdout, stderr);
|
|
4450
|
-
}
|
|
4451
|
-
return runRequest(ctx, method, path12);
|
|
4452
|
-
}
|
|
4453
|
-
function listRoutes(ctx) {
|
|
4454
|
-
const { flags, color, stdout, stderr } = ctx;
|
|
4455
|
-
let routes = API_ROUTES;
|
|
4456
|
-
const tagFilter = flags.tag;
|
|
4457
|
-
if (typeof tagFilter === "string") {
|
|
4458
|
-
routes = routes.filter(
|
|
4459
|
-
(r) => r.tag.toLowerCase() === tagFilter.toLowerCase()
|
|
4460
|
-
);
|
|
4461
|
-
}
|
|
4462
|
-
if (flags.json) {
|
|
4463
|
-
stdout.push(JSON.stringify(routes, null, 2));
|
|
4464
|
-
return result(0, stdout, stderr);
|
|
4465
|
-
}
|
|
4466
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
4467
|
-
for (const r of routes) {
|
|
4468
|
-
const tag = r.tag || "Other";
|
|
4469
|
-
let list = grouped.get(tag);
|
|
4470
|
-
if (!list) {
|
|
4471
|
-
list = [];
|
|
4472
|
-
grouped.set(tag, list);
|
|
4473
|
-
}
|
|
4474
|
-
list.push(r);
|
|
4475
|
-
}
|
|
4476
|
-
const methodPad = 7;
|
|
4477
|
-
for (const [tag, tagRoutes] of grouped) {
|
|
4478
|
-
stdout.push("");
|
|
4479
|
-
stdout.push(boldText(tag, color));
|
|
4480
|
-
for (const r of tagRoutes) {
|
|
4481
|
-
const m = r.method.padEnd(methodPad);
|
|
4482
|
-
const desc = r.summary ? dimText(` ${r.operationId} \u2014 ${r.summary}`, color) : dimText(` ${r.operationId}`, color);
|
|
4483
|
-
stdout.push(` ${m} ${r.path}${desc}`);
|
|
4484
|
-
}
|
|
4485
|
-
}
|
|
4486
|
-
stdout.push("");
|
|
4487
|
-
return result(0, stdout, stderr);
|
|
4488
|
-
}
|
|
4489
|
-
async function runLogin(ctx) {
|
|
4490
|
-
const { flags, opts, stdout, stderr } = ctx;
|
|
4491
|
-
const token = flags.token;
|
|
4492
|
-
if (typeof token === "string" && token.length > 0) {
|
|
4493
|
-
const storage = resolveTokenStorage(opts);
|
|
4494
|
-
storage.set(token);
|
|
4495
|
-
stdout.push("Token stored.");
|
|
4496
|
-
return result(0, stdout, stderr);
|
|
4497
|
-
}
|
|
4498
|
-
return runBrowserLogin(ctx);
|
|
4499
|
-
}
|
|
4500
|
-
async function runBrowserLogin(ctx) {
|
|
4501
|
-
const { flags, opts, color, stdout, stderr } = ctx;
|
|
4502
|
-
const http = await import("http");
|
|
4503
|
-
const crypto = await import("crypto");
|
|
4504
|
-
const { exec } = await import("child_process");
|
|
4505
|
-
const baseUrl = resolveBaseUrl(flags, opts);
|
|
4506
|
-
const code = crypto.randomBytes(3).toString("hex").toUpperCase();
|
|
4507
|
-
return new Promise((resolve7) => {
|
|
4508
|
-
let settled = false;
|
|
4509
|
-
const settle = (exitCode) => {
|
|
4510
|
-
if (settled) return;
|
|
4511
|
-
settled = true;
|
|
4512
|
-
server.close();
|
|
4513
|
-
clearTimeout(timeout);
|
|
4514
|
-
resolve7(result(exitCode, stdout, stderr));
|
|
4515
|
-
};
|
|
4516
|
-
const timeout = setTimeout(() => {
|
|
4517
|
-
stderr.push("Timed out waiting for browser authorization.");
|
|
4518
|
-
settle(1);
|
|
4519
|
-
}, 12e4);
|
|
4520
|
-
const server = http.createServer((req, res) => {
|
|
4521
|
-
if (!req.url?.startsWith("/callback")) {
|
|
4522
|
-
res.writeHead(404);
|
|
4523
|
-
res.end();
|
|
4524
|
-
return;
|
|
4525
|
-
}
|
|
4526
|
-
const url = new URL(req.url, `http://127.0.0.1`);
|
|
4527
|
-
const receivedToken = url.searchParams.get("token");
|
|
4528
|
-
const receivedCode = url.searchParams.get("code");
|
|
4529
|
-
if (receivedCode !== code) {
|
|
4530
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4531
|
-
res.end(JSON.stringify({ error: "Code mismatch" }));
|
|
4532
|
-
stderr.push("Received callback with mismatched code. Aborting.");
|
|
4533
|
-
settle(1);
|
|
4534
|
-
return;
|
|
4535
|
-
}
|
|
4536
|
-
if (!receivedToken) {
|
|
4537
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4538
|
-
res.end(JSON.stringify({ error: "Missing token" }));
|
|
4539
|
-
stderr.push("Received callback without token. Aborting.");
|
|
4540
|
-
settle(1);
|
|
4541
|
-
return;
|
|
4542
|
-
}
|
|
4543
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4544
|
-
res.end(JSON.stringify({ ok: true }));
|
|
4545
|
-
const storage = resolveTokenStorage(opts);
|
|
4546
|
-
storage.set(receivedToken);
|
|
4547
|
-
stdout.push(dimText("Authenticated successfully.", color));
|
|
4548
|
-
settle(0);
|
|
4549
|
-
});
|
|
4550
|
-
server.listen(0, "127.0.0.1", () => {
|
|
4551
|
-
const addr = server.address();
|
|
4552
|
-
if (!addr || typeof addr === "string") {
|
|
4553
|
-
stderr.push("Failed to start local server.");
|
|
4554
|
-
settle(1);
|
|
4555
|
-
return;
|
|
4556
|
-
}
|
|
4557
|
-
const port = addr.port;
|
|
4558
|
-
const authUrl = `${baseUrl}/cli-auth?code=${encodeURIComponent(code)}&port=${port}`;
|
|
4559
|
-
stdout.push(dimText("Opening browser to authorize...", color));
|
|
4560
|
-
stdout.push("");
|
|
4561
|
-
stdout.push(` Confirmation code: ${boldText(code, color)}`);
|
|
4562
|
-
stdout.push("");
|
|
4563
|
-
stdout.push(
|
|
4564
|
-
dimText("If the browser doesn't open, visit this URL manually:", color)
|
|
4565
|
-
);
|
|
4566
|
-
stdout.push(` ${authUrl}`);
|
|
4567
|
-
stdout.push("");
|
|
4568
|
-
process.stdout.write(result(0, stdout, stderr).stdout);
|
|
4569
|
-
stdout.length = 0;
|
|
4570
|
-
openBrowser(authUrl, exec);
|
|
4571
|
-
});
|
|
4572
|
-
server.on("error", (err2) => {
|
|
4573
|
-
stderr.push(`Failed to start local server: ${err2.message}`);
|
|
4574
|
-
settle(1);
|
|
4575
|
-
});
|
|
4576
|
-
});
|
|
4577
|
-
}
|
|
4578
|
-
function openBrowser(url, exec) {
|
|
4579
|
-
const platform = process.platform;
|
|
4580
|
-
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
4581
|
-
exec(`${cmd} ${JSON.stringify(url)}`);
|
|
4582
|
-
}
|
|
4583
|
-
function runStatus(ctx) {
|
|
4584
|
-
const { flags, opts, stdout, stderr } = ctx;
|
|
4585
|
-
const storage = resolveTokenStorage(opts);
|
|
4586
|
-
const baseUrl = resolveBaseUrl(flags, opts);
|
|
4587
|
-
const token = storage.get();
|
|
4588
|
-
stdout.push(`baseUrl: ${baseUrl}`);
|
|
4589
|
-
stdout.push(`auth: ${token ? "authenticated" : "not authenticated"}`);
|
|
4590
|
-
return result(token ? 0 : 1, stdout, stderr);
|
|
4591
|
-
}
|
|
4592
|
-
async function runRequest(ctx, method, path12) {
|
|
4593
|
-
const { flags, opts, color, stdout, stderr } = ctx;
|
|
4594
|
-
const tokenFromFlag = flags.token;
|
|
4595
|
-
const token = typeof tokenFromFlag === "string" ? tokenFromFlag : resolveTokenStorage(opts).get();
|
|
4596
|
-
if (!token) {
|
|
4597
|
-
stderr.push("Not authenticated. Run: uidex api login --token <value>");
|
|
4598
|
-
return result(1, stdout, stderr);
|
|
4599
|
-
}
|
|
4600
|
-
const baseUrl = resolveBaseUrl(flags, opts);
|
|
4601
|
-
const http = opts.http ?? createHttpClient({ baseUrl, token: () => token });
|
|
4602
|
-
const bodyStr = typeof flags.body === "string" ? flags.body : void 0;
|
|
4603
|
-
if (bodyStr !== void 0) {
|
|
4604
|
-
try {
|
|
4605
|
-
JSON.parse(bodyStr);
|
|
4606
|
-
} catch {
|
|
4607
|
-
stderr.push("Invalid JSON in --body");
|
|
4608
|
-
return result(1, stdout, stderr);
|
|
4609
|
-
}
|
|
4610
|
-
}
|
|
4611
|
-
const res = await http.request(path12, {
|
|
4612
|
-
method,
|
|
4613
|
-
body: bodyStr,
|
|
4614
|
-
query: typeof flags.query === "string" ? flags.query : void 0
|
|
4615
|
-
});
|
|
4616
|
-
const isSuccess = res.status >= 200 && res.status < 300;
|
|
4617
|
-
if (!isSuccess) {
|
|
4618
|
-
stderr.push(formatStatus(res.status, color));
|
|
4619
|
-
}
|
|
4620
|
-
if (res.body !== void 0 && res.body !== null && res.body !== "") {
|
|
4621
|
-
if (flags.raw) {
|
|
4622
|
-
stdout.push(res.raw);
|
|
4623
|
-
} else {
|
|
4624
|
-
stdout.push(highlightJson(res.body, color));
|
|
4625
|
-
}
|
|
4626
|
-
}
|
|
4627
|
-
return result(isSuccess ? 0 : 1, stdout, stderr);
|
|
4628
|
-
}
|
|
4629
|
-
function resolveTokenStorage(opts) {
|
|
4630
|
-
if (opts.tokenStorage) return opts.tokenStorage;
|
|
4631
|
-
const envToken = opts.env?.UIDEX_TOKEN;
|
|
4632
|
-
if (envToken) return createMemoryTokenStorage(envToken);
|
|
4633
|
-
return createFileTokenStorage({ path: defaultTokenPath() });
|
|
4634
|
-
}
|
|
4635
|
-
function resolveBaseUrl(flags, opts) {
|
|
4636
|
-
return (typeof flags["base-url"] === "string" ? flags["base-url"] : void 0) ?? opts.baseUrl ?? opts.env?.UIDEX_API_URL ?? "https://app.uidex.dev";
|
|
4637
|
-
}
|
|
4638
|
-
function result(exitCode, stdout, stderr) {
|
|
4639
|
-
return {
|
|
4640
|
-
exitCode,
|
|
4641
|
-
stdout: stdout.join("\n") + (stdout.length ? "\n" : ""),
|
|
4642
|
-
stderr: stderr.join("\n") + (stderr.length ? "\n" : "")
|
|
4643
|
-
};
|
|
4644
|
-
}
|
|
4645
|
-
|
|
4646
4705
|
// src/scanner/cli/cli.ts
|
|
4647
4706
|
var argv = process.argv.slice(2);
|
|
4648
|
-
var
|
|
4649
|
-
var resultP = command === "api" ? runApiCommand({ argv: argv.slice(1) }) : run({ argv });
|
|
4707
|
+
var resultP = run({ argv });
|
|
4650
4708
|
resultP.then(
|
|
4651
|
-
(
|
|
4652
|
-
if (
|
|
4653
|
-
if (
|
|
4654
|
-
process.exit(
|
|
4709
|
+
(result) => {
|
|
4710
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
4711
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
4712
|
+
process.exit(result.exitCode);
|
|
4655
4713
|
},
|
|
4656
4714
|
(error) => {
|
|
4657
4715
|
process.stderr.write(
|