uidex 0.6.0 → 0.7.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/README.md +3 -3
- package/dist/cli/cli.cjs +1510 -1244
- package/dist/cli/cli.cjs.map +1 -1
- package/dist/cloud/index.cjs +385 -175
- package/dist/cloud/index.cjs.map +1 -1
- package/dist/cloud/index.d.cts +192 -4
- package/dist/cloud/index.d.ts +192 -4
- package/dist/cloud/index.js +377 -177
- package/dist/cloud/index.js.map +1 -1
- package/dist/headless/index.cjs +82 -255
- package/dist/headless/index.cjs.map +1 -1
- package/dist/headless/index.d.cts +5 -11
- package/dist/headless/index.d.ts +5 -11
- package/dist/headless/index.js +82 -257
- package/dist/headless/index.js.map +1 -1
- package/dist/index.cjs +721 -1053
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +149 -160
- package/dist/index.d.ts +149 -160
- package/dist/index.js +741 -1068
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +729 -1000
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +99 -86
- package/dist/react/index.d.ts +99 -86
- package/dist/react/index.js +745 -1015
- package/dist/react/index.js.map +1 -1
- package/dist/scan/index.cjs +1518 -1237
- package/dist/scan/index.cjs.map +1 -1
- package/dist/scan/index.d.cts +209 -12
- package/dist/scan/index.d.ts +209 -12
- package/dist/scan/index.js +1515 -1236
- package/dist/scan/index.js.map +1 -1
- package/package.json +22 -21
- package/templates/claude/SKILL.md +71 -0
- package/templates/claude/references/audit.md +43 -0
- package/templates/claude/{rules.md → references/conventions.md} +25 -28
- package/templates/claude/audit.md +0 -43
- /package/templates/claude/{api.md → references/api.md} +0 -0
package/dist/cli/cli.cjs
CHANGED
|
@@ -24,8 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/scanner/scan/cli.ts
|
|
27
|
-
var
|
|
28
|
-
var
|
|
27
|
+
var fs8 = __toESM(require("fs"), 1);
|
|
28
|
+
var path11 = __toESM(require("path"), 1);
|
|
29
29
|
|
|
30
30
|
// src/scanner/scan/ai/index.ts
|
|
31
31
|
var p = __toESM(require("@clack/prompts"), 1);
|
|
@@ -40,9 +40,9 @@ var path = __toESM(require("path"), 1);
|
|
|
40
40
|
function templatePath(rel) {
|
|
41
41
|
const candidates = [
|
|
42
42
|
path.resolve(__dirname, "../../templates", rel),
|
|
43
|
-
// dist/cli/cli.cjs
|
|
44
|
-
path.resolve(__dirname, "
|
|
45
|
-
// src/scan/ai
|
|
43
|
+
// dist/cli/cli.cjs → ../../templates
|
|
44
|
+
path.resolve(__dirname, "../../../../templates", rel)
|
|
45
|
+
// src/scanner/scan/ai → ../../../../templates
|
|
46
46
|
];
|
|
47
47
|
for (const c of candidates) {
|
|
48
48
|
try {
|
|
@@ -62,18 +62,33 @@ function readTemplate(rel) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// src/scanner/scan/ai/providers/claude.ts
|
|
65
|
-
var
|
|
66
|
-
{ dest: ".claude/
|
|
67
|
-
{
|
|
68
|
-
|
|
65
|
+
var SKILL_FILES = [
|
|
66
|
+
{ dest: ".claude/skills/uidex/SKILL.md", template: "claude/SKILL.md" },
|
|
67
|
+
{
|
|
68
|
+
dest: ".claude/skills/uidex/references/conventions.md",
|
|
69
|
+
template: "claude/references/conventions.md"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
dest: ".claude/skills/uidex/references/audit.md",
|
|
73
|
+
template: "claude/references/audit.md"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
dest: ".claude/skills/uidex/references/api.md",
|
|
77
|
+
template: "claude/references/api.md"
|
|
78
|
+
}
|
|
79
|
+
];
|
|
80
|
+
var LEGACY_FILES = [
|
|
81
|
+
".claude/rules/uidex.md",
|
|
82
|
+
".claude/commands/uidex/audit.md",
|
|
83
|
+
".claude/commands/uidex/api.md"
|
|
69
84
|
];
|
|
70
85
|
var claudeProvider = {
|
|
71
86
|
id: "claude",
|
|
72
87
|
label: "Claude Code",
|
|
73
|
-
description: "Adds .claude/
|
|
88
|
+
description: "Adds .claude/skills/uidex/ skill with conventions, audit, and API references.",
|
|
74
89
|
async install({ cwd, force }) {
|
|
75
90
|
const changes = [];
|
|
76
|
-
for (const file of
|
|
91
|
+
for (const file of SKILL_FILES) {
|
|
77
92
|
const dest = path2.join(cwd, file.dest);
|
|
78
93
|
const exists = fs2.existsSync(dest);
|
|
79
94
|
if (exists && !force) {
|
|
@@ -91,11 +106,21 @@ var claudeProvider = {
|
|
|
91
106
|
action: exists ? "overwritten" : "created"
|
|
92
107
|
});
|
|
93
108
|
}
|
|
109
|
+
for (const rel of LEGACY_FILES) {
|
|
110
|
+
const dest = path2.join(cwd, rel);
|
|
111
|
+
if (fs2.existsSync(dest)) {
|
|
112
|
+
fs2.unlinkSync(dest);
|
|
113
|
+
changes.push({ path: rel, action: "removed" });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
cleanupEmpty(path2.join(cwd, ".claude/commands/uidex"));
|
|
117
|
+
cleanupEmpty(path2.join(cwd, ".claude/commands"));
|
|
118
|
+
cleanupEmpty(path2.join(cwd, ".claude/rules"));
|
|
94
119
|
return { changes };
|
|
95
120
|
},
|
|
96
121
|
async uninstall({ cwd }) {
|
|
97
122
|
const changes = [];
|
|
98
|
-
for (const file of
|
|
123
|
+
for (const file of SKILL_FILES) {
|
|
99
124
|
const dest = path2.join(cwd, file.dest);
|
|
100
125
|
if (!fs2.existsSync(dest)) {
|
|
101
126
|
changes.push({ path: file.dest, action: "skipped", reason: "absent" });
|
|
@@ -104,6 +129,16 @@ var claudeProvider = {
|
|
|
104
129
|
fs2.unlinkSync(dest);
|
|
105
130
|
changes.push({ path: file.dest, action: "removed" });
|
|
106
131
|
}
|
|
132
|
+
cleanupEmpty(path2.join(cwd, ".claude/skills/uidex/references"));
|
|
133
|
+
cleanupEmpty(path2.join(cwd, ".claude/skills/uidex"));
|
|
134
|
+
cleanupEmpty(path2.join(cwd, ".claude/skills"));
|
|
135
|
+
for (const rel of LEGACY_FILES) {
|
|
136
|
+
const dest = path2.join(cwd, rel);
|
|
137
|
+
if (fs2.existsSync(dest)) {
|
|
138
|
+
fs2.unlinkSync(dest);
|
|
139
|
+
changes.push({ path: rel, action: "removed" });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
107
142
|
cleanupEmpty(path2.join(cwd, ".claude/commands/uidex"));
|
|
108
143
|
cleanupEmpty(path2.join(cwd, ".claude/commands"));
|
|
109
144
|
cleanupEmpty(path2.join(cwd, ".claude/rules"));
|
|
@@ -240,7 +275,6 @@ var fs3 = __toESM(require("fs"), 1);
|
|
|
240
275
|
var path3 = __toESM(require("path"), 1);
|
|
241
276
|
|
|
242
277
|
// src/scanner/scan/config.ts
|
|
243
|
-
var DEFAULT_TYPE_MODE = "strict";
|
|
244
278
|
var WELL_KNOWN_FILES = {
|
|
245
279
|
page: "uidex.page.ts",
|
|
246
280
|
feature: "uidex.feature.ts"
|
|
@@ -265,11 +299,9 @@ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
265
299
|
"exclude",
|
|
266
300
|
"output",
|
|
267
301
|
"flows",
|
|
268
|
-
"typeMode",
|
|
269
302
|
"audit",
|
|
270
303
|
"conventions"
|
|
271
304
|
]);
|
|
272
|
-
var ALLOWED_TYPE_MODES = /* @__PURE__ */ new Set(["strict", "loose"]);
|
|
273
305
|
var ALLOWED_SOURCE_KEYS = /* @__PURE__ */ new Set(["rootDir", "include", "exclude", "prefix"]);
|
|
274
306
|
var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
|
|
275
307
|
"primitives",
|
|
@@ -282,14 +314,14 @@ var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set(["scopeLeak", "coverage", "acce
|
|
|
282
314
|
function fail(msg) {
|
|
283
315
|
throw new ConfigError(`Invalid .uidex.json: ${msg}`);
|
|
284
316
|
}
|
|
285
|
-
function assertObject(value,
|
|
317
|
+
function assertObject(value, path12) {
|
|
286
318
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
287
|
-
fail(`${
|
|
319
|
+
fail(`${path12} must be an object`);
|
|
288
320
|
}
|
|
289
321
|
}
|
|
290
|
-
function assertStringArray(value,
|
|
322
|
+
function assertStringArray(value, path12) {
|
|
291
323
|
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
|
|
292
|
-
fail(`${
|
|
324
|
+
fail(`${path12} must be a string[]`);
|
|
293
325
|
}
|
|
294
326
|
}
|
|
295
327
|
function validateConfig(raw) {
|
|
@@ -337,11 +369,6 @@ function validateConfig(raw) {
|
|
|
337
369
|
}
|
|
338
370
|
if (raw.exclude !== void 0) assertStringArray(raw.exclude, `exclude`);
|
|
339
371
|
if (raw.flows !== void 0) assertStringArray(raw.flows, `flows`);
|
|
340
|
-
if (raw.typeMode !== void 0) {
|
|
341
|
-
if (typeof raw.typeMode !== "string" || !ALLOWED_TYPE_MODES.has(raw.typeMode)) {
|
|
342
|
-
fail(`"typeMode" must be "strict" or "loose"`);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
372
|
if (raw.audit !== void 0) {
|
|
346
373
|
assertObject(raw.audit, "audit");
|
|
347
374
|
for (const key of Object.keys(raw.audit)) {
|
|
@@ -380,7 +407,6 @@ function validateConfig(raw) {
|
|
|
380
407
|
exclude: raw.exclude,
|
|
381
408
|
output: raw.output,
|
|
382
409
|
flows: raw.flows,
|
|
383
|
-
typeMode: raw.typeMode ?? DEFAULT_TYPE_MODE,
|
|
384
410
|
audit: raw.audit,
|
|
385
411
|
conventions: raw.conventions
|
|
386
412
|
};
|
|
@@ -462,12 +488,104 @@ function discover(options = {}) {
|
|
|
462
488
|
return results.sort((a, b) => a.configPath.localeCompare(b.configPath));
|
|
463
489
|
}
|
|
464
490
|
|
|
491
|
+
// src/scanner/scan/fix.ts
|
|
492
|
+
var fs4 = __toESM(require("fs"), 1);
|
|
493
|
+
var path4 = __toESM(require("path"), 1);
|
|
494
|
+
function applyFixes(diagnostics) {
|
|
495
|
+
const entries = [];
|
|
496
|
+
for (const d of diagnostics) {
|
|
497
|
+
if (!d.fix) continue;
|
|
498
|
+
entries.push({
|
|
499
|
+
code: d.code,
|
|
500
|
+
description: d.fix.description,
|
|
501
|
+
file: d.file,
|
|
502
|
+
edits: d.fix.edits ?? [],
|
|
503
|
+
createFiles: d.fix.createFiles ?? [],
|
|
504
|
+
deleteFiles: d.fix.deleteFiles ?? []
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
if (entries.length === 0) return { applied: [], skipped: [] };
|
|
508
|
+
const seenEdits = /* @__PURE__ */ new Set();
|
|
509
|
+
const editsByFile = /* @__PURE__ */ new Map();
|
|
510
|
+
for (const entry of entries) {
|
|
511
|
+
for (const edit of entry.edits) {
|
|
512
|
+
const key = `${edit.path}:${edit.start}:${edit.end}:${edit.replacement}`;
|
|
513
|
+
if (seenEdits.has(key)) continue;
|
|
514
|
+
seenEdits.add(key);
|
|
515
|
+
let list = editsByFile.get(edit.path);
|
|
516
|
+
if (!list) {
|
|
517
|
+
list = [];
|
|
518
|
+
editsByFile.set(edit.path, list);
|
|
519
|
+
}
|
|
520
|
+
list.push({ ...edit, entry });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
for (const [filePath, edits] of editsByFile) {
|
|
524
|
+
let content;
|
|
525
|
+
try {
|
|
526
|
+
content = fs4.readFileSync(filePath, "utf8");
|
|
527
|
+
} catch {
|
|
528
|
+
for (const e of edits) e.entry.skippedReason ??= "file is unreadable";
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
edits.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
532
|
+
const kept = [];
|
|
533
|
+
let prevEnd = -1;
|
|
534
|
+
for (const edit of edits) {
|
|
535
|
+
if (edit.start < prevEnd) {
|
|
536
|
+
edit.entry.skippedReason ??= "overlapping edit";
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
kept.push(edit);
|
|
540
|
+
prevEnd = edit.end;
|
|
541
|
+
}
|
|
542
|
+
for (let i = kept.length - 1; i >= 0; i--) {
|
|
543
|
+
const edit = kept[i];
|
|
544
|
+
content = content.slice(0, edit.start) + edit.replacement + content.slice(edit.end);
|
|
545
|
+
}
|
|
546
|
+
if (kept.length > 0) fs4.writeFileSync(filePath, content, "utf8");
|
|
547
|
+
}
|
|
548
|
+
for (const entry of entries) {
|
|
549
|
+
if (entry.skippedReason) continue;
|
|
550
|
+
for (const create of entry.createFiles) {
|
|
551
|
+
if (fs4.existsSync(create.path)) {
|
|
552
|
+
entry.skippedReason = `${path4.basename(create.path)} already exists`;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
fs4.mkdirSync(path4.dirname(create.path), { recursive: true });
|
|
556
|
+
fs4.writeFileSync(create.path, create.content, "utf8");
|
|
557
|
+
}
|
|
558
|
+
if (entry.skippedReason) continue;
|
|
559
|
+
for (const del of entry.deleteFiles) {
|
|
560
|
+
try {
|
|
561
|
+
fs4.unlinkSync(del);
|
|
562
|
+
} catch {
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const applied = [];
|
|
567
|
+
const skipped = [];
|
|
568
|
+
for (const entry of entries) {
|
|
569
|
+
const summary = {
|
|
570
|
+
code: entry.code,
|
|
571
|
+
description: entry.description,
|
|
572
|
+
file: entry.file
|
|
573
|
+
};
|
|
574
|
+
if (entry.skippedReason) {
|
|
575
|
+
skipped.push({ ...summary, reason: entry.skippedReason });
|
|
576
|
+
} else {
|
|
577
|
+
applied.push(summary);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return { applied, skipped };
|
|
581
|
+
}
|
|
582
|
+
|
|
465
583
|
// src/scanner/scan/pipeline.ts
|
|
466
|
-
var
|
|
467
|
-
var
|
|
584
|
+
var fs6 = __toESM(require("fs"), 1);
|
|
585
|
+
var path9 = __toESM(require("path"), 1);
|
|
468
586
|
|
|
469
587
|
// src/scanner/scan/audit.ts
|
|
470
|
-
var
|
|
588
|
+
var path5 = __toESM(require("path"), 1);
|
|
471
589
|
|
|
472
590
|
// src/shared/entities/types.ts
|
|
473
591
|
var ENTITY_KINDS = [
|
|
@@ -553,13 +671,14 @@ function createRegistry() {
|
|
|
553
671
|
};
|
|
554
672
|
const getPatternsForKind = (kind) => {
|
|
555
673
|
const cached = patternCache.get(kind);
|
|
556
|
-
if (cached !== void 0)
|
|
557
|
-
return cached;
|
|
674
|
+
if (cached !== void 0) return cached;
|
|
558
675
|
const patterns = [];
|
|
559
676
|
for (const [key, entity] of store[kind]) {
|
|
560
|
-
if (key.
|
|
677
|
+
if (key.includes("*")) {
|
|
678
|
+
const segments = key.split("*");
|
|
561
679
|
patterns.push({
|
|
562
|
-
|
|
680
|
+
segments,
|
|
681
|
+
staticLength: segments.reduce((n, s) => n + s.length, 0),
|
|
563
682
|
entity
|
|
564
683
|
});
|
|
565
684
|
}
|
|
@@ -570,13 +689,25 @@ function createRegistry() {
|
|
|
570
689
|
);
|
|
571
690
|
return patterns;
|
|
572
691
|
};
|
|
692
|
+
const matchesSegments = (segments, id) => {
|
|
693
|
+
const first = segments[0];
|
|
694
|
+
const last = segments[segments.length - 1];
|
|
695
|
+
if (!id.startsWith(first)) return false;
|
|
696
|
+
let pos = first.length;
|
|
697
|
+
for (let i = 1; i < segments.length - 1; i++) {
|
|
698
|
+
const idx = id.indexOf(segments[i], pos);
|
|
699
|
+
if (idx === -1) return false;
|
|
700
|
+
pos = idx + segments[i].length;
|
|
701
|
+
}
|
|
702
|
+
return id.endsWith(last) && id.length - last.length >= pos;
|
|
703
|
+
};
|
|
573
704
|
const matchPattern = (kind, id) => {
|
|
574
705
|
assertEntityKind(kind);
|
|
575
706
|
const patterns = getPatternsForKind(kind);
|
|
576
707
|
if (patterns.length === 0) return void 0;
|
|
577
708
|
let best;
|
|
578
709
|
for (const entry of patterns) {
|
|
579
|
-
if (
|
|
710
|
+
if (matchesSegments(entry.segments, id) && (best === void 0 || entry.staticLength > best.staticLength)) {
|
|
580
711
|
best = entry;
|
|
581
712
|
}
|
|
582
713
|
}
|
|
@@ -646,7 +777,6 @@ function createRegistry() {
|
|
|
646
777
|
}
|
|
647
778
|
|
|
648
779
|
// src/scanner/scan/audit.ts
|
|
649
|
-
var MARKER_FILENAMES = ["UIDEX_PAGE.md", "UIDEX_FEATURE.md"];
|
|
650
780
|
function audit(opts) {
|
|
651
781
|
const diagnostics = [];
|
|
652
782
|
const { registry, extracted, files, config } = opts;
|
|
@@ -656,22 +786,15 @@ function audit(opts) {
|
|
|
656
786
|
const scopeLeakEnabled = config.audit?.scopeLeak ?? true;
|
|
657
787
|
const coverageEnabled = config.audit?.coverage ?? true;
|
|
658
788
|
if (opts.resolveDiagnostics) diagnostics.push(...opts.resolveDiagnostics);
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
code: "marker-md-ignored",
|
|
665
|
-
severity: "warning",
|
|
666
|
-
message: `Marker file "${base}" is ignored in v2; migrate to \`export const uidex\``,
|
|
667
|
-
file: f.displayPath
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
}
|
|
789
|
+
for (const ef of extracted) {
|
|
790
|
+
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
791
|
+
}
|
|
792
|
+
for (const ef of opts.flowExtracted ?? []) {
|
|
793
|
+
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
671
794
|
}
|
|
672
795
|
if (check && opts.generated !== void 0) {
|
|
673
796
|
const outRel = opts.outputPath ?? config.output;
|
|
674
|
-
const fresh =
|
|
797
|
+
const fresh = normalizeForCheck(opts.generated);
|
|
675
798
|
if (opts.existingOnDisk === null || opts.existingOnDisk === void 0) {
|
|
676
799
|
diagnostics.push({
|
|
677
800
|
code: "gen-missing",
|
|
@@ -681,7 +804,7 @@ function audit(opts) {
|
|
|
681
804
|
hint: "Run `uidex scan` (without --check) to regenerate"
|
|
682
805
|
});
|
|
683
806
|
} else {
|
|
684
|
-
const existing =
|
|
807
|
+
const existing = normalizeForCheck(opts.existingOnDisk);
|
|
685
808
|
if (existing !== fresh) {
|
|
686
809
|
const changed = diffEntities(existing, opts.generated, registry);
|
|
687
810
|
const summary2 = formatChangedSummary(changed);
|
|
@@ -695,22 +818,6 @@ function audit(opts) {
|
|
|
695
818
|
}
|
|
696
819
|
}
|
|
697
820
|
}
|
|
698
|
-
if (lint) {
|
|
699
|
-
for (const ef of extracted) {
|
|
700
|
-
for (const a of ef.annotations) {
|
|
701
|
-
const migration = legacyJsdocMigration(a);
|
|
702
|
-
if (!migration) continue;
|
|
703
|
-
diagnostics.push({
|
|
704
|
-
code: "legacy-jsdoc",
|
|
705
|
-
severity: "warning",
|
|
706
|
-
message: migration.message,
|
|
707
|
-
file: a.file,
|
|
708
|
-
line: a.line,
|
|
709
|
-
hint: migration.hint
|
|
710
|
-
});
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
821
|
if (lint && acceptanceEnabled) {
|
|
715
822
|
for (const kind of ["widget", "feature", "page"]) {
|
|
716
823
|
for (const e of registry.list(kind)) {
|
|
@@ -756,8 +863,8 @@ function audit(opts) {
|
|
|
756
863
|
if (typeof m.id !== "string") continue;
|
|
757
864
|
const filePath = ef.file.displayPath;
|
|
758
865
|
const wellKnownName = WELL_KNOWN_FILES[m.kind];
|
|
759
|
-
if (
|
|
760
|
-
const dir =
|
|
866
|
+
if (path5.posix.basename(filePath) === wellKnownName) continue;
|
|
867
|
+
const dir = path5.posix.dirname(filePath);
|
|
761
868
|
const wellKnownPath = dir === "." ? wellKnownName : `${dir}/${wellKnownName}`;
|
|
762
869
|
if (scannedPaths.has(wellKnownPath)) continue;
|
|
763
870
|
const kindLabel = m.kind === "page" ? "Page" : "Feature";
|
|
@@ -774,53 +881,55 @@ function audit(opts) {
|
|
|
774
881
|
}
|
|
775
882
|
}
|
|
776
883
|
if (lint) {
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
for (const f of files) {
|
|
780
|
-
const templatePrefixPositions = /* @__PURE__ */ new Set();
|
|
781
|
-
templateWithPrefixRe.lastIndex = 0;
|
|
782
|
-
let tm;
|
|
783
|
-
while ((tm = templateWithPrefixRe.exec(f.content)) !== null) {
|
|
784
|
-
templatePrefixPositions.add(tm.index);
|
|
785
|
-
}
|
|
786
|
-
let m;
|
|
787
|
-
dynamicAttrRe.lastIndex = 0;
|
|
788
|
-
while ((m = dynamicAttrRe.exec(f.content)) !== null) {
|
|
789
|
-
if (templatePrefixPositions.has(m.index)) continue;
|
|
790
|
-
const kind = m[1] ?? "element";
|
|
791
|
-
let line = 1;
|
|
792
|
-
for (let i = 0; i < m.index; i++) if (f.content[i] === "\n") line++;
|
|
793
|
-
const attrName = m[1] ? `data-uidex-${m[1]}` : "data-uidex";
|
|
884
|
+
for (const ef of extracted) {
|
|
885
|
+
for (const fact of ef.dynamicAttrs ?? []) {
|
|
794
886
|
diagnostics.push({
|
|
795
887
|
code: "dynamic-attr",
|
|
796
888
|
severity: "warning",
|
|
797
|
-
message: `\`${attrName}={\u2026}\` uses a dynamic expression; the scanner cannot resolve the ${kind} id statically`,
|
|
798
|
-
file:
|
|
799
|
-
line,
|
|
800
|
-
hint: dynamicAttrHint(kind)
|
|
889
|
+
message: `\`${fact.attrName}={\u2026}\` uses a dynamic expression; the scanner cannot resolve the ${fact.kind} id statically`,
|
|
890
|
+
file: ef.file.displayPath,
|
|
891
|
+
line: fact.line,
|
|
892
|
+
hint: dynamicAttrHint(fact.kind)
|
|
801
893
|
});
|
|
802
894
|
}
|
|
803
895
|
}
|
|
804
896
|
}
|
|
805
897
|
if (lint) {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
898
|
+
const usedElementIds = new Set(registry.list("element").map((e) => e.id));
|
|
899
|
+
for (const ef of extracted) {
|
|
900
|
+
for (const fact of ef.unannotatedInteractive ?? []) {
|
|
901
|
+
if (fact.hasSpread) {
|
|
902
|
+
diagnostics.push({
|
|
903
|
+
code: "spread-attr",
|
|
904
|
+
severity: "info",
|
|
905
|
+
message: `Interactive <${fact.tag}> spreads dynamic props and has no static data-uidex attribute; if the annotation is forwarded via props the scanner cannot register it`,
|
|
906
|
+
file: ef.file.displayPath,
|
|
907
|
+
line: fact.line,
|
|
908
|
+
hint: "Prefer a string-literal data-uidex on the element itself, or annotate at the call site."
|
|
909
|
+
});
|
|
910
|
+
} else {
|
|
911
|
+
const id = uniqueElementId(fact, usedElementIds);
|
|
912
|
+
usedElementIds.add(id);
|
|
913
|
+
diagnostics.push({
|
|
914
|
+
code: "missing-element-annotation",
|
|
915
|
+
severity: "info",
|
|
916
|
+
message: `Interactive <${fact.tag}> without data-uidex annotation`,
|
|
917
|
+
file: ef.file.displayPath,
|
|
918
|
+
line: fact.line,
|
|
919
|
+
hint: `Add \`data-uidex="${id}"\` (or run \`uidex scan --fix\`).`,
|
|
920
|
+
fix: {
|
|
921
|
+
description: `Add data-uidex="${id}" to <${fact.tag}>`,
|
|
922
|
+
edits: [
|
|
923
|
+
{
|
|
924
|
+
path: ef.file.sourcePath,
|
|
925
|
+
start: fact.nameEnd,
|
|
926
|
+
end: fact.nameEnd,
|
|
927
|
+
replacement: ` data-uidex="${id}"`
|
|
928
|
+
}
|
|
929
|
+
]
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
}
|
|
824
933
|
}
|
|
825
934
|
}
|
|
826
935
|
}
|
|
@@ -837,12 +946,11 @@ function audit(opts) {
|
|
|
837
946
|
}
|
|
838
947
|
}
|
|
839
948
|
}
|
|
840
|
-
for (const
|
|
841
|
-
const
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
const
|
|
845
|
-
const baseName2 = spec.split("/").pop() ?? "";
|
|
949
|
+
for (const ef of extracted) {
|
|
950
|
+
const displayPath = ef.file.displayPath;
|
|
951
|
+
for (const imp of ef.imports ?? []) {
|
|
952
|
+
if (imp.isTypeOnly) continue;
|
|
953
|
+
const baseName2 = imp.specifier.split("/").pop() ?? "";
|
|
846
954
|
const primitive = byName.get(
|
|
847
955
|
baseName2.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
|
|
848
956
|
);
|
|
@@ -850,25 +958,37 @@ function audit(opts) {
|
|
|
850
958
|
const scope = primitive.scopes?.[0];
|
|
851
959
|
if (!scope) continue;
|
|
852
960
|
const [kind, id] = scope.split(":");
|
|
853
|
-
const importerSegments =
|
|
961
|
+
const importerSegments = displayPath.split("/");
|
|
854
962
|
if (importerSegments.includes(id) && importerSegments.includes(kind + "s")) {
|
|
855
963
|
continue;
|
|
856
964
|
}
|
|
857
965
|
if (kind === "feature" && importerSegments.includes(id)) continue;
|
|
858
|
-
if (kind === "feature" && declaredFeatures.get(
|
|
966
|
+
if (kind === "feature" && declaredFeatures.get(displayPath)?.has(id)) {
|
|
859
967
|
continue;
|
|
860
968
|
}
|
|
861
969
|
diagnostics.push({
|
|
862
970
|
code: "scope-leak",
|
|
863
971
|
severity: "warning",
|
|
864
|
-
message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${
|
|
865
|
-
file:
|
|
972
|
+
message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${displayPath}`,
|
|
973
|
+
file: displayPath,
|
|
974
|
+
line: imp.line
|
|
866
975
|
});
|
|
867
976
|
}
|
|
868
977
|
}
|
|
869
978
|
}
|
|
870
979
|
if (lint && coverageEnabled) {
|
|
980
|
+
const factsByLoc = /* @__PURE__ */ new Map();
|
|
981
|
+
for (const ef of opts.flowExtracted ?? []) {
|
|
982
|
+
for (const fact of ef.flows ?? []) {
|
|
983
|
+
const lines = /* @__PURE__ */ new Map();
|
|
984
|
+
for (const call of fact.calls) {
|
|
985
|
+
if (!lines.has(call.id)) lines.set(call.id, call.line);
|
|
986
|
+
}
|
|
987
|
+
factsByLoc.set(`${ef.file.displayPath}:${fact.line}`, lines);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
871
990
|
for (const flow of registry.list("flow")) {
|
|
991
|
+
const callLines = factsByLoc.get(`${flow.loc.file}:${flow.loc.line}`);
|
|
872
992
|
for (const touchedId of flow.touches) {
|
|
873
993
|
const found = registry.get("element", touchedId) ?? registry.get("widget", touchedId) ?? registry.get("region", touchedId) ?? registry.matchPattern("element", touchedId) ?? registry.matchPattern("widget", touchedId) ?? registry.matchPattern("region", touchedId);
|
|
874
994
|
if (!found) {
|
|
@@ -877,54 +997,131 @@ function audit(opts) {
|
|
|
877
997
|
severity: "warning",
|
|
878
998
|
message: `Flow "${flow.id}" references unknown entity "${touchedId}"`,
|
|
879
999
|
file: flow.loc.file,
|
|
880
|
-
|
|
1000
|
+
// Point at the uidex() call itself when the spec facts are
|
|
1001
|
+
// available; the describe line is the fallback.
|
|
1002
|
+
line: callLines?.get(touchedId) ?? flow.loc.line
|
|
881
1003
|
});
|
|
882
1004
|
}
|
|
883
1005
|
}
|
|
884
1006
|
}
|
|
885
1007
|
}
|
|
1008
|
+
if (lint) {
|
|
1009
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
1010
|
+
for (const ef of extracted) {
|
|
1011
|
+
for (const a of ef.annotations) {
|
|
1012
|
+
if (a.kind !== "element" && a.kind !== "region" && a.kind !== "widget" && a.kind !== "primitive") {
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
const key = `${a.kind}:${a.id}`;
|
|
1016
|
+
let list = occurrences.get(key);
|
|
1017
|
+
if (!list) {
|
|
1018
|
+
list = [];
|
|
1019
|
+
occurrences.set(key, list);
|
|
1020
|
+
}
|
|
1021
|
+
list.push({ file: a.file, line: a.line });
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
for (const [key, list] of occurrences) {
|
|
1025
|
+
const filesSeen = new Set(list.map((o) => o.file));
|
|
1026
|
+
if (filesSeen.size < 2) continue;
|
|
1027
|
+
const [kind, id] = key.split(/:(.*)/s);
|
|
1028
|
+
const others = list.slice(1).map((o) => `${o.file}:${o.line}`).join(", ");
|
|
1029
|
+
diagnostics.push({
|
|
1030
|
+
code: "duplicate-id",
|
|
1031
|
+
severity: kind === "widget" || kind === "primitive" ? "warning" : "info",
|
|
1032
|
+
message: `${kind} id "${id}" is declared in ${filesSeen.size} files (also at ${others}); the registry keeps only one entry`,
|
|
1033
|
+
file: list[0].file,
|
|
1034
|
+
line: list[0].line,
|
|
1035
|
+
entity: { kind, id },
|
|
1036
|
+
hint: kind === "element" || kind === "region" ? "If these are variants of the same logical element this is fine; otherwise rename one (`uidex rename` updates flow references too)." : "Rename one of the definitions; two definitions with the same id silently merge."
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (lint && coverageEnabled) {
|
|
1041
|
+
for (const ef of opts.flowExtracted ?? []) {
|
|
1042
|
+
for (const fact of ef.flows ?? []) {
|
|
1043
|
+
for (const dyn of fact.dynamicCalls ?? []) {
|
|
1044
|
+
diagnostics.push({
|
|
1045
|
+
code: "dynamic-flow-reference",
|
|
1046
|
+
severity: "warning",
|
|
1047
|
+
message: `\`uidex(\u2026)\` call in flow "${fact.title}" uses a dynamic expression; the id is invisible to coverage and registry validation`,
|
|
1048
|
+
file: ef.file.displayPath,
|
|
1049
|
+
line: dyn.line,
|
|
1050
|
+
hint: "Use a string-literal id (component ids inside uidex() must be statically analysable)."
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (lint && coverageEnabled) {
|
|
1057
|
+
for (const ef of extracted) {
|
|
1058
|
+
if (!ef.metadata) continue;
|
|
1059
|
+
for (const m of ef.metadata) {
|
|
1060
|
+
const check2 = (refKind, ids, spans) => {
|
|
1061
|
+
for (let i = 0; i < (ids?.length ?? 0); i++) {
|
|
1062
|
+
const refId = ids[i];
|
|
1063
|
+
const found = registry.get(refKind, refId) ?? registry.matchPattern(refKind, refId);
|
|
1064
|
+
if (found) continue;
|
|
1065
|
+
diagnostics.push({
|
|
1066
|
+
code: "unknown-reference",
|
|
1067
|
+
severity: "warning",
|
|
1068
|
+
message: `\`export const uidex\` in ${ef.file.displayPath} references unknown ${refKind} "${refId}"`,
|
|
1069
|
+
file: ef.file.displayPath,
|
|
1070
|
+
line: spans?.[i] ? lineOfOffset(ef.file.content, spans[i].start) : m.loc.line,
|
|
1071
|
+
hint: `No ${refKind} with id "${refId}" exists in the registry; fix the reference or add the ${refKind}.`
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
check2("feature", m.features, m.featureSpans);
|
|
1076
|
+
check2("widget", m.widgets, m.widgetSpans);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
886
1080
|
const summary = {
|
|
887
1081
|
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
888
1082
|
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
889
1083
|
};
|
|
890
1084
|
return { diagnostics, summary };
|
|
891
1085
|
}
|
|
892
|
-
function
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
};
|
|
921
|
-
default:
|
|
922
|
-
return null;
|
|
1086
|
+
function lineOfOffset(content, offset) {
|
|
1087
|
+
let line = 1;
|
|
1088
|
+
for (let i = 0; i < offset && i < content.length; i++) {
|
|
1089
|
+
if (content[i] === "\n") line++;
|
|
1090
|
+
}
|
|
1091
|
+
return line;
|
|
1092
|
+
}
|
|
1093
|
+
var TAG_FALLBACK_ID = {
|
|
1094
|
+
a: "link",
|
|
1095
|
+
button: "button",
|
|
1096
|
+
input: "input",
|
|
1097
|
+
select: "select",
|
|
1098
|
+
textarea: "textarea"
|
|
1099
|
+
};
|
|
1100
|
+
function kebabId(str) {
|
|
1101
|
+
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
1102
|
+
}
|
|
1103
|
+
function deriveElementId(fact) {
|
|
1104
|
+
const fromHint = fact.nameHint ? kebabId(fact.nameHint) : "";
|
|
1105
|
+
const capped = fromHint.split("-").filter(Boolean).slice(0, 5).join("-");
|
|
1106
|
+
return capped || TAG_FALLBACK_ID[fact.tag] || fact.tag;
|
|
1107
|
+
}
|
|
1108
|
+
function uniqueElementId(fact, used) {
|
|
1109
|
+
const base = deriveElementId(fact);
|
|
1110
|
+
if (!used.has(base)) return base;
|
|
1111
|
+
for (let n = 2; ; n++) {
|
|
1112
|
+
const candidate = `${base}-${n}`;
|
|
1113
|
+
if (!used.has(candidate)) return candidate;
|
|
923
1114
|
}
|
|
924
1115
|
}
|
|
925
1116
|
function normalizeLineEndings(s) {
|
|
926
1117
|
return s.replace(/\r\n/g, "\n");
|
|
927
1118
|
}
|
|
1119
|
+
function normalizeForCheck(s) {
|
|
1120
|
+
return normalizeLineEndings(s).replace(
|
|
1121
|
+
/export const gitContext = \{[\s\S]*?\} as const/,
|
|
1122
|
+
"export const gitContext = {} as const"
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
928
1125
|
function formatChangedSummary(change) {
|
|
929
1126
|
const parts = [];
|
|
930
1127
|
const fmt = (kind, names) => {
|
|
@@ -1023,62 +1220,11 @@ function extractEntitiesArray(source) {
|
|
|
1023
1220
|
}
|
|
1024
1221
|
return null;
|
|
1025
1222
|
}
|
|
1026
|
-
function findJsxOpeningEnd(src, start) {
|
|
1027
|
-
let i = start;
|
|
1028
|
-
while (i < src.length) {
|
|
1029
|
-
const ch = src[i];
|
|
1030
|
-
if (ch === ">" || ch === "/" && src[i + 1] === ">") return i;
|
|
1031
|
-
if (ch === '"' || ch === "'" || ch === "`") {
|
|
1032
|
-
i = skipString(src, i);
|
|
1033
|
-
} else if (ch === "{") {
|
|
1034
|
-
i = skipBraces(src, i);
|
|
1035
|
-
} else {
|
|
1036
|
-
i++;
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
return -1;
|
|
1040
|
-
}
|
|
1041
|
-
function skipString(src, start) {
|
|
1042
|
-
const quote = src[start];
|
|
1043
|
-
let i = start + 1;
|
|
1044
|
-
while (i < src.length) {
|
|
1045
|
-
if (src[i] === "\\" && quote !== "`") {
|
|
1046
|
-
i += 2;
|
|
1047
|
-
continue;
|
|
1048
|
-
}
|
|
1049
|
-
if (quote === "`" && src[i] === "$" && src[i + 1] === "{") {
|
|
1050
|
-
i = skipBraces(src, i + 1);
|
|
1051
|
-
continue;
|
|
1052
|
-
}
|
|
1053
|
-
if (src[i] === quote) return i + 1;
|
|
1054
|
-
i++;
|
|
1055
|
-
}
|
|
1056
|
-
return i;
|
|
1057
|
-
}
|
|
1058
|
-
function skipBraces(src, start) {
|
|
1059
|
-
let depth = 1;
|
|
1060
|
-
let i = start + 1;
|
|
1061
|
-
while (i < src.length && depth > 0) {
|
|
1062
|
-
const ch = src[i];
|
|
1063
|
-
if (ch === "{") {
|
|
1064
|
-
depth++;
|
|
1065
|
-
i++;
|
|
1066
|
-
} else if (ch === "}") {
|
|
1067
|
-
depth--;
|
|
1068
|
-
i++;
|
|
1069
|
-
} else if (ch === '"' || ch === "'" || ch === "`") {
|
|
1070
|
-
i = skipString(src, i);
|
|
1071
|
-
} else {
|
|
1072
|
-
i++;
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
return i;
|
|
1076
|
-
}
|
|
1077
1223
|
function dynamicAttrHint(kind) {
|
|
1078
1224
|
if (kind === "region") {
|
|
1079
1225
|
return `Use a string literal: \`data-uidex-region="id"\`, or declare the region via \`export const uidex = { region: "id" } as const satisfies Uidex.Region\` on the file that passes the region value`;
|
|
1080
1226
|
}
|
|
1081
|
-
return `The scanner
|
|
1227
|
+
return `The scanner resolves string literals, same-file const references, ternaries with literal branches, and template literals with static text (dynamic parts become \`*\` patterns). If this component forwards the annotation via a prop, restructure so the caller provides the annotated element directly (e.g. via a slot or render prop)`;
|
|
1082
1228
|
}
|
|
1083
1229
|
function stableStringify(value) {
|
|
1084
1230
|
return JSON.stringify(value, stableReplacer);
|
|
@@ -1111,9 +1257,7 @@ function replacerSorted(_key, value) {
|
|
|
1111
1257
|
}
|
|
1112
1258
|
return value;
|
|
1113
1259
|
}
|
|
1114
|
-
function emitIdUnion(name, ids
|
|
1115
|
-
if (typeMode === "loose") return `export type ${name} = string
|
|
1116
|
-
`;
|
|
1260
|
+
function emitIdUnion(name, ids) {
|
|
1117
1261
|
if (ids.length === 0) return `export type ${name} = never
|
|
1118
1262
|
`;
|
|
1119
1263
|
const sorted = [...ids].sort();
|
|
@@ -1123,12 +1267,7 @@ ${body}
|
|
|
1123
1267
|
`;
|
|
1124
1268
|
}
|
|
1125
1269
|
function emit(opts) {
|
|
1126
|
-
const {
|
|
1127
|
-
registry,
|
|
1128
|
-
gitContext,
|
|
1129
|
-
uidexImport = "uidex",
|
|
1130
|
-
typeMode = "strict"
|
|
1131
|
-
} = opts;
|
|
1270
|
+
const { registry, gitContext, uidexImport = "uidex" } = opts;
|
|
1132
1271
|
const routes = [...registry.list("route")].sort(
|
|
1133
1272
|
(a, b) => a.path.localeCompare(b.path)
|
|
1134
1273
|
);
|
|
@@ -1151,57 +1290,49 @@ function emit(opts) {
|
|
|
1151
1290
|
lines.push(
|
|
1152
1291
|
emitIdUnion(
|
|
1153
1292
|
"PageId",
|
|
1154
|
-
pages.map((e) => e.id)
|
|
1155
|
-
typeMode
|
|
1293
|
+
pages.map((e) => e.id)
|
|
1156
1294
|
)
|
|
1157
1295
|
);
|
|
1158
1296
|
lines.push(
|
|
1159
1297
|
emitIdUnion(
|
|
1160
1298
|
"FeatureId",
|
|
1161
|
-
features.map((e) => e.id)
|
|
1162
|
-
typeMode
|
|
1299
|
+
features.map((e) => e.id)
|
|
1163
1300
|
)
|
|
1164
1301
|
);
|
|
1165
1302
|
lines.push(
|
|
1166
1303
|
emitIdUnion(
|
|
1167
1304
|
"WidgetId",
|
|
1168
|
-
widgets.map((e) => e.id)
|
|
1169
|
-
typeMode
|
|
1305
|
+
widgets.map((e) => e.id)
|
|
1170
1306
|
)
|
|
1171
1307
|
);
|
|
1172
1308
|
lines.push(
|
|
1173
1309
|
emitIdUnion(
|
|
1174
1310
|
"RegionId",
|
|
1175
|
-
regions.map((e) => e.id)
|
|
1176
|
-
typeMode
|
|
1311
|
+
regions.map((e) => e.id)
|
|
1177
1312
|
)
|
|
1178
1313
|
);
|
|
1179
1314
|
lines.push(
|
|
1180
1315
|
emitIdUnion(
|
|
1181
1316
|
"ElementId",
|
|
1182
|
-
elements.map((e) => e.id)
|
|
1183
|
-
typeMode
|
|
1317
|
+
elements.map((e) => e.id)
|
|
1184
1318
|
)
|
|
1185
1319
|
);
|
|
1186
1320
|
lines.push(
|
|
1187
1321
|
emitIdUnion(
|
|
1188
1322
|
"PrimitiveId",
|
|
1189
|
-
primitives.map((e) => e.id)
|
|
1190
|
-
typeMode
|
|
1323
|
+
primitives.map((e) => e.id)
|
|
1191
1324
|
)
|
|
1192
1325
|
);
|
|
1193
1326
|
lines.push(
|
|
1194
1327
|
emitIdUnion(
|
|
1195
1328
|
"FlowId",
|
|
1196
|
-
flows.map((e) => e.id)
|
|
1197
|
-
typeMode
|
|
1329
|
+
flows.map((e) => e.id)
|
|
1198
1330
|
)
|
|
1199
1331
|
);
|
|
1200
1332
|
lines.push(
|
|
1201
1333
|
emitIdUnion(
|
|
1202
1334
|
"RouteId",
|
|
1203
|
-
routes.map((e) => e.path)
|
|
1204
|
-
typeMode
|
|
1335
|
+
routes.map((e) => e.path)
|
|
1205
1336
|
)
|
|
1206
1337
|
);
|
|
1207
1338
|
lines.push("");
|
|
@@ -1280,6 +1411,90 @@ function emit(opts) {
|
|
|
1280
1411
|
return lines.join("\n");
|
|
1281
1412
|
}
|
|
1282
1413
|
|
|
1414
|
+
// src/scanner/scan/ast.ts
|
|
1415
|
+
var path6 = __toESM(require("path"), 1);
|
|
1416
|
+
var import_oxc_parser = require("oxc-parser");
|
|
1417
|
+
function langFor(sourcePath) {
|
|
1418
|
+
switch (path6.extname(sourcePath)) {
|
|
1419
|
+
case ".tsx":
|
|
1420
|
+
return "tsx";
|
|
1421
|
+
case ".ts":
|
|
1422
|
+
case ".mts":
|
|
1423
|
+
case ".cts":
|
|
1424
|
+
return "ts";
|
|
1425
|
+
default:
|
|
1426
|
+
return "jsx";
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
function makeLineAt(content) {
|
|
1430
|
+
const starts = [0];
|
|
1431
|
+
for (let i = 0; i < content.length; i++) {
|
|
1432
|
+
if (content[i] === "\n") starts.push(i + 1);
|
|
1433
|
+
}
|
|
1434
|
+
return (offset) => {
|
|
1435
|
+
let lo = 0;
|
|
1436
|
+
let hi = starts.length - 1;
|
|
1437
|
+
while (lo < hi) {
|
|
1438
|
+
const mid = lo + hi + 1 >> 1;
|
|
1439
|
+
if (starts[mid] <= offset) lo = mid;
|
|
1440
|
+
else hi = mid - 1;
|
|
1441
|
+
}
|
|
1442
|
+
return lo + 1;
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
function parseSource(file) {
|
|
1446
|
+
const lineAt = makeLineAt(file.content);
|
|
1447
|
+
try {
|
|
1448
|
+
const result2 = (0, import_oxc_parser.parseSync)(file.sourcePath, file.content, {
|
|
1449
|
+
lang: langFor(file.sourcePath),
|
|
1450
|
+
sourceType: "module"
|
|
1451
|
+
});
|
|
1452
|
+
return {
|
|
1453
|
+
program: result2.program,
|
|
1454
|
+
hasErrors: result2.errors.length > 0,
|
|
1455
|
+
comments: result2.comments ?? [],
|
|
1456
|
+
lineAt
|
|
1457
|
+
};
|
|
1458
|
+
} catch {
|
|
1459
|
+
return { program: null, hasErrors: true, comments: [], lineAt };
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
function isNode(value) {
|
|
1463
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
1464
|
+
}
|
|
1465
|
+
function walkAst(root, visit) {
|
|
1466
|
+
if (!isNode(root)) return;
|
|
1467
|
+
if (visit(root) === false) return;
|
|
1468
|
+
for (const key of Object.keys(root)) {
|
|
1469
|
+
if (key === "type" || key === "start" || key === "end") continue;
|
|
1470
|
+
const value = root[key];
|
|
1471
|
+
if (Array.isArray(value)) {
|
|
1472
|
+
for (const item of value) walkAst(item, visit);
|
|
1473
|
+
} else if (isNode(value)) {
|
|
1474
|
+
walkAst(value, visit);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
function unwrapTsExpression(expr) {
|
|
1479
|
+
let node = expr;
|
|
1480
|
+
for (; ; ) {
|
|
1481
|
+
switch (node.type) {
|
|
1482
|
+
case "TSAsExpression":
|
|
1483
|
+
case "TSSatisfiesExpression":
|
|
1484
|
+
case "TSNonNullExpression":
|
|
1485
|
+
case "TSTypeAssertion":
|
|
1486
|
+
case "ParenthesizedExpression": {
|
|
1487
|
+
const inner = node.expression;
|
|
1488
|
+
if (!isNode(inner)) return node;
|
|
1489
|
+
node = inner;
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
default:
|
|
1493
|
+
return node;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1283
1498
|
// src/scanner/scan/extract-uidex-export.ts
|
|
1284
1499
|
var KIND_DISCRIMINATORS = [
|
|
1285
1500
|
"page",
|
|
@@ -1317,6 +1532,16 @@ var FALSEABLE = /* @__PURE__ */ new Set([
|
|
|
1317
1532
|
"primitive",
|
|
1318
1533
|
"region"
|
|
1319
1534
|
]);
|
|
1535
|
+
var SATISFIES_NAMES = {
|
|
1536
|
+
page: "Page",
|
|
1537
|
+
feature: "Feature",
|
|
1538
|
+
primitive: "Primitive",
|
|
1539
|
+
widget: "Widget",
|
|
1540
|
+
region: "Region",
|
|
1541
|
+
flow: "Flow",
|
|
1542
|
+
notFlow: "NotFlow"
|
|
1543
|
+
};
|
|
1544
|
+
var KNOWN_SATISFIES = new Set(Object.values(SATISFIES_NAMES));
|
|
1320
1545
|
var ExtractError = class extends Error {
|
|
1321
1546
|
code;
|
|
1322
1547
|
hint;
|
|
@@ -1328,649 +1553,285 @@ var ExtractError = class extends Error {
|
|
|
1328
1553
|
this.hint = hint;
|
|
1329
1554
|
}
|
|
1330
1555
|
};
|
|
1331
|
-
function extractUidexExports(file) {
|
|
1556
|
+
function extractUidexExports(file, parsed) {
|
|
1332
1557
|
const exports2 = [];
|
|
1333
1558
|
const diagnostics = [];
|
|
1334
1559
|
const { content, displayPath } = file;
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
} catch (e) {
|
|
1346
|
-
if (e instanceof ExtractError) {
|
|
1347
|
-
diagnostics.push({
|
|
1348
|
-
code: e.code,
|
|
1349
|
-
severity: "error",
|
|
1350
|
-
message: e.message,
|
|
1351
|
-
file: displayPath,
|
|
1352
|
-
line: e.pos.line,
|
|
1353
|
-
hint: e.hint
|
|
1354
|
-
});
|
|
1355
|
-
} else {
|
|
1356
|
-
throw e;
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
return { exports: exports2, diagnostics };
|
|
1361
|
-
}
|
|
1362
|
-
var HEADER_RE = /(?:^|\n)[\t ]*export\s+const\s+uidex\b(?:\s*:\s*[^=\n]+?)?\s*=\s*/g;
|
|
1363
|
-
function findExportHeaders(content) {
|
|
1364
|
-
const out2 = [];
|
|
1365
|
-
HEADER_RE.lastIndex = 0;
|
|
1366
|
-
let m;
|
|
1367
|
-
while ((m = HEADER_RE.exec(content)) !== null) {
|
|
1368
|
-
const leadingNewline = m[0].startsWith("\n") ? 1 : 0;
|
|
1369
|
-
const headerOffset = m.index + leadingNewline;
|
|
1370
|
-
const exprStart = m.index + m[0].length;
|
|
1371
|
-
if (isInsideCommentOrString(content, headerOffset)) continue;
|
|
1372
|
-
out2.push({
|
|
1373
|
-
headerPos: posAt(content, headerOffset),
|
|
1374
|
-
exprStart
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
return out2;
|
|
1378
|
-
}
|
|
1379
|
-
function isInsideCommentOrString(content, target) {
|
|
1380
|
-
let i = 0;
|
|
1381
|
-
let inLineComment = false;
|
|
1382
|
-
let inBlockComment = false;
|
|
1383
|
-
let stringDelim = null;
|
|
1384
|
-
let inTemplate = false;
|
|
1385
|
-
let templateDepth = 0;
|
|
1386
|
-
while (i < target) {
|
|
1387
|
-
const c = content[i];
|
|
1388
|
-
const n = content[i + 1];
|
|
1389
|
-
if (inLineComment) {
|
|
1390
|
-
if (c === "\n") inLineComment = false;
|
|
1391
|
-
i++;
|
|
1392
|
-
continue;
|
|
1393
|
-
}
|
|
1394
|
-
if (inBlockComment) {
|
|
1395
|
-
if (c === "*" && n === "/") {
|
|
1396
|
-
inBlockComment = false;
|
|
1397
|
-
i += 2;
|
|
1398
|
-
continue;
|
|
1399
|
-
}
|
|
1400
|
-
i++;
|
|
1401
|
-
continue;
|
|
1402
|
-
}
|
|
1403
|
-
if (stringDelim !== null) {
|
|
1404
|
-
if (c === "\\") {
|
|
1405
|
-
i += 2;
|
|
1406
|
-
continue;
|
|
1407
|
-
}
|
|
1408
|
-
if (c === stringDelim) stringDelim = null;
|
|
1409
|
-
i++;
|
|
1410
|
-
continue;
|
|
1411
|
-
}
|
|
1412
|
-
if (inTemplate) {
|
|
1413
|
-
if (c === "\\") {
|
|
1414
|
-
i += 2;
|
|
1415
|
-
continue;
|
|
1416
|
-
}
|
|
1417
|
-
if (c === "$" && n === "{") {
|
|
1418
|
-
templateDepth++;
|
|
1419
|
-
i += 2;
|
|
1560
|
+
const p2 = parsed ?? parseSource(file);
|
|
1561
|
+
if (p2.program === null) return { exports: exports2, diagnostics };
|
|
1562
|
+
for (const stmt of p2.program.body) {
|
|
1563
|
+
if (stmt.type !== "ExportNamedDeclaration") continue;
|
|
1564
|
+
const decl = stmt.declaration;
|
|
1565
|
+
if (!isNode2(decl) || decl.type !== "VariableDeclaration") continue;
|
|
1566
|
+
if (decl.kind !== "const") continue;
|
|
1567
|
+
for (const declarator of decl.declarations ?? []) {
|
|
1568
|
+
const id = declarator.id;
|
|
1569
|
+
if (!id || id.type !== "Identifier" || String(id.name) !== "uidex") {
|
|
1420
1570
|
continue;
|
|
1421
1571
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
}
|
|
1432
|
-
i++;
|
|
1433
|
-
continue;
|
|
1434
|
-
}
|
|
1435
|
-
if (c === "/" && n === "/") {
|
|
1436
|
-
inLineComment = true;
|
|
1437
|
-
i += 2;
|
|
1438
|
-
continue;
|
|
1439
|
-
}
|
|
1440
|
-
if (c === "/" && n === "*") {
|
|
1441
|
-
inBlockComment = true;
|
|
1442
|
-
i += 2;
|
|
1443
|
-
continue;
|
|
1444
|
-
}
|
|
1445
|
-
if (c === '"' || c === "'") {
|
|
1446
|
-
stringDelim = c;
|
|
1447
|
-
i++;
|
|
1448
|
-
continue;
|
|
1449
|
-
}
|
|
1450
|
-
if (c === "`") {
|
|
1451
|
-
inTemplate = true;
|
|
1452
|
-
i++;
|
|
1453
|
-
continue;
|
|
1454
|
-
}
|
|
1455
|
-
i++;
|
|
1456
|
-
}
|
|
1457
|
-
return inLineComment || inBlockComment || stringDelim !== null || inTemplate;
|
|
1458
|
-
}
|
|
1459
|
-
var Tokenizer = class {
|
|
1460
|
-
constructor(src, start) {
|
|
1461
|
-
this.src = src;
|
|
1462
|
-
this.pos = start;
|
|
1463
|
-
let line = 1;
|
|
1464
|
-
let lineStart = 0;
|
|
1465
|
-
for (let i = 0; i < start; i++) {
|
|
1466
|
-
if (src[i] === "\n") {
|
|
1467
|
-
line++;
|
|
1468
|
-
lineStart = i + 1;
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
this.line = line;
|
|
1472
|
-
this.lineStart = lineStart;
|
|
1473
|
-
}
|
|
1474
|
-
src;
|
|
1475
|
-
pos;
|
|
1476
|
-
line;
|
|
1477
|
-
lineStart;
|
|
1478
|
-
currentPos() {
|
|
1479
|
-
return {
|
|
1480
|
-
offset: this.pos,
|
|
1481
|
-
line: this.line,
|
|
1482
|
-
column: this.pos - this.lineStart + 1
|
|
1483
|
-
};
|
|
1484
|
-
}
|
|
1485
|
-
advance(n = 1) {
|
|
1486
|
-
for (let i = 0; i < n; i++) {
|
|
1487
|
-
if (this.pos < this.src.length && this.src[this.pos] === "\n") {
|
|
1488
|
-
this.line++;
|
|
1489
|
-
this.lineStart = this.pos + 1;
|
|
1490
|
-
}
|
|
1491
|
-
this.pos++;
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
skipTrivia() {
|
|
1495
|
-
while (this.pos < this.src.length) {
|
|
1496
|
-
const c = this.src[this.pos];
|
|
1497
|
-
const n = this.src[this.pos + 1];
|
|
1498
|
-
if (c === " " || c === " " || c === "\r" || c === "\n") {
|
|
1499
|
-
this.advance();
|
|
1500
|
-
continue;
|
|
1501
|
-
}
|
|
1502
|
-
if (c === "/" && n === "/") {
|
|
1503
|
-
while (this.pos < this.src.length && this.src[this.pos] !== "\n") {
|
|
1504
|
-
this.advance();
|
|
1572
|
+
const headerPos = posAt(content, stmt.start, p2);
|
|
1573
|
+
try {
|
|
1574
|
+
const init = declarator.init;
|
|
1575
|
+
if (!isNode2(init)) {
|
|
1576
|
+
throw new ExtractError(
|
|
1577
|
+
"uidex-export-invalid-literal",
|
|
1578
|
+
"`export const uidex` must be assigned an object literal.",
|
|
1579
|
+
headerPos
|
|
1580
|
+
);
|
|
1505
1581
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1582
|
+
const value = toLitValue(unwrapTsExpression(init), content, p2);
|
|
1583
|
+
const metadata = buildMetadata(
|
|
1584
|
+
value,
|
|
1585
|
+
displayPath,
|
|
1586
|
+
file.sourcePath,
|
|
1587
|
+
headerPos,
|
|
1588
|
+
diagnostics
|
|
1589
|
+
);
|
|
1590
|
+
metadata.span = statementSpan(stmt, content);
|
|
1591
|
+
checkSatisfies(init, metadata, displayPath, p2, diagnostics);
|
|
1592
|
+
exports2.push(metadata);
|
|
1593
|
+
} catch (e) {
|
|
1594
|
+
if (e instanceof ExtractError) {
|
|
1595
|
+
diagnostics.push({
|
|
1596
|
+
code: e.code,
|
|
1597
|
+
severity: "error",
|
|
1598
|
+
message: e.message,
|
|
1599
|
+
file: displayPath,
|
|
1600
|
+
line: e.pos.line,
|
|
1601
|
+
hint: e.hint
|
|
1602
|
+
});
|
|
1603
|
+
} else {
|
|
1604
|
+
throw e;
|
|
1516
1605
|
}
|
|
1517
|
-
continue;
|
|
1518
1606
|
}
|
|
1519
|
-
break;
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
next() {
|
|
1523
|
-
this.skipTrivia();
|
|
1524
|
-
if (this.pos >= this.src.length) {
|
|
1525
|
-
return { kind: "eof", value: "", pos: this.currentPos(), end: this.pos };
|
|
1526
|
-
}
|
|
1527
|
-
const pos = this.currentPos();
|
|
1528
|
-
const c = this.src[this.pos];
|
|
1529
|
-
switch (c) {
|
|
1530
|
-
case "{":
|
|
1531
|
-
this.advance();
|
|
1532
|
-
return { kind: "lbrace", value: c, pos, end: this.pos };
|
|
1533
|
-
case "}":
|
|
1534
|
-
this.advance();
|
|
1535
|
-
return { kind: "rbrace", value: c, pos, end: this.pos };
|
|
1536
|
-
case "[":
|
|
1537
|
-
this.advance();
|
|
1538
|
-
return { kind: "lbracket", value: c, pos, end: this.pos };
|
|
1539
|
-
case "]":
|
|
1540
|
-
this.advance();
|
|
1541
|
-
return { kind: "rbracket", value: c, pos, end: this.pos };
|
|
1542
|
-
case "(":
|
|
1543
|
-
this.advance();
|
|
1544
|
-
return { kind: "lparen", value: c, pos, end: this.pos };
|
|
1545
|
-
case ")":
|
|
1546
|
-
this.advance();
|
|
1547
|
-
return { kind: "rparen", value: c, pos, end: this.pos };
|
|
1548
|
-
case ",":
|
|
1549
|
-
this.advance();
|
|
1550
|
-
return { kind: "comma", value: c, pos, end: this.pos };
|
|
1551
|
-
case ":":
|
|
1552
|
-
this.advance();
|
|
1553
|
-
return { kind: "colon", value: c, pos, end: this.pos };
|
|
1554
|
-
}
|
|
1555
|
-
if (c === "." && this.src[this.pos + 1] === "." && this.src[this.pos + 2] === ".") {
|
|
1556
|
-
this.advance(3);
|
|
1557
|
-
return { kind: "spread", value: "...", pos, end: this.pos };
|
|
1558
|
-
}
|
|
1559
|
-
if (c === '"' || c === "'") {
|
|
1560
|
-
return this.readString(pos, c);
|
|
1561
|
-
}
|
|
1562
|
-
if (c === "`") {
|
|
1563
|
-
return this.readTemplate(pos);
|
|
1564
|
-
}
|
|
1565
|
-
if (isDigit(c) || c === "-" && isDigit(this.src[this.pos + 1])) {
|
|
1566
|
-
return this.readNumber(pos);
|
|
1567
1607
|
}
|
|
1568
|
-
if (isIdentStart(c)) {
|
|
1569
|
-
return this.readIdent(pos);
|
|
1570
|
-
}
|
|
1571
|
-
this.advance();
|
|
1572
|
-
return { kind: "punct", value: c, pos, end: this.pos };
|
|
1573
1608
|
}
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
}
|
|
1592
|
-
value += c;
|
|
1593
|
-
this.advance();
|
|
1594
|
-
}
|
|
1595
|
-
return { kind: "punct", value: delim, pos, end: this.pos };
|
|
1596
|
-
}
|
|
1597
|
-
readTemplate(pos) {
|
|
1598
|
-
this.advance();
|
|
1599
|
-
let value = "";
|
|
1600
|
-
let hasExpression = false;
|
|
1601
|
-
while (this.pos < this.src.length) {
|
|
1602
|
-
const c = this.src[this.pos];
|
|
1603
|
-
const n = this.src[this.pos + 1];
|
|
1604
|
-
if (c === "\\") {
|
|
1605
|
-
const esc = this.src[this.pos + 1];
|
|
1606
|
-
this.advance(2);
|
|
1607
|
-
value += decodeEscape(esc);
|
|
1608
|
-
continue;
|
|
1609
|
-
}
|
|
1610
|
-
if (c === "$" && n === "{") {
|
|
1611
|
-
hasExpression = true;
|
|
1612
|
-
this.advance(2);
|
|
1613
|
-
let depth = 1;
|
|
1614
|
-
while (this.pos < this.src.length && depth > 0) {
|
|
1615
|
-
const ch = this.src[this.pos];
|
|
1616
|
-
if (ch === "{") depth++;
|
|
1617
|
-
else if (ch === "}") depth--;
|
|
1618
|
-
this.advance();
|
|
1609
|
+
return { exports: exports2, diagnostics };
|
|
1610
|
+
}
|
|
1611
|
+
function toLitValue(node, content, p2) {
|
|
1612
|
+
const unwrapped = unwrapTsExpression(node);
|
|
1613
|
+
const pos = posAt(content, unwrapped.start, p2);
|
|
1614
|
+
const span = { start: unwrapped.start, end: unwrapped.end };
|
|
1615
|
+
switch (unwrapped.type) {
|
|
1616
|
+
case "Literal": {
|
|
1617
|
+
const v = unwrapped.value;
|
|
1618
|
+
if (typeof v === "string") return { kind: "string", value: v, pos, span };
|
|
1619
|
+
if (typeof v === "number") {
|
|
1620
|
+
if (!Number.isFinite(v)) {
|
|
1621
|
+
throw new ExtractError(
|
|
1622
|
+
"uidex-export-invalid-literal",
|
|
1623
|
+
`Invalid numeric literal in \`export const uidex\`.`,
|
|
1624
|
+
pos
|
|
1625
|
+
);
|
|
1619
1626
|
}
|
|
1620
|
-
|
|
1627
|
+
return { kind: "number", value: v, pos, span };
|
|
1621
1628
|
}
|
|
1622
|
-
if (
|
|
1623
|
-
|
|
1624
|
-
if (hasExpression) {
|
|
1625
|
-
return { kind: "template", value, pos, end: this.pos };
|
|
1626
|
-
}
|
|
1627
|
-
return { kind: "string", value, pos, end: this.pos };
|
|
1629
|
+
if (typeof v === "boolean") {
|
|
1630
|
+
return { kind: "boolean", value: v, pos, span };
|
|
1628
1631
|
}
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
}
|
|
1632
|
-
return { kind: "template", value, pos, end: this.pos };
|
|
1633
|
-
}
|
|
1634
|
-
readNumber(pos) {
|
|
1635
|
-
const start = this.pos;
|
|
1636
|
-
if (this.src[this.pos] === "-") this.advance();
|
|
1637
|
-
while (this.pos < this.src.length && isDigit(this.src[this.pos])) {
|
|
1638
|
-
this.advance();
|
|
1639
|
-
}
|
|
1640
|
-
if (this.src[this.pos] === ".") {
|
|
1641
|
-
this.advance();
|
|
1642
|
-
while (this.pos < this.src.length && isDigit(this.src[this.pos])) {
|
|
1643
|
-
this.advance();
|
|
1632
|
+
if (v === null && unwrapped.raw === "null") {
|
|
1633
|
+
return { kind: "null", pos, span };
|
|
1644
1634
|
}
|
|
1635
|
+
throw new ExtractError(
|
|
1636
|
+
"uidex-export-invalid-literal",
|
|
1637
|
+
`Unsupported literal in \`export const uidex\`; only strings, numbers, booleans, and null are allowed.`,
|
|
1638
|
+
pos
|
|
1639
|
+
);
|
|
1645
1640
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
if (
|
|
1649
|
-
|
|
1650
|
-
}
|
|
1651
|
-
while (this.pos < this.src.length && isDigit(this.src[this.pos])) {
|
|
1652
|
-
this.advance();
|
|
1641
|
+
case "UnaryExpression": {
|
|
1642
|
+
const arg = unwrapped.argument;
|
|
1643
|
+
if (unwrapped.operator === "-" && isNode2(arg) && arg.type === "Literal" && typeof arg.value === "number") {
|
|
1644
|
+
return { kind: "number", value: -arg.value, pos, span };
|
|
1653
1645
|
}
|
|
1646
|
+
throw new ExtractError(
|
|
1647
|
+
"uidex-export-invalid-literal",
|
|
1648
|
+
"Unary expressions are not allowed in `export const uidex`.",
|
|
1649
|
+
pos
|
|
1650
|
+
);
|
|
1654
1651
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
readIdent(pos) {
|
|
1659
|
-
const start = this.pos;
|
|
1660
|
-
while (this.pos < this.src.length && isIdentPart(this.src[this.pos])) {
|
|
1661
|
-
this.advance();
|
|
1662
|
-
}
|
|
1663
|
-
const value = this.src.slice(start, this.pos);
|
|
1664
|
-
return { kind: "ident", value, pos, end: this.pos };
|
|
1665
|
-
}
|
|
1666
|
-
};
|
|
1667
|
-
function isDigit(c) {
|
|
1668
|
-
return c !== void 0 && c >= "0" && c <= "9";
|
|
1669
|
-
}
|
|
1670
|
-
function isIdentStart(c) {
|
|
1671
|
-
if (c === void 0) return false;
|
|
1672
|
-
return c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_" || c === "$";
|
|
1673
|
-
}
|
|
1674
|
-
function isIdentPart(c) {
|
|
1675
|
-
return isIdentStart(c) || isDigit(c);
|
|
1676
|
-
}
|
|
1677
|
-
function decodeEscape(esc) {
|
|
1678
|
-
switch (esc) {
|
|
1679
|
-
case "n":
|
|
1680
|
-
return "\n";
|
|
1681
|
-
case "t":
|
|
1682
|
-
return " ";
|
|
1683
|
-
case "r":
|
|
1684
|
-
return "\r";
|
|
1685
|
-
case "\\":
|
|
1686
|
-
return "\\";
|
|
1687
|
-
case "'":
|
|
1688
|
-
return "'";
|
|
1689
|
-
case '"':
|
|
1690
|
-
return '"';
|
|
1691
|
-
case "`":
|
|
1692
|
-
return "`";
|
|
1693
|
-
case "0":
|
|
1694
|
-
return "\0";
|
|
1695
|
-
case "b":
|
|
1696
|
-
return "\b";
|
|
1697
|
-
case "f":
|
|
1698
|
-
return "\f";
|
|
1699
|
-
case "v":
|
|
1700
|
-
return "\v";
|
|
1701
|
-
default:
|
|
1702
|
-
return esc ?? "";
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
function parseExpression(content, start) {
|
|
1706
|
-
const tokenizer = new Tokenizer(content, start);
|
|
1707
|
-
const parser = new Parser(tokenizer);
|
|
1708
|
-
const value = parser.parseValue();
|
|
1709
|
-
parser.consumeTrailingAssertions();
|
|
1710
|
-
return value;
|
|
1711
|
-
}
|
|
1712
|
-
var Parser = class {
|
|
1713
|
-
constructor(tok) {
|
|
1714
|
-
this.tok = tok;
|
|
1715
|
-
}
|
|
1716
|
-
tok;
|
|
1717
|
-
lookahead = null;
|
|
1718
|
-
peek() {
|
|
1719
|
-
if (this.lookahead === null) this.lookahead = this.tok.next();
|
|
1720
|
-
return this.lookahead;
|
|
1721
|
-
}
|
|
1722
|
-
consume() {
|
|
1723
|
-
const t = this.peek();
|
|
1724
|
-
this.lookahead = null;
|
|
1725
|
-
return t;
|
|
1726
|
-
}
|
|
1727
|
-
parseValue() {
|
|
1728
|
-
const t = this.peek();
|
|
1729
|
-
switch (t.kind) {
|
|
1730
|
-
case "lbrace":
|
|
1731
|
-
return this.parseObject();
|
|
1732
|
-
case "lbracket":
|
|
1733
|
-
return this.parseArray();
|
|
1734
|
-
case "string":
|
|
1735
|
-
this.consume();
|
|
1736
|
-
return { kind: "string", value: t.value, pos: t.pos };
|
|
1737
|
-
case "template":
|
|
1652
|
+
case "TemplateLiteral": {
|
|
1653
|
+
const expressions = unwrapped.expressions ?? [];
|
|
1654
|
+
if (expressions.length > 0) {
|
|
1738
1655
|
throw new ExtractError(
|
|
1739
1656
|
"uidex-export-invalid-literal",
|
|
1740
1657
|
"Template literal with expression parts is not allowed in `export const uidex`; use a plain string literal.",
|
|
1741
|
-
|
|
1658
|
+
pos
|
|
1742
1659
|
);
|
|
1743
|
-
case "number": {
|
|
1744
|
-
this.consume();
|
|
1745
|
-
const n = Number(t.value);
|
|
1746
|
-
if (!Number.isFinite(n)) {
|
|
1747
|
-
throw new ExtractError(
|
|
1748
|
-
"uidex-export-invalid-literal",
|
|
1749
|
-
`Invalid numeric literal "${t.value}" in \`export const uidex\`.`,
|
|
1750
|
-
t.pos
|
|
1751
|
-
);
|
|
1752
|
-
}
|
|
1753
|
-
return { kind: "number", value: n, pos: t.pos };
|
|
1754
1660
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
return {
|
|
1759
|
-
kind: "boolean",
|
|
1760
|
-
value: t.value === "true",
|
|
1761
|
-
pos: t.pos
|
|
1762
|
-
};
|
|
1763
|
-
}
|
|
1764
|
-
if (t.value === "null") {
|
|
1765
|
-
this.consume();
|
|
1766
|
-
return { kind: "null", pos: t.pos };
|
|
1767
|
-
}
|
|
1768
|
-
if (t.value === "undefined") {
|
|
1769
|
-
throw new ExtractError(
|
|
1770
|
-
"uidex-export-invalid-literal",
|
|
1771
|
-
"`undefined` is not allowed as a value in `export const uidex`; omit the field instead.",
|
|
1772
|
-
t.pos
|
|
1773
|
-
);
|
|
1774
|
-
}
|
|
1775
|
-
throw new ExtractError(
|
|
1776
|
-
"uidex-export-invalid-literal",
|
|
1777
|
-
`Identifier reference "${t.value}" is not allowed in \`export const uidex\`; the right-hand side must be a plain literal.`,
|
|
1778
|
-
t.pos
|
|
1779
|
-
);
|
|
1780
|
-
case "spread":
|
|
1781
|
-
throw new ExtractError(
|
|
1782
|
-
"uidex-export-invalid-literal",
|
|
1783
|
-
"Spread (`...`) is not allowed in `export const uidex`; the right-hand side must be a plain literal.",
|
|
1784
|
-
t.pos
|
|
1785
|
-
);
|
|
1786
|
-
case "lparen":
|
|
1787
|
-
throw new ExtractError(
|
|
1788
|
-
"uidex-export-invalid-literal",
|
|
1789
|
-
"Parenthesised or grouped expressions are not allowed in `export const uidex`.",
|
|
1790
|
-
t.pos
|
|
1791
|
-
);
|
|
1792
|
-
case "punct":
|
|
1793
|
-
throw new ExtractError(
|
|
1794
|
-
"uidex-export-invalid-literal",
|
|
1795
|
-
`Unexpected token "${t.value}" in \`export const uidex\`.`,
|
|
1796
|
-
t.pos
|
|
1797
|
-
);
|
|
1798
|
-
case "eof":
|
|
1799
|
-
throw new ExtractError(
|
|
1800
|
-
"uidex-export-invalid-literal",
|
|
1801
|
-
"Expected a value for `export const uidex` but reached end of file.",
|
|
1802
|
-
t.pos
|
|
1803
|
-
);
|
|
1804
|
-
default:
|
|
1805
|
-
throw new ExtractError(
|
|
1806
|
-
"uidex-export-invalid-literal",
|
|
1807
|
-
`Unexpected token in \`export const uidex\`.`,
|
|
1808
|
-
t.pos
|
|
1809
|
-
);
|
|
1661
|
+
const quasis = unwrapped.quasis ?? [];
|
|
1662
|
+
const cooked = quasis[0]?.value?.cooked ?? "";
|
|
1663
|
+
return { kind: "string", value: cooked, pos, span };
|
|
1810
1664
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
const entries = [];
|
|
1815
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1816
|
-
while (true) {
|
|
1817
|
-
const t = this.peek();
|
|
1818
|
-
if (t.kind === "rbrace") {
|
|
1819
|
-
this.consume();
|
|
1820
|
-
break;
|
|
1821
|
-
}
|
|
1822
|
-
if (t.kind === "spread") {
|
|
1665
|
+
case "Identifier": {
|
|
1666
|
+
const name = String(unwrapped.name);
|
|
1667
|
+
if (name === "undefined") {
|
|
1823
1668
|
throw new ExtractError(
|
|
1824
1669
|
"uidex-export-invalid-literal",
|
|
1825
|
-
"
|
|
1826
|
-
|
|
1670
|
+
"`undefined` is not allowed as a value in `export const uidex`; omit the field instead.",
|
|
1671
|
+
pos
|
|
1827
1672
|
);
|
|
1828
1673
|
}
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
const
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1674
|
+
throw new ExtractError(
|
|
1675
|
+
"uidex-export-invalid-literal",
|
|
1676
|
+
`Identifier reference "${name}" is not allowed in \`export const uidex\`; the right-hand side must be a plain literal.`,
|
|
1677
|
+
pos
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
case "ObjectExpression":
|
|
1681
|
+
return objectLit(unwrapped, content, p2, pos, span);
|
|
1682
|
+
case "ArrayExpression":
|
|
1683
|
+
return arrayLit(unwrapped, content, p2, pos, span);
|
|
1684
|
+
case "SpreadElement":
|
|
1685
|
+
throw new ExtractError(
|
|
1686
|
+
"uidex-export-invalid-literal",
|
|
1687
|
+
"Spread (`...`) is not allowed in `export const uidex`; the right-hand side must be a plain literal.",
|
|
1688
|
+
pos
|
|
1689
|
+
);
|
|
1690
|
+
case "CallExpression":
|
|
1691
|
+
throw new ExtractError(
|
|
1692
|
+
"uidex-export-invalid-literal",
|
|
1693
|
+
"Function calls are not allowed in `export const uidex`; the right-hand side must be a plain literal.",
|
|
1694
|
+
pos
|
|
1695
|
+
);
|
|
1696
|
+
case "ConditionalExpression":
|
|
1697
|
+
throw new ExtractError(
|
|
1698
|
+
"uidex-export-invalid-literal",
|
|
1699
|
+
"Conditional expressions are not allowed in `export const uidex`; the right-hand side must be a plain literal.",
|
|
1700
|
+
pos
|
|
1701
|
+
);
|
|
1702
|
+
default:
|
|
1703
|
+
throw new ExtractError(
|
|
1704
|
+
"uidex-export-invalid-literal",
|
|
1705
|
+
`Computed expressions are not allowed in \`export const uidex\`; the right-hand side must be a plain literal.`,
|
|
1706
|
+
pos
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
function objectLit(node, content, p2, pos, span) {
|
|
1711
|
+
const entries = [];
|
|
1712
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1713
|
+
for (const prop of node.properties ?? []) {
|
|
1714
|
+
if (prop.type === "SpreadElement") {
|
|
1715
|
+
throw new ExtractError(
|
|
1716
|
+
"uidex-export-invalid-literal",
|
|
1717
|
+
"Spread (`...`) is not allowed inside `export const uidex`.",
|
|
1718
|
+
posAt(content, prop.start, p2)
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
if (prop.type !== "Property") {
|
|
1722
|
+
throw new ExtractError(
|
|
1723
|
+
"uidex-export-invalid-literal",
|
|
1724
|
+
"Unexpected member inside `export const uidex` object.",
|
|
1725
|
+
posAt(content, prop.start, p2)
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
const keyNode = prop.key;
|
|
1729
|
+
const keyPos = posAt(content, keyNode.start, p2);
|
|
1730
|
+
if (prop.shorthand) {
|
|
1731
|
+
throw new ExtractError(
|
|
1732
|
+
"uidex-export-invalid-literal",
|
|
1733
|
+
`Shorthand property "${String(keyNode.name)}" is not allowed; write "${String(keyNode.name)}: ..." with a literal value.`,
|
|
1734
|
+
keyPos
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
let key;
|
|
1738
|
+
if (prop.computed) {
|
|
1739
|
+
if (keyNode.type !== "Literal" || typeof keyNode.value !== "string") {
|
|
1881
1740
|
throw new ExtractError(
|
|
1882
1741
|
"uidex-export-invalid-literal",
|
|
1883
|
-
`
|
|
1884
|
-
|
|
1742
|
+
"Computed property keys must be string literals in `export const uidex`.",
|
|
1743
|
+
keyPos
|
|
1885
1744
|
);
|
|
1886
1745
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
this.consume();
|
|
1894
|
-
break;
|
|
1895
|
-
}
|
|
1746
|
+
key = keyNode.value;
|
|
1747
|
+
} else if (keyNode.type === "Identifier") {
|
|
1748
|
+
key = String(keyNode.name);
|
|
1749
|
+
} else if (keyNode.type === "Literal" && typeof keyNode.value === "string") {
|
|
1750
|
+
key = keyNode.value;
|
|
1751
|
+
} else {
|
|
1896
1752
|
throw new ExtractError(
|
|
1897
1753
|
"uidex-export-invalid-literal",
|
|
1898
|
-
|
|
1899
|
-
|
|
1754
|
+
"Numeric property keys are not allowed in `export const uidex`.",
|
|
1755
|
+
keyPos
|
|
1900
1756
|
);
|
|
1901
1757
|
}
|
|
1902
|
-
return { kind: "object", entries, pos: open.pos };
|
|
1903
|
-
}
|
|
1904
|
-
recordEntry(entries, seen, key, value, pos) {
|
|
1905
1758
|
if (seen.has(key)) {
|
|
1906
1759
|
throw new ExtractError(
|
|
1907
1760
|
"uidex-export-duplicate-field",
|
|
1908
1761
|
`Duplicate field "${key}" in \`export const uidex\`.`,
|
|
1909
|
-
|
|
1762
|
+
keyPos
|
|
1910
1763
|
);
|
|
1911
1764
|
}
|
|
1912
1765
|
seen.add(key);
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
"Spread (`...`) is not allowed inside `export const uidex`.",
|
|
1928
|
-
t.pos
|
|
1929
|
-
);
|
|
1930
|
-
}
|
|
1931
|
-
const value = this.parseValue();
|
|
1932
|
-
if (value.kind === "object") {
|
|
1933
|
-
}
|
|
1934
|
-
items.push(value);
|
|
1935
|
-
const after = this.peek();
|
|
1936
|
-
if (after.kind === "comma") {
|
|
1937
|
-
this.consume();
|
|
1938
|
-
continue;
|
|
1939
|
-
}
|
|
1940
|
-
if (after.kind === "rbracket") {
|
|
1941
|
-
this.consume();
|
|
1942
|
-
break;
|
|
1943
|
-
}
|
|
1766
|
+
const value = toLitValue(prop.value, content, p2);
|
|
1767
|
+
entries.push({
|
|
1768
|
+
key,
|
|
1769
|
+
value,
|
|
1770
|
+
keyPos,
|
|
1771
|
+
span: removalSpan(content, prop.start, prop.end)
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
return { kind: "object", entries, pos, span };
|
|
1775
|
+
}
|
|
1776
|
+
function arrayLit(node, content, p2, pos, span) {
|
|
1777
|
+
const items = [];
|
|
1778
|
+
for (const el of node.elements ?? []) {
|
|
1779
|
+
if (el === null) {
|
|
1944
1780
|
throw new ExtractError(
|
|
1945
1781
|
"uidex-export-invalid-literal",
|
|
1946
|
-
|
|
1947
|
-
|
|
1782
|
+
"Array holes are not allowed in `export const uidex`.",
|
|
1783
|
+
pos
|
|
1948
1784
|
);
|
|
1949
1785
|
}
|
|
1950
|
-
|
|
1786
|
+
if (el.type === "SpreadElement") {
|
|
1787
|
+
throw new ExtractError(
|
|
1788
|
+
"uidex-export-invalid-literal",
|
|
1789
|
+
"Spread (`...`) is not allowed inside `export const uidex`.",
|
|
1790
|
+
posAt(content, el.start, p2)
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
items.push(toLitValue(el, content, p2));
|
|
1951
1794
|
}
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
"uidex-export-invalid-literal",
|
|
1962
|
-
"Only `as const` is allowed after the `export const uidex` value.",
|
|
1963
|
-
next.pos
|
|
1964
|
-
);
|
|
1965
|
-
}
|
|
1795
|
+
return { kind: "array", items, pos, span };
|
|
1796
|
+
}
|
|
1797
|
+
function checkSatisfies(init, metadata, file, p2, diagnostics) {
|
|
1798
|
+
let node = init;
|
|
1799
|
+
let satisfiesType;
|
|
1800
|
+
while (isNode2(node)) {
|
|
1801
|
+
if (node.type === "TSSatisfiesExpression") {
|
|
1802
|
+
satisfiesType = node.typeAnnotation;
|
|
1803
|
+
break;
|
|
1966
1804
|
}
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1805
|
+
if (node.type === "TSAsExpression" || node.type === "TSNonNullExpression" || node.type === "TSTypeAssertion" || node.type === "ParenthesizedExpression") {
|
|
1806
|
+
node = node.expression;
|
|
1807
|
+
continue;
|
|
1970
1808
|
}
|
|
1809
|
+
break;
|
|
1971
1810
|
}
|
|
1972
|
-
|
|
1973
|
-
|
|
1811
|
+
if (!isNode2(satisfiesType) || satisfiesType.type !== "TSTypeReference") return;
|
|
1812
|
+
const typeName = satisfiesType.typeName;
|
|
1813
|
+
if (!isNode2(typeName) || typeName.type !== "TSQualifiedName") return;
|
|
1814
|
+
const left = typeName.left;
|
|
1815
|
+
const right = typeName.right;
|
|
1816
|
+
if (!isNode2(left) || left.type !== "Identifier" || left.name !== "Uidex") {
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
if (!isNode2(right) || right.type !== "Identifier") return;
|
|
1820
|
+
const actual = String(right.name);
|
|
1821
|
+
if (!KNOWN_SATISFIES.has(actual)) return;
|
|
1822
|
+
const discriminator = metadata.notFlow ? "notFlow" : metadata.kind;
|
|
1823
|
+
const expected = SATISFIES_NAMES[discriminator];
|
|
1824
|
+
if (actual === expected) return;
|
|
1825
|
+
diagnostics.push({
|
|
1826
|
+
code: "uidex-export-satisfies-mismatch",
|
|
1827
|
+
severity: "warning",
|
|
1828
|
+
message: `\`export const uidex\` declares kind "${discriminator}" but is annotated \`satisfies Uidex.${actual}\`; expected \`Uidex.${expected}\`.`,
|
|
1829
|
+
file,
|
|
1830
|
+
line: p2.lineAt(satisfiesType.start),
|
|
1831
|
+
hint: `Change the annotation to \`satisfies Uidex.${expected}\` or fix the kind discriminator.`
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
1974
1835
|
if (value.kind !== "object") {
|
|
1975
1836
|
throw new ExtractError(
|
|
1976
1837
|
"uidex-export-invalid-literal",
|
|
@@ -1979,7 +1840,7 @@ function buildMetadata(value, file, headerPos, diagnostics) {
|
|
|
1979
1840
|
);
|
|
1980
1841
|
}
|
|
1981
1842
|
const byKey = /* @__PURE__ */ new Map();
|
|
1982
|
-
for (const
|
|
1843
|
+
for (const entry of value.entries) byKey.set(entry.key, entry);
|
|
1983
1844
|
const presentKinds = KIND_DISCRIMINATORS.filter(
|
|
1984
1845
|
(k) => byKey.has(k)
|
|
1985
1846
|
);
|
|
@@ -2002,49 +1863,58 @@ function buildMetadata(value, file, headerPos, diagnostics) {
|
|
|
2002
1863
|
const discriminator = presentKinds[0];
|
|
2003
1864
|
const kind = discriminator === "notFlow" ? "flow" : discriminator;
|
|
2004
1865
|
const allowed = ALLOWED_FIELDS[kind];
|
|
2005
|
-
for (const
|
|
2006
|
-
if (!allowed.has(
|
|
2007
|
-
const fieldVal = byKey.get(k);
|
|
1866
|
+
for (const entry of value.entries) {
|
|
1867
|
+
if (!allowed.has(entry.key)) {
|
|
2008
1868
|
throw new ExtractError(
|
|
2009
1869
|
"uidex-export-unknown-field",
|
|
2010
|
-
`Unknown field "${
|
|
1870
|
+
`Unknown field "${entry.key}" in \`export const uidex\` for kind "${kind}". Allowed: ${Array.from(
|
|
2011
1871
|
allowed
|
|
2012
1872
|
).sort().join(", ")}.`,
|
|
2013
|
-
|
|
1873
|
+
entry.value.pos
|
|
2014
1874
|
);
|
|
2015
1875
|
}
|
|
2016
1876
|
}
|
|
2017
1877
|
const idField = discriminator === "notFlow" ? "flow" : discriminator;
|
|
2018
|
-
const idValue = byKey.get(discriminator);
|
|
1878
|
+
const idValue = byKey.get(discriminator).value;
|
|
2019
1879
|
let id;
|
|
2020
1880
|
if (discriminator === "notFlow") {
|
|
2021
|
-
|
|
2022
|
-
if (v.kind !== "boolean" || v.value !== true) {
|
|
1881
|
+
if (idValue.kind !== "boolean" || idValue.value !== true) {
|
|
2023
1882
|
throw new ExtractError(
|
|
2024
1883
|
"uidex-export-invalid-field",
|
|
2025
1884
|
"`notFlow` must be `true`.",
|
|
2026
|
-
|
|
1885
|
+
idValue.pos
|
|
2027
1886
|
);
|
|
2028
1887
|
}
|
|
2029
1888
|
id = false;
|
|
2030
1889
|
} else {
|
|
2031
1890
|
id = readIdField(idValue, kind, idField);
|
|
2032
1891
|
}
|
|
2033
|
-
const acceptance = readStringArrayField(byKey, "acceptance");
|
|
1892
|
+
const acceptance = readStringArrayField(byKey, "acceptance")?.values;
|
|
2034
1893
|
const description = readStringField(byKey, "description");
|
|
2035
1894
|
const name = readStringField(byKey, "name");
|
|
2036
1895
|
if (name === "") {
|
|
2037
|
-
const
|
|
1896
|
+
const entry = byKey.get("name");
|
|
2038
1897
|
diagnostics.push({
|
|
2039
1898
|
code: "uidex-export-empty-name",
|
|
2040
1899
|
severity: "info",
|
|
2041
1900
|
message: "`name` is an empty string; treating as unset.",
|
|
2042
1901
|
file,
|
|
2043
|
-
line: pos.line
|
|
1902
|
+
line: entry.value.pos.line,
|
|
1903
|
+
fix: {
|
|
1904
|
+
description: "Remove the empty `name` field",
|
|
1905
|
+
edits: [
|
|
1906
|
+
{
|
|
1907
|
+
path: sourcePath,
|
|
1908
|
+
start: entry.span.start,
|
|
1909
|
+
end: entry.span.end,
|
|
1910
|
+
replacement: ""
|
|
1911
|
+
}
|
|
1912
|
+
]
|
|
1913
|
+
}
|
|
2044
1914
|
});
|
|
2045
1915
|
}
|
|
2046
|
-
const
|
|
2047
|
-
const
|
|
1916
|
+
const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
1917
|
+
const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
2048
1918
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
2049
1919
|
const metadata = {
|
|
2050
1920
|
source: "ts-export",
|
|
@@ -2059,9 +1929,21 @@ function buildMetadata(value, file, headerPos, diagnostics) {
|
|
|
2059
1929
|
if (name) metadata.name = name;
|
|
2060
1930
|
if (acceptance) metadata.acceptance = acceptance;
|
|
2061
1931
|
if (description) metadata.description = description;
|
|
2062
|
-
if (
|
|
2063
|
-
|
|
1932
|
+
if (featuresField) {
|
|
1933
|
+
metadata.features = featuresField.values;
|
|
1934
|
+
metadata.featureSpans = featuresField.spans;
|
|
1935
|
+
}
|
|
1936
|
+
if (widgetsField) {
|
|
1937
|
+
metadata.widgets = widgetsField.values;
|
|
1938
|
+
metadata.widgetSpans = widgetsField.spans;
|
|
1939
|
+
}
|
|
2064
1940
|
if (notFlow) metadata.notFlow = true;
|
|
1941
|
+
if (typeof id === "string" && idValue.kind === "string") {
|
|
1942
|
+
metadata.idSpan = idValue.span;
|
|
1943
|
+
}
|
|
1944
|
+
const fieldSpans = {};
|
|
1945
|
+
for (const entry of value.entries) fieldSpans[entry.key] = entry.span;
|
|
1946
|
+
metadata.fieldSpans = fieldSpans;
|
|
2065
1947
|
return metadata;
|
|
2066
1948
|
}
|
|
2067
1949
|
function readIdField(value, kind, fieldName) {
|
|
@@ -2092,29 +1974,30 @@ function readIdField(value, kind, fieldName) {
|
|
|
2092
1974
|
);
|
|
2093
1975
|
}
|
|
2094
1976
|
function readStringField(byKey, name) {
|
|
2095
|
-
const
|
|
2096
|
-
if (!
|
|
2097
|
-
if (
|
|
1977
|
+
const entry = byKey.get(name);
|
|
1978
|
+
if (!entry) return void 0;
|
|
1979
|
+
if (entry.value.kind !== "string") {
|
|
2098
1980
|
throw new ExtractError(
|
|
2099
1981
|
"uidex-export-invalid-field",
|
|
2100
1982
|
`\`${name}\` must be a string.`,
|
|
2101
|
-
|
|
1983
|
+
entry.value.pos
|
|
2102
1984
|
);
|
|
2103
1985
|
}
|
|
2104
|
-
return
|
|
1986
|
+
return entry.value.value;
|
|
2105
1987
|
}
|
|
2106
1988
|
function readStringArrayField(byKey, name) {
|
|
2107
|
-
const
|
|
2108
|
-
if (!
|
|
2109
|
-
if (
|
|
1989
|
+
const entry = byKey.get(name);
|
|
1990
|
+
if (!entry) return void 0;
|
|
1991
|
+
if (entry.value.kind !== "array") {
|
|
2110
1992
|
throw new ExtractError(
|
|
2111
1993
|
"uidex-export-invalid-field",
|
|
2112
1994
|
`\`${name}\` must be an array of strings.`,
|
|
2113
|
-
|
|
1995
|
+
entry.value.pos
|
|
2114
1996
|
);
|
|
2115
1997
|
}
|
|
2116
|
-
const
|
|
2117
|
-
|
|
1998
|
+
const values = [];
|
|
1999
|
+
const spans = [];
|
|
2000
|
+
for (const item of entry.value.items) {
|
|
2118
2001
|
if (item.kind !== "string") {
|
|
2119
2002
|
throw new ExtractError(
|
|
2120
2003
|
"uidex-export-invalid-field",
|
|
@@ -2122,318 +2005,522 @@ function readStringArrayField(byKey, name) {
|
|
|
2122
2005
|
item.pos
|
|
2123
2006
|
);
|
|
2124
2007
|
}
|
|
2125
|
-
|
|
2008
|
+
values.push(item.value);
|
|
2009
|
+
spans.push(item.span);
|
|
2010
|
+
}
|
|
2011
|
+
return { values, spans };
|
|
2012
|
+
}
|
|
2013
|
+
function posAt(content, offset, p2) {
|
|
2014
|
+
const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
|
|
2015
|
+
return { offset, line: p2.lineAt(offset), column: offset - lineStart + 1 };
|
|
2016
|
+
}
|
|
2017
|
+
function removalSpan(content, start, end) {
|
|
2018
|
+
let e = end;
|
|
2019
|
+
while (e < content.length && /[ \t]/.test(content[e])) e++;
|
|
2020
|
+
if (content[e] === ",") return { start, end: e + 1 };
|
|
2021
|
+
let s = start;
|
|
2022
|
+
while (s > 0 && /[\s]/.test(content[s - 1])) s--;
|
|
2023
|
+
if (content[s - 1] === ",") return { start: s - 1, end };
|
|
2024
|
+
return { start, end };
|
|
2025
|
+
}
|
|
2026
|
+
function statementSpan(stmt, content) {
|
|
2027
|
+
let end = stmt.end;
|
|
2028
|
+
while (end < content.length && /[ \t]/.test(content[end])) end++;
|
|
2029
|
+
if (content[end] === ";") end++;
|
|
2030
|
+
while (end < content.length && /[ \t]/.test(content[end])) end++;
|
|
2031
|
+
if (content[end] === "\r") end++;
|
|
2032
|
+
if (content[end] === "\n") end++;
|
|
2033
|
+
return { start: stmt.start, end };
|
|
2034
|
+
}
|
|
2035
|
+
function isNode2(value) {
|
|
2036
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// src/scanner/scan/flow-facts.ts
|
|
2040
|
+
function collectFlowFacts(parsed, content) {
|
|
2041
|
+
if (parsed.program === null || !content.includes("test.describe")) {
|
|
2042
|
+
return [];
|
|
2043
|
+
}
|
|
2044
|
+
const facts = [];
|
|
2045
|
+
walkAst(parsed.program, (node) => {
|
|
2046
|
+
if (node.type !== "CallExpression") return void 0;
|
|
2047
|
+
const fact = readTaggedDescribe(node, parsed);
|
|
2048
|
+
if (fact) facts.push(fact);
|
|
2049
|
+
return void 0;
|
|
2050
|
+
});
|
|
2051
|
+
return facts;
|
|
2052
|
+
}
|
|
2053
|
+
function readTaggedDescribe(call, parsed) {
|
|
2054
|
+
const callee = call.callee;
|
|
2055
|
+
if (!callee || callee.type !== "MemberExpression" || !isIdentifier(callee.object, "test") || !isIdentifier(callee.property, "describe")) {
|
|
2056
|
+
return null;
|
|
2057
|
+
}
|
|
2058
|
+
const args = call.arguments ?? [];
|
|
2059
|
+
const title = stringLiteralValue(args[0]);
|
|
2060
|
+
if (title === null) return null;
|
|
2061
|
+
if (!hasFlowTag(args[1])) return null;
|
|
2062
|
+
const body = args[2];
|
|
2063
|
+
const { calls, dynamicCalls } = body ? collectUidexCalls(body, parsed) : { calls: [], dynamicCalls: [] };
|
|
2064
|
+
return {
|
|
2065
|
+
title,
|
|
2066
|
+
line: parsed.lineAt(call.start),
|
|
2067
|
+
calls,
|
|
2068
|
+
...dynamicCalls.length > 0 ? { dynamicCalls } : {}
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
function hasFlowTag(node) {
|
|
2072
|
+
if (!node || node.type !== "ObjectExpression") return false;
|
|
2073
|
+
for (const prop of node.properties ?? []) {
|
|
2074
|
+
if (prop.type !== "Property") continue;
|
|
2075
|
+
const key = prop.key;
|
|
2076
|
+
const keyName = key?.type === "Identifier" ? String(key.name) : key?.type === "Literal" ? String(key.value) : null;
|
|
2077
|
+
if (keyName !== "tag") continue;
|
|
2078
|
+
const value = prop.value;
|
|
2079
|
+
if (!value) return false;
|
|
2080
|
+
if (stringLiteralValue(value) === "@uidex:flow") return true;
|
|
2081
|
+
if (value.type === "ArrayExpression") {
|
|
2082
|
+
for (const el of value.elements ?? []) {
|
|
2083
|
+
if (el && stringLiteralValue(el) === "@uidex:flow") return true;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
return false;
|
|
2126
2087
|
}
|
|
2127
|
-
return
|
|
2088
|
+
return false;
|
|
2128
2089
|
}
|
|
2129
|
-
function
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2090
|
+
function collectUidexCalls(body, parsed) {
|
|
2091
|
+
const calls = [];
|
|
2092
|
+
const dynamicCalls = [];
|
|
2093
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
2094
|
+
const record = (node, action) => {
|
|
2095
|
+
if (claimed.has(node)) return;
|
|
2096
|
+
claimed.add(node);
|
|
2097
|
+
const resolved = uidexCallId(node);
|
|
2098
|
+
if (resolved === null) {
|
|
2099
|
+
dynamicCalls.push({ line: parsed.lineAt(node.start) });
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
calls.push({
|
|
2103
|
+
id: resolved.id,
|
|
2104
|
+
...action ? { action } : {},
|
|
2105
|
+
line: parsed.lineAt(node.start),
|
|
2106
|
+
span: resolved.span
|
|
2107
|
+
});
|
|
2108
|
+
};
|
|
2109
|
+
walkAst(body, (node) => {
|
|
2110
|
+
if (node.type !== "CallExpression") return void 0;
|
|
2111
|
+
const callee = node.callee;
|
|
2112
|
+
if (callee?.type === "MemberExpression") {
|
|
2113
|
+
const inner = callee.object;
|
|
2114
|
+
if (inner && isUidexCall(inner)) {
|
|
2115
|
+
const property = callee.property;
|
|
2116
|
+
const action = property?.type === "Identifier" ? String(property.name) : void 0;
|
|
2117
|
+
record(inner, action);
|
|
2118
|
+
}
|
|
2119
|
+
return void 0;
|
|
2136
2120
|
}
|
|
2121
|
+
if (isUidexCall(node)) record(node);
|
|
2122
|
+
return void 0;
|
|
2123
|
+
});
|
|
2124
|
+
return { calls, dynamicCalls };
|
|
2125
|
+
}
|
|
2126
|
+
function isUidexCall(node) {
|
|
2127
|
+
if (node.type !== "CallExpression") return false;
|
|
2128
|
+
if (!isIdentifier(node.callee, "uidex")) return false;
|
|
2129
|
+
return (node.arguments ?? []).length >= 1;
|
|
2130
|
+
}
|
|
2131
|
+
function uidexCallId(node) {
|
|
2132
|
+
const args = node.arguments ?? [];
|
|
2133
|
+
const arg = args[0];
|
|
2134
|
+
const id = stringLiteralValue(arg);
|
|
2135
|
+
if (id === null) return null;
|
|
2136
|
+
return { id, span: { start: arg.start, end: arg.end } };
|
|
2137
|
+
}
|
|
2138
|
+
function stringLiteralValue(node) {
|
|
2139
|
+
if (!node) return null;
|
|
2140
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
2141
|
+
return node.value.length > 0 ? node.value : null;
|
|
2142
|
+
}
|
|
2143
|
+
if (node.type === "TemplateLiteral") {
|
|
2144
|
+
const expressions = node.expressions ?? [];
|
|
2145
|
+
if (expressions.length > 0) return null;
|
|
2146
|
+
const quasis = node.quasis ?? [];
|
|
2147
|
+
const cooked = quasis[0]?.value?.cooked;
|
|
2148
|
+
return cooked && cooked.length > 0 ? cooked : null;
|
|
2137
2149
|
}
|
|
2138
|
-
return
|
|
2150
|
+
return null;
|
|
2151
|
+
}
|
|
2152
|
+
function isIdentifier(node, name) {
|
|
2153
|
+
return typeof node === "object" && node !== null && node.type === "Identifier" && String(node.name) === name;
|
|
2139
2154
|
}
|
|
2140
2155
|
|
|
2141
2156
|
// src/scanner/scan/jsx-ancestry.ts
|
|
2142
|
-
var
|
|
2143
|
-
var
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
const
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2157
|
+
var ATTR_NAME_RE = /^data-uidex(?:-(region|widget|primitive))?$/;
|
|
2158
|
+
var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["button", "a", "input", "select", "textarea"]);
|
|
2159
|
+
var LANDMARK_TAGS = /* @__PURE__ */ new Set(["header", "nav", "main", "aside", "footer"]);
|
|
2160
|
+
function attrKind(node) {
|
|
2161
|
+
const name = node.name;
|
|
2162
|
+
if (!name || name.type !== "JSXIdentifier") return null;
|
|
2163
|
+
const m = ATTR_NAME_RE.exec(String(name.name));
|
|
2164
|
+
if (!m) return null;
|
|
2165
|
+
return m[1] ?? "element";
|
|
2166
|
+
}
|
|
2167
|
+
function collectConstStrings(program) {
|
|
2168
|
+
const consts = /* @__PURE__ */ new Map();
|
|
2169
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2170
|
+
walkAst(program, (node) => {
|
|
2171
|
+
if (node.type !== "VariableDeclaration" || node.kind !== "const") {
|
|
2172
|
+
return void 0;
|
|
2173
|
+
}
|
|
2174
|
+
for (const decl of node.declarations ?? []) {
|
|
2175
|
+
const id = decl.id;
|
|
2176
|
+
if (!id || id.type !== "Identifier") continue;
|
|
2177
|
+
const name = String(id.name);
|
|
2178
|
+
if (seen.has(name)) {
|
|
2179
|
+
consts.delete(name);
|
|
2180
|
+
continue;
|
|
2181
|
+
}
|
|
2182
|
+
seen.add(name);
|
|
2183
|
+
const init = decl.init;
|
|
2184
|
+
if (!init) continue;
|
|
2185
|
+
const value = staticString(unwrapTsExpression(init));
|
|
2186
|
+
if (value !== null) consts.set(name, value);
|
|
2187
|
+
}
|
|
2188
|
+
return void 0;
|
|
2189
|
+
});
|
|
2190
|
+
return consts;
|
|
2191
|
+
}
|
|
2192
|
+
function staticString(node) {
|
|
2193
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
2194
|
+
return node.value;
|
|
2151
2195
|
}
|
|
2152
|
-
|
|
2153
|
-
const
|
|
2154
|
-
|
|
2155
|
-
|
|
2196
|
+
if (node.type === "TemplateLiteral") {
|
|
2197
|
+
const expressions = node.expressions ?? [];
|
|
2198
|
+
if (expressions.length > 0) return null;
|
|
2199
|
+
const quasis = node.quasis ?? [];
|
|
2200
|
+
return quasis[0]?.value?.cooked ?? "";
|
|
2156
2201
|
}
|
|
2157
|
-
return
|
|
2202
|
+
return null;
|
|
2158
2203
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
const
|
|
2162
|
-
const
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
i++;
|
|
2177
|
-
continue;
|
|
2178
|
-
}
|
|
2179
|
-
if (c === "/" && content[i + 1] === "/") {
|
|
2180
|
-
while (i < N && content[i] !== "\n") i++;
|
|
2181
|
-
continue;
|
|
2182
|
-
}
|
|
2183
|
-
if (c === "/" && content[i + 1] === "*") {
|
|
2184
|
-
const end = content.indexOf("*/", i + 2);
|
|
2185
|
-
const next = end === -1 ? N : end + 2;
|
|
2186
|
-
advanceLines(i, next);
|
|
2187
|
-
i = next;
|
|
2188
|
-
continue;
|
|
2189
|
-
}
|
|
2190
|
-
if (c === '"' || c === "'") {
|
|
2191
|
-
const next = skipString2(content, i, c);
|
|
2192
|
-
advanceLines(i, next);
|
|
2193
|
-
i = next;
|
|
2194
|
-
continue;
|
|
2204
|
+
var UNRESOLVED = { resolved: false };
|
|
2205
|
+
function evalIdExpression(expr, consts) {
|
|
2206
|
+
const node = unwrapTsExpression(expr);
|
|
2207
|
+
const literal = staticString(node);
|
|
2208
|
+
if (literal !== null) {
|
|
2209
|
+
return literal.length > 0 ? { resolved: true, ids: [literal] } : UNRESOLVED;
|
|
2210
|
+
}
|
|
2211
|
+
if (node.type === "TemplateLiteral") {
|
|
2212
|
+
const quasis = node.quasis ?? [];
|
|
2213
|
+
const expressions = node.expressions ?? [];
|
|
2214
|
+
let out2 = "";
|
|
2215
|
+
for (let i = 0; i < quasis.length; i++) {
|
|
2216
|
+
out2 += quasis[i].value?.cooked ?? "";
|
|
2217
|
+
if (i < expressions.length) {
|
|
2218
|
+
const part = evalIdExpression(expressions[i], consts);
|
|
2219
|
+
out2 += part.resolved && part.ids.length === 1 ? part.ids[0] : "*";
|
|
2220
|
+
}
|
|
2195
2221
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
i = next;
|
|
2200
|
-
continue;
|
|
2222
|
+
out2 = out2.replace(/\*{2,}/g, "*");
|
|
2223
|
+
if (!out2.includes("*")) {
|
|
2224
|
+
return out2.length > 0 ? { resolved: true, ids: [out2] } : UNRESOLVED;
|
|
2201
2225
|
}
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2226
|
+
return out2.replace(/\*/g, "").length > 0 ? { resolved: true, ids: [out2] } : UNRESOLVED;
|
|
2227
|
+
}
|
|
2228
|
+
if (node.type === "Identifier") {
|
|
2229
|
+
const value = consts.get(String(node.name));
|
|
2230
|
+
return value !== void 0 && value.length > 0 ? { resolved: true, ids: [value] } : UNRESOLVED;
|
|
2231
|
+
}
|
|
2232
|
+
if (node.type === "ConditionalExpression") {
|
|
2233
|
+
const left = evalIdExpression(node.consequent, consts);
|
|
2234
|
+
const right = evalIdExpression(node.alternate, consts);
|
|
2235
|
+
if (!left.resolved || !right.resolved) return UNRESOLVED;
|
|
2236
|
+
return { resolved: true, ids: [.../* @__PURE__ */ new Set([...left.ids, ...right.ids])] };
|
|
2237
|
+
}
|
|
2238
|
+
return UNRESOLVED;
|
|
2239
|
+
}
|
|
2240
|
+
function collectElementAttrs(opening, consts, dynamicAttrs, lineAt) {
|
|
2241
|
+
const statics = [];
|
|
2242
|
+
const patterns = [];
|
|
2243
|
+
const attributes = opening.attributes ?? [];
|
|
2244
|
+
for (const attr of attributes) {
|
|
2245
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
2246
|
+
const kind = attrKind(attr);
|
|
2247
|
+
if (!kind) continue;
|
|
2248
|
+
const value = attr.value;
|
|
2249
|
+
if (!value) continue;
|
|
2250
|
+
let result2 = UNRESOLVED;
|
|
2251
|
+
let valueSpan;
|
|
2252
|
+
if (value.type === "Literal") {
|
|
2253
|
+
const v = staticString(value);
|
|
2254
|
+
result2 = v !== null && v.length > 0 ? { resolved: true, ids: [v] } : UNRESOLVED;
|
|
2255
|
+
if (result2.resolved) valueSpan = { start: value.start, end: value.end };
|
|
2256
|
+
} else if (value.type === "JSXExpressionContainer") {
|
|
2257
|
+
const expr = value.expression;
|
|
2258
|
+
if (expr && expr.type !== "JSXEmptyExpression") {
|
|
2259
|
+
result2 = evalIdExpression(expr, consts);
|
|
2260
|
+
const inner = unwrapTsExpression(expr);
|
|
2261
|
+
if (result2.resolved && staticString(inner) !== null) {
|
|
2262
|
+
valueSpan = { start: inner.start, end: inner.end };
|
|
2218
2263
|
}
|
|
2219
|
-
advanceLines(i, end + 1);
|
|
2220
|
-
i = end + 1;
|
|
2221
|
-
continue;
|
|
2222
2264
|
}
|
|
2223
|
-
if (
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
if (tagName) {
|
|
2230
|
-
const attrs = parseDataAttrs(tagSource);
|
|
2231
|
-
if (attrs.length > 0) {
|
|
2232
|
-
const snapshot = ancestors.slice();
|
|
2233
|
-
for (const a of attrs) {
|
|
2234
|
-
out2.push({ kind: a.kind, id: a.id, line, ancestors: snapshot });
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
if (!isSelf) {
|
|
2238
|
-
for (const a of attrs) ancestors.push(a);
|
|
2239
|
-
stack.push({ tagName, pushed: attrs.length });
|
|
2240
|
-
}
|
|
2241
|
-
}
|
|
2242
|
-
advanceLines(i, end + 1);
|
|
2243
|
-
i = end + 1;
|
|
2265
|
+
if (!result2.resolved) {
|
|
2266
|
+
dynamicAttrs.push({
|
|
2267
|
+
kind,
|
|
2268
|
+
attrName: kind === "element" ? "data-uidex" : `data-uidex-${kind}`,
|
|
2269
|
+
line: lineAt(attr.start)
|
|
2270
|
+
});
|
|
2244
2271
|
continue;
|
|
2245
2272
|
}
|
|
2246
2273
|
}
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2274
|
+
if (!result2.resolved) continue;
|
|
2275
|
+
for (const id of result2.ids) {
|
|
2276
|
+
const resolved = {
|
|
2277
|
+
kind,
|
|
2278
|
+
id,
|
|
2279
|
+
start: attr.start,
|
|
2280
|
+
isPattern: id.includes("*"),
|
|
2281
|
+
// Only a single plain string literal is renameable in place.
|
|
2282
|
+
...result2.ids.length === 1 && valueSpan ? { span: valueSpan } : {}
|
|
2283
|
+
};
|
|
2284
|
+
if (resolved.isPattern) patterns.push(resolved);
|
|
2285
|
+
else statics.push(resolved);
|
|
2259
2286
|
}
|
|
2260
|
-
if (c === quote) return i + 1;
|
|
2261
|
-
i++;
|
|
2262
2287
|
}
|
|
2263
|
-
return
|
|
2288
|
+
return [...statics, ...patterns];
|
|
2264
2289
|
}
|
|
2265
|
-
function
|
|
2266
|
-
const
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2290
|
+
function collectJSXFacts(parsed) {
|
|
2291
|
+
const occurrences = [];
|
|
2292
|
+
const dynamicAttrs = [];
|
|
2293
|
+
const unannotatedInteractive = [];
|
|
2294
|
+
const landmarks = [];
|
|
2295
|
+
if (parsed.program === null) {
|
|
2296
|
+
return { occurrences, dynamicAttrs, unannotatedInteractive, landmarks };
|
|
2297
|
+
}
|
|
2298
|
+
const consts = collectConstStrings(parsed.program);
|
|
2299
|
+
const ancestors = [];
|
|
2300
|
+
const visit = (node) => {
|
|
2301
|
+
if (!isNode3(node)) return;
|
|
2302
|
+
if (node.type === "JSXElement") {
|
|
2303
|
+
const opening = node.openingElement;
|
|
2304
|
+
const attrs = collectElementAttrs(
|
|
2305
|
+
opening,
|
|
2306
|
+
consts,
|
|
2307
|
+
dynamicAttrs,
|
|
2308
|
+
parsed.lineAt
|
|
2309
|
+
);
|
|
2310
|
+
const interactive = readInteractive(node, parsed.lineAt);
|
|
2311
|
+
if (interactive) unannotatedInteractive.push(interactive);
|
|
2312
|
+
if (attrs.length > 0) {
|
|
2313
|
+
const snapshot = ancestors.slice();
|
|
2314
|
+
for (const a of attrs) {
|
|
2315
|
+
occurrences.push({
|
|
2316
|
+
kind: a.kind,
|
|
2317
|
+
id: a.id,
|
|
2318
|
+
line: parsed.lineAt(a.start),
|
|
2319
|
+
ancestors: snapshot,
|
|
2320
|
+
...a.span ? { span: a.span } : {}
|
|
2321
|
+
});
|
|
2283
2322
|
}
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2323
|
+
}
|
|
2324
|
+
let pushed = attrs.length;
|
|
2325
|
+
for (const a of attrs) ancestors.push({ kind: a.kind, id: a.id });
|
|
2326
|
+
const landmark = readLandmark(opening, parsed.lineAt);
|
|
2327
|
+
if (landmark) {
|
|
2328
|
+
landmarks.push(landmark);
|
|
2329
|
+
if (!attrs.some((a) => a.kind === "region")) {
|
|
2330
|
+
ancestors.push({ kind: "region", id: landmark.tag });
|
|
2331
|
+
pushed++;
|
|
2287
2332
|
}
|
|
2288
|
-
if (cj === "{") depth++;
|
|
2289
|
-
else if (cj === "}") depth--;
|
|
2290
|
-
i++;
|
|
2291
2333
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
let i = start;
|
|
2301
|
-
while (i < N) {
|
|
2302
|
-
const c = content[i];
|
|
2303
|
-
if (c === '"' || c === "'") {
|
|
2304
|
-
i = skipString2(content, i, c);
|
|
2305
|
-
continue;
|
|
2306
|
-
}
|
|
2307
|
-
if (c === "`") {
|
|
2308
|
-
i = skipTemplate(content, i);
|
|
2309
|
-
continue;
|
|
2334
|
+
visitChildren(opening);
|
|
2335
|
+
for (const child of node.children ?? []) {
|
|
2336
|
+
visit(child);
|
|
2337
|
+
}
|
|
2338
|
+
const closing = node.closingElement;
|
|
2339
|
+
if (isNode3(closing)) visitChildren(closing);
|
|
2340
|
+
ancestors.length -= pushed;
|
|
2341
|
+
return;
|
|
2310
2342
|
}
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
i = skipTemplate(content, i);
|
|
2322
|
-
continue;
|
|
2323
|
-
}
|
|
2324
|
-
if (cj === "{") depth++;
|
|
2325
|
-
else if (cj === "}") depth--;
|
|
2326
|
-
i++;
|
|
2343
|
+
visitChildren(node);
|
|
2344
|
+
};
|
|
2345
|
+
const visitChildren = (node) => {
|
|
2346
|
+
for (const key of Object.keys(node)) {
|
|
2347
|
+
if (key === "type" || key === "start" || key === "end") continue;
|
|
2348
|
+
const value = node[key];
|
|
2349
|
+
if (Array.isArray(value)) {
|
|
2350
|
+
for (const item of value) visit(item);
|
|
2351
|
+
} else {
|
|
2352
|
+
visit(value);
|
|
2327
2353
|
}
|
|
2328
|
-
continue;
|
|
2329
2354
|
}
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
}
|
|
2333
|
-
return -1;
|
|
2355
|
+
};
|
|
2356
|
+
visit(parsed.program);
|
|
2357
|
+
return { occurrences, dynamicAttrs, unannotatedInteractive, landmarks };
|
|
2334
2358
|
}
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2359
|
+
function readLandmark(opening, lineAt) {
|
|
2360
|
+
const name = opening.name;
|
|
2361
|
+
if (!name || name.type !== "JSXIdentifier") return null;
|
|
2362
|
+
const tag = String(name.name);
|
|
2363
|
+
if (LANDMARK_TAGS.has(tag)) {
|
|
2364
|
+
return { tag, line: lineAt(opening.start) };
|
|
2365
|
+
}
|
|
2366
|
+
for (const attr of opening.attributes ?? []) {
|
|
2367
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
2368
|
+
const attrName = attr.name;
|
|
2369
|
+
if (!attrName || String(attrName.name) !== "role") continue;
|
|
2370
|
+
const value = attr.value;
|
|
2371
|
+
if (value && value.type === "Literal" && value.value === "region") {
|
|
2372
|
+
return { tag: "region", line: lineAt(opening.start) };
|
|
2373
|
+
}
|
|
2342
2374
|
}
|
|
2343
|
-
return
|
|
2375
|
+
return null;
|
|
2344
2376
|
}
|
|
2345
|
-
function
|
|
2346
|
-
const
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
const
|
|
2350
|
-
|
|
2351
|
-
let
|
|
2352
|
-
for (const
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
const uidex = line.match(
|
|
2356
|
-
/^@uidex\s+(page|feature|widget)\s+(\S+)(?:\s+-\s+(.+))?/
|
|
2357
|
-
);
|
|
2358
|
-
if (uidex) {
|
|
2359
|
-
kind = uidex[1];
|
|
2360
|
-
id = uidex[2];
|
|
2361
|
-
if (uidex[3]) desc.push(uidex[3].trim());
|
|
2377
|
+
function readInteractive(element, lineAt) {
|
|
2378
|
+
const opening = element.openingElement;
|
|
2379
|
+
const name = opening.name;
|
|
2380
|
+
if (!name || name.type !== "JSXIdentifier") return null;
|
|
2381
|
+
const tag = String(name.name);
|
|
2382
|
+
if (!INTERACTIVE_TAGS.has(tag)) return null;
|
|
2383
|
+
let hasSpread = false;
|
|
2384
|
+
for (const attr of opening.attributes ?? []) {
|
|
2385
|
+
if (attr.type === "JSXSpreadAttribute") {
|
|
2386
|
+
hasSpread = true;
|
|
2362
2387
|
continue;
|
|
2363
2388
|
}
|
|
2364
|
-
if (
|
|
2365
|
-
|
|
2366
|
-
|
|
2389
|
+
if (attr.type === "JSXAttribute" && attrKind(attr) !== null) return null;
|
|
2390
|
+
}
|
|
2391
|
+
const nameHint = interactiveNameHint(element, opening);
|
|
2392
|
+
return {
|
|
2393
|
+
tag,
|
|
2394
|
+
line: lineAt(opening.start),
|
|
2395
|
+
hasSpread,
|
|
2396
|
+
nameEnd: name.end,
|
|
2397
|
+
...nameHint ? { nameHint } : {}
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
function staticAttrValue(opening, attrName) {
|
|
2401
|
+
for (const attr of opening.attributes ?? []) {
|
|
2402
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
2403
|
+
const n = attr.name;
|
|
2404
|
+
if (!n || String(n.name) !== attrName) continue;
|
|
2405
|
+
const value = attr.value;
|
|
2406
|
+
if (!value) return null;
|
|
2407
|
+
if (value.type === "Literal") return staticString(value);
|
|
2408
|
+
if (value.type === "JSXExpressionContainer") {
|
|
2409
|
+
const expr = value.expression;
|
|
2410
|
+
return expr ? staticString(unwrapTsExpression(expr)) : null;
|
|
2367
2411
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2412
|
+
return null;
|
|
2413
|
+
}
|
|
2414
|
+
return null;
|
|
2415
|
+
}
|
|
2416
|
+
function staticChildText(element) {
|
|
2417
|
+
const parts = [];
|
|
2418
|
+
for (const child of element.children ?? []) {
|
|
2419
|
+
if (child.type === "JSXText") {
|
|
2420
|
+
parts.push(String(child.value ?? ""));
|
|
2372
2421
|
}
|
|
2373
|
-
if (line.startsWith("@")) continue;
|
|
2374
|
-
desc.push(line);
|
|
2375
2422
|
}
|
|
2423
|
+
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
2424
|
+
}
|
|
2425
|
+
function interactiveNameHint(element, opening) {
|
|
2426
|
+
const ariaLabel = staticAttrValue(opening, "aria-label");
|
|
2427
|
+
if (ariaLabel) return ariaLabel;
|
|
2428
|
+
const text = staticChildText(element);
|
|
2429
|
+
if (text) return text;
|
|
2430
|
+
for (const attr of ["title", "name", "placeholder"]) {
|
|
2431
|
+
const v = staticAttrValue(opening, attr);
|
|
2432
|
+
if (v) return v;
|
|
2433
|
+
}
|
|
2434
|
+
return void 0;
|
|
2435
|
+
}
|
|
2436
|
+
function isNode3(value) {
|
|
2437
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// src/scanner/scan/extract.ts
|
|
2441
|
+
function parseFailureDiagnostic(file, parsed) {
|
|
2442
|
+
const fatal = parsed.program === null || parsed.hasErrors && parsed.program.body.length === 0;
|
|
2443
|
+
if (!fatal) return null;
|
|
2376
2444
|
return {
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2445
|
+
code: "parse-error",
|
|
2446
|
+
severity: "warning",
|
|
2447
|
+
message: "File could not be parsed \u2014 data-uidex attributes and flow facts in it were skipped, and their ids will drop out of the gen file",
|
|
2448
|
+
file: file.displayPath,
|
|
2449
|
+
line: 1,
|
|
2450
|
+
hint: "Fix the file's syntax (or exclude it from .uidex.json sources) so the scanner can read its annotations"
|
|
2382
2451
|
};
|
|
2383
2452
|
}
|
|
2384
2453
|
function extract(files) {
|
|
2385
2454
|
return files.map((file) => {
|
|
2386
|
-
const
|
|
2387
|
-
const
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
};
|
|
2455
|
+
const parsed = parseSource(file);
|
|
2456
|
+
const { exports: exports2, diagnostics } = extractUidexExports(file, parsed);
|
|
2457
|
+
const parseFailure = parseFailureDiagnostic(file, parsed);
|
|
2458
|
+
if (parseFailure) diagnostics.push(parseFailure);
|
|
2459
|
+
const out2 = { file, annotations: [] };
|
|
2460
|
+
out2.annotations = extractOne(file, parsed, out2);
|
|
2391
2461
|
if (exports2.length > 0) out2.metadata = exports2;
|
|
2392
2462
|
if (diagnostics.length > 0) out2.diagnostics = diagnostics;
|
|
2463
|
+
const flows = collectFlowFacts(parsed, file.content);
|
|
2464
|
+
if (flows.length > 0) out2.flows = flows;
|
|
2465
|
+
const imports = collectImportFacts(parsed);
|
|
2466
|
+
if (imports.length > 0) out2.imports = imports;
|
|
2393
2467
|
return out2;
|
|
2394
2468
|
});
|
|
2395
2469
|
}
|
|
2396
|
-
function extractOne(file) {
|
|
2470
|
+
function extractOne(file, parsed, out2) {
|
|
2397
2471
|
const annotations = [];
|
|
2398
|
-
const {
|
|
2399
|
-
|
|
2472
|
+
const { displayPath } = file;
|
|
2473
|
+
const jsx = collectJSXFacts(parsed);
|
|
2474
|
+
if (jsx.dynamicAttrs.length > 0) out2.dynamicAttrs = jsx.dynamicAttrs;
|
|
2475
|
+
if (jsx.unannotatedInteractive.length > 0) {
|
|
2476
|
+
out2.unannotatedInteractive = jsx.unannotatedInteractive;
|
|
2477
|
+
}
|
|
2478
|
+
if (jsx.landmarks.length > 0) out2.landmarks = jsx.landmarks;
|
|
2479
|
+
for (const occ of jsx.occurrences) {
|
|
2400
2480
|
annotations.push({
|
|
2401
2481
|
kind: occ.kind,
|
|
2402
2482
|
id: occ.id,
|
|
2403
2483
|
file: displayPath,
|
|
2404
2484
|
line: occ.line,
|
|
2405
|
-
...occ.ancestors.length > 0 ? { ancestors: occ.ancestors } : {}
|
|
2485
|
+
...occ.ancestors.length > 0 ? { ancestors: occ.ancestors } : {},
|
|
2486
|
+
...occ.span ? { span: occ.span } : {}
|
|
2406
2487
|
});
|
|
2407
2488
|
}
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
}
|
|
2426
|
-
} else if (
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
line,
|
|
2432
|
-
acceptance: parsed.acceptance
|
|
2433
|
-
});
|
|
2489
|
+
return annotations;
|
|
2490
|
+
}
|
|
2491
|
+
function collectImportFacts(parsed) {
|
|
2492
|
+
if (parsed.program === null) return [];
|
|
2493
|
+
const out2 = [];
|
|
2494
|
+
for (const stmt of parsed.program.body) {
|
|
2495
|
+
let source;
|
|
2496
|
+
let isTypeOnly = false;
|
|
2497
|
+
const names = [];
|
|
2498
|
+
if (stmt.type === "ImportDeclaration") {
|
|
2499
|
+
source = stmt.source;
|
|
2500
|
+
isTypeOnly = stmt.importKind === "type";
|
|
2501
|
+
for (const spec of stmt.specifiers ?? []) {
|
|
2502
|
+
const local = spec.local;
|
|
2503
|
+
if (local && local.type === "Identifier") {
|
|
2504
|
+
names.push(String(local.name));
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
} else if ((stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportAllDeclaration") && stmt.source) {
|
|
2508
|
+
source = stmt.source;
|
|
2509
|
+
isTypeOnly = stmt.exportKind === "type";
|
|
2510
|
+
} else {
|
|
2511
|
+
continue;
|
|
2434
2512
|
}
|
|
2513
|
+
if (!source || source.type !== "Literal") continue;
|
|
2514
|
+
if (typeof source.value !== "string") continue;
|
|
2515
|
+
out2.push({
|
|
2516
|
+
specifier: source.value,
|
|
2517
|
+
line: parsed.lineAt(stmt.start),
|
|
2518
|
+
span: { start: stmt.start, end: stmt.end },
|
|
2519
|
+
isTypeOnly,
|
|
2520
|
+
names
|
|
2521
|
+
});
|
|
2435
2522
|
}
|
|
2436
|
-
return
|
|
2523
|
+
return out2;
|
|
2437
2524
|
}
|
|
2438
2525
|
|
|
2439
2526
|
// src/scanner/scan/git.ts
|
|
@@ -2469,7 +2556,7 @@ function parseGitHubRef(ref) {
|
|
|
2469
2556
|
}
|
|
2470
2557
|
|
|
2471
2558
|
// src/scanner/scan/resolve.ts
|
|
2472
|
-
var
|
|
2559
|
+
var path8 = __toESM(require("path"), 1);
|
|
2473
2560
|
|
|
2474
2561
|
// src/scanner/scan/routes.ts
|
|
2475
2562
|
var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
@@ -2538,8 +2625,8 @@ function pathToId(routePath) {
|
|
|
2538
2625
|
}
|
|
2539
2626
|
|
|
2540
2627
|
// src/scanner/scan/walk.ts
|
|
2541
|
-
var
|
|
2542
|
-
var
|
|
2628
|
+
var fs5 = __toESM(require("fs"), 1);
|
|
2629
|
+
var path7 = __toESM(require("path"), 1);
|
|
2543
2630
|
var DEFAULT_INCLUDES = ["**/*.{ts,tsx,js,jsx,mjs,cjs}"];
|
|
2544
2631
|
var BASE_EXCLUDES = [
|
|
2545
2632
|
"**/node_modules/**",
|
|
@@ -2603,7 +2690,7 @@ function globToRegExp(glob) {
|
|
|
2603
2690
|
return new RegExp(`^${out2}$`);
|
|
2604
2691
|
}
|
|
2605
2692
|
function toPosix(p2) {
|
|
2606
|
-
return p2.split(
|
|
2693
|
+
return p2.split(path7.sep).join("/");
|
|
2607
2694
|
}
|
|
2608
2695
|
function matchesAny(rel, patterns) {
|
|
2609
2696
|
return patterns.some((g) => globToRegExp(g).test(rel));
|
|
@@ -2619,18 +2706,18 @@ function walk(sources, options) {
|
|
|
2619
2706
|
...globalExcludes,
|
|
2620
2707
|
...source.exclude ?? []
|
|
2621
2708
|
];
|
|
2622
|
-
const absRoot =
|
|
2709
|
+
const absRoot = path7.resolve(cwd, source.rootDir);
|
|
2623
2710
|
for (const filePath of walkDir(absRoot, absRoot)) {
|
|
2624
|
-
const rel = toPosix(
|
|
2711
|
+
const rel = toPosix(path7.relative(absRoot, filePath));
|
|
2625
2712
|
if (matchesAny(rel, excludes)) continue;
|
|
2626
2713
|
if (!matchesAny(rel, includes)) continue;
|
|
2627
2714
|
let content;
|
|
2628
2715
|
try {
|
|
2629
|
-
content =
|
|
2716
|
+
content = fs5.readFileSync(filePath, "utf8");
|
|
2630
2717
|
} catch {
|
|
2631
2718
|
continue;
|
|
2632
2719
|
}
|
|
2633
|
-
const relFromCwd = toPosix(
|
|
2720
|
+
const relFromCwd = toPosix(path7.relative(cwd, filePath));
|
|
2634
2721
|
const displayPath = source.prefix ? `${source.prefix.replace(/\/$/, "")}/${rel}` : relFromCwd;
|
|
2635
2722
|
out2.push({
|
|
2636
2723
|
sourcePath: filePath,
|
|
@@ -2645,12 +2732,12 @@ function walk(sources, options) {
|
|
|
2645
2732
|
function* walkDir(root, dir) {
|
|
2646
2733
|
let entries;
|
|
2647
2734
|
try {
|
|
2648
|
-
entries =
|
|
2735
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
2649
2736
|
} catch {
|
|
2650
2737
|
return;
|
|
2651
2738
|
}
|
|
2652
2739
|
for (const entry of entries) {
|
|
2653
|
-
const full =
|
|
2740
|
+
const full = path7.join(dir, entry.name);
|
|
2654
2741
|
if (entry.isDirectory()) {
|
|
2655
2742
|
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git" || entry.name === "build" || entry.name === ".next") {
|
|
2656
2743
|
continue;
|
|
@@ -2682,21 +2769,9 @@ function kebab(str) {
|
|
|
2682
2769
|
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").replace(/[^a-zA-Z0-9-]/g, "").toLowerCase();
|
|
2683
2770
|
}
|
|
2684
2771
|
function baseName(file) {
|
|
2685
|
-
const b =
|
|
2772
|
+
const b = path8.posix.basename(file);
|
|
2686
2773
|
return b.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "");
|
|
2687
2774
|
}
|
|
2688
|
-
var LANDMARK_RE = /<(header|nav|main|aside|footer)(\s[^>]*)?>|role=["']region["']/gi;
|
|
2689
|
-
function extractLandmarks(file) {
|
|
2690
|
-
const out2 = [];
|
|
2691
|
-
LANDMARK_RE.lastIndex = 0;
|
|
2692
|
-
let m;
|
|
2693
|
-
while ((m = LANDMARK_RE.exec(file.content)) !== null) {
|
|
2694
|
-
const tag = m[1] ?? "region";
|
|
2695
|
-
const line = 1 + file.content.slice(0, m.index).split("\n").length - 1;
|
|
2696
|
-
out2.push({ tag, line });
|
|
2697
|
-
}
|
|
2698
|
-
return out2;
|
|
2699
|
-
}
|
|
2700
2775
|
function fileMatchesAny(displayPath, patterns) {
|
|
2701
2776
|
return patterns.some((g) => globToRegExp(g).test(displayPath));
|
|
2702
2777
|
}
|
|
@@ -2757,7 +2832,7 @@ function resolve3(ctx) {
|
|
|
2757
2832
|
const routes = conventions.pages === "auto" ? detectRoutes(ctx.extracted.map((e) => e.file)) : [];
|
|
2758
2833
|
const handledPageFiles = /* @__PURE__ */ new Set();
|
|
2759
2834
|
for (const route of routes) {
|
|
2760
|
-
const routeDir =
|
|
2835
|
+
const routeDir = path8.posix.dirname(route.file);
|
|
2761
2836
|
const wellKnownPath = `${routeDir}/${WELL_KNOWN_FILES.page}`;
|
|
2762
2837
|
const wellKnownExp = exportFor(wellKnownPath, "page");
|
|
2763
2838
|
const routeExp = exportFor(route.file, "page");
|
|
@@ -2811,7 +2886,7 @@ function resolve3(ctx) {
|
|
|
2811
2886
|
const dir = extractFeatureDir(ef.file.displayPath, featureGlob);
|
|
2812
2887
|
if (!dir) continue;
|
|
2813
2888
|
conventionalFeatureDirs.add(dir);
|
|
2814
|
-
const isWellKnown =
|
|
2889
|
+
const isWellKnown = path8.posix.basename(ef.file.displayPath) === WELL_KNOWN_FILES.feature;
|
|
2815
2890
|
if (isWellKnown) wellKnownFeatureFileByDir.set(dir, ef.file.displayPath);
|
|
2816
2891
|
const exp = exportFor(ef.file.displayPath, "feature");
|
|
2817
2892
|
if (exp) {
|
|
@@ -2848,7 +2923,7 @@ function resolve3(ctx) {
|
|
|
2848
2923
|
} else if (allExports.length > 0) {
|
|
2849
2924
|
exp = allExports[0].exp;
|
|
2850
2925
|
}
|
|
2851
|
-
const id = exp && typeof exp.id === "string" ? exp.id :
|
|
2926
|
+
const id = exp && typeof exp.id === "string" ? exp.id : path8.posix.basename(dir);
|
|
2852
2927
|
const meta = exp ? buildMetaFromExport(exp) : void 0;
|
|
2853
2928
|
const feature = {
|
|
2854
2929
|
kind: "feature",
|
|
@@ -2944,8 +3019,8 @@ function resolve3(ctx) {
|
|
|
2944
3019
|
}
|
|
2945
3020
|
if (conventions.regions === "landmarks") {
|
|
2946
3021
|
for (const ef of ctx.extracted) {
|
|
2947
|
-
for (const lm of
|
|
2948
|
-
const id =
|
|
3022
|
+
for (const lm of ef.landmarks ?? []) {
|
|
3023
|
+
const id = lm.tag;
|
|
2949
3024
|
if (!registry.get("region", id)) {
|
|
2950
3025
|
const meta = metaWithComposes("region", id);
|
|
2951
3026
|
const region = {
|
|
@@ -3056,7 +3131,7 @@ function resolve3(ctx) {
|
|
|
3056
3131
|
const flowExport = (ff.metadata ?? []).find(
|
|
3057
3132
|
(m) => m.kind === "flow" && typeof m.id === "string"
|
|
3058
3133
|
);
|
|
3059
|
-
const derived =
|
|
3134
|
+
const derived = flowsFromFacts(ff);
|
|
3060
3135
|
if (flowExport && typeof flowExport.id === "string" && derived.length === 1) {
|
|
3061
3136
|
const base = derived[0];
|
|
3062
3137
|
const flow = {
|
|
@@ -3111,52 +3186,14 @@ function computeScope(displayPath) {
|
|
|
3111
3186
|
}
|
|
3112
3187
|
return null;
|
|
3113
3188
|
}
|
|
3114
|
-
function
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
const line = 1 + source.slice(0, m.index).split("\n").length - 1;
|
|
3123
|
-
const after = source.slice(m.index + m[0].length);
|
|
3124
|
-
const arrow = after.match(/=>\s*\{/);
|
|
3125
|
-
if (!arrow || arrow.index === void 0) continue;
|
|
3126
|
-
const bodyStart = m.index + m[0].length + arrow.index + arrow[0].length;
|
|
3127
|
-
let depth = 1;
|
|
3128
|
-
let bodyEnd = -1;
|
|
3129
|
-
for (let i = bodyStart; i < source.length; i++) {
|
|
3130
|
-
if (source[i] === "{") depth++;
|
|
3131
|
-
else if (source[i] === "}") {
|
|
3132
|
-
depth--;
|
|
3133
|
-
if (depth === 0) {
|
|
3134
|
-
bodyEnd = i;
|
|
3135
|
-
break;
|
|
3136
|
-
}
|
|
3137
|
-
}
|
|
3138
|
-
}
|
|
3139
|
-
if (bodyEnd === -1) continue;
|
|
3140
|
-
const body = source.slice(bodyStart, bodyEnd);
|
|
3141
|
-
const touches = captureUidexIds(body);
|
|
3142
|
-
flows.push({
|
|
3143
|
-
kind: "flow",
|
|
3144
|
-
id,
|
|
3145
|
-
loc: { file: file.displayPath, line },
|
|
3146
|
-
touches: dedupe(touches.map((t) => t.id)),
|
|
3147
|
-
steps: touches.filter((t) => t.action).map((t) => ({ entityId: t.id, action: t.action }))
|
|
3148
|
-
});
|
|
3149
|
-
}
|
|
3150
|
-
return flows;
|
|
3151
|
-
}
|
|
3152
|
-
function captureUidexIds(body) {
|
|
3153
|
-
const out2 = [];
|
|
3154
|
-
const re = /uidex\(\s*(?:'([^']+)'|"([^"]+)"|`([^`$]+)`)\s*\)(?:\.(\w+)\s*\()?/g;
|
|
3155
|
-
let m;
|
|
3156
|
-
while ((m = re.exec(body)) !== null) {
|
|
3157
|
-
out2.push({ id: m[1] || m[2] || m[3], action: m[4] });
|
|
3158
|
-
}
|
|
3159
|
-
return out2;
|
|
3189
|
+
function flowsFromFacts(ff) {
|
|
3190
|
+
return (ff.flows ?? []).map((fact) => ({
|
|
3191
|
+
kind: "flow",
|
|
3192
|
+
id: kebab(fact.title),
|
|
3193
|
+
loc: { file: ff.file.displayPath, line: fact.line },
|
|
3194
|
+
touches: dedupe(fact.calls.map((c) => c.id)),
|
|
3195
|
+
steps: fact.calls.filter((c) => c.action).map((c) => ({ entityId: c.id, action: c.action }))
|
|
3196
|
+
}));
|
|
3160
3197
|
}
|
|
3161
3198
|
function dedupe(arr) {
|
|
3162
3199
|
return Array.from(new Set(arr));
|
|
@@ -3191,25 +3228,28 @@ function runOne(dc, opts) {
|
|
|
3191
3228
|
const gitContext = resolveGitContext({ cwd: configDir });
|
|
3192
3229
|
const generated = emit({
|
|
3193
3230
|
registry: resolved.registry,
|
|
3194
|
-
gitContext
|
|
3195
|
-
typeMode: config.typeMode
|
|
3231
|
+
gitContext
|
|
3196
3232
|
});
|
|
3197
|
-
const outputPath =
|
|
3233
|
+
const outputPath = path9.resolve(configDir, config.output);
|
|
3198
3234
|
const outputRel = config.output;
|
|
3199
3235
|
let existingOnDisk = null;
|
|
3200
3236
|
if (opts.check) {
|
|
3201
3237
|
try {
|
|
3202
|
-
existingOnDisk =
|
|
3238
|
+
existingOnDisk = fs6.readFileSync(outputPath, "utf8");
|
|
3203
3239
|
} catch {
|
|
3204
3240
|
existingOnDisk = null;
|
|
3205
3241
|
}
|
|
3206
3242
|
}
|
|
3243
|
+
const hasExtractDiagnostics = [...extracted, ...extractedFlows].some(
|
|
3244
|
+
(ef) => (ef.diagnostics?.length ?? 0) > 0
|
|
3245
|
+
);
|
|
3207
3246
|
let auditResult;
|
|
3208
|
-
if (opts.check || opts.lint || resolved.diagnostics.length > 0) {
|
|
3247
|
+
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
|
|
3209
3248
|
auditResult = audit({
|
|
3210
3249
|
registry: resolved.registry,
|
|
3211
3250
|
extracted,
|
|
3212
3251
|
files: sourceFiles,
|
|
3252
|
+
flowExtracted: extractedFlows,
|
|
3213
3253
|
config,
|
|
3214
3254
|
check: opts.check,
|
|
3215
3255
|
lint: opts.lint,
|
|
@@ -3230,29 +3270,185 @@ function runOne(dc, opts) {
|
|
|
3230
3270
|
};
|
|
3231
3271
|
}
|
|
3232
3272
|
function writeScanResult(result2) {
|
|
3233
|
-
|
|
3234
|
-
|
|
3273
|
+
fs6.mkdirSync(path9.dirname(result2.outputPath), { recursive: true });
|
|
3274
|
+
fs6.writeFileSync(result2.outputPath, result2.generated, "utf8");
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
// src/scanner/scan/rename.ts
|
|
3278
|
+
var ID_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
3279
|
+
function renameEntity(opts) {
|
|
3280
|
+
const { cwd, kind, oldId, newId, force = false } = opts;
|
|
3281
|
+
const manual = [];
|
|
3282
|
+
const errors = [];
|
|
3283
|
+
if (!ID_RE.test(newId)) {
|
|
3284
|
+
return {
|
|
3285
|
+
edits: 0,
|
|
3286
|
+
manual,
|
|
3287
|
+
errors: [`New id "${newId}" is not kebab-case`]
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
const configs = discover({ cwd });
|
|
3291
|
+
if (configs.length === 0) {
|
|
3292
|
+
return { edits: 0, manual, errors: [`No .uidex.json found under ${cwd}`] };
|
|
3293
|
+
}
|
|
3294
|
+
const edits = [];
|
|
3295
|
+
for (const dc of configs) {
|
|
3296
|
+
const { config, configDir } = dc;
|
|
3297
|
+
const sourceFiles = walk(config.sources, {
|
|
3298
|
+
cwd: configDir,
|
|
3299
|
+
globalExcludes: config.exclude
|
|
3300
|
+
});
|
|
3301
|
+
const extracted = extract(sourceFiles);
|
|
3302
|
+
const flowFiles = config.flows ? walk(
|
|
3303
|
+
config.flows.map((glob) => ({ rootDir: ".", include: [glob] })),
|
|
3304
|
+
{ cwd: configDir, includeTests: true }
|
|
3305
|
+
) : [];
|
|
3306
|
+
const extractedFlows = extract(flowFiles);
|
|
3307
|
+
const scan = runScan({ cwd: configDir, configs: [dc] })[0];
|
|
3308
|
+
const registry = scan.registry;
|
|
3309
|
+
if (!registry.get(kind, oldId)) {
|
|
3310
|
+
if (registry.matchPattern(kind, oldId)) {
|
|
3311
|
+
errors.push(
|
|
3312
|
+
`${kind} "${oldId}" only matches via a pattern id; pattern-backed ids cannot be renamed mechanically`
|
|
3313
|
+
);
|
|
3314
|
+
} else {
|
|
3315
|
+
errors.push(`${kind} "${oldId}" not found in registry`);
|
|
3316
|
+
}
|
|
3317
|
+
continue;
|
|
3318
|
+
}
|
|
3319
|
+
if (registry.get(kind, newId) && !force) {
|
|
3320
|
+
errors.push(
|
|
3321
|
+
`${kind} "${newId}" already exists; pass --force to merge the ids`
|
|
3322
|
+
);
|
|
3323
|
+
continue;
|
|
3324
|
+
}
|
|
3325
|
+
const quoteAs = (content, start) => {
|
|
3326
|
+
const q = content[start];
|
|
3327
|
+
return q === '"' || q === "'" || q === "`" ? `${q}${newId}${q}` : `"${newId}"`;
|
|
3328
|
+
};
|
|
3329
|
+
for (const ef of extracted) {
|
|
3330
|
+
for (const a of ef.annotations) {
|
|
3331
|
+
if (a.kind !== kind || a.id !== oldId) continue;
|
|
3332
|
+
if (a.span) {
|
|
3333
|
+
edits.push({
|
|
3334
|
+
path: ef.file.sourcePath,
|
|
3335
|
+
start: a.span.start,
|
|
3336
|
+
end: a.span.end,
|
|
3337
|
+
replacement: quoteAs(ef.file.content, a.span.start)
|
|
3338
|
+
});
|
|
3339
|
+
} else {
|
|
3340
|
+
manual.push({
|
|
3341
|
+
file: a.file,
|
|
3342
|
+
line: a.line,
|
|
3343
|
+
reason: "attribute value is not a plain string literal (const reference, ternary, or template)"
|
|
3344
|
+
});
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
for (const m of ef.metadata ?? []) {
|
|
3348
|
+
if (kind === "widget" && m.kind === "widget" && m.id === oldId) {
|
|
3349
|
+
if (m.idSpan) {
|
|
3350
|
+
edits.push({
|
|
3351
|
+
path: ef.file.sourcePath,
|
|
3352
|
+
start: m.idSpan.start,
|
|
3353
|
+
end: m.idSpan.end,
|
|
3354
|
+
replacement: quoteAs(ef.file.content, m.idSpan.start)
|
|
3355
|
+
});
|
|
3356
|
+
} else {
|
|
3357
|
+
manual.push({
|
|
3358
|
+
file: ef.file.displayPath,
|
|
3359
|
+
line: m.loc.line ?? 1,
|
|
3360
|
+
reason: "widget export id is not a plain string literal"
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
if (kind === "widget" && m.widgets) {
|
|
3365
|
+
for (let i = 0; i < m.widgets.length; i++) {
|
|
3366
|
+
if (m.widgets[i] !== oldId) continue;
|
|
3367
|
+
const span = m.widgetSpans?.[i];
|
|
3368
|
+
if (span) {
|
|
3369
|
+
edits.push({
|
|
3370
|
+
path: ef.file.sourcePath,
|
|
3371
|
+
start: span.start,
|
|
3372
|
+
end: span.end,
|
|
3373
|
+
replacement: quoteAs(ef.file.content, span.start)
|
|
3374
|
+
});
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
for (const ef of extractedFlows) {
|
|
3381
|
+
for (const fact of ef.flows ?? []) {
|
|
3382
|
+
for (const call of fact.calls) {
|
|
3383
|
+
if (call.id !== oldId) continue;
|
|
3384
|
+
if (call.span) {
|
|
3385
|
+
edits.push({
|
|
3386
|
+
path: ef.file.sourcePath,
|
|
3387
|
+
start: call.span.start,
|
|
3388
|
+
end: call.span.end,
|
|
3389
|
+
replacement: quoteAs(ef.file.content, call.span.start)
|
|
3390
|
+
});
|
|
3391
|
+
} else {
|
|
3392
|
+
manual.push({
|
|
3393
|
+
file: ef.file.displayPath,
|
|
3394
|
+
line: call.line,
|
|
3395
|
+
reason: "uidex() argument is not a plain string literal"
|
|
3396
|
+
});
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
if (errors.length > 0) {
|
|
3403
|
+
return { edits: 0, manual, errors };
|
|
3404
|
+
}
|
|
3405
|
+
if (edits.length === 0 && manual.length === 0) {
|
|
3406
|
+
return {
|
|
3407
|
+
edits: 0,
|
|
3408
|
+
manual,
|
|
3409
|
+
errors: [
|
|
3410
|
+
`${kind} "${oldId}" has no editable occurrences (convention-derived ids like landmarks cannot be renamed)`
|
|
3411
|
+
]
|
|
3412
|
+
};
|
|
3413
|
+
}
|
|
3414
|
+
const result2 = applyFixes([
|
|
3415
|
+
{
|
|
3416
|
+
code: "rename",
|
|
3417
|
+
severity: "info",
|
|
3418
|
+
message: "",
|
|
3419
|
+
fix: {
|
|
3420
|
+
description: `Rename ${kind} "${oldId}" to "${newId}"`,
|
|
3421
|
+
edits
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
]);
|
|
3425
|
+
if (result2.skipped.length > 0) {
|
|
3426
|
+
errors.push(`Some edits were skipped: ${result2.skipped[0].reason}`);
|
|
3427
|
+
}
|
|
3428
|
+
for (const r of runScan({ cwd })) writeScanResult(r);
|
|
3429
|
+
return { edits: edits.length, manual, errors };
|
|
3235
3430
|
}
|
|
3236
3431
|
|
|
3237
3432
|
// src/scanner/scan/scaffold.ts
|
|
3238
|
-
var
|
|
3239
|
-
var
|
|
3240
|
-
function
|
|
3433
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
3434
|
+
var path10 = __toESM(require("path"), 1);
|
|
3435
|
+
function scaffoldSpec(opts) {
|
|
3241
3436
|
const {
|
|
3242
3437
|
registry,
|
|
3243
|
-
|
|
3438
|
+
kind,
|
|
3439
|
+
id,
|
|
3244
3440
|
outDir,
|
|
3245
3441
|
force = false,
|
|
3246
3442
|
fixtureImport = "./fixtures"
|
|
3247
3443
|
} = opts;
|
|
3248
|
-
const
|
|
3249
|
-
if (!
|
|
3250
|
-
throw new Error(
|
|
3251
|
-
}
|
|
3252
|
-
const criteria =
|
|
3253
|
-
const filename = `widget-${
|
|
3254
|
-
const outputPath =
|
|
3255
|
-
if (
|
|
3444
|
+
const entity = registry.get(kind, id);
|
|
3445
|
+
if (!entity) {
|
|
3446
|
+
throw new Error(`${capitalize(kind)} "${id}" not found in registry`);
|
|
3447
|
+
}
|
|
3448
|
+
const criteria = entity.meta?.acceptance ?? [];
|
|
3449
|
+
const filename = kind === "widget" ? `widget-${id}.spec.ts` : `flow-${id}.spec.ts`;
|
|
3450
|
+
const outputPath = path10.resolve(outDir, filename);
|
|
3451
|
+
if (fs7.existsSync(outputPath) && !force) {
|
|
3256
3452
|
return {
|
|
3257
3453
|
outputPath,
|
|
3258
3454
|
written: false,
|
|
@@ -3260,15 +3456,14 @@ function scaffoldWidgetSpec(opts) {
|
|
|
3260
3456
|
reason: `spec already exists at ${outputPath}; pass --force to overwrite`
|
|
3261
3457
|
};
|
|
3262
3458
|
}
|
|
3263
|
-
const content = renderSpec({
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
fixtureImport
|
|
3267
|
-
});
|
|
3268
|
-
fs6.mkdirSync(path8.dirname(outputPath), { recursive: true });
|
|
3269
|
-
fs6.writeFileSync(outputPath, content, "utf8");
|
|
3459
|
+
const content = renderSpec({ id, criteria, fixtureImport });
|
|
3460
|
+
fs7.mkdirSync(path10.dirname(outputPath), { recursive: true });
|
|
3461
|
+
fs7.writeFileSync(outputPath, content, "utf8");
|
|
3270
3462
|
return { outputPath, written: true, skipped: false };
|
|
3271
3463
|
}
|
|
3464
|
+
function capitalize(s) {
|
|
3465
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
3466
|
+
}
|
|
3272
3467
|
function renderSpec(args) {
|
|
3273
3468
|
const lines = [];
|
|
3274
3469
|
lines.push(
|
|
@@ -3276,7 +3471,7 @@ function renderSpec(args) {
|
|
|
3276
3471
|
);
|
|
3277
3472
|
lines.push("");
|
|
3278
3473
|
lines.push(
|
|
3279
|
-
`test.describe(${JSON.stringify(args.
|
|
3474
|
+
`test.describe(${JSON.stringify(args.id)}, { tag: "@uidex:flow" }, () => {`
|
|
3280
3475
|
);
|
|
3281
3476
|
if (args.criteria.length === 0) {
|
|
3282
3477
|
lines.push(` test("TODO: add acceptance criteria", async () => {`);
|
|
@@ -3342,6 +3537,8 @@ async function run(opts) {
|
|
|
3342
3537
|
return runScanCommand(cwd, flags, writer);
|
|
3343
3538
|
case "scaffold":
|
|
3344
3539
|
return runScaffold(cwd, positional.slice(1), flags, writer);
|
|
3540
|
+
case "rename":
|
|
3541
|
+
return runRename(cwd, positional.slice(1), flags, writer);
|
|
3345
3542
|
case "ai": {
|
|
3346
3543
|
const result2 = await runAiCommand({
|
|
3347
3544
|
cwd,
|
|
@@ -3368,7 +3565,8 @@ function helpText2() {
|
|
|
3368
3565
|
"Commands:",
|
|
3369
3566
|
" init Create a .uidex.json",
|
|
3370
3567
|
" scan [flags] Run the scanner pipeline",
|
|
3371
|
-
" scaffold widget <id> Emit a Playwright spec from
|
|
3568
|
+
" scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
|
|
3569
|
+
" rename <element|widget|region> <old-id> <new-id> Rename an id everywhere (DOM attr, flows, exports)",
|
|
3372
3570
|
" ai <install|uninstall|providers> Manage AI assistant integrations",
|
|
3373
3571
|
" api <METHOD> <PATH> Call the uidex API",
|
|
3374
3572
|
" api --list Show available API routes",
|
|
@@ -3377,16 +3575,17 @@ function helpText2() {
|
|
|
3377
3575
|
"",
|
|
3378
3576
|
"Flags:",
|
|
3379
3577
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
3380
|
-
" --lint Run lint diagnostics (missing annotations, scope leak,
|
|
3578
|
+
" --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
|
|
3381
3579
|
" --audit Equivalent to --check --lint (read-only)",
|
|
3580
|
+
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
|
|
3382
3581
|
" --json Emit JSON diagnostics on stdout",
|
|
3383
3582
|
" --force (scaffold) overwrite existing spec",
|
|
3384
3583
|
""
|
|
3385
3584
|
].join("\n");
|
|
3386
3585
|
}
|
|
3387
3586
|
function runInit(cwd, w) {
|
|
3388
|
-
const configPath =
|
|
3389
|
-
if (
|
|
3587
|
+
const configPath = path11.join(cwd, CONFIG_FILENAME);
|
|
3588
|
+
if (fs8.existsSync(configPath)) {
|
|
3390
3589
|
w.err(`.uidex.json already exists at ${configPath}`);
|
|
3391
3590
|
return w.result(1);
|
|
3392
3591
|
}
|
|
@@ -3395,16 +3594,16 @@ function runInit(cwd, w) {
|
|
|
3395
3594
|
sources: [{ rootDir: "src" }],
|
|
3396
3595
|
output: "src/uidex.gen.ts"
|
|
3397
3596
|
};
|
|
3398
|
-
|
|
3597
|
+
fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3399
3598
|
w.out(`Created ${configPath}`);
|
|
3400
|
-
const gitignorePath =
|
|
3599
|
+
const gitignorePath = path11.join(cwd, ".gitignore");
|
|
3401
3600
|
const entry = "*.gen.ts";
|
|
3402
|
-
if (
|
|
3403
|
-
const existing =
|
|
3601
|
+
if (fs8.existsSync(gitignorePath)) {
|
|
3602
|
+
const existing = fs8.readFileSync(gitignorePath, "utf8");
|
|
3404
3603
|
const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
|
|
3405
3604
|
if (!hasEntry) {
|
|
3406
3605
|
const needsNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
3407
|
-
|
|
3606
|
+
fs8.appendFileSync(
|
|
3408
3607
|
gitignorePath,
|
|
3409
3608
|
`${needsNewline ? "\n" : ""}${entry}
|
|
3410
3609
|
`,
|
|
@@ -3413,21 +3612,33 @@ function runInit(cwd, w) {
|
|
|
3413
3612
|
w.out(`Appended ${entry} to ${gitignorePath}`);
|
|
3414
3613
|
}
|
|
3415
3614
|
} else {
|
|
3416
|
-
|
|
3615
|
+
fs8.writeFileSync(gitignorePath, `${entry}
|
|
3417
3616
|
`, "utf8");
|
|
3418
3617
|
w.out(`Created ${gitignorePath} with ${entry}`);
|
|
3419
3618
|
}
|
|
3420
3619
|
return w.result(0);
|
|
3421
3620
|
}
|
|
3422
3621
|
function runScanCommand(cwd, flags, w) {
|
|
3423
|
-
const
|
|
3424
|
-
const
|
|
3622
|
+
const fix = Boolean(flags.fix);
|
|
3623
|
+
const check = !fix && Boolean(flags.check || flags.audit);
|
|
3624
|
+
const lint = Boolean(flags.lint || flags.audit || fix);
|
|
3425
3625
|
const asJson = Boolean(flags.json);
|
|
3426
|
-
|
|
3626
|
+
let configs = discover({ cwd });
|
|
3427
3627
|
if (configs.length === 0) {
|
|
3428
3628
|
w.err(`No ${CONFIG_FILENAME} found under ${cwd}`);
|
|
3429
3629
|
return w.result(1);
|
|
3430
3630
|
}
|
|
3631
|
+
let fixed = [];
|
|
3632
|
+
let fixSkipped = [];
|
|
3633
|
+
if (fix) {
|
|
3634
|
+
const discovery = runScan({ cwd, check: true, lint: true, configs });
|
|
3635
|
+
const result2 = applyFixes(
|
|
3636
|
+
discovery.flatMap((r) => r.audit?.diagnostics ?? [])
|
|
3637
|
+
);
|
|
3638
|
+
fixed = result2.applied;
|
|
3639
|
+
fixSkipped = result2.skipped;
|
|
3640
|
+
configs = discover({ cwd });
|
|
3641
|
+
}
|
|
3431
3642
|
const results = runScan({ cwd, check, lint, configs });
|
|
3432
3643
|
if (!check) {
|
|
3433
3644
|
for (const r of results) writeScanResult(r);
|
|
@@ -3442,9 +3653,21 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3442
3653
|
{ errors: 0, warnings: 0 }
|
|
3443
3654
|
);
|
|
3444
3655
|
if (asJson) {
|
|
3445
|
-
const out2 = {
|
|
3656
|
+
const out2 = {
|
|
3657
|
+
diagnostics: allDiagnostics.map(jsonDiagnostic),
|
|
3658
|
+
summary,
|
|
3659
|
+
...fix ? { fixed, fixSkipped } : {}
|
|
3660
|
+
};
|
|
3446
3661
|
w.out(JSON.stringify(out2, null, 2));
|
|
3447
3662
|
} else {
|
|
3663
|
+
for (const f of fixed) {
|
|
3664
|
+
w.out(`FIXED [${f.code}] ${f.file ?? ""} ${f.description}`);
|
|
3665
|
+
}
|
|
3666
|
+
for (const s of fixSkipped) {
|
|
3667
|
+
w.out(
|
|
3668
|
+
`SKIPPED [${s.code}] ${s.file ?? ""} ${s.description} (${s.reason})`
|
|
3669
|
+
);
|
|
3670
|
+
}
|
|
3448
3671
|
for (const r of results) {
|
|
3449
3672
|
if (check) {
|
|
3450
3673
|
w.out(`Checked ${r.outputPath}`);
|
|
@@ -3454,7 +3677,10 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3454
3677
|
for (const d of r.audit?.diagnostics ?? []) {
|
|
3455
3678
|
const loc = d.file ? `${d.file}${d.line ? `:${d.line}` : ""}` : "";
|
|
3456
3679
|
const stream = d.severity === "error" ? w.err : w.out;
|
|
3457
|
-
|
|
3680
|
+
const fixable = d.fix && !fix ? " [fixable: run with --fix]" : "";
|
|
3681
|
+
stream(
|
|
3682
|
+
`${d.severity.toUpperCase()} [${d.code}] ${loc} ${d.message}${fixable}`
|
|
3683
|
+
);
|
|
3458
3684
|
if (d.hint) stream(` hint: ${d.hint}`);
|
|
3459
3685
|
}
|
|
3460
3686
|
}
|
|
@@ -3465,20 +3691,27 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3465
3691
|
const exit = summary.errors > 0 ? 1 : 0;
|
|
3466
3692
|
return w.result(exit);
|
|
3467
3693
|
}
|
|
3694
|
+
function jsonDiagnostic(d) {
|
|
3695
|
+
const { fix, ...rest } = d;
|
|
3696
|
+
return fix ? { ...rest, fixable: true } : rest;
|
|
3697
|
+
}
|
|
3698
|
+
var SCAFFOLD_KINDS = /* @__PURE__ */ new Set(["widget", "page", "feature"]);
|
|
3468
3699
|
function runScaffold(cwd, args, flags, w) {
|
|
3469
3700
|
const [kind, id] = args;
|
|
3470
|
-
if (kind
|
|
3471
|
-
w.err("Usage: uidex scaffold widget <id> [--force]");
|
|
3701
|
+
if (!kind || !SCAFFOLD_KINDS.has(kind) || !id) {
|
|
3702
|
+
w.err("Usage: uidex scaffold <widget|page|feature> <id> [--force]");
|
|
3472
3703
|
return w.result(1);
|
|
3473
3704
|
}
|
|
3705
|
+
const scaffoldKind = kind;
|
|
3474
3706
|
const results = runScan({ cwd });
|
|
3475
3707
|
for (const r of results) {
|
|
3476
|
-
const
|
|
3477
|
-
if (!
|
|
3478
|
-
const outDir =
|
|
3479
|
-
const result2 =
|
|
3708
|
+
const entity = r.registry.get(scaffoldKind, id);
|
|
3709
|
+
if (!entity) continue;
|
|
3710
|
+
const outDir = path11.resolve(r.configDir, "e2e");
|
|
3711
|
+
const result2 = scaffoldSpec({
|
|
3480
3712
|
registry: r.registry,
|
|
3481
|
-
|
|
3713
|
+
kind: scaffoldKind,
|
|
3714
|
+
id,
|
|
3482
3715
|
outDir,
|
|
3483
3716
|
force: Boolean(flags.force)
|
|
3484
3717
|
});
|
|
@@ -3489,9 +3722,43 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
3489
3722
|
w.out(`Wrote ${result2.outputPath}`);
|
|
3490
3723
|
return w.result(0);
|
|
3491
3724
|
}
|
|
3492
|
-
w.err(
|
|
3725
|
+
w.err(
|
|
3726
|
+
`${scaffoldKind.charAt(0).toUpperCase() + scaffoldKind.slice(1)} "${id}" not found in registry`
|
|
3727
|
+
);
|
|
3493
3728
|
return w.result(1);
|
|
3494
3729
|
}
|
|
3730
|
+
var RENAME_KINDS = /* @__PURE__ */ new Set(["element", "widget", "region"]);
|
|
3731
|
+
function runRename(cwd, args, flags, w) {
|
|
3732
|
+
const [kind, oldId, newId] = args;
|
|
3733
|
+
if (!kind || !RENAME_KINDS.has(kind) || !oldId || !newId) {
|
|
3734
|
+
w.err(
|
|
3735
|
+
"Usage: uidex rename <element|widget|region> <old-id> <new-id> [--force]"
|
|
3736
|
+
);
|
|
3737
|
+
return w.result(1);
|
|
3738
|
+
}
|
|
3739
|
+
const result2 = renameEntity({
|
|
3740
|
+
cwd,
|
|
3741
|
+
kind,
|
|
3742
|
+
oldId,
|
|
3743
|
+
newId,
|
|
3744
|
+
force: Boolean(flags.force)
|
|
3745
|
+
});
|
|
3746
|
+
for (const e of result2.errors) w.err(e);
|
|
3747
|
+
for (const m of result2.manual) {
|
|
3748
|
+
w.err(`MANUAL ${m.file}:${m.line} \u2014 ${m.reason}`);
|
|
3749
|
+
}
|
|
3750
|
+
if (result2.errors.length > 0) return w.result(1);
|
|
3751
|
+
w.out(
|
|
3752
|
+
`Renamed ${kind} "${oldId}" \u2192 "${newId}" (${result2.edits} edit(s)); gen file regenerated`
|
|
3753
|
+
);
|
|
3754
|
+
if (result2.manual.length > 0) {
|
|
3755
|
+
w.err(
|
|
3756
|
+
`${result2.manual.length} occurrence(s) need manual follow-up (listed above)`
|
|
3757
|
+
);
|
|
3758
|
+
return w.result(1);
|
|
3759
|
+
}
|
|
3760
|
+
return w.result(0);
|
|
3761
|
+
}
|
|
3495
3762
|
function createWriter() {
|
|
3496
3763
|
let stdout = "";
|
|
3497
3764
|
let stderr = "";
|
|
@@ -3551,15 +3818,15 @@ function createFileTokenStorage(options) {
|
|
|
3551
3818
|
}
|
|
3552
3819
|
function defaultTokenPath() {
|
|
3553
3820
|
const os = require("os");
|
|
3554
|
-
const
|
|
3555
|
-
return
|
|
3821
|
+
const path12 = require("path");
|
|
3822
|
+
return path12.join(os.homedir(), ".uidex", "cloud-token.json");
|
|
3556
3823
|
}
|
|
3557
3824
|
|
|
3558
3825
|
// src/scanner/cli/http.ts
|
|
3559
3826
|
function createHttpClient(options) {
|
|
3560
3827
|
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
3561
|
-
async function request(
|
|
3562
|
-
let url = `${baseUrl}${
|
|
3828
|
+
async function request(path12, opts = {}) {
|
|
3829
|
+
let url = `${baseUrl}${path12.startsWith("/") ? path12 : `/${path12}`}`;
|
|
3563
3830
|
if (opts.query) {
|
|
3564
3831
|
url += (url.includes("?") ? "&" : "?") + opts.query;
|
|
3565
3832
|
}
|
|
@@ -3947,9 +4214,9 @@ var API_ROUTES = [
|
|
|
3947
4214
|
},
|
|
3948
4215
|
{
|
|
3949
4216
|
"method": "POST",
|
|
3950
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/reports/
|
|
3951
|
-
"operationId": "
|
|
3952
|
-
"summary": "
|
|
4217
|
+
"path": "/api/organizations/{orgId}/projects/{projectId}/reports/bulk-status",
|
|
4218
|
+
"operationId": "bulkUpdateReportStatus",
|
|
4219
|
+
"summary": "Update status on multiple feedback records.",
|
|
3953
4220
|
"tag": "Reports",
|
|
3954
4221
|
"params": [
|
|
3955
4222
|
"orgId",
|
|
@@ -4171,17 +4438,17 @@ async function runApiCommand(opts) {
|
|
|
4171
4438
|
if (sub === "login") return runLogin(ctx);
|
|
4172
4439
|
if (sub === "status") return runStatus(ctx);
|
|
4173
4440
|
const method = sub?.toUpperCase();
|
|
4174
|
-
const
|
|
4441
|
+
const path12 = positional[1];
|
|
4175
4442
|
if (!method || !METHODS.has(method)) {
|
|
4176
4443
|
stderr.push(`Unknown command or method: ${sub}`);
|
|
4177
4444
|
stderr.push(HELP);
|
|
4178
4445
|
return result(1, stdout, stderr);
|
|
4179
4446
|
}
|
|
4180
|
-
if (!
|
|
4447
|
+
if (!path12) {
|
|
4181
4448
|
stderr.push("Missing path. Usage: uidex api GET /api/organizations");
|
|
4182
4449
|
return result(1, stdout, stderr);
|
|
4183
4450
|
}
|
|
4184
|
-
return runRequest(ctx, method,
|
|
4451
|
+
return runRequest(ctx, method, path12);
|
|
4185
4452
|
}
|
|
4186
4453
|
function listRoutes(ctx) {
|
|
4187
4454
|
const { flags, color, stdout, stderr } = ctx;
|
|
@@ -4220,7 +4487,7 @@ function listRoutes(ctx) {
|
|
|
4220
4487
|
return result(0, stdout, stderr);
|
|
4221
4488
|
}
|
|
4222
4489
|
async function runLogin(ctx) {
|
|
4223
|
-
const { flags, opts,
|
|
4490
|
+
const { flags, opts, stdout, stderr } = ctx;
|
|
4224
4491
|
const token = flags.token;
|
|
4225
4492
|
if (typeof token === "string" && token.length > 0) {
|
|
4226
4493
|
const storage = resolveTokenStorage(opts);
|
|
@@ -4300,7 +4567,7 @@ async function runBrowserLogin(ctx) {
|
|
|
4300
4567
|
stdout.push("");
|
|
4301
4568
|
process.stdout.write(result(0, stdout, stderr).stdout);
|
|
4302
4569
|
stdout.length = 0;
|
|
4303
|
-
openBrowser(authUrl);
|
|
4570
|
+
openBrowser(authUrl, exec);
|
|
4304
4571
|
});
|
|
4305
4572
|
server.on("error", (err2) => {
|
|
4306
4573
|
stderr.push(`Failed to start local server: ${err2.message}`);
|
|
@@ -4308,8 +4575,7 @@ async function runBrowserLogin(ctx) {
|
|
|
4308
4575
|
});
|
|
4309
4576
|
});
|
|
4310
4577
|
}
|
|
4311
|
-
function openBrowser(url) {
|
|
4312
|
-
const { exec } = require("child_process");
|
|
4578
|
+
function openBrowser(url, exec) {
|
|
4313
4579
|
const platform = process.platform;
|
|
4314
4580
|
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
4315
4581
|
exec(`${cmd} ${JSON.stringify(url)}`);
|
|
@@ -4323,7 +4589,7 @@ function runStatus(ctx) {
|
|
|
4323
4589
|
stdout.push(`auth: ${token ? "authenticated" : "not authenticated"}`);
|
|
4324
4590
|
return result(token ? 0 : 1, stdout, stderr);
|
|
4325
4591
|
}
|
|
4326
|
-
async function runRequest(ctx, method,
|
|
4592
|
+
async function runRequest(ctx, method, path12) {
|
|
4327
4593
|
const { flags, opts, color, stdout, stderr } = ctx;
|
|
4328
4594
|
const tokenFromFlag = flags.token;
|
|
4329
4595
|
const token = typeof tokenFromFlag === "string" ? tokenFromFlag : resolveTokenStorage(opts).get();
|
|
@@ -4342,7 +4608,7 @@ async function runRequest(ctx, method, path10) {
|
|
|
4342
4608
|
return result(1, stdout, stderr);
|
|
4343
4609
|
}
|
|
4344
4610
|
}
|
|
4345
|
-
const res = await http.request(
|
|
4611
|
+
const res = await http.request(path12, {
|
|
4346
4612
|
method,
|
|
4347
4613
|
body: bodyStr,
|
|
4348
4614
|
query: typeof flags.query === "string" ? flags.query : void 0
|