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/scan/index.cjs
CHANGED
|
@@ -33,7 +33,7 @@ __export(scan_exports, {
|
|
|
33
33
|
CONFIG_FILENAME: () => CONFIG_FILENAME,
|
|
34
34
|
ConfigError: () => ConfigError,
|
|
35
35
|
DEFAULT_CONVENTIONS: () => DEFAULT_CONVENTIONS,
|
|
36
|
-
|
|
36
|
+
applyFixes: () => applyFixes,
|
|
37
37
|
audit: () => audit,
|
|
38
38
|
detectRoutes: () => detectRoutes,
|
|
39
39
|
discover: () => discover,
|
|
@@ -43,10 +43,12 @@ __export(scan_exports, {
|
|
|
43
43
|
globToRegExp: () => globToRegExp,
|
|
44
44
|
parseConfig: () => parseConfig,
|
|
45
45
|
pathToId: () => pathToId,
|
|
46
|
+
renameEntity: () => renameEntity,
|
|
46
47
|
resolve: () => resolve2,
|
|
47
48
|
resolveGitContext: () => resolveGitContext,
|
|
48
49
|
runCli: () => run,
|
|
49
50
|
runScan: () => runScan,
|
|
51
|
+
scaffoldSpec: () => scaffoldSpec,
|
|
50
52
|
scaffoldWidgetSpec: () => scaffoldWidgetSpec,
|
|
51
53
|
validateConfig: () => validateConfig,
|
|
52
54
|
walk: () => walk,
|
|
@@ -59,7 +61,6 @@ var fs = __toESM(require("fs"), 1);
|
|
|
59
61
|
var path = __toESM(require("path"), 1);
|
|
60
62
|
|
|
61
63
|
// src/scanner/scan/config.ts
|
|
62
|
-
var DEFAULT_TYPE_MODE = "strict";
|
|
63
64
|
var WELL_KNOWN_FILES = {
|
|
64
65
|
page: "uidex.page.ts",
|
|
65
66
|
feature: "uidex.feature.ts"
|
|
@@ -84,11 +85,9 @@ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
84
85
|
"exclude",
|
|
85
86
|
"output",
|
|
86
87
|
"flows",
|
|
87
|
-
"typeMode",
|
|
88
88
|
"audit",
|
|
89
89
|
"conventions"
|
|
90
90
|
]);
|
|
91
|
-
var ALLOWED_TYPE_MODES = /* @__PURE__ */ new Set(["strict", "loose"]);
|
|
92
91
|
var ALLOWED_SOURCE_KEYS = /* @__PURE__ */ new Set(["rootDir", "include", "exclude", "prefix"]);
|
|
93
92
|
var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
|
|
94
93
|
"primitives",
|
|
@@ -101,14 +100,14 @@ var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set(["scopeLeak", "coverage", "acce
|
|
|
101
100
|
function fail(msg) {
|
|
102
101
|
throw new ConfigError(`Invalid .uidex.json: ${msg}`);
|
|
103
102
|
}
|
|
104
|
-
function assertObject(value,
|
|
103
|
+
function assertObject(value, path12) {
|
|
105
104
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
106
|
-
fail(`${
|
|
105
|
+
fail(`${path12} must be an object`);
|
|
107
106
|
}
|
|
108
107
|
}
|
|
109
|
-
function assertStringArray(value,
|
|
108
|
+
function assertStringArray(value, path12) {
|
|
110
109
|
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
|
|
111
|
-
fail(`${
|
|
110
|
+
fail(`${path12} must be a string[]`);
|
|
112
111
|
}
|
|
113
112
|
}
|
|
114
113
|
function validateConfig(raw) {
|
|
@@ -156,11 +155,6 @@ function validateConfig(raw) {
|
|
|
156
155
|
}
|
|
157
156
|
if (raw.exclude !== void 0) assertStringArray(raw.exclude, `exclude`);
|
|
158
157
|
if (raw.flows !== void 0) assertStringArray(raw.flows, `flows`);
|
|
159
|
-
if (raw.typeMode !== void 0) {
|
|
160
|
-
if (typeof raw.typeMode !== "string" || !ALLOWED_TYPE_MODES.has(raw.typeMode)) {
|
|
161
|
-
fail(`"typeMode" must be "strict" or "loose"`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
158
|
if (raw.audit !== void 0) {
|
|
165
159
|
assertObject(raw.audit, "audit");
|
|
166
160
|
for (const key of Object.keys(raw.audit)) {
|
|
@@ -199,7 +193,6 @@ function validateConfig(raw) {
|
|
|
199
193
|
exclude: raw.exclude,
|
|
200
194
|
output: raw.output,
|
|
201
195
|
flows: raw.flows,
|
|
202
|
-
typeMode: raw.typeMode ?? DEFAULT_TYPE_MODE,
|
|
203
196
|
audit: raw.audit,
|
|
204
197
|
conventions: raw.conventions
|
|
205
198
|
};
|
|
@@ -406,6 +399,90 @@ function* walkDir(root, dir) {
|
|
|
406
399
|
}
|
|
407
400
|
}
|
|
408
401
|
|
|
402
|
+
// src/scanner/scan/ast.ts
|
|
403
|
+
var path3 = __toESM(require("path"), 1);
|
|
404
|
+
var import_oxc_parser = require("oxc-parser");
|
|
405
|
+
function langFor(sourcePath) {
|
|
406
|
+
switch (path3.extname(sourcePath)) {
|
|
407
|
+
case ".tsx":
|
|
408
|
+
return "tsx";
|
|
409
|
+
case ".ts":
|
|
410
|
+
case ".mts":
|
|
411
|
+
case ".cts":
|
|
412
|
+
return "ts";
|
|
413
|
+
default:
|
|
414
|
+
return "jsx";
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function makeLineAt(content) {
|
|
418
|
+
const starts = [0];
|
|
419
|
+
for (let i = 0; i < content.length; i++) {
|
|
420
|
+
if (content[i] === "\n") starts.push(i + 1);
|
|
421
|
+
}
|
|
422
|
+
return (offset) => {
|
|
423
|
+
let lo = 0;
|
|
424
|
+
let hi = starts.length - 1;
|
|
425
|
+
while (lo < hi) {
|
|
426
|
+
const mid = lo + hi + 1 >> 1;
|
|
427
|
+
if (starts[mid] <= offset) lo = mid;
|
|
428
|
+
else hi = mid - 1;
|
|
429
|
+
}
|
|
430
|
+
return lo + 1;
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function parseSource(file) {
|
|
434
|
+
const lineAt = makeLineAt(file.content);
|
|
435
|
+
try {
|
|
436
|
+
const result = (0, import_oxc_parser.parseSync)(file.sourcePath, file.content, {
|
|
437
|
+
lang: langFor(file.sourcePath),
|
|
438
|
+
sourceType: "module"
|
|
439
|
+
});
|
|
440
|
+
return {
|
|
441
|
+
program: result.program,
|
|
442
|
+
hasErrors: result.errors.length > 0,
|
|
443
|
+
comments: result.comments ?? [],
|
|
444
|
+
lineAt
|
|
445
|
+
};
|
|
446
|
+
} catch {
|
|
447
|
+
return { program: null, hasErrors: true, comments: [], lineAt };
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function isNode(value) {
|
|
451
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
452
|
+
}
|
|
453
|
+
function walkAst(root, visit) {
|
|
454
|
+
if (!isNode(root)) return;
|
|
455
|
+
if (visit(root) === false) return;
|
|
456
|
+
for (const key of Object.keys(root)) {
|
|
457
|
+
if (key === "type" || key === "start" || key === "end") continue;
|
|
458
|
+
const value = root[key];
|
|
459
|
+
if (Array.isArray(value)) {
|
|
460
|
+
for (const item of value) walkAst(item, visit);
|
|
461
|
+
} else if (isNode(value)) {
|
|
462
|
+
walkAst(value, visit);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function unwrapTsExpression(expr) {
|
|
467
|
+
let node = expr;
|
|
468
|
+
for (; ; ) {
|
|
469
|
+
switch (node.type) {
|
|
470
|
+
case "TSAsExpression":
|
|
471
|
+
case "TSSatisfiesExpression":
|
|
472
|
+
case "TSNonNullExpression":
|
|
473
|
+
case "TSTypeAssertion":
|
|
474
|
+
case "ParenthesizedExpression": {
|
|
475
|
+
const inner = node.expression;
|
|
476
|
+
if (!isNode(inner)) return node;
|
|
477
|
+
node = inner;
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
default:
|
|
481
|
+
return node;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
409
486
|
// src/scanner/scan/extract-uidex-export.ts
|
|
410
487
|
var KIND_DISCRIMINATORS = [
|
|
411
488
|
"page",
|
|
@@ -443,6 +520,16 @@ var FALSEABLE = /* @__PURE__ */ new Set([
|
|
|
443
520
|
"primitive",
|
|
444
521
|
"region"
|
|
445
522
|
]);
|
|
523
|
+
var SATISFIES_NAMES = {
|
|
524
|
+
page: "Page",
|
|
525
|
+
feature: "Feature",
|
|
526
|
+
primitive: "Primitive",
|
|
527
|
+
widget: "Widget",
|
|
528
|
+
region: "Region",
|
|
529
|
+
flow: "Flow",
|
|
530
|
+
notFlow: "NotFlow"
|
|
531
|
+
};
|
|
532
|
+
var KNOWN_SATISFIES = new Set(Object.values(SATISFIES_NAMES));
|
|
446
533
|
var ExtractError = class extends Error {
|
|
447
534
|
code;
|
|
448
535
|
hint;
|
|
@@ -454,649 +541,285 @@ var ExtractError = class extends Error {
|
|
|
454
541
|
this.hint = hint;
|
|
455
542
|
}
|
|
456
543
|
};
|
|
457
|
-
function extractUidexExports(file) {
|
|
544
|
+
function extractUidexExports(file, parsed) {
|
|
458
545
|
const exports2 = [];
|
|
459
546
|
const diagnostics = [];
|
|
460
547
|
const { content, displayPath } = file;
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
} catch (e) {
|
|
472
|
-
if (e instanceof ExtractError) {
|
|
473
|
-
diagnostics.push({
|
|
474
|
-
code: e.code,
|
|
475
|
-
severity: "error",
|
|
476
|
-
message: e.message,
|
|
477
|
-
file: displayPath,
|
|
478
|
-
line: e.pos.line,
|
|
479
|
-
hint: e.hint
|
|
480
|
-
});
|
|
481
|
-
} else {
|
|
482
|
-
throw e;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
return { exports: exports2, diagnostics };
|
|
487
|
-
}
|
|
488
|
-
var HEADER_RE = /(?:^|\n)[\t ]*export\s+const\s+uidex\b(?:\s*:\s*[^=\n]+?)?\s*=\s*/g;
|
|
489
|
-
function findExportHeaders(content) {
|
|
490
|
-
const out2 = [];
|
|
491
|
-
HEADER_RE.lastIndex = 0;
|
|
492
|
-
let m;
|
|
493
|
-
while ((m = HEADER_RE.exec(content)) !== null) {
|
|
494
|
-
const leadingNewline = m[0].startsWith("\n") ? 1 : 0;
|
|
495
|
-
const headerOffset = m.index + leadingNewline;
|
|
496
|
-
const exprStart = m.index + m[0].length;
|
|
497
|
-
if (isInsideCommentOrString(content, headerOffset)) continue;
|
|
498
|
-
out2.push({
|
|
499
|
-
headerPos: posAt(content, headerOffset),
|
|
500
|
-
exprStart
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
return out2;
|
|
504
|
-
}
|
|
505
|
-
function isInsideCommentOrString(content, target) {
|
|
506
|
-
let i = 0;
|
|
507
|
-
let inLineComment = false;
|
|
508
|
-
let inBlockComment = false;
|
|
509
|
-
let stringDelim = null;
|
|
510
|
-
let inTemplate = false;
|
|
511
|
-
let templateDepth = 0;
|
|
512
|
-
while (i < target) {
|
|
513
|
-
const c = content[i];
|
|
514
|
-
const n = content[i + 1];
|
|
515
|
-
if (inLineComment) {
|
|
516
|
-
if (c === "\n") inLineComment = false;
|
|
517
|
-
i++;
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
if (inBlockComment) {
|
|
521
|
-
if (c === "*" && n === "/") {
|
|
522
|
-
inBlockComment = false;
|
|
523
|
-
i += 2;
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
i++;
|
|
527
|
-
continue;
|
|
528
|
-
}
|
|
529
|
-
if (stringDelim !== null) {
|
|
530
|
-
if (c === "\\") {
|
|
531
|
-
i += 2;
|
|
532
|
-
continue;
|
|
533
|
-
}
|
|
534
|
-
if (c === stringDelim) stringDelim = null;
|
|
535
|
-
i++;
|
|
536
|
-
continue;
|
|
537
|
-
}
|
|
538
|
-
if (inTemplate) {
|
|
539
|
-
if (c === "\\") {
|
|
540
|
-
i += 2;
|
|
541
|
-
continue;
|
|
542
|
-
}
|
|
543
|
-
if (c === "$" && n === "{") {
|
|
544
|
-
templateDepth++;
|
|
545
|
-
i += 2;
|
|
546
|
-
continue;
|
|
547
|
-
}
|
|
548
|
-
if (c === "`" && templateDepth === 0) {
|
|
549
|
-
inTemplate = false;
|
|
550
|
-
i++;
|
|
548
|
+
const p2 = parsed ?? parseSource(file);
|
|
549
|
+
if (p2.program === null) return { exports: exports2, diagnostics };
|
|
550
|
+
for (const stmt of p2.program.body) {
|
|
551
|
+
if (stmt.type !== "ExportNamedDeclaration") continue;
|
|
552
|
+
const decl = stmt.declaration;
|
|
553
|
+
if (!isNode2(decl) || decl.type !== "VariableDeclaration") continue;
|
|
554
|
+
if (decl.kind !== "const") continue;
|
|
555
|
+
for (const declarator of decl.declarations ?? []) {
|
|
556
|
+
const id = declarator.id;
|
|
557
|
+
if (!id || id.type !== "Identifier" || String(id.name) !== "uidex") {
|
|
551
558
|
continue;
|
|
552
559
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
inLineComment = true;
|
|
563
|
-
i += 2;
|
|
564
|
-
continue;
|
|
565
|
-
}
|
|
566
|
-
if (c === "/" && n === "*") {
|
|
567
|
-
inBlockComment = true;
|
|
568
|
-
i += 2;
|
|
569
|
-
continue;
|
|
570
|
-
}
|
|
571
|
-
if (c === '"' || c === "'") {
|
|
572
|
-
stringDelim = c;
|
|
573
|
-
i++;
|
|
574
|
-
continue;
|
|
575
|
-
}
|
|
576
|
-
if (c === "`") {
|
|
577
|
-
inTemplate = true;
|
|
578
|
-
i++;
|
|
579
|
-
continue;
|
|
580
|
-
}
|
|
581
|
-
i++;
|
|
582
|
-
}
|
|
583
|
-
return inLineComment || inBlockComment || stringDelim !== null || inTemplate;
|
|
584
|
-
}
|
|
585
|
-
var Tokenizer = class {
|
|
586
|
-
constructor(src, start) {
|
|
587
|
-
this.src = src;
|
|
588
|
-
this.pos = start;
|
|
589
|
-
let line = 1;
|
|
590
|
-
let lineStart = 0;
|
|
591
|
-
for (let i = 0; i < start; i++) {
|
|
592
|
-
if (src[i] === "\n") {
|
|
593
|
-
line++;
|
|
594
|
-
lineStart = i + 1;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
this.line = line;
|
|
598
|
-
this.lineStart = lineStart;
|
|
599
|
-
}
|
|
600
|
-
src;
|
|
601
|
-
pos;
|
|
602
|
-
line;
|
|
603
|
-
lineStart;
|
|
604
|
-
currentPos() {
|
|
605
|
-
return {
|
|
606
|
-
offset: this.pos,
|
|
607
|
-
line: this.line,
|
|
608
|
-
column: this.pos - this.lineStart + 1
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
advance(n = 1) {
|
|
612
|
-
for (let i = 0; i < n; i++) {
|
|
613
|
-
if (this.pos < this.src.length && this.src[this.pos] === "\n") {
|
|
614
|
-
this.line++;
|
|
615
|
-
this.lineStart = this.pos + 1;
|
|
616
|
-
}
|
|
617
|
-
this.pos++;
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
skipTrivia() {
|
|
621
|
-
while (this.pos < this.src.length) {
|
|
622
|
-
const c = this.src[this.pos];
|
|
623
|
-
const n = this.src[this.pos + 1];
|
|
624
|
-
if (c === " " || c === " " || c === "\r" || c === "\n") {
|
|
625
|
-
this.advance();
|
|
626
|
-
continue;
|
|
627
|
-
}
|
|
628
|
-
if (c === "/" && n === "/") {
|
|
629
|
-
while (this.pos < this.src.length && this.src[this.pos] !== "\n") {
|
|
630
|
-
this.advance();
|
|
560
|
+
const headerPos = posAt(content, stmt.start, p2);
|
|
561
|
+
try {
|
|
562
|
+
const init = declarator.init;
|
|
563
|
+
if (!isNode2(init)) {
|
|
564
|
+
throw new ExtractError(
|
|
565
|
+
"uidex-export-invalid-literal",
|
|
566
|
+
"`export const uidex` must be assigned an object literal.",
|
|
567
|
+
headerPos
|
|
568
|
+
);
|
|
631
569
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
570
|
+
const value = toLitValue(unwrapTsExpression(init), content, p2);
|
|
571
|
+
const metadata = buildMetadata(
|
|
572
|
+
value,
|
|
573
|
+
displayPath,
|
|
574
|
+
file.sourcePath,
|
|
575
|
+
headerPos,
|
|
576
|
+
diagnostics
|
|
577
|
+
);
|
|
578
|
+
metadata.span = statementSpan(stmt, content);
|
|
579
|
+
checkSatisfies(init, metadata, displayPath, p2, diagnostics);
|
|
580
|
+
exports2.push(metadata);
|
|
581
|
+
} catch (e) {
|
|
582
|
+
if (e instanceof ExtractError) {
|
|
583
|
+
diagnostics.push({
|
|
584
|
+
code: e.code,
|
|
585
|
+
severity: "error",
|
|
586
|
+
message: e.message,
|
|
587
|
+
file: displayPath,
|
|
588
|
+
line: e.pos.line,
|
|
589
|
+
hint: e.hint
|
|
590
|
+
});
|
|
591
|
+
} else {
|
|
592
|
+
throw e;
|
|
642
593
|
}
|
|
643
|
-
continue;
|
|
644
594
|
}
|
|
645
|
-
break;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
next() {
|
|
649
|
-
this.skipTrivia();
|
|
650
|
-
if (this.pos >= this.src.length) {
|
|
651
|
-
return { kind: "eof", value: "", pos: this.currentPos(), end: this.pos };
|
|
652
|
-
}
|
|
653
|
-
const pos = this.currentPos();
|
|
654
|
-
const c = this.src[this.pos];
|
|
655
|
-
switch (c) {
|
|
656
|
-
case "{":
|
|
657
|
-
this.advance();
|
|
658
|
-
return { kind: "lbrace", value: c, pos, end: this.pos };
|
|
659
|
-
case "}":
|
|
660
|
-
this.advance();
|
|
661
|
-
return { kind: "rbrace", value: c, pos, end: this.pos };
|
|
662
|
-
case "[":
|
|
663
|
-
this.advance();
|
|
664
|
-
return { kind: "lbracket", value: c, pos, end: this.pos };
|
|
665
|
-
case "]":
|
|
666
|
-
this.advance();
|
|
667
|
-
return { kind: "rbracket", value: c, pos, end: this.pos };
|
|
668
|
-
case "(":
|
|
669
|
-
this.advance();
|
|
670
|
-
return { kind: "lparen", value: c, pos, end: this.pos };
|
|
671
|
-
case ")":
|
|
672
|
-
this.advance();
|
|
673
|
-
return { kind: "rparen", value: c, pos, end: this.pos };
|
|
674
|
-
case ",":
|
|
675
|
-
this.advance();
|
|
676
|
-
return { kind: "comma", value: c, pos, end: this.pos };
|
|
677
|
-
case ":":
|
|
678
|
-
this.advance();
|
|
679
|
-
return { kind: "colon", value: c, pos, end: this.pos };
|
|
680
|
-
}
|
|
681
|
-
if (c === "." && this.src[this.pos + 1] === "." && this.src[this.pos + 2] === ".") {
|
|
682
|
-
this.advance(3);
|
|
683
|
-
return { kind: "spread", value: "...", pos, end: this.pos };
|
|
684
|
-
}
|
|
685
|
-
if (c === '"' || c === "'") {
|
|
686
|
-
return this.readString(pos, c);
|
|
687
|
-
}
|
|
688
|
-
if (c === "`") {
|
|
689
|
-
return this.readTemplate(pos);
|
|
690
|
-
}
|
|
691
|
-
if (isDigit(c) || c === "-" && isDigit(this.src[this.pos + 1])) {
|
|
692
|
-
return this.readNumber(pos);
|
|
693
595
|
}
|
|
694
|
-
if (isIdentStart(c)) {
|
|
695
|
-
return this.readIdent(pos);
|
|
696
|
-
}
|
|
697
|
-
this.advance();
|
|
698
|
-
return { kind: "punct", value: c, pos, end: this.pos };
|
|
699
596
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
}
|
|
718
|
-
value += c;
|
|
719
|
-
this.advance();
|
|
720
|
-
}
|
|
721
|
-
return { kind: "punct", value: delim, pos, end: this.pos };
|
|
722
|
-
}
|
|
723
|
-
readTemplate(pos) {
|
|
724
|
-
this.advance();
|
|
725
|
-
let value = "";
|
|
726
|
-
let hasExpression = false;
|
|
727
|
-
while (this.pos < this.src.length) {
|
|
728
|
-
const c = this.src[this.pos];
|
|
729
|
-
const n = this.src[this.pos + 1];
|
|
730
|
-
if (c === "\\") {
|
|
731
|
-
const esc = this.src[this.pos + 1];
|
|
732
|
-
this.advance(2);
|
|
733
|
-
value += decodeEscape(esc);
|
|
734
|
-
continue;
|
|
735
|
-
}
|
|
736
|
-
if (c === "$" && n === "{") {
|
|
737
|
-
hasExpression = true;
|
|
738
|
-
this.advance(2);
|
|
739
|
-
let depth = 1;
|
|
740
|
-
while (this.pos < this.src.length && depth > 0) {
|
|
741
|
-
const ch = this.src[this.pos];
|
|
742
|
-
if (ch === "{") depth++;
|
|
743
|
-
else if (ch === "}") depth--;
|
|
744
|
-
this.advance();
|
|
597
|
+
return { exports: exports2, diagnostics };
|
|
598
|
+
}
|
|
599
|
+
function toLitValue(node, content, p2) {
|
|
600
|
+
const unwrapped = unwrapTsExpression(node);
|
|
601
|
+
const pos = posAt(content, unwrapped.start, p2);
|
|
602
|
+
const span = { start: unwrapped.start, end: unwrapped.end };
|
|
603
|
+
switch (unwrapped.type) {
|
|
604
|
+
case "Literal": {
|
|
605
|
+
const v = unwrapped.value;
|
|
606
|
+
if (typeof v === "string") return { kind: "string", value: v, pos, span };
|
|
607
|
+
if (typeof v === "number") {
|
|
608
|
+
if (!Number.isFinite(v)) {
|
|
609
|
+
throw new ExtractError(
|
|
610
|
+
"uidex-export-invalid-literal",
|
|
611
|
+
`Invalid numeric literal in \`export const uidex\`.`,
|
|
612
|
+
pos
|
|
613
|
+
);
|
|
745
614
|
}
|
|
746
|
-
|
|
615
|
+
return { kind: "number", value: v, pos, span };
|
|
747
616
|
}
|
|
748
|
-
if (
|
|
749
|
-
|
|
750
|
-
if (hasExpression) {
|
|
751
|
-
return { kind: "template", value, pos, end: this.pos };
|
|
752
|
-
}
|
|
753
|
-
return { kind: "string", value, pos, end: this.pos };
|
|
617
|
+
if (typeof v === "boolean") {
|
|
618
|
+
return { kind: "boolean", value: v, pos, span };
|
|
754
619
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
}
|
|
758
|
-
return { kind: "template", value, pos, end: this.pos };
|
|
759
|
-
}
|
|
760
|
-
readNumber(pos) {
|
|
761
|
-
const start = this.pos;
|
|
762
|
-
if (this.src[this.pos] === "-") this.advance();
|
|
763
|
-
while (this.pos < this.src.length && isDigit(this.src[this.pos])) {
|
|
764
|
-
this.advance();
|
|
765
|
-
}
|
|
766
|
-
if (this.src[this.pos] === ".") {
|
|
767
|
-
this.advance();
|
|
768
|
-
while (this.pos < this.src.length && isDigit(this.src[this.pos])) {
|
|
769
|
-
this.advance();
|
|
620
|
+
if (v === null && unwrapped.raw === "null") {
|
|
621
|
+
return { kind: "null", pos, span };
|
|
770
622
|
}
|
|
623
|
+
throw new ExtractError(
|
|
624
|
+
"uidex-export-invalid-literal",
|
|
625
|
+
`Unsupported literal in \`export const uidex\`; only strings, numbers, booleans, and null are allowed.`,
|
|
626
|
+
pos
|
|
627
|
+
);
|
|
771
628
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
if (
|
|
775
|
-
|
|
629
|
+
case "UnaryExpression": {
|
|
630
|
+
const arg = unwrapped.argument;
|
|
631
|
+
if (unwrapped.operator === "-" && isNode2(arg) && arg.type === "Literal" && typeof arg.value === "number") {
|
|
632
|
+
return { kind: "number", value: -arg.value, pos, span };
|
|
776
633
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
return { kind: "number", value, pos, end: this.pos };
|
|
783
|
-
}
|
|
784
|
-
readIdent(pos) {
|
|
785
|
-
const start = this.pos;
|
|
786
|
-
while (this.pos < this.src.length && isIdentPart(this.src[this.pos])) {
|
|
787
|
-
this.advance();
|
|
634
|
+
throw new ExtractError(
|
|
635
|
+
"uidex-export-invalid-literal",
|
|
636
|
+
"Unary expressions are not allowed in `export const uidex`.",
|
|
637
|
+
pos
|
|
638
|
+
);
|
|
788
639
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
};
|
|
793
|
-
function isDigit(c) {
|
|
794
|
-
return c !== void 0 && c >= "0" && c <= "9";
|
|
795
|
-
}
|
|
796
|
-
function isIdentStart(c) {
|
|
797
|
-
if (c === void 0) return false;
|
|
798
|
-
return c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_" || c === "$";
|
|
799
|
-
}
|
|
800
|
-
function isIdentPart(c) {
|
|
801
|
-
return isIdentStart(c) || isDigit(c);
|
|
802
|
-
}
|
|
803
|
-
function decodeEscape(esc) {
|
|
804
|
-
switch (esc) {
|
|
805
|
-
case "n":
|
|
806
|
-
return "\n";
|
|
807
|
-
case "t":
|
|
808
|
-
return " ";
|
|
809
|
-
case "r":
|
|
810
|
-
return "\r";
|
|
811
|
-
case "\\":
|
|
812
|
-
return "\\";
|
|
813
|
-
case "'":
|
|
814
|
-
return "'";
|
|
815
|
-
case '"':
|
|
816
|
-
return '"';
|
|
817
|
-
case "`":
|
|
818
|
-
return "`";
|
|
819
|
-
case "0":
|
|
820
|
-
return "\0";
|
|
821
|
-
case "b":
|
|
822
|
-
return "\b";
|
|
823
|
-
case "f":
|
|
824
|
-
return "\f";
|
|
825
|
-
case "v":
|
|
826
|
-
return "\v";
|
|
827
|
-
default:
|
|
828
|
-
return esc ?? "";
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
function parseExpression(content, start) {
|
|
832
|
-
const tokenizer = new Tokenizer(content, start);
|
|
833
|
-
const parser = new Parser(tokenizer);
|
|
834
|
-
const value = parser.parseValue();
|
|
835
|
-
parser.consumeTrailingAssertions();
|
|
836
|
-
return value;
|
|
837
|
-
}
|
|
838
|
-
var Parser = class {
|
|
839
|
-
constructor(tok) {
|
|
840
|
-
this.tok = tok;
|
|
841
|
-
}
|
|
842
|
-
tok;
|
|
843
|
-
lookahead = null;
|
|
844
|
-
peek() {
|
|
845
|
-
if (this.lookahead === null) this.lookahead = this.tok.next();
|
|
846
|
-
return this.lookahead;
|
|
847
|
-
}
|
|
848
|
-
consume() {
|
|
849
|
-
const t = this.peek();
|
|
850
|
-
this.lookahead = null;
|
|
851
|
-
return t;
|
|
852
|
-
}
|
|
853
|
-
parseValue() {
|
|
854
|
-
const t = this.peek();
|
|
855
|
-
switch (t.kind) {
|
|
856
|
-
case "lbrace":
|
|
857
|
-
return this.parseObject();
|
|
858
|
-
case "lbracket":
|
|
859
|
-
return this.parseArray();
|
|
860
|
-
case "string":
|
|
861
|
-
this.consume();
|
|
862
|
-
return { kind: "string", value: t.value, pos: t.pos };
|
|
863
|
-
case "template":
|
|
640
|
+
case "TemplateLiteral": {
|
|
641
|
+
const expressions = unwrapped.expressions ?? [];
|
|
642
|
+
if (expressions.length > 0) {
|
|
864
643
|
throw new ExtractError(
|
|
865
644
|
"uidex-export-invalid-literal",
|
|
866
645
|
"Template literal with expression parts is not allowed in `export const uidex`; use a plain string literal.",
|
|
867
|
-
|
|
868
|
-
);
|
|
869
|
-
case "number": {
|
|
870
|
-
this.consume();
|
|
871
|
-
const n = Number(t.value);
|
|
872
|
-
if (!Number.isFinite(n)) {
|
|
873
|
-
throw new ExtractError(
|
|
874
|
-
"uidex-export-invalid-literal",
|
|
875
|
-
`Invalid numeric literal "${t.value}" in \`export const uidex\`.`,
|
|
876
|
-
t.pos
|
|
877
|
-
);
|
|
878
|
-
}
|
|
879
|
-
return { kind: "number", value: n, pos: t.pos };
|
|
880
|
-
}
|
|
881
|
-
case "ident":
|
|
882
|
-
if (t.value === "true" || t.value === "false") {
|
|
883
|
-
this.consume();
|
|
884
|
-
return {
|
|
885
|
-
kind: "boolean",
|
|
886
|
-
value: t.value === "true",
|
|
887
|
-
pos: t.pos
|
|
888
|
-
};
|
|
889
|
-
}
|
|
890
|
-
if (t.value === "null") {
|
|
891
|
-
this.consume();
|
|
892
|
-
return { kind: "null", pos: t.pos };
|
|
893
|
-
}
|
|
894
|
-
if (t.value === "undefined") {
|
|
895
|
-
throw new ExtractError(
|
|
896
|
-
"uidex-export-invalid-literal",
|
|
897
|
-
"`undefined` is not allowed as a value in `export const uidex`; omit the field instead.",
|
|
898
|
-
t.pos
|
|
899
|
-
);
|
|
900
|
-
}
|
|
901
|
-
throw new ExtractError(
|
|
902
|
-
"uidex-export-invalid-literal",
|
|
903
|
-
`Identifier reference "${t.value}" is not allowed in \`export const uidex\`; the right-hand side must be a plain literal.`,
|
|
904
|
-
t.pos
|
|
905
|
-
);
|
|
906
|
-
case "spread":
|
|
907
|
-
throw new ExtractError(
|
|
908
|
-
"uidex-export-invalid-literal",
|
|
909
|
-
"Spread (`...`) is not allowed in `export const uidex`; the right-hand side must be a plain literal.",
|
|
910
|
-
t.pos
|
|
911
|
-
);
|
|
912
|
-
case "lparen":
|
|
913
|
-
throw new ExtractError(
|
|
914
|
-
"uidex-export-invalid-literal",
|
|
915
|
-
"Parenthesised or grouped expressions are not allowed in `export const uidex`.",
|
|
916
|
-
t.pos
|
|
917
|
-
);
|
|
918
|
-
case "punct":
|
|
919
|
-
throw new ExtractError(
|
|
920
|
-
"uidex-export-invalid-literal",
|
|
921
|
-
`Unexpected token "${t.value}" in \`export const uidex\`.`,
|
|
922
|
-
t.pos
|
|
923
|
-
);
|
|
924
|
-
case "eof":
|
|
925
|
-
throw new ExtractError(
|
|
926
|
-
"uidex-export-invalid-literal",
|
|
927
|
-
"Expected a value for `export const uidex` but reached end of file.",
|
|
928
|
-
t.pos
|
|
929
|
-
);
|
|
930
|
-
default:
|
|
931
|
-
throw new ExtractError(
|
|
932
|
-
"uidex-export-invalid-literal",
|
|
933
|
-
`Unexpected token in \`export const uidex\`.`,
|
|
934
|
-
t.pos
|
|
646
|
+
pos
|
|
935
647
|
);
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
parseObject() {
|
|
939
|
-
const open = this.consume();
|
|
940
|
-
const entries = [];
|
|
941
|
-
const seen = /* @__PURE__ */ new Set();
|
|
942
|
-
while (true) {
|
|
943
|
-
const t = this.peek();
|
|
944
|
-
if (t.kind === "rbrace") {
|
|
945
|
-
this.consume();
|
|
946
|
-
break;
|
|
947
648
|
}
|
|
948
|
-
|
|
649
|
+
const quasis = unwrapped.quasis ?? [];
|
|
650
|
+
const cooked = quasis[0]?.value?.cooked ?? "";
|
|
651
|
+
return { kind: "string", value: cooked, pos, span };
|
|
652
|
+
}
|
|
653
|
+
case "Identifier": {
|
|
654
|
+
const name = String(unwrapped.name);
|
|
655
|
+
if (name === "undefined") {
|
|
949
656
|
throw new ExtractError(
|
|
950
657
|
"uidex-export-invalid-literal",
|
|
951
|
-
"
|
|
952
|
-
|
|
658
|
+
"`undefined` is not allowed as a value in `export const uidex`; omit the field instead.",
|
|
659
|
+
pos
|
|
953
660
|
);
|
|
954
661
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
662
|
+
throw new ExtractError(
|
|
663
|
+
"uidex-export-invalid-literal",
|
|
664
|
+
`Identifier reference "${name}" is not allowed in \`export const uidex\`; the right-hand side must be a plain literal.`,
|
|
665
|
+
pos
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
case "ObjectExpression":
|
|
669
|
+
return objectLit(unwrapped, content, p2, pos, span);
|
|
670
|
+
case "ArrayExpression":
|
|
671
|
+
return arrayLit(unwrapped, content, p2, pos, span);
|
|
672
|
+
case "SpreadElement":
|
|
673
|
+
throw new ExtractError(
|
|
674
|
+
"uidex-export-invalid-literal",
|
|
675
|
+
"Spread (`...`) is not allowed in `export const uidex`; the right-hand side must be a plain literal.",
|
|
676
|
+
pos
|
|
677
|
+
);
|
|
678
|
+
case "CallExpression":
|
|
679
|
+
throw new ExtractError(
|
|
680
|
+
"uidex-export-invalid-literal",
|
|
681
|
+
"Function calls are not allowed in `export const uidex`; the right-hand side must be a plain literal.",
|
|
682
|
+
pos
|
|
683
|
+
);
|
|
684
|
+
case "ConditionalExpression":
|
|
685
|
+
throw new ExtractError(
|
|
686
|
+
"uidex-export-invalid-literal",
|
|
687
|
+
"Conditional expressions are not allowed in `export const uidex`; the right-hand side must be a plain literal.",
|
|
688
|
+
pos
|
|
689
|
+
);
|
|
690
|
+
default:
|
|
691
|
+
throw new ExtractError(
|
|
692
|
+
"uidex-export-invalid-literal",
|
|
693
|
+
`Computed expressions are not allowed in \`export const uidex\`; the right-hand side must be a plain literal.`,
|
|
694
|
+
pos
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function objectLit(node, content, p2, pos, span) {
|
|
699
|
+
const entries = [];
|
|
700
|
+
const seen = /* @__PURE__ */ new Set();
|
|
701
|
+
for (const prop of node.properties ?? []) {
|
|
702
|
+
if (prop.type === "SpreadElement") {
|
|
703
|
+
throw new ExtractError(
|
|
704
|
+
"uidex-export-invalid-literal",
|
|
705
|
+
"Spread (`...`) is not allowed inside `export const uidex`.",
|
|
706
|
+
posAt(content, prop.start, p2)
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
if (prop.type !== "Property") {
|
|
710
|
+
throw new ExtractError(
|
|
711
|
+
"uidex-export-invalid-literal",
|
|
712
|
+
"Unexpected member inside `export const uidex` object.",
|
|
713
|
+
posAt(content, prop.start, p2)
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
const keyNode = prop.key;
|
|
717
|
+
const keyPos = posAt(content, keyNode.start, p2);
|
|
718
|
+
if (prop.shorthand) {
|
|
719
|
+
throw new ExtractError(
|
|
720
|
+
"uidex-export-invalid-literal",
|
|
721
|
+
`Shorthand property "${String(keyNode.name)}" is not allowed; write "${String(keyNode.name)}: ..." with a literal value.`,
|
|
722
|
+
keyPos
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
let key;
|
|
726
|
+
if (prop.computed) {
|
|
727
|
+
if (keyNode.type !== "Literal" || typeof keyNode.value !== "string") {
|
|
1007
728
|
throw new ExtractError(
|
|
1008
729
|
"uidex-export-invalid-literal",
|
|
1009
|
-
`
|
|
1010
|
-
|
|
730
|
+
"Computed property keys must be string literals in `export const uidex`.",
|
|
731
|
+
keyPos
|
|
1011
732
|
);
|
|
1012
733
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
this.consume();
|
|
1020
|
-
break;
|
|
1021
|
-
}
|
|
734
|
+
key = keyNode.value;
|
|
735
|
+
} else if (keyNode.type === "Identifier") {
|
|
736
|
+
key = String(keyNode.name);
|
|
737
|
+
} else if (keyNode.type === "Literal" && typeof keyNode.value === "string") {
|
|
738
|
+
key = keyNode.value;
|
|
739
|
+
} else {
|
|
1022
740
|
throw new ExtractError(
|
|
1023
741
|
"uidex-export-invalid-literal",
|
|
1024
|
-
|
|
1025
|
-
|
|
742
|
+
"Numeric property keys are not allowed in `export const uidex`.",
|
|
743
|
+
keyPos
|
|
1026
744
|
);
|
|
1027
745
|
}
|
|
1028
|
-
return { kind: "object", entries, pos: open.pos };
|
|
1029
|
-
}
|
|
1030
|
-
recordEntry(entries, seen, key, value, pos) {
|
|
1031
746
|
if (seen.has(key)) {
|
|
1032
747
|
throw new ExtractError(
|
|
1033
748
|
"uidex-export-duplicate-field",
|
|
1034
749
|
`Duplicate field "${key}" in \`export const uidex\`.`,
|
|
1035
|
-
|
|
750
|
+
keyPos
|
|
1036
751
|
);
|
|
1037
752
|
}
|
|
1038
753
|
seen.add(key);
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
items.push(value);
|
|
1061
|
-
const after = this.peek();
|
|
1062
|
-
if (after.kind === "comma") {
|
|
1063
|
-
this.consume();
|
|
1064
|
-
continue;
|
|
1065
|
-
}
|
|
1066
|
-
if (after.kind === "rbracket") {
|
|
1067
|
-
this.consume();
|
|
1068
|
-
break;
|
|
1069
|
-
}
|
|
754
|
+
const value = toLitValue(prop.value, content, p2);
|
|
755
|
+
entries.push({
|
|
756
|
+
key,
|
|
757
|
+
value,
|
|
758
|
+
keyPos,
|
|
759
|
+
span: removalSpan(content, prop.start, prop.end)
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
return { kind: "object", entries, pos, span };
|
|
763
|
+
}
|
|
764
|
+
function arrayLit(node, content, p2, pos, span) {
|
|
765
|
+
const items = [];
|
|
766
|
+
for (const el of node.elements ?? []) {
|
|
767
|
+
if (el === null) {
|
|
768
|
+
throw new ExtractError(
|
|
769
|
+
"uidex-export-invalid-literal",
|
|
770
|
+
"Array holes are not allowed in `export const uidex`.",
|
|
771
|
+
pos
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
if (el.type === "SpreadElement") {
|
|
1070
775
|
throw new ExtractError(
|
|
1071
776
|
"uidex-export-invalid-literal",
|
|
1072
|
-
|
|
1073
|
-
|
|
777
|
+
"Spread (`...`) is not allowed inside `export const uidex`.",
|
|
778
|
+
posAt(content, el.start, p2)
|
|
1074
779
|
);
|
|
1075
780
|
}
|
|
1076
|
-
|
|
781
|
+
items.push(toLitValue(el, content, p2));
|
|
1077
782
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
"uidex-export-invalid-literal",
|
|
1088
|
-
"Only `as const` is allowed after the `export const uidex` value.",
|
|
1089
|
-
next.pos
|
|
1090
|
-
);
|
|
1091
|
-
}
|
|
783
|
+
return { kind: "array", items, pos, span };
|
|
784
|
+
}
|
|
785
|
+
function checkSatisfies(init, metadata, file, p2, diagnostics) {
|
|
786
|
+
let node = init;
|
|
787
|
+
let satisfiesType;
|
|
788
|
+
while (isNode2(node)) {
|
|
789
|
+
if (node.type === "TSSatisfiesExpression") {
|
|
790
|
+
satisfiesType = node.typeAnnotation;
|
|
791
|
+
break;
|
|
1092
792
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
793
|
+
if (node.type === "TSAsExpression" || node.type === "TSNonNullExpression" || node.type === "TSTypeAssertion" || node.type === "ParenthesizedExpression") {
|
|
794
|
+
node = node.expression;
|
|
795
|
+
continue;
|
|
1096
796
|
}
|
|
797
|
+
break;
|
|
1097
798
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
799
|
+
if (!isNode2(satisfiesType) || satisfiesType.type !== "TSTypeReference") return;
|
|
800
|
+
const typeName = satisfiesType.typeName;
|
|
801
|
+
if (!isNode2(typeName) || typeName.type !== "TSQualifiedName") return;
|
|
802
|
+
const left = typeName.left;
|
|
803
|
+
const right = typeName.right;
|
|
804
|
+
if (!isNode2(left) || left.type !== "Identifier" || left.name !== "Uidex") {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
if (!isNode2(right) || right.type !== "Identifier") return;
|
|
808
|
+
const actual = String(right.name);
|
|
809
|
+
if (!KNOWN_SATISFIES.has(actual)) return;
|
|
810
|
+
const discriminator = metadata.notFlow ? "notFlow" : metadata.kind;
|
|
811
|
+
const expected = SATISFIES_NAMES[discriminator];
|
|
812
|
+
if (actual === expected) return;
|
|
813
|
+
diagnostics.push({
|
|
814
|
+
code: "uidex-export-satisfies-mismatch",
|
|
815
|
+
severity: "warning",
|
|
816
|
+
message: `\`export const uidex\` declares kind "${discriminator}" but is annotated \`satisfies Uidex.${actual}\`; expected \`Uidex.${expected}\`.`,
|
|
817
|
+
file,
|
|
818
|
+
line: p2.lineAt(satisfiesType.start),
|
|
819
|
+
hint: `Change the annotation to \`satisfies Uidex.${expected}\` or fix the kind discriminator.`
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
1100
823
|
if (value.kind !== "object") {
|
|
1101
824
|
throw new ExtractError(
|
|
1102
825
|
"uidex-export-invalid-literal",
|
|
@@ -1105,7 +828,7 @@ function buildMetadata(value, file, headerPos, diagnostics) {
|
|
|
1105
828
|
);
|
|
1106
829
|
}
|
|
1107
830
|
const byKey = /* @__PURE__ */ new Map();
|
|
1108
|
-
for (const
|
|
831
|
+
for (const entry of value.entries) byKey.set(entry.key, entry);
|
|
1109
832
|
const presentKinds = KIND_DISCRIMINATORS.filter(
|
|
1110
833
|
(k) => byKey.has(k)
|
|
1111
834
|
);
|
|
@@ -1128,49 +851,58 @@ function buildMetadata(value, file, headerPos, diagnostics) {
|
|
|
1128
851
|
const discriminator = presentKinds[0];
|
|
1129
852
|
const kind = discriminator === "notFlow" ? "flow" : discriminator;
|
|
1130
853
|
const allowed = ALLOWED_FIELDS[kind];
|
|
1131
|
-
for (const
|
|
1132
|
-
if (!allowed.has(
|
|
1133
|
-
const fieldVal = byKey.get(k);
|
|
854
|
+
for (const entry of value.entries) {
|
|
855
|
+
if (!allowed.has(entry.key)) {
|
|
1134
856
|
throw new ExtractError(
|
|
1135
857
|
"uidex-export-unknown-field",
|
|
1136
|
-
`Unknown field "${
|
|
858
|
+
`Unknown field "${entry.key}" in \`export const uidex\` for kind "${kind}". Allowed: ${Array.from(
|
|
1137
859
|
allowed
|
|
1138
860
|
).sort().join(", ")}.`,
|
|
1139
|
-
|
|
861
|
+
entry.value.pos
|
|
1140
862
|
);
|
|
1141
863
|
}
|
|
1142
864
|
}
|
|
1143
865
|
const idField = discriminator === "notFlow" ? "flow" : discriminator;
|
|
1144
|
-
const idValue = byKey.get(discriminator);
|
|
866
|
+
const idValue = byKey.get(discriminator).value;
|
|
1145
867
|
let id;
|
|
1146
868
|
if (discriminator === "notFlow") {
|
|
1147
|
-
|
|
1148
|
-
if (v.kind !== "boolean" || v.value !== true) {
|
|
869
|
+
if (idValue.kind !== "boolean" || idValue.value !== true) {
|
|
1149
870
|
throw new ExtractError(
|
|
1150
871
|
"uidex-export-invalid-field",
|
|
1151
872
|
"`notFlow` must be `true`.",
|
|
1152
|
-
|
|
873
|
+
idValue.pos
|
|
1153
874
|
);
|
|
1154
875
|
}
|
|
1155
876
|
id = false;
|
|
1156
877
|
} else {
|
|
1157
878
|
id = readIdField(idValue, kind, idField);
|
|
1158
879
|
}
|
|
1159
|
-
const acceptance = readStringArrayField(byKey, "acceptance");
|
|
880
|
+
const acceptance = readStringArrayField(byKey, "acceptance")?.values;
|
|
1160
881
|
const description = readStringField(byKey, "description");
|
|
1161
882
|
const name = readStringField(byKey, "name");
|
|
1162
883
|
if (name === "") {
|
|
1163
|
-
const
|
|
884
|
+
const entry = byKey.get("name");
|
|
1164
885
|
diagnostics.push({
|
|
1165
886
|
code: "uidex-export-empty-name",
|
|
1166
887
|
severity: "info",
|
|
1167
888
|
message: "`name` is an empty string; treating as unset.",
|
|
1168
889
|
file,
|
|
1169
|
-
line: pos.line
|
|
890
|
+
line: entry.value.pos.line,
|
|
891
|
+
fix: {
|
|
892
|
+
description: "Remove the empty `name` field",
|
|
893
|
+
edits: [
|
|
894
|
+
{
|
|
895
|
+
path: sourcePath,
|
|
896
|
+
start: entry.span.start,
|
|
897
|
+
end: entry.span.end,
|
|
898
|
+
replacement: ""
|
|
899
|
+
}
|
|
900
|
+
]
|
|
901
|
+
}
|
|
1170
902
|
});
|
|
1171
903
|
}
|
|
1172
|
-
const
|
|
1173
|
-
const
|
|
904
|
+
const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
905
|
+
const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
1174
906
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
1175
907
|
const metadata = {
|
|
1176
908
|
source: "ts-export",
|
|
@@ -1185,9 +917,21 @@ function buildMetadata(value, file, headerPos, diagnostics) {
|
|
|
1185
917
|
if (name) metadata.name = name;
|
|
1186
918
|
if (acceptance) metadata.acceptance = acceptance;
|
|
1187
919
|
if (description) metadata.description = description;
|
|
1188
|
-
if (
|
|
1189
|
-
|
|
920
|
+
if (featuresField) {
|
|
921
|
+
metadata.features = featuresField.values;
|
|
922
|
+
metadata.featureSpans = featuresField.spans;
|
|
923
|
+
}
|
|
924
|
+
if (widgetsField) {
|
|
925
|
+
metadata.widgets = widgetsField.values;
|
|
926
|
+
metadata.widgetSpans = widgetsField.spans;
|
|
927
|
+
}
|
|
1190
928
|
if (notFlow) metadata.notFlow = true;
|
|
929
|
+
if (typeof id === "string" && idValue.kind === "string") {
|
|
930
|
+
metadata.idSpan = idValue.span;
|
|
931
|
+
}
|
|
932
|
+
const fieldSpans = {};
|
|
933
|
+
for (const entry of value.entries) fieldSpans[entry.key] = entry.span;
|
|
934
|
+
metadata.fieldSpans = fieldSpans;
|
|
1191
935
|
return metadata;
|
|
1192
936
|
}
|
|
1193
937
|
function readIdField(value, kind, fieldName) {
|
|
@@ -1218,29 +962,30 @@ function readIdField(value, kind, fieldName) {
|
|
|
1218
962
|
);
|
|
1219
963
|
}
|
|
1220
964
|
function readStringField(byKey, name) {
|
|
1221
|
-
const
|
|
1222
|
-
if (!
|
|
1223
|
-
if (
|
|
965
|
+
const entry = byKey.get(name);
|
|
966
|
+
if (!entry) return void 0;
|
|
967
|
+
if (entry.value.kind !== "string") {
|
|
1224
968
|
throw new ExtractError(
|
|
1225
969
|
"uidex-export-invalid-field",
|
|
1226
970
|
`\`${name}\` must be a string.`,
|
|
1227
|
-
|
|
971
|
+
entry.value.pos
|
|
1228
972
|
);
|
|
1229
973
|
}
|
|
1230
|
-
return
|
|
974
|
+
return entry.value.value;
|
|
1231
975
|
}
|
|
1232
976
|
function readStringArrayField(byKey, name) {
|
|
1233
|
-
const
|
|
1234
|
-
if (!
|
|
1235
|
-
if (
|
|
977
|
+
const entry = byKey.get(name);
|
|
978
|
+
if (!entry) return void 0;
|
|
979
|
+
if (entry.value.kind !== "array") {
|
|
1236
980
|
throw new ExtractError(
|
|
1237
981
|
"uidex-export-invalid-field",
|
|
1238
982
|
`\`${name}\` must be an array of strings.`,
|
|
1239
|
-
|
|
983
|
+
entry.value.pos
|
|
1240
984
|
);
|
|
1241
985
|
}
|
|
1242
|
-
const
|
|
1243
|
-
|
|
986
|
+
const values = [];
|
|
987
|
+
const spans = [];
|
|
988
|
+
for (const item of entry.value.items) {
|
|
1244
989
|
if (item.kind !== "string") {
|
|
1245
990
|
throw new ExtractError(
|
|
1246
991
|
"uidex-export-invalid-field",
|
|
@@ -1248,316 +993,526 @@ function readStringArrayField(byKey, name) {
|
|
|
1248
993
|
item.pos
|
|
1249
994
|
);
|
|
1250
995
|
}
|
|
1251
|
-
|
|
996
|
+
values.push(item.value);
|
|
997
|
+
spans.push(item.span);
|
|
1252
998
|
}
|
|
1253
|
-
return
|
|
999
|
+
return { values, spans };
|
|
1254
1000
|
}
|
|
1255
|
-
function posAt(content, offset) {
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1001
|
+
function posAt(content, offset, p2) {
|
|
1002
|
+
const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
|
|
1003
|
+
return { offset, line: p2.lineAt(offset), column: offset - lineStart + 1 };
|
|
1004
|
+
}
|
|
1005
|
+
function removalSpan(content, start, end) {
|
|
1006
|
+
let e = end;
|
|
1007
|
+
while (e < content.length && /[ \t]/.test(content[e])) e++;
|
|
1008
|
+
if (content[e] === ",") return { start, end: e + 1 };
|
|
1009
|
+
let s = start;
|
|
1010
|
+
while (s > 0 && /[\s]/.test(content[s - 1])) s--;
|
|
1011
|
+
if (content[s - 1] === ",") return { start: s - 1, end };
|
|
1012
|
+
return { start, end };
|
|
1013
|
+
}
|
|
1014
|
+
function statementSpan(stmt, content) {
|
|
1015
|
+
let end = stmt.end;
|
|
1016
|
+
while (end < content.length && /[ \t]/.test(content[end])) end++;
|
|
1017
|
+
if (content[end] === ";") end++;
|
|
1018
|
+
while (end < content.length && /[ \t]/.test(content[end])) end++;
|
|
1019
|
+
if (content[end] === "\r") end++;
|
|
1020
|
+
if (content[end] === "\n") end++;
|
|
1021
|
+
return { start: stmt.start, end };
|
|
1022
|
+
}
|
|
1023
|
+
function isNode2(value) {
|
|
1024
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
1265
1025
|
}
|
|
1266
1026
|
|
|
1267
|
-
// src/scanner/scan/
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1027
|
+
// src/scanner/scan/flow-facts.ts
|
|
1028
|
+
function collectFlowFacts(parsed, content) {
|
|
1029
|
+
if (parsed.program === null || !content.includes("test.describe")) {
|
|
1030
|
+
return [];
|
|
1031
|
+
}
|
|
1032
|
+
const facts = [];
|
|
1033
|
+
walkAst(parsed.program, (node) => {
|
|
1034
|
+
if (node.type !== "CallExpression") return void 0;
|
|
1035
|
+
const fact = readTaggedDescribe(node, parsed);
|
|
1036
|
+
if (fact) facts.push(fact);
|
|
1037
|
+
return void 0;
|
|
1038
|
+
});
|
|
1039
|
+
return facts;
|
|
1278
1040
|
}
|
|
1279
|
-
function
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1041
|
+
function readTaggedDescribe(call, parsed) {
|
|
1042
|
+
const callee = call.callee;
|
|
1043
|
+
if (!callee || callee.type !== "MemberExpression" || !isIdentifier(callee.object, "test") || !isIdentifier(callee.property, "describe")) {
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
const args = call.arguments ?? [];
|
|
1047
|
+
const title = stringLiteralValue(args[0]);
|
|
1048
|
+
if (title === null) return null;
|
|
1049
|
+
if (!hasFlowTag(args[1])) return null;
|
|
1050
|
+
const body = args[2];
|
|
1051
|
+
const { calls, dynamicCalls } = body ? collectUidexCalls(body, parsed) : { calls: [], dynamicCalls: [] };
|
|
1052
|
+
return {
|
|
1053
|
+
title,
|
|
1054
|
+
line: parsed.lineAt(call.start),
|
|
1055
|
+
calls,
|
|
1056
|
+
...dynamicCalls.length > 0 ? { dynamicCalls } : {}
|
|
1291
1057
|
};
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
if (
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
if (
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
i = next;
|
|
1308
|
-
continue;
|
|
1309
|
-
}
|
|
1310
|
-
if (c === '"' || c === "'") {
|
|
1311
|
-
const next = skipString(content, i, c);
|
|
1312
|
-
advanceLines(i, next);
|
|
1313
|
-
i = next;
|
|
1314
|
-
continue;
|
|
1058
|
+
}
|
|
1059
|
+
function hasFlowTag(node) {
|
|
1060
|
+
if (!node || node.type !== "ObjectExpression") return false;
|
|
1061
|
+
for (const prop of node.properties ?? []) {
|
|
1062
|
+
if (prop.type !== "Property") continue;
|
|
1063
|
+
const key = prop.key;
|
|
1064
|
+
const keyName = key?.type === "Identifier" ? String(key.name) : key?.type === "Literal" ? String(key.value) : null;
|
|
1065
|
+
if (keyName !== "tag") continue;
|
|
1066
|
+
const value = prop.value;
|
|
1067
|
+
if (!value) return false;
|
|
1068
|
+
if (stringLiteralValue(value) === "@uidex:flow") return true;
|
|
1069
|
+
if (value.type === "ArrayExpression") {
|
|
1070
|
+
for (const el of value.elements ?? []) {
|
|
1071
|
+
if (el && stringLiteralValue(el) === "@uidex:flow") return true;
|
|
1072
|
+
}
|
|
1315
1073
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
function collectUidexCalls(body, parsed) {
|
|
1079
|
+
const calls = [];
|
|
1080
|
+
const dynamicCalls = [];
|
|
1081
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
1082
|
+
const record = (node, action) => {
|
|
1083
|
+
if (claimed.has(node)) return;
|
|
1084
|
+
claimed.add(node);
|
|
1085
|
+
const resolved = uidexCallId(node);
|
|
1086
|
+
if (resolved === null) {
|
|
1087
|
+
dynamicCalls.push({ line: parsed.lineAt(node.start) });
|
|
1088
|
+
return;
|
|
1321
1089
|
}
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1090
|
+
calls.push({
|
|
1091
|
+
id: resolved.id,
|
|
1092
|
+
...action ? { action } : {},
|
|
1093
|
+
line: parsed.lineAt(node.start),
|
|
1094
|
+
span: resolved.span
|
|
1095
|
+
});
|
|
1096
|
+
};
|
|
1097
|
+
walkAst(body, (node) => {
|
|
1098
|
+
if (node.type !== "CallExpression") return void 0;
|
|
1099
|
+
const callee = node.callee;
|
|
1100
|
+
if (callee?.type === "MemberExpression") {
|
|
1101
|
+
const inner = callee.object;
|
|
1102
|
+
if (inner && isUidexCall(inner)) {
|
|
1103
|
+
const property = callee.property;
|
|
1104
|
+
const action = property?.type === "Identifier" ? String(property.name) : void 0;
|
|
1105
|
+
record(inner, action);
|
|
1106
|
+
}
|
|
1107
|
+
return void 0;
|
|
1108
|
+
}
|
|
1109
|
+
if (isUidexCall(node)) record(node);
|
|
1110
|
+
return void 0;
|
|
1111
|
+
});
|
|
1112
|
+
return { calls, dynamicCalls };
|
|
1113
|
+
}
|
|
1114
|
+
function isUidexCall(node) {
|
|
1115
|
+
if (node.type !== "CallExpression") return false;
|
|
1116
|
+
if (!isIdentifier(node.callee, "uidex")) return false;
|
|
1117
|
+
return (node.arguments ?? []).length >= 1;
|
|
1118
|
+
}
|
|
1119
|
+
function uidexCallId(node) {
|
|
1120
|
+
const args = node.arguments ?? [];
|
|
1121
|
+
const arg = args[0];
|
|
1122
|
+
const id = stringLiteralValue(arg);
|
|
1123
|
+
if (id === null) return null;
|
|
1124
|
+
return { id, span: { start: arg.start, end: arg.end } };
|
|
1125
|
+
}
|
|
1126
|
+
function stringLiteralValue(node) {
|
|
1127
|
+
if (!node) return null;
|
|
1128
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
1129
|
+
return node.value.length > 0 ? node.value : null;
|
|
1130
|
+
}
|
|
1131
|
+
if (node.type === "TemplateLiteral") {
|
|
1132
|
+
const expressions = node.expressions ?? [];
|
|
1133
|
+
if (expressions.length > 0) return null;
|
|
1134
|
+
const quasis = node.quasis ?? [];
|
|
1135
|
+
const cooked = quasis[0]?.value?.cooked;
|
|
1136
|
+
return cooked && cooked.length > 0 ? cooked : null;
|
|
1137
|
+
}
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
function isIdentifier(node, name) {
|
|
1141
|
+
return typeof node === "object" && node !== null && node.type === "Identifier" && String(node.name) === name;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// src/scanner/scan/jsx-ancestry.ts
|
|
1145
|
+
var ATTR_NAME_RE = /^data-uidex(?:-(region|widget|primitive))?$/;
|
|
1146
|
+
var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["button", "a", "input", "select", "textarea"]);
|
|
1147
|
+
var LANDMARK_TAGS = /* @__PURE__ */ new Set(["header", "nav", "main", "aside", "footer"]);
|
|
1148
|
+
function attrKind(node) {
|
|
1149
|
+
const name = node.name;
|
|
1150
|
+
if (!name || name.type !== "JSXIdentifier") return null;
|
|
1151
|
+
const m = ATTR_NAME_RE.exec(String(name.name));
|
|
1152
|
+
if (!m) return null;
|
|
1153
|
+
return m[1] ?? "element";
|
|
1154
|
+
}
|
|
1155
|
+
function collectConstStrings(program) {
|
|
1156
|
+
const consts = /* @__PURE__ */ new Map();
|
|
1157
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1158
|
+
walkAst(program, (node) => {
|
|
1159
|
+
if (node.type !== "VariableDeclaration" || node.kind !== "const") {
|
|
1160
|
+
return void 0;
|
|
1161
|
+
}
|
|
1162
|
+
for (const decl of node.declarations ?? []) {
|
|
1163
|
+
const id = decl.id;
|
|
1164
|
+
if (!id || id.type !== "Identifier") continue;
|
|
1165
|
+
const name = String(id.name);
|
|
1166
|
+
if (seen.has(name)) {
|
|
1167
|
+
consts.delete(name);
|
|
1364
1168
|
continue;
|
|
1365
1169
|
}
|
|
1170
|
+
seen.add(name);
|
|
1171
|
+
const init = decl.init;
|
|
1172
|
+
if (!init) continue;
|
|
1173
|
+
const value = staticString(unwrapTsExpression(init));
|
|
1174
|
+
if (value !== null) consts.set(name, value);
|
|
1366
1175
|
}
|
|
1367
|
-
|
|
1368
|
-
}
|
|
1369
|
-
return
|
|
1176
|
+
return void 0;
|
|
1177
|
+
});
|
|
1178
|
+
return consts;
|
|
1370
1179
|
}
|
|
1371
|
-
function
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
while (i < N) {
|
|
1375
|
-
const c = content[i];
|
|
1376
|
-
if (c === "\\") {
|
|
1377
|
-
i += 2;
|
|
1378
|
-
continue;
|
|
1379
|
-
}
|
|
1380
|
-
if (c === quote) return i + 1;
|
|
1381
|
-
i++;
|
|
1180
|
+
function staticString(node) {
|
|
1181
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
1182
|
+
return node.value;
|
|
1382
1183
|
}
|
|
1383
|
-
|
|
1184
|
+
if (node.type === "TemplateLiteral") {
|
|
1185
|
+
const expressions = node.expressions ?? [];
|
|
1186
|
+
if (expressions.length > 0) return null;
|
|
1187
|
+
const quasis = node.quasis ?? [];
|
|
1188
|
+
return quasis[0]?.value?.cooked ?? "";
|
|
1189
|
+
}
|
|
1190
|
+
return null;
|
|
1384
1191
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1192
|
+
var UNRESOLVED = { resolved: false };
|
|
1193
|
+
function evalIdExpression(expr, consts) {
|
|
1194
|
+
const node = unwrapTsExpression(expr);
|
|
1195
|
+
const literal = staticString(node);
|
|
1196
|
+
if (literal !== null) {
|
|
1197
|
+
return literal.length > 0 ? { resolved: true, ids: [literal] } : UNRESOLVED;
|
|
1198
|
+
}
|
|
1199
|
+
if (node.type === "TemplateLiteral") {
|
|
1200
|
+
const quasis = node.quasis ?? [];
|
|
1201
|
+
const expressions = node.expressions ?? [];
|
|
1202
|
+
let out2 = "";
|
|
1203
|
+
for (let i = 0; i < quasis.length; i++) {
|
|
1204
|
+
out2 += quasis[i].value?.cooked ?? "";
|
|
1205
|
+
if (i < expressions.length) {
|
|
1206
|
+
const part = evalIdExpression(expressions[i], consts);
|
|
1207
|
+
out2 += part.resolved && part.ids.length === 1 ? part.ids[0] : "*";
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
out2 = out2.replace(/\*{2,}/g, "*");
|
|
1211
|
+
if (!out2.includes("*")) {
|
|
1212
|
+
return out2.length > 0 ? { resolved: true, ids: [out2] } : UNRESOLVED;
|
|
1213
|
+
}
|
|
1214
|
+
return out2.replace(/\*/g, "").length > 0 ? { resolved: true, ids: [out2] } : UNRESOLVED;
|
|
1215
|
+
}
|
|
1216
|
+
if (node.type === "Identifier") {
|
|
1217
|
+
const value = consts.get(String(node.name));
|
|
1218
|
+
return value !== void 0 && value.length > 0 ? { resolved: true, ids: [value] } : UNRESOLVED;
|
|
1219
|
+
}
|
|
1220
|
+
if (node.type === "ConditionalExpression") {
|
|
1221
|
+
const left = evalIdExpression(node.consequent, consts);
|
|
1222
|
+
const right = evalIdExpression(node.alternate, consts);
|
|
1223
|
+
if (!left.resolved || !right.resolved) return UNRESOLVED;
|
|
1224
|
+
return { resolved: true, ids: [.../* @__PURE__ */ new Set([...left.ids, ...right.ids])] };
|
|
1225
|
+
}
|
|
1226
|
+
return UNRESOLVED;
|
|
1227
|
+
}
|
|
1228
|
+
function collectElementAttrs(opening, consts, dynamicAttrs, lineAt) {
|
|
1229
|
+
const statics = [];
|
|
1230
|
+
const patterns = [];
|
|
1231
|
+
const attributes = opening.attributes ?? [];
|
|
1232
|
+
for (const attr of attributes) {
|
|
1233
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
1234
|
+
const kind = attrKind(attr);
|
|
1235
|
+
if (!kind) continue;
|
|
1236
|
+
const value = attr.value;
|
|
1237
|
+
if (!value) continue;
|
|
1238
|
+
let result = UNRESOLVED;
|
|
1239
|
+
let valueSpan;
|
|
1240
|
+
if (value.type === "Literal") {
|
|
1241
|
+
const v = staticString(value);
|
|
1242
|
+
result = v !== null && v.length > 0 ? { resolved: true, ids: [v] } : UNRESOLVED;
|
|
1243
|
+
if (result.resolved) valueSpan = { start: value.start, end: value.end };
|
|
1244
|
+
} else if (value.type === "JSXExpressionContainer") {
|
|
1245
|
+
const expr = value.expression;
|
|
1246
|
+
if (expr && expr.type !== "JSXEmptyExpression") {
|
|
1247
|
+
result = evalIdExpression(expr, consts);
|
|
1248
|
+
const inner = unwrapTsExpression(expr);
|
|
1249
|
+
if (result.resolved && staticString(inner) !== null) {
|
|
1250
|
+
valueSpan = { start: inner.start, end: inner.end };
|
|
1407
1251
|
}
|
|
1408
|
-
if (cj === "{") depth++;
|
|
1409
|
-
else if (cj === "}") depth--;
|
|
1410
|
-
i++;
|
|
1411
1252
|
}
|
|
1412
|
-
|
|
1253
|
+
if (!result.resolved) {
|
|
1254
|
+
dynamicAttrs.push({
|
|
1255
|
+
kind,
|
|
1256
|
+
attrName: kind === "element" ? "data-uidex" : `data-uidex-${kind}`,
|
|
1257
|
+
line: lineAt(attr.start)
|
|
1258
|
+
});
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (!result.resolved) continue;
|
|
1263
|
+
for (const id of result.ids) {
|
|
1264
|
+
const resolved = {
|
|
1265
|
+
kind,
|
|
1266
|
+
id,
|
|
1267
|
+
start: attr.start,
|
|
1268
|
+
isPattern: id.includes("*"),
|
|
1269
|
+
// Only a single plain string literal is renameable in place.
|
|
1270
|
+
...result.ids.length === 1 && valueSpan ? { span: valueSpan } : {}
|
|
1271
|
+
};
|
|
1272
|
+
if (resolved.isPattern) patterns.push(resolved);
|
|
1273
|
+
else statics.push(resolved);
|
|
1413
1274
|
}
|
|
1414
|
-
i++;
|
|
1415
1275
|
}
|
|
1416
|
-
return
|
|
1276
|
+
return [...statics, ...patterns];
|
|
1417
1277
|
}
|
|
1418
|
-
function
|
|
1419
|
-
const
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1278
|
+
function collectJSXFacts(parsed) {
|
|
1279
|
+
const occurrences = [];
|
|
1280
|
+
const dynamicAttrs = [];
|
|
1281
|
+
const unannotatedInteractive = [];
|
|
1282
|
+
const landmarks = [];
|
|
1283
|
+
if (parsed.program === null) {
|
|
1284
|
+
return { occurrences, dynamicAttrs, unannotatedInteractive, landmarks };
|
|
1285
|
+
}
|
|
1286
|
+
const consts = collectConstStrings(parsed.program);
|
|
1287
|
+
const ancestors = [];
|
|
1288
|
+
const visit = (node) => {
|
|
1289
|
+
if (!isNode3(node)) return;
|
|
1290
|
+
if (node.type === "JSXElement") {
|
|
1291
|
+
const opening = node.openingElement;
|
|
1292
|
+
const attrs = collectElementAttrs(
|
|
1293
|
+
opening,
|
|
1294
|
+
consts,
|
|
1295
|
+
dynamicAttrs,
|
|
1296
|
+
parsed.lineAt
|
|
1297
|
+
);
|
|
1298
|
+
const interactive = readInteractive(node, parsed.lineAt);
|
|
1299
|
+
if (interactive) unannotatedInteractive.push(interactive);
|
|
1300
|
+
if (attrs.length > 0) {
|
|
1301
|
+
const snapshot = ancestors.slice();
|
|
1302
|
+
for (const a of attrs) {
|
|
1303
|
+
occurrences.push({
|
|
1304
|
+
kind: a.kind,
|
|
1305
|
+
id: a.id,
|
|
1306
|
+
line: parsed.lineAt(a.start),
|
|
1307
|
+
ancestors: snapshot,
|
|
1308
|
+
...a.span ? { span: a.span } : {}
|
|
1309
|
+
});
|
|
1439
1310
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1311
|
+
}
|
|
1312
|
+
let pushed = attrs.length;
|
|
1313
|
+
for (const a of attrs) ancestors.push({ kind: a.kind, id: a.id });
|
|
1314
|
+
const landmark = readLandmark(opening, parsed.lineAt);
|
|
1315
|
+
if (landmark) {
|
|
1316
|
+
landmarks.push(landmark);
|
|
1317
|
+
if (!attrs.some((a) => a.kind === "region")) {
|
|
1318
|
+
ancestors.push({ kind: "region", id: landmark.tag });
|
|
1319
|
+
pushed++;
|
|
1443
1320
|
}
|
|
1444
|
-
if (cj === "{") depth++;
|
|
1445
|
-
else if (cj === "}") depth--;
|
|
1446
|
-
i++;
|
|
1447
1321
|
}
|
|
1448
|
-
|
|
1322
|
+
visitChildren(opening);
|
|
1323
|
+
for (const child of node.children ?? []) {
|
|
1324
|
+
visit(child);
|
|
1325
|
+
}
|
|
1326
|
+
const closing = node.closingElement;
|
|
1327
|
+
if (isNode3(closing)) visitChildren(closing);
|
|
1328
|
+
ancestors.length -= pushed;
|
|
1329
|
+
return;
|
|
1449
1330
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1331
|
+
visitChildren(node);
|
|
1332
|
+
};
|
|
1333
|
+
const visitChildren = (node) => {
|
|
1334
|
+
for (const key of Object.keys(node)) {
|
|
1335
|
+
if (key === "type" || key === "start" || key === "end") continue;
|
|
1336
|
+
const value = node[key];
|
|
1337
|
+
if (Array.isArray(value)) {
|
|
1338
|
+
for (const item of value) visit(item);
|
|
1339
|
+
} else {
|
|
1340
|
+
visit(value);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
visit(parsed.program);
|
|
1345
|
+
return { occurrences, dynamicAttrs, unannotatedInteractive, landmarks };
|
|
1454
1346
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
if (content[i] === "\n") line++;
|
|
1347
|
+
function readLandmark(opening, lineAt) {
|
|
1348
|
+
const name = opening.name;
|
|
1349
|
+
if (!name || name.type !== "JSXIdentifier") return null;
|
|
1350
|
+
const tag = String(name.name);
|
|
1351
|
+
if (LANDMARK_TAGS.has(tag)) {
|
|
1352
|
+
return { tag, line: lineAt(opening.start) };
|
|
1462
1353
|
}
|
|
1463
|
-
|
|
1354
|
+
for (const attr of opening.attributes ?? []) {
|
|
1355
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
1356
|
+
const attrName = attr.name;
|
|
1357
|
+
if (!attrName || String(attrName.name) !== "role") continue;
|
|
1358
|
+
const value = attr.value;
|
|
1359
|
+
if (value && value.type === "Literal" && value.value === "region") {
|
|
1360
|
+
return { tag: "region", line: lineAt(opening.start) };
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
return null;
|
|
1464
1364
|
}
|
|
1465
|
-
function
|
|
1466
|
-
const
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1471
|
-
let
|
|
1472
|
-
for (const
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
const uidex = line.match(
|
|
1476
|
-
/^@uidex\s+(page|feature|widget)\s+(\S+)(?:\s+-\s+(.+))?/
|
|
1477
|
-
);
|
|
1478
|
-
if (uidex) {
|
|
1479
|
-
kind = uidex[1];
|
|
1480
|
-
id = uidex[2];
|
|
1481
|
-
if (uidex[3]) desc.push(uidex[3].trim());
|
|
1365
|
+
function readInteractive(element, lineAt) {
|
|
1366
|
+
const opening = element.openingElement;
|
|
1367
|
+
const name = opening.name;
|
|
1368
|
+
if (!name || name.type !== "JSXIdentifier") return null;
|
|
1369
|
+
const tag = String(name.name);
|
|
1370
|
+
if (!INTERACTIVE_TAGS.has(tag)) return null;
|
|
1371
|
+
let hasSpread = false;
|
|
1372
|
+
for (const attr of opening.attributes ?? []) {
|
|
1373
|
+
if (attr.type === "JSXSpreadAttribute") {
|
|
1374
|
+
hasSpread = true;
|
|
1482
1375
|
continue;
|
|
1483
1376
|
}
|
|
1484
|
-
if (
|
|
1485
|
-
|
|
1486
|
-
|
|
1377
|
+
if (attr.type === "JSXAttribute" && attrKind(attr) !== null) return null;
|
|
1378
|
+
}
|
|
1379
|
+
const nameHint = interactiveNameHint(element, opening);
|
|
1380
|
+
return {
|
|
1381
|
+
tag,
|
|
1382
|
+
line: lineAt(opening.start),
|
|
1383
|
+
hasSpread,
|
|
1384
|
+
nameEnd: name.end,
|
|
1385
|
+
...nameHint ? { nameHint } : {}
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
function staticAttrValue(opening, attrName) {
|
|
1389
|
+
for (const attr of opening.attributes ?? []) {
|
|
1390
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
1391
|
+
const n = attr.name;
|
|
1392
|
+
if (!n || String(n.name) !== attrName) continue;
|
|
1393
|
+
const value = attr.value;
|
|
1394
|
+
if (!value) return null;
|
|
1395
|
+
if (value.type === "Literal") return staticString(value);
|
|
1396
|
+
if (value.type === "JSXExpressionContainer") {
|
|
1397
|
+
const expr = value.expression;
|
|
1398
|
+
return expr ? staticString(unwrapTsExpression(expr)) : null;
|
|
1487
1399
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1400
|
+
return null;
|
|
1401
|
+
}
|
|
1402
|
+
return null;
|
|
1403
|
+
}
|
|
1404
|
+
function staticChildText(element) {
|
|
1405
|
+
const parts = [];
|
|
1406
|
+
for (const child of element.children ?? []) {
|
|
1407
|
+
if (child.type === "JSXText") {
|
|
1408
|
+
parts.push(String(child.value ?? ""));
|
|
1492
1409
|
}
|
|
1493
|
-
if (line.startsWith("@")) continue;
|
|
1494
|
-
desc.push(line);
|
|
1495
1410
|
}
|
|
1411
|
+
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
1412
|
+
}
|
|
1413
|
+
function interactiveNameHint(element, opening) {
|
|
1414
|
+
const ariaLabel = staticAttrValue(opening, "aria-label");
|
|
1415
|
+
if (ariaLabel) return ariaLabel;
|
|
1416
|
+
const text = staticChildText(element);
|
|
1417
|
+
if (text) return text;
|
|
1418
|
+
for (const attr of ["title", "name", "placeholder"]) {
|
|
1419
|
+
const v = staticAttrValue(opening, attr);
|
|
1420
|
+
if (v) return v;
|
|
1421
|
+
}
|
|
1422
|
+
return void 0;
|
|
1423
|
+
}
|
|
1424
|
+
function isNode3(value) {
|
|
1425
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/scanner/scan/extract.ts
|
|
1429
|
+
function parseFailureDiagnostic(file, parsed) {
|
|
1430
|
+
const fatal = parsed.program === null || parsed.hasErrors && parsed.program.body.length === 0;
|
|
1431
|
+
if (!fatal) return null;
|
|
1496
1432
|
return {
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1433
|
+
code: "parse-error",
|
|
1434
|
+
severity: "warning",
|
|
1435
|
+
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",
|
|
1436
|
+
file: file.displayPath,
|
|
1437
|
+
line: 1,
|
|
1438
|
+
hint: "Fix the file's syntax (or exclude it from .uidex.json sources) so the scanner can read its annotations"
|
|
1502
1439
|
};
|
|
1503
1440
|
}
|
|
1504
1441
|
function extract(files) {
|
|
1505
1442
|
return files.map((file) => {
|
|
1506
|
-
const
|
|
1507
|
-
const
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
};
|
|
1443
|
+
const parsed = parseSource(file);
|
|
1444
|
+
const { exports: exports2, diagnostics } = extractUidexExports(file, parsed);
|
|
1445
|
+
const parseFailure = parseFailureDiagnostic(file, parsed);
|
|
1446
|
+
if (parseFailure) diagnostics.push(parseFailure);
|
|
1447
|
+
const out2 = { file, annotations: [] };
|
|
1448
|
+
out2.annotations = extractOne(file, parsed, out2);
|
|
1511
1449
|
if (exports2.length > 0) out2.metadata = exports2;
|
|
1512
1450
|
if (diagnostics.length > 0) out2.diagnostics = diagnostics;
|
|
1451
|
+
const flows = collectFlowFacts(parsed, file.content);
|
|
1452
|
+
if (flows.length > 0) out2.flows = flows;
|
|
1453
|
+
const imports = collectImportFacts(parsed);
|
|
1454
|
+
if (imports.length > 0) out2.imports = imports;
|
|
1513
1455
|
return out2;
|
|
1514
1456
|
});
|
|
1515
1457
|
}
|
|
1516
|
-
function extractOne(file) {
|
|
1458
|
+
function extractOne(file, parsed, out2) {
|
|
1517
1459
|
const annotations = [];
|
|
1518
|
-
const {
|
|
1519
|
-
|
|
1460
|
+
const { displayPath } = file;
|
|
1461
|
+
const jsx = collectJSXFacts(parsed);
|
|
1462
|
+
if (jsx.dynamicAttrs.length > 0) out2.dynamicAttrs = jsx.dynamicAttrs;
|
|
1463
|
+
if (jsx.unannotatedInteractive.length > 0) {
|
|
1464
|
+
out2.unannotatedInteractive = jsx.unannotatedInteractive;
|
|
1465
|
+
}
|
|
1466
|
+
if (jsx.landmarks.length > 0) out2.landmarks = jsx.landmarks;
|
|
1467
|
+
for (const occ of jsx.occurrences) {
|
|
1520
1468
|
annotations.push({
|
|
1521
1469
|
kind: occ.kind,
|
|
1522
1470
|
id: occ.id,
|
|
1523
1471
|
file: displayPath,
|
|
1524
1472
|
line: occ.line,
|
|
1525
|
-
...occ.ancestors.length > 0 ? { ancestors: occ.ancestors } : {}
|
|
1473
|
+
...occ.ancestors.length > 0 ? { ancestors: occ.ancestors } : {},
|
|
1474
|
+
...occ.span ? { span: occ.span } : {}
|
|
1526
1475
|
});
|
|
1527
1476
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
}
|
|
1546
|
-
} else if (
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
line,
|
|
1552
|
-
acceptance: parsed.acceptance
|
|
1553
|
-
});
|
|
1477
|
+
return annotations;
|
|
1478
|
+
}
|
|
1479
|
+
function collectImportFacts(parsed) {
|
|
1480
|
+
if (parsed.program === null) return [];
|
|
1481
|
+
const out2 = [];
|
|
1482
|
+
for (const stmt of parsed.program.body) {
|
|
1483
|
+
let source;
|
|
1484
|
+
let isTypeOnly = false;
|
|
1485
|
+
const names = [];
|
|
1486
|
+
if (stmt.type === "ImportDeclaration") {
|
|
1487
|
+
source = stmt.source;
|
|
1488
|
+
isTypeOnly = stmt.importKind === "type";
|
|
1489
|
+
for (const spec of stmt.specifiers ?? []) {
|
|
1490
|
+
const local = spec.local;
|
|
1491
|
+
if (local && local.type === "Identifier") {
|
|
1492
|
+
names.push(String(local.name));
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
} else if ((stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportAllDeclaration") && stmt.source) {
|
|
1496
|
+
source = stmt.source;
|
|
1497
|
+
isTypeOnly = stmt.exportKind === "type";
|
|
1498
|
+
} else {
|
|
1499
|
+
continue;
|
|
1554
1500
|
}
|
|
1501
|
+
if (!source || source.type !== "Literal") continue;
|
|
1502
|
+
if (typeof source.value !== "string") continue;
|
|
1503
|
+
out2.push({
|
|
1504
|
+
specifier: source.value,
|
|
1505
|
+
line: parsed.lineAt(stmt.start),
|
|
1506
|
+
span: { start: stmt.start, end: stmt.end },
|
|
1507
|
+
isTypeOnly,
|
|
1508
|
+
names
|
|
1509
|
+
});
|
|
1555
1510
|
}
|
|
1556
|
-
return
|
|
1511
|
+
return out2;
|
|
1557
1512
|
}
|
|
1558
1513
|
|
|
1559
1514
|
// src/scanner/scan/resolve.ts
|
|
1560
|
-
var
|
|
1515
|
+
var path4 = __toESM(require("path"), 1);
|
|
1561
1516
|
|
|
1562
1517
|
// src/shared/entities/types.ts
|
|
1563
1518
|
var ENTITY_KINDS = [
|
|
@@ -1623,6 +1578,7 @@ function freezeEntity(entity, flows) {
|
|
|
1623
1578
|
function createRegistry() {
|
|
1624
1579
|
const store = emptyStore();
|
|
1625
1580
|
let flowsCache = null;
|
|
1581
|
+
const patternCache = /* @__PURE__ */ new Map();
|
|
1626
1582
|
const getFlows = () => {
|
|
1627
1583
|
if (flowsCache === null) flowsCache = Array.from(store.flow.values());
|
|
1628
1584
|
return flowsCache;
|
|
@@ -1632,6 +1588,7 @@ function createRegistry() {
|
|
|
1632
1588
|
const key = entityKey(entity);
|
|
1633
1589
|
store[entity.kind].set(key, entity);
|
|
1634
1590
|
flowsCache = null;
|
|
1591
|
+
patternCache.delete(entity.kind);
|
|
1635
1592
|
};
|
|
1636
1593
|
const get = (kind, id) => {
|
|
1637
1594
|
assertEntityKind(kind);
|
|
@@ -1639,6 +1596,51 @@ function createRegistry() {
|
|
|
1639
1596
|
if (raw === void 0) return void 0;
|
|
1640
1597
|
return freezeEntity(raw, getFlows());
|
|
1641
1598
|
};
|
|
1599
|
+
const getPatternsForKind = (kind) => {
|
|
1600
|
+
const cached = patternCache.get(kind);
|
|
1601
|
+
if (cached !== void 0) return cached;
|
|
1602
|
+
const patterns = [];
|
|
1603
|
+
for (const [key, entity] of store[kind]) {
|
|
1604
|
+
if (key.includes("*")) {
|
|
1605
|
+
const segments = key.split("*");
|
|
1606
|
+
patterns.push({
|
|
1607
|
+
segments,
|
|
1608
|
+
staticLength: segments.reduce((n, s) => n + s.length, 0),
|
|
1609
|
+
entity
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
patternCache.set(
|
|
1614
|
+
kind,
|
|
1615
|
+
patterns
|
|
1616
|
+
);
|
|
1617
|
+
return patterns;
|
|
1618
|
+
};
|
|
1619
|
+
const matchesSegments = (segments, id) => {
|
|
1620
|
+
const first = segments[0];
|
|
1621
|
+
const last = segments[segments.length - 1];
|
|
1622
|
+
if (!id.startsWith(first)) return false;
|
|
1623
|
+
let pos = first.length;
|
|
1624
|
+
for (let i = 1; i < segments.length - 1; i++) {
|
|
1625
|
+
const idx = id.indexOf(segments[i], pos);
|
|
1626
|
+
if (idx === -1) return false;
|
|
1627
|
+
pos = idx + segments[i].length;
|
|
1628
|
+
}
|
|
1629
|
+
return id.endsWith(last) && id.length - last.length >= pos;
|
|
1630
|
+
};
|
|
1631
|
+
const matchPattern = (kind, id) => {
|
|
1632
|
+
assertEntityKind(kind);
|
|
1633
|
+
const patterns = getPatternsForKind(kind);
|
|
1634
|
+
if (patterns.length === 0) return void 0;
|
|
1635
|
+
let best;
|
|
1636
|
+
for (const entry of patterns) {
|
|
1637
|
+
if (matchesSegments(entry.segments, id) && (best === void 0 || entry.staticLength > best.staticLength)) {
|
|
1638
|
+
best = entry;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
if (best === void 0) return void 0;
|
|
1642
|
+
return freezeEntity(best.entity, getFlows());
|
|
1643
|
+
};
|
|
1642
1644
|
const list = (kind) => {
|
|
1643
1645
|
assertEntityKind(kind);
|
|
1644
1646
|
const flows = getFlows();
|
|
@@ -1689,6 +1691,7 @@ function createRegistry() {
|
|
|
1689
1691
|
return {
|
|
1690
1692
|
add,
|
|
1691
1693
|
get,
|
|
1694
|
+
matchPattern,
|
|
1692
1695
|
list,
|
|
1693
1696
|
query,
|
|
1694
1697
|
byScope,
|
|
@@ -1785,21 +1788,9 @@ function resolveConventions(c) {
|
|
|
1785
1788
|
function kebab(str) {
|
|
1786
1789
|
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").replace(/[^a-zA-Z0-9-]/g, "").toLowerCase();
|
|
1787
1790
|
}
|
|
1788
|
-
function baseName(file) {
|
|
1789
|
-
const b =
|
|
1790
|
-
return b.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "");
|
|
1791
|
-
}
|
|
1792
|
-
var LANDMARK_RE = /<(header|nav|main|aside|footer)(\s[^>]*)?>|role=["']region["']/gi;
|
|
1793
|
-
function extractLandmarks(file) {
|
|
1794
|
-
const out2 = [];
|
|
1795
|
-
LANDMARK_RE.lastIndex = 0;
|
|
1796
|
-
let m;
|
|
1797
|
-
while ((m = LANDMARK_RE.exec(file.content)) !== null) {
|
|
1798
|
-
const tag = m[1] ?? "region";
|
|
1799
|
-
const line = 1 + file.content.slice(0, m.index).split("\n").length - 1;
|
|
1800
|
-
out2.push({ tag, line });
|
|
1801
|
-
}
|
|
1802
|
-
return out2;
|
|
1791
|
+
function baseName(file) {
|
|
1792
|
+
const b = path4.posix.basename(file);
|
|
1793
|
+
return b.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "");
|
|
1803
1794
|
}
|
|
1804
1795
|
function fileMatchesAny(displayPath, patterns) {
|
|
1805
1796
|
return patterns.some((g) => globToRegExp(g).test(displayPath));
|
|
@@ -1861,7 +1852,7 @@ function resolve2(ctx) {
|
|
|
1861
1852
|
const routes = conventions.pages === "auto" ? detectRoutes(ctx.extracted.map((e) => e.file)) : [];
|
|
1862
1853
|
const handledPageFiles = /* @__PURE__ */ new Set();
|
|
1863
1854
|
for (const route of routes) {
|
|
1864
|
-
const routeDir =
|
|
1855
|
+
const routeDir = path4.posix.dirname(route.file);
|
|
1865
1856
|
const wellKnownPath = `${routeDir}/${WELL_KNOWN_FILES.page}`;
|
|
1866
1857
|
const wellKnownExp = exportFor(wellKnownPath, "page");
|
|
1867
1858
|
const routeExp = exportFor(route.file, "page");
|
|
@@ -1915,7 +1906,7 @@ function resolve2(ctx) {
|
|
|
1915
1906
|
const dir = extractFeatureDir(ef.file.displayPath, featureGlob);
|
|
1916
1907
|
if (!dir) continue;
|
|
1917
1908
|
conventionalFeatureDirs.add(dir);
|
|
1918
|
-
const isWellKnown =
|
|
1909
|
+
const isWellKnown = path4.posix.basename(ef.file.displayPath) === WELL_KNOWN_FILES.feature;
|
|
1919
1910
|
if (isWellKnown) wellKnownFeatureFileByDir.set(dir, ef.file.displayPath);
|
|
1920
1911
|
const exp = exportFor(ef.file.displayPath, "feature");
|
|
1921
1912
|
if (exp) {
|
|
@@ -1952,7 +1943,7 @@ function resolve2(ctx) {
|
|
|
1952
1943
|
} else if (allExports.length > 0) {
|
|
1953
1944
|
exp = allExports[0].exp;
|
|
1954
1945
|
}
|
|
1955
|
-
const id = exp && typeof exp.id === "string" ? exp.id :
|
|
1946
|
+
const id = exp && typeof exp.id === "string" ? exp.id : path4.posix.basename(dir);
|
|
1956
1947
|
const meta = exp ? buildMetaFromExport(exp) : void 0;
|
|
1957
1948
|
const feature = {
|
|
1958
1949
|
kind: "feature",
|
|
@@ -2048,8 +2039,8 @@ function resolve2(ctx) {
|
|
|
2048
2039
|
}
|
|
2049
2040
|
if (conventions.regions === "landmarks") {
|
|
2050
2041
|
for (const ef of ctx.extracted) {
|
|
2051
|
-
for (const lm of
|
|
2052
|
-
const id =
|
|
2042
|
+
for (const lm of ef.landmarks ?? []) {
|
|
2043
|
+
const id = lm.tag;
|
|
2053
2044
|
if (!registry.get("region", id)) {
|
|
2054
2045
|
const meta = metaWithComposes("region", id);
|
|
2055
2046
|
const region = {
|
|
@@ -2160,7 +2151,7 @@ function resolve2(ctx) {
|
|
|
2160
2151
|
const flowExport = (ff.metadata ?? []).find(
|
|
2161
2152
|
(m) => m.kind === "flow" && typeof m.id === "string"
|
|
2162
2153
|
);
|
|
2163
|
-
const derived =
|
|
2154
|
+
const derived = flowsFromFacts(ff);
|
|
2164
2155
|
if (flowExport && typeof flowExport.id === "string" && derived.length === 1) {
|
|
2165
2156
|
const base = derived[0];
|
|
2166
2157
|
const flow = {
|
|
@@ -2215,60 +2206,21 @@ function computeScope(displayPath) {
|
|
|
2215
2206
|
}
|
|
2216
2207
|
return null;
|
|
2217
2208
|
}
|
|
2218
|
-
function
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
const line = 1 + source.slice(0, m.index).split("\n").length - 1;
|
|
2227
|
-
const after = source.slice(m.index + m[0].length);
|
|
2228
|
-
const arrow = after.match(/=>\s*\{/);
|
|
2229
|
-
if (!arrow || arrow.index === void 0) continue;
|
|
2230
|
-
const bodyStart = m.index + m[0].length + arrow.index + arrow[0].length;
|
|
2231
|
-
let depth = 1;
|
|
2232
|
-
let bodyEnd = -1;
|
|
2233
|
-
for (let i = bodyStart; i < source.length; i++) {
|
|
2234
|
-
if (source[i] === "{") depth++;
|
|
2235
|
-
else if (source[i] === "}") {
|
|
2236
|
-
depth--;
|
|
2237
|
-
if (depth === 0) {
|
|
2238
|
-
bodyEnd = i;
|
|
2239
|
-
break;
|
|
2240
|
-
}
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2243
|
-
if (bodyEnd === -1) continue;
|
|
2244
|
-
const body = source.slice(bodyStart, bodyEnd);
|
|
2245
|
-
const touches = captureUidexIds(body);
|
|
2246
|
-
flows.push({
|
|
2247
|
-
kind: "flow",
|
|
2248
|
-
id,
|
|
2249
|
-
loc: { file: file.displayPath, line },
|
|
2250
|
-
touches: dedupe(touches.map((t) => t.id)),
|
|
2251
|
-
steps: touches.filter((t) => t.action).map((t) => ({ entityId: t.id, action: t.action }))
|
|
2252
|
-
});
|
|
2253
|
-
}
|
|
2254
|
-
return flows;
|
|
2255
|
-
}
|
|
2256
|
-
function captureUidexIds(body) {
|
|
2257
|
-
const out2 = [];
|
|
2258
|
-
const re = /uidex\(\s*(?:'([^']+)'|"([^"]+)"|`([^`$]+)`)\s*\)(?:\.(\w+)\s*\()?/g;
|
|
2259
|
-
let m;
|
|
2260
|
-
while ((m = re.exec(body)) !== null) {
|
|
2261
|
-
out2.push({ id: m[1] || m[2] || m[3], action: m[4] });
|
|
2262
|
-
}
|
|
2263
|
-
return out2;
|
|
2209
|
+
function flowsFromFacts(ff) {
|
|
2210
|
+
return (ff.flows ?? []).map((fact) => ({
|
|
2211
|
+
kind: "flow",
|
|
2212
|
+
id: kebab(fact.title),
|
|
2213
|
+
loc: { file: ff.file.displayPath, line: fact.line },
|
|
2214
|
+
touches: dedupe(fact.calls.map((c) => c.id)),
|
|
2215
|
+
steps: fact.calls.filter((c) => c.action).map((c) => ({ entityId: c.id, action: c.action }))
|
|
2216
|
+
}));
|
|
2264
2217
|
}
|
|
2265
2218
|
function dedupe(arr) {
|
|
2266
2219
|
return Array.from(new Set(arr));
|
|
2267
2220
|
}
|
|
2268
2221
|
|
|
2269
2222
|
// src/scanner/scan/audit.ts
|
|
2270
|
-
var
|
|
2271
|
-
var MARKER_FILENAMES = ["UIDEX_PAGE.md", "UIDEX_FEATURE.md"];
|
|
2223
|
+
var path5 = __toESM(require("path"), 1);
|
|
2272
2224
|
function audit(opts) {
|
|
2273
2225
|
const diagnostics = [];
|
|
2274
2226
|
const { registry, extracted, files, config } = opts;
|
|
@@ -2278,22 +2230,15 @@ function audit(opts) {
|
|
|
2278
2230
|
const scopeLeakEnabled = config.audit?.scopeLeak ?? true;
|
|
2279
2231
|
const coverageEnabled = config.audit?.coverage ?? true;
|
|
2280
2232
|
if (opts.resolveDiagnostics) diagnostics.push(...opts.resolveDiagnostics);
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
code: "marker-md-ignored",
|
|
2287
|
-
severity: "warning",
|
|
2288
|
-
message: `Marker file "${base}" is ignored in v2; migrate to \`export const uidex\``,
|
|
2289
|
-
file: f.displayPath
|
|
2290
|
-
});
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
2233
|
+
for (const ef of extracted) {
|
|
2234
|
+
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
2235
|
+
}
|
|
2236
|
+
for (const ef of opts.flowExtracted ?? []) {
|
|
2237
|
+
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
2293
2238
|
}
|
|
2294
2239
|
if (check && opts.generated !== void 0) {
|
|
2295
2240
|
const outRel = opts.outputPath ?? config.output;
|
|
2296
|
-
const fresh =
|
|
2241
|
+
const fresh = normalizeForCheck(opts.generated);
|
|
2297
2242
|
if (opts.existingOnDisk === null || opts.existingOnDisk === void 0) {
|
|
2298
2243
|
diagnostics.push({
|
|
2299
2244
|
code: "gen-missing",
|
|
@@ -2303,7 +2248,7 @@ function audit(opts) {
|
|
|
2303
2248
|
hint: "Run `uidex scan` (without --check) to regenerate"
|
|
2304
2249
|
});
|
|
2305
2250
|
} else {
|
|
2306
|
-
const existing =
|
|
2251
|
+
const existing = normalizeForCheck(opts.existingOnDisk);
|
|
2307
2252
|
if (existing !== fresh) {
|
|
2308
2253
|
const changed = diffEntities(existing, opts.generated, registry);
|
|
2309
2254
|
const summary2 = formatChangedSummary(changed);
|
|
@@ -2317,22 +2262,6 @@ function audit(opts) {
|
|
|
2317
2262
|
}
|
|
2318
2263
|
}
|
|
2319
2264
|
}
|
|
2320
|
-
if (lint) {
|
|
2321
|
-
for (const ef of extracted) {
|
|
2322
|
-
for (const a of ef.annotations) {
|
|
2323
|
-
const migration = legacyJsdocMigration(a);
|
|
2324
|
-
if (!migration) continue;
|
|
2325
|
-
diagnostics.push({
|
|
2326
|
-
code: "legacy-jsdoc",
|
|
2327
|
-
severity: "warning",
|
|
2328
|
-
message: migration.message,
|
|
2329
|
-
file: a.file,
|
|
2330
|
-
line: a.line,
|
|
2331
|
-
hint: migration.hint
|
|
2332
|
-
});
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
2265
|
if (lint && acceptanceEnabled) {
|
|
2337
2266
|
for (const kind of ["widget", "feature", "page"]) {
|
|
2338
2267
|
for (const e of registry.list(kind)) {
|
|
@@ -2378,8 +2307,8 @@ function audit(opts) {
|
|
|
2378
2307
|
if (typeof m.id !== "string") continue;
|
|
2379
2308
|
const filePath = ef.file.displayPath;
|
|
2380
2309
|
const wellKnownName = WELL_KNOWN_FILES[m.kind];
|
|
2381
|
-
if (
|
|
2382
|
-
const dir =
|
|
2310
|
+
if (path5.posix.basename(filePath) === wellKnownName) continue;
|
|
2311
|
+
const dir = path5.posix.dirname(filePath);
|
|
2383
2312
|
const wellKnownPath = dir === "." ? wellKnownName : `${dir}/${wellKnownName}`;
|
|
2384
2313
|
if (scannedPaths.has(wellKnownPath)) continue;
|
|
2385
2314
|
const kindLabel = m.kind === "page" ? "Page" : "Feature";
|
|
@@ -2396,45 +2325,55 @@ function audit(opts) {
|
|
|
2396
2325
|
}
|
|
2397
2326
|
}
|
|
2398
2327
|
if (lint) {
|
|
2399
|
-
const
|
|
2400
|
-
|
|
2401
|
-
let m;
|
|
2402
|
-
dynamicAttrRe.lastIndex = 0;
|
|
2403
|
-
while ((m = dynamicAttrRe.exec(f.content)) !== null) {
|
|
2404
|
-
const kind = m[1] ?? "element";
|
|
2405
|
-
let line = 1;
|
|
2406
|
-
for (let i = 0; i < m.index; i++) if (f.content[i] === "\n") line++;
|
|
2407
|
-
const attrName = m[1] ? `data-uidex-${m[1]}` : "data-uidex";
|
|
2328
|
+
for (const ef of extracted) {
|
|
2329
|
+
for (const fact of ef.dynamicAttrs ?? []) {
|
|
2408
2330
|
diagnostics.push({
|
|
2409
2331
|
code: "dynamic-attr",
|
|
2410
2332
|
severity: "warning",
|
|
2411
|
-
message: `\`${attrName}={\u2026}\` uses a dynamic expression; the scanner cannot resolve the ${kind} id statically`,
|
|
2412
|
-
file:
|
|
2413
|
-
line,
|
|
2414
|
-
hint: dynamicAttrHint(kind)
|
|
2333
|
+
message: `\`${fact.attrName}={\u2026}\` uses a dynamic expression; the scanner cannot resolve the ${fact.kind} id statically`,
|
|
2334
|
+
file: ef.file.displayPath,
|
|
2335
|
+
line: fact.line,
|
|
2336
|
+
hint: dynamicAttrHint(fact.kind)
|
|
2415
2337
|
});
|
|
2416
2338
|
}
|
|
2417
2339
|
}
|
|
2418
2340
|
}
|
|
2419
2341
|
if (lint) {
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2342
|
+
const usedElementIds = new Set(registry.list("element").map((e) => e.id));
|
|
2343
|
+
for (const ef of extracted) {
|
|
2344
|
+
for (const fact of ef.unannotatedInteractive ?? []) {
|
|
2345
|
+
if (fact.hasSpread) {
|
|
2346
|
+
diagnostics.push({
|
|
2347
|
+
code: "spread-attr",
|
|
2348
|
+
severity: "info",
|
|
2349
|
+
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`,
|
|
2350
|
+
file: ef.file.displayPath,
|
|
2351
|
+
line: fact.line,
|
|
2352
|
+
hint: "Prefer a string-literal data-uidex on the element itself, or annotate at the call site."
|
|
2353
|
+
});
|
|
2354
|
+
} else {
|
|
2355
|
+
const id = uniqueElementId(fact, usedElementIds);
|
|
2356
|
+
usedElementIds.add(id);
|
|
2357
|
+
diagnostics.push({
|
|
2358
|
+
code: "missing-element-annotation",
|
|
2359
|
+
severity: "info",
|
|
2360
|
+
message: `Interactive <${fact.tag}> without data-uidex annotation`,
|
|
2361
|
+
file: ef.file.displayPath,
|
|
2362
|
+
line: fact.line,
|
|
2363
|
+
hint: `Add \`data-uidex="${id}"\` (or run \`uidex scan --fix\`).`,
|
|
2364
|
+
fix: {
|
|
2365
|
+
description: `Add data-uidex="${id}" to <${fact.tag}>`,
|
|
2366
|
+
edits: [
|
|
2367
|
+
{
|
|
2368
|
+
path: ef.file.sourcePath,
|
|
2369
|
+
start: fact.nameEnd,
|
|
2370
|
+
end: fact.nameEnd,
|
|
2371
|
+
replacement: ` data-uidex="${id}"`
|
|
2372
|
+
}
|
|
2373
|
+
]
|
|
2374
|
+
}
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2438
2377
|
}
|
|
2439
2378
|
}
|
|
2440
2379
|
}
|
|
@@ -2451,12 +2390,11 @@ function audit(opts) {
|
|
|
2451
2390
|
}
|
|
2452
2391
|
}
|
|
2453
2392
|
}
|
|
2454
|
-
for (const
|
|
2455
|
-
const
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
const
|
|
2459
|
-
const baseName2 = spec.split("/").pop() ?? "";
|
|
2393
|
+
for (const ef of extracted) {
|
|
2394
|
+
const displayPath = ef.file.displayPath;
|
|
2395
|
+
for (const imp of ef.imports ?? []) {
|
|
2396
|
+
if (imp.isTypeOnly) continue;
|
|
2397
|
+
const baseName2 = imp.specifier.split("/").pop() ?? "";
|
|
2460
2398
|
const primitive = byName.get(
|
|
2461
2399
|
baseName2.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
|
|
2462
2400
|
);
|
|
@@ -2464,81 +2402,170 @@ function audit(opts) {
|
|
|
2464
2402
|
const scope = primitive.scopes?.[0];
|
|
2465
2403
|
if (!scope) continue;
|
|
2466
2404
|
const [kind, id] = scope.split(":");
|
|
2467
|
-
const importerSegments =
|
|
2405
|
+
const importerSegments = displayPath.split("/");
|
|
2468
2406
|
if (importerSegments.includes(id) && importerSegments.includes(kind + "s")) {
|
|
2469
2407
|
continue;
|
|
2470
2408
|
}
|
|
2471
2409
|
if (kind === "feature" && importerSegments.includes(id)) continue;
|
|
2472
|
-
if (kind === "feature" && declaredFeatures.get(
|
|
2410
|
+
if (kind === "feature" && declaredFeatures.get(displayPath)?.has(id)) {
|
|
2473
2411
|
continue;
|
|
2474
2412
|
}
|
|
2475
2413
|
diagnostics.push({
|
|
2476
2414
|
code: "scope-leak",
|
|
2477
2415
|
severity: "warning",
|
|
2478
|
-
message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${
|
|
2479
|
-
file:
|
|
2416
|
+
message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${displayPath}`,
|
|
2417
|
+
file: displayPath,
|
|
2418
|
+
line: imp.line
|
|
2480
2419
|
});
|
|
2481
2420
|
}
|
|
2482
2421
|
}
|
|
2483
2422
|
}
|
|
2484
2423
|
if (lint && coverageEnabled) {
|
|
2424
|
+
const factsByLoc = /* @__PURE__ */ new Map();
|
|
2425
|
+
for (const ef of opts.flowExtracted ?? []) {
|
|
2426
|
+
for (const fact of ef.flows ?? []) {
|
|
2427
|
+
const lines = /* @__PURE__ */ new Map();
|
|
2428
|
+
for (const call of fact.calls) {
|
|
2429
|
+
if (!lines.has(call.id)) lines.set(call.id, call.line);
|
|
2430
|
+
}
|
|
2431
|
+
factsByLoc.set(`${ef.file.displayPath}:${fact.line}`, lines);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2485
2434
|
for (const flow of registry.list("flow")) {
|
|
2435
|
+
const callLines = factsByLoc.get(`${flow.loc.file}:${flow.loc.line}`);
|
|
2486
2436
|
for (const touchedId of flow.touches) {
|
|
2487
|
-
const found = registry.get("element", touchedId) ?? registry.get("widget", touchedId) ?? registry.get("region", touchedId);
|
|
2437
|
+
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);
|
|
2488
2438
|
if (!found) {
|
|
2489
2439
|
diagnostics.push({
|
|
2490
2440
|
code: "unknown-reference",
|
|
2491
2441
|
severity: "warning",
|
|
2492
2442
|
message: `Flow "${flow.id}" references unknown entity "${touchedId}"`,
|
|
2493
2443
|
file: flow.loc.file,
|
|
2494
|
-
|
|
2444
|
+
// Point at the uidex() call itself when the spec facts are
|
|
2445
|
+
// available; the describe line is the fallback.
|
|
2446
|
+
line: callLines?.get(touchedId) ?? flow.loc.line
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
if (lint) {
|
|
2453
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
2454
|
+
for (const ef of extracted) {
|
|
2455
|
+
for (const a of ef.annotations) {
|
|
2456
|
+
if (a.kind !== "element" && a.kind !== "region" && a.kind !== "widget" && a.kind !== "primitive") {
|
|
2457
|
+
continue;
|
|
2458
|
+
}
|
|
2459
|
+
const key = `${a.kind}:${a.id}`;
|
|
2460
|
+
let list = occurrences.get(key);
|
|
2461
|
+
if (!list) {
|
|
2462
|
+
list = [];
|
|
2463
|
+
occurrences.set(key, list);
|
|
2464
|
+
}
|
|
2465
|
+
list.push({ file: a.file, line: a.line });
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
for (const [key, list] of occurrences) {
|
|
2469
|
+
const filesSeen = new Set(list.map((o) => o.file));
|
|
2470
|
+
if (filesSeen.size < 2) continue;
|
|
2471
|
+
const [kind, id] = key.split(/:(.*)/s);
|
|
2472
|
+
const others = list.slice(1).map((o) => `${o.file}:${o.line}`).join(", ");
|
|
2473
|
+
diagnostics.push({
|
|
2474
|
+
code: "duplicate-id",
|
|
2475
|
+
severity: kind === "widget" || kind === "primitive" ? "warning" : "info",
|
|
2476
|
+
message: `${kind} id "${id}" is declared in ${filesSeen.size} files (also at ${others}); the registry keeps only one entry`,
|
|
2477
|
+
file: list[0].file,
|
|
2478
|
+
line: list[0].line,
|
|
2479
|
+
entity: { kind, id },
|
|
2480
|
+
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."
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
if (lint && coverageEnabled) {
|
|
2485
|
+
for (const ef of opts.flowExtracted ?? []) {
|
|
2486
|
+
for (const fact of ef.flows ?? []) {
|
|
2487
|
+
for (const dyn of fact.dynamicCalls ?? []) {
|
|
2488
|
+
diagnostics.push({
|
|
2489
|
+
code: "dynamic-flow-reference",
|
|
2490
|
+
severity: "warning",
|
|
2491
|
+
message: `\`uidex(\u2026)\` call in flow "${fact.title}" uses a dynamic expression; the id is invisible to coverage and registry validation`,
|
|
2492
|
+
file: ef.file.displayPath,
|
|
2493
|
+
line: dyn.line,
|
|
2494
|
+
hint: "Use a string-literal id (component ids inside uidex() must be statically analysable)."
|
|
2495
2495
|
});
|
|
2496
2496
|
}
|
|
2497
2497
|
}
|
|
2498
2498
|
}
|
|
2499
2499
|
}
|
|
2500
|
+
if (lint && coverageEnabled) {
|
|
2501
|
+
for (const ef of extracted) {
|
|
2502
|
+
if (!ef.metadata) continue;
|
|
2503
|
+
for (const m of ef.metadata) {
|
|
2504
|
+
const check2 = (refKind, ids, spans) => {
|
|
2505
|
+
for (let i = 0; i < (ids?.length ?? 0); i++) {
|
|
2506
|
+
const refId = ids[i];
|
|
2507
|
+
const found = registry.get(refKind, refId) ?? registry.matchPattern(refKind, refId);
|
|
2508
|
+
if (found) continue;
|
|
2509
|
+
diagnostics.push({
|
|
2510
|
+
code: "unknown-reference",
|
|
2511
|
+
severity: "warning",
|
|
2512
|
+
message: `\`export const uidex\` in ${ef.file.displayPath} references unknown ${refKind} "${refId}"`,
|
|
2513
|
+
file: ef.file.displayPath,
|
|
2514
|
+
line: spans?.[i] ? lineOfOffset(ef.file.content, spans[i].start) : m.loc.line,
|
|
2515
|
+
hint: `No ${refKind} with id "${refId}" exists in the registry; fix the reference or add the ${refKind}.`
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2518
|
+
};
|
|
2519
|
+
check2("feature", m.features, m.featureSpans);
|
|
2520
|
+
check2("widget", m.widgets, m.widgetSpans);
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2500
2524
|
const summary = {
|
|
2501
2525
|
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
2502
2526
|
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
2503
2527
|
};
|
|
2504
2528
|
return { diagnostics, summary };
|
|
2505
2529
|
}
|
|
2506
|
-
function
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
};
|
|
2535
|
-
default:
|
|
2536
|
-
return null;
|
|
2530
|
+
function lineOfOffset(content, offset) {
|
|
2531
|
+
let line = 1;
|
|
2532
|
+
for (let i = 0; i < offset && i < content.length; i++) {
|
|
2533
|
+
if (content[i] === "\n") line++;
|
|
2534
|
+
}
|
|
2535
|
+
return line;
|
|
2536
|
+
}
|
|
2537
|
+
var TAG_FALLBACK_ID = {
|
|
2538
|
+
a: "link",
|
|
2539
|
+
button: "button",
|
|
2540
|
+
input: "input",
|
|
2541
|
+
select: "select",
|
|
2542
|
+
textarea: "textarea"
|
|
2543
|
+
};
|
|
2544
|
+
function kebabId(str) {
|
|
2545
|
+
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
2546
|
+
}
|
|
2547
|
+
function deriveElementId(fact) {
|
|
2548
|
+
const fromHint = fact.nameHint ? kebabId(fact.nameHint) : "";
|
|
2549
|
+
const capped = fromHint.split("-").filter(Boolean).slice(0, 5).join("-");
|
|
2550
|
+
return capped || TAG_FALLBACK_ID[fact.tag] || fact.tag;
|
|
2551
|
+
}
|
|
2552
|
+
function uniqueElementId(fact, used) {
|
|
2553
|
+
const base = deriveElementId(fact);
|
|
2554
|
+
if (!used.has(base)) return base;
|
|
2555
|
+
for (let n = 2; ; n++) {
|
|
2556
|
+
const candidate = `${base}-${n}`;
|
|
2557
|
+
if (!used.has(candidate)) return candidate;
|
|
2537
2558
|
}
|
|
2538
2559
|
}
|
|
2539
2560
|
function normalizeLineEndings(s) {
|
|
2540
2561
|
return s.replace(/\r\n/g, "\n");
|
|
2541
2562
|
}
|
|
2563
|
+
function normalizeForCheck(s) {
|
|
2564
|
+
return normalizeLineEndings(s).replace(
|
|
2565
|
+
/export const gitContext = \{[\s\S]*?\} as const/,
|
|
2566
|
+
"export const gitContext = {} as const"
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2542
2569
|
function formatChangedSummary(change) {
|
|
2543
2570
|
const parts = [];
|
|
2544
2571
|
const fmt = (kind, names) => {
|
|
@@ -2637,62 +2664,11 @@ function extractEntitiesArray(source) {
|
|
|
2637
2664
|
}
|
|
2638
2665
|
return null;
|
|
2639
2666
|
}
|
|
2640
|
-
function findJsxOpeningEnd(src, start) {
|
|
2641
|
-
let i = start;
|
|
2642
|
-
while (i < src.length) {
|
|
2643
|
-
const ch = src[i];
|
|
2644
|
-
if (ch === ">" || ch === "/" && src[i + 1] === ">") return i;
|
|
2645
|
-
if (ch === '"' || ch === "'" || ch === "`") {
|
|
2646
|
-
i = skipString2(src, i);
|
|
2647
|
-
} else if (ch === "{") {
|
|
2648
|
-
i = skipBraces(src, i);
|
|
2649
|
-
} else {
|
|
2650
|
-
i++;
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2653
|
-
return -1;
|
|
2654
|
-
}
|
|
2655
|
-
function skipString2(src, start) {
|
|
2656
|
-
const quote = src[start];
|
|
2657
|
-
let i = start + 1;
|
|
2658
|
-
while (i < src.length) {
|
|
2659
|
-
if (src[i] === "\\" && quote !== "`") {
|
|
2660
|
-
i += 2;
|
|
2661
|
-
continue;
|
|
2662
|
-
}
|
|
2663
|
-
if (quote === "`" && src[i] === "$" && src[i + 1] === "{") {
|
|
2664
|
-
i = skipBraces(src, i + 1);
|
|
2665
|
-
continue;
|
|
2666
|
-
}
|
|
2667
|
-
if (src[i] === quote) return i + 1;
|
|
2668
|
-
i++;
|
|
2669
|
-
}
|
|
2670
|
-
return i;
|
|
2671
|
-
}
|
|
2672
|
-
function skipBraces(src, start) {
|
|
2673
|
-
let depth = 1;
|
|
2674
|
-
let i = start + 1;
|
|
2675
|
-
while (i < src.length && depth > 0) {
|
|
2676
|
-
const ch = src[i];
|
|
2677
|
-
if (ch === "{") {
|
|
2678
|
-
depth++;
|
|
2679
|
-
i++;
|
|
2680
|
-
} else if (ch === "}") {
|
|
2681
|
-
depth--;
|
|
2682
|
-
i++;
|
|
2683
|
-
} else if (ch === '"' || ch === "'" || ch === "`") {
|
|
2684
|
-
i = skipString2(src, i);
|
|
2685
|
-
} else {
|
|
2686
|
-
i++;
|
|
2687
|
-
}
|
|
2688
|
-
}
|
|
2689
|
-
return i;
|
|
2690
|
-
}
|
|
2691
2667
|
function dynamicAttrHint(kind) {
|
|
2692
2668
|
if (kind === "region") {
|
|
2693
2669
|
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`;
|
|
2694
2670
|
}
|
|
2695
|
-
return `The scanner
|
|
2671
|
+
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)`;
|
|
2696
2672
|
}
|
|
2697
2673
|
function stableStringify(value) {
|
|
2698
2674
|
return JSON.stringify(value, stableReplacer);
|
|
@@ -2725,9 +2701,7 @@ function replacerSorted(_key, value) {
|
|
|
2725
2701
|
}
|
|
2726
2702
|
return value;
|
|
2727
2703
|
}
|
|
2728
|
-
function emitIdUnion(name, ids
|
|
2729
|
-
if (typeMode === "loose") return `export type ${name} = string
|
|
2730
|
-
`;
|
|
2704
|
+
function emitIdUnion(name, ids) {
|
|
2731
2705
|
if (ids.length === 0) return `export type ${name} = never
|
|
2732
2706
|
`;
|
|
2733
2707
|
const sorted = [...ids].sort();
|
|
@@ -2737,12 +2711,7 @@ ${body}
|
|
|
2737
2711
|
`;
|
|
2738
2712
|
}
|
|
2739
2713
|
function emit(opts) {
|
|
2740
|
-
const {
|
|
2741
|
-
registry,
|
|
2742
|
-
gitContext,
|
|
2743
|
-
uidexImport = "uidex",
|
|
2744
|
-
typeMode = "strict"
|
|
2745
|
-
} = opts;
|
|
2714
|
+
const { registry, gitContext, uidexImport = "uidex" } = opts;
|
|
2746
2715
|
const routes = [...registry.list("route")].sort(
|
|
2747
2716
|
(a, b) => a.path.localeCompare(b.path)
|
|
2748
2717
|
);
|
|
@@ -2765,57 +2734,49 @@ function emit(opts) {
|
|
|
2765
2734
|
lines.push(
|
|
2766
2735
|
emitIdUnion(
|
|
2767
2736
|
"PageId",
|
|
2768
|
-
pages.map((e) => e.id)
|
|
2769
|
-
typeMode
|
|
2737
|
+
pages.map((e) => e.id)
|
|
2770
2738
|
)
|
|
2771
2739
|
);
|
|
2772
2740
|
lines.push(
|
|
2773
2741
|
emitIdUnion(
|
|
2774
2742
|
"FeatureId",
|
|
2775
|
-
features.map((e) => e.id)
|
|
2776
|
-
typeMode
|
|
2743
|
+
features.map((e) => e.id)
|
|
2777
2744
|
)
|
|
2778
2745
|
);
|
|
2779
2746
|
lines.push(
|
|
2780
2747
|
emitIdUnion(
|
|
2781
2748
|
"WidgetId",
|
|
2782
|
-
widgets.map((e) => e.id)
|
|
2783
|
-
typeMode
|
|
2749
|
+
widgets.map((e) => e.id)
|
|
2784
2750
|
)
|
|
2785
2751
|
);
|
|
2786
2752
|
lines.push(
|
|
2787
2753
|
emitIdUnion(
|
|
2788
2754
|
"RegionId",
|
|
2789
|
-
regions.map((e) => e.id)
|
|
2790
|
-
typeMode
|
|
2755
|
+
regions.map((e) => e.id)
|
|
2791
2756
|
)
|
|
2792
2757
|
);
|
|
2793
2758
|
lines.push(
|
|
2794
2759
|
emitIdUnion(
|
|
2795
2760
|
"ElementId",
|
|
2796
|
-
elements.map((e) => e.id)
|
|
2797
|
-
typeMode
|
|
2761
|
+
elements.map((e) => e.id)
|
|
2798
2762
|
)
|
|
2799
2763
|
);
|
|
2800
2764
|
lines.push(
|
|
2801
2765
|
emitIdUnion(
|
|
2802
2766
|
"PrimitiveId",
|
|
2803
|
-
primitives.map((e) => e.id)
|
|
2804
|
-
typeMode
|
|
2767
|
+
primitives.map((e) => e.id)
|
|
2805
2768
|
)
|
|
2806
2769
|
);
|
|
2807
2770
|
lines.push(
|
|
2808
2771
|
emitIdUnion(
|
|
2809
2772
|
"FlowId",
|
|
2810
|
-
flows.map((e) => e.id)
|
|
2811
|
-
typeMode
|
|
2773
|
+
flows.map((e) => e.id)
|
|
2812
2774
|
)
|
|
2813
2775
|
);
|
|
2814
2776
|
lines.push(
|
|
2815
2777
|
emitIdUnion(
|
|
2816
2778
|
"RouteId",
|
|
2817
|
-
routes.map((e) => e.path)
|
|
2818
|
-
typeMode
|
|
2779
|
+
routes.map((e) => e.path)
|
|
2819
2780
|
)
|
|
2820
2781
|
);
|
|
2821
2782
|
lines.push("");
|
|
@@ -2928,22 +2889,33 @@ function parseGitHubRef(ref) {
|
|
|
2928
2889
|
|
|
2929
2890
|
// src/scanner/scan/scaffold.ts
|
|
2930
2891
|
var fs3 = __toESM(require("fs"), 1);
|
|
2931
|
-
var
|
|
2892
|
+
var path6 = __toESM(require("path"), 1);
|
|
2932
2893
|
function scaffoldWidgetSpec(opts) {
|
|
2894
|
+
return scaffoldSpec({
|
|
2895
|
+
registry: opts.registry,
|
|
2896
|
+
kind: "widget",
|
|
2897
|
+
id: opts.widgetId,
|
|
2898
|
+
outDir: opts.outDir,
|
|
2899
|
+
force: opts.force,
|
|
2900
|
+
fixtureImport: opts.fixtureImport
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
function scaffoldSpec(opts) {
|
|
2933
2904
|
const {
|
|
2934
2905
|
registry,
|
|
2935
|
-
|
|
2906
|
+
kind,
|
|
2907
|
+
id,
|
|
2936
2908
|
outDir,
|
|
2937
2909
|
force = false,
|
|
2938
2910
|
fixtureImport = "./fixtures"
|
|
2939
2911
|
} = opts;
|
|
2940
|
-
const
|
|
2941
|
-
if (!
|
|
2942
|
-
throw new Error(
|
|
2912
|
+
const entity = registry.get(kind, id);
|
|
2913
|
+
if (!entity) {
|
|
2914
|
+
throw new Error(`${capitalize(kind)} "${id}" not found in registry`);
|
|
2943
2915
|
}
|
|
2944
|
-
const criteria =
|
|
2945
|
-
const filename = `widget-${
|
|
2946
|
-
const outputPath =
|
|
2916
|
+
const criteria = entity.meta?.acceptance ?? [];
|
|
2917
|
+
const filename = kind === "widget" ? `widget-${id}.spec.ts` : `flow-${id}.spec.ts`;
|
|
2918
|
+
const outputPath = path6.resolve(outDir, filename);
|
|
2947
2919
|
if (fs3.existsSync(outputPath) && !force) {
|
|
2948
2920
|
return {
|
|
2949
2921
|
outputPath,
|
|
@@ -2952,15 +2924,14 @@ function scaffoldWidgetSpec(opts) {
|
|
|
2952
2924
|
reason: `spec already exists at ${outputPath}; pass --force to overwrite`
|
|
2953
2925
|
};
|
|
2954
2926
|
}
|
|
2955
|
-
const content = renderSpec({
|
|
2956
|
-
|
|
2957
|
-
criteria,
|
|
2958
|
-
fixtureImport
|
|
2959
|
-
});
|
|
2960
|
-
fs3.mkdirSync(path5.dirname(outputPath), { recursive: true });
|
|
2927
|
+
const content = renderSpec({ id, criteria, fixtureImport });
|
|
2928
|
+
fs3.mkdirSync(path6.dirname(outputPath), { recursive: true });
|
|
2961
2929
|
fs3.writeFileSync(outputPath, content, "utf8");
|
|
2962
2930
|
return { outputPath, written: true, skipped: false };
|
|
2963
2931
|
}
|
|
2932
|
+
function capitalize(s) {
|
|
2933
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
2934
|
+
}
|
|
2964
2935
|
function renderSpec(args) {
|
|
2965
2936
|
const lines = [];
|
|
2966
2937
|
lines.push(
|
|
@@ -2968,7 +2939,7 @@ function renderSpec(args) {
|
|
|
2968
2939
|
);
|
|
2969
2940
|
lines.push("");
|
|
2970
2941
|
lines.push(
|
|
2971
|
-
`test.describe(${JSON.stringify(args.
|
|
2942
|
+
`test.describe(${JSON.stringify(args.id)}, { tag: "@uidex:flow" }, () => {`
|
|
2972
2943
|
);
|
|
2973
2944
|
if (args.criteria.length === 0) {
|
|
2974
2945
|
lines.push(` test("TODO: add acceptance criteria", async () => {`);
|
|
@@ -2991,7 +2962,7 @@ function renderSpec(args) {
|
|
|
2991
2962
|
|
|
2992
2963
|
// src/scanner/scan/pipeline.ts
|
|
2993
2964
|
var fs4 = __toESM(require("fs"), 1);
|
|
2994
|
-
var
|
|
2965
|
+
var path7 = __toESM(require("path"), 1);
|
|
2995
2966
|
function runScan(opts = {}) {
|
|
2996
2967
|
const cwd = opts.cwd ?? process.cwd();
|
|
2997
2968
|
const configs = opts.configs ?? discover({ cwd });
|
|
@@ -3020,10 +2991,9 @@ function runOne(dc, opts) {
|
|
|
3020
2991
|
const gitContext = resolveGitContext({ cwd: configDir });
|
|
3021
2992
|
const generated = emit({
|
|
3022
2993
|
registry: resolved.registry,
|
|
3023
|
-
gitContext
|
|
3024
|
-
typeMode: config.typeMode
|
|
2994
|
+
gitContext
|
|
3025
2995
|
});
|
|
3026
|
-
const outputPath =
|
|
2996
|
+
const outputPath = path7.resolve(configDir, config.output);
|
|
3027
2997
|
const outputRel = config.output;
|
|
3028
2998
|
let existingOnDisk = null;
|
|
3029
2999
|
if (opts.check) {
|
|
@@ -3033,12 +3003,16 @@ function runOne(dc, opts) {
|
|
|
3033
3003
|
existingOnDisk = null;
|
|
3034
3004
|
}
|
|
3035
3005
|
}
|
|
3006
|
+
const hasExtractDiagnostics = [...extracted, ...extractedFlows].some(
|
|
3007
|
+
(ef) => (ef.diagnostics?.length ?? 0) > 0
|
|
3008
|
+
);
|
|
3036
3009
|
let auditResult;
|
|
3037
|
-
if (opts.check || opts.lint || resolved.diagnostics.length > 0) {
|
|
3010
|
+
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
|
|
3038
3011
|
auditResult = audit({
|
|
3039
3012
|
registry: resolved.registry,
|
|
3040
3013
|
extracted,
|
|
3041
3014
|
files: sourceFiles,
|
|
3015
|
+
flowExtracted: extractedFlows,
|
|
3042
3016
|
config,
|
|
3043
3017
|
check: opts.check,
|
|
3044
3018
|
lint: opts.lint,
|
|
@@ -3059,34 +3033,281 @@ function runOne(dc, opts) {
|
|
|
3059
3033
|
};
|
|
3060
3034
|
}
|
|
3061
3035
|
function writeScanResult(result) {
|
|
3062
|
-
fs4.mkdirSync(
|
|
3036
|
+
fs4.mkdirSync(path7.dirname(result.outputPath), { recursive: true });
|
|
3063
3037
|
fs4.writeFileSync(result.outputPath, result.generated, "utf8");
|
|
3064
3038
|
}
|
|
3065
3039
|
|
|
3040
|
+
// src/scanner/scan/fix.ts
|
|
3041
|
+
var fs5 = __toESM(require("fs"), 1);
|
|
3042
|
+
var path8 = __toESM(require("path"), 1);
|
|
3043
|
+
function applyFixes(diagnostics) {
|
|
3044
|
+
const entries = [];
|
|
3045
|
+
for (const d of diagnostics) {
|
|
3046
|
+
if (!d.fix) continue;
|
|
3047
|
+
entries.push({
|
|
3048
|
+
code: d.code,
|
|
3049
|
+
description: d.fix.description,
|
|
3050
|
+
file: d.file,
|
|
3051
|
+
edits: d.fix.edits ?? [],
|
|
3052
|
+
createFiles: d.fix.createFiles ?? [],
|
|
3053
|
+
deleteFiles: d.fix.deleteFiles ?? []
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
if (entries.length === 0) return { applied: [], skipped: [] };
|
|
3057
|
+
const seenEdits = /* @__PURE__ */ new Set();
|
|
3058
|
+
const editsByFile = /* @__PURE__ */ new Map();
|
|
3059
|
+
for (const entry of entries) {
|
|
3060
|
+
for (const edit of entry.edits) {
|
|
3061
|
+
const key = `${edit.path}:${edit.start}:${edit.end}:${edit.replacement}`;
|
|
3062
|
+
if (seenEdits.has(key)) continue;
|
|
3063
|
+
seenEdits.add(key);
|
|
3064
|
+
let list = editsByFile.get(edit.path);
|
|
3065
|
+
if (!list) {
|
|
3066
|
+
list = [];
|
|
3067
|
+
editsByFile.set(edit.path, list);
|
|
3068
|
+
}
|
|
3069
|
+
list.push({ ...edit, entry });
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
for (const [filePath, edits] of editsByFile) {
|
|
3073
|
+
let content;
|
|
3074
|
+
try {
|
|
3075
|
+
content = fs5.readFileSync(filePath, "utf8");
|
|
3076
|
+
} catch {
|
|
3077
|
+
for (const e of edits) e.entry.skippedReason ??= "file is unreadable";
|
|
3078
|
+
continue;
|
|
3079
|
+
}
|
|
3080
|
+
edits.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
3081
|
+
const kept = [];
|
|
3082
|
+
let prevEnd = -1;
|
|
3083
|
+
for (const edit of edits) {
|
|
3084
|
+
if (edit.start < prevEnd) {
|
|
3085
|
+
edit.entry.skippedReason ??= "overlapping edit";
|
|
3086
|
+
continue;
|
|
3087
|
+
}
|
|
3088
|
+
kept.push(edit);
|
|
3089
|
+
prevEnd = edit.end;
|
|
3090
|
+
}
|
|
3091
|
+
for (let i = kept.length - 1; i >= 0; i--) {
|
|
3092
|
+
const edit = kept[i];
|
|
3093
|
+
content = content.slice(0, edit.start) + edit.replacement + content.slice(edit.end);
|
|
3094
|
+
}
|
|
3095
|
+
if (kept.length > 0) fs5.writeFileSync(filePath, content, "utf8");
|
|
3096
|
+
}
|
|
3097
|
+
for (const entry of entries) {
|
|
3098
|
+
if (entry.skippedReason) continue;
|
|
3099
|
+
for (const create of entry.createFiles) {
|
|
3100
|
+
if (fs5.existsSync(create.path)) {
|
|
3101
|
+
entry.skippedReason = `${path8.basename(create.path)} already exists`;
|
|
3102
|
+
continue;
|
|
3103
|
+
}
|
|
3104
|
+
fs5.mkdirSync(path8.dirname(create.path), { recursive: true });
|
|
3105
|
+
fs5.writeFileSync(create.path, create.content, "utf8");
|
|
3106
|
+
}
|
|
3107
|
+
if (entry.skippedReason) continue;
|
|
3108
|
+
for (const del of entry.deleteFiles) {
|
|
3109
|
+
try {
|
|
3110
|
+
fs5.unlinkSync(del);
|
|
3111
|
+
} catch {
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
const applied = [];
|
|
3116
|
+
const skipped = [];
|
|
3117
|
+
for (const entry of entries) {
|
|
3118
|
+
const summary = {
|
|
3119
|
+
code: entry.code,
|
|
3120
|
+
description: entry.description,
|
|
3121
|
+
file: entry.file
|
|
3122
|
+
};
|
|
3123
|
+
if (entry.skippedReason) {
|
|
3124
|
+
skipped.push({ ...summary, reason: entry.skippedReason });
|
|
3125
|
+
} else {
|
|
3126
|
+
applied.push(summary);
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
return { applied, skipped };
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
// src/scanner/scan/rename.ts
|
|
3133
|
+
var ID_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
3134
|
+
function renameEntity(opts) {
|
|
3135
|
+
const { cwd, kind, oldId, newId, force = false } = opts;
|
|
3136
|
+
const manual = [];
|
|
3137
|
+
const errors = [];
|
|
3138
|
+
if (!ID_RE.test(newId)) {
|
|
3139
|
+
return {
|
|
3140
|
+
edits: 0,
|
|
3141
|
+
manual,
|
|
3142
|
+
errors: [`New id "${newId}" is not kebab-case`]
|
|
3143
|
+
};
|
|
3144
|
+
}
|
|
3145
|
+
const configs = discover({ cwd });
|
|
3146
|
+
if (configs.length === 0) {
|
|
3147
|
+
return { edits: 0, manual, errors: [`No .uidex.json found under ${cwd}`] };
|
|
3148
|
+
}
|
|
3149
|
+
const edits = [];
|
|
3150
|
+
for (const dc of configs) {
|
|
3151
|
+
const { config, configDir } = dc;
|
|
3152
|
+
const sourceFiles = walk(config.sources, {
|
|
3153
|
+
cwd: configDir,
|
|
3154
|
+
globalExcludes: config.exclude
|
|
3155
|
+
});
|
|
3156
|
+
const extracted = extract(sourceFiles);
|
|
3157
|
+
const flowFiles = config.flows ? walk(
|
|
3158
|
+
config.flows.map((glob) => ({ rootDir: ".", include: [glob] })),
|
|
3159
|
+
{ cwd: configDir, includeTests: true }
|
|
3160
|
+
) : [];
|
|
3161
|
+
const extractedFlows = extract(flowFiles);
|
|
3162
|
+
const scan = runScan({ cwd: configDir, configs: [dc] })[0];
|
|
3163
|
+
const registry = scan.registry;
|
|
3164
|
+
if (!registry.get(kind, oldId)) {
|
|
3165
|
+
if (registry.matchPattern(kind, oldId)) {
|
|
3166
|
+
errors.push(
|
|
3167
|
+
`${kind} "${oldId}" only matches via a pattern id; pattern-backed ids cannot be renamed mechanically`
|
|
3168
|
+
);
|
|
3169
|
+
} else {
|
|
3170
|
+
errors.push(`${kind} "${oldId}" not found in registry`);
|
|
3171
|
+
}
|
|
3172
|
+
continue;
|
|
3173
|
+
}
|
|
3174
|
+
if (registry.get(kind, newId) && !force) {
|
|
3175
|
+
errors.push(
|
|
3176
|
+
`${kind} "${newId}" already exists; pass --force to merge the ids`
|
|
3177
|
+
);
|
|
3178
|
+
continue;
|
|
3179
|
+
}
|
|
3180
|
+
const quoteAs = (content, start) => {
|
|
3181
|
+
const q = content[start];
|
|
3182
|
+
return q === '"' || q === "'" || q === "`" ? `${q}${newId}${q}` : `"${newId}"`;
|
|
3183
|
+
};
|
|
3184
|
+
for (const ef of extracted) {
|
|
3185
|
+
for (const a of ef.annotations) {
|
|
3186
|
+
if (a.kind !== kind || a.id !== oldId) continue;
|
|
3187
|
+
if (a.span) {
|
|
3188
|
+
edits.push({
|
|
3189
|
+
path: ef.file.sourcePath,
|
|
3190
|
+
start: a.span.start,
|
|
3191
|
+
end: a.span.end,
|
|
3192
|
+
replacement: quoteAs(ef.file.content, a.span.start)
|
|
3193
|
+
});
|
|
3194
|
+
} else {
|
|
3195
|
+
manual.push({
|
|
3196
|
+
file: a.file,
|
|
3197
|
+
line: a.line,
|
|
3198
|
+
reason: "attribute value is not a plain string literal (const reference, ternary, or template)"
|
|
3199
|
+
});
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
for (const m of ef.metadata ?? []) {
|
|
3203
|
+
if (kind === "widget" && m.kind === "widget" && m.id === oldId) {
|
|
3204
|
+
if (m.idSpan) {
|
|
3205
|
+
edits.push({
|
|
3206
|
+
path: ef.file.sourcePath,
|
|
3207
|
+
start: m.idSpan.start,
|
|
3208
|
+
end: m.idSpan.end,
|
|
3209
|
+
replacement: quoteAs(ef.file.content, m.idSpan.start)
|
|
3210
|
+
});
|
|
3211
|
+
} else {
|
|
3212
|
+
manual.push({
|
|
3213
|
+
file: ef.file.displayPath,
|
|
3214
|
+
line: m.loc.line ?? 1,
|
|
3215
|
+
reason: "widget export id is not a plain string literal"
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
if (kind === "widget" && m.widgets) {
|
|
3220
|
+
for (let i = 0; i < m.widgets.length; i++) {
|
|
3221
|
+
if (m.widgets[i] !== oldId) continue;
|
|
3222
|
+
const span = m.widgetSpans?.[i];
|
|
3223
|
+
if (span) {
|
|
3224
|
+
edits.push({
|
|
3225
|
+
path: ef.file.sourcePath,
|
|
3226
|
+
start: span.start,
|
|
3227
|
+
end: span.end,
|
|
3228
|
+
replacement: quoteAs(ef.file.content, span.start)
|
|
3229
|
+
});
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
for (const ef of extractedFlows) {
|
|
3236
|
+
for (const fact of ef.flows ?? []) {
|
|
3237
|
+
for (const call of fact.calls) {
|
|
3238
|
+
if (call.id !== oldId) continue;
|
|
3239
|
+
if (call.span) {
|
|
3240
|
+
edits.push({
|
|
3241
|
+
path: ef.file.sourcePath,
|
|
3242
|
+
start: call.span.start,
|
|
3243
|
+
end: call.span.end,
|
|
3244
|
+
replacement: quoteAs(ef.file.content, call.span.start)
|
|
3245
|
+
});
|
|
3246
|
+
} else {
|
|
3247
|
+
manual.push({
|
|
3248
|
+
file: ef.file.displayPath,
|
|
3249
|
+
line: call.line,
|
|
3250
|
+
reason: "uidex() argument is not a plain string literal"
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
if (errors.length > 0) {
|
|
3258
|
+
return { edits: 0, manual, errors };
|
|
3259
|
+
}
|
|
3260
|
+
if (edits.length === 0 && manual.length === 0) {
|
|
3261
|
+
return {
|
|
3262
|
+
edits: 0,
|
|
3263
|
+
manual,
|
|
3264
|
+
errors: [
|
|
3265
|
+
`${kind} "${oldId}" has no editable occurrences (convention-derived ids like landmarks cannot be renamed)`
|
|
3266
|
+
]
|
|
3267
|
+
};
|
|
3268
|
+
}
|
|
3269
|
+
const result = applyFixes([
|
|
3270
|
+
{
|
|
3271
|
+
code: "rename",
|
|
3272
|
+
severity: "info",
|
|
3273
|
+
message: "",
|
|
3274
|
+
fix: {
|
|
3275
|
+
description: `Rename ${kind} "${oldId}" to "${newId}"`,
|
|
3276
|
+
edits
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
]);
|
|
3280
|
+
if (result.skipped.length > 0) {
|
|
3281
|
+
errors.push(`Some edits were skipped: ${result.skipped[0].reason}`);
|
|
3282
|
+
}
|
|
3283
|
+
for (const r of runScan({ cwd })) writeScanResult(r);
|
|
3284
|
+
return { edits: edits.length, manual, errors };
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3066
3287
|
// src/scanner/scan/cli.ts
|
|
3067
|
-
var
|
|
3068
|
-
var
|
|
3288
|
+
var fs8 = __toESM(require("fs"), 1);
|
|
3289
|
+
var path11 = __toESM(require("path"), 1);
|
|
3069
3290
|
|
|
3070
3291
|
// src/scanner/scan/ai/index.ts
|
|
3071
3292
|
var p = __toESM(require("@clack/prompts"), 1);
|
|
3072
3293
|
|
|
3073
3294
|
// src/scanner/scan/ai/providers/claude.ts
|
|
3074
|
-
var
|
|
3075
|
-
var
|
|
3295
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
3296
|
+
var path10 = __toESM(require("path"), 1);
|
|
3076
3297
|
|
|
3077
3298
|
// src/scanner/scan/ai/templates.ts
|
|
3078
|
-
var
|
|
3079
|
-
var
|
|
3299
|
+
var fs6 = __toESM(require("fs"), 1);
|
|
3300
|
+
var path9 = __toESM(require("path"), 1);
|
|
3080
3301
|
function templatePath(rel) {
|
|
3081
3302
|
const candidates = [
|
|
3082
|
-
|
|
3083
|
-
// dist/cli/cli.cjs
|
|
3084
|
-
|
|
3085
|
-
// src/scan/ai
|
|
3303
|
+
path9.resolve(__dirname, "../../templates", rel),
|
|
3304
|
+
// dist/cli/cli.cjs → ../../templates
|
|
3305
|
+
path9.resolve(__dirname, "../../../../templates", rel)
|
|
3306
|
+
// src/scanner/scan/ai → ../../../../templates
|
|
3086
3307
|
];
|
|
3087
3308
|
for (const c of candidates) {
|
|
3088
3309
|
try {
|
|
3089
|
-
|
|
3310
|
+
fs6.accessSync(c, fs6.constants.R_OK);
|
|
3090
3311
|
return c;
|
|
3091
3312
|
} catch {
|
|
3092
3313
|
continue;
|
|
@@ -3098,24 +3319,39 @@ function templatePath(rel) {
|
|
|
3098
3319
|
);
|
|
3099
3320
|
}
|
|
3100
3321
|
function readTemplate(rel) {
|
|
3101
|
-
return
|
|
3322
|
+
return fs6.readFileSync(templatePath(rel), "utf8");
|
|
3102
3323
|
}
|
|
3103
3324
|
|
|
3104
3325
|
// src/scanner/scan/ai/providers/claude.ts
|
|
3105
|
-
var
|
|
3106
|
-
{ dest: ".claude/
|
|
3107
|
-
{
|
|
3108
|
-
|
|
3326
|
+
var SKILL_FILES = [
|
|
3327
|
+
{ dest: ".claude/skills/uidex/SKILL.md", template: "claude/SKILL.md" },
|
|
3328
|
+
{
|
|
3329
|
+
dest: ".claude/skills/uidex/references/conventions.md",
|
|
3330
|
+
template: "claude/references/conventions.md"
|
|
3331
|
+
},
|
|
3332
|
+
{
|
|
3333
|
+
dest: ".claude/skills/uidex/references/audit.md",
|
|
3334
|
+
template: "claude/references/audit.md"
|
|
3335
|
+
},
|
|
3336
|
+
{
|
|
3337
|
+
dest: ".claude/skills/uidex/references/api.md",
|
|
3338
|
+
template: "claude/references/api.md"
|
|
3339
|
+
}
|
|
3340
|
+
];
|
|
3341
|
+
var LEGACY_FILES = [
|
|
3342
|
+
".claude/rules/uidex.md",
|
|
3343
|
+
".claude/commands/uidex/audit.md",
|
|
3344
|
+
".claude/commands/uidex/api.md"
|
|
3109
3345
|
];
|
|
3110
3346
|
var claudeProvider = {
|
|
3111
3347
|
id: "claude",
|
|
3112
3348
|
label: "Claude Code",
|
|
3113
|
-
description: "Adds .claude/
|
|
3349
|
+
description: "Adds .claude/skills/uidex/ skill with conventions, audit, and API references.",
|
|
3114
3350
|
async install({ cwd, force }) {
|
|
3115
3351
|
const changes = [];
|
|
3116
|
-
for (const file of
|
|
3117
|
-
const dest =
|
|
3118
|
-
const exists =
|
|
3352
|
+
for (const file of SKILL_FILES) {
|
|
3353
|
+
const dest = path10.join(cwd, file.dest);
|
|
3354
|
+
const exists = fs7.existsSync(dest);
|
|
3119
3355
|
if (exists && !force) {
|
|
3120
3356
|
changes.push({
|
|
3121
3357
|
path: file.dest,
|
|
@@ -3124,36 +3360,56 @@ var claudeProvider = {
|
|
|
3124
3360
|
});
|
|
3125
3361
|
continue;
|
|
3126
3362
|
}
|
|
3127
|
-
|
|
3128
|
-
|
|
3363
|
+
fs7.mkdirSync(path10.dirname(dest), { recursive: true });
|
|
3364
|
+
fs7.writeFileSync(dest, readTemplate(file.template));
|
|
3129
3365
|
changes.push({
|
|
3130
3366
|
path: file.dest,
|
|
3131
3367
|
action: exists ? "overwritten" : "created"
|
|
3132
3368
|
});
|
|
3133
3369
|
}
|
|
3370
|
+
for (const rel of LEGACY_FILES) {
|
|
3371
|
+
const dest = path10.join(cwd, rel);
|
|
3372
|
+
if (fs7.existsSync(dest)) {
|
|
3373
|
+
fs7.unlinkSync(dest);
|
|
3374
|
+
changes.push({ path: rel, action: "removed" });
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
cleanupEmpty(path10.join(cwd, ".claude/commands/uidex"));
|
|
3378
|
+
cleanupEmpty(path10.join(cwd, ".claude/commands"));
|
|
3379
|
+
cleanupEmpty(path10.join(cwd, ".claude/rules"));
|
|
3134
3380
|
return { changes };
|
|
3135
3381
|
},
|
|
3136
3382
|
async uninstall({ cwd }) {
|
|
3137
3383
|
const changes = [];
|
|
3138
|
-
for (const file of
|
|
3139
|
-
const dest =
|
|
3140
|
-
if (!
|
|
3384
|
+
for (const file of SKILL_FILES) {
|
|
3385
|
+
const dest = path10.join(cwd, file.dest);
|
|
3386
|
+
if (!fs7.existsSync(dest)) {
|
|
3141
3387
|
changes.push({ path: file.dest, action: "skipped", reason: "absent" });
|
|
3142
3388
|
continue;
|
|
3143
3389
|
}
|
|
3144
|
-
|
|
3390
|
+
fs7.unlinkSync(dest);
|
|
3145
3391
|
changes.push({ path: file.dest, action: "removed" });
|
|
3146
3392
|
}
|
|
3147
|
-
cleanupEmpty(
|
|
3148
|
-
cleanupEmpty(
|
|
3149
|
-
cleanupEmpty(
|
|
3393
|
+
cleanupEmpty(path10.join(cwd, ".claude/skills/uidex/references"));
|
|
3394
|
+
cleanupEmpty(path10.join(cwd, ".claude/skills/uidex"));
|
|
3395
|
+
cleanupEmpty(path10.join(cwd, ".claude/skills"));
|
|
3396
|
+
for (const rel of LEGACY_FILES) {
|
|
3397
|
+
const dest = path10.join(cwd, rel);
|
|
3398
|
+
if (fs7.existsSync(dest)) {
|
|
3399
|
+
fs7.unlinkSync(dest);
|
|
3400
|
+
changes.push({ path: rel, action: "removed" });
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
cleanupEmpty(path10.join(cwd, ".claude/commands/uidex"));
|
|
3404
|
+
cleanupEmpty(path10.join(cwd, ".claude/commands"));
|
|
3405
|
+
cleanupEmpty(path10.join(cwd, ".claude/rules"));
|
|
3150
3406
|
return { changes };
|
|
3151
3407
|
}
|
|
3152
3408
|
};
|
|
3153
3409
|
function cleanupEmpty(dir) {
|
|
3154
3410
|
try {
|
|
3155
|
-
const entries =
|
|
3156
|
-
if (entries.length === 0)
|
|
3411
|
+
const entries = fs7.readdirSync(dir);
|
|
3412
|
+
if (entries.length === 0) fs7.rmdirSync(dir);
|
|
3157
3413
|
} catch {
|
|
3158
3414
|
}
|
|
3159
3415
|
}
|
|
@@ -3320,6 +3576,8 @@ async function run(opts) {
|
|
|
3320
3576
|
return runScanCommand(cwd, flags, writer);
|
|
3321
3577
|
case "scaffold":
|
|
3322
3578
|
return runScaffold(cwd, positional.slice(1), flags, writer);
|
|
3579
|
+
case "rename":
|
|
3580
|
+
return runRename(cwd, positional.slice(1), flags, writer);
|
|
3323
3581
|
case "ai": {
|
|
3324
3582
|
const result = await runAiCommand({
|
|
3325
3583
|
cwd,
|
|
@@ -3346,7 +3604,8 @@ function helpText2() {
|
|
|
3346
3604
|
"Commands:",
|
|
3347
3605
|
" init Create a .uidex.json",
|
|
3348
3606
|
" scan [flags] Run the scanner pipeline",
|
|
3349
|
-
" scaffold widget <id> Emit a Playwright spec from
|
|
3607
|
+
" scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
|
|
3608
|
+
" rename <element|widget|region> <old-id> <new-id> Rename an id everywhere (DOM attr, flows, exports)",
|
|
3350
3609
|
" ai <install|uninstall|providers> Manage AI assistant integrations",
|
|
3351
3610
|
" api <METHOD> <PATH> Call the uidex API",
|
|
3352
3611
|
" api --list Show available API routes",
|
|
@@ -3355,16 +3614,17 @@ function helpText2() {
|
|
|
3355
3614
|
"",
|
|
3356
3615
|
"Flags:",
|
|
3357
3616
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
3358
|
-
" --lint Run lint diagnostics (missing annotations, scope leak,
|
|
3617
|
+
" --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
|
|
3359
3618
|
" --audit Equivalent to --check --lint (read-only)",
|
|
3619
|
+
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
|
|
3360
3620
|
" --json Emit JSON diagnostics on stdout",
|
|
3361
3621
|
" --force (scaffold) overwrite existing spec",
|
|
3362
3622
|
""
|
|
3363
3623
|
].join("\n");
|
|
3364
3624
|
}
|
|
3365
3625
|
function runInit(cwd, w) {
|
|
3366
|
-
const configPath =
|
|
3367
|
-
if (
|
|
3626
|
+
const configPath = path11.join(cwd, CONFIG_FILENAME);
|
|
3627
|
+
if (fs8.existsSync(configPath)) {
|
|
3368
3628
|
w.err(`.uidex.json already exists at ${configPath}`);
|
|
3369
3629
|
return w.result(1);
|
|
3370
3630
|
}
|
|
@@ -3373,16 +3633,16 @@ function runInit(cwd, w) {
|
|
|
3373
3633
|
sources: [{ rootDir: "src" }],
|
|
3374
3634
|
output: "src/uidex.gen.ts"
|
|
3375
3635
|
};
|
|
3376
|
-
|
|
3636
|
+
fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3377
3637
|
w.out(`Created ${configPath}`);
|
|
3378
|
-
const gitignorePath =
|
|
3638
|
+
const gitignorePath = path11.join(cwd, ".gitignore");
|
|
3379
3639
|
const entry = "*.gen.ts";
|
|
3380
|
-
if (
|
|
3381
|
-
const existing =
|
|
3640
|
+
if (fs8.existsSync(gitignorePath)) {
|
|
3641
|
+
const existing = fs8.readFileSync(gitignorePath, "utf8");
|
|
3382
3642
|
const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
|
|
3383
3643
|
if (!hasEntry) {
|
|
3384
3644
|
const needsNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
3385
|
-
|
|
3645
|
+
fs8.appendFileSync(
|
|
3386
3646
|
gitignorePath,
|
|
3387
3647
|
`${needsNewline ? "\n" : ""}${entry}
|
|
3388
3648
|
`,
|
|
@@ -3391,21 +3651,33 @@ function runInit(cwd, w) {
|
|
|
3391
3651
|
w.out(`Appended ${entry} to ${gitignorePath}`);
|
|
3392
3652
|
}
|
|
3393
3653
|
} else {
|
|
3394
|
-
|
|
3654
|
+
fs8.writeFileSync(gitignorePath, `${entry}
|
|
3395
3655
|
`, "utf8");
|
|
3396
3656
|
w.out(`Created ${gitignorePath} with ${entry}`);
|
|
3397
3657
|
}
|
|
3398
3658
|
return w.result(0);
|
|
3399
3659
|
}
|
|
3400
3660
|
function runScanCommand(cwd, flags, w) {
|
|
3401
|
-
const
|
|
3402
|
-
const
|
|
3661
|
+
const fix = Boolean(flags.fix);
|
|
3662
|
+
const check = !fix && Boolean(flags.check || flags.audit);
|
|
3663
|
+
const lint = Boolean(flags.lint || flags.audit || fix);
|
|
3403
3664
|
const asJson = Boolean(flags.json);
|
|
3404
|
-
|
|
3665
|
+
let configs = discover({ cwd });
|
|
3405
3666
|
if (configs.length === 0) {
|
|
3406
3667
|
w.err(`No ${CONFIG_FILENAME} found under ${cwd}`);
|
|
3407
3668
|
return w.result(1);
|
|
3408
3669
|
}
|
|
3670
|
+
let fixed = [];
|
|
3671
|
+
let fixSkipped = [];
|
|
3672
|
+
if (fix) {
|
|
3673
|
+
const discovery = runScan({ cwd, check: true, lint: true, configs });
|
|
3674
|
+
const result = applyFixes(
|
|
3675
|
+
discovery.flatMap((r) => r.audit?.diagnostics ?? [])
|
|
3676
|
+
);
|
|
3677
|
+
fixed = result.applied;
|
|
3678
|
+
fixSkipped = result.skipped;
|
|
3679
|
+
configs = discover({ cwd });
|
|
3680
|
+
}
|
|
3409
3681
|
const results = runScan({ cwd, check, lint, configs });
|
|
3410
3682
|
if (!check) {
|
|
3411
3683
|
for (const r of results) writeScanResult(r);
|
|
@@ -3420,9 +3692,21 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3420
3692
|
{ errors: 0, warnings: 0 }
|
|
3421
3693
|
);
|
|
3422
3694
|
if (asJson) {
|
|
3423
|
-
const out2 = {
|
|
3695
|
+
const out2 = {
|
|
3696
|
+
diagnostics: allDiagnostics.map(jsonDiagnostic),
|
|
3697
|
+
summary,
|
|
3698
|
+
...fix ? { fixed, fixSkipped } : {}
|
|
3699
|
+
};
|
|
3424
3700
|
w.out(JSON.stringify(out2, null, 2));
|
|
3425
3701
|
} else {
|
|
3702
|
+
for (const f of fixed) {
|
|
3703
|
+
w.out(`FIXED [${f.code}] ${f.file ?? ""} ${f.description}`);
|
|
3704
|
+
}
|
|
3705
|
+
for (const s of fixSkipped) {
|
|
3706
|
+
w.out(
|
|
3707
|
+
`SKIPPED [${s.code}] ${s.file ?? ""} ${s.description} (${s.reason})`
|
|
3708
|
+
);
|
|
3709
|
+
}
|
|
3426
3710
|
for (const r of results) {
|
|
3427
3711
|
if (check) {
|
|
3428
3712
|
w.out(`Checked ${r.outputPath}`);
|
|
@@ -3432,7 +3716,10 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3432
3716
|
for (const d of r.audit?.diagnostics ?? []) {
|
|
3433
3717
|
const loc = d.file ? `${d.file}${d.line ? `:${d.line}` : ""}` : "";
|
|
3434
3718
|
const stream = d.severity === "error" ? w.err : w.out;
|
|
3435
|
-
|
|
3719
|
+
const fixable = d.fix && !fix ? " [fixable: run with --fix]" : "";
|
|
3720
|
+
stream(
|
|
3721
|
+
`${d.severity.toUpperCase()} [${d.code}] ${loc} ${d.message}${fixable}`
|
|
3722
|
+
);
|
|
3436
3723
|
if (d.hint) stream(` hint: ${d.hint}`);
|
|
3437
3724
|
}
|
|
3438
3725
|
}
|
|
@@ -3443,20 +3730,27 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3443
3730
|
const exit = summary.errors > 0 ? 1 : 0;
|
|
3444
3731
|
return w.result(exit);
|
|
3445
3732
|
}
|
|
3733
|
+
function jsonDiagnostic(d) {
|
|
3734
|
+
const { fix, ...rest } = d;
|
|
3735
|
+
return fix ? { ...rest, fixable: true } : rest;
|
|
3736
|
+
}
|
|
3737
|
+
var SCAFFOLD_KINDS = /* @__PURE__ */ new Set(["widget", "page", "feature"]);
|
|
3446
3738
|
function runScaffold(cwd, args, flags, w) {
|
|
3447
3739
|
const [kind, id] = args;
|
|
3448
|
-
if (kind
|
|
3449
|
-
w.err("Usage: uidex scaffold widget <id> [--force]");
|
|
3740
|
+
if (!kind || !SCAFFOLD_KINDS.has(kind) || !id) {
|
|
3741
|
+
w.err("Usage: uidex scaffold <widget|page|feature> <id> [--force]");
|
|
3450
3742
|
return w.result(1);
|
|
3451
3743
|
}
|
|
3744
|
+
const scaffoldKind = kind;
|
|
3452
3745
|
const results = runScan({ cwd });
|
|
3453
3746
|
for (const r of results) {
|
|
3454
|
-
const
|
|
3455
|
-
if (!
|
|
3456
|
-
const outDir =
|
|
3457
|
-
const result =
|
|
3747
|
+
const entity = r.registry.get(scaffoldKind, id);
|
|
3748
|
+
if (!entity) continue;
|
|
3749
|
+
const outDir = path11.resolve(r.configDir, "e2e");
|
|
3750
|
+
const result = scaffoldSpec({
|
|
3458
3751
|
registry: r.registry,
|
|
3459
|
-
|
|
3752
|
+
kind: scaffoldKind,
|
|
3753
|
+
id,
|
|
3460
3754
|
outDir,
|
|
3461
3755
|
force: Boolean(flags.force)
|
|
3462
3756
|
});
|
|
@@ -3467,9 +3761,43 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
3467
3761
|
w.out(`Wrote ${result.outputPath}`);
|
|
3468
3762
|
return w.result(0);
|
|
3469
3763
|
}
|
|
3470
|
-
w.err(
|
|
3764
|
+
w.err(
|
|
3765
|
+
`${scaffoldKind.charAt(0).toUpperCase() + scaffoldKind.slice(1)} "${id}" not found in registry`
|
|
3766
|
+
);
|
|
3471
3767
|
return w.result(1);
|
|
3472
3768
|
}
|
|
3769
|
+
var RENAME_KINDS = /* @__PURE__ */ new Set(["element", "widget", "region"]);
|
|
3770
|
+
function runRename(cwd, args, flags, w) {
|
|
3771
|
+
const [kind, oldId, newId] = args;
|
|
3772
|
+
if (!kind || !RENAME_KINDS.has(kind) || !oldId || !newId) {
|
|
3773
|
+
w.err(
|
|
3774
|
+
"Usage: uidex rename <element|widget|region> <old-id> <new-id> [--force]"
|
|
3775
|
+
);
|
|
3776
|
+
return w.result(1);
|
|
3777
|
+
}
|
|
3778
|
+
const result = renameEntity({
|
|
3779
|
+
cwd,
|
|
3780
|
+
kind,
|
|
3781
|
+
oldId,
|
|
3782
|
+
newId,
|
|
3783
|
+
force: Boolean(flags.force)
|
|
3784
|
+
});
|
|
3785
|
+
for (const e of result.errors) w.err(e);
|
|
3786
|
+
for (const m of result.manual) {
|
|
3787
|
+
w.err(`MANUAL ${m.file}:${m.line} \u2014 ${m.reason}`);
|
|
3788
|
+
}
|
|
3789
|
+
if (result.errors.length > 0) return w.result(1);
|
|
3790
|
+
w.out(
|
|
3791
|
+
`Renamed ${kind} "${oldId}" \u2192 "${newId}" (${result.edits} edit(s)); gen file regenerated`
|
|
3792
|
+
);
|
|
3793
|
+
if (result.manual.length > 0) {
|
|
3794
|
+
w.err(
|
|
3795
|
+
`${result.manual.length} occurrence(s) need manual follow-up (listed above)`
|
|
3796
|
+
);
|
|
3797
|
+
return w.result(1);
|
|
3798
|
+
}
|
|
3799
|
+
return w.result(0);
|
|
3800
|
+
}
|
|
3473
3801
|
function createWriter() {
|
|
3474
3802
|
let stdout = "";
|
|
3475
3803
|
let stderr = "";
|
|
@@ -3490,7 +3818,7 @@ function createWriter() {
|
|
|
3490
3818
|
CONFIG_FILENAME,
|
|
3491
3819
|
ConfigError,
|
|
3492
3820
|
DEFAULT_CONVENTIONS,
|
|
3493
|
-
|
|
3821
|
+
applyFixes,
|
|
3494
3822
|
audit,
|
|
3495
3823
|
detectRoutes,
|
|
3496
3824
|
discover,
|
|
@@ -3500,10 +3828,12 @@ function createWriter() {
|
|
|
3500
3828
|
globToRegExp,
|
|
3501
3829
|
parseConfig,
|
|
3502
3830
|
pathToId,
|
|
3831
|
+
renameEntity,
|
|
3503
3832
|
resolve,
|
|
3504
3833
|
resolveGitContext,
|
|
3505
3834
|
runCli,
|
|
3506
3835
|
runScan,
|
|
3836
|
+
scaffoldSpec,
|
|
3507
3837
|
scaffoldWidgetSpec,
|
|
3508
3838
|
validateConfig,
|
|
3509
3839
|
walk,
|