uidex 0.5.2 → 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 +1542 -1227
- 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 +116 -251
- package/dist/headless/index.cjs.map +1 -1
- package/dist/headless/index.d.cts +6 -11
- package/dist/headless/index.d.ts +6 -11
- package/dist/headless/index.js +116 -253
- package/dist/headless/index.js.map +1 -1
- package/dist/index.cjs +776 -1055
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +152 -160
- package/dist/index.d.ts +152 -160
- package/dist/index.js +792 -1066
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +801 -1019
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +102 -86
- package/dist/react/index.d.ts +102 -86
- package/dist/react/index.js +821 -1038
- package/dist/react/index.js.map +1 -1
- package/dist/scan/index.cjs +1550 -1220
- package/dist/scan/index.cjs.map +1 -1
- package/dist/scan/index.d.cts +210 -12
- package/dist/scan/index.d.ts +210 -12
- package/dist/scan/index.js +1547 -1219
- 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 = [
|
|
@@ -533,6 +651,7 @@ function freezeEntity(entity, flows) {
|
|
|
533
651
|
function createRegistry() {
|
|
534
652
|
const store = emptyStore();
|
|
535
653
|
let flowsCache = null;
|
|
654
|
+
const patternCache = /* @__PURE__ */ new Map();
|
|
536
655
|
const getFlows = () => {
|
|
537
656
|
if (flowsCache === null) flowsCache = Array.from(store.flow.values());
|
|
538
657
|
return flowsCache;
|
|
@@ -542,6 +661,7 @@ function createRegistry() {
|
|
|
542
661
|
const key = entityKey(entity);
|
|
543
662
|
store[entity.kind].set(key, entity);
|
|
544
663
|
flowsCache = null;
|
|
664
|
+
patternCache.delete(entity.kind);
|
|
545
665
|
};
|
|
546
666
|
const get = (kind, id) => {
|
|
547
667
|
assertEntityKind(kind);
|
|
@@ -549,6 +669,51 @@ function createRegistry() {
|
|
|
549
669
|
if (raw === void 0) return void 0;
|
|
550
670
|
return freezeEntity(raw, getFlows());
|
|
551
671
|
};
|
|
672
|
+
const getPatternsForKind = (kind) => {
|
|
673
|
+
const cached = patternCache.get(kind);
|
|
674
|
+
if (cached !== void 0) return cached;
|
|
675
|
+
const patterns = [];
|
|
676
|
+
for (const [key, entity] of store[kind]) {
|
|
677
|
+
if (key.includes("*")) {
|
|
678
|
+
const segments = key.split("*");
|
|
679
|
+
patterns.push({
|
|
680
|
+
segments,
|
|
681
|
+
staticLength: segments.reduce((n, s) => n + s.length, 0),
|
|
682
|
+
entity
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
patternCache.set(
|
|
687
|
+
kind,
|
|
688
|
+
patterns
|
|
689
|
+
);
|
|
690
|
+
return patterns;
|
|
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
|
+
};
|
|
704
|
+
const matchPattern = (kind, id) => {
|
|
705
|
+
assertEntityKind(kind);
|
|
706
|
+
const patterns = getPatternsForKind(kind);
|
|
707
|
+
if (patterns.length === 0) return void 0;
|
|
708
|
+
let best;
|
|
709
|
+
for (const entry of patterns) {
|
|
710
|
+
if (matchesSegments(entry.segments, id) && (best === void 0 || entry.staticLength > best.staticLength)) {
|
|
711
|
+
best = entry;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (best === void 0) return void 0;
|
|
715
|
+
return freezeEntity(best.entity, getFlows());
|
|
716
|
+
};
|
|
552
717
|
const list = (kind) => {
|
|
553
718
|
assertEntityKind(kind);
|
|
554
719
|
const flows = getFlows();
|
|
@@ -599,6 +764,7 @@ function createRegistry() {
|
|
|
599
764
|
return {
|
|
600
765
|
add,
|
|
601
766
|
get,
|
|
767
|
+
matchPattern,
|
|
602
768
|
list,
|
|
603
769
|
query,
|
|
604
770
|
byScope,
|
|
@@ -611,7 +777,6 @@ function createRegistry() {
|
|
|
611
777
|
}
|
|
612
778
|
|
|
613
779
|
// src/scanner/scan/audit.ts
|
|
614
|
-
var MARKER_FILENAMES = ["UIDEX_PAGE.md", "UIDEX_FEATURE.md"];
|
|
615
780
|
function audit(opts) {
|
|
616
781
|
const diagnostics = [];
|
|
617
782
|
const { registry, extracted, files, config } = opts;
|
|
@@ -621,22 +786,15 @@ function audit(opts) {
|
|
|
621
786
|
const scopeLeakEnabled = config.audit?.scopeLeak ?? true;
|
|
622
787
|
const coverageEnabled = config.audit?.coverage ?? true;
|
|
623
788
|
if (opts.resolveDiagnostics) diagnostics.push(...opts.resolveDiagnostics);
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
code: "marker-md-ignored",
|
|
630
|
-
severity: "warning",
|
|
631
|
-
message: `Marker file "${base}" is ignored in v2; migrate to \`export const uidex\``,
|
|
632
|
-
file: f.displayPath
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
}
|
|
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);
|
|
636
794
|
}
|
|
637
795
|
if (check && opts.generated !== void 0) {
|
|
638
796
|
const outRel = opts.outputPath ?? config.output;
|
|
639
|
-
const fresh =
|
|
797
|
+
const fresh = normalizeForCheck(opts.generated);
|
|
640
798
|
if (opts.existingOnDisk === null || opts.existingOnDisk === void 0) {
|
|
641
799
|
diagnostics.push({
|
|
642
800
|
code: "gen-missing",
|
|
@@ -646,7 +804,7 @@ function audit(opts) {
|
|
|
646
804
|
hint: "Run `uidex scan` (without --check) to regenerate"
|
|
647
805
|
});
|
|
648
806
|
} else {
|
|
649
|
-
const existing =
|
|
807
|
+
const existing = normalizeForCheck(opts.existingOnDisk);
|
|
650
808
|
if (existing !== fresh) {
|
|
651
809
|
const changed = diffEntities(existing, opts.generated, registry);
|
|
652
810
|
const summary2 = formatChangedSummary(changed);
|
|
@@ -660,22 +818,6 @@ function audit(opts) {
|
|
|
660
818
|
}
|
|
661
819
|
}
|
|
662
820
|
}
|
|
663
|
-
if (lint) {
|
|
664
|
-
for (const ef of extracted) {
|
|
665
|
-
for (const a of ef.annotations) {
|
|
666
|
-
const migration = legacyJsdocMigration(a);
|
|
667
|
-
if (!migration) continue;
|
|
668
|
-
diagnostics.push({
|
|
669
|
-
code: "legacy-jsdoc",
|
|
670
|
-
severity: "warning",
|
|
671
|
-
message: migration.message,
|
|
672
|
-
file: a.file,
|
|
673
|
-
line: a.line,
|
|
674
|
-
hint: migration.hint
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
821
|
if (lint && acceptanceEnabled) {
|
|
680
822
|
for (const kind of ["widget", "feature", "page"]) {
|
|
681
823
|
for (const e of registry.list(kind)) {
|
|
@@ -721,8 +863,8 @@ function audit(opts) {
|
|
|
721
863
|
if (typeof m.id !== "string") continue;
|
|
722
864
|
const filePath = ef.file.displayPath;
|
|
723
865
|
const wellKnownName = WELL_KNOWN_FILES[m.kind];
|
|
724
|
-
if (
|
|
725
|
-
const dir =
|
|
866
|
+
if (path5.posix.basename(filePath) === wellKnownName) continue;
|
|
867
|
+
const dir = path5.posix.dirname(filePath);
|
|
726
868
|
const wellKnownPath = dir === "." ? wellKnownName : `${dir}/${wellKnownName}`;
|
|
727
869
|
if (scannedPaths.has(wellKnownPath)) continue;
|
|
728
870
|
const kindLabel = m.kind === "page" ? "Page" : "Feature";
|
|
@@ -739,45 +881,55 @@ function audit(opts) {
|
|
|
739
881
|
}
|
|
740
882
|
}
|
|
741
883
|
if (lint) {
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
let m;
|
|
745
|
-
dynamicAttrRe.lastIndex = 0;
|
|
746
|
-
while ((m = dynamicAttrRe.exec(f.content)) !== null) {
|
|
747
|
-
const kind = m[1] ?? "element";
|
|
748
|
-
let line = 1;
|
|
749
|
-
for (let i = 0; i < m.index; i++) if (f.content[i] === "\n") line++;
|
|
750
|
-
const attrName = m[1] ? `data-uidex-${m[1]}` : "data-uidex";
|
|
884
|
+
for (const ef of extracted) {
|
|
885
|
+
for (const fact of ef.dynamicAttrs ?? []) {
|
|
751
886
|
diagnostics.push({
|
|
752
887
|
code: "dynamic-attr",
|
|
753
888
|
severity: "warning",
|
|
754
|
-
message: `\`${attrName}={\u2026}\` uses a dynamic expression; the scanner cannot resolve the ${kind} id statically`,
|
|
755
|
-
file:
|
|
756
|
-
line,
|
|
757
|
-
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)
|
|
758
893
|
});
|
|
759
894
|
}
|
|
760
895
|
}
|
|
761
896
|
}
|
|
762
897
|
if (lint) {
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
+
}
|
|
781
933
|
}
|
|
782
934
|
}
|
|
783
935
|
}
|
|
@@ -794,12 +946,11 @@ function audit(opts) {
|
|
|
794
946
|
}
|
|
795
947
|
}
|
|
796
948
|
}
|
|
797
|
-
for (const
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
const
|
|
802
|
-
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() ?? "";
|
|
803
954
|
const primitive = byName.get(
|
|
804
955
|
baseName2.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
|
|
805
956
|
);
|
|
@@ -807,81 +958,170 @@ function audit(opts) {
|
|
|
807
958
|
const scope = primitive.scopes?.[0];
|
|
808
959
|
if (!scope) continue;
|
|
809
960
|
const [kind, id] = scope.split(":");
|
|
810
|
-
const importerSegments =
|
|
961
|
+
const importerSegments = displayPath.split("/");
|
|
811
962
|
if (importerSegments.includes(id) && importerSegments.includes(kind + "s")) {
|
|
812
963
|
continue;
|
|
813
964
|
}
|
|
814
965
|
if (kind === "feature" && importerSegments.includes(id)) continue;
|
|
815
|
-
if (kind === "feature" && declaredFeatures.get(
|
|
966
|
+
if (kind === "feature" && declaredFeatures.get(displayPath)?.has(id)) {
|
|
816
967
|
continue;
|
|
817
968
|
}
|
|
818
969
|
diagnostics.push({
|
|
819
970
|
code: "scope-leak",
|
|
820
971
|
severity: "warning",
|
|
821
|
-
message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${
|
|
822
|
-
file:
|
|
972
|
+
message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${displayPath}`,
|
|
973
|
+
file: displayPath,
|
|
974
|
+
line: imp.line
|
|
823
975
|
});
|
|
824
976
|
}
|
|
825
977
|
}
|
|
826
978
|
}
|
|
827
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
|
+
}
|
|
828
990
|
for (const flow of registry.list("flow")) {
|
|
991
|
+
const callLines = factsByLoc.get(`${flow.loc.file}:${flow.loc.line}`);
|
|
829
992
|
for (const touchedId of flow.touches) {
|
|
830
|
-
const found = registry.get("element", touchedId) ?? registry.get("widget", touchedId) ?? registry.get("region", touchedId);
|
|
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);
|
|
831
994
|
if (!found) {
|
|
832
995
|
diagnostics.push({
|
|
833
996
|
code: "unknown-reference",
|
|
834
997
|
severity: "warning",
|
|
835
998
|
message: `Flow "${flow.id}" references unknown entity "${touchedId}"`,
|
|
836
999
|
file: flow.loc.file,
|
|
837
|
-
|
|
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
|
|
838
1003
|
});
|
|
839
1004
|
}
|
|
840
1005
|
}
|
|
841
1006
|
}
|
|
842
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
|
+
}
|
|
843
1080
|
const summary = {
|
|
844
1081
|
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
845
1082
|
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
846
1083
|
};
|
|
847
1084
|
return { diagnostics, summary };
|
|
848
1085
|
}
|
|
849
|
-
function
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
};
|
|
878
|
-
default:
|
|
879
|
-
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;
|
|
880
1114
|
}
|
|
881
1115
|
}
|
|
882
1116
|
function normalizeLineEndings(s) {
|
|
883
1117
|
return s.replace(/\r\n/g, "\n");
|
|
884
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
|
+
}
|
|
885
1125
|
function formatChangedSummary(change) {
|
|
886
1126
|
const parts = [];
|
|
887
1127
|
const fmt = (kind, names) => {
|
|
@@ -980,62 +1220,11 @@ function extractEntitiesArray(source) {
|
|
|
980
1220
|
}
|
|
981
1221
|
return null;
|
|
982
1222
|
}
|
|
983
|
-
function findJsxOpeningEnd(src, start) {
|
|
984
|
-
let i = start;
|
|
985
|
-
while (i < src.length) {
|
|
986
|
-
const ch = src[i];
|
|
987
|
-
if (ch === ">" || ch === "/" && src[i + 1] === ">") return i;
|
|
988
|
-
if (ch === '"' || ch === "'" || ch === "`") {
|
|
989
|
-
i = skipString(src, i);
|
|
990
|
-
} else if (ch === "{") {
|
|
991
|
-
i = skipBraces(src, i);
|
|
992
|
-
} else {
|
|
993
|
-
i++;
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
return -1;
|
|
997
|
-
}
|
|
998
|
-
function skipString(src, start) {
|
|
999
|
-
const quote = src[start];
|
|
1000
|
-
let i = start + 1;
|
|
1001
|
-
while (i < src.length) {
|
|
1002
|
-
if (src[i] === "\\" && quote !== "`") {
|
|
1003
|
-
i += 2;
|
|
1004
|
-
continue;
|
|
1005
|
-
}
|
|
1006
|
-
if (quote === "`" && src[i] === "$" && src[i + 1] === "{") {
|
|
1007
|
-
i = skipBraces(src, i + 1);
|
|
1008
|
-
continue;
|
|
1009
|
-
}
|
|
1010
|
-
if (src[i] === quote) return i + 1;
|
|
1011
|
-
i++;
|
|
1012
|
-
}
|
|
1013
|
-
return i;
|
|
1014
|
-
}
|
|
1015
|
-
function skipBraces(src, start) {
|
|
1016
|
-
let depth = 1;
|
|
1017
|
-
let i = start + 1;
|
|
1018
|
-
while (i < src.length && depth > 0) {
|
|
1019
|
-
const ch = src[i];
|
|
1020
|
-
if (ch === "{") {
|
|
1021
|
-
depth++;
|
|
1022
|
-
i++;
|
|
1023
|
-
} else if (ch === "}") {
|
|
1024
|
-
depth--;
|
|
1025
|
-
i++;
|
|
1026
|
-
} else if (ch === '"' || ch === "'" || ch === "`") {
|
|
1027
|
-
i = skipString(src, i);
|
|
1028
|
-
} else {
|
|
1029
|
-
i++;
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
return i;
|
|
1033
|
-
}
|
|
1034
1223
|
function dynamicAttrHint(kind) {
|
|
1035
1224
|
if (kind === "region") {
|
|
1036
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`;
|
|
1037
1226
|
}
|
|
1038
|
-
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)`;
|
|
1039
1228
|
}
|
|
1040
1229
|
function stableStringify(value) {
|
|
1041
1230
|
return JSON.stringify(value, stableReplacer);
|
|
@@ -1068,9 +1257,7 @@ function replacerSorted(_key, value) {
|
|
|
1068
1257
|
}
|
|
1069
1258
|
return value;
|
|
1070
1259
|
}
|
|
1071
|
-
function emitIdUnion(name, ids
|
|
1072
|
-
if (typeMode === "loose") return `export type ${name} = string
|
|
1073
|
-
`;
|
|
1260
|
+
function emitIdUnion(name, ids) {
|
|
1074
1261
|
if (ids.length === 0) return `export type ${name} = never
|
|
1075
1262
|
`;
|
|
1076
1263
|
const sorted = [...ids].sort();
|
|
@@ -1080,12 +1267,7 @@ ${body}
|
|
|
1080
1267
|
`;
|
|
1081
1268
|
}
|
|
1082
1269
|
function emit(opts) {
|
|
1083
|
-
const {
|
|
1084
|
-
registry,
|
|
1085
|
-
gitContext,
|
|
1086
|
-
uidexImport = "uidex",
|
|
1087
|
-
typeMode = "strict"
|
|
1088
|
-
} = opts;
|
|
1270
|
+
const { registry, gitContext, uidexImport = "uidex" } = opts;
|
|
1089
1271
|
const routes = [...registry.list("route")].sort(
|
|
1090
1272
|
(a, b) => a.path.localeCompare(b.path)
|
|
1091
1273
|
);
|
|
@@ -1108,57 +1290,49 @@ function emit(opts) {
|
|
|
1108
1290
|
lines.push(
|
|
1109
1291
|
emitIdUnion(
|
|
1110
1292
|
"PageId",
|
|
1111
|
-
pages.map((e) => e.id)
|
|
1112
|
-
typeMode
|
|
1293
|
+
pages.map((e) => e.id)
|
|
1113
1294
|
)
|
|
1114
1295
|
);
|
|
1115
1296
|
lines.push(
|
|
1116
1297
|
emitIdUnion(
|
|
1117
1298
|
"FeatureId",
|
|
1118
|
-
features.map((e) => e.id)
|
|
1119
|
-
typeMode
|
|
1299
|
+
features.map((e) => e.id)
|
|
1120
1300
|
)
|
|
1121
1301
|
);
|
|
1122
1302
|
lines.push(
|
|
1123
1303
|
emitIdUnion(
|
|
1124
1304
|
"WidgetId",
|
|
1125
|
-
widgets.map((e) => e.id)
|
|
1126
|
-
typeMode
|
|
1305
|
+
widgets.map((e) => e.id)
|
|
1127
1306
|
)
|
|
1128
1307
|
);
|
|
1129
1308
|
lines.push(
|
|
1130
1309
|
emitIdUnion(
|
|
1131
1310
|
"RegionId",
|
|
1132
|
-
regions.map((e) => e.id)
|
|
1133
|
-
typeMode
|
|
1311
|
+
regions.map((e) => e.id)
|
|
1134
1312
|
)
|
|
1135
1313
|
);
|
|
1136
1314
|
lines.push(
|
|
1137
1315
|
emitIdUnion(
|
|
1138
1316
|
"ElementId",
|
|
1139
|
-
elements.map((e) => e.id)
|
|
1140
|
-
typeMode
|
|
1317
|
+
elements.map((e) => e.id)
|
|
1141
1318
|
)
|
|
1142
1319
|
);
|
|
1143
1320
|
lines.push(
|
|
1144
1321
|
emitIdUnion(
|
|
1145
1322
|
"PrimitiveId",
|
|
1146
|
-
primitives.map((e) => e.id)
|
|
1147
|
-
typeMode
|
|
1323
|
+
primitives.map((e) => e.id)
|
|
1148
1324
|
)
|
|
1149
1325
|
);
|
|
1150
1326
|
lines.push(
|
|
1151
1327
|
emitIdUnion(
|
|
1152
1328
|
"FlowId",
|
|
1153
|
-
flows.map((e) => e.id)
|
|
1154
|
-
typeMode
|
|
1329
|
+
flows.map((e) => e.id)
|
|
1155
1330
|
)
|
|
1156
1331
|
);
|
|
1157
1332
|
lines.push(
|
|
1158
1333
|
emitIdUnion(
|
|
1159
1334
|
"RouteId",
|
|
1160
|
-
routes.map((e) => e.path)
|
|
1161
|
-
typeMode
|
|
1335
|
+
routes.map((e) => e.path)
|
|
1162
1336
|
)
|
|
1163
1337
|
);
|
|
1164
1338
|
lines.push("");
|
|
@@ -1237,6 +1411,90 @@ function emit(opts) {
|
|
|
1237
1411
|
return lines.join("\n");
|
|
1238
1412
|
}
|
|
1239
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
|
+
|
|
1240
1498
|
// src/scanner/scan/extract-uidex-export.ts
|
|
1241
1499
|
var KIND_DISCRIMINATORS = [
|
|
1242
1500
|
"page",
|
|
@@ -1274,6 +1532,16 @@ var FALSEABLE = /* @__PURE__ */ new Set([
|
|
|
1274
1532
|
"primitive",
|
|
1275
1533
|
"region"
|
|
1276
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));
|
|
1277
1545
|
var ExtractError = class extends Error {
|
|
1278
1546
|
code;
|
|
1279
1547
|
hint;
|
|
@@ -1285,649 +1553,285 @@ var ExtractError = class extends Error {
|
|
|
1285
1553
|
this.hint = hint;
|
|
1286
1554
|
}
|
|
1287
1555
|
};
|
|
1288
|
-
function extractUidexExports(file) {
|
|
1556
|
+
function extractUidexExports(file, parsed) {
|
|
1289
1557
|
const exports2 = [];
|
|
1290
1558
|
const diagnostics = [];
|
|
1291
1559
|
const { content, displayPath } = file;
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
} catch (e) {
|
|
1303
|
-
if (e instanceof ExtractError) {
|
|
1304
|
-
diagnostics.push({
|
|
1305
|
-
code: e.code,
|
|
1306
|
-
severity: "error",
|
|
1307
|
-
message: e.message,
|
|
1308
|
-
file: displayPath,
|
|
1309
|
-
line: e.pos.line,
|
|
1310
|
-
hint: e.hint
|
|
1311
|
-
});
|
|
1312
|
-
} else {
|
|
1313
|
-
throw e;
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
return { exports: exports2, diagnostics };
|
|
1318
|
-
}
|
|
1319
|
-
var HEADER_RE = /(?:^|\n)[\t ]*export\s+const\s+uidex\b(?:\s*:\s*[^=\n]+?)?\s*=\s*/g;
|
|
1320
|
-
function findExportHeaders(content) {
|
|
1321
|
-
const out2 = [];
|
|
1322
|
-
HEADER_RE.lastIndex = 0;
|
|
1323
|
-
let m;
|
|
1324
|
-
while ((m = HEADER_RE.exec(content)) !== null) {
|
|
1325
|
-
const leadingNewline = m[0].startsWith("\n") ? 1 : 0;
|
|
1326
|
-
const headerOffset = m.index + leadingNewline;
|
|
1327
|
-
const exprStart = m.index + m[0].length;
|
|
1328
|
-
if (isInsideCommentOrString(content, headerOffset)) continue;
|
|
1329
|
-
out2.push({
|
|
1330
|
-
headerPos: posAt(content, headerOffset),
|
|
1331
|
-
exprStart
|
|
1332
|
-
});
|
|
1333
|
-
}
|
|
1334
|
-
return out2;
|
|
1335
|
-
}
|
|
1336
|
-
function isInsideCommentOrString(content, target) {
|
|
1337
|
-
let i = 0;
|
|
1338
|
-
let inLineComment = false;
|
|
1339
|
-
let inBlockComment = false;
|
|
1340
|
-
let stringDelim = null;
|
|
1341
|
-
let inTemplate = false;
|
|
1342
|
-
let templateDepth = 0;
|
|
1343
|
-
while (i < target) {
|
|
1344
|
-
const c = content[i];
|
|
1345
|
-
const n = content[i + 1];
|
|
1346
|
-
if (inLineComment) {
|
|
1347
|
-
if (c === "\n") inLineComment = false;
|
|
1348
|
-
i++;
|
|
1349
|
-
continue;
|
|
1350
|
-
}
|
|
1351
|
-
if (inBlockComment) {
|
|
1352
|
-
if (c === "*" && n === "/") {
|
|
1353
|
-
inBlockComment = false;
|
|
1354
|
-
i += 2;
|
|
1355
|
-
continue;
|
|
1356
|
-
}
|
|
1357
|
-
i++;
|
|
1358
|
-
continue;
|
|
1359
|
-
}
|
|
1360
|
-
if (stringDelim !== null) {
|
|
1361
|
-
if (c === "\\") {
|
|
1362
|
-
i += 2;
|
|
1363
|
-
continue;
|
|
1364
|
-
}
|
|
1365
|
-
if (c === stringDelim) stringDelim = null;
|
|
1366
|
-
i++;
|
|
1367
|
-
continue;
|
|
1368
|
-
}
|
|
1369
|
-
if (inTemplate) {
|
|
1370
|
-
if (c === "\\") {
|
|
1371
|
-
i += 2;
|
|
1372
|
-
continue;
|
|
1373
|
-
}
|
|
1374
|
-
if (c === "$" && n === "{") {
|
|
1375
|
-
templateDepth++;
|
|
1376
|
-
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") {
|
|
1377
1570
|
continue;
|
|
1378
1571
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
}
|
|
1389
|
-
i++;
|
|
1390
|
-
continue;
|
|
1391
|
-
}
|
|
1392
|
-
if (c === "/" && n === "/") {
|
|
1393
|
-
inLineComment = true;
|
|
1394
|
-
i += 2;
|
|
1395
|
-
continue;
|
|
1396
|
-
}
|
|
1397
|
-
if (c === "/" && n === "*") {
|
|
1398
|
-
inBlockComment = true;
|
|
1399
|
-
i += 2;
|
|
1400
|
-
continue;
|
|
1401
|
-
}
|
|
1402
|
-
if (c === '"' || c === "'") {
|
|
1403
|
-
stringDelim = c;
|
|
1404
|
-
i++;
|
|
1405
|
-
continue;
|
|
1406
|
-
}
|
|
1407
|
-
if (c === "`") {
|
|
1408
|
-
inTemplate = true;
|
|
1409
|
-
i++;
|
|
1410
|
-
continue;
|
|
1411
|
-
}
|
|
1412
|
-
i++;
|
|
1413
|
-
}
|
|
1414
|
-
return inLineComment || inBlockComment || stringDelim !== null || inTemplate;
|
|
1415
|
-
}
|
|
1416
|
-
var Tokenizer = class {
|
|
1417
|
-
constructor(src, start) {
|
|
1418
|
-
this.src = src;
|
|
1419
|
-
this.pos = start;
|
|
1420
|
-
let line = 1;
|
|
1421
|
-
let lineStart = 0;
|
|
1422
|
-
for (let i = 0; i < start; i++) {
|
|
1423
|
-
if (src[i] === "\n") {
|
|
1424
|
-
line++;
|
|
1425
|
-
lineStart = i + 1;
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
this.line = line;
|
|
1429
|
-
this.lineStart = lineStart;
|
|
1430
|
-
}
|
|
1431
|
-
src;
|
|
1432
|
-
pos;
|
|
1433
|
-
line;
|
|
1434
|
-
lineStart;
|
|
1435
|
-
currentPos() {
|
|
1436
|
-
return {
|
|
1437
|
-
offset: this.pos,
|
|
1438
|
-
line: this.line,
|
|
1439
|
-
column: this.pos - this.lineStart + 1
|
|
1440
|
-
};
|
|
1441
|
-
}
|
|
1442
|
-
advance(n = 1) {
|
|
1443
|
-
for (let i = 0; i < n; i++) {
|
|
1444
|
-
if (this.pos < this.src.length && this.src[this.pos] === "\n") {
|
|
1445
|
-
this.line++;
|
|
1446
|
-
this.lineStart = this.pos + 1;
|
|
1447
|
-
}
|
|
1448
|
-
this.pos++;
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
skipTrivia() {
|
|
1452
|
-
while (this.pos < this.src.length) {
|
|
1453
|
-
const c = this.src[this.pos];
|
|
1454
|
-
const n = this.src[this.pos + 1];
|
|
1455
|
-
if (c === " " || c === " " || c === "\r" || c === "\n") {
|
|
1456
|
-
this.advance();
|
|
1457
|
-
continue;
|
|
1458
|
-
}
|
|
1459
|
-
if (c === "/" && n === "/") {
|
|
1460
|
-
while (this.pos < this.src.length && this.src[this.pos] !== "\n") {
|
|
1461
|
-
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
|
+
);
|
|
1462
1581
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
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;
|
|
1473
1605
|
}
|
|
1474
|
-
continue;
|
|
1475
1606
|
}
|
|
1476
|
-
break;
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
next() {
|
|
1480
|
-
this.skipTrivia();
|
|
1481
|
-
if (this.pos >= this.src.length) {
|
|
1482
|
-
return { kind: "eof", value: "", pos: this.currentPos(), end: this.pos };
|
|
1483
|
-
}
|
|
1484
|
-
const pos = this.currentPos();
|
|
1485
|
-
const c = this.src[this.pos];
|
|
1486
|
-
switch (c) {
|
|
1487
|
-
case "{":
|
|
1488
|
-
this.advance();
|
|
1489
|
-
return { kind: "lbrace", value: c, pos, end: this.pos };
|
|
1490
|
-
case "}":
|
|
1491
|
-
this.advance();
|
|
1492
|
-
return { kind: "rbrace", value: c, pos, end: this.pos };
|
|
1493
|
-
case "[":
|
|
1494
|
-
this.advance();
|
|
1495
|
-
return { kind: "lbracket", value: c, pos, end: this.pos };
|
|
1496
|
-
case "]":
|
|
1497
|
-
this.advance();
|
|
1498
|
-
return { kind: "rbracket", value: c, pos, end: this.pos };
|
|
1499
|
-
case "(":
|
|
1500
|
-
this.advance();
|
|
1501
|
-
return { kind: "lparen", value: c, pos, end: this.pos };
|
|
1502
|
-
case ")":
|
|
1503
|
-
this.advance();
|
|
1504
|
-
return { kind: "rparen", value: c, pos, end: this.pos };
|
|
1505
|
-
case ",":
|
|
1506
|
-
this.advance();
|
|
1507
|
-
return { kind: "comma", value: c, pos, end: this.pos };
|
|
1508
|
-
case ":":
|
|
1509
|
-
this.advance();
|
|
1510
|
-
return { kind: "colon", value: c, pos, end: this.pos };
|
|
1511
|
-
}
|
|
1512
|
-
if (c === "." && this.src[this.pos + 1] === "." && this.src[this.pos + 2] === ".") {
|
|
1513
|
-
this.advance(3);
|
|
1514
|
-
return { kind: "spread", value: "...", pos, end: this.pos };
|
|
1515
|
-
}
|
|
1516
|
-
if (c === '"' || c === "'") {
|
|
1517
|
-
return this.readString(pos, c);
|
|
1518
|
-
}
|
|
1519
|
-
if (c === "`") {
|
|
1520
|
-
return this.readTemplate(pos);
|
|
1521
|
-
}
|
|
1522
|
-
if (isDigit(c) || c === "-" && isDigit(this.src[this.pos + 1])) {
|
|
1523
|
-
return this.readNumber(pos);
|
|
1524
|
-
}
|
|
1525
|
-
if (isIdentStart(c)) {
|
|
1526
|
-
return this.readIdent(pos);
|
|
1527
1607
|
}
|
|
1528
|
-
this.advance();
|
|
1529
|
-
return { kind: "punct", value: c, pos, end: this.pos };
|
|
1530
1608
|
}
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
}
|
|
1549
|
-
value += c;
|
|
1550
|
-
this.advance();
|
|
1551
|
-
}
|
|
1552
|
-
return { kind: "punct", value: delim, pos, end: this.pos };
|
|
1553
|
-
}
|
|
1554
|
-
readTemplate(pos) {
|
|
1555
|
-
this.advance();
|
|
1556
|
-
let value = "";
|
|
1557
|
-
let hasExpression = false;
|
|
1558
|
-
while (this.pos < this.src.length) {
|
|
1559
|
-
const c = this.src[this.pos];
|
|
1560
|
-
const n = this.src[this.pos + 1];
|
|
1561
|
-
if (c === "\\") {
|
|
1562
|
-
const esc = this.src[this.pos + 1];
|
|
1563
|
-
this.advance(2);
|
|
1564
|
-
value += decodeEscape(esc);
|
|
1565
|
-
continue;
|
|
1566
|
-
}
|
|
1567
|
-
if (c === "$" && n === "{") {
|
|
1568
|
-
hasExpression = true;
|
|
1569
|
-
this.advance(2);
|
|
1570
|
-
let depth = 1;
|
|
1571
|
-
while (this.pos < this.src.length && depth > 0) {
|
|
1572
|
-
const ch = this.src[this.pos];
|
|
1573
|
-
if (ch === "{") depth++;
|
|
1574
|
-
else if (ch === "}") depth--;
|
|
1575
|
-
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
|
+
);
|
|
1576
1626
|
}
|
|
1577
|
-
|
|
1627
|
+
return { kind: "number", value: v, pos, span };
|
|
1578
1628
|
}
|
|
1579
|
-
if (
|
|
1580
|
-
|
|
1581
|
-
if (hasExpression) {
|
|
1582
|
-
return { kind: "template", value, pos, end: this.pos };
|
|
1583
|
-
}
|
|
1584
|
-
return { kind: "string", value, pos, end: this.pos };
|
|
1629
|
+
if (typeof v === "boolean") {
|
|
1630
|
+
return { kind: "boolean", value: v, pos, span };
|
|
1585
1631
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
}
|
|
1589
|
-
return { kind: "template", value, pos, end: this.pos };
|
|
1590
|
-
}
|
|
1591
|
-
readNumber(pos) {
|
|
1592
|
-
const start = this.pos;
|
|
1593
|
-
if (this.src[this.pos] === "-") this.advance();
|
|
1594
|
-
while (this.pos < this.src.length && isDigit(this.src[this.pos])) {
|
|
1595
|
-
this.advance();
|
|
1596
|
-
}
|
|
1597
|
-
if (this.src[this.pos] === ".") {
|
|
1598
|
-
this.advance();
|
|
1599
|
-
while (this.pos < this.src.length && isDigit(this.src[this.pos])) {
|
|
1600
|
-
this.advance();
|
|
1632
|
+
if (v === null && unwrapped.raw === "null") {
|
|
1633
|
+
return { kind: "null", pos, span };
|
|
1601
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
|
+
);
|
|
1602
1640
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
if (
|
|
1606
|
-
|
|
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 };
|
|
1607
1645
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
return { kind: "number", value, pos, end: this.pos };
|
|
1614
|
-
}
|
|
1615
|
-
readIdent(pos) {
|
|
1616
|
-
const start = this.pos;
|
|
1617
|
-
while (this.pos < this.src.length && isIdentPart(this.src[this.pos])) {
|
|
1618
|
-
this.advance();
|
|
1646
|
+
throw new ExtractError(
|
|
1647
|
+
"uidex-export-invalid-literal",
|
|
1648
|
+
"Unary expressions are not allowed in `export const uidex`.",
|
|
1649
|
+
pos
|
|
1650
|
+
);
|
|
1619
1651
|
}
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
};
|
|
1624
|
-
function isDigit(c) {
|
|
1625
|
-
return c !== void 0 && c >= "0" && c <= "9";
|
|
1626
|
-
}
|
|
1627
|
-
function isIdentStart(c) {
|
|
1628
|
-
if (c === void 0) return false;
|
|
1629
|
-
return c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_" || c === "$";
|
|
1630
|
-
}
|
|
1631
|
-
function isIdentPart(c) {
|
|
1632
|
-
return isIdentStart(c) || isDigit(c);
|
|
1633
|
-
}
|
|
1634
|
-
function decodeEscape(esc) {
|
|
1635
|
-
switch (esc) {
|
|
1636
|
-
case "n":
|
|
1637
|
-
return "\n";
|
|
1638
|
-
case "t":
|
|
1639
|
-
return " ";
|
|
1640
|
-
case "r":
|
|
1641
|
-
return "\r";
|
|
1642
|
-
case "\\":
|
|
1643
|
-
return "\\";
|
|
1644
|
-
case "'":
|
|
1645
|
-
return "'";
|
|
1646
|
-
case '"':
|
|
1647
|
-
return '"';
|
|
1648
|
-
case "`":
|
|
1649
|
-
return "`";
|
|
1650
|
-
case "0":
|
|
1651
|
-
return "\0";
|
|
1652
|
-
case "b":
|
|
1653
|
-
return "\b";
|
|
1654
|
-
case "f":
|
|
1655
|
-
return "\f";
|
|
1656
|
-
case "v":
|
|
1657
|
-
return "\v";
|
|
1658
|
-
default:
|
|
1659
|
-
return esc ?? "";
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
function parseExpression(content, start) {
|
|
1663
|
-
const tokenizer = new Tokenizer(content, start);
|
|
1664
|
-
const parser = new Parser(tokenizer);
|
|
1665
|
-
const value = parser.parseValue();
|
|
1666
|
-
parser.consumeTrailingAssertions();
|
|
1667
|
-
return value;
|
|
1668
|
-
}
|
|
1669
|
-
var Parser = class {
|
|
1670
|
-
constructor(tok) {
|
|
1671
|
-
this.tok = tok;
|
|
1672
|
-
}
|
|
1673
|
-
tok;
|
|
1674
|
-
lookahead = null;
|
|
1675
|
-
peek() {
|
|
1676
|
-
if (this.lookahead === null) this.lookahead = this.tok.next();
|
|
1677
|
-
return this.lookahead;
|
|
1678
|
-
}
|
|
1679
|
-
consume() {
|
|
1680
|
-
const t = this.peek();
|
|
1681
|
-
this.lookahead = null;
|
|
1682
|
-
return t;
|
|
1683
|
-
}
|
|
1684
|
-
parseValue() {
|
|
1685
|
-
const t = this.peek();
|
|
1686
|
-
switch (t.kind) {
|
|
1687
|
-
case "lbrace":
|
|
1688
|
-
return this.parseObject();
|
|
1689
|
-
case "lbracket":
|
|
1690
|
-
return this.parseArray();
|
|
1691
|
-
case "string":
|
|
1692
|
-
this.consume();
|
|
1693
|
-
return { kind: "string", value: t.value, pos: t.pos };
|
|
1694
|
-
case "template":
|
|
1652
|
+
case "TemplateLiteral": {
|
|
1653
|
+
const expressions = unwrapped.expressions ?? [];
|
|
1654
|
+
if (expressions.length > 0) {
|
|
1695
1655
|
throw new ExtractError(
|
|
1696
1656
|
"uidex-export-invalid-literal",
|
|
1697
1657
|
"Template literal with expression parts is not allowed in `export const uidex`; use a plain string literal.",
|
|
1698
|
-
|
|
1658
|
+
pos
|
|
1699
1659
|
);
|
|
1700
|
-
case "number": {
|
|
1701
|
-
this.consume();
|
|
1702
|
-
const n = Number(t.value);
|
|
1703
|
-
if (!Number.isFinite(n)) {
|
|
1704
|
-
throw new ExtractError(
|
|
1705
|
-
"uidex-export-invalid-literal",
|
|
1706
|
-
`Invalid numeric literal "${t.value}" in \`export const uidex\`.`,
|
|
1707
|
-
t.pos
|
|
1708
|
-
);
|
|
1709
|
-
}
|
|
1710
|
-
return { kind: "number", value: n, pos: t.pos };
|
|
1711
1660
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
return {
|
|
1716
|
-
kind: "boolean",
|
|
1717
|
-
value: t.value === "true",
|
|
1718
|
-
pos: t.pos
|
|
1719
|
-
};
|
|
1720
|
-
}
|
|
1721
|
-
if (t.value === "null") {
|
|
1722
|
-
this.consume();
|
|
1723
|
-
return { kind: "null", pos: t.pos };
|
|
1724
|
-
}
|
|
1725
|
-
if (t.value === "undefined") {
|
|
1726
|
-
throw new ExtractError(
|
|
1727
|
-
"uidex-export-invalid-literal",
|
|
1728
|
-
"`undefined` is not allowed as a value in `export const uidex`; omit the field instead.",
|
|
1729
|
-
t.pos
|
|
1730
|
-
);
|
|
1731
|
-
}
|
|
1732
|
-
throw new ExtractError(
|
|
1733
|
-
"uidex-export-invalid-literal",
|
|
1734
|
-
`Identifier reference "${t.value}" is not allowed in \`export const uidex\`; the right-hand side must be a plain literal.`,
|
|
1735
|
-
t.pos
|
|
1736
|
-
);
|
|
1737
|
-
case "spread":
|
|
1738
|
-
throw new ExtractError(
|
|
1739
|
-
"uidex-export-invalid-literal",
|
|
1740
|
-
"Spread (`...`) is not allowed in `export const uidex`; the right-hand side must be a plain literal.",
|
|
1741
|
-
t.pos
|
|
1742
|
-
);
|
|
1743
|
-
case "lparen":
|
|
1744
|
-
throw new ExtractError(
|
|
1745
|
-
"uidex-export-invalid-literal",
|
|
1746
|
-
"Parenthesised or grouped expressions are not allowed in `export const uidex`.",
|
|
1747
|
-
t.pos
|
|
1748
|
-
);
|
|
1749
|
-
case "punct":
|
|
1750
|
-
throw new ExtractError(
|
|
1751
|
-
"uidex-export-invalid-literal",
|
|
1752
|
-
`Unexpected token "${t.value}" in \`export const uidex\`.`,
|
|
1753
|
-
t.pos
|
|
1754
|
-
);
|
|
1755
|
-
case "eof":
|
|
1756
|
-
throw new ExtractError(
|
|
1757
|
-
"uidex-export-invalid-literal",
|
|
1758
|
-
"Expected a value for `export const uidex` but reached end of file.",
|
|
1759
|
-
t.pos
|
|
1760
|
-
);
|
|
1761
|
-
default:
|
|
1762
|
-
throw new ExtractError(
|
|
1763
|
-
"uidex-export-invalid-literal",
|
|
1764
|
-
`Unexpected token in \`export const uidex\`.`,
|
|
1765
|
-
t.pos
|
|
1766
|
-
);
|
|
1661
|
+
const quasis = unwrapped.quasis ?? [];
|
|
1662
|
+
const cooked = quasis[0]?.value?.cooked ?? "";
|
|
1663
|
+
return { kind: "string", value: cooked, pos, span };
|
|
1767
1664
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
const entries = [];
|
|
1772
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1773
|
-
while (true) {
|
|
1774
|
-
const t = this.peek();
|
|
1775
|
-
if (t.kind === "rbrace") {
|
|
1776
|
-
this.consume();
|
|
1777
|
-
break;
|
|
1778
|
-
}
|
|
1779
|
-
if (t.kind === "spread") {
|
|
1665
|
+
case "Identifier": {
|
|
1666
|
+
const name = String(unwrapped.name);
|
|
1667
|
+
if (name === "undefined") {
|
|
1780
1668
|
throw new ExtractError(
|
|
1781
1669
|
"uidex-export-invalid-literal",
|
|
1782
|
-
"
|
|
1783
|
-
|
|
1670
|
+
"`undefined` is not allowed as a value in `export const uidex`; omit the field instead.",
|
|
1671
|
+
pos
|
|
1784
1672
|
);
|
|
1785
1673
|
}
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
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") {
|
|
1838
1740
|
throw new ExtractError(
|
|
1839
1741
|
"uidex-export-invalid-literal",
|
|
1840
|
-
`
|
|
1841
|
-
|
|
1742
|
+
"Computed property keys must be string literals in `export const uidex`.",
|
|
1743
|
+
keyPos
|
|
1842
1744
|
);
|
|
1843
1745
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
this.consume();
|
|
1851
|
-
break;
|
|
1852
|
-
}
|
|
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 {
|
|
1853
1752
|
throw new ExtractError(
|
|
1854
1753
|
"uidex-export-invalid-literal",
|
|
1855
|
-
|
|
1856
|
-
|
|
1754
|
+
"Numeric property keys are not allowed in `export const uidex`.",
|
|
1755
|
+
keyPos
|
|
1857
1756
|
);
|
|
1858
1757
|
}
|
|
1859
|
-
return { kind: "object", entries, pos: open.pos };
|
|
1860
|
-
}
|
|
1861
|
-
recordEntry(entries, seen, key, value, pos) {
|
|
1862
1758
|
if (seen.has(key)) {
|
|
1863
1759
|
throw new ExtractError(
|
|
1864
1760
|
"uidex-export-duplicate-field",
|
|
1865
1761
|
`Duplicate field "${key}" in \`export const uidex\`.`,
|
|
1866
|
-
|
|
1762
|
+
keyPos
|
|
1867
1763
|
);
|
|
1868
1764
|
}
|
|
1869
1765
|
seen.add(key);
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
items.push(value);
|
|
1892
|
-
const after = this.peek();
|
|
1893
|
-
if (after.kind === "comma") {
|
|
1894
|
-
this.consume();
|
|
1895
|
-
continue;
|
|
1896
|
-
}
|
|
1897
|
-
if (after.kind === "rbracket") {
|
|
1898
|
-
this.consume();
|
|
1899
|
-
break;
|
|
1900
|
-
}
|
|
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) {
|
|
1780
|
+
throw new ExtractError(
|
|
1781
|
+
"uidex-export-invalid-literal",
|
|
1782
|
+
"Array holes are not allowed in `export const uidex`.",
|
|
1783
|
+
pos
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
if (el.type === "SpreadElement") {
|
|
1901
1787
|
throw new ExtractError(
|
|
1902
1788
|
"uidex-export-invalid-literal",
|
|
1903
|
-
|
|
1904
|
-
|
|
1789
|
+
"Spread (`...`) is not allowed inside `export const uidex`.",
|
|
1790
|
+
posAt(content, el.start, p2)
|
|
1905
1791
|
);
|
|
1906
1792
|
}
|
|
1907
|
-
|
|
1793
|
+
items.push(toLitValue(el, content, p2));
|
|
1908
1794
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
"uidex-export-invalid-literal",
|
|
1919
|
-
"Only `as const` is allowed after the `export const uidex` value.",
|
|
1920
|
-
next.pos
|
|
1921
|
-
);
|
|
1922
|
-
}
|
|
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;
|
|
1923
1804
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1805
|
+
if (node.type === "TSAsExpression" || node.type === "TSNonNullExpression" || node.type === "TSTypeAssertion" || node.type === "ParenthesizedExpression") {
|
|
1806
|
+
node = node.expression;
|
|
1807
|
+
continue;
|
|
1927
1808
|
}
|
|
1809
|
+
break;
|
|
1928
1810
|
}
|
|
1929
|
-
|
|
1930
|
-
|
|
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) {
|
|
1931
1835
|
if (value.kind !== "object") {
|
|
1932
1836
|
throw new ExtractError(
|
|
1933
1837
|
"uidex-export-invalid-literal",
|
|
@@ -1936,7 +1840,7 @@ function buildMetadata(value, file, headerPos, diagnostics) {
|
|
|
1936
1840
|
);
|
|
1937
1841
|
}
|
|
1938
1842
|
const byKey = /* @__PURE__ */ new Map();
|
|
1939
|
-
for (const
|
|
1843
|
+
for (const entry of value.entries) byKey.set(entry.key, entry);
|
|
1940
1844
|
const presentKinds = KIND_DISCRIMINATORS.filter(
|
|
1941
1845
|
(k) => byKey.has(k)
|
|
1942
1846
|
);
|
|
@@ -1959,49 +1863,58 @@ function buildMetadata(value, file, headerPos, diagnostics) {
|
|
|
1959
1863
|
const discriminator = presentKinds[0];
|
|
1960
1864
|
const kind = discriminator === "notFlow" ? "flow" : discriminator;
|
|
1961
1865
|
const allowed = ALLOWED_FIELDS[kind];
|
|
1962
|
-
for (const
|
|
1963
|
-
if (!allowed.has(
|
|
1964
|
-
const fieldVal = byKey.get(k);
|
|
1866
|
+
for (const entry of value.entries) {
|
|
1867
|
+
if (!allowed.has(entry.key)) {
|
|
1965
1868
|
throw new ExtractError(
|
|
1966
1869
|
"uidex-export-unknown-field",
|
|
1967
|
-
`Unknown field "${
|
|
1870
|
+
`Unknown field "${entry.key}" in \`export const uidex\` for kind "${kind}". Allowed: ${Array.from(
|
|
1968
1871
|
allowed
|
|
1969
1872
|
).sort().join(", ")}.`,
|
|
1970
|
-
|
|
1873
|
+
entry.value.pos
|
|
1971
1874
|
);
|
|
1972
1875
|
}
|
|
1973
1876
|
}
|
|
1974
1877
|
const idField = discriminator === "notFlow" ? "flow" : discriminator;
|
|
1975
|
-
const idValue = byKey.get(discriminator);
|
|
1878
|
+
const idValue = byKey.get(discriminator).value;
|
|
1976
1879
|
let id;
|
|
1977
1880
|
if (discriminator === "notFlow") {
|
|
1978
|
-
|
|
1979
|
-
if (v.kind !== "boolean" || v.value !== true) {
|
|
1881
|
+
if (idValue.kind !== "boolean" || idValue.value !== true) {
|
|
1980
1882
|
throw new ExtractError(
|
|
1981
1883
|
"uidex-export-invalid-field",
|
|
1982
1884
|
"`notFlow` must be `true`.",
|
|
1983
|
-
|
|
1885
|
+
idValue.pos
|
|
1984
1886
|
);
|
|
1985
1887
|
}
|
|
1986
1888
|
id = false;
|
|
1987
1889
|
} else {
|
|
1988
1890
|
id = readIdField(idValue, kind, idField);
|
|
1989
1891
|
}
|
|
1990
|
-
const acceptance = readStringArrayField(byKey, "acceptance");
|
|
1892
|
+
const acceptance = readStringArrayField(byKey, "acceptance")?.values;
|
|
1991
1893
|
const description = readStringField(byKey, "description");
|
|
1992
1894
|
const name = readStringField(byKey, "name");
|
|
1993
1895
|
if (name === "") {
|
|
1994
|
-
const
|
|
1896
|
+
const entry = byKey.get("name");
|
|
1995
1897
|
diagnostics.push({
|
|
1996
1898
|
code: "uidex-export-empty-name",
|
|
1997
1899
|
severity: "info",
|
|
1998
1900
|
message: "`name` is an empty string; treating as unset.",
|
|
1999
1901
|
file,
|
|
2000
|
-
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
|
+
}
|
|
2001
1914
|
});
|
|
2002
1915
|
}
|
|
2003
|
-
const
|
|
2004
|
-
const
|
|
1916
|
+
const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
1917
|
+
const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
2005
1918
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
2006
1919
|
const metadata = {
|
|
2007
1920
|
source: "ts-export",
|
|
@@ -2016,9 +1929,21 @@ function buildMetadata(value, file, headerPos, diagnostics) {
|
|
|
2016
1929
|
if (name) metadata.name = name;
|
|
2017
1930
|
if (acceptance) metadata.acceptance = acceptance;
|
|
2018
1931
|
if (description) metadata.description = description;
|
|
2019
|
-
if (
|
|
2020
|
-
|
|
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
|
+
}
|
|
2021
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;
|
|
2022
1947
|
return metadata;
|
|
2023
1948
|
}
|
|
2024
1949
|
function readIdField(value, kind, fieldName) {
|
|
@@ -2049,29 +1974,30 @@ function readIdField(value, kind, fieldName) {
|
|
|
2049
1974
|
);
|
|
2050
1975
|
}
|
|
2051
1976
|
function readStringField(byKey, name) {
|
|
2052
|
-
const
|
|
2053
|
-
if (!
|
|
2054
|
-
if (
|
|
1977
|
+
const entry = byKey.get(name);
|
|
1978
|
+
if (!entry) return void 0;
|
|
1979
|
+
if (entry.value.kind !== "string") {
|
|
2055
1980
|
throw new ExtractError(
|
|
2056
1981
|
"uidex-export-invalid-field",
|
|
2057
1982
|
`\`${name}\` must be a string.`,
|
|
2058
|
-
|
|
1983
|
+
entry.value.pos
|
|
2059
1984
|
);
|
|
2060
1985
|
}
|
|
2061
|
-
return
|
|
1986
|
+
return entry.value.value;
|
|
2062
1987
|
}
|
|
2063
1988
|
function readStringArrayField(byKey, name) {
|
|
2064
|
-
const
|
|
2065
|
-
if (!
|
|
2066
|
-
if (
|
|
1989
|
+
const entry = byKey.get(name);
|
|
1990
|
+
if (!entry) return void 0;
|
|
1991
|
+
if (entry.value.kind !== "array") {
|
|
2067
1992
|
throw new ExtractError(
|
|
2068
1993
|
"uidex-export-invalid-field",
|
|
2069
1994
|
`\`${name}\` must be an array of strings.`,
|
|
2070
|
-
|
|
1995
|
+
entry.value.pos
|
|
2071
1996
|
);
|
|
2072
1997
|
}
|
|
2073
|
-
const
|
|
2074
|
-
|
|
1998
|
+
const values = [];
|
|
1999
|
+
const spans = [];
|
|
2000
|
+
for (const item of entry.value.items) {
|
|
2075
2001
|
if (item.kind !== "string") {
|
|
2076
2002
|
throw new ExtractError(
|
|
2077
2003
|
"uidex-export-invalid-field",
|
|
@@ -2079,312 +2005,522 @@ function readStringArrayField(byKey, name) {
|
|
|
2079
2005
|
item.pos
|
|
2080
2006
|
);
|
|
2081
2007
|
}
|
|
2082
|
-
|
|
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;
|
|
2083
2087
|
}
|
|
2084
|
-
return
|
|
2088
|
+
return false;
|
|
2085
2089
|
}
|
|
2086
|
-
function
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
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;
|
|
2093
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;
|
|
2094
2149
|
}
|
|
2095
|
-
return
|
|
2150
|
+
return null;
|
|
2151
|
+
}
|
|
2152
|
+
function isIdentifier(node, name) {
|
|
2153
|
+
return typeof node === "object" && node !== null && node.type === "Identifier" && String(node.name) === name;
|
|
2096
2154
|
}
|
|
2097
2155
|
|
|
2098
2156
|
// src/scanner/scan/jsx-ancestry.ts
|
|
2099
|
-
var
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
return out2;
|
|
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";
|
|
2109
2166
|
}
|
|
2110
|
-
function
|
|
2111
|
-
|
|
2112
|
-
const
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
while (i < N) {
|
|
2124
|
-
const c = content[i];
|
|
2125
|
-
if (c === "\n") {
|
|
2126
|
-
line++;
|
|
2127
|
-
i++;
|
|
2128
|
-
continue;
|
|
2129
|
-
}
|
|
2130
|
-
if (c === "/" && content[i + 1] === "/") {
|
|
2131
|
-
while (i < N && content[i] !== "\n") i++;
|
|
2132
|
-
continue;
|
|
2133
|
-
}
|
|
2134
|
-
if (c === "/" && content[i + 1] === "*") {
|
|
2135
|
-
const end = content.indexOf("*/", i + 2);
|
|
2136
|
-
const next = end === -1 ? N : end + 2;
|
|
2137
|
-
advanceLines(i, next);
|
|
2138
|
-
i = next;
|
|
2139
|
-
continue;
|
|
2140
|
-
}
|
|
2141
|
-
if (c === '"' || c === "'") {
|
|
2142
|
-
const next = skipString2(content, i, c);
|
|
2143
|
-
advanceLines(i, next);
|
|
2144
|
-
i = next;
|
|
2145
|
-
continue;
|
|
2146
|
-
}
|
|
2147
|
-
if (c === "`") {
|
|
2148
|
-
const next = skipTemplate(content, i);
|
|
2149
|
-
advanceLines(i, next);
|
|
2150
|
-
i = next;
|
|
2151
|
-
continue;
|
|
2152
|
-
}
|
|
2153
|
-
if (c === "<") {
|
|
2154
|
-
const nextCh = content[i + 1];
|
|
2155
|
-
if (nextCh === "/") {
|
|
2156
|
-
const end = content.indexOf(">", i);
|
|
2157
|
-
if (end === -1) break;
|
|
2158
|
-
const tagName = content.slice(i + 2, end).match(/^\s*([\w.-]*)/)?.[1] ?? "";
|
|
2159
|
-
if (tagName) {
|
|
2160
|
-
for (let k = stack.length - 1; k >= 0; k--) {
|
|
2161
|
-
if (stack[k].tagName === tagName) {
|
|
2162
|
-
for (let j = stack.length - 1; j >= k; j--) {
|
|
2163
|
-
ancestors.length -= stack[j].pushed;
|
|
2164
|
-
}
|
|
2165
|
-
stack.length = k;
|
|
2166
|
-
break;
|
|
2167
|
-
}
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
advanceLines(i, end + 1);
|
|
2171
|
-
i = end + 1;
|
|
2172
|
-
continue;
|
|
2173
|
-
}
|
|
2174
|
-
if (nextCh && /[A-Za-z_]/.test(nextCh)) {
|
|
2175
|
-
const end = findTagEnd(content, i + 1);
|
|
2176
|
-
if (end === -1) break;
|
|
2177
|
-
const tagSource = content.slice(i, end + 1);
|
|
2178
|
-
const tagName = tagSource.match(/^<\s*([\w.-]*)/)?.[1] ?? "";
|
|
2179
|
-
const isSelf = content[end - 1] === "/";
|
|
2180
|
-
if (tagName) {
|
|
2181
|
-
const attrs = parseDataAttrs(tagSource);
|
|
2182
|
-
if (attrs.length > 0) {
|
|
2183
|
-
const snapshot = ancestors.slice();
|
|
2184
|
-
for (const a of attrs) {
|
|
2185
|
-
out2.push({ kind: a.kind, id: a.id, line, ancestors: snapshot });
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
|
-
if (!isSelf) {
|
|
2189
|
-
for (const a of attrs) ancestors.push(a);
|
|
2190
|
-
stack.push({ tagName, pushed: attrs.length });
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
advanceLines(i, end + 1);
|
|
2194
|
-
i = end + 1;
|
|
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);
|
|
2195
2180
|
continue;
|
|
2196
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);
|
|
2197
2187
|
}
|
|
2198
|
-
|
|
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;
|
|
2199
2195
|
}
|
|
2200
|
-
|
|
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 ?? "";
|
|
2201
|
+
}
|
|
2202
|
+
return null;
|
|
2201
2203
|
}
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
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
|
+
}
|
|
2210
2221
|
}
|
|
2211
|
-
|
|
2212
|
-
|
|
2222
|
+
out2 = out2.replace(/\*{2,}/g, "*");
|
|
2223
|
+
if (!out2.includes("*")) {
|
|
2224
|
+
return out2.length > 0 ? { resolved: true, ids: [out2] } : UNRESOLVED;
|
|
2225
|
+
}
|
|
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;
|
|
2213
2231
|
}
|
|
2214
|
-
|
|
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;
|
|
2215
2239
|
}
|
|
2216
|
-
function
|
|
2217
|
-
const
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
if (
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
if (
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
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 };
|
|
2234
2263
|
}
|
|
2235
|
-
if (cj === "`") {
|
|
2236
|
-
i = skipTemplate(content, i);
|
|
2237
|
-
continue;
|
|
2238
|
-
}
|
|
2239
|
-
if (cj === "{") depth++;
|
|
2240
|
-
else if (cj === "}") depth--;
|
|
2241
|
-
i++;
|
|
2242
2264
|
}
|
|
2243
|
-
|
|
2265
|
+
if (!result2.resolved) {
|
|
2266
|
+
dynamicAttrs.push({
|
|
2267
|
+
kind,
|
|
2268
|
+
attrName: kind === "element" ? "data-uidex" : `data-uidex-${kind}`,
|
|
2269
|
+
line: lineAt(attr.start)
|
|
2270
|
+
});
|
|
2271
|
+
continue;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
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);
|
|
2244
2286
|
}
|
|
2245
|
-
i++;
|
|
2246
2287
|
}
|
|
2247
|
-
return
|
|
2288
|
+
return [...statics, ...patterns];
|
|
2248
2289
|
}
|
|
2249
|
-
function
|
|
2250
|
-
const
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
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
|
+
});
|
|
2270
2322
|
}
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
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++;
|
|
2274
2332
|
}
|
|
2275
|
-
if (cj === "{") depth++;
|
|
2276
|
-
else if (cj === "}") depth--;
|
|
2277
|
-
i++;
|
|
2278
2333
|
}
|
|
2279
|
-
|
|
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;
|
|
2280
2342
|
}
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
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);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
};
|
|
2356
|
+
visit(parsed.program);
|
|
2357
|
+
return { occurrences, dynamicAttrs, unannotatedInteractive, landmarks };
|
|
2285
2358
|
}
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
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
|
+
}
|
|
2293
2374
|
}
|
|
2294
|
-
return
|
|
2375
|
+
return null;
|
|
2295
2376
|
}
|
|
2296
|
-
function
|
|
2297
|
-
const
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
const
|
|
2301
|
-
|
|
2302
|
-
let
|
|
2303
|
-
for (const
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
const uidex = line.match(
|
|
2307
|
-
/^@uidex\s+(page|feature|widget)\s+(\S+)(?:\s+-\s+(.+))?/
|
|
2308
|
-
);
|
|
2309
|
-
if (uidex) {
|
|
2310
|
-
kind = uidex[1];
|
|
2311
|
-
id = uidex[2];
|
|
2312
|
-
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;
|
|
2313
2387
|
continue;
|
|
2314
2388
|
}
|
|
2315
|
-
if (
|
|
2316
|
-
|
|
2317
|
-
|
|
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;
|
|
2318
2411
|
}
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
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 ?? ""));
|
|
2323
2421
|
}
|
|
2324
|
-
if (line.startsWith("@")) continue;
|
|
2325
|
-
desc.push(line);
|
|
2326
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;
|
|
2327
2444
|
return {
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
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"
|
|
2333
2451
|
};
|
|
2334
2452
|
}
|
|
2335
2453
|
function extract(files) {
|
|
2336
2454
|
return files.map((file) => {
|
|
2337
|
-
const
|
|
2338
|
-
const
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
};
|
|
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);
|
|
2342
2461
|
if (exports2.length > 0) out2.metadata = exports2;
|
|
2343
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;
|
|
2344
2467
|
return out2;
|
|
2345
2468
|
});
|
|
2346
2469
|
}
|
|
2347
|
-
function extractOne(file) {
|
|
2470
|
+
function extractOne(file, parsed, out2) {
|
|
2348
2471
|
const annotations = [];
|
|
2349
|
-
const {
|
|
2350
|
-
|
|
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) {
|
|
2351
2480
|
annotations.push({
|
|
2352
2481
|
kind: occ.kind,
|
|
2353
2482
|
id: occ.id,
|
|
2354
2483
|
file: displayPath,
|
|
2355
2484
|
line: occ.line,
|
|
2356
|
-
...occ.ancestors.length > 0 ? { ancestors: occ.ancestors } : {}
|
|
2485
|
+
...occ.ancestors.length > 0 ? { ancestors: occ.ancestors } : {},
|
|
2486
|
+
...occ.span ? { span: occ.span } : {}
|
|
2357
2487
|
});
|
|
2358
2488
|
}
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
}
|
|
2377
|
-
} else if (
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
line,
|
|
2383
|
-
acceptance: parsed.acceptance
|
|
2384
|
-
});
|
|
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;
|
|
2385
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
|
+
});
|
|
2386
2522
|
}
|
|
2387
|
-
return
|
|
2523
|
+
return out2;
|
|
2388
2524
|
}
|
|
2389
2525
|
|
|
2390
2526
|
// src/scanner/scan/git.ts
|
|
@@ -2420,7 +2556,7 @@ function parseGitHubRef(ref) {
|
|
|
2420
2556
|
}
|
|
2421
2557
|
|
|
2422
2558
|
// src/scanner/scan/resolve.ts
|
|
2423
|
-
var
|
|
2559
|
+
var path8 = __toESM(require("path"), 1);
|
|
2424
2560
|
|
|
2425
2561
|
// src/scanner/scan/routes.ts
|
|
2426
2562
|
var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
@@ -2489,8 +2625,8 @@ function pathToId(routePath) {
|
|
|
2489
2625
|
}
|
|
2490
2626
|
|
|
2491
2627
|
// src/scanner/scan/walk.ts
|
|
2492
|
-
var
|
|
2493
|
-
var
|
|
2628
|
+
var fs5 = __toESM(require("fs"), 1);
|
|
2629
|
+
var path7 = __toESM(require("path"), 1);
|
|
2494
2630
|
var DEFAULT_INCLUDES = ["**/*.{ts,tsx,js,jsx,mjs,cjs}"];
|
|
2495
2631
|
var BASE_EXCLUDES = [
|
|
2496
2632
|
"**/node_modules/**",
|
|
@@ -2554,7 +2690,7 @@ function globToRegExp(glob) {
|
|
|
2554
2690
|
return new RegExp(`^${out2}$`);
|
|
2555
2691
|
}
|
|
2556
2692
|
function toPosix(p2) {
|
|
2557
|
-
return p2.split(
|
|
2693
|
+
return p2.split(path7.sep).join("/");
|
|
2558
2694
|
}
|
|
2559
2695
|
function matchesAny(rel, patterns) {
|
|
2560
2696
|
return patterns.some((g) => globToRegExp(g).test(rel));
|
|
@@ -2570,18 +2706,18 @@ function walk(sources, options) {
|
|
|
2570
2706
|
...globalExcludes,
|
|
2571
2707
|
...source.exclude ?? []
|
|
2572
2708
|
];
|
|
2573
|
-
const absRoot =
|
|
2709
|
+
const absRoot = path7.resolve(cwd, source.rootDir);
|
|
2574
2710
|
for (const filePath of walkDir(absRoot, absRoot)) {
|
|
2575
|
-
const rel = toPosix(
|
|
2711
|
+
const rel = toPosix(path7.relative(absRoot, filePath));
|
|
2576
2712
|
if (matchesAny(rel, excludes)) continue;
|
|
2577
2713
|
if (!matchesAny(rel, includes)) continue;
|
|
2578
2714
|
let content;
|
|
2579
2715
|
try {
|
|
2580
|
-
content =
|
|
2716
|
+
content = fs5.readFileSync(filePath, "utf8");
|
|
2581
2717
|
} catch {
|
|
2582
2718
|
continue;
|
|
2583
2719
|
}
|
|
2584
|
-
const relFromCwd = toPosix(
|
|
2720
|
+
const relFromCwd = toPosix(path7.relative(cwd, filePath));
|
|
2585
2721
|
const displayPath = source.prefix ? `${source.prefix.replace(/\/$/, "")}/${rel}` : relFromCwd;
|
|
2586
2722
|
out2.push({
|
|
2587
2723
|
sourcePath: filePath,
|
|
@@ -2596,12 +2732,12 @@ function walk(sources, options) {
|
|
|
2596
2732
|
function* walkDir(root, dir) {
|
|
2597
2733
|
let entries;
|
|
2598
2734
|
try {
|
|
2599
|
-
entries =
|
|
2735
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
2600
2736
|
} catch {
|
|
2601
2737
|
return;
|
|
2602
2738
|
}
|
|
2603
2739
|
for (const entry of entries) {
|
|
2604
|
-
const full =
|
|
2740
|
+
const full = path7.join(dir, entry.name);
|
|
2605
2741
|
if (entry.isDirectory()) {
|
|
2606
2742
|
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git" || entry.name === "build" || entry.name === ".next") {
|
|
2607
2743
|
continue;
|
|
@@ -2633,21 +2769,9 @@ function kebab(str) {
|
|
|
2633
2769
|
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").replace(/[^a-zA-Z0-9-]/g, "").toLowerCase();
|
|
2634
2770
|
}
|
|
2635
2771
|
function baseName(file) {
|
|
2636
|
-
const b =
|
|
2772
|
+
const b = path8.posix.basename(file);
|
|
2637
2773
|
return b.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "");
|
|
2638
2774
|
}
|
|
2639
|
-
var LANDMARK_RE = /<(header|nav|main|aside|footer)(\s[^>]*)?>|role=["']region["']/gi;
|
|
2640
|
-
function extractLandmarks(file) {
|
|
2641
|
-
const out2 = [];
|
|
2642
|
-
LANDMARK_RE.lastIndex = 0;
|
|
2643
|
-
let m;
|
|
2644
|
-
while ((m = LANDMARK_RE.exec(file.content)) !== null) {
|
|
2645
|
-
const tag = m[1] ?? "region";
|
|
2646
|
-
const line = 1 + file.content.slice(0, m.index).split("\n").length - 1;
|
|
2647
|
-
out2.push({ tag, line });
|
|
2648
|
-
}
|
|
2649
|
-
return out2;
|
|
2650
|
-
}
|
|
2651
2775
|
function fileMatchesAny(displayPath, patterns) {
|
|
2652
2776
|
return patterns.some((g) => globToRegExp(g).test(displayPath));
|
|
2653
2777
|
}
|
|
@@ -2708,7 +2832,7 @@ function resolve3(ctx) {
|
|
|
2708
2832
|
const routes = conventions.pages === "auto" ? detectRoutes(ctx.extracted.map((e) => e.file)) : [];
|
|
2709
2833
|
const handledPageFiles = /* @__PURE__ */ new Set();
|
|
2710
2834
|
for (const route of routes) {
|
|
2711
|
-
const routeDir =
|
|
2835
|
+
const routeDir = path8.posix.dirname(route.file);
|
|
2712
2836
|
const wellKnownPath = `${routeDir}/${WELL_KNOWN_FILES.page}`;
|
|
2713
2837
|
const wellKnownExp = exportFor(wellKnownPath, "page");
|
|
2714
2838
|
const routeExp = exportFor(route.file, "page");
|
|
@@ -2762,7 +2886,7 @@ function resolve3(ctx) {
|
|
|
2762
2886
|
const dir = extractFeatureDir(ef.file.displayPath, featureGlob);
|
|
2763
2887
|
if (!dir) continue;
|
|
2764
2888
|
conventionalFeatureDirs.add(dir);
|
|
2765
|
-
const isWellKnown =
|
|
2889
|
+
const isWellKnown = path8.posix.basename(ef.file.displayPath) === WELL_KNOWN_FILES.feature;
|
|
2766
2890
|
if (isWellKnown) wellKnownFeatureFileByDir.set(dir, ef.file.displayPath);
|
|
2767
2891
|
const exp = exportFor(ef.file.displayPath, "feature");
|
|
2768
2892
|
if (exp) {
|
|
@@ -2799,7 +2923,7 @@ function resolve3(ctx) {
|
|
|
2799
2923
|
} else if (allExports.length > 0) {
|
|
2800
2924
|
exp = allExports[0].exp;
|
|
2801
2925
|
}
|
|
2802
|
-
const id = exp && typeof exp.id === "string" ? exp.id :
|
|
2926
|
+
const id = exp && typeof exp.id === "string" ? exp.id : path8.posix.basename(dir);
|
|
2803
2927
|
const meta = exp ? buildMetaFromExport(exp) : void 0;
|
|
2804
2928
|
const feature = {
|
|
2805
2929
|
kind: "feature",
|
|
@@ -2895,8 +3019,8 @@ function resolve3(ctx) {
|
|
|
2895
3019
|
}
|
|
2896
3020
|
if (conventions.regions === "landmarks") {
|
|
2897
3021
|
for (const ef of ctx.extracted) {
|
|
2898
|
-
for (const lm of
|
|
2899
|
-
const id =
|
|
3022
|
+
for (const lm of ef.landmarks ?? []) {
|
|
3023
|
+
const id = lm.tag;
|
|
2900
3024
|
if (!registry.get("region", id)) {
|
|
2901
3025
|
const meta = metaWithComposes("region", id);
|
|
2902
3026
|
const region = {
|
|
@@ -3007,7 +3131,7 @@ function resolve3(ctx) {
|
|
|
3007
3131
|
const flowExport = (ff.metadata ?? []).find(
|
|
3008
3132
|
(m) => m.kind === "flow" && typeof m.id === "string"
|
|
3009
3133
|
);
|
|
3010
|
-
const derived =
|
|
3134
|
+
const derived = flowsFromFacts(ff);
|
|
3011
3135
|
if (flowExport && typeof flowExport.id === "string" && derived.length === 1) {
|
|
3012
3136
|
const base = derived[0];
|
|
3013
3137
|
const flow = {
|
|
@@ -3062,52 +3186,14 @@ function computeScope(displayPath) {
|
|
|
3062
3186
|
}
|
|
3063
3187
|
return null;
|
|
3064
3188
|
}
|
|
3065
|
-
function
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
const line = 1 + source.slice(0, m.index).split("\n").length - 1;
|
|
3074
|
-
const after = source.slice(m.index + m[0].length);
|
|
3075
|
-
const arrow = after.match(/=>\s*\{/);
|
|
3076
|
-
if (!arrow || arrow.index === void 0) continue;
|
|
3077
|
-
const bodyStart = m.index + m[0].length + arrow.index + arrow[0].length;
|
|
3078
|
-
let depth = 1;
|
|
3079
|
-
let bodyEnd = -1;
|
|
3080
|
-
for (let i = bodyStart; i < source.length; i++) {
|
|
3081
|
-
if (source[i] === "{") depth++;
|
|
3082
|
-
else if (source[i] === "}") {
|
|
3083
|
-
depth--;
|
|
3084
|
-
if (depth === 0) {
|
|
3085
|
-
bodyEnd = i;
|
|
3086
|
-
break;
|
|
3087
|
-
}
|
|
3088
|
-
}
|
|
3089
|
-
}
|
|
3090
|
-
if (bodyEnd === -1) continue;
|
|
3091
|
-
const body = source.slice(bodyStart, bodyEnd);
|
|
3092
|
-
const touches = captureUidexIds(body);
|
|
3093
|
-
flows.push({
|
|
3094
|
-
kind: "flow",
|
|
3095
|
-
id,
|
|
3096
|
-
loc: { file: file.displayPath, line },
|
|
3097
|
-
touches: dedupe(touches.map((t) => t.id)),
|
|
3098
|
-
steps: touches.filter((t) => t.action).map((t) => ({ entityId: t.id, action: t.action }))
|
|
3099
|
-
});
|
|
3100
|
-
}
|
|
3101
|
-
return flows;
|
|
3102
|
-
}
|
|
3103
|
-
function captureUidexIds(body) {
|
|
3104
|
-
const out2 = [];
|
|
3105
|
-
const re = /uidex\(\s*(?:'([^']+)'|"([^"]+)"|`([^`$]+)`)\s*\)(?:\.(\w+)\s*\()?/g;
|
|
3106
|
-
let m;
|
|
3107
|
-
while ((m = re.exec(body)) !== null) {
|
|
3108
|
-
out2.push({ id: m[1] || m[2] || m[3], action: m[4] });
|
|
3109
|
-
}
|
|
3110
|
-
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
|
+
}));
|
|
3111
3197
|
}
|
|
3112
3198
|
function dedupe(arr) {
|
|
3113
3199
|
return Array.from(new Set(arr));
|
|
@@ -3142,25 +3228,28 @@ function runOne(dc, opts) {
|
|
|
3142
3228
|
const gitContext = resolveGitContext({ cwd: configDir });
|
|
3143
3229
|
const generated = emit({
|
|
3144
3230
|
registry: resolved.registry,
|
|
3145
|
-
gitContext
|
|
3146
|
-
typeMode: config.typeMode
|
|
3231
|
+
gitContext
|
|
3147
3232
|
});
|
|
3148
|
-
const outputPath =
|
|
3233
|
+
const outputPath = path9.resolve(configDir, config.output);
|
|
3149
3234
|
const outputRel = config.output;
|
|
3150
3235
|
let existingOnDisk = null;
|
|
3151
3236
|
if (opts.check) {
|
|
3152
3237
|
try {
|
|
3153
|
-
existingOnDisk =
|
|
3238
|
+
existingOnDisk = fs6.readFileSync(outputPath, "utf8");
|
|
3154
3239
|
} catch {
|
|
3155
3240
|
existingOnDisk = null;
|
|
3156
3241
|
}
|
|
3157
3242
|
}
|
|
3243
|
+
const hasExtractDiagnostics = [...extracted, ...extractedFlows].some(
|
|
3244
|
+
(ef) => (ef.diagnostics?.length ?? 0) > 0
|
|
3245
|
+
);
|
|
3158
3246
|
let auditResult;
|
|
3159
|
-
if (opts.check || opts.lint || resolved.diagnostics.length > 0) {
|
|
3247
|
+
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
|
|
3160
3248
|
auditResult = audit({
|
|
3161
3249
|
registry: resolved.registry,
|
|
3162
3250
|
extracted,
|
|
3163
3251
|
files: sourceFiles,
|
|
3252
|
+
flowExtracted: extractedFlows,
|
|
3164
3253
|
config,
|
|
3165
3254
|
check: opts.check,
|
|
3166
3255
|
lint: opts.lint,
|
|
@@ -3181,29 +3270,185 @@ function runOne(dc, opts) {
|
|
|
3181
3270
|
};
|
|
3182
3271
|
}
|
|
3183
3272
|
function writeScanResult(result2) {
|
|
3184
|
-
|
|
3185
|
-
|
|
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 };
|
|
3186
3430
|
}
|
|
3187
3431
|
|
|
3188
3432
|
// src/scanner/scan/scaffold.ts
|
|
3189
|
-
var
|
|
3190
|
-
var
|
|
3191
|
-
function
|
|
3433
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
3434
|
+
var path10 = __toESM(require("path"), 1);
|
|
3435
|
+
function scaffoldSpec(opts) {
|
|
3192
3436
|
const {
|
|
3193
3437
|
registry,
|
|
3194
|
-
|
|
3438
|
+
kind,
|
|
3439
|
+
id,
|
|
3195
3440
|
outDir,
|
|
3196
3441
|
force = false,
|
|
3197
3442
|
fixtureImport = "./fixtures"
|
|
3198
3443
|
} = opts;
|
|
3199
|
-
const
|
|
3200
|
-
if (!
|
|
3201
|
-
throw new Error(
|
|
3202
|
-
}
|
|
3203
|
-
const criteria =
|
|
3204
|
-
const filename = `widget-${
|
|
3205
|
-
const outputPath =
|
|
3206
|
-
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) {
|
|
3207
3452
|
return {
|
|
3208
3453
|
outputPath,
|
|
3209
3454
|
written: false,
|
|
@@ -3211,15 +3456,14 @@ function scaffoldWidgetSpec(opts) {
|
|
|
3211
3456
|
reason: `spec already exists at ${outputPath}; pass --force to overwrite`
|
|
3212
3457
|
};
|
|
3213
3458
|
}
|
|
3214
|
-
const content = renderSpec({
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
fixtureImport
|
|
3218
|
-
});
|
|
3219
|
-
fs6.mkdirSync(path8.dirname(outputPath), { recursive: true });
|
|
3220
|
-
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");
|
|
3221
3462
|
return { outputPath, written: true, skipped: false };
|
|
3222
3463
|
}
|
|
3464
|
+
function capitalize(s) {
|
|
3465
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
3466
|
+
}
|
|
3223
3467
|
function renderSpec(args) {
|
|
3224
3468
|
const lines = [];
|
|
3225
3469
|
lines.push(
|
|
@@ -3227,7 +3471,7 @@ function renderSpec(args) {
|
|
|
3227
3471
|
);
|
|
3228
3472
|
lines.push("");
|
|
3229
3473
|
lines.push(
|
|
3230
|
-
`test.describe(${JSON.stringify(args.
|
|
3474
|
+
`test.describe(${JSON.stringify(args.id)}, { tag: "@uidex:flow" }, () => {`
|
|
3231
3475
|
);
|
|
3232
3476
|
if (args.criteria.length === 0) {
|
|
3233
3477
|
lines.push(` test("TODO: add acceptance criteria", async () => {`);
|
|
@@ -3293,6 +3537,8 @@ async function run(opts) {
|
|
|
3293
3537
|
return runScanCommand(cwd, flags, writer);
|
|
3294
3538
|
case "scaffold":
|
|
3295
3539
|
return runScaffold(cwd, positional.slice(1), flags, writer);
|
|
3540
|
+
case "rename":
|
|
3541
|
+
return runRename(cwd, positional.slice(1), flags, writer);
|
|
3296
3542
|
case "ai": {
|
|
3297
3543
|
const result2 = await runAiCommand({
|
|
3298
3544
|
cwd,
|
|
@@ -3319,7 +3565,8 @@ function helpText2() {
|
|
|
3319
3565
|
"Commands:",
|
|
3320
3566
|
" init Create a .uidex.json",
|
|
3321
3567
|
" scan [flags] Run the scanner pipeline",
|
|
3322
|
-
" 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)",
|
|
3323
3570
|
" ai <install|uninstall|providers> Manage AI assistant integrations",
|
|
3324
3571
|
" api <METHOD> <PATH> Call the uidex API",
|
|
3325
3572
|
" api --list Show available API routes",
|
|
@@ -3328,16 +3575,17 @@ function helpText2() {
|
|
|
3328
3575
|
"",
|
|
3329
3576
|
"Flags:",
|
|
3330
3577
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
3331
|
-
" --lint Run lint diagnostics (missing annotations, scope leak,
|
|
3578
|
+
" --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
|
|
3332
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",
|
|
3333
3581
|
" --json Emit JSON diagnostics on stdout",
|
|
3334
3582
|
" --force (scaffold) overwrite existing spec",
|
|
3335
3583
|
""
|
|
3336
3584
|
].join("\n");
|
|
3337
3585
|
}
|
|
3338
3586
|
function runInit(cwd, w) {
|
|
3339
|
-
const configPath =
|
|
3340
|
-
if (
|
|
3587
|
+
const configPath = path11.join(cwd, CONFIG_FILENAME);
|
|
3588
|
+
if (fs8.existsSync(configPath)) {
|
|
3341
3589
|
w.err(`.uidex.json already exists at ${configPath}`);
|
|
3342
3590
|
return w.result(1);
|
|
3343
3591
|
}
|
|
@@ -3346,16 +3594,16 @@ function runInit(cwd, w) {
|
|
|
3346
3594
|
sources: [{ rootDir: "src" }],
|
|
3347
3595
|
output: "src/uidex.gen.ts"
|
|
3348
3596
|
};
|
|
3349
|
-
|
|
3597
|
+
fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3350
3598
|
w.out(`Created ${configPath}`);
|
|
3351
|
-
const gitignorePath =
|
|
3599
|
+
const gitignorePath = path11.join(cwd, ".gitignore");
|
|
3352
3600
|
const entry = "*.gen.ts";
|
|
3353
|
-
if (
|
|
3354
|
-
const existing =
|
|
3601
|
+
if (fs8.existsSync(gitignorePath)) {
|
|
3602
|
+
const existing = fs8.readFileSync(gitignorePath, "utf8");
|
|
3355
3603
|
const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
|
|
3356
3604
|
if (!hasEntry) {
|
|
3357
3605
|
const needsNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
3358
|
-
|
|
3606
|
+
fs8.appendFileSync(
|
|
3359
3607
|
gitignorePath,
|
|
3360
3608
|
`${needsNewline ? "\n" : ""}${entry}
|
|
3361
3609
|
`,
|
|
@@ -3364,21 +3612,33 @@ function runInit(cwd, w) {
|
|
|
3364
3612
|
w.out(`Appended ${entry} to ${gitignorePath}`);
|
|
3365
3613
|
}
|
|
3366
3614
|
} else {
|
|
3367
|
-
|
|
3615
|
+
fs8.writeFileSync(gitignorePath, `${entry}
|
|
3368
3616
|
`, "utf8");
|
|
3369
3617
|
w.out(`Created ${gitignorePath} with ${entry}`);
|
|
3370
3618
|
}
|
|
3371
3619
|
return w.result(0);
|
|
3372
3620
|
}
|
|
3373
3621
|
function runScanCommand(cwd, flags, w) {
|
|
3374
|
-
const
|
|
3375
|
-
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);
|
|
3376
3625
|
const asJson = Boolean(flags.json);
|
|
3377
|
-
|
|
3626
|
+
let configs = discover({ cwd });
|
|
3378
3627
|
if (configs.length === 0) {
|
|
3379
3628
|
w.err(`No ${CONFIG_FILENAME} found under ${cwd}`);
|
|
3380
3629
|
return w.result(1);
|
|
3381
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
|
+
}
|
|
3382
3642
|
const results = runScan({ cwd, check, lint, configs });
|
|
3383
3643
|
if (!check) {
|
|
3384
3644
|
for (const r of results) writeScanResult(r);
|
|
@@ -3393,9 +3653,21 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3393
3653
|
{ errors: 0, warnings: 0 }
|
|
3394
3654
|
);
|
|
3395
3655
|
if (asJson) {
|
|
3396
|
-
const out2 = {
|
|
3656
|
+
const out2 = {
|
|
3657
|
+
diagnostics: allDiagnostics.map(jsonDiagnostic),
|
|
3658
|
+
summary,
|
|
3659
|
+
...fix ? { fixed, fixSkipped } : {}
|
|
3660
|
+
};
|
|
3397
3661
|
w.out(JSON.stringify(out2, null, 2));
|
|
3398
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
|
+
}
|
|
3399
3671
|
for (const r of results) {
|
|
3400
3672
|
if (check) {
|
|
3401
3673
|
w.out(`Checked ${r.outputPath}`);
|
|
@@ -3405,7 +3677,10 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3405
3677
|
for (const d of r.audit?.diagnostics ?? []) {
|
|
3406
3678
|
const loc = d.file ? `${d.file}${d.line ? `:${d.line}` : ""}` : "";
|
|
3407
3679
|
const stream = d.severity === "error" ? w.err : w.out;
|
|
3408
|
-
|
|
3680
|
+
const fixable = d.fix && !fix ? " [fixable: run with --fix]" : "";
|
|
3681
|
+
stream(
|
|
3682
|
+
`${d.severity.toUpperCase()} [${d.code}] ${loc} ${d.message}${fixable}`
|
|
3683
|
+
);
|
|
3409
3684
|
if (d.hint) stream(` hint: ${d.hint}`);
|
|
3410
3685
|
}
|
|
3411
3686
|
}
|
|
@@ -3416,20 +3691,27 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3416
3691
|
const exit = summary.errors > 0 ? 1 : 0;
|
|
3417
3692
|
return w.result(exit);
|
|
3418
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"]);
|
|
3419
3699
|
function runScaffold(cwd, args, flags, w) {
|
|
3420
3700
|
const [kind, id] = args;
|
|
3421
|
-
if (kind
|
|
3422
|
-
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]");
|
|
3423
3703
|
return w.result(1);
|
|
3424
3704
|
}
|
|
3705
|
+
const scaffoldKind = kind;
|
|
3425
3706
|
const results = runScan({ cwd });
|
|
3426
3707
|
for (const r of results) {
|
|
3427
|
-
const
|
|
3428
|
-
if (!
|
|
3429
|
-
const outDir =
|
|
3430
|
-
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({
|
|
3431
3712
|
registry: r.registry,
|
|
3432
|
-
|
|
3713
|
+
kind: scaffoldKind,
|
|
3714
|
+
id,
|
|
3433
3715
|
outDir,
|
|
3434
3716
|
force: Boolean(flags.force)
|
|
3435
3717
|
});
|
|
@@ -3440,9 +3722,43 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
3440
3722
|
w.out(`Wrote ${result2.outputPath}`);
|
|
3441
3723
|
return w.result(0);
|
|
3442
3724
|
}
|
|
3443
|
-
w.err(
|
|
3725
|
+
w.err(
|
|
3726
|
+
`${scaffoldKind.charAt(0).toUpperCase() + scaffoldKind.slice(1)} "${id}" not found in registry`
|
|
3727
|
+
);
|
|
3444
3728
|
return w.result(1);
|
|
3445
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
|
+
}
|
|
3446
3762
|
function createWriter() {
|
|
3447
3763
|
let stdout = "";
|
|
3448
3764
|
let stderr = "";
|
|
@@ -3502,15 +3818,15 @@ function createFileTokenStorage(options) {
|
|
|
3502
3818
|
}
|
|
3503
3819
|
function defaultTokenPath() {
|
|
3504
3820
|
const os = require("os");
|
|
3505
|
-
const
|
|
3506
|
-
return
|
|
3821
|
+
const path12 = require("path");
|
|
3822
|
+
return path12.join(os.homedir(), ".uidex", "cloud-token.json");
|
|
3507
3823
|
}
|
|
3508
3824
|
|
|
3509
3825
|
// src/scanner/cli/http.ts
|
|
3510
3826
|
function createHttpClient(options) {
|
|
3511
3827
|
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
3512
|
-
async function request(
|
|
3513
|
-
let url = `${baseUrl}${
|
|
3828
|
+
async function request(path12, opts = {}) {
|
|
3829
|
+
let url = `${baseUrl}${path12.startsWith("/") ? path12 : `/${path12}`}`;
|
|
3514
3830
|
if (opts.query) {
|
|
3515
3831
|
url += (url.includes("?") ? "&" : "?") + opts.query;
|
|
3516
3832
|
}
|
|
@@ -3898,9 +4214,9 @@ var API_ROUTES = [
|
|
|
3898
4214
|
},
|
|
3899
4215
|
{
|
|
3900
4216
|
"method": "POST",
|
|
3901
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/reports/
|
|
3902
|
-
"operationId": "
|
|
3903
|
-
"summary": "
|
|
4217
|
+
"path": "/api/organizations/{orgId}/projects/{projectId}/reports/bulk-status",
|
|
4218
|
+
"operationId": "bulkUpdateReportStatus",
|
|
4219
|
+
"summary": "Update status on multiple feedback records.",
|
|
3904
4220
|
"tag": "Reports",
|
|
3905
4221
|
"params": [
|
|
3906
4222
|
"orgId",
|
|
@@ -4122,17 +4438,17 @@ async function runApiCommand(opts) {
|
|
|
4122
4438
|
if (sub === "login") return runLogin(ctx);
|
|
4123
4439
|
if (sub === "status") return runStatus(ctx);
|
|
4124
4440
|
const method = sub?.toUpperCase();
|
|
4125
|
-
const
|
|
4441
|
+
const path12 = positional[1];
|
|
4126
4442
|
if (!method || !METHODS.has(method)) {
|
|
4127
4443
|
stderr.push(`Unknown command or method: ${sub}`);
|
|
4128
4444
|
stderr.push(HELP);
|
|
4129
4445
|
return result(1, stdout, stderr);
|
|
4130
4446
|
}
|
|
4131
|
-
if (!
|
|
4447
|
+
if (!path12) {
|
|
4132
4448
|
stderr.push("Missing path. Usage: uidex api GET /api/organizations");
|
|
4133
4449
|
return result(1, stdout, stderr);
|
|
4134
4450
|
}
|
|
4135
|
-
return runRequest(ctx, method,
|
|
4451
|
+
return runRequest(ctx, method, path12);
|
|
4136
4452
|
}
|
|
4137
4453
|
function listRoutes(ctx) {
|
|
4138
4454
|
const { flags, color, stdout, stderr } = ctx;
|
|
@@ -4171,7 +4487,7 @@ function listRoutes(ctx) {
|
|
|
4171
4487
|
return result(0, stdout, stderr);
|
|
4172
4488
|
}
|
|
4173
4489
|
async function runLogin(ctx) {
|
|
4174
|
-
const { flags, opts,
|
|
4490
|
+
const { flags, opts, stdout, stderr } = ctx;
|
|
4175
4491
|
const token = flags.token;
|
|
4176
4492
|
if (typeof token === "string" && token.length > 0) {
|
|
4177
4493
|
const storage = resolveTokenStorage(opts);
|
|
@@ -4251,7 +4567,7 @@ async function runBrowserLogin(ctx) {
|
|
|
4251
4567
|
stdout.push("");
|
|
4252
4568
|
process.stdout.write(result(0, stdout, stderr).stdout);
|
|
4253
4569
|
stdout.length = 0;
|
|
4254
|
-
openBrowser(authUrl);
|
|
4570
|
+
openBrowser(authUrl, exec);
|
|
4255
4571
|
});
|
|
4256
4572
|
server.on("error", (err2) => {
|
|
4257
4573
|
stderr.push(`Failed to start local server: ${err2.message}`);
|
|
@@ -4259,8 +4575,7 @@ async function runBrowserLogin(ctx) {
|
|
|
4259
4575
|
});
|
|
4260
4576
|
});
|
|
4261
4577
|
}
|
|
4262
|
-
function openBrowser(url) {
|
|
4263
|
-
const { exec } = require("child_process");
|
|
4578
|
+
function openBrowser(url, exec) {
|
|
4264
4579
|
const platform = process.platform;
|
|
4265
4580
|
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
4266
4581
|
exec(`${cmd} ${JSON.stringify(url)}`);
|
|
@@ -4274,7 +4589,7 @@ function runStatus(ctx) {
|
|
|
4274
4589
|
stdout.push(`auth: ${token ? "authenticated" : "not authenticated"}`);
|
|
4275
4590
|
return result(token ? 0 : 1, stdout, stderr);
|
|
4276
4591
|
}
|
|
4277
|
-
async function runRequest(ctx, method,
|
|
4592
|
+
async function runRequest(ctx, method, path12) {
|
|
4278
4593
|
const { flags, opts, color, stdout, stderr } = ctx;
|
|
4279
4594
|
const tokenFromFlag = flags.token;
|
|
4280
4595
|
const token = typeof tokenFromFlag === "string" ? tokenFromFlag : resolveTokenStorage(opts).get();
|
|
@@ -4293,7 +4608,7 @@ async function runRequest(ctx, method, path10) {
|
|
|
4293
4608
|
return result(1, stdout, stderr);
|
|
4294
4609
|
}
|
|
4295
4610
|
}
|
|
4296
|
-
const res = await http.request(
|
|
4611
|
+
const res = await http.request(path12, {
|
|
4297
4612
|
method,
|
|
4298
4613
|
body: bodyStr,
|
|
4299
4614
|
query: typeof flags.query === "string" ? flags.query : void 0
|