uidex 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli/cli.cjs +1510 -1244
- package/dist/cli/cli.cjs.map +1 -1
- package/dist/cloud/index.cjs +385 -175
- package/dist/cloud/index.cjs.map +1 -1
- package/dist/cloud/index.d.cts +192 -4
- package/dist/cloud/index.d.ts +192 -4
- package/dist/cloud/index.js +377 -177
- package/dist/cloud/index.js.map +1 -1
- package/dist/headless/index.cjs +82 -255
- package/dist/headless/index.cjs.map +1 -1
- package/dist/headless/index.d.cts +5 -11
- package/dist/headless/index.d.ts +5 -11
- package/dist/headless/index.js +82 -257
- package/dist/headless/index.js.map +1 -1
- package/dist/index.cjs +721 -1053
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +149 -160
- package/dist/index.d.ts +149 -160
- package/dist/index.js +741 -1068
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +729 -1000
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +99 -86
- package/dist/react/index.d.ts +99 -86
- package/dist/react/index.js +745 -1015
- package/dist/react/index.js.map +1 -1
- package/dist/scan/index.cjs +1518 -1237
- package/dist/scan/index.cjs.map +1 -1
- package/dist/scan/index.d.cts +209 -12
- package/dist/scan/index.d.ts +209 -12
- package/dist/scan/index.js +1515 -1236
- package/dist/scan/index.js.map +1 -1
- package/package.json +22 -21
- package/templates/claude/SKILL.md +71 -0
- package/templates/claude/references/audit.md +43 -0
- package/templates/claude/{rules.md → references/conventions.md} +25 -28
- package/templates/claude/audit.md +0 -43
- /package/templates/claude/{api.md → references/api.md} +0 -0
package/dist/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;
|
|
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") {
|
|
541
558
|
continue;
|
|
542
559
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
553
|
-
if (templateDepth > 0 && c === "}") {
|
|
554
|
-
templateDepth--;
|
|
555
|
-
i++;
|
|
556
|
-
continue;
|
|
557
|
-
}
|
|
558
|
-
i++;
|
|
559
|
-
continue;
|
|
560
|
-
}
|
|
561
|
-
if (c === "/" && n === "/") {
|
|
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
595
|
}
|
|
647
596
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
}
|
|
694
|
-
if (isIdentStart(c)) {
|
|
695
|
-
return this.readIdent(pos);
|
|
696
|
-
}
|
|
697
|
-
this.advance();
|
|
698
|
-
return { kind: "punct", value: c, pos, end: this.pos };
|
|
699
|
-
}
|
|
700
|
-
readString(pos, delim) {
|
|
701
|
-
this.advance();
|
|
702
|
-
let value = "";
|
|
703
|
-
while (this.pos < this.src.length) {
|
|
704
|
-
const c = this.src[this.pos];
|
|
705
|
-
if (c === "\\") {
|
|
706
|
-
const esc = this.src[this.pos + 1];
|
|
707
|
-
this.advance(2);
|
|
708
|
-
value += decodeEscape(esc);
|
|
709
|
-
continue;
|
|
710
|
-
}
|
|
711
|
-
if (c === delim) {
|
|
712
|
-
this.advance();
|
|
713
|
-
return { kind: "string", value, pos, end: this.pos };
|
|
714
|
-
}
|
|
715
|
-
if (c === "\n") {
|
|
716
|
-
return { kind: "punct", value: delim, pos, end: this.pos };
|
|
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
|
|
646
|
+
pos
|
|
905
647
|
);
|
|
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
|
|
935
|
-
);
|
|
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,322 +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
|
-
|
|
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";
|
|
1025
|
+
}
|
|
1026
|
+
|
|
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;
|
|
1040
|
+
}
|
|
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 } : {}
|
|
1057
|
+
};
|
|
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
|
+
}
|
|
1262
1073
|
}
|
|
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;
|
|
1089
|
+
}
|
|
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;
|
|
1263
1137
|
}
|
|
1264
|
-
return
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
function isIdentifier(node, name) {
|
|
1141
|
+
return typeof node === "object" && node !== null && node.type === "Identifier" && String(node.name) === name;
|
|
1265
1142
|
}
|
|
1266
1143
|
|
|
1267
1144
|
// src/scanner/scan/jsx-ancestry.ts
|
|
1268
|
-
var
|
|
1269
|
-
var
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
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);
|
|
1168
|
+
continue;
|
|
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);
|
|
1175
|
+
}
|
|
1176
|
+
return void 0;
|
|
1177
|
+
});
|
|
1178
|
+
return consts;
|
|
1179
|
+
}
|
|
1180
|
+
function staticString(node) {
|
|
1181
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
1182
|
+
return node.value;
|
|
1277
1183
|
}
|
|
1278
|
-
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1281
|
-
|
|
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 ?? "";
|
|
1282
1189
|
}
|
|
1283
|
-
return
|
|
1190
|
+
return null;
|
|
1284
1191
|
}
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
const
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
if (
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
if (
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
}
|
|
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 };
|
|
1344
1251
|
}
|
|
1345
|
-
advanceLines(i, end + 1);
|
|
1346
|
-
i = end + 1;
|
|
1347
|
-
continue;
|
|
1348
1252
|
}
|
|
1349
|
-
if (
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
if (tagName) {
|
|
1356
|
-
const attrs = parseDataAttrs(tagSource);
|
|
1357
|
-
if (attrs.length > 0) {
|
|
1358
|
-
const snapshot = ancestors.slice();
|
|
1359
|
-
for (const a of attrs) {
|
|
1360
|
-
out2.push({ kind: a.kind, id: a.id, line, ancestors: snapshot });
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
if (!isSelf) {
|
|
1364
|
-
for (const a of attrs) ancestors.push(a);
|
|
1365
|
-
stack.push({ tagName, pushed: attrs.length });
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
advanceLines(i, end + 1);
|
|
1369
|
-
i = end + 1;
|
|
1253
|
+
if (!result.resolved) {
|
|
1254
|
+
dynamicAttrs.push({
|
|
1255
|
+
kind,
|
|
1256
|
+
attrName: kind === "element" ? "data-uidex" : `data-uidex-${kind}`,
|
|
1257
|
+
line: lineAt(attr.start)
|
|
1258
|
+
});
|
|
1370
1259
|
continue;
|
|
1371
1260
|
}
|
|
1372
1261
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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);
|
|
1385
1274
|
}
|
|
1386
|
-
if (c === quote) return i + 1;
|
|
1387
|
-
i++;
|
|
1388
1275
|
}
|
|
1389
|
-
return
|
|
1276
|
+
return [...statics, ...patterns];
|
|
1390
1277
|
}
|
|
1391
|
-
function
|
|
1392
|
-
const
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
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
|
+
});
|
|
1409
1310
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
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++;
|
|
1413
1320
|
}
|
|
1414
|
-
if (cj === "{") depth++;
|
|
1415
|
-
else if (cj === "}") depth--;
|
|
1416
|
-
i++;
|
|
1417
1321
|
}
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
let i = start;
|
|
1427
|
-
while (i < N) {
|
|
1428
|
-
const c = content[i];
|
|
1429
|
-
if (c === '"' || c === "'") {
|
|
1430
|
-
i = skipString(content, i, c);
|
|
1431
|
-
continue;
|
|
1432
|
-
}
|
|
1433
|
-
if (c === "`") {
|
|
1434
|
-
i = skipTemplate(content, i);
|
|
1435
|
-
continue;
|
|
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;
|
|
1436
1330
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
i = skipTemplate(content, i);
|
|
1448
|
-
continue;
|
|
1449
|
-
}
|
|
1450
|
-
if (cj === "{") depth++;
|
|
1451
|
-
else if (cj === "}") depth--;
|
|
1452
|
-
i++;
|
|
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);
|
|
1453
1341
|
}
|
|
1454
|
-
continue;
|
|
1455
1342
|
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
}
|
|
1459
|
-
return -1;
|
|
1343
|
+
};
|
|
1344
|
+
visit(parsed.program);
|
|
1345
|
+
return { occurrences, dynamicAttrs, unannotatedInteractive, landmarks };
|
|
1460
1346
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
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) };
|
|
1468
1353
|
}
|
|
1469
|
-
|
|
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;
|
|
1470
1364
|
}
|
|
1471
|
-
function
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1477
|
-
let
|
|
1478
|
-
for (const
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
const uidex = line.match(
|
|
1482
|
-
/^@uidex\s+(page|feature|widget)\s+(\S+)(?:\s+-\s+(.+))?/
|
|
1483
|
-
);
|
|
1484
|
-
if (uidex) {
|
|
1485
|
-
kind = uidex[1];
|
|
1486
|
-
id = uidex[2];
|
|
1487
|
-
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;
|
|
1488
1375
|
continue;
|
|
1489
1376
|
}
|
|
1490
|
-
if (
|
|
1491
|
-
|
|
1492
|
-
|
|
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;
|
|
1493
1399
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
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 ?? ""));
|
|
1498
1409
|
}
|
|
1499
|
-
if (line.startsWith("@")) continue;
|
|
1500
|
-
desc.push(line);
|
|
1501
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;
|
|
1502
1432
|
return {
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
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"
|
|
1508
1439
|
};
|
|
1509
1440
|
}
|
|
1510
1441
|
function extract(files) {
|
|
1511
1442
|
return files.map((file) => {
|
|
1512
|
-
const
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
};
|
|
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);
|
|
1517
1449
|
if (exports2.length > 0) out2.metadata = exports2;
|
|
1518
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;
|
|
1519
1455
|
return out2;
|
|
1520
1456
|
});
|
|
1521
1457
|
}
|
|
1522
|
-
function extractOne(file) {
|
|
1458
|
+
function extractOne(file, parsed, out2) {
|
|
1523
1459
|
const annotations = [];
|
|
1524
|
-
const {
|
|
1525
|
-
|
|
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) {
|
|
1526
1468
|
annotations.push({
|
|
1527
1469
|
kind: occ.kind,
|
|
1528
1470
|
id: occ.id,
|
|
1529
1471
|
file: displayPath,
|
|
1530
1472
|
line: occ.line,
|
|
1531
|
-
...occ.ancestors.length > 0 ? { ancestors: occ.ancestors } : {}
|
|
1473
|
+
...occ.ancestors.length > 0 ? { ancestors: occ.ancestors } : {},
|
|
1474
|
+
...occ.span ? { span: occ.span } : {}
|
|
1532
1475
|
});
|
|
1533
1476
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
}
|
|
1552
|
-
} else if (
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
line,
|
|
1558
|
-
acceptance: parsed.acceptance
|
|
1559
|
-
});
|
|
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;
|
|
1560
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
|
+
});
|
|
1561
1510
|
}
|
|
1562
|
-
return
|
|
1511
|
+
return out2;
|
|
1563
1512
|
}
|
|
1564
1513
|
|
|
1565
1514
|
// src/scanner/scan/resolve.ts
|
|
1566
|
-
var
|
|
1515
|
+
var path4 = __toESM(require("path"), 1);
|
|
1567
1516
|
|
|
1568
1517
|
// src/shared/entities/types.ts
|
|
1569
1518
|
var ENTITY_KINDS = [
|
|
@@ -1649,13 +1598,14 @@ function createRegistry() {
|
|
|
1649
1598
|
};
|
|
1650
1599
|
const getPatternsForKind = (kind) => {
|
|
1651
1600
|
const cached = patternCache.get(kind);
|
|
1652
|
-
if (cached !== void 0)
|
|
1653
|
-
return cached;
|
|
1601
|
+
if (cached !== void 0) return cached;
|
|
1654
1602
|
const patterns = [];
|
|
1655
1603
|
for (const [key, entity] of store[kind]) {
|
|
1656
|
-
if (key.
|
|
1604
|
+
if (key.includes("*")) {
|
|
1605
|
+
const segments = key.split("*");
|
|
1657
1606
|
patterns.push({
|
|
1658
|
-
|
|
1607
|
+
segments,
|
|
1608
|
+
staticLength: segments.reduce((n, s) => n + s.length, 0),
|
|
1659
1609
|
entity
|
|
1660
1610
|
});
|
|
1661
1611
|
}
|
|
@@ -1666,13 +1616,25 @@ function createRegistry() {
|
|
|
1666
1616
|
);
|
|
1667
1617
|
return patterns;
|
|
1668
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
|
+
};
|
|
1669
1631
|
const matchPattern = (kind, id) => {
|
|
1670
1632
|
assertEntityKind(kind);
|
|
1671
1633
|
const patterns = getPatternsForKind(kind);
|
|
1672
1634
|
if (patterns.length === 0) return void 0;
|
|
1673
1635
|
let best;
|
|
1674
1636
|
for (const entry of patterns) {
|
|
1675
|
-
if (
|
|
1637
|
+
if (matchesSegments(entry.segments, id) && (best === void 0 || entry.staticLength > best.staticLength)) {
|
|
1676
1638
|
best = entry;
|
|
1677
1639
|
}
|
|
1678
1640
|
}
|
|
@@ -1826,21 +1788,9 @@ function resolveConventions(c) {
|
|
|
1826
1788
|
function kebab(str) {
|
|
1827
1789
|
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").replace(/[^a-zA-Z0-9-]/g, "").toLowerCase();
|
|
1828
1790
|
}
|
|
1829
|
-
function baseName(file) {
|
|
1830
|
-
const b =
|
|
1831
|
-
return b.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "");
|
|
1832
|
-
}
|
|
1833
|
-
var LANDMARK_RE = /<(header|nav|main|aside|footer)(\s[^>]*)?>|role=["']region["']/gi;
|
|
1834
|
-
function extractLandmarks(file) {
|
|
1835
|
-
const out2 = [];
|
|
1836
|
-
LANDMARK_RE.lastIndex = 0;
|
|
1837
|
-
let m;
|
|
1838
|
-
while ((m = LANDMARK_RE.exec(file.content)) !== null) {
|
|
1839
|
-
const tag = m[1] ?? "region";
|
|
1840
|
-
const line = 1 + file.content.slice(0, m.index).split("\n").length - 1;
|
|
1841
|
-
out2.push({ tag, line });
|
|
1842
|
-
}
|
|
1843
|
-
return out2;
|
|
1791
|
+
function baseName(file) {
|
|
1792
|
+
const b = path4.posix.basename(file);
|
|
1793
|
+
return b.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "");
|
|
1844
1794
|
}
|
|
1845
1795
|
function fileMatchesAny(displayPath, patterns) {
|
|
1846
1796
|
return patterns.some((g) => globToRegExp(g).test(displayPath));
|
|
@@ -1902,7 +1852,7 @@ function resolve2(ctx) {
|
|
|
1902
1852
|
const routes = conventions.pages === "auto" ? detectRoutes(ctx.extracted.map((e) => e.file)) : [];
|
|
1903
1853
|
const handledPageFiles = /* @__PURE__ */ new Set();
|
|
1904
1854
|
for (const route of routes) {
|
|
1905
|
-
const routeDir =
|
|
1855
|
+
const routeDir = path4.posix.dirname(route.file);
|
|
1906
1856
|
const wellKnownPath = `${routeDir}/${WELL_KNOWN_FILES.page}`;
|
|
1907
1857
|
const wellKnownExp = exportFor(wellKnownPath, "page");
|
|
1908
1858
|
const routeExp = exportFor(route.file, "page");
|
|
@@ -1956,7 +1906,7 @@ function resolve2(ctx) {
|
|
|
1956
1906
|
const dir = extractFeatureDir(ef.file.displayPath, featureGlob);
|
|
1957
1907
|
if (!dir) continue;
|
|
1958
1908
|
conventionalFeatureDirs.add(dir);
|
|
1959
|
-
const isWellKnown =
|
|
1909
|
+
const isWellKnown = path4.posix.basename(ef.file.displayPath) === WELL_KNOWN_FILES.feature;
|
|
1960
1910
|
if (isWellKnown) wellKnownFeatureFileByDir.set(dir, ef.file.displayPath);
|
|
1961
1911
|
const exp = exportFor(ef.file.displayPath, "feature");
|
|
1962
1912
|
if (exp) {
|
|
@@ -1993,7 +1943,7 @@ function resolve2(ctx) {
|
|
|
1993
1943
|
} else if (allExports.length > 0) {
|
|
1994
1944
|
exp = allExports[0].exp;
|
|
1995
1945
|
}
|
|
1996
|
-
const id = exp && typeof exp.id === "string" ? exp.id :
|
|
1946
|
+
const id = exp && typeof exp.id === "string" ? exp.id : path4.posix.basename(dir);
|
|
1997
1947
|
const meta = exp ? buildMetaFromExport(exp) : void 0;
|
|
1998
1948
|
const feature = {
|
|
1999
1949
|
kind: "feature",
|
|
@@ -2089,8 +2039,8 @@ function resolve2(ctx) {
|
|
|
2089
2039
|
}
|
|
2090
2040
|
if (conventions.regions === "landmarks") {
|
|
2091
2041
|
for (const ef of ctx.extracted) {
|
|
2092
|
-
for (const lm of
|
|
2093
|
-
const id =
|
|
2042
|
+
for (const lm of ef.landmarks ?? []) {
|
|
2043
|
+
const id = lm.tag;
|
|
2094
2044
|
if (!registry.get("region", id)) {
|
|
2095
2045
|
const meta = metaWithComposes("region", id);
|
|
2096
2046
|
const region = {
|
|
@@ -2201,7 +2151,7 @@ function resolve2(ctx) {
|
|
|
2201
2151
|
const flowExport = (ff.metadata ?? []).find(
|
|
2202
2152
|
(m) => m.kind === "flow" && typeof m.id === "string"
|
|
2203
2153
|
);
|
|
2204
|
-
const derived =
|
|
2154
|
+
const derived = flowsFromFacts(ff);
|
|
2205
2155
|
if (flowExport && typeof flowExport.id === "string" && derived.length === 1) {
|
|
2206
2156
|
const base = derived[0];
|
|
2207
2157
|
const flow = {
|
|
@@ -2256,60 +2206,21 @@ function computeScope(displayPath) {
|
|
|
2256
2206
|
}
|
|
2257
2207
|
return null;
|
|
2258
2208
|
}
|
|
2259
|
-
function
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
const line = 1 + source.slice(0, m.index).split("\n").length - 1;
|
|
2268
|
-
const after = source.slice(m.index + m[0].length);
|
|
2269
|
-
const arrow = after.match(/=>\s*\{/);
|
|
2270
|
-
if (!arrow || arrow.index === void 0) continue;
|
|
2271
|
-
const bodyStart = m.index + m[0].length + arrow.index + arrow[0].length;
|
|
2272
|
-
let depth = 1;
|
|
2273
|
-
let bodyEnd = -1;
|
|
2274
|
-
for (let i = bodyStart; i < source.length; i++) {
|
|
2275
|
-
if (source[i] === "{") depth++;
|
|
2276
|
-
else if (source[i] === "}") {
|
|
2277
|
-
depth--;
|
|
2278
|
-
if (depth === 0) {
|
|
2279
|
-
bodyEnd = i;
|
|
2280
|
-
break;
|
|
2281
|
-
}
|
|
2282
|
-
}
|
|
2283
|
-
}
|
|
2284
|
-
if (bodyEnd === -1) continue;
|
|
2285
|
-
const body = source.slice(bodyStart, bodyEnd);
|
|
2286
|
-
const touches = captureUidexIds(body);
|
|
2287
|
-
flows.push({
|
|
2288
|
-
kind: "flow",
|
|
2289
|
-
id,
|
|
2290
|
-
loc: { file: file.displayPath, line },
|
|
2291
|
-
touches: dedupe(touches.map((t) => t.id)),
|
|
2292
|
-
steps: touches.filter((t) => t.action).map((t) => ({ entityId: t.id, action: t.action }))
|
|
2293
|
-
});
|
|
2294
|
-
}
|
|
2295
|
-
return flows;
|
|
2296
|
-
}
|
|
2297
|
-
function captureUidexIds(body) {
|
|
2298
|
-
const out2 = [];
|
|
2299
|
-
const re = /uidex\(\s*(?:'([^']+)'|"([^"]+)"|`([^`$]+)`)\s*\)(?:\.(\w+)\s*\()?/g;
|
|
2300
|
-
let m;
|
|
2301
|
-
while ((m = re.exec(body)) !== null) {
|
|
2302
|
-
out2.push({ id: m[1] || m[2] || m[3], action: m[4] });
|
|
2303
|
-
}
|
|
2304
|
-
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
|
+
}));
|
|
2305
2217
|
}
|
|
2306
2218
|
function dedupe(arr) {
|
|
2307
2219
|
return Array.from(new Set(arr));
|
|
2308
2220
|
}
|
|
2309
2221
|
|
|
2310
2222
|
// src/scanner/scan/audit.ts
|
|
2311
|
-
var
|
|
2312
|
-
var MARKER_FILENAMES = ["UIDEX_PAGE.md", "UIDEX_FEATURE.md"];
|
|
2223
|
+
var path5 = __toESM(require("path"), 1);
|
|
2313
2224
|
function audit(opts) {
|
|
2314
2225
|
const diagnostics = [];
|
|
2315
2226
|
const { registry, extracted, files, config } = opts;
|
|
@@ -2319,22 +2230,15 @@ function audit(opts) {
|
|
|
2319
2230
|
const scopeLeakEnabled = config.audit?.scopeLeak ?? true;
|
|
2320
2231
|
const coverageEnabled = config.audit?.coverage ?? true;
|
|
2321
2232
|
if (opts.resolveDiagnostics) diagnostics.push(...opts.resolveDiagnostics);
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
code: "marker-md-ignored",
|
|
2328
|
-
severity: "warning",
|
|
2329
|
-
message: `Marker file "${base}" is ignored in v2; migrate to \`export const uidex\``,
|
|
2330
|
-
file: f.displayPath
|
|
2331
|
-
});
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
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);
|
|
2334
2238
|
}
|
|
2335
2239
|
if (check && opts.generated !== void 0) {
|
|
2336
2240
|
const outRel = opts.outputPath ?? config.output;
|
|
2337
|
-
const fresh =
|
|
2241
|
+
const fresh = normalizeForCheck(opts.generated);
|
|
2338
2242
|
if (opts.existingOnDisk === null || opts.existingOnDisk === void 0) {
|
|
2339
2243
|
diagnostics.push({
|
|
2340
2244
|
code: "gen-missing",
|
|
@@ -2344,7 +2248,7 @@ function audit(opts) {
|
|
|
2344
2248
|
hint: "Run `uidex scan` (without --check) to regenerate"
|
|
2345
2249
|
});
|
|
2346
2250
|
} else {
|
|
2347
|
-
const existing =
|
|
2251
|
+
const existing = normalizeForCheck(opts.existingOnDisk);
|
|
2348
2252
|
if (existing !== fresh) {
|
|
2349
2253
|
const changed = diffEntities(existing, opts.generated, registry);
|
|
2350
2254
|
const summary2 = formatChangedSummary(changed);
|
|
@@ -2358,22 +2262,6 @@ function audit(opts) {
|
|
|
2358
2262
|
}
|
|
2359
2263
|
}
|
|
2360
2264
|
}
|
|
2361
|
-
if (lint) {
|
|
2362
|
-
for (const ef of extracted) {
|
|
2363
|
-
for (const a of ef.annotations) {
|
|
2364
|
-
const migration = legacyJsdocMigration(a);
|
|
2365
|
-
if (!migration) continue;
|
|
2366
|
-
diagnostics.push({
|
|
2367
|
-
code: "legacy-jsdoc",
|
|
2368
|
-
severity: "warning",
|
|
2369
|
-
message: migration.message,
|
|
2370
|
-
file: a.file,
|
|
2371
|
-
line: a.line,
|
|
2372
|
-
hint: migration.hint
|
|
2373
|
-
});
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
2265
|
if (lint && acceptanceEnabled) {
|
|
2378
2266
|
for (const kind of ["widget", "feature", "page"]) {
|
|
2379
2267
|
for (const e of registry.list(kind)) {
|
|
@@ -2419,8 +2307,8 @@ function audit(opts) {
|
|
|
2419
2307
|
if (typeof m.id !== "string") continue;
|
|
2420
2308
|
const filePath = ef.file.displayPath;
|
|
2421
2309
|
const wellKnownName = WELL_KNOWN_FILES[m.kind];
|
|
2422
|
-
if (
|
|
2423
|
-
const dir =
|
|
2310
|
+
if (path5.posix.basename(filePath) === wellKnownName) continue;
|
|
2311
|
+
const dir = path5.posix.dirname(filePath);
|
|
2424
2312
|
const wellKnownPath = dir === "." ? wellKnownName : `${dir}/${wellKnownName}`;
|
|
2425
2313
|
if (scannedPaths.has(wellKnownPath)) continue;
|
|
2426
2314
|
const kindLabel = m.kind === "page" ? "Page" : "Feature";
|
|
@@ -2437,53 +2325,55 @@ function audit(opts) {
|
|
|
2437
2325
|
}
|
|
2438
2326
|
}
|
|
2439
2327
|
if (lint) {
|
|
2440
|
-
const
|
|
2441
|
-
|
|
2442
|
-
for (const f of files) {
|
|
2443
|
-
const templatePrefixPositions = /* @__PURE__ */ new Set();
|
|
2444
|
-
templateWithPrefixRe.lastIndex = 0;
|
|
2445
|
-
let tm;
|
|
2446
|
-
while ((tm = templateWithPrefixRe.exec(f.content)) !== null) {
|
|
2447
|
-
templatePrefixPositions.add(tm.index);
|
|
2448
|
-
}
|
|
2449
|
-
let m;
|
|
2450
|
-
dynamicAttrRe.lastIndex = 0;
|
|
2451
|
-
while ((m = dynamicAttrRe.exec(f.content)) !== null) {
|
|
2452
|
-
if (templatePrefixPositions.has(m.index)) continue;
|
|
2453
|
-
const kind = m[1] ?? "element";
|
|
2454
|
-
let line = 1;
|
|
2455
|
-
for (let i = 0; i < m.index; i++) if (f.content[i] === "\n") line++;
|
|
2456
|
-
const attrName = m[1] ? `data-uidex-${m[1]}` : "data-uidex";
|
|
2328
|
+
for (const ef of extracted) {
|
|
2329
|
+
for (const fact of ef.dynamicAttrs ?? []) {
|
|
2457
2330
|
diagnostics.push({
|
|
2458
2331
|
code: "dynamic-attr",
|
|
2459
2332
|
severity: "warning",
|
|
2460
|
-
message: `\`${attrName}={\u2026}\` uses a dynamic expression; the scanner cannot resolve the ${kind} id statically`,
|
|
2461
|
-
file:
|
|
2462
|
-
line,
|
|
2463
|
-
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)
|
|
2464
2337
|
});
|
|
2465
2338
|
}
|
|
2466
2339
|
}
|
|
2467
2340
|
}
|
|
2468
2341
|
if (lint) {
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
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
|
+
}
|
|
2487
2377
|
}
|
|
2488
2378
|
}
|
|
2489
2379
|
}
|
|
@@ -2500,12 +2390,11 @@ function audit(opts) {
|
|
|
2500
2390
|
}
|
|
2501
2391
|
}
|
|
2502
2392
|
}
|
|
2503
|
-
for (const
|
|
2504
|
-
const
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
const
|
|
2508
|
-
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() ?? "";
|
|
2509
2398
|
const primitive = byName.get(
|
|
2510
2399
|
baseName2.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
|
|
2511
2400
|
);
|
|
@@ -2513,25 +2402,37 @@ function audit(opts) {
|
|
|
2513
2402
|
const scope = primitive.scopes?.[0];
|
|
2514
2403
|
if (!scope) continue;
|
|
2515
2404
|
const [kind, id] = scope.split(":");
|
|
2516
|
-
const importerSegments =
|
|
2405
|
+
const importerSegments = displayPath.split("/");
|
|
2517
2406
|
if (importerSegments.includes(id) && importerSegments.includes(kind + "s")) {
|
|
2518
2407
|
continue;
|
|
2519
2408
|
}
|
|
2520
2409
|
if (kind === "feature" && importerSegments.includes(id)) continue;
|
|
2521
|
-
if (kind === "feature" && declaredFeatures.get(
|
|
2410
|
+
if (kind === "feature" && declaredFeatures.get(displayPath)?.has(id)) {
|
|
2522
2411
|
continue;
|
|
2523
2412
|
}
|
|
2524
2413
|
diagnostics.push({
|
|
2525
2414
|
code: "scope-leak",
|
|
2526
2415
|
severity: "warning",
|
|
2527
|
-
message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${
|
|
2528
|
-
file:
|
|
2416
|
+
message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${displayPath}`,
|
|
2417
|
+
file: displayPath,
|
|
2418
|
+
line: imp.line
|
|
2529
2419
|
});
|
|
2530
2420
|
}
|
|
2531
2421
|
}
|
|
2532
2422
|
}
|
|
2533
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
|
+
}
|
|
2534
2434
|
for (const flow of registry.list("flow")) {
|
|
2435
|
+
const callLines = factsByLoc.get(`${flow.loc.file}:${flow.loc.line}`);
|
|
2535
2436
|
for (const touchedId of flow.touches) {
|
|
2536
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);
|
|
2537
2438
|
if (!found) {
|
|
@@ -2540,54 +2441,131 @@ function audit(opts) {
|
|
|
2540
2441
|
severity: "warning",
|
|
2541
2442
|
message: `Flow "${flow.id}" references unknown entity "${touchedId}"`,
|
|
2542
2443
|
file: flow.loc.file,
|
|
2543
|
-
|
|
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)."
|
|
2544
2495
|
});
|
|
2545
2496
|
}
|
|
2546
2497
|
}
|
|
2547
2498
|
}
|
|
2548
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
|
+
}
|
|
2549
2524
|
const summary = {
|
|
2550
2525
|
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
2551
2526
|
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
2552
2527
|
};
|
|
2553
2528
|
return { diagnostics, summary };
|
|
2554
2529
|
}
|
|
2555
|
-
function
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
};
|
|
2584
|
-
default:
|
|
2585
|
-
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;
|
|
2586
2558
|
}
|
|
2587
2559
|
}
|
|
2588
2560
|
function normalizeLineEndings(s) {
|
|
2589
2561
|
return s.replace(/\r\n/g, "\n");
|
|
2590
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
|
+
}
|
|
2591
2569
|
function formatChangedSummary(change) {
|
|
2592
2570
|
const parts = [];
|
|
2593
2571
|
const fmt = (kind, names) => {
|
|
@@ -2686,62 +2664,11 @@ function extractEntitiesArray(source) {
|
|
|
2686
2664
|
}
|
|
2687
2665
|
return null;
|
|
2688
2666
|
}
|
|
2689
|
-
function findJsxOpeningEnd(src, start) {
|
|
2690
|
-
let i = start;
|
|
2691
|
-
while (i < src.length) {
|
|
2692
|
-
const ch = src[i];
|
|
2693
|
-
if (ch === ">" || ch === "/" && src[i + 1] === ">") return i;
|
|
2694
|
-
if (ch === '"' || ch === "'" || ch === "`") {
|
|
2695
|
-
i = skipString2(src, i);
|
|
2696
|
-
} else if (ch === "{") {
|
|
2697
|
-
i = skipBraces(src, i);
|
|
2698
|
-
} else {
|
|
2699
|
-
i++;
|
|
2700
|
-
}
|
|
2701
|
-
}
|
|
2702
|
-
return -1;
|
|
2703
|
-
}
|
|
2704
|
-
function skipString2(src, start) {
|
|
2705
|
-
const quote = src[start];
|
|
2706
|
-
let i = start + 1;
|
|
2707
|
-
while (i < src.length) {
|
|
2708
|
-
if (src[i] === "\\" && quote !== "`") {
|
|
2709
|
-
i += 2;
|
|
2710
|
-
continue;
|
|
2711
|
-
}
|
|
2712
|
-
if (quote === "`" && src[i] === "$" && src[i + 1] === "{") {
|
|
2713
|
-
i = skipBraces(src, i + 1);
|
|
2714
|
-
continue;
|
|
2715
|
-
}
|
|
2716
|
-
if (src[i] === quote) return i + 1;
|
|
2717
|
-
i++;
|
|
2718
|
-
}
|
|
2719
|
-
return i;
|
|
2720
|
-
}
|
|
2721
|
-
function skipBraces(src, start) {
|
|
2722
|
-
let depth = 1;
|
|
2723
|
-
let i = start + 1;
|
|
2724
|
-
while (i < src.length && depth > 0) {
|
|
2725
|
-
const ch = src[i];
|
|
2726
|
-
if (ch === "{") {
|
|
2727
|
-
depth++;
|
|
2728
|
-
i++;
|
|
2729
|
-
} else if (ch === "}") {
|
|
2730
|
-
depth--;
|
|
2731
|
-
i++;
|
|
2732
|
-
} else if (ch === '"' || ch === "'" || ch === "`") {
|
|
2733
|
-
i = skipString2(src, i);
|
|
2734
|
-
} else {
|
|
2735
|
-
i++;
|
|
2736
|
-
}
|
|
2737
|
-
}
|
|
2738
|
-
return i;
|
|
2739
|
-
}
|
|
2740
2667
|
function dynamicAttrHint(kind) {
|
|
2741
2668
|
if (kind === "region") {
|
|
2742
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`;
|
|
2743
2670
|
}
|
|
2744
|
-
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)`;
|
|
2745
2672
|
}
|
|
2746
2673
|
function stableStringify(value) {
|
|
2747
2674
|
return JSON.stringify(value, stableReplacer);
|
|
@@ -2774,9 +2701,7 @@ function replacerSorted(_key, value) {
|
|
|
2774
2701
|
}
|
|
2775
2702
|
return value;
|
|
2776
2703
|
}
|
|
2777
|
-
function emitIdUnion(name, ids
|
|
2778
|
-
if (typeMode === "loose") return `export type ${name} = string
|
|
2779
|
-
`;
|
|
2704
|
+
function emitIdUnion(name, ids) {
|
|
2780
2705
|
if (ids.length === 0) return `export type ${name} = never
|
|
2781
2706
|
`;
|
|
2782
2707
|
const sorted = [...ids].sort();
|
|
@@ -2786,12 +2711,7 @@ ${body}
|
|
|
2786
2711
|
`;
|
|
2787
2712
|
}
|
|
2788
2713
|
function emit(opts) {
|
|
2789
|
-
const {
|
|
2790
|
-
registry,
|
|
2791
|
-
gitContext,
|
|
2792
|
-
uidexImport = "uidex",
|
|
2793
|
-
typeMode = "strict"
|
|
2794
|
-
} = opts;
|
|
2714
|
+
const { registry, gitContext, uidexImport = "uidex" } = opts;
|
|
2795
2715
|
const routes = [...registry.list("route")].sort(
|
|
2796
2716
|
(a, b) => a.path.localeCompare(b.path)
|
|
2797
2717
|
);
|
|
@@ -2814,57 +2734,49 @@ function emit(opts) {
|
|
|
2814
2734
|
lines.push(
|
|
2815
2735
|
emitIdUnion(
|
|
2816
2736
|
"PageId",
|
|
2817
|
-
pages.map((e) => e.id)
|
|
2818
|
-
typeMode
|
|
2737
|
+
pages.map((e) => e.id)
|
|
2819
2738
|
)
|
|
2820
2739
|
);
|
|
2821
2740
|
lines.push(
|
|
2822
2741
|
emitIdUnion(
|
|
2823
2742
|
"FeatureId",
|
|
2824
|
-
features.map((e) => e.id)
|
|
2825
|
-
typeMode
|
|
2743
|
+
features.map((e) => e.id)
|
|
2826
2744
|
)
|
|
2827
2745
|
);
|
|
2828
2746
|
lines.push(
|
|
2829
2747
|
emitIdUnion(
|
|
2830
2748
|
"WidgetId",
|
|
2831
|
-
widgets.map((e) => e.id)
|
|
2832
|
-
typeMode
|
|
2749
|
+
widgets.map((e) => e.id)
|
|
2833
2750
|
)
|
|
2834
2751
|
);
|
|
2835
2752
|
lines.push(
|
|
2836
2753
|
emitIdUnion(
|
|
2837
2754
|
"RegionId",
|
|
2838
|
-
regions.map((e) => e.id)
|
|
2839
|
-
typeMode
|
|
2755
|
+
regions.map((e) => e.id)
|
|
2840
2756
|
)
|
|
2841
2757
|
);
|
|
2842
2758
|
lines.push(
|
|
2843
2759
|
emitIdUnion(
|
|
2844
2760
|
"ElementId",
|
|
2845
|
-
elements.map((e) => e.id)
|
|
2846
|
-
typeMode
|
|
2761
|
+
elements.map((e) => e.id)
|
|
2847
2762
|
)
|
|
2848
2763
|
);
|
|
2849
2764
|
lines.push(
|
|
2850
2765
|
emitIdUnion(
|
|
2851
2766
|
"PrimitiveId",
|
|
2852
|
-
primitives.map((e) => e.id)
|
|
2853
|
-
typeMode
|
|
2767
|
+
primitives.map((e) => e.id)
|
|
2854
2768
|
)
|
|
2855
2769
|
);
|
|
2856
2770
|
lines.push(
|
|
2857
2771
|
emitIdUnion(
|
|
2858
2772
|
"FlowId",
|
|
2859
|
-
flows.map((e) => e.id)
|
|
2860
|
-
typeMode
|
|
2773
|
+
flows.map((e) => e.id)
|
|
2861
2774
|
)
|
|
2862
2775
|
);
|
|
2863
2776
|
lines.push(
|
|
2864
2777
|
emitIdUnion(
|
|
2865
2778
|
"RouteId",
|
|
2866
|
-
routes.map((e) => e.path)
|
|
2867
|
-
typeMode
|
|
2779
|
+
routes.map((e) => e.path)
|
|
2868
2780
|
)
|
|
2869
2781
|
);
|
|
2870
2782
|
lines.push("");
|
|
@@ -2977,22 +2889,33 @@ function parseGitHubRef(ref) {
|
|
|
2977
2889
|
|
|
2978
2890
|
// src/scanner/scan/scaffold.ts
|
|
2979
2891
|
var fs3 = __toESM(require("fs"), 1);
|
|
2980
|
-
var
|
|
2892
|
+
var path6 = __toESM(require("path"), 1);
|
|
2981
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) {
|
|
2982
2904
|
const {
|
|
2983
2905
|
registry,
|
|
2984
|
-
|
|
2906
|
+
kind,
|
|
2907
|
+
id,
|
|
2985
2908
|
outDir,
|
|
2986
2909
|
force = false,
|
|
2987
2910
|
fixtureImport = "./fixtures"
|
|
2988
2911
|
} = opts;
|
|
2989
|
-
const
|
|
2990
|
-
if (!
|
|
2991
|
-
throw new Error(
|
|
2912
|
+
const entity = registry.get(kind, id);
|
|
2913
|
+
if (!entity) {
|
|
2914
|
+
throw new Error(`${capitalize(kind)} "${id}" not found in registry`);
|
|
2992
2915
|
}
|
|
2993
|
-
const criteria =
|
|
2994
|
-
const filename = `widget-${
|
|
2995
|
-
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);
|
|
2996
2919
|
if (fs3.existsSync(outputPath) && !force) {
|
|
2997
2920
|
return {
|
|
2998
2921
|
outputPath,
|
|
@@ -3001,15 +2924,14 @@ function scaffoldWidgetSpec(opts) {
|
|
|
3001
2924
|
reason: `spec already exists at ${outputPath}; pass --force to overwrite`
|
|
3002
2925
|
};
|
|
3003
2926
|
}
|
|
3004
|
-
const content = renderSpec({
|
|
3005
|
-
|
|
3006
|
-
criteria,
|
|
3007
|
-
fixtureImport
|
|
3008
|
-
});
|
|
3009
|
-
fs3.mkdirSync(path5.dirname(outputPath), { recursive: true });
|
|
2927
|
+
const content = renderSpec({ id, criteria, fixtureImport });
|
|
2928
|
+
fs3.mkdirSync(path6.dirname(outputPath), { recursive: true });
|
|
3010
2929
|
fs3.writeFileSync(outputPath, content, "utf8");
|
|
3011
2930
|
return { outputPath, written: true, skipped: false };
|
|
3012
2931
|
}
|
|
2932
|
+
function capitalize(s) {
|
|
2933
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
2934
|
+
}
|
|
3013
2935
|
function renderSpec(args) {
|
|
3014
2936
|
const lines = [];
|
|
3015
2937
|
lines.push(
|
|
@@ -3017,7 +2939,7 @@ function renderSpec(args) {
|
|
|
3017
2939
|
);
|
|
3018
2940
|
lines.push("");
|
|
3019
2941
|
lines.push(
|
|
3020
|
-
`test.describe(${JSON.stringify(args.
|
|
2942
|
+
`test.describe(${JSON.stringify(args.id)}, { tag: "@uidex:flow" }, () => {`
|
|
3021
2943
|
);
|
|
3022
2944
|
if (args.criteria.length === 0) {
|
|
3023
2945
|
lines.push(` test("TODO: add acceptance criteria", async () => {`);
|
|
@@ -3040,7 +2962,7 @@ function renderSpec(args) {
|
|
|
3040
2962
|
|
|
3041
2963
|
// src/scanner/scan/pipeline.ts
|
|
3042
2964
|
var fs4 = __toESM(require("fs"), 1);
|
|
3043
|
-
var
|
|
2965
|
+
var path7 = __toESM(require("path"), 1);
|
|
3044
2966
|
function runScan(opts = {}) {
|
|
3045
2967
|
const cwd = opts.cwd ?? process.cwd();
|
|
3046
2968
|
const configs = opts.configs ?? discover({ cwd });
|
|
@@ -3069,10 +2991,9 @@ function runOne(dc, opts) {
|
|
|
3069
2991
|
const gitContext = resolveGitContext({ cwd: configDir });
|
|
3070
2992
|
const generated = emit({
|
|
3071
2993
|
registry: resolved.registry,
|
|
3072
|
-
gitContext
|
|
3073
|
-
typeMode: config.typeMode
|
|
2994
|
+
gitContext
|
|
3074
2995
|
});
|
|
3075
|
-
const outputPath =
|
|
2996
|
+
const outputPath = path7.resolve(configDir, config.output);
|
|
3076
2997
|
const outputRel = config.output;
|
|
3077
2998
|
let existingOnDisk = null;
|
|
3078
2999
|
if (opts.check) {
|
|
@@ -3082,12 +3003,16 @@ function runOne(dc, opts) {
|
|
|
3082
3003
|
existingOnDisk = null;
|
|
3083
3004
|
}
|
|
3084
3005
|
}
|
|
3006
|
+
const hasExtractDiagnostics = [...extracted, ...extractedFlows].some(
|
|
3007
|
+
(ef) => (ef.diagnostics?.length ?? 0) > 0
|
|
3008
|
+
);
|
|
3085
3009
|
let auditResult;
|
|
3086
|
-
if (opts.check || opts.lint || resolved.diagnostics.length > 0) {
|
|
3010
|
+
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
|
|
3087
3011
|
auditResult = audit({
|
|
3088
3012
|
registry: resolved.registry,
|
|
3089
3013
|
extracted,
|
|
3090
3014
|
files: sourceFiles,
|
|
3015
|
+
flowExtracted: extractedFlows,
|
|
3091
3016
|
config,
|
|
3092
3017
|
check: opts.check,
|
|
3093
3018
|
lint: opts.lint,
|
|
@@ -3108,34 +3033,281 @@ function runOne(dc, opts) {
|
|
|
3108
3033
|
};
|
|
3109
3034
|
}
|
|
3110
3035
|
function writeScanResult(result) {
|
|
3111
|
-
fs4.mkdirSync(
|
|
3036
|
+
fs4.mkdirSync(path7.dirname(result.outputPath), { recursive: true });
|
|
3112
3037
|
fs4.writeFileSync(result.outputPath, result.generated, "utf8");
|
|
3113
3038
|
}
|
|
3114
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
|
+
|
|
3115
3287
|
// src/scanner/scan/cli.ts
|
|
3116
|
-
var
|
|
3117
|
-
var
|
|
3288
|
+
var fs8 = __toESM(require("fs"), 1);
|
|
3289
|
+
var path11 = __toESM(require("path"), 1);
|
|
3118
3290
|
|
|
3119
3291
|
// src/scanner/scan/ai/index.ts
|
|
3120
3292
|
var p = __toESM(require("@clack/prompts"), 1);
|
|
3121
3293
|
|
|
3122
3294
|
// src/scanner/scan/ai/providers/claude.ts
|
|
3123
|
-
var
|
|
3124
|
-
var
|
|
3295
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
3296
|
+
var path10 = __toESM(require("path"), 1);
|
|
3125
3297
|
|
|
3126
3298
|
// src/scanner/scan/ai/templates.ts
|
|
3127
|
-
var
|
|
3128
|
-
var
|
|
3299
|
+
var fs6 = __toESM(require("fs"), 1);
|
|
3300
|
+
var path9 = __toESM(require("path"), 1);
|
|
3129
3301
|
function templatePath(rel) {
|
|
3130
3302
|
const candidates = [
|
|
3131
|
-
|
|
3132
|
-
// dist/cli/cli.cjs
|
|
3133
|
-
|
|
3134
|
-
// 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
|
|
3135
3307
|
];
|
|
3136
3308
|
for (const c of candidates) {
|
|
3137
3309
|
try {
|
|
3138
|
-
|
|
3310
|
+
fs6.accessSync(c, fs6.constants.R_OK);
|
|
3139
3311
|
return c;
|
|
3140
3312
|
} catch {
|
|
3141
3313
|
continue;
|
|
@@ -3147,24 +3319,39 @@ function templatePath(rel) {
|
|
|
3147
3319
|
);
|
|
3148
3320
|
}
|
|
3149
3321
|
function readTemplate(rel) {
|
|
3150
|
-
return
|
|
3322
|
+
return fs6.readFileSync(templatePath(rel), "utf8");
|
|
3151
3323
|
}
|
|
3152
3324
|
|
|
3153
3325
|
// src/scanner/scan/ai/providers/claude.ts
|
|
3154
|
-
var
|
|
3155
|
-
{ dest: ".claude/
|
|
3156
|
-
{
|
|
3157
|
-
|
|
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"
|
|
3158
3345
|
];
|
|
3159
3346
|
var claudeProvider = {
|
|
3160
3347
|
id: "claude",
|
|
3161
3348
|
label: "Claude Code",
|
|
3162
|
-
description: "Adds .claude/
|
|
3349
|
+
description: "Adds .claude/skills/uidex/ skill with conventions, audit, and API references.",
|
|
3163
3350
|
async install({ cwd, force }) {
|
|
3164
3351
|
const changes = [];
|
|
3165
|
-
for (const file of
|
|
3166
|
-
const dest =
|
|
3167
|
-
const exists =
|
|
3352
|
+
for (const file of SKILL_FILES) {
|
|
3353
|
+
const dest = path10.join(cwd, file.dest);
|
|
3354
|
+
const exists = fs7.existsSync(dest);
|
|
3168
3355
|
if (exists && !force) {
|
|
3169
3356
|
changes.push({
|
|
3170
3357
|
path: file.dest,
|
|
@@ -3173,36 +3360,56 @@ var claudeProvider = {
|
|
|
3173
3360
|
});
|
|
3174
3361
|
continue;
|
|
3175
3362
|
}
|
|
3176
|
-
|
|
3177
|
-
|
|
3363
|
+
fs7.mkdirSync(path10.dirname(dest), { recursive: true });
|
|
3364
|
+
fs7.writeFileSync(dest, readTemplate(file.template));
|
|
3178
3365
|
changes.push({
|
|
3179
3366
|
path: file.dest,
|
|
3180
3367
|
action: exists ? "overwritten" : "created"
|
|
3181
3368
|
});
|
|
3182
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"));
|
|
3183
3380
|
return { changes };
|
|
3184
3381
|
},
|
|
3185
3382
|
async uninstall({ cwd }) {
|
|
3186
3383
|
const changes = [];
|
|
3187
|
-
for (const file of
|
|
3188
|
-
const dest =
|
|
3189
|
-
if (!
|
|
3384
|
+
for (const file of SKILL_FILES) {
|
|
3385
|
+
const dest = path10.join(cwd, file.dest);
|
|
3386
|
+
if (!fs7.existsSync(dest)) {
|
|
3190
3387
|
changes.push({ path: file.dest, action: "skipped", reason: "absent" });
|
|
3191
3388
|
continue;
|
|
3192
3389
|
}
|
|
3193
|
-
|
|
3390
|
+
fs7.unlinkSync(dest);
|
|
3194
3391
|
changes.push({ path: file.dest, action: "removed" });
|
|
3195
3392
|
}
|
|
3196
|
-
cleanupEmpty(
|
|
3197
|
-
cleanupEmpty(
|
|
3198
|
-
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"));
|
|
3199
3406
|
return { changes };
|
|
3200
3407
|
}
|
|
3201
3408
|
};
|
|
3202
3409
|
function cleanupEmpty(dir) {
|
|
3203
3410
|
try {
|
|
3204
|
-
const entries =
|
|
3205
|
-
if (entries.length === 0)
|
|
3411
|
+
const entries = fs7.readdirSync(dir);
|
|
3412
|
+
if (entries.length === 0) fs7.rmdirSync(dir);
|
|
3206
3413
|
} catch {
|
|
3207
3414
|
}
|
|
3208
3415
|
}
|
|
@@ -3369,6 +3576,8 @@ async function run(opts) {
|
|
|
3369
3576
|
return runScanCommand(cwd, flags, writer);
|
|
3370
3577
|
case "scaffold":
|
|
3371
3578
|
return runScaffold(cwd, positional.slice(1), flags, writer);
|
|
3579
|
+
case "rename":
|
|
3580
|
+
return runRename(cwd, positional.slice(1), flags, writer);
|
|
3372
3581
|
case "ai": {
|
|
3373
3582
|
const result = await runAiCommand({
|
|
3374
3583
|
cwd,
|
|
@@ -3395,7 +3604,8 @@ function helpText2() {
|
|
|
3395
3604
|
"Commands:",
|
|
3396
3605
|
" init Create a .uidex.json",
|
|
3397
3606
|
" scan [flags] Run the scanner pipeline",
|
|
3398
|
-
" 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)",
|
|
3399
3609
|
" ai <install|uninstall|providers> Manage AI assistant integrations",
|
|
3400
3610
|
" api <METHOD> <PATH> Call the uidex API",
|
|
3401
3611
|
" api --list Show available API routes",
|
|
@@ -3404,16 +3614,17 @@ function helpText2() {
|
|
|
3404
3614
|
"",
|
|
3405
3615
|
"Flags:",
|
|
3406
3616
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
3407
|
-
" --lint Run lint diagnostics (missing annotations, scope leak,
|
|
3617
|
+
" --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
|
|
3408
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",
|
|
3409
3620
|
" --json Emit JSON diagnostics on stdout",
|
|
3410
3621
|
" --force (scaffold) overwrite existing spec",
|
|
3411
3622
|
""
|
|
3412
3623
|
].join("\n");
|
|
3413
3624
|
}
|
|
3414
3625
|
function runInit(cwd, w) {
|
|
3415
|
-
const configPath =
|
|
3416
|
-
if (
|
|
3626
|
+
const configPath = path11.join(cwd, CONFIG_FILENAME);
|
|
3627
|
+
if (fs8.existsSync(configPath)) {
|
|
3417
3628
|
w.err(`.uidex.json already exists at ${configPath}`);
|
|
3418
3629
|
return w.result(1);
|
|
3419
3630
|
}
|
|
@@ -3422,16 +3633,16 @@ function runInit(cwd, w) {
|
|
|
3422
3633
|
sources: [{ rootDir: "src" }],
|
|
3423
3634
|
output: "src/uidex.gen.ts"
|
|
3424
3635
|
};
|
|
3425
|
-
|
|
3636
|
+
fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3426
3637
|
w.out(`Created ${configPath}`);
|
|
3427
|
-
const gitignorePath =
|
|
3638
|
+
const gitignorePath = path11.join(cwd, ".gitignore");
|
|
3428
3639
|
const entry = "*.gen.ts";
|
|
3429
|
-
if (
|
|
3430
|
-
const existing =
|
|
3640
|
+
if (fs8.existsSync(gitignorePath)) {
|
|
3641
|
+
const existing = fs8.readFileSync(gitignorePath, "utf8");
|
|
3431
3642
|
const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
|
|
3432
3643
|
if (!hasEntry) {
|
|
3433
3644
|
const needsNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
3434
|
-
|
|
3645
|
+
fs8.appendFileSync(
|
|
3435
3646
|
gitignorePath,
|
|
3436
3647
|
`${needsNewline ? "\n" : ""}${entry}
|
|
3437
3648
|
`,
|
|
@@ -3440,21 +3651,33 @@ function runInit(cwd, w) {
|
|
|
3440
3651
|
w.out(`Appended ${entry} to ${gitignorePath}`);
|
|
3441
3652
|
}
|
|
3442
3653
|
} else {
|
|
3443
|
-
|
|
3654
|
+
fs8.writeFileSync(gitignorePath, `${entry}
|
|
3444
3655
|
`, "utf8");
|
|
3445
3656
|
w.out(`Created ${gitignorePath} with ${entry}`);
|
|
3446
3657
|
}
|
|
3447
3658
|
return w.result(0);
|
|
3448
3659
|
}
|
|
3449
3660
|
function runScanCommand(cwd, flags, w) {
|
|
3450
|
-
const
|
|
3451
|
-
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);
|
|
3452
3664
|
const asJson = Boolean(flags.json);
|
|
3453
|
-
|
|
3665
|
+
let configs = discover({ cwd });
|
|
3454
3666
|
if (configs.length === 0) {
|
|
3455
3667
|
w.err(`No ${CONFIG_FILENAME} found under ${cwd}`);
|
|
3456
3668
|
return w.result(1);
|
|
3457
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
|
+
}
|
|
3458
3681
|
const results = runScan({ cwd, check, lint, configs });
|
|
3459
3682
|
if (!check) {
|
|
3460
3683
|
for (const r of results) writeScanResult(r);
|
|
@@ -3469,9 +3692,21 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3469
3692
|
{ errors: 0, warnings: 0 }
|
|
3470
3693
|
);
|
|
3471
3694
|
if (asJson) {
|
|
3472
|
-
const out2 = {
|
|
3695
|
+
const out2 = {
|
|
3696
|
+
diagnostics: allDiagnostics.map(jsonDiagnostic),
|
|
3697
|
+
summary,
|
|
3698
|
+
...fix ? { fixed, fixSkipped } : {}
|
|
3699
|
+
};
|
|
3473
3700
|
w.out(JSON.stringify(out2, null, 2));
|
|
3474
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
|
+
}
|
|
3475
3710
|
for (const r of results) {
|
|
3476
3711
|
if (check) {
|
|
3477
3712
|
w.out(`Checked ${r.outputPath}`);
|
|
@@ -3481,7 +3716,10 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3481
3716
|
for (const d of r.audit?.diagnostics ?? []) {
|
|
3482
3717
|
const loc = d.file ? `${d.file}${d.line ? `:${d.line}` : ""}` : "";
|
|
3483
3718
|
const stream = d.severity === "error" ? w.err : w.out;
|
|
3484
|
-
|
|
3719
|
+
const fixable = d.fix && !fix ? " [fixable: run with --fix]" : "";
|
|
3720
|
+
stream(
|
|
3721
|
+
`${d.severity.toUpperCase()} [${d.code}] ${loc} ${d.message}${fixable}`
|
|
3722
|
+
);
|
|
3485
3723
|
if (d.hint) stream(` hint: ${d.hint}`);
|
|
3486
3724
|
}
|
|
3487
3725
|
}
|
|
@@ -3492,20 +3730,27 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3492
3730
|
const exit = summary.errors > 0 ? 1 : 0;
|
|
3493
3731
|
return w.result(exit);
|
|
3494
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"]);
|
|
3495
3738
|
function runScaffold(cwd, args, flags, w) {
|
|
3496
3739
|
const [kind, id] = args;
|
|
3497
|
-
if (kind
|
|
3498
|
-
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]");
|
|
3499
3742
|
return w.result(1);
|
|
3500
3743
|
}
|
|
3744
|
+
const scaffoldKind = kind;
|
|
3501
3745
|
const results = runScan({ cwd });
|
|
3502
3746
|
for (const r of results) {
|
|
3503
|
-
const
|
|
3504
|
-
if (!
|
|
3505
|
-
const outDir =
|
|
3506
|
-
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({
|
|
3507
3751
|
registry: r.registry,
|
|
3508
|
-
|
|
3752
|
+
kind: scaffoldKind,
|
|
3753
|
+
id,
|
|
3509
3754
|
outDir,
|
|
3510
3755
|
force: Boolean(flags.force)
|
|
3511
3756
|
});
|
|
@@ -3516,9 +3761,43 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
3516
3761
|
w.out(`Wrote ${result.outputPath}`);
|
|
3517
3762
|
return w.result(0);
|
|
3518
3763
|
}
|
|
3519
|
-
w.err(
|
|
3764
|
+
w.err(
|
|
3765
|
+
`${scaffoldKind.charAt(0).toUpperCase() + scaffoldKind.slice(1)} "${id}" not found in registry`
|
|
3766
|
+
);
|
|
3520
3767
|
return w.result(1);
|
|
3521
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
|
+
}
|
|
3522
3801
|
function createWriter() {
|
|
3523
3802
|
let stdout = "";
|
|
3524
3803
|
let stderr = "";
|
|
@@ -3539,7 +3818,7 @@ function createWriter() {
|
|
|
3539
3818
|
CONFIG_FILENAME,
|
|
3540
3819
|
ConfigError,
|
|
3541
3820
|
DEFAULT_CONVENTIONS,
|
|
3542
|
-
|
|
3821
|
+
applyFixes,
|
|
3543
3822
|
audit,
|
|
3544
3823
|
detectRoutes,
|
|
3545
3824
|
discover,
|
|
@@ -3549,10 +3828,12 @@ function createWriter() {
|
|
|
3549
3828
|
globToRegExp,
|
|
3550
3829
|
parseConfig,
|
|
3551
3830
|
pathToId,
|
|
3831
|
+
renameEntity,
|
|
3552
3832
|
resolve,
|
|
3553
3833
|
resolveGitContext,
|
|
3554
3834
|
runCli,
|
|
3555
3835
|
runScan,
|
|
3836
|
+
scaffoldSpec,
|
|
3556
3837
|
scaffoldWidgetSpec,
|
|
3557
3838
|
validateConfig,
|
|
3558
3839
|
walk,
|