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.js
CHANGED
|
@@ -3,7 +3,6 @@ import * as fs from "fs";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
|
|
5
5
|
// src/scanner/scan/config.ts
|
|
6
|
-
var DEFAULT_TYPE_MODE = "strict";
|
|
7
6
|
var WELL_KNOWN_FILES = {
|
|
8
7
|
page: "uidex.page.ts",
|
|
9
8
|
feature: "uidex.feature.ts"
|
|
@@ -28,11 +27,9 @@ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
28
27
|
"exclude",
|
|
29
28
|
"output",
|
|
30
29
|
"flows",
|
|
31
|
-
"typeMode",
|
|
32
30
|
"audit",
|
|
33
31
|
"conventions"
|
|
34
32
|
]);
|
|
35
|
-
var ALLOWED_TYPE_MODES = /* @__PURE__ */ new Set(["strict", "loose"]);
|
|
36
33
|
var ALLOWED_SOURCE_KEYS = /* @__PURE__ */ new Set(["rootDir", "include", "exclude", "prefix"]);
|
|
37
34
|
var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
|
|
38
35
|
"primitives",
|
|
@@ -45,14 +42,14 @@ var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set(["scopeLeak", "coverage", "acce
|
|
|
45
42
|
function fail(msg) {
|
|
46
43
|
throw new ConfigError(`Invalid .uidex.json: ${msg}`);
|
|
47
44
|
}
|
|
48
|
-
function assertObject(value,
|
|
45
|
+
function assertObject(value, path12) {
|
|
49
46
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
50
|
-
fail(`${
|
|
47
|
+
fail(`${path12} must be an object`);
|
|
51
48
|
}
|
|
52
49
|
}
|
|
53
|
-
function assertStringArray(value,
|
|
50
|
+
function assertStringArray(value, path12) {
|
|
54
51
|
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
|
|
55
|
-
fail(`${
|
|
52
|
+
fail(`${path12} must be a string[]`);
|
|
56
53
|
}
|
|
57
54
|
}
|
|
58
55
|
function validateConfig(raw) {
|
|
@@ -100,11 +97,6 @@ function validateConfig(raw) {
|
|
|
100
97
|
}
|
|
101
98
|
if (raw.exclude !== void 0) assertStringArray(raw.exclude, `exclude`);
|
|
102
99
|
if (raw.flows !== void 0) assertStringArray(raw.flows, `flows`);
|
|
103
|
-
if (raw.typeMode !== void 0) {
|
|
104
|
-
if (typeof raw.typeMode !== "string" || !ALLOWED_TYPE_MODES.has(raw.typeMode)) {
|
|
105
|
-
fail(`"typeMode" must be "strict" or "loose"`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
100
|
if (raw.audit !== void 0) {
|
|
109
101
|
assertObject(raw.audit, "audit");
|
|
110
102
|
for (const key of Object.keys(raw.audit)) {
|
|
@@ -143,7 +135,6 @@ function validateConfig(raw) {
|
|
|
143
135
|
exclude: raw.exclude,
|
|
144
136
|
output: raw.output,
|
|
145
137
|
flows: raw.flows,
|
|
146
|
-
typeMode: raw.typeMode ?? DEFAULT_TYPE_MODE,
|
|
147
138
|
audit: raw.audit,
|
|
148
139
|
conventions: raw.conventions
|
|
149
140
|
};
|
|
@@ -350,6 +341,90 @@ function* walkDir(root, dir) {
|
|
|
350
341
|
}
|
|
351
342
|
}
|
|
352
343
|
|
|
344
|
+
// src/scanner/scan/ast.ts
|
|
345
|
+
import * as path3 from "path";
|
|
346
|
+
import { parseSync } from "oxc-parser";
|
|
347
|
+
function langFor(sourcePath) {
|
|
348
|
+
switch (path3.extname(sourcePath)) {
|
|
349
|
+
case ".tsx":
|
|
350
|
+
return "tsx";
|
|
351
|
+
case ".ts":
|
|
352
|
+
case ".mts":
|
|
353
|
+
case ".cts":
|
|
354
|
+
return "ts";
|
|
355
|
+
default:
|
|
356
|
+
return "jsx";
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function makeLineAt(content) {
|
|
360
|
+
const starts = [0];
|
|
361
|
+
for (let i = 0; i < content.length; i++) {
|
|
362
|
+
if (content[i] === "\n") starts.push(i + 1);
|
|
363
|
+
}
|
|
364
|
+
return (offset) => {
|
|
365
|
+
let lo = 0;
|
|
366
|
+
let hi = starts.length - 1;
|
|
367
|
+
while (lo < hi) {
|
|
368
|
+
const mid = lo + hi + 1 >> 1;
|
|
369
|
+
if (starts[mid] <= offset) lo = mid;
|
|
370
|
+
else hi = mid - 1;
|
|
371
|
+
}
|
|
372
|
+
return lo + 1;
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function parseSource(file) {
|
|
376
|
+
const lineAt = makeLineAt(file.content);
|
|
377
|
+
try {
|
|
378
|
+
const result = parseSync(file.sourcePath, file.content, {
|
|
379
|
+
lang: langFor(file.sourcePath),
|
|
380
|
+
sourceType: "module"
|
|
381
|
+
});
|
|
382
|
+
return {
|
|
383
|
+
program: result.program,
|
|
384
|
+
hasErrors: result.errors.length > 0,
|
|
385
|
+
comments: result.comments ?? [],
|
|
386
|
+
lineAt
|
|
387
|
+
};
|
|
388
|
+
} catch {
|
|
389
|
+
return { program: null, hasErrors: true, comments: [], lineAt };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function isNode(value) {
|
|
393
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
394
|
+
}
|
|
395
|
+
function walkAst(root, visit) {
|
|
396
|
+
if (!isNode(root)) return;
|
|
397
|
+
if (visit(root) === false) return;
|
|
398
|
+
for (const key of Object.keys(root)) {
|
|
399
|
+
if (key === "type" || key === "start" || key === "end") continue;
|
|
400
|
+
const value = root[key];
|
|
401
|
+
if (Array.isArray(value)) {
|
|
402
|
+
for (const item of value) walkAst(item, visit);
|
|
403
|
+
} else if (isNode(value)) {
|
|
404
|
+
walkAst(value, visit);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function unwrapTsExpression(expr) {
|
|
409
|
+
let node = expr;
|
|
410
|
+
for (; ; ) {
|
|
411
|
+
switch (node.type) {
|
|
412
|
+
case "TSAsExpression":
|
|
413
|
+
case "TSSatisfiesExpression":
|
|
414
|
+
case "TSNonNullExpression":
|
|
415
|
+
case "TSTypeAssertion":
|
|
416
|
+
case "ParenthesizedExpression": {
|
|
417
|
+
const inner = node.expression;
|
|
418
|
+
if (!isNode(inner)) return node;
|
|
419
|
+
node = inner;
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
default:
|
|
423
|
+
return node;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
353
428
|
// src/scanner/scan/extract-uidex-export.ts
|
|
354
429
|
var KIND_DISCRIMINATORS = [
|
|
355
430
|
"page",
|
|
@@ -387,6 +462,16 @@ var FALSEABLE = /* @__PURE__ */ new Set([
|
|
|
387
462
|
"primitive",
|
|
388
463
|
"region"
|
|
389
464
|
]);
|
|
465
|
+
var SATISFIES_NAMES = {
|
|
466
|
+
page: "Page",
|
|
467
|
+
feature: "Feature",
|
|
468
|
+
primitive: "Primitive",
|
|
469
|
+
widget: "Widget",
|
|
470
|
+
region: "Region",
|
|
471
|
+
flow: "Flow",
|
|
472
|
+
notFlow: "NotFlow"
|
|
473
|
+
};
|
|
474
|
+
var KNOWN_SATISFIES = new Set(Object.values(SATISFIES_NAMES));
|
|
390
475
|
var ExtractError = class extends Error {
|
|
391
476
|
code;
|
|
392
477
|
hint;
|
|
@@ -398,649 +483,285 @@ var ExtractError = class extends Error {
|
|
|
398
483
|
this.hint = hint;
|
|
399
484
|
}
|
|
400
485
|
};
|
|
401
|
-
function extractUidexExports(file) {
|
|
486
|
+
function extractUidexExports(file, parsed) {
|
|
402
487
|
const exports = [];
|
|
403
488
|
const diagnostics = [];
|
|
404
489
|
const { content, displayPath } = file;
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
} catch (e) {
|
|
416
|
-
if (e instanceof ExtractError) {
|
|
417
|
-
diagnostics.push({
|
|
418
|
-
code: e.code,
|
|
419
|
-
severity: "error",
|
|
420
|
-
message: e.message,
|
|
421
|
-
file: displayPath,
|
|
422
|
-
line: e.pos.line,
|
|
423
|
-
hint: e.hint
|
|
424
|
-
});
|
|
425
|
-
} else {
|
|
426
|
-
throw e;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
return { exports, diagnostics };
|
|
431
|
-
}
|
|
432
|
-
var HEADER_RE = /(?:^|\n)[\t ]*export\s+const\s+uidex\b(?:\s*:\s*[^=\n]+?)?\s*=\s*/g;
|
|
433
|
-
function findExportHeaders(content) {
|
|
434
|
-
const out2 = [];
|
|
435
|
-
HEADER_RE.lastIndex = 0;
|
|
436
|
-
let m;
|
|
437
|
-
while ((m = HEADER_RE.exec(content)) !== null) {
|
|
438
|
-
const leadingNewline = m[0].startsWith("\n") ? 1 : 0;
|
|
439
|
-
const headerOffset = m.index + leadingNewline;
|
|
440
|
-
const exprStart = m.index + m[0].length;
|
|
441
|
-
if (isInsideCommentOrString(content, headerOffset)) continue;
|
|
442
|
-
out2.push({
|
|
443
|
-
headerPos: posAt(content, headerOffset),
|
|
444
|
-
exprStart
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
return out2;
|
|
448
|
-
}
|
|
449
|
-
function isInsideCommentOrString(content, target) {
|
|
450
|
-
let i = 0;
|
|
451
|
-
let inLineComment = false;
|
|
452
|
-
let inBlockComment = false;
|
|
453
|
-
let stringDelim = null;
|
|
454
|
-
let inTemplate = false;
|
|
455
|
-
let templateDepth = 0;
|
|
456
|
-
while (i < target) {
|
|
457
|
-
const c = content[i];
|
|
458
|
-
const n = content[i + 1];
|
|
459
|
-
if (inLineComment) {
|
|
460
|
-
if (c === "\n") inLineComment = false;
|
|
461
|
-
i++;
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
if (inBlockComment) {
|
|
465
|
-
if (c === "*" && n === "/") {
|
|
466
|
-
inBlockComment = false;
|
|
467
|
-
i += 2;
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
i++;
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
if (stringDelim !== null) {
|
|
474
|
-
if (c === "\\") {
|
|
475
|
-
i += 2;
|
|
476
|
-
continue;
|
|
477
|
-
}
|
|
478
|
-
if (c === stringDelim) stringDelim = null;
|
|
479
|
-
i++;
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
if (inTemplate) {
|
|
483
|
-
if (c === "\\") {
|
|
484
|
-
i += 2;
|
|
490
|
+
const p2 = parsed ?? parseSource(file);
|
|
491
|
+
if (p2.program === null) return { exports, diagnostics };
|
|
492
|
+
for (const stmt of p2.program.body) {
|
|
493
|
+
if (stmt.type !== "ExportNamedDeclaration") continue;
|
|
494
|
+
const decl = stmt.declaration;
|
|
495
|
+
if (!isNode2(decl) || decl.type !== "VariableDeclaration") continue;
|
|
496
|
+
if (decl.kind !== "const") continue;
|
|
497
|
+
for (const declarator of decl.declarations ?? []) {
|
|
498
|
+
const id = declarator.id;
|
|
499
|
+
if (!id || id.type !== "Identifier" || String(id.name) !== "uidex") {
|
|
485
500
|
continue;
|
|
486
501
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
if (templateDepth > 0 && c === "}") {
|
|
498
|
-
templateDepth--;
|
|
499
|
-
i++;
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
i++;
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
if (c === "/" && n === "/") {
|
|
506
|
-
inLineComment = true;
|
|
507
|
-
i += 2;
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
if (c === "/" && n === "*") {
|
|
511
|
-
inBlockComment = true;
|
|
512
|
-
i += 2;
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
if (c === '"' || c === "'") {
|
|
516
|
-
stringDelim = c;
|
|
517
|
-
i++;
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
if (c === "`") {
|
|
521
|
-
inTemplate = true;
|
|
522
|
-
i++;
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
i++;
|
|
526
|
-
}
|
|
527
|
-
return inLineComment || inBlockComment || stringDelim !== null || inTemplate;
|
|
528
|
-
}
|
|
529
|
-
var Tokenizer = class {
|
|
530
|
-
constructor(src, start) {
|
|
531
|
-
this.src = src;
|
|
532
|
-
this.pos = start;
|
|
533
|
-
let line = 1;
|
|
534
|
-
let lineStart = 0;
|
|
535
|
-
for (let i = 0; i < start; i++) {
|
|
536
|
-
if (src[i] === "\n") {
|
|
537
|
-
line++;
|
|
538
|
-
lineStart = i + 1;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
this.line = line;
|
|
542
|
-
this.lineStart = lineStart;
|
|
543
|
-
}
|
|
544
|
-
src;
|
|
545
|
-
pos;
|
|
546
|
-
line;
|
|
547
|
-
lineStart;
|
|
548
|
-
currentPos() {
|
|
549
|
-
return {
|
|
550
|
-
offset: this.pos,
|
|
551
|
-
line: this.line,
|
|
552
|
-
column: this.pos - this.lineStart + 1
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
advance(n = 1) {
|
|
556
|
-
for (let i = 0; i < n; i++) {
|
|
557
|
-
if (this.pos < this.src.length && this.src[this.pos] === "\n") {
|
|
558
|
-
this.line++;
|
|
559
|
-
this.lineStart = this.pos + 1;
|
|
560
|
-
}
|
|
561
|
-
this.pos++;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
skipTrivia() {
|
|
565
|
-
while (this.pos < this.src.length) {
|
|
566
|
-
const c = this.src[this.pos];
|
|
567
|
-
const n = this.src[this.pos + 1];
|
|
568
|
-
if (c === " " || c === " " || c === "\r" || c === "\n") {
|
|
569
|
-
this.advance();
|
|
570
|
-
continue;
|
|
571
|
-
}
|
|
572
|
-
if (c === "/" && n === "/") {
|
|
573
|
-
while (this.pos < this.src.length && this.src[this.pos] !== "\n") {
|
|
574
|
-
this.advance();
|
|
502
|
+
const headerPos = posAt(content, stmt.start, p2);
|
|
503
|
+
try {
|
|
504
|
+
const init = declarator.init;
|
|
505
|
+
if (!isNode2(init)) {
|
|
506
|
+
throw new ExtractError(
|
|
507
|
+
"uidex-export-invalid-literal",
|
|
508
|
+
"`export const uidex` must be assigned an object literal.",
|
|
509
|
+
headerPos
|
|
510
|
+
);
|
|
575
511
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
512
|
+
const value = toLitValue(unwrapTsExpression(init), content, p2);
|
|
513
|
+
const metadata = buildMetadata(
|
|
514
|
+
value,
|
|
515
|
+
displayPath,
|
|
516
|
+
file.sourcePath,
|
|
517
|
+
headerPos,
|
|
518
|
+
diagnostics
|
|
519
|
+
);
|
|
520
|
+
metadata.span = statementSpan(stmt, content);
|
|
521
|
+
checkSatisfies(init, metadata, displayPath, p2, diagnostics);
|
|
522
|
+
exports.push(metadata);
|
|
523
|
+
} catch (e) {
|
|
524
|
+
if (e instanceof ExtractError) {
|
|
525
|
+
diagnostics.push({
|
|
526
|
+
code: e.code,
|
|
527
|
+
severity: "error",
|
|
528
|
+
message: e.message,
|
|
529
|
+
file: displayPath,
|
|
530
|
+
line: e.pos.line,
|
|
531
|
+
hint: e.hint
|
|
532
|
+
});
|
|
533
|
+
} else {
|
|
534
|
+
throw e;
|
|
586
535
|
}
|
|
587
|
-
continue;
|
|
588
536
|
}
|
|
589
|
-
break;
|
|
590
537
|
}
|
|
591
538
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
case "]":
|
|
610
|
-
this.advance();
|
|
611
|
-
return { kind: "rbracket", value: c, pos, end: this.pos };
|
|
612
|
-
case "(":
|
|
613
|
-
this.advance();
|
|
614
|
-
return { kind: "lparen", value: c, pos, end: this.pos };
|
|
615
|
-
case ")":
|
|
616
|
-
this.advance();
|
|
617
|
-
return { kind: "rparen", value: c, pos, end: this.pos };
|
|
618
|
-
case ",":
|
|
619
|
-
this.advance();
|
|
620
|
-
return { kind: "comma", value: c, pos, end: this.pos };
|
|
621
|
-
case ":":
|
|
622
|
-
this.advance();
|
|
623
|
-
return { kind: "colon", value: c, pos, end: this.pos };
|
|
624
|
-
}
|
|
625
|
-
if (c === "." && this.src[this.pos + 1] === "." && this.src[this.pos + 2] === ".") {
|
|
626
|
-
this.advance(3);
|
|
627
|
-
return { kind: "spread", value: "...", pos, end: this.pos };
|
|
628
|
-
}
|
|
629
|
-
if (c === '"' || c === "'") {
|
|
630
|
-
return this.readString(pos, c);
|
|
631
|
-
}
|
|
632
|
-
if (c === "`") {
|
|
633
|
-
return this.readTemplate(pos);
|
|
634
|
-
}
|
|
635
|
-
if (isDigit(c) || c === "-" && isDigit(this.src[this.pos + 1])) {
|
|
636
|
-
return this.readNumber(pos);
|
|
637
|
-
}
|
|
638
|
-
if (isIdentStart(c)) {
|
|
639
|
-
return this.readIdent(pos);
|
|
640
|
-
}
|
|
641
|
-
this.advance();
|
|
642
|
-
return { kind: "punct", value: c, pos, end: this.pos };
|
|
643
|
-
}
|
|
644
|
-
readString(pos, delim) {
|
|
645
|
-
this.advance();
|
|
646
|
-
let value = "";
|
|
647
|
-
while (this.pos < this.src.length) {
|
|
648
|
-
const c = this.src[this.pos];
|
|
649
|
-
if (c === "\\") {
|
|
650
|
-
const esc = this.src[this.pos + 1];
|
|
651
|
-
this.advance(2);
|
|
652
|
-
value += decodeEscape(esc);
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
if (c === delim) {
|
|
656
|
-
this.advance();
|
|
657
|
-
return { kind: "string", value, pos, end: this.pos };
|
|
658
|
-
}
|
|
659
|
-
if (c === "\n") {
|
|
660
|
-
return { kind: "punct", value: delim, pos, end: this.pos };
|
|
661
|
-
}
|
|
662
|
-
value += c;
|
|
663
|
-
this.advance();
|
|
664
|
-
}
|
|
665
|
-
return { kind: "punct", value: delim, pos, end: this.pos };
|
|
666
|
-
}
|
|
667
|
-
readTemplate(pos) {
|
|
668
|
-
this.advance();
|
|
669
|
-
let value = "";
|
|
670
|
-
let hasExpression = false;
|
|
671
|
-
while (this.pos < this.src.length) {
|
|
672
|
-
const c = this.src[this.pos];
|
|
673
|
-
const n = this.src[this.pos + 1];
|
|
674
|
-
if (c === "\\") {
|
|
675
|
-
const esc = this.src[this.pos + 1];
|
|
676
|
-
this.advance(2);
|
|
677
|
-
value += decodeEscape(esc);
|
|
678
|
-
continue;
|
|
679
|
-
}
|
|
680
|
-
if (c === "$" && n === "{") {
|
|
681
|
-
hasExpression = true;
|
|
682
|
-
this.advance(2);
|
|
683
|
-
let depth = 1;
|
|
684
|
-
while (this.pos < this.src.length && depth > 0) {
|
|
685
|
-
const ch = this.src[this.pos];
|
|
686
|
-
if (ch === "{") depth++;
|
|
687
|
-
else if (ch === "}") depth--;
|
|
688
|
-
this.advance();
|
|
539
|
+
return { exports, diagnostics };
|
|
540
|
+
}
|
|
541
|
+
function toLitValue(node, content, p2) {
|
|
542
|
+
const unwrapped = unwrapTsExpression(node);
|
|
543
|
+
const pos = posAt(content, unwrapped.start, p2);
|
|
544
|
+
const span = { start: unwrapped.start, end: unwrapped.end };
|
|
545
|
+
switch (unwrapped.type) {
|
|
546
|
+
case "Literal": {
|
|
547
|
+
const v = unwrapped.value;
|
|
548
|
+
if (typeof v === "string") return { kind: "string", value: v, pos, span };
|
|
549
|
+
if (typeof v === "number") {
|
|
550
|
+
if (!Number.isFinite(v)) {
|
|
551
|
+
throw new ExtractError(
|
|
552
|
+
"uidex-export-invalid-literal",
|
|
553
|
+
`Invalid numeric literal in \`export const uidex\`.`,
|
|
554
|
+
pos
|
|
555
|
+
);
|
|
689
556
|
}
|
|
690
|
-
|
|
557
|
+
return { kind: "number", value: v, pos, span };
|
|
691
558
|
}
|
|
692
|
-
if (
|
|
693
|
-
|
|
694
|
-
if (hasExpression) {
|
|
695
|
-
return { kind: "template", value, pos, end: this.pos };
|
|
696
|
-
}
|
|
697
|
-
return { kind: "string", value, pos, end: this.pos };
|
|
559
|
+
if (typeof v === "boolean") {
|
|
560
|
+
return { kind: "boolean", value: v, pos, span };
|
|
698
561
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}
|
|
702
|
-
return { kind: "template", value, pos, end: this.pos };
|
|
703
|
-
}
|
|
704
|
-
readNumber(pos) {
|
|
705
|
-
const start = this.pos;
|
|
706
|
-
if (this.src[this.pos] === "-") this.advance();
|
|
707
|
-
while (this.pos < this.src.length && isDigit(this.src[this.pos])) {
|
|
708
|
-
this.advance();
|
|
709
|
-
}
|
|
710
|
-
if (this.src[this.pos] === ".") {
|
|
711
|
-
this.advance();
|
|
712
|
-
while (this.pos < this.src.length && isDigit(this.src[this.pos])) {
|
|
713
|
-
this.advance();
|
|
562
|
+
if (v === null && unwrapped.raw === "null") {
|
|
563
|
+
return { kind: "null", pos, span };
|
|
714
564
|
}
|
|
565
|
+
throw new ExtractError(
|
|
566
|
+
"uidex-export-invalid-literal",
|
|
567
|
+
`Unsupported literal in \`export const uidex\`; only strings, numbers, booleans, and null are allowed.`,
|
|
568
|
+
pos
|
|
569
|
+
);
|
|
715
570
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
if (
|
|
719
|
-
|
|
571
|
+
case "UnaryExpression": {
|
|
572
|
+
const arg = unwrapped.argument;
|
|
573
|
+
if (unwrapped.operator === "-" && isNode2(arg) && arg.type === "Literal" && typeof arg.value === "number") {
|
|
574
|
+
return { kind: "number", value: -arg.value, pos, span };
|
|
720
575
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
return { kind: "number", value, pos, end: this.pos };
|
|
727
|
-
}
|
|
728
|
-
readIdent(pos) {
|
|
729
|
-
const start = this.pos;
|
|
730
|
-
while (this.pos < this.src.length && isIdentPart(this.src[this.pos])) {
|
|
731
|
-
this.advance();
|
|
576
|
+
throw new ExtractError(
|
|
577
|
+
"uidex-export-invalid-literal",
|
|
578
|
+
"Unary expressions are not allowed in `export const uidex`.",
|
|
579
|
+
pos
|
|
580
|
+
);
|
|
732
581
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
};
|
|
737
|
-
function isDigit(c) {
|
|
738
|
-
return c !== void 0 && c >= "0" && c <= "9";
|
|
739
|
-
}
|
|
740
|
-
function isIdentStart(c) {
|
|
741
|
-
if (c === void 0) return false;
|
|
742
|
-
return c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_" || c === "$";
|
|
743
|
-
}
|
|
744
|
-
function isIdentPart(c) {
|
|
745
|
-
return isIdentStart(c) || isDigit(c);
|
|
746
|
-
}
|
|
747
|
-
function decodeEscape(esc) {
|
|
748
|
-
switch (esc) {
|
|
749
|
-
case "n":
|
|
750
|
-
return "\n";
|
|
751
|
-
case "t":
|
|
752
|
-
return " ";
|
|
753
|
-
case "r":
|
|
754
|
-
return "\r";
|
|
755
|
-
case "\\":
|
|
756
|
-
return "\\";
|
|
757
|
-
case "'":
|
|
758
|
-
return "'";
|
|
759
|
-
case '"':
|
|
760
|
-
return '"';
|
|
761
|
-
case "`":
|
|
762
|
-
return "`";
|
|
763
|
-
case "0":
|
|
764
|
-
return "\0";
|
|
765
|
-
case "b":
|
|
766
|
-
return "\b";
|
|
767
|
-
case "f":
|
|
768
|
-
return "\f";
|
|
769
|
-
case "v":
|
|
770
|
-
return "\v";
|
|
771
|
-
default:
|
|
772
|
-
return esc ?? "";
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
function parseExpression(content, start) {
|
|
776
|
-
const tokenizer = new Tokenizer(content, start);
|
|
777
|
-
const parser = new Parser(tokenizer);
|
|
778
|
-
const value = parser.parseValue();
|
|
779
|
-
parser.consumeTrailingAssertions();
|
|
780
|
-
return value;
|
|
781
|
-
}
|
|
782
|
-
var Parser = class {
|
|
783
|
-
constructor(tok) {
|
|
784
|
-
this.tok = tok;
|
|
785
|
-
}
|
|
786
|
-
tok;
|
|
787
|
-
lookahead = null;
|
|
788
|
-
peek() {
|
|
789
|
-
if (this.lookahead === null) this.lookahead = this.tok.next();
|
|
790
|
-
return this.lookahead;
|
|
791
|
-
}
|
|
792
|
-
consume() {
|
|
793
|
-
const t = this.peek();
|
|
794
|
-
this.lookahead = null;
|
|
795
|
-
return t;
|
|
796
|
-
}
|
|
797
|
-
parseValue() {
|
|
798
|
-
const t = this.peek();
|
|
799
|
-
switch (t.kind) {
|
|
800
|
-
case "lbrace":
|
|
801
|
-
return this.parseObject();
|
|
802
|
-
case "lbracket":
|
|
803
|
-
return this.parseArray();
|
|
804
|
-
case "string":
|
|
805
|
-
this.consume();
|
|
806
|
-
return { kind: "string", value: t.value, pos: t.pos };
|
|
807
|
-
case "template":
|
|
582
|
+
case "TemplateLiteral": {
|
|
583
|
+
const expressions = unwrapped.expressions ?? [];
|
|
584
|
+
if (expressions.length > 0) {
|
|
808
585
|
throw new ExtractError(
|
|
809
586
|
"uidex-export-invalid-literal",
|
|
810
587
|
"Template literal with expression parts is not allowed in `export const uidex`; use a plain string literal.",
|
|
811
|
-
|
|
812
|
-
);
|
|
813
|
-
case "number": {
|
|
814
|
-
this.consume();
|
|
815
|
-
const n = Number(t.value);
|
|
816
|
-
if (!Number.isFinite(n)) {
|
|
817
|
-
throw new ExtractError(
|
|
818
|
-
"uidex-export-invalid-literal",
|
|
819
|
-
`Invalid numeric literal "${t.value}" in \`export const uidex\`.`,
|
|
820
|
-
t.pos
|
|
821
|
-
);
|
|
822
|
-
}
|
|
823
|
-
return { kind: "number", value: n, pos: t.pos };
|
|
824
|
-
}
|
|
825
|
-
case "ident":
|
|
826
|
-
if (t.value === "true" || t.value === "false") {
|
|
827
|
-
this.consume();
|
|
828
|
-
return {
|
|
829
|
-
kind: "boolean",
|
|
830
|
-
value: t.value === "true",
|
|
831
|
-
pos: t.pos
|
|
832
|
-
};
|
|
833
|
-
}
|
|
834
|
-
if (t.value === "null") {
|
|
835
|
-
this.consume();
|
|
836
|
-
return { kind: "null", pos: t.pos };
|
|
837
|
-
}
|
|
838
|
-
if (t.value === "undefined") {
|
|
839
|
-
throw new ExtractError(
|
|
840
|
-
"uidex-export-invalid-literal",
|
|
841
|
-
"`undefined` is not allowed as a value in `export const uidex`; omit the field instead.",
|
|
842
|
-
t.pos
|
|
843
|
-
);
|
|
844
|
-
}
|
|
845
|
-
throw new ExtractError(
|
|
846
|
-
"uidex-export-invalid-literal",
|
|
847
|
-
`Identifier reference "${t.value}" is not allowed in \`export const uidex\`; the right-hand side must be a plain literal.`,
|
|
848
|
-
t.pos
|
|
588
|
+
pos
|
|
849
589
|
);
|
|
850
|
-
case "spread":
|
|
851
|
-
throw new ExtractError(
|
|
852
|
-
"uidex-export-invalid-literal",
|
|
853
|
-
"Spread (`...`) is not allowed in `export const uidex`; the right-hand side must be a plain literal.",
|
|
854
|
-
t.pos
|
|
855
|
-
);
|
|
856
|
-
case "lparen":
|
|
857
|
-
throw new ExtractError(
|
|
858
|
-
"uidex-export-invalid-literal",
|
|
859
|
-
"Parenthesised or grouped expressions are not allowed in `export const uidex`.",
|
|
860
|
-
t.pos
|
|
861
|
-
);
|
|
862
|
-
case "punct":
|
|
863
|
-
throw new ExtractError(
|
|
864
|
-
"uidex-export-invalid-literal",
|
|
865
|
-
`Unexpected token "${t.value}" in \`export const uidex\`.`,
|
|
866
|
-
t.pos
|
|
867
|
-
);
|
|
868
|
-
case "eof":
|
|
869
|
-
throw new ExtractError(
|
|
870
|
-
"uidex-export-invalid-literal",
|
|
871
|
-
"Expected a value for `export const uidex` but reached end of file.",
|
|
872
|
-
t.pos
|
|
873
|
-
);
|
|
874
|
-
default:
|
|
875
|
-
throw new ExtractError(
|
|
876
|
-
"uidex-export-invalid-literal",
|
|
877
|
-
`Unexpected token in \`export const uidex\`.`,
|
|
878
|
-
t.pos
|
|
879
|
-
);
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
parseObject() {
|
|
883
|
-
const open = this.consume();
|
|
884
|
-
const entries = [];
|
|
885
|
-
const seen = /* @__PURE__ */ new Set();
|
|
886
|
-
while (true) {
|
|
887
|
-
const t = this.peek();
|
|
888
|
-
if (t.kind === "rbrace") {
|
|
889
|
-
this.consume();
|
|
890
|
-
break;
|
|
891
590
|
}
|
|
892
|
-
|
|
591
|
+
const quasis = unwrapped.quasis ?? [];
|
|
592
|
+
const cooked = quasis[0]?.value?.cooked ?? "";
|
|
593
|
+
return { kind: "string", value: cooked, pos, span };
|
|
594
|
+
}
|
|
595
|
+
case "Identifier": {
|
|
596
|
+
const name = String(unwrapped.name);
|
|
597
|
+
if (name === "undefined") {
|
|
893
598
|
throw new ExtractError(
|
|
894
599
|
"uidex-export-invalid-literal",
|
|
895
|
-
"
|
|
896
|
-
|
|
600
|
+
"`undefined` is not allowed as a value in `export const uidex`; omit the field instead.",
|
|
601
|
+
pos
|
|
897
602
|
);
|
|
898
603
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
604
|
+
throw new ExtractError(
|
|
605
|
+
"uidex-export-invalid-literal",
|
|
606
|
+
`Identifier reference "${name}" is not allowed in \`export const uidex\`; the right-hand side must be a plain literal.`,
|
|
607
|
+
pos
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
case "ObjectExpression":
|
|
611
|
+
return objectLit(unwrapped, content, p2, pos, span);
|
|
612
|
+
case "ArrayExpression":
|
|
613
|
+
return arrayLit(unwrapped, content, p2, pos, span);
|
|
614
|
+
case "SpreadElement":
|
|
615
|
+
throw new ExtractError(
|
|
616
|
+
"uidex-export-invalid-literal",
|
|
617
|
+
"Spread (`...`) is not allowed in `export const uidex`; the right-hand side must be a plain literal.",
|
|
618
|
+
pos
|
|
619
|
+
);
|
|
620
|
+
case "CallExpression":
|
|
621
|
+
throw new ExtractError(
|
|
622
|
+
"uidex-export-invalid-literal",
|
|
623
|
+
"Function calls are not allowed in `export const uidex`; the right-hand side must be a plain literal.",
|
|
624
|
+
pos
|
|
625
|
+
);
|
|
626
|
+
case "ConditionalExpression":
|
|
627
|
+
throw new ExtractError(
|
|
628
|
+
"uidex-export-invalid-literal",
|
|
629
|
+
"Conditional expressions are not allowed in `export const uidex`; the right-hand side must be a plain literal.",
|
|
630
|
+
pos
|
|
631
|
+
);
|
|
632
|
+
default:
|
|
633
|
+
throw new ExtractError(
|
|
634
|
+
"uidex-export-invalid-literal",
|
|
635
|
+
`Computed expressions are not allowed in \`export const uidex\`; the right-hand side must be a plain literal.`,
|
|
636
|
+
pos
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function objectLit(node, content, p2, pos, span) {
|
|
641
|
+
const entries = [];
|
|
642
|
+
const seen = /* @__PURE__ */ new Set();
|
|
643
|
+
for (const prop of node.properties ?? []) {
|
|
644
|
+
if (prop.type === "SpreadElement") {
|
|
645
|
+
throw new ExtractError(
|
|
646
|
+
"uidex-export-invalid-literal",
|
|
647
|
+
"Spread (`...`) is not allowed inside `export const uidex`.",
|
|
648
|
+
posAt(content, prop.start, p2)
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
if (prop.type !== "Property") {
|
|
652
|
+
throw new ExtractError(
|
|
653
|
+
"uidex-export-invalid-literal",
|
|
654
|
+
"Unexpected member inside `export const uidex` object.",
|
|
655
|
+
posAt(content, prop.start, p2)
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
const keyNode = prop.key;
|
|
659
|
+
const keyPos = posAt(content, keyNode.start, p2);
|
|
660
|
+
if (prop.shorthand) {
|
|
661
|
+
throw new ExtractError(
|
|
662
|
+
"uidex-export-invalid-literal",
|
|
663
|
+
`Shorthand property "${String(keyNode.name)}" is not allowed; write "${String(keyNode.name)}: ..." with a literal value.`,
|
|
664
|
+
keyPos
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
let key;
|
|
668
|
+
if (prop.computed) {
|
|
669
|
+
if (keyNode.type !== "Literal" || typeof keyNode.value !== "string") {
|
|
951
670
|
throw new ExtractError(
|
|
952
671
|
"uidex-export-invalid-literal",
|
|
953
|
-
`
|
|
954
|
-
|
|
672
|
+
"Computed property keys must be string literals in `export const uidex`.",
|
|
673
|
+
keyPos
|
|
955
674
|
);
|
|
956
675
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
this.consume();
|
|
964
|
-
break;
|
|
965
|
-
}
|
|
676
|
+
key = keyNode.value;
|
|
677
|
+
} else if (keyNode.type === "Identifier") {
|
|
678
|
+
key = String(keyNode.name);
|
|
679
|
+
} else if (keyNode.type === "Literal" && typeof keyNode.value === "string") {
|
|
680
|
+
key = keyNode.value;
|
|
681
|
+
} else {
|
|
966
682
|
throw new ExtractError(
|
|
967
683
|
"uidex-export-invalid-literal",
|
|
968
|
-
|
|
969
|
-
|
|
684
|
+
"Numeric property keys are not allowed in `export const uidex`.",
|
|
685
|
+
keyPos
|
|
970
686
|
);
|
|
971
687
|
}
|
|
972
|
-
return { kind: "object", entries, pos: open.pos };
|
|
973
|
-
}
|
|
974
|
-
recordEntry(entries, seen, key, value, pos) {
|
|
975
688
|
if (seen.has(key)) {
|
|
976
689
|
throw new ExtractError(
|
|
977
690
|
"uidex-export-duplicate-field",
|
|
978
691
|
`Duplicate field "${key}" in \`export const uidex\`.`,
|
|
979
|
-
|
|
692
|
+
keyPos
|
|
980
693
|
);
|
|
981
694
|
}
|
|
982
695
|
seen.add(key);
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
items.push(value);
|
|
1005
|
-
const after = this.peek();
|
|
1006
|
-
if (after.kind === "comma") {
|
|
1007
|
-
this.consume();
|
|
1008
|
-
continue;
|
|
1009
|
-
}
|
|
1010
|
-
if (after.kind === "rbracket") {
|
|
1011
|
-
this.consume();
|
|
1012
|
-
break;
|
|
1013
|
-
}
|
|
696
|
+
const value = toLitValue(prop.value, content, p2);
|
|
697
|
+
entries.push({
|
|
698
|
+
key,
|
|
699
|
+
value,
|
|
700
|
+
keyPos,
|
|
701
|
+
span: removalSpan(content, prop.start, prop.end)
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
return { kind: "object", entries, pos, span };
|
|
705
|
+
}
|
|
706
|
+
function arrayLit(node, content, p2, pos, span) {
|
|
707
|
+
const items = [];
|
|
708
|
+
for (const el of node.elements ?? []) {
|
|
709
|
+
if (el === null) {
|
|
710
|
+
throw new ExtractError(
|
|
711
|
+
"uidex-export-invalid-literal",
|
|
712
|
+
"Array holes are not allowed in `export const uidex`.",
|
|
713
|
+
pos
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
if (el.type === "SpreadElement") {
|
|
1014
717
|
throw new ExtractError(
|
|
1015
718
|
"uidex-export-invalid-literal",
|
|
1016
|
-
|
|
1017
|
-
|
|
719
|
+
"Spread (`...`) is not allowed inside `export const uidex`.",
|
|
720
|
+
posAt(content, el.start, p2)
|
|
1018
721
|
);
|
|
1019
722
|
}
|
|
1020
|
-
|
|
723
|
+
items.push(toLitValue(el, content, p2));
|
|
1021
724
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
"uidex-export-invalid-literal",
|
|
1032
|
-
"Only `as const` is allowed after the `export const uidex` value.",
|
|
1033
|
-
next.pos
|
|
1034
|
-
);
|
|
1035
|
-
}
|
|
725
|
+
return { kind: "array", items, pos, span };
|
|
726
|
+
}
|
|
727
|
+
function checkSatisfies(init, metadata, file, p2, diagnostics) {
|
|
728
|
+
let node = init;
|
|
729
|
+
let satisfiesType;
|
|
730
|
+
while (isNode2(node)) {
|
|
731
|
+
if (node.type === "TSSatisfiesExpression") {
|
|
732
|
+
satisfiesType = node.typeAnnotation;
|
|
733
|
+
break;
|
|
1036
734
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
735
|
+
if (node.type === "TSAsExpression" || node.type === "TSNonNullExpression" || node.type === "TSTypeAssertion" || node.type === "ParenthesizedExpression") {
|
|
736
|
+
node = node.expression;
|
|
737
|
+
continue;
|
|
1040
738
|
}
|
|
739
|
+
break;
|
|
1041
740
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
741
|
+
if (!isNode2(satisfiesType) || satisfiesType.type !== "TSTypeReference") return;
|
|
742
|
+
const typeName = satisfiesType.typeName;
|
|
743
|
+
if (!isNode2(typeName) || typeName.type !== "TSQualifiedName") return;
|
|
744
|
+
const left = typeName.left;
|
|
745
|
+
const right = typeName.right;
|
|
746
|
+
if (!isNode2(left) || left.type !== "Identifier" || left.name !== "Uidex") {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (!isNode2(right) || right.type !== "Identifier") return;
|
|
750
|
+
const actual = String(right.name);
|
|
751
|
+
if (!KNOWN_SATISFIES.has(actual)) return;
|
|
752
|
+
const discriminator = metadata.notFlow ? "notFlow" : metadata.kind;
|
|
753
|
+
const expected = SATISFIES_NAMES[discriminator];
|
|
754
|
+
if (actual === expected) return;
|
|
755
|
+
diagnostics.push({
|
|
756
|
+
code: "uidex-export-satisfies-mismatch",
|
|
757
|
+
severity: "warning",
|
|
758
|
+
message: `\`export const uidex\` declares kind "${discriminator}" but is annotated \`satisfies Uidex.${actual}\`; expected \`Uidex.${expected}\`.`,
|
|
759
|
+
file,
|
|
760
|
+
line: p2.lineAt(satisfiesType.start),
|
|
761
|
+
hint: `Change the annotation to \`satisfies Uidex.${expected}\` or fix the kind discriminator.`
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
1044
765
|
if (value.kind !== "object") {
|
|
1045
766
|
throw new ExtractError(
|
|
1046
767
|
"uidex-export-invalid-literal",
|
|
@@ -1049,7 +770,7 @@ function buildMetadata(value, file, headerPos, diagnostics) {
|
|
|
1049
770
|
);
|
|
1050
771
|
}
|
|
1051
772
|
const byKey = /* @__PURE__ */ new Map();
|
|
1052
|
-
for (const
|
|
773
|
+
for (const entry of value.entries) byKey.set(entry.key, entry);
|
|
1053
774
|
const presentKinds = KIND_DISCRIMINATORS.filter(
|
|
1054
775
|
(k) => byKey.has(k)
|
|
1055
776
|
);
|
|
@@ -1072,49 +793,58 @@ function buildMetadata(value, file, headerPos, diagnostics) {
|
|
|
1072
793
|
const discriminator = presentKinds[0];
|
|
1073
794
|
const kind = discriminator === "notFlow" ? "flow" : discriminator;
|
|
1074
795
|
const allowed = ALLOWED_FIELDS[kind];
|
|
1075
|
-
for (const
|
|
1076
|
-
if (!allowed.has(
|
|
1077
|
-
const fieldVal = byKey.get(k);
|
|
796
|
+
for (const entry of value.entries) {
|
|
797
|
+
if (!allowed.has(entry.key)) {
|
|
1078
798
|
throw new ExtractError(
|
|
1079
799
|
"uidex-export-unknown-field",
|
|
1080
|
-
`Unknown field "${
|
|
800
|
+
`Unknown field "${entry.key}" in \`export const uidex\` for kind "${kind}". Allowed: ${Array.from(
|
|
1081
801
|
allowed
|
|
1082
802
|
).sort().join(", ")}.`,
|
|
1083
|
-
|
|
803
|
+
entry.value.pos
|
|
1084
804
|
);
|
|
1085
805
|
}
|
|
1086
806
|
}
|
|
1087
807
|
const idField = discriminator === "notFlow" ? "flow" : discriminator;
|
|
1088
|
-
const idValue = byKey.get(discriminator);
|
|
808
|
+
const idValue = byKey.get(discriminator).value;
|
|
1089
809
|
let id;
|
|
1090
810
|
if (discriminator === "notFlow") {
|
|
1091
|
-
|
|
1092
|
-
if (v.kind !== "boolean" || v.value !== true) {
|
|
811
|
+
if (idValue.kind !== "boolean" || idValue.value !== true) {
|
|
1093
812
|
throw new ExtractError(
|
|
1094
813
|
"uidex-export-invalid-field",
|
|
1095
814
|
"`notFlow` must be `true`.",
|
|
1096
|
-
|
|
815
|
+
idValue.pos
|
|
1097
816
|
);
|
|
1098
817
|
}
|
|
1099
818
|
id = false;
|
|
1100
819
|
} else {
|
|
1101
820
|
id = readIdField(idValue, kind, idField);
|
|
1102
821
|
}
|
|
1103
|
-
const acceptance = readStringArrayField(byKey, "acceptance");
|
|
822
|
+
const acceptance = readStringArrayField(byKey, "acceptance")?.values;
|
|
1104
823
|
const description = readStringField(byKey, "description");
|
|
1105
824
|
const name = readStringField(byKey, "name");
|
|
1106
825
|
if (name === "") {
|
|
1107
|
-
const
|
|
826
|
+
const entry = byKey.get("name");
|
|
1108
827
|
diagnostics.push({
|
|
1109
828
|
code: "uidex-export-empty-name",
|
|
1110
829
|
severity: "info",
|
|
1111
830
|
message: "`name` is an empty string; treating as unset.",
|
|
1112
831
|
file,
|
|
1113
|
-
line: pos.line
|
|
832
|
+
line: entry.value.pos.line,
|
|
833
|
+
fix: {
|
|
834
|
+
description: "Remove the empty `name` field",
|
|
835
|
+
edits: [
|
|
836
|
+
{
|
|
837
|
+
path: sourcePath,
|
|
838
|
+
start: entry.span.start,
|
|
839
|
+
end: entry.span.end,
|
|
840
|
+
replacement: ""
|
|
841
|
+
}
|
|
842
|
+
]
|
|
843
|
+
}
|
|
1114
844
|
});
|
|
1115
845
|
}
|
|
1116
|
-
const
|
|
1117
|
-
const
|
|
846
|
+
const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
847
|
+
const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
1118
848
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
1119
849
|
const metadata = {
|
|
1120
850
|
source: "ts-export",
|
|
@@ -1129,9 +859,21 @@ function buildMetadata(value, file, headerPos, diagnostics) {
|
|
|
1129
859
|
if (name) metadata.name = name;
|
|
1130
860
|
if (acceptance) metadata.acceptance = acceptance;
|
|
1131
861
|
if (description) metadata.description = description;
|
|
1132
|
-
if (
|
|
1133
|
-
|
|
862
|
+
if (featuresField) {
|
|
863
|
+
metadata.features = featuresField.values;
|
|
864
|
+
metadata.featureSpans = featuresField.spans;
|
|
865
|
+
}
|
|
866
|
+
if (widgetsField) {
|
|
867
|
+
metadata.widgets = widgetsField.values;
|
|
868
|
+
metadata.widgetSpans = widgetsField.spans;
|
|
869
|
+
}
|
|
1134
870
|
if (notFlow) metadata.notFlow = true;
|
|
871
|
+
if (typeof id === "string" && idValue.kind === "string") {
|
|
872
|
+
metadata.idSpan = idValue.span;
|
|
873
|
+
}
|
|
874
|
+
const fieldSpans = {};
|
|
875
|
+
for (const entry of value.entries) fieldSpans[entry.key] = entry.span;
|
|
876
|
+
metadata.fieldSpans = fieldSpans;
|
|
1135
877
|
return metadata;
|
|
1136
878
|
}
|
|
1137
879
|
function readIdField(value, kind, fieldName) {
|
|
@@ -1162,29 +904,30 @@ function readIdField(value, kind, fieldName) {
|
|
|
1162
904
|
);
|
|
1163
905
|
}
|
|
1164
906
|
function readStringField(byKey, name) {
|
|
1165
|
-
const
|
|
1166
|
-
if (!
|
|
1167
|
-
if (
|
|
907
|
+
const entry = byKey.get(name);
|
|
908
|
+
if (!entry) return void 0;
|
|
909
|
+
if (entry.value.kind !== "string") {
|
|
1168
910
|
throw new ExtractError(
|
|
1169
911
|
"uidex-export-invalid-field",
|
|
1170
912
|
`\`${name}\` must be a string.`,
|
|
1171
|
-
|
|
913
|
+
entry.value.pos
|
|
1172
914
|
);
|
|
1173
915
|
}
|
|
1174
|
-
return
|
|
916
|
+
return entry.value.value;
|
|
1175
917
|
}
|
|
1176
918
|
function readStringArrayField(byKey, name) {
|
|
1177
|
-
const
|
|
1178
|
-
if (!
|
|
1179
|
-
if (
|
|
919
|
+
const entry = byKey.get(name);
|
|
920
|
+
if (!entry) return void 0;
|
|
921
|
+
if (entry.value.kind !== "array") {
|
|
1180
922
|
throw new ExtractError(
|
|
1181
923
|
"uidex-export-invalid-field",
|
|
1182
924
|
`\`${name}\` must be an array of strings.`,
|
|
1183
|
-
|
|
925
|
+
entry.value.pos
|
|
1184
926
|
);
|
|
1185
927
|
}
|
|
1186
|
-
const
|
|
1187
|
-
|
|
928
|
+
const values = [];
|
|
929
|
+
const spans = [];
|
|
930
|
+
for (const item of entry.value.items) {
|
|
1188
931
|
if (item.kind !== "string") {
|
|
1189
932
|
throw new ExtractError(
|
|
1190
933
|
"uidex-export-invalid-field",
|
|
@@ -1192,322 +935,526 @@ function readStringArrayField(byKey, name) {
|
|
|
1192
935
|
item.pos
|
|
1193
936
|
);
|
|
1194
937
|
}
|
|
1195
|
-
|
|
938
|
+
values.push(item.value);
|
|
939
|
+
spans.push(item.span);
|
|
1196
940
|
}
|
|
1197
|
-
return
|
|
941
|
+
return { values, spans };
|
|
1198
942
|
}
|
|
1199
|
-
function posAt(content, offset) {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
943
|
+
function posAt(content, offset, p2) {
|
|
944
|
+
const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
|
|
945
|
+
return { offset, line: p2.lineAt(offset), column: offset - lineStart + 1 };
|
|
946
|
+
}
|
|
947
|
+
function removalSpan(content, start, end) {
|
|
948
|
+
let e = end;
|
|
949
|
+
while (e < content.length && /[ \t]/.test(content[e])) e++;
|
|
950
|
+
if (content[e] === ",") return { start, end: e + 1 };
|
|
951
|
+
let s = start;
|
|
952
|
+
while (s > 0 && /[\s]/.test(content[s - 1])) s--;
|
|
953
|
+
if (content[s - 1] === ",") return { start: s - 1, end };
|
|
954
|
+
return { start, end };
|
|
955
|
+
}
|
|
956
|
+
function statementSpan(stmt, content) {
|
|
957
|
+
let end = stmt.end;
|
|
958
|
+
while (end < content.length && /[ \t]/.test(content[end])) end++;
|
|
959
|
+
if (content[end] === ";") end++;
|
|
960
|
+
while (end < content.length && /[ \t]/.test(content[end])) end++;
|
|
961
|
+
if (content[end] === "\r") end++;
|
|
962
|
+
if (content[end] === "\n") end++;
|
|
963
|
+
return { start: stmt.start, end };
|
|
964
|
+
}
|
|
965
|
+
function isNode2(value) {
|
|
966
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/scanner/scan/flow-facts.ts
|
|
970
|
+
function collectFlowFacts(parsed, content) {
|
|
971
|
+
if (parsed.program === null || !content.includes("test.describe")) {
|
|
972
|
+
return [];
|
|
973
|
+
}
|
|
974
|
+
const facts = [];
|
|
975
|
+
walkAst(parsed.program, (node) => {
|
|
976
|
+
if (node.type !== "CallExpression") return void 0;
|
|
977
|
+
const fact = readTaggedDescribe(node, parsed);
|
|
978
|
+
if (fact) facts.push(fact);
|
|
979
|
+
return void 0;
|
|
980
|
+
});
|
|
981
|
+
return facts;
|
|
982
|
+
}
|
|
983
|
+
function readTaggedDescribe(call, parsed) {
|
|
984
|
+
const callee = call.callee;
|
|
985
|
+
if (!callee || callee.type !== "MemberExpression" || !isIdentifier(callee.object, "test") || !isIdentifier(callee.property, "describe")) {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
const args = call.arguments ?? [];
|
|
989
|
+
const title = stringLiteralValue(args[0]);
|
|
990
|
+
if (title === null) return null;
|
|
991
|
+
if (!hasFlowTag(args[1])) return null;
|
|
992
|
+
const body = args[2];
|
|
993
|
+
const { calls, dynamicCalls } = body ? collectUidexCalls(body, parsed) : { calls: [], dynamicCalls: [] };
|
|
994
|
+
return {
|
|
995
|
+
title,
|
|
996
|
+
line: parsed.lineAt(call.start),
|
|
997
|
+
calls,
|
|
998
|
+
...dynamicCalls.length > 0 ? { dynamicCalls } : {}
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function hasFlowTag(node) {
|
|
1002
|
+
if (!node || node.type !== "ObjectExpression") return false;
|
|
1003
|
+
for (const prop of node.properties ?? []) {
|
|
1004
|
+
if (prop.type !== "Property") continue;
|
|
1005
|
+
const key = prop.key;
|
|
1006
|
+
const keyName = key?.type === "Identifier" ? String(key.name) : key?.type === "Literal" ? String(key.value) : null;
|
|
1007
|
+
if (keyName !== "tag") continue;
|
|
1008
|
+
const value = prop.value;
|
|
1009
|
+
if (!value) return false;
|
|
1010
|
+
if (stringLiteralValue(value) === "@uidex:flow") return true;
|
|
1011
|
+
if (value.type === "ArrayExpression") {
|
|
1012
|
+
for (const el of value.elements ?? []) {
|
|
1013
|
+
if (el && stringLiteralValue(el) === "@uidex:flow") return true;
|
|
1014
|
+
}
|
|
1206
1015
|
}
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
function collectUidexCalls(body, parsed) {
|
|
1021
|
+
const calls = [];
|
|
1022
|
+
const dynamicCalls = [];
|
|
1023
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
1024
|
+
const record = (node, action) => {
|
|
1025
|
+
if (claimed.has(node)) return;
|
|
1026
|
+
claimed.add(node);
|
|
1027
|
+
const resolved = uidexCallId(node);
|
|
1028
|
+
if (resolved === null) {
|
|
1029
|
+
dynamicCalls.push({ line: parsed.lineAt(node.start) });
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
calls.push({
|
|
1033
|
+
id: resolved.id,
|
|
1034
|
+
...action ? { action } : {},
|
|
1035
|
+
line: parsed.lineAt(node.start),
|
|
1036
|
+
span: resolved.span
|
|
1037
|
+
});
|
|
1038
|
+
};
|
|
1039
|
+
walkAst(body, (node) => {
|
|
1040
|
+
if (node.type !== "CallExpression") return void 0;
|
|
1041
|
+
const callee = node.callee;
|
|
1042
|
+
if (callee?.type === "MemberExpression") {
|
|
1043
|
+
const inner = callee.object;
|
|
1044
|
+
if (inner && isUidexCall(inner)) {
|
|
1045
|
+
const property = callee.property;
|
|
1046
|
+
const action = property?.type === "Identifier" ? String(property.name) : void 0;
|
|
1047
|
+
record(inner, action);
|
|
1048
|
+
}
|
|
1049
|
+
return void 0;
|
|
1050
|
+
}
|
|
1051
|
+
if (isUidexCall(node)) record(node);
|
|
1052
|
+
return void 0;
|
|
1053
|
+
});
|
|
1054
|
+
return { calls, dynamicCalls };
|
|
1055
|
+
}
|
|
1056
|
+
function isUidexCall(node) {
|
|
1057
|
+
if (node.type !== "CallExpression") return false;
|
|
1058
|
+
if (!isIdentifier(node.callee, "uidex")) return false;
|
|
1059
|
+
return (node.arguments ?? []).length >= 1;
|
|
1060
|
+
}
|
|
1061
|
+
function uidexCallId(node) {
|
|
1062
|
+
const args = node.arguments ?? [];
|
|
1063
|
+
const arg = args[0];
|
|
1064
|
+
const id = stringLiteralValue(arg);
|
|
1065
|
+
if (id === null) return null;
|
|
1066
|
+
return { id, span: { start: arg.start, end: arg.end } };
|
|
1067
|
+
}
|
|
1068
|
+
function stringLiteralValue(node) {
|
|
1069
|
+
if (!node) return null;
|
|
1070
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
1071
|
+
return node.value.length > 0 ? node.value : null;
|
|
1072
|
+
}
|
|
1073
|
+
if (node.type === "TemplateLiteral") {
|
|
1074
|
+
const expressions = node.expressions ?? [];
|
|
1075
|
+
if (expressions.length > 0) return null;
|
|
1076
|
+
const quasis = node.quasis ?? [];
|
|
1077
|
+
const cooked = quasis[0]?.value?.cooked;
|
|
1078
|
+
return cooked && cooked.length > 0 ? cooked : null;
|
|
1207
1079
|
}
|
|
1208
|
-
return
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
function isIdentifier(node, name) {
|
|
1083
|
+
return typeof node === "object" && node !== null && node.type === "Identifier" && String(node.name) === name;
|
|
1209
1084
|
}
|
|
1210
1085
|
|
|
1211
1086
|
// src/scanner/scan/jsx-ancestry.ts
|
|
1212
|
-
var
|
|
1213
|
-
var
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1087
|
+
var ATTR_NAME_RE = /^data-uidex(?:-(region|widget|primitive))?$/;
|
|
1088
|
+
var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["button", "a", "input", "select", "textarea"]);
|
|
1089
|
+
var LANDMARK_TAGS = /* @__PURE__ */ new Set(["header", "nav", "main", "aside", "footer"]);
|
|
1090
|
+
function attrKind(node) {
|
|
1091
|
+
const name = node.name;
|
|
1092
|
+
if (!name || name.type !== "JSXIdentifier") return null;
|
|
1093
|
+
const m = ATTR_NAME_RE.exec(String(name.name));
|
|
1094
|
+
if (!m) return null;
|
|
1095
|
+
return m[1] ?? "element";
|
|
1096
|
+
}
|
|
1097
|
+
function collectConstStrings(program) {
|
|
1098
|
+
const consts = /* @__PURE__ */ new Map();
|
|
1099
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1100
|
+
walkAst(program, (node) => {
|
|
1101
|
+
if (node.type !== "VariableDeclaration" || node.kind !== "const") {
|
|
1102
|
+
return void 0;
|
|
1103
|
+
}
|
|
1104
|
+
for (const decl of node.declarations ?? []) {
|
|
1105
|
+
const id = decl.id;
|
|
1106
|
+
if (!id || id.type !== "Identifier") continue;
|
|
1107
|
+
const name = String(id.name);
|
|
1108
|
+
if (seen.has(name)) {
|
|
1109
|
+
consts.delete(name);
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
seen.add(name);
|
|
1113
|
+
const init = decl.init;
|
|
1114
|
+
if (!init) continue;
|
|
1115
|
+
const value = staticString(unwrapTsExpression(init));
|
|
1116
|
+
if (value !== null) consts.set(name, value);
|
|
1117
|
+
}
|
|
1118
|
+
return void 0;
|
|
1119
|
+
});
|
|
1120
|
+
return consts;
|
|
1121
|
+
}
|
|
1122
|
+
function staticString(node) {
|
|
1123
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
1124
|
+
return node.value;
|
|
1221
1125
|
}
|
|
1222
|
-
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1225
|
-
|
|
1126
|
+
if (node.type === "TemplateLiteral") {
|
|
1127
|
+
const expressions = node.expressions ?? [];
|
|
1128
|
+
if (expressions.length > 0) return null;
|
|
1129
|
+
const quasis = node.quasis ?? [];
|
|
1130
|
+
return quasis[0]?.value?.cooked ?? "";
|
|
1226
1131
|
}
|
|
1227
|
-
return
|
|
1132
|
+
return null;
|
|
1228
1133
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
const
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
if (
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
if (
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
}
|
|
1134
|
+
var UNRESOLVED = { resolved: false };
|
|
1135
|
+
function evalIdExpression(expr, consts) {
|
|
1136
|
+
const node = unwrapTsExpression(expr);
|
|
1137
|
+
const literal = staticString(node);
|
|
1138
|
+
if (literal !== null) {
|
|
1139
|
+
return literal.length > 0 ? { resolved: true, ids: [literal] } : UNRESOLVED;
|
|
1140
|
+
}
|
|
1141
|
+
if (node.type === "TemplateLiteral") {
|
|
1142
|
+
const quasis = node.quasis ?? [];
|
|
1143
|
+
const expressions = node.expressions ?? [];
|
|
1144
|
+
let out2 = "";
|
|
1145
|
+
for (let i = 0; i < quasis.length; i++) {
|
|
1146
|
+
out2 += quasis[i].value?.cooked ?? "";
|
|
1147
|
+
if (i < expressions.length) {
|
|
1148
|
+
const part = evalIdExpression(expressions[i], consts);
|
|
1149
|
+
out2 += part.resolved && part.ids.length === 1 ? part.ids[0] : "*";
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
out2 = out2.replace(/\*{2,}/g, "*");
|
|
1153
|
+
if (!out2.includes("*")) {
|
|
1154
|
+
return out2.length > 0 ? { resolved: true, ids: [out2] } : UNRESOLVED;
|
|
1155
|
+
}
|
|
1156
|
+
return out2.replace(/\*/g, "").length > 0 ? { resolved: true, ids: [out2] } : UNRESOLVED;
|
|
1157
|
+
}
|
|
1158
|
+
if (node.type === "Identifier") {
|
|
1159
|
+
const value = consts.get(String(node.name));
|
|
1160
|
+
return value !== void 0 && value.length > 0 ? { resolved: true, ids: [value] } : UNRESOLVED;
|
|
1161
|
+
}
|
|
1162
|
+
if (node.type === "ConditionalExpression") {
|
|
1163
|
+
const left = evalIdExpression(node.consequent, consts);
|
|
1164
|
+
const right = evalIdExpression(node.alternate, consts);
|
|
1165
|
+
if (!left.resolved || !right.resolved) return UNRESOLVED;
|
|
1166
|
+
return { resolved: true, ids: [.../* @__PURE__ */ new Set([...left.ids, ...right.ids])] };
|
|
1167
|
+
}
|
|
1168
|
+
return UNRESOLVED;
|
|
1169
|
+
}
|
|
1170
|
+
function collectElementAttrs(opening, consts, dynamicAttrs, lineAt) {
|
|
1171
|
+
const statics = [];
|
|
1172
|
+
const patterns = [];
|
|
1173
|
+
const attributes = opening.attributes ?? [];
|
|
1174
|
+
for (const attr of attributes) {
|
|
1175
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
1176
|
+
const kind = attrKind(attr);
|
|
1177
|
+
if (!kind) continue;
|
|
1178
|
+
const value = attr.value;
|
|
1179
|
+
if (!value) continue;
|
|
1180
|
+
let result = UNRESOLVED;
|
|
1181
|
+
let valueSpan;
|
|
1182
|
+
if (value.type === "Literal") {
|
|
1183
|
+
const v = staticString(value);
|
|
1184
|
+
result = v !== null && v.length > 0 ? { resolved: true, ids: [v] } : UNRESOLVED;
|
|
1185
|
+
if (result.resolved) valueSpan = { start: value.start, end: value.end };
|
|
1186
|
+
} else if (value.type === "JSXExpressionContainer") {
|
|
1187
|
+
const expr = value.expression;
|
|
1188
|
+
if (expr && expr.type !== "JSXEmptyExpression") {
|
|
1189
|
+
result = evalIdExpression(expr, consts);
|
|
1190
|
+
const inner = unwrapTsExpression(expr);
|
|
1191
|
+
if (result.resolved && staticString(inner) !== null) {
|
|
1192
|
+
valueSpan = { start: inner.start, end: inner.end };
|
|
1288
1193
|
}
|
|
1289
|
-
advanceLines(i, end + 1);
|
|
1290
|
-
i = end + 1;
|
|
1291
|
-
continue;
|
|
1292
1194
|
}
|
|
1293
|
-
if (
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
if (tagName) {
|
|
1300
|
-
const attrs = parseDataAttrs(tagSource);
|
|
1301
|
-
if (attrs.length > 0) {
|
|
1302
|
-
const snapshot = ancestors.slice();
|
|
1303
|
-
for (const a of attrs) {
|
|
1304
|
-
out2.push({ kind: a.kind, id: a.id, line, ancestors: snapshot });
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
if (!isSelf) {
|
|
1308
|
-
for (const a of attrs) ancestors.push(a);
|
|
1309
|
-
stack.push({ tagName, pushed: attrs.length });
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
advanceLines(i, end + 1);
|
|
1313
|
-
i = end + 1;
|
|
1195
|
+
if (!result.resolved) {
|
|
1196
|
+
dynamicAttrs.push({
|
|
1197
|
+
kind,
|
|
1198
|
+
attrName: kind === "element" ? "data-uidex" : `data-uidex-${kind}`,
|
|
1199
|
+
line: lineAt(attr.start)
|
|
1200
|
+
});
|
|
1314
1201
|
continue;
|
|
1315
1202
|
}
|
|
1316
1203
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1204
|
+
if (!result.resolved) continue;
|
|
1205
|
+
for (const id of result.ids) {
|
|
1206
|
+
const resolved = {
|
|
1207
|
+
kind,
|
|
1208
|
+
id,
|
|
1209
|
+
start: attr.start,
|
|
1210
|
+
isPattern: id.includes("*"),
|
|
1211
|
+
// Only a single plain string literal is renameable in place.
|
|
1212
|
+
...result.ids.length === 1 && valueSpan ? { span: valueSpan } : {}
|
|
1213
|
+
};
|
|
1214
|
+
if (resolved.isPattern) patterns.push(resolved);
|
|
1215
|
+
else statics.push(resolved);
|
|
1329
1216
|
}
|
|
1330
|
-
if (c === quote) return i + 1;
|
|
1331
|
-
i++;
|
|
1332
1217
|
}
|
|
1333
|
-
return
|
|
1218
|
+
return [...statics, ...patterns];
|
|
1334
1219
|
}
|
|
1335
|
-
function
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1220
|
+
function collectJSXFacts(parsed) {
|
|
1221
|
+
const occurrences = [];
|
|
1222
|
+
const dynamicAttrs = [];
|
|
1223
|
+
const unannotatedInteractive = [];
|
|
1224
|
+
const landmarks = [];
|
|
1225
|
+
if (parsed.program === null) {
|
|
1226
|
+
return { occurrences, dynamicAttrs, unannotatedInteractive, landmarks };
|
|
1227
|
+
}
|
|
1228
|
+
const consts = collectConstStrings(parsed.program);
|
|
1229
|
+
const ancestors = [];
|
|
1230
|
+
const visit = (node) => {
|
|
1231
|
+
if (!isNode3(node)) return;
|
|
1232
|
+
if (node.type === "JSXElement") {
|
|
1233
|
+
const opening = node.openingElement;
|
|
1234
|
+
const attrs = collectElementAttrs(
|
|
1235
|
+
opening,
|
|
1236
|
+
consts,
|
|
1237
|
+
dynamicAttrs,
|
|
1238
|
+
parsed.lineAt
|
|
1239
|
+
);
|
|
1240
|
+
const interactive = readInteractive(node, parsed.lineAt);
|
|
1241
|
+
if (interactive) unannotatedInteractive.push(interactive);
|
|
1242
|
+
if (attrs.length > 0) {
|
|
1243
|
+
const snapshot = ancestors.slice();
|
|
1244
|
+
for (const a of attrs) {
|
|
1245
|
+
occurrences.push({
|
|
1246
|
+
kind: a.kind,
|
|
1247
|
+
id: a.id,
|
|
1248
|
+
line: parsed.lineAt(a.start),
|
|
1249
|
+
ancestors: snapshot,
|
|
1250
|
+
...a.span ? { span: a.span } : {}
|
|
1251
|
+
});
|
|
1353
1252
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1253
|
+
}
|
|
1254
|
+
let pushed = attrs.length;
|
|
1255
|
+
for (const a of attrs) ancestors.push({ kind: a.kind, id: a.id });
|
|
1256
|
+
const landmark = readLandmark(opening, parsed.lineAt);
|
|
1257
|
+
if (landmark) {
|
|
1258
|
+
landmarks.push(landmark);
|
|
1259
|
+
if (!attrs.some((a) => a.kind === "region")) {
|
|
1260
|
+
ancestors.push({ kind: "region", id: landmark.tag });
|
|
1261
|
+
pushed++;
|
|
1357
1262
|
}
|
|
1358
|
-
if (cj === "{") depth++;
|
|
1359
|
-
else if (cj === "}") depth--;
|
|
1360
|
-
i++;
|
|
1361
1263
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
let i = start;
|
|
1371
|
-
while (i < N) {
|
|
1372
|
-
const c = content[i];
|
|
1373
|
-
if (c === '"' || c === "'") {
|
|
1374
|
-
i = skipString(content, i, c);
|
|
1375
|
-
continue;
|
|
1376
|
-
}
|
|
1377
|
-
if (c === "`") {
|
|
1378
|
-
i = skipTemplate(content, i);
|
|
1379
|
-
continue;
|
|
1264
|
+
visitChildren(opening);
|
|
1265
|
+
for (const child of node.children ?? []) {
|
|
1266
|
+
visit(child);
|
|
1267
|
+
}
|
|
1268
|
+
const closing = node.closingElement;
|
|
1269
|
+
if (isNode3(closing)) visitChildren(closing);
|
|
1270
|
+
ancestors.length -= pushed;
|
|
1271
|
+
return;
|
|
1380
1272
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
i = skipTemplate(content, i);
|
|
1392
|
-
continue;
|
|
1393
|
-
}
|
|
1394
|
-
if (cj === "{") depth++;
|
|
1395
|
-
else if (cj === "}") depth--;
|
|
1396
|
-
i++;
|
|
1273
|
+
visitChildren(node);
|
|
1274
|
+
};
|
|
1275
|
+
const visitChildren = (node) => {
|
|
1276
|
+
for (const key of Object.keys(node)) {
|
|
1277
|
+
if (key === "type" || key === "start" || key === "end") continue;
|
|
1278
|
+
const value = node[key];
|
|
1279
|
+
if (Array.isArray(value)) {
|
|
1280
|
+
for (const item of value) visit(item);
|
|
1281
|
+
} else {
|
|
1282
|
+
visit(value);
|
|
1397
1283
|
}
|
|
1398
|
-
continue;
|
|
1399
1284
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
}
|
|
1403
|
-
return -1;
|
|
1285
|
+
};
|
|
1286
|
+
visit(parsed.program);
|
|
1287
|
+
return { occurrences, dynamicAttrs, unannotatedInteractive, landmarks };
|
|
1404
1288
|
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
if (content[i] === "\n") line++;
|
|
1289
|
+
function readLandmark(opening, lineAt) {
|
|
1290
|
+
const name = opening.name;
|
|
1291
|
+
if (!name || name.type !== "JSXIdentifier") return null;
|
|
1292
|
+
const tag = String(name.name);
|
|
1293
|
+
if (LANDMARK_TAGS.has(tag)) {
|
|
1294
|
+
return { tag, line: lineAt(opening.start) };
|
|
1412
1295
|
}
|
|
1413
|
-
|
|
1296
|
+
for (const attr of opening.attributes ?? []) {
|
|
1297
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
1298
|
+
const attrName = attr.name;
|
|
1299
|
+
if (!attrName || String(attrName.name) !== "role") continue;
|
|
1300
|
+
const value = attr.value;
|
|
1301
|
+
if (value && value.type === "Literal" && value.value === "region") {
|
|
1302
|
+
return { tag: "region", line: lineAt(opening.start) };
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return null;
|
|
1414
1306
|
}
|
|
1415
|
-
function
|
|
1416
|
-
const
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
const
|
|
1420
|
-
|
|
1421
|
-
let
|
|
1422
|
-
for (const
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
const uidex = line.match(
|
|
1426
|
-
/^@uidex\s+(page|feature|widget)\s+(\S+)(?:\s+-\s+(.+))?/
|
|
1427
|
-
);
|
|
1428
|
-
if (uidex) {
|
|
1429
|
-
kind = uidex[1];
|
|
1430
|
-
id = uidex[2];
|
|
1431
|
-
if (uidex[3]) desc.push(uidex[3].trim());
|
|
1307
|
+
function readInteractive(element, lineAt) {
|
|
1308
|
+
const opening = element.openingElement;
|
|
1309
|
+
const name = opening.name;
|
|
1310
|
+
if (!name || name.type !== "JSXIdentifier") return null;
|
|
1311
|
+
const tag = String(name.name);
|
|
1312
|
+
if (!INTERACTIVE_TAGS.has(tag)) return null;
|
|
1313
|
+
let hasSpread = false;
|
|
1314
|
+
for (const attr of opening.attributes ?? []) {
|
|
1315
|
+
if (attr.type === "JSXSpreadAttribute") {
|
|
1316
|
+
hasSpread = true;
|
|
1432
1317
|
continue;
|
|
1433
1318
|
}
|
|
1434
|
-
if (
|
|
1435
|
-
|
|
1436
|
-
|
|
1319
|
+
if (attr.type === "JSXAttribute" && attrKind(attr) !== null) return null;
|
|
1320
|
+
}
|
|
1321
|
+
const nameHint = interactiveNameHint(element, opening);
|
|
1322
|
+
return {
|
|
1323
|
+
tag,
|
|
1324
|
+
line: lineAt(opening.start),
|
|
1325
|
+
hasSpread,
|
|
1326
|
+
nameEnd: name.end,
|
|
1327
|
+
...nameHint ? { nameHint } : {}
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
function staticAttrValue(opening, attrName) {
|
|
1331
|
+
for (const attr of opening.attributes ?? []) {
|
|
1332
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
1333
|
+
const n = attr.name;
|
|
1334
|
+
if (!n || String(n.name) !== attrName) continue;
|
|
1335
|
+
const value = attr.value;
|
|
1336
|
+
if (!value) return null;
|
|
1337
|
+
if (value.type === "Literal") return staticString(value);
|
|
1338
|
+
if (value.type === "JSXExpressionContainer") {
|
|
1339
|
+
const expr = value.expression;
|
|
1340
|
+
return expr ? staticString(unwrapTsExpression(expr)) : null;
|
|
1437
1341
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
function staticChildText(element) {
|
|
1347
|
+
const parts = [];
|
|
1348
|
+
for (const child of element.children ?? []) {
|
|
1349
|
+
if (child.type === "JSXText") {
|
|
1350
|
+
parts.push(String(child.value ?? ""));
|
|
1442
1351
|
}
|
|
1443
|
-
if (line.startsWith("@")) continue;
|
|
1444
|
-
desc.push(line);
|
|
1445
1352
|
}
|
|
1353
|
+
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
1354
|
+
}
|
|
1355
|
+
function interactiveNameHint(element, opening) {
|
|
1356
|
+
const ariaLabel = staticAttrValue(opening, "aria-label");
|
|
1357
|
+
if (ariaLabel) return ariaLabel;
|
|
1358
|
+
const text = staticChildText(element);
|
|
1359
|
+
if (text) return text;
|
|
1360
|
+
for (const attr of ["title", "name", "placeholder"]) {
|
|
1361
|
+
const v = staticAttrValue(opening, attr);
|
|
1362
|
+
if (v) return v;
|
|
1363
|
+
}
|
|
1364
|
+
return void 0;
|
|
1365
|
+
}
|
|
1366
|
+
function isNode3(value) {
|
|
1367
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// src/scanner/scan/extract.ts
|
|
1371
|
+
function parseFailureDiagnostic(file, parsed) {
|
|
1372
|
+
const fatal = parsed.program === null || parsed.hasErrors && parsed.program.body.length === 0;
|
|
1373
|
+
if (!fatal) return null;
|
|
1446
1374
|
return {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1375
|
+
code: "parse-error",
|
|
1376
|
+
severity: "warning",
|
|
1377
|
+
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",
|
|
1378
|
+
file: file.displayPath,
|
|
1379
|
+
line: 1,
|
|
1380
|
+
hint: "Fix the file's syntax (or exclude it from .uidex.json sources) so the scanner can read its annotations"
|
|
1452
1381
|
};
|
|
1453
1382
|
}
|
|
1454
1383
|
function extract(files) {
|
|
1455
1384
|
return files.map((file) => {
|
|
1456
|
-
const
|
|
1457
|
-
const
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
};
|
|
1385
|
+
const parsed = parseSource(file);
|
|
1386
|
+
const { exports, diagnostics } = extractUidexExports(file, parsed);
|
|
1387
|
+
const parseFailure = parseFailureDiagnostic(file, parsed);
|
|
1388
|
+
if (parseFailure) diagnostics.push(parseFailure);
|
|
1389
|
+
const out2 = { file, annotations: [] };
|
|
1390
|
+
out2.annotations = extractOne(file, parsed, out2);
|
|
1461
1391
|
if (exports.length > 0) out2.metadata = exports;
|
|
1462
1392
|
if (diagnostics.length > 0) out2.diagnostics = diagnostics;
|
|
1393
|
+
const flows = collectFlowFacts(parsed, file.content);
|
|
1394
|
+
if (flows.length > 0) out2.flows = flows;
|
|
1395
|
+
const imports = collectImportFacts(parsed);
|
|
1396
|
+
if (imports.length > 0) out2.imports = imports;
|
|
1463
1397
|
return out2;
|
|
1464
1398
|
});
|
|
1465
1399
|
}
|
|
1466
|
-
function extractOne(file) {
|
|
1400
|
+
function extractOne(file, parsed, out2) {
|
|
1467
1401
|
const annotations = [];
|
|
1468
|
-
const {
|
|
1469
|
-
|
|
1402
|
+
const { displayPath } = file;
|
|
1403
|
+
const jsx = collectJSXFacts(parsed);
|
|
1404
|
+
if (jsx.dynamicAttrs.length > 0) out2.dynamicAttrs = jsx.dynamicAttrs;
|
|
1405
|
+
if (jsx.unannotatedInteractive.length > 0) {
|
|
1406
|
+
out2.unannotatedInteractive = jsx.unannotatedInteractive;
|
|
1407
|
+
}
|
|
1408
|
+
if (jsx.landmarks.length > 0) out2.landmarks = jsx.landmarks;
|
|
1409
|
+
for (const occ of jsx.occurrences) {
|
|
1470
1410
|
annotations.push({
|
|
1471
1411
|
kind: occ.kind,
|
|
1472
1412
|
id: occ.id,
|
|
1473
1413
|
file: displayPath,
|
|
1474
1414
|
line: occ.line,
|
|
1475
|
-
...occ.ancestors.length > 0 ? { ancestors: occ.ancestors } : {}
|
|
1415
|
+
...occ.ancestors.length > 0 ? { ancestors: occ.ancestors } : {},
|
|
1416
|
+
...occ.span ? { span: occ.span } : {}
|
|
1476
1417
|
});
|
|
1477
1418
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
}
|
|
1496
|
-
} else if (
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
line,
|
|
1502
|
-
acceptance: parsed.acceptance
|
|
1503
|
-
});
|
|
1419
|
+
return annotations;
|
|
1420
|
+
}
|
|
1421
|
+
function collectImportFacts(parsed) {
|
|
1422
|
+
if (parsed.program === null) return [];
|
|
1423
|
+
const out2 = [];
|
|
1424
|
+
for (const stmt of parsed.program.body) {
|
|
1425
|
+
let source;
|
|
1426
|
+
let isTypeOnly = false;
|
|
1427
|
+
const names = [];
|
|
1428
|
+
if (stmt.type === "ImportDeclaration") {
|
|
1429
|
+
source = stmt.source;
|
|
1430
|
+
isTypeOnly = stmt.importKind === "type";
|
|
1431
|
+
for (const spec of stmt.specifiers ?? []) {
|
|
1432
|
+
const local = spec.local;
|
|
1433
|
+
if (local && local.type === "Identifier") {
|
|
1434
|
+
names.push(String(local.name));
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
} else if ((stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportAllDeclaration") && stmt.source) {
|
|
1438
|
+
source = stmt.source;
|
|
1439
|
+
isTypeOnly = stmt.exportKind === "type";
|
|
1440
|
+
} else {
|
|
1441
|
+
continue;
|
|
1504
1442
|
}
|
|
1443
|
+
if (!source || source.type !== "Literal") continue;
|
|
1444
|
+
if (typeof source.value !== "string") continue;
|
|
1445
|
+
out2.push({
|
|
1446
|
+
specifier: source.value,
|
|
1447
|
+
line: parsed.lineAt(stmt.start),
|
|
1448
|
+
span: { start: stmt.start, end: stmt.end },
|
|
1449
|
+
isTypeOnly,
|
|
1450
|
+
names
|
|
1451
|
+
});
|
|
1505
1452
|
}
|
|
1506
|
-
return
|
|
1453
|
+
return out2;
|
|
1507
1454
|
}
|
|
1508
1455
|
|
|
1509
1456
|
// src/scanner/scan/resolve.ts
|
|
1510
|
-
import * as
|
|
1457
|
+
import * as path4 from "path";
|
|
1511
1458
|
|
|
1512
1459
|
// src/shared/entities/types.ts
|
|
1513
1460
|
var ENTITY_KINDS = [
|
|
@@ -1593,13 +1540,14 @@ function createRegistry() {
|
|
|
1593
1540
|
};
|
|
1594
1541
|
const getPatternsForKind = (kind) => {
|
|
1595
1542
|
const cached = patternCache.get(kind);
|
|
1596
|
-
if (cached !== void 0)
|
|
1597
|
-
return cached;
|
|
1543
|
+
if (cached !== void 0) return cached;
|
|
1598
1544
|
const patterns = [];
|
|
1599
1545
|
for (const [key, entity] of store[kind]) {
|
|
1600
|
-
if (key.
|
|
1546
|
+
if (key.includes("*")) {
|
|
1547
|
+
const segments = key.split("*");
|
|
1601
1548
|
patterns.push({
|
|
1602
|
-
|
|
1549
|
+
segments,
|
|
1550
|
+
staticLength: segments.reduce((n, s) => n + s.length, 0),
|
|
1603
1551
|
entity
|
|
1604
1552
|
});
|
|
1605
1553
|
}
|
|
@@ -1610,13 +1558,25 @@ function createRegistry() {
|
|
|
1610
1558
|
);
|
|
1611
1559
|
return patterns;
|
|
1612
1560
|
};
|
|
1561
|
+
const matchesSegments = (segments, id) => {
|
|
1562
|
+
const first = segments[0];
|
|
1563
|
+
const last = segments[segments.length - 1];
|
|
1564
|
+
if (!id.startsWith(first)) return false;
|
|
1565
|
+
let pos = first.length;
|
|
1566
|
+
for (let i = 1; i < segments.length - 1; i++) {
|
|
1567
|
+
const idx = id.indexOf(segments[i], pos);
|
|
1568
|
+
if (idx === -1) return false;
|
|
1569
|
+
pos = idx + segments[i].length;
|
|
1570
|
+
}
|
|
1571
|
+
return id.endsWith(last) && id.length - last.length >= pos;
|
|
1572
|
+
};
|
|
1613
1573
|
const matchPattern = (kind, id) => {
|
|
1614
1574
|
assertEntityKind(kind);
|
|
1615
1575
|
const patterns = getPatternsForKind(kind);
|
|
1616
1576
|
if (patterns.length === 0) return void 0;
|
|
1617
1577
|
let best;
|
|
1618
1578
|
for (const entry of patterns) {
|
|
1619
|
-
if (
|
|
1579
|
+
if (matchesSegments(entry.segments, id) && (best === void 0 || entry.staticLength > best.staticLength)) {
|
|
1620
1580
|
best = entry;
|
|
1621
1581
|
}
|
|
1622
1582
|
}
|
|
@@ -1770,21 +1730,9 @@ function resolveConventions(c) {
|
|
|
1770
1730
|
function kebab(str) {
|
|
1771
1731
|
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").replace(/[^a-zA-Z0-9-]/g, "").toLowerCase();
|
|
1772
1732
|
}
|
|
1773
|
-
function baseName(file) {
|
|
1774
|
-
const b =
|
|
1775
|
-
return b.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "");
|
|
1776
|
-
}
|
|
1777
|
-
var LANDMARK_RE = /<(header|nav|main|aside|footer)(\s[^>]*)?>|role=["']region["']/gi;
|
|
1778
|
-
function extractLandmarks(file) {
|
|
1779
|
-
const out2 = [];
|
|
1780
|
-
LANDMARK_RE.lastIndex = 0;
|
|
1781
|
-
let m;
|
|
1782
|
-
while ((m = LANDMARK_RE.exec(file.content)) !== null) {
|
|
1783
|
-
const tag = m[1] ?? "region";
|
|
1784
|
-
const line = 1 + file.content.slice(0, m.index).split("\n").length - 1;
|
|
1785
|
-
out2.push({ tag, line });
|
|
1786
|
-
}
|
|
1787
|
-
return out2;
|
|
1733
|
+
function baseName(file) {
|
|
1734
|
+
const b = path4.posix.basename(file);
|
|
1735
|
+
return b.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "");
|
|
1788
1736
|
}
|
|
1789
1737
|
function fileMatchesAny(displayPath, patterns) {
|
|
1790
1738
|
return patterns.some((g) => globToRegExp(g).test(displayPath));
|
|
@@ -1846,7 +1794,7 @@ function resolve2(ctx) {
|
|
|
1846
1794
|
const routes = conventions.pages === "auto" ? detectRoutes(ctx.extracted.map((e) => e.file)) : [];
|
|
1847
1795
|
const handledPageFiles = /* @__PURE__ */ new Set();
|
|
1848
1796
|
for (const route of routes) {
|
|
1849
|
-
const routeDir =
|
|
1797
|
+
const routeDir = path4.posix.dirname(route.file);
|
|
1850
1798
|
const wellKnownPath = `${routeDir}/${WELL_KNOWN_FILES.page}`;
|
|
1851
1799
|
const wellKnownExp = exportFor(wellKnownPath, "page");
|
|
1852
1800
|
const routeExp = exportFor(route.file, "page");
|
|
@@ -1900,7 +1848,7 @@ function resolve2(ctx) {
|
|
|
1900
1848
|
const dir = extractFeatureDir(ef.file.displayPath, featureGlob);
|
|
1901
1849
|
if (!dir) continue;
|
|
1902
1850
|
conventionalFeatureDirs.add(dir);
|
|
1903
|
-
const isWellKnown =
|
|
1851
|
+
const isWellKnown = path4.posix.basename(ef.file.displayPath) === WELL_KNOWN_FILES.feature;
|
|
1904
1852
|
if (isWellKnown) wellKnownFeatureFileByDir.set(dir, ef.file.displayPath);
|
|
1905
1853
|
const exp = exportFor(ef.file.displayPath, "feature");
|
|
1906
1854
|
if (exp) {
|
|
@@ -1937,7 +1885,7 @@ function resolve2(ctx) {
|
|
|
1937
1885
|
} else if (allExports.length > 0) {
|
|
1938
1886
|
exp = allExports[0].exp;
|
|
1939
1887
|
}
|
|
1940
|
-
const id = exp && typeof exp.id === "string" ? exp.id :
|
|
1888
|
+
const id = exp && typeof exp.id === "string" ? exp.id : path4.posix.basename(dir);
|
|
1941
1889
|
const meta = exp ? buildMetaFromExport(exp) : void 0;
|
|
1942
1890
|
const feature = {
|
|
1943
1891
|
kind: "feature",
|
|
@@ -2033,8 +1981,8 @@ function resolve2(ctx) {
|
|
|
2033
1981
|
}
|
|
2034
1982
|
if (conventions.regions === "landmarks") {
|
|
2035
1983
|
for (const ef of ctx.extracted) {
|
|
2036
|
-
for (const lm of
|
|
2037
|
-
const id =
|
|
1984
|
+
for (const lm of ef.landmarks ?? []) {
|
|
1985
|
+
const id = lm.tag;
|
|
2038
1986
|
if (!registry.get("region", id)) {
|
|
2039
1987
|
const meta = metaWithComposes("region", id);
|
|
2040
1988
|
const region = {
|
|
@@ -2145,7 +2093,7 @@ function resolve2(ctx) {
|
|
|
2145
2093
|
const flowExport = (ff.metadata ?? []).find(
|
|
2146
2094
|
(m) => m.kind === "flow" && typeof m.id === "string"
|
|
2147
2095
|
);
|
|
2148
|
-
const derived =
|
|
2096
|
+
const derived = flowsFromFacts(ff);
|
|
2149
2097
|
if (flowExport && typeof flowExport.id === "string" && derived.length === 1) {
|
|
2150
2098
|
const base = derived[0];
|
|
2151
2099
|
const flow = {
|
|
@@ -2200,60 +2148,21 @@ function computeScope(displayPath) {
|
|
|
2200
2148
|
}
|
|
2201
2149
|
return null;
|
|
2202
2150
|
}
|
|
2203
|
-
function
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
const line = 1 + source.slice(0, m.index).split("\n").length - 1;
|
|
2212
|
-
const after = source.slice(m.index + m[0].length);
|
|
2213
|
-
const arrow = after.match(/=>\s*\{/);
|
|
2214
|
-
if (!arrow || arrow.index === void 0) continue;
|
|
2215
|
-
const bodyStart = m.index + m[0].length + arrow.index + arrow[0].length;
|
|
2216
|
-
let depth = 1;
|
|
2217
|
-
let bodyEnd = -1;
|
|
2218
|
-
for (let i = bodyStart; i < source.length; i++) {
|
|
2219
|
-
if (source[i] === "{") depth++;
|
|
2220
|
-
else if (source[i] === "}") {
|
|
2221
|
-
depth--;
|
|
2222
|
-
if (depth === 0) {
|
|
2223
|
-
bodyEnd = i;
|
|
2224
|
-
break;
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
|
-
if (bodyEnd === -1) continue;
|
|
2229
|
-
const body = source.slice(bodyStart, bodyEnd);
|
|
2230
|
-
const touches = captureUidexIds(body);
|
|
2231
|
-
flows.push({
|
|
2232
|
-
kind: "flow",
|
|
2233
|
-
id,
|
|
2234
|
-
loc: { file: file.displayPath, line },
|
|
2235
|
-
touches: dedupe(touches.map((t) => t.id)),
|
|
2236
|
-
steps: touches.filter((t) => t.action).map((t) => ({ entityId: t.id, action: t.action }))
|
|
2237
|
-
});
|
|
2238
|
-
}
|
|
2239
|
-
return flows;
|
|
2240
|
-
}
|
|
2241
|
-
function captureUidexIds(body) {
|
|
2242
|
-
const out2 = [];
|
|
2243
|
-
const re = /uidex\(\s*(?:'([^']+)'|"([^"]+)"|`([^`$]+)`)\s*\)(?:\.(\w+)\s*\()?/g;
|
|
2244
|
-
let m;
|
|
2245
|
-
while ((m = re.exec(body)) !== null) {
|
|
2246
|
-
out2.push({ id: m[1] || m[2] || m[3], action: m[4] });
|
|
2247
|
-
}
|
|
2248
|
-
return out2;
|
|
2151
|
+
function flowsFromFacts(ff) {
|
|
2152
|
+
return (ff.flows ?? []).map((fact) => ({
|
|
2153
|
+
kind: "flow",
|
|
2154
|
+
id: kebab(fact.title),
|
|
2155
|
+
loc: { file: ff.file.displayPath, line: fact.line },
|
|
2156
|
+
touches: dedupe(fact.calls.map((c) => c.id)),
|
|
2157
|
+
steps: fact.calls.filter((c) => c.action).map((c) => ({ entityId: c.id, action: c.action }))
|
|
2158
|
+
}));
|
|
2249
2159
|
}
|
|
2250
2160
|
function dedupe(arr) {
|
|
2251
2161
|
return Array.from(new Set(arr));
|
|
2252
2162
|
}
|
|
2253
2163
|
|
|
2254
2164
|
// src/scanner/scan/audit.ts
|
|
2255
|
-
import * as
|
|
2256
|
-
var MARKER_FILENAMES = ["UIDEX_PAGE.md", "UIDEX_FEATURE.md"];
|
|
2165
|
+
import * as path5 from "path";
|
|
2257
2166
|
function audit(opts) {
|
|
2258
2167
|
const diagnostics = [];
|
|
2259
2168
|
const { registry, extracted, files, config } = opts;
|
|
@@ -2263,22 +2172,15 @@ function audit(opts) {
|
|
|
2263
2172
|
const scopeLeakEnabled = config.audit?.scopeLeak ?? true;
|
|
2264
2173
|
const coverageEnabled = config.audit?.coverage ?? true;
|
|
2265
2174
|
if (opts.resolveDiagnostics) diagnostics.push(...opts.resolveDiagnostics);
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
code: "marker-md-ignored",
|
|
2272
|
-
severity: "warning",
|
|
2273
|
-
message: `Marker file "${base}" is ignored in v2; migrate to \`export const uidex\``,
|
|
2274
|
-
file: f.displayPath
|
|
2275
|
-
});
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2175
|
+
for (const ef of extracted) {
|
|
2176
|
+
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
2177
|
+
}
|
|
2178
|
+
for (const ef of opts.flowExtracted ?? []) {
|
|
2179
|
+
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
2278
2180
|
}
|
|
2279
2181
|
if (check && opts.generated !== void 0) {
|
|
2280
2182
|
const outRel = opts.outputPath ?? config.output;
|
|
2281
|
-
const fresh =
|
|
2183
|
+
const fresh = normalizeForCheck(opts.generated);
|
|
2282
2184
|
if (opts.existingOnDisk === null || opts.existingOnDisk === void 0) {
|
|
2283
2185
|
diagnostics.push({
|
|
2284
2186
|
code: "gen-missing",
|
|
@@ -2288,7 +2190,7 @@ function audit(opts) {
|
|
|
2288
2190
|
hint: "Run `uidex scan` (without --check) to regenerate"
|
|
2289
2191
|
});
|
|
2290
2192
|
} else {
|
|
2291
|
-
const existing =
|
|
2193
|
+
const existing = normalizeForCheck(opts.existingOnDisk);
|
|
2292
2194
|
if (existing !== fresh) {
|
|
2293
2195
|
const changed = diffEntities(existing, opts.generated, registry);
|
|
2294
2196
|
const summary2 = formatChangedSummary(changed);
|
|
@@ -2302,22 +2204,6 @@ function audit(opts) {
|
|
|
2302
2204
|
}
|
|
2303
2205
|
}
|
|
2304
2206
|
}
|
|
2305
|
-
if (lint) {
|
|
2306
|
-
for (const ef of extracted) {
|
|
2307
|
-
for (const a of ef.annotations) {
|
|
2308
|
-
const migration = legacyJsdocMigration(a);
|
|
2309
|
-
if (!migration) continue;
|
|
2310
|
-
diagnostics.push({
|
|
2311
|
-
code: "legacy-jsdoc",
|
|
2312
|
-
severity: "warning",
|
|
2313
|
-
message: migration.message,
|
|
2314
|
-
file: a.file,
|
|
2315
|
-
line: a.line,
|
|
2316
|
-
hint: migration.hint
|
|
2317
|
-
});
|
|
2318
|
-
}
|
|
2319
|
-
}
|
|
2320
|
-
}
|
|
2321
2207
|
if (lint && acceptanceEnabled) {
|
|
2322
2208
|
for (const kind of ["widget", "feature", "page"]) {
|
|
2323
2209
|
for (const e of registry.list(kind)) {
|
|
@@ -2363,8 +2249,8 @@ function audit(opts) {
|
|
|
2363
2249
|
if (typeof m.id !== "string") continue;
|
|
2364
2250
|
const filePath = ef.file.displayPath;
|
|
2365
2251
|
const wellKnownName = WELL_KNOWN_FILES[m.kind];
|
|
2366
|
-
if (
|
|
2367
|
-
const dir =
|
|
2252
|
+
if (path5.posix.basename(filePath) === wellKnownName) continue;
|
|
2253
|
+
const dir = path5.posix.dirname(filePath);
|
|
2368
2254
|
const wellKnownPath = dir === "." ? wellKnownName : `${dir}/${wellKnownName}`;
|
|
2369
2255
|
if (scannedPaths.has(wellKnownPath)) continue;
|
|
2370
2256
|
const kindLabel = m.kind === "page" ? "Page" : "Feature";
|
|
@@ -2381,53 +2267,55 @@ function audit(opts) {
|
|
|
2381
2267
|
}
|
|
2382
2268
|
}
|
|
2383
2269
|
if (lint) {
|
|
2384
|
-
const
|
|
2385
|
-
|
|
2386
|
-
for (const f of files) {
|
|
2387
|
-
const templatePrefixPositions = /* @__PURE__ */ new Set();
|
|
2388
|
-
templateWithPrefixRe.lastIndex = 0;
|
|
2389
|
-
let tm;
|
|
2390
|
-
while ((tm = templateWithPrefixRe.exec(f.content)) !== null) {
|
|
2391
|
-
templatePrefixPositions.add(tm.index);
|
|
2392
|
-
}
|
|
2393
|
-
let m;
|
|
2394
|
-
dynamicAttrRe.lastIndex = 0;
|
|
2395
|
-
while ((m = dynamicAttrRe.exec(f.content)) !== null) {
|
|
2396
|
-
if (templatePrefixPositions.has(m.index)) continue;
|
|
2397
|
-
const kind = m[1] ?? "element";
|
|
2398
|
-
let line = 1;
|
|
2399
|
-
for (let i = 0; i < m.index; i++) if (f.content[i] === "\n") line++;
|
|
2400
|
-
const attrName = m[1] ? `data-uidex-${m[1]}` : "data-uidex";
|
|
2270
|
+
for (const ef of extracted) {
|
|
2271
|
+
for (const fact of ef.dynamicAttrs ?? []) {
|
|
2401
2272
|
diagnostics.push({
|
|
2402
2273
|
code: "dynamic-attr",
|
|
2403
2274
|
severity: "warning",
|
|
2404
|
-
message: `\`${attrName}={\u2026}\` uses a dynamic expression; the scanner cannot resolve the ${kind} id statically`,
|
|
2405
|
-
file:
|
|
2406
|
-
line,
|
|
2407
|
-
hint: dynamicAttrHint(kind)
|
|
2275
|
+
message: `\`${fact.attrName}={\u2026}\` uses a dynamic expression; the scanner cannot resolve the ${fact.kind} id statically`,
|
|
2276
|
+
file: ef.file.displayPath,
|
|
2277
|
+
line: fact.line,
|
|
2278
|
+
hint: dynamicAttrHint(fact.kind)
|
|
2408
2279
|
});
|
|
2409
2280
|
}
|
|
2410
2281
|
}
|
|
2411
2282
|
}
|
|
2412
2283
|
if (lint) {
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2284
|
+
const usedElementIds = new Set(registry.list("element").map((e) => e.id));
|
|
2285
|
+
for (const ef of extracted) {
|
|
2286
|
+
for (const fact of ef.unannotatedInteractive ?? []) {
|
|
2287
|
+
if (fact.hasSpread) {
|
|
2288
|
+
diagnostics.push({
|
|
2289
|
+
code: "spread-attr",
|
|
2290
|
+
severity: "info",
|
|
2291
|
+
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`,
|
|
2292
|
+
file: ef.file.displayPath,
|
|
2293
|
+
line: fact.line,
|
|
2294
|
+
hint: "Prefer a string-literal data-uidex on the element itself, or annotate at the call site."
|
|
2295
|
+
});
|
|
2296
|
+
} else {
|
|
2297
|
+
const id = uniqueElementId(fact, usedElementIds);
|
|
2298
|
+
usedElementIds.add(id);
|
|
2299
|
+
diagnostics.push({
|
|
2300
|
+
code: "missing-element-annotation",
|
|
2301
|
+
severity: "info",
|
|
2302
|
+
message: `Interactive <${fact.tag}> without data-uidex annotation`,
|
|
2303
|
+
file: ef.file.displayPath,
|
|
2304
|
+
line: fact.line,
|
|
2305
|
+
hint: `Add \`data-uidex="${id}"\` (or run \`uidex scan --fix\`).`,
|
|
2306
|
+
fix: {
|
|
2307
|
+
description: `Add data-uidex="${id}" to <${fact.tag}>`,
|
|
2308
|
+
edits: [
|
|
2309
|
+
{
|
|
2310
|
+
path: ef.file.sourcePath,
|
|
2311
|
+
start: fact.nameEnd,
|
|
2312
|
+
end: fact.nameEnd,
|
|
2313
|
+
replacement: ` data-uidex="${id}"`
|
|
2314
|
+
}
|
|
2315
|
+
]
|
|
2316
|
+
}
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2431
2319
|
}
|
|
2432
2320
|
}
|
|
2433
2321
|
}
|
|
@@ -2444,12 +2332,11 @@ function audit(opts) {
|
|
|
2444
2332
|
}
|
|
2445
2333
|
}
|
|
2446
2334
|
}
|
|
2447
|
-
for (const
|
|
2448
|
-
const
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
const
|
|
2452
|
-
const baseName2 = spec.split("/").pop() ?? "";
|
|
2335
|
+
for (const ef of extracted) {
|
|
2336
|
+
const displayPath = ef.file.displayPath;
|
|
2337
|
+
for (const imp of ef.imports ?? []) {
|
|
2338
|
+
if (imp.isTypeOnly) continue;
|
|
2339
|
+
const baseName2 = imp.specifier.split("/").pop() ?? "";
|
|
2453
2340
|
const primitive = byName.get(
|
|
2454
2341
|
baseName2.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
|
|
2455
2342
|
);
|
|
@@ -2457,25 +2344,37 @@ function audit(opts) {
|
|
|
2457
2344
|
const scope = primitive.scopes?.[0];
|
|
2458
2345
|
if (!scope) continue;
|
|
2459
2346
|
const [kind, id] = scope.split(":");
|
|
2460
|
-
const importerSegments =
|
|
2347
|
+
const importerSegments = displayPath.split("/");
|
|
2461
2348
|
if (importerSegments.includes(id) && importerSegments.includes(kind + "s")) {
|
|
2462
2349
|
continue;
|
|
2463
2350
|
}
|
|
2464
2351
|
if (kind === "feature" && importerSegments.includes(id)) continue;
|
|
2465
|
-
if (kind === "feature" && declaredFeatures.get(
|
|
2352
|
+
if (kind === "feature" && declaredFeatures.get(displayPath)?.has(id)) {
|
|
2466
2353
|
continue;
|
|
2467
2354
|
}
|
|
2468
2355
|
diagnostics.push({
|
|
2469
2356
|
code: "scope-leak",
|
|
2470
2357
|
severity: "warning",
|
|
2471
|
-
message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${
|
|
2472
|
-
file:
|
|
2358
|
+
message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${displayPath}`,
|
|
2359
|
+
file: displayPath,
|
|
2360
|
+
line: imp.line
|
|
2473
2361
|
});
|
|
2474
2362
|
}
|
|
2475
2363
|
}
|
|
2476
2364
|
}
|
|
2477
2365
|
if (lint && coverageEnabled) {
|
|
2366
|
+
const factsByLoc = /* @__PURE__ */ new Map();
|
|
2367
|
+
for (const ef of opts.flowExtracted ?? []) {
|
|
2368
|
+
for (const fact of ef.flows ?? []) {
|
|
2369
|
+
const lines = /* @__PURE__ */ new Map();
|
|
2370
|
+
for (const call of fact.calls) {
|
|
2371
|
+
if (!lines.has(call.id)) lines.set(call.id, call.line);
|
|
2372
|
+
}
|
|
2373
|
+
factsByLoc.set(`${ef.file.displayPath}:${fact.line}`, lines);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2478
2376
|
for (const flow of registry.list("flow")) {
|
|
2377
|
+
const callLines = factsByLoc.get(`${flow.loc.file}:${flow.loc.line}`);
|
|
2479
2378
|
for (const touchedId of flow.touches) {
|
|
2480
2379
|
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);
|
|
2481
2380
|
if (!found) {
|
|
@@ -2484,54 +2383,131 @@ function audit(opts) {
|
|
|
2484
2383
|
severity: "warning",
|
|
2485
2384
|
message: `Flow "${flow.id}" references unknown entity "${touchedId}"`,
|
|
2486
2385
|
file: flow.loc.file,
|
|
2487
|
-
|
|
2386
|
+
// Point at the uidex() call itself when the spec facts are
|
|
2387
|
+
// available; the describe line is the fallback.
|
|
2388
|
+
line: callLines?.get(touchedId) ?? flow.loc.line
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
if (lint) {
|
|
2395
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
2396
|
+
for (const ef of extracted) {
|
|
2397
|
+
for (const a of ef.annotations) {
|
|
2398
|
+
if (a.kind !== "element" && a.kind !== "region" && a.kind !== "widget" && a.kind !== "primitive") {
|
|
2399
|
+
continue;
|
|
2400
|
+
}
|
|
2401
|
+
const key = `${a.kind}:${a.id}`;
|
|
2402
|
+
let list = occurrences.get(key);
|
|
2403
|
+
if (!list) {
|
|
2404
|
+
list = [];
|
|
2405
|
+
occurrences.set(key, list);
|
|
2406
|
+
}
|
|
2407
|
+
list.push({ file: a.file, line: a.line });
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
for (const [key, list] of occurrences) {
|
|
2411
|
+
const filesSeen = new Set(list.map((o) => o.file));
|
|
2412
|
+
if (filesSeen.size < 2) continue;
|
|
2413
|
+
const [kind, id] = key.split(/:(.*)/s);
|
|
2414
|
+
const others = list.slice(1).map((o) => `${o.file}:${o.line}`).join(", ");
|
|
2415
|
+
diagnostics.push({
|
|
2416
|
+
code: "duplicate-id",
|
|
2417
|
+
severity: kind === "widget" || kind === "primitive" ? "warning" : "info",
|
|
2418
|
+
message: `${kind} id "${id}" is declared in ${filesSeen.size} files (also at ${others}); the registry keeps only one entry`,
|
|
2419
|
+
file: list[0].file,
|
|
2420
|
+
line: list[0].line,
|
|
2421
|
+
entity: { kind, id },
|
|
2422
|
+
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."
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
if (lint && coverageEnabled) {
|
|
2427
|
+
for (const ef of opts.flowExtracted ?? []) {
|
|
2428
|
+
for (const fact of ef.flows ?? []) {
|
|
2429
|
+
for (const dyn of fact.dynamicCalls ?? []) {
|
|
2430
|
+
diagnostics.push({
|
|
2431
|
+
code: "dynamic-flow-reference",
|
|
2432
|
+
severity: "warning",
|
|
2433
|
+
message: `\`uidex(\u2026)\` call in flow "${fact.title}" uses a dynamic expression; the id is invisible to coverage and registry validation`,
|
|
2434
|
+
file: ef.file.displayPath,
|
|
2435
|
+
line: dyn.line,
|
|
2436
|
+
hint: "Use a string-literal id (component ids inside uidex() must be statically analysable)."
|
|
2488
2437
|
});
|
|
2489
2438
|
}
|
|
2490
2439
|
}
|
|
2491
2440
|
}
|
|
2492
2441
|
}
|
|
2442
|
+
if (lint && coverageEnabled) {
|
|
2443
|
+
for (const ef of extracted) {
|
|
2444
|
+
if (!ef.metadata) continue;
|
|
2445
|
+
for (const m of ef.metadata) {
|
|
2446
|
+
const check2 = (refKind, ids, spans) => {
|
|
2447
|
+
for (let i = 0; i < (ids?.length ?? 0); i++) {
|
|
2448
|
+
const refId = ids[i];
|
|
2449
|
+
const found = registry.get(refKind, refId) ?? registry.matchPattern(refKind, refId);
|
|
2450
|
+
if (found) continue;
|
|
2451
|
+
diagnostics.push({
|
|
2452
|
+
code: "unknown-reference",
|
|
2453
|
+
severity: "warning",
|
|
2454
|
+
message: `\`export const uidex\` in ${ef.file.displayPath} references unknown ${refKind} "${refId}"`,
|
|
2455
|
+
file: ef.file.displayPath,
|
|
2456
|
+
line: spans?.[i] ? lineOfOffset(ef.file.content, spans[i].start) : m.loc.line,
|
|
2457
|
+
hint: `No ${refKind} with id "${refId}" exists in the registry; fix the reference or add the ${refKind}.`
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
};
|
|
2461
|
+
check2("feature", m.features, m.featureSpans);
|
|
2462
|
+
check2("widget", m.widgets, m.widgetSpans);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2493
2466
|
const summary = {
|
|
2494
2467
|
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
2495
2468
|
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
2496
2469
|
};
|
|
2497
2470
|
return { diagnostics, summary };
|
|
2498
2471
|
}
|
|
2499
|
-
function
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
};
|
|
2528
|
-
default:
|
|
2529
|
-
return null;
|
|
2472
|
+
function lineOfOffset(content, offset) {
|
|
2473
|
+
let line = 1;
|
|
2474
|
+
for (let i = 0; i < offset && i < content.length; i++) {
|
|
2475
|
+
if (content[i] === "\n") line++;
|
|
2476
|
+
}
|
|
2477
|
+
return line;
|
|
2478
|
+
}
|
|
2479
|
+
var TAG_FALLBACK_ID = {
|
|
2480
|
+
a: "link",
|
|
2481
|
+
button: "button",
|
|
2482
|
+
input: "input",
|
|
2483
|
+
select: "select",
|
|
2484
|
+
textarea: "textarea"
|
|
2485
|
+
};
|
|
2486
|
+
function kebabId(str) {
|
|
2487
|
+
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
2488
|
+
}
|
|
2489
|
+
function deriveElementId(fact) {
|
|
2490
|
+
const fromHint = fact.nameHint ? kebabId(fact.nameHint) : "";
|
|
2491
|
+
const capped = fromHint.split("-").filter(Boolean).slice(0, 5).join("-");
|
|
2492
|
+
return capped || TAG_FALLBACK_ID[fact.tag] || fact.tag;
|
|
2493
|
+
}
|
|
2494
|
+
function uniqueElementId(fact, used) {
|
|
2495
|
+
const base = deriveElementId(fact);
|
|
2496
|
+
if (!used.has(base)) return base;
|
|
2497
|
+
for (let n = 2; ; n++) {
|
|
2498
|
+
const candidate = `${base}-${n}`;
|
|
2499
|
+
if (!used.has(candidate)) return candidate;
|
|
2530
2500
|
}
|
|
2531
2501
|
}
|
|
2532
2502
|
function normalizeLineEndings(s) {
|
|
2533
2503
|
return s.replace(/\r\n/g, "\n");
|
|
2534
2504
|
}
|
|
2505
|
+
function normalizeForCheck(s) {
|
|
2506
|
+
return normalizeLineEndings(s).replace(
|
|
2507
|
+
/export const gitContext = \{[\s\S]*?\} as const/,
|
|
2508
|
+
"export const gitContext = {} as const"
|
|
2509
|
+
);
|
|
2510
|
+
}
|
|
2535
2511
|
function formatChangedSummary(change) {
|
|
2536
2512
|
const parts = [];
|
|
2537
2513
|
const fmt = (kind, names) => {
|
|
@@ -2630,62 +2606,11 @@ function extractEntitiesArray(source) {
|
|
|
2630
2606
|
}
|
|
2631
2607
|
return null;
|
|
2632
2608
|
}
|
|
2633
|
-
function findJsxOpeningEnd(src, start) {
|
|
2634
|
-
let i = start;
|
|
2635
|
-
while (i < src.length) {
|
|
2636
|
-
const ch = src[i];
|
|
2637
|
-
if (ch === ">" || ch === "/" && src[i + 1] === ">") return i;
|
|
2638
|
-
if (ch === '"' || ch === "'" || ch === "`") {
|
|
2639
|
-
i = skipString2(src, i);
|
|
2640
|
-
} else if (ch === "{") {
|
|
2641
|
-
i = skipBraces(src, i);
|
|
2642
|
-
} else {
|
|
2643
|
-
i++;
|
|
2644
|
-
}
|
|
2645
|
-
}
|
|
2646
|
-
return -1;
|
|
2647
|
-
}
|
|
2648
|
-
function skipString2(src, start) {
|
|
2649
|
-
const quote = src[start];
|
|
2650
|
-
let i = start + 1;
|
|
2651
|
-
while (i < src.length) {
|
|
2652
|
-
if (src[i] === "\\" && quote !== "`") {
|
|
2653
|
-
i += 2;
|
|
2654
|
-
continue;
|
|
2655
|
-
}
|
|
2656
|
-
if (quote === "`" && src[i] === "$" && src[i + 1] === "{") {
|
|
2657
|
-
i = skipBraces(src, i + 1);
|
|
2658
|
-
continue;
|
|
2659
|
-
}
|
|
2660
|
-
if (src[i] === quote) return i + 1;
|
|
2661
|
-
i++;
|
|
2662
|
-
}
|
|
2663
|
-
return i;
|
|
2664
|
-
}
|
|
2665
|
-
function skipBraces(src, start) {
|
|
2666
|
-
let depth = 1;
|
|
2667
|
-
let i = start + 1;
|
|
2668
|
-
while (i < src.length && depth > 0) {
|
|
2669
|
-
const ch = src[i];
|
|
2670
|
-
if (ch === "{") {
|
|
2671
|
-
depth++;
|
|
2672
|
-
i++;
|
|
2673
|
-
} else if (ch === "}") {
|
|
2674
|
-
depth--;
|
|
2675
|
-
i++;
|
|
2676
|
-
} else if (ch === '"' || ch === "'" || ch === "`") {
|
|
2677
|
-
i = skipString2(src, i);
|
|
2678
|
-
} else {
|
|
2679
|
-
i++;
|
|
2680
|
-
}
|
|
2681
|
-
}
|
|
2682
|
-
return i;
|
|
2683
|
-
}
|
|
2684
2609
|
function dynamicAttrHint(kind) {
|
|
2685
2610
|
if (kind === "region") {
|
|
2686
2611
|
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`;
|
|
2687
2612
|
}
|
|
2688
|
-
return `The scanner
|
|
2613
|
+
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)`;
|
|
2689
2614
|
}
|
|
2690
2615
|
function stableStringify(value) {
|
|
2691
2616
|
return JSON.stringify(value, stableReplacer);
|
|
@@ -2718,9 +2643,7 @@ function replacerSorted(_key, value) {
|
|
|
2718
2643
|
}
|
|
2719
2644
|
return value;
|
|
2720
2645
|
}
|
|
2721
|
-
function emitIdUnion(name, ids
|
|
2722
|
-
if (typeMode === "loose") return `export type ${name} = string
|
|
2723
|
-
`;
|
|
2646
|
+
function emitIdUnion(name, ids) {
|
|
2724
2647
|
if (ids.length === 0) return `export type ${name} = never
|
|
2725
2648
|
`;
|
|
2726
2649
|
const sorted = [...ids].sort();
|
|
@@ -2730,12 +2653,7 @@ ${body}
|
|
|
2730
2653
|
`;
|
|
2731
2654
|
}
|
|
2732
2655
|
function emit(opts) {
|
|
2733
|
-
const {
|
|
2734
|
-
registry,
|
|
2735
|
-
gitContext,
|
|
2736
|
-
uidexImport = "uidex",
|
|
2737
|
-
typeMode = "strict"
|
|
2738
|
-
} = opts;
|
|
2656
|
+
const { registry, gitContext, uidexImport = "uidex" } = opts;
|
|
2739
2657
|
const routes = [...registry.list("route")].sort(
|
|
2740
2658
|
(a, b) => a.path.localeCompare(b.path)
|
|
2741
2659
|
);
|
|
@@ -2758,57 +2676,49 @@ function emit(opts) {
|
|
|
2758
2676
|
lines.push(
|
|
2759
2677
|
emitIdUnion(
|
|
2760
2678
|
"PageId",
|
|
2761
|
-
pages.map((e) => e.id)
|
|
2762
|
-
typeMode
|
|
2679
|
+
pages.map((e) => e.id)
|
|
2763
2680
|
)
|
|
2764
2681
|
);
|
|
2765
2682
|
lines.push(
|
|
2766
2683
|
emitIdUnion(
|
|
2767
2684
|
"FeatureId",
|
|
2768
|
-
features.map((e) => e.id)
|
|
2769
|
-
typeMode
|
|
2685
|
+
features.map((e) => e.id)
|
|
2770
2686
|
)
|
|
2771
2687
|
);
|
|
2772
2688
|
lines.push(
|
|
2773
2689
|
emitIdUnion(
|
|
2774
2690
|
"WidgetId",
|
|
2775
|
-
widgets.map((e) => e.id)
|
|
2776
|
-
typeMode
|
|
2691
|
+
widgets.map((e) => e.id)
|
|
2777
2692
|
)
|
|
2778
2693
|
);
|
|
2779
2694
|
lines.push(
|
|
2780
2695
|
emitIdUnion(
|
|
2781
2696
|
"RegionId",
|
|
2782
|
-
regions.map((e) => e.id)
|
|
2783
|
-
typeMode
|
|
2697
|
+
regions.map((e) => e.id)
|
|
2784
2698
|
)
|
|
2785
2699
|
);
|
|
2786
2700
|
lines.push(
|
|
2787
2701
|
emitIdUnion(
|
|
2788
2702
|
"ElementId",
|
|
2789
|
-
elements.map((e) => e.id)
|
|
2790
|
-
typeMode
|
|
2703
|
+
elements.map((e) => e.id)
|
|
2791
2704
|
)
|
|
2792
2705
|
);
|
|
2793
2706
|
lines.push(
|
|
2794
2707
|
emitIdUnion(
|
|
2795
2708
|
"PrimitiveId",
|
|
2796
|
-
primitives.map((e) => e.id)
|
|
2797
|
-
typeMode
|
|
2709
|
+
primitives.map((e) => e.id)
|
|
2798
2710
|
)
|
|
2799
2711
|
);
|
|
2800
2712
|
lines.push(
|
|
2801
2713
|
emitIdUnion(
|
|
2802
2714
|
"FlowId",
|
|
2803
|
-
flows.map((e) => e.id)
|
|
2804
|
-
typeMode
|
|
2715
|
+
flows.map((e) => e.id)
|
|
2805
2716
|
)
|
|
2806
2717
|
);
|
|
2807
2718
|
lines.push(
|
|
2808
2719
|
emitIdUnion(
|
|
2809
2720
|
"RouteId",
|
|
2810
|
-
routes.map((e) => e.path)
|
|
2811
|
-
typeMode
|
|
2721
|
+
routes.map((e) => e.path)
|
|
2812
2722
|
)
|
|
2813
2723
|
);
|
|
2814
2724
|
lines.push("");
|
|
@@ -2921,22 +2831,33 @@ function parseGitHubRef(ref) {
|
|
|
2921
2831
|
|
|
2922
2832
|
// src/scanner/scan/scaffold.ts
|
|
2923
2833
|
import * as fs3 from "fs";
|
|
2924
|
-
import * as
|
|
2834
|
+
import * as path6 from "path";
|
|
2925
2835
|
function scaffoldWidgetSpec(opts) {
|
|
2836
|
+
return scaffoldSpec({
|
|
2837
|
+
registry: opts.registry,
|
|
2838
|
+
kind: "widget",
|
|
2839
|
+
id: opts.widgetId,
|
|
2840
|
+
outDir: opts.outDir,
|
|
2841
|
+
force: opts.force,
|
|
2842
|
+
fixtureImport: opts.fixtureImport
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
function scaffoldSpec(opts) {
|
|
2926
2846
|
const {
|
|
2927
2847
|
registry,
|
|
2928
|
-
|
|
2848
|
+
kind,
|
|
2849
|
+
id,
|
|
2929
2850
|
outDir,
|
|
2930
2851
|
force = false,
|
|
2931
2852
|
fixtureImport = "./fixtures"
|
|
2932
2853
|
} = opts;
|
|
2933
|
-
const
|
|
2934
|
-
if (!
|
|
2935
|
-
throw new Error(
|
|
2854
|
+
const entity = registry.get(kind, id);
|
|
2855
|
+
if (!entity) {
|
|
2856
|
+
throw new Error(`${capitalize(kind)} "${id}" not found in registry`);
|
|
2936
2857
|
}
|
|
2937
|
-
const criteria =
|
|
2938
|
-
const filename = `widget-${
|
|
2939
|
-
const outputPath =
|
|
2858
|
+
const criteria = entity.meta?.acceptance ?? [];
|
|
2859
|
+
const filename = kind === "widget" ? `widget-${id}.spec.ts` : `flow-${id}.spec.ts`;
|
|
2860
|
+
const outputPath = path6.resolve(outDir, filename);
|
|
2940
2861
|
if (fs3.existsSync(outputPath) && !force) {
|
|
2941
2862
|
return {
|
|
2942
2863
|
outputPath,
|
|
@@ -2945,15 +2866,14 @@ function scaffoldWidgetSpec(opts) {
|
|
|
2945
2866
|
reason: `spec already exists at ${outputPath}; pass --force to overwrite`
|
|
2946
2867
|
};
|
|
2947
2868
|
}
|
|
2948
|
-
const content = renderSpec({
|
|
2949
|
-
|
|
2950
|
-
criteria,
|
|
2951
|
-
fixtureImport
|
|
2952
|
-
});
|
|
2953
|
-
fs3.mkdirSync(path5.dirname(outputPath), { recursive: true });
|
|
2869
|
+
const content = renderSpec({ id, criteria, fixtureImport });
|
|
2870
|
+
fs3.mkdirSync(path6.dirname(outputPath), { recursive: true });
|
|
2954
2871
|
fs3.writeFileSync(outputPath, content, "utf8");
|
|
2955
2872
|
return { outputPath, written: true, skipped: false };
|
|
2956
2873
|
}
|
|
2874
|
+
function capitalize(s) {
|
|
2875
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
2876
|
+
}
|
|
2957
2877
|
function renderSpec(args) {
|
|
2958
2878
|
const lines = [];
|
|
2959
2879
|
lines.push(
|
|
@@ -2961,7 +2881,7 @@ function renderSpec(args) {
|
|
|
2961
2881
|
);
|
|
2962
2882
|
lines.push("");
|
|
2963
2883
|
lines.push(
|
|
2964
|
-
`test.describe(${JSON.stringify(args.
|
|
2884
|
+
`test.describe(${JSON.stringify(args.id)}, { tag: "@uidex:flow" }, () => {`
|
|
2965
2885
|
);
|
|
2966
2886
|
if (args.criteria.length === 0) {
|
|
2967
2887
|
lines.push(` test("TODO: add acceptance criteria", async () => {`);
|
|
@@ -2984,7 +2904,7 @@ function renderSpec(args) {
|
|
|
2984
2904
|
|
|
2985
2905
|
// src/scanner/scan/pipeline.ts
|
|
2986
2906
|
import * as fs4 from "fs";
|
|
2987
|
-
import * as
|
|
2907
|
+
import * as path7 from "path";
|
|
2988
2908
|
function runScan(opts = {}) {
|
|
2989
2909
|
const cwd = opts.cwd ?? process.cwd();
|
|
2990
2910
|
const configs = opts.configs ?? discover({ cwd });
|
|
@@ -3013,10 +2933,9 @@ function runOne(dc, opts) {
|
|
|
3013
2933
|
const gitContext = resolveGitContext({ cwd: configDir });
|
|
3014
2934
|
const generated = emit({
|
|
3015
2935
|
registry: resolved.registry,
|
|
3016
|
-
gitContext
|
|
3017
|
-
typeMode: config.typeMode
|
|
2936
|
+
gitContext
|
|
3018
2937
|
});
|
|
3019
|
-
const outputPath =
|
|
2938
|
+
const outputPath = path7.resolve(configDir, config.output);
|
|
3020
2939
|
const outputRel = config.output;
|
|
3021
2940
|
let existingOnDisk = null;
|
|
3022
2941
|
if (opts.check) {
|
|
@@ -3026,12 +2945,16 @@ function runOne(dc, opts) {
|
|
|
3026
2945
|
existingOnDisk = null;
|
|
3027
2946
|
}
|
|
3028
2947
|
}
|
|
2948
|
+
const hasExtractDiagnostics = [...extracted, ...extractedFlows].some(
|
|
2949
|
+
(ef) => (ef.diagnostics?.length ?? 0) > 0
|
|
2950
|
+
);
|
|
3029
2951
|
let auditResult;
|
|
3030
|
-
if (opts.check || opts.lint || resolved.diagnostics.length > 0) {
|
|
2952
|
+
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
|
|
3031
2953
|
auditResult = audit({
|
|
3032
2954
|
registry: resolved.registry,
|
|
3033
2955
|
extracted,
|
|
3034
2956
|
files: sourceFiles,
|
|
2957
|
+
flowExtracted: extractedFlows,
|
|
3035
2958
|
config,
|
|
3036
2959
|
check: opts.check,
|
|
3037
2960
|
lint: opts.lint,
|
|
@@ -3052,34 +2975,281 @@ function runOne(dc, opts) {
|
|
|
3052
2975
|
};
|
|
3053
2976
|
}
|
|
3054
2977
|
function writeScanResult(result) {
|
|
3055
|
-
fs4.mkdirSync(
|
|
2978
|
+
fs4.mkdirSync(path7.dirname(result.outputPath), { recursive: true });
|
|
3056
2979
|
fs4.writeFileSync(result.outputPath, result.generated, "utf8");
|
|
3057
2980
|
}
|
|
3058
2981
|
|
|
2982
|
+
// src/scanner/scan/fix.ts
|
|
2983
|
+
import * as fs5 from "fs";
|
|
2984
|
+
import * as path8 from "path";
|
|
2985
|
+
function applyFixes(diagnostics) {
|
|
2986
|
+
const entries = [];
|
|
2987
|
+
for (const d of diagnostics) {
|
|
2988
|
+
if (!d.fix) continue;
|
|
2989
|
+
entries.push({
|
|
2990
|
+
code: d.code,
|
|
2991
|
+
description: d.fix.description,
|
|
2992
|
+
file: d.file,
|
|
2993
|
+
edits: d.fix.edits ?? [],
|
|
2994
|
+
createFiles: d.fix.createFiles ?? [],
|
|
2995
|
+
deleteFiles: d.fix.deleteFiles ?? []
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
if (entries.length === 0) return { applied: [], skipped: [] };
|
|
2999
|
+
const seenEdits = /* @__PURE__ */ new Set();
|
|
3000
|
+
const editsByFile = /* @__PURE__ */ new Map();
|
|
3001
|
+
for (const entry of entries) {
|
|
3002
|
+
for (const edit of entry.edits) {
|
|
3003
|
+
const key = `${edit.path}:${edit.start}:${edit.end}:${edit.replacement}`;
|
|
3004
|
+
if (seenEdits.has(key)) continue;
|
|
3005
|
+
seenEdits.add(key);
|
|
3006
|
+
let list = editsByFile.get(edit.path);
|
|
3007
|
+
if (!list) {
|
|
3008
|
+
list = [];
|
|
3009
|
+
editsByFile.set(edit.path, list);
|
|
3010
|
+
}
|
|
3011
|
+
list.push({ ...edit, entry });
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
for (const [filePath, edits] of editsByFile) {
|
|
3015
|
+
let content;
|
|
3016
|
+
try {
|
|
3017
|
+
content = fs5.readFileSync(filePath, "utf8");
|
|
3018
|
+
} catch {
|
|
3019
|
+
for (const e of edits) e.entry.skippedReason ??= "file is unreadable";
|
|
3020
|
+
continue;
|
|
3021
|
+
}
|
|
3022
|
+
edits.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
3023
|
+
const kept = [];
|
|
3024
|
+
let prevEnd = -1;
|
|
3025
|
+
for (const edit of edits) {
|
|
3026
|
+
if (edit.start < prevEnd) {
|
|
3027
|
+
edit.entry.skippedReason ??= "overlapping edit";
|
|
3028
|
+
continue;
|
|
3029
|
+
}
|
|
3030
|
+
kept.push(edit);
|
|
3031
|
+
prevEnd = edit.end;
|
|
3032
|
+
}
|
|
3033
|
+
for (let i = kept.length - 1; i >= 0; i--) {
|
|
3034
|
+
const edit = kept[i];
|
|
3035
|
+
content = content.slice(0, edit.start) + edit.replacement + content.slice(edit.end);
|
|
3036
|
+
}
|
|
3037
|
+
if (kept.length > 0) fs5.writeFileSync(filePath, content, "utf8");
|
|
3038
|
+
}
|
|
3039
|
+
for (const entry of entries) {
|
|
3040
|
+
if (entry.skippedReason) continue;
|
|
3041
|
+
for (const create of entry.createFiles) {
|
|
3042
|
+
if (fs5.existsSync(create.path)) {
|
|
3043
|
+
entry.skippedReason = `${path8.basename(create.path)} already exists`;
|
|
3044
|
+
continue;
|
|
3045
|
+
}
|
|
3046
|
+
fs5.mkdirSync(path8.dirname(create.path), { recursive: true });
|
|
3047
|
+
fs5.writeFileSync(create.path, create.content, "utf8");
|
|
3048
|
+
}
|
|
3049
|
+
if (entry.skippedReason) continue;
|
|
3050
|
+
for (const del of entry.deleteFiles) {
|
|
3051
|
+
try {
|
|
3052
|
+
fs5.unlinkSync(del);
|
|
3053
|
+
} catch {
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
const applied = [];
|
|
3058
|
+
const skipped = [];
|
|
3059
|
+
for (const entry of entries) {
|
|
3060
|
+
const summary = {
|
|
3061
|
+
code: entry.code,
|
|
3062
|
+
description: entry.description,
|
|
3063
|
+
file: entry.file
|
|
3064
|
+
};
|
|
3065
|
+
if (entry.skippedReason) {
|
|
3066
|
+
skipped.push({ ...summary, reason: entry.skippedReason });
|
|
3067
|
+
} else {
|
|
3068
|
+
applied.push(summary);
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
return { applied, skipped };
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
// src/scanner/scan/rename.ts
|
|
3075
|
+
var ID_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
3076
|
+
function renameEntity(opts) {
|
|
3077
|
+
const { cwd, kind, oldId, newId, force = false } = opts;
|
|
3078
|
+
const manual = [];
|
|
3079
|
+
const errors = [];
|
|
3080
|
+
if (!ID_RE.test(newId)) {
|
|
3081
|
+
return {
|
|
3082
|
+
edits: 0,
|
|
3083
|
+
manual,
|
|
3084
|
+
errors: [`New id "${newId}" is not kebab-case`]
|
|
3085
|
+
};
|
|
3086
|
+
}
|
|
3087
|
+
const configs = discover({ cwd });
|
|
3088
|
+
if (configs.length === 0) {
|
|
3089
|
+
return { edits: 0, manual, errors: [`No .uidex.json found under ${cwd}`] };
|
|
3090
|
+
}
|
|
3091
|
+
const edits = [];
|
|
3092
|
+
for (const dc of configs) {
|
|
3093
|
+
const { config, configDir } = dc;
|
|
3094
|
+
const sourceFiles = walk(config.sources, {
|
|
3095
|
+
cwd: configDir,
|
|
3096
|
+
globalExcludes: config.exclude
|
|
3097
|
+
});
|
|
3098
|
+
const extracted = extract(sourceFiles);
|
|
3099
|
+
const flowFiles = config.flows ? walk(
|
|
3100
|
+
config.flows.map((glob) => ({ rootDir: ".", include: [glob] })),
|
|
3101
|
+
{ cwd: configDir, includeTests: true }
|
|
3102
|
+
) : [];
|
|
3103
|
+
const extractedFlows = extract(flowFiles);
|
|
3104
|
+
const scan = runScan({ cwd: configDir, configs: [dc] })[0];
|
|
3105
|
+
const registry = scan.registry;
|
|
3106
|
+
if (!registry.get(kind, oldId)) {
|
|
3107
|
+
if (registry.matchPattern(kind, oldId)) {
|
|
3108
|
+
errors.push(
|
|
3109
|
+
`${kind} "${oldId}" only matches via a pattern id; pattern-backed ids cannot be renamed mechanically`
|
|
3110
|
+
);
|
|
3111
|
+
} else {
|
|
3112
|
+
errors.push(`${kind} "${oldId}" not found in registry`);
|
|
3113
|
+
}
|
|
3114
|
+
continue;
|
|
3115
|
+
}
|
|
3116
|
+
if (registry.get(kind, newId) && !force) {
|
|
3117
|
+
errors.push(
|
|
3118
|
+
`${kind} "${newId}" already exists; pass --force to merge the ids`
|
|
3119
|
+
);
|
|
3120
|
+
continue;
|
|
3121
|
+
}
|
|
3122
|
+
const quoteAs = (content, start) => {
|
|
3123
|
+
const q = content[start];
|
|
3124
|
+
return q === '"' || q === "'" || q === "`" ? `${q}${newId}${q}` : `"${newId}"`;
|
|
3125
|
+
};
|
|
3126
|
+
for (const ef of extracted) {
|
|
3127
|
+
for (const a of ef.annotations) {
|
|
3128
|
+
if (a.kind !== kind || a.id !== oldId) continue;
|
|
3129
|
+
if (a.span) {
|
|
3130
|
+
edits.push({
|
|
3131
|
+
path: ef.file.sourcePath,
|
|
3132
|
+
start: a.span.start,
|
|
3133
|
+
end: a.span.end,
|
|
3134
|
+
replacement: quoteAs(ef.file.content, a.span.start)
|
|
3135
|
+
});
|
|
3136
|
+
} else {
|
|
3137
|
+
manual.push({
|
|
3138
|
+
file: a.file,
|
|
3139
|
+
line: a.line,
|
|
3140
|
+
reason: "attribute value is not a plain string literal (const reference, ternary, or template)"
|
|
3141
|
+
});
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
for (const m of ef.metadata ?? []) {
|
|
3145
|
+
if (kind === "widget" && m.kind === "widget" && m.id === oldId) {
|
|
3146
|
+
if (m.idSpan) {
|
|
3147
|
+
edits.push({
|
|
3148
|
+
path: ef.file.sourcePath,
|
|
3149
|
+
start: m.idSpan.start,
|
|
3150
|
+
end: m.idSpan.end,
|
|
3151
|
+
replacement: quoteAs(ef.file.content, m.idSpan.start)
|
|
3152
|
+
});
|
|
3153
|
+
} else {
|
|
3154
|
+
manual.push({
|
|
3155
|
+
file: ef.file.displayPath,
|
|
3156
|
+
line: m.loc.line ?? 1,
|
|
3157
|
+
reason: "widget export id is not a plain string literal"
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
if (kind === "widget" && m.widgets) {
|
|
3162
|
+
for (let i = 0; i < m.widgets.length; i++) {
|
|
3163
|
+
if (m.widgets[i] !== oldId) continue;
|
|
3164
|
+
const span = m.widgetSpans?.[i];
|
|
3165
|
+
if (span) {
|
|
3166
|
+
edits.push({
|
|
3167
|
+
path: ef.file.sourcePath,
|
|
3168
|
+
start: span.start,
|
|
3169
|
+
end: span.end,
|
|
3170
|
+
replacement: quoteAs(ef.file.content, span.start)
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
for (const ef of extractedFlows) {
|
|
3178
|
+
for (const fact of ef.flows ?? []) {
|
|
3179
|
+
for (const call of fact.calls) {
|
|
3180
|
+
if (call.id !== oldId) continue;
|
|
3181
|
+
if (call.span) {
|
|
3182
|
+
edits.push({
|
|
3183
|
+
path: ef.file.sourcePath,
|
|
3184
|
+
start: call.span.start,
|
|
3185
|
+
end: call.span.end,
|
|
3186
|
+
replacement: quoteAs(ef.file.content, call.span.start)
|
|
3187
|
+
});
|
|
3188
|
+
} else {
|
|
3189
|
+
manual.push({
|
|
3190
|
+
file: ef.file.displayPath,
|
|
3191
|
+
line: call.line,
|
|
3192
|
+
reason: "uidex() argument is not a plain string literal"
|
|
3193
|
+
});
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
if (errors.length > 0) {
|
|
3200
|
+
return { edits: 0, manual, errors };
|
|
3201
|
+
}
|
|
3202
|
+
if (edits.length === 0 && manual.length === 0) {
|
|
3203
|
+
return {
|
|
3204
|
+
edits: 0,
|
|
3205
|
+
manual,
|
|
3206
|
+
errors: [
|
|
3207
|
+
`${kind} "${oldId}" has no editable occurrences (convention-derived ids like landmarks cannot be renamed)`
|
|
3208
|
+
]
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
const result = applyFixes([
|
|
3212
|
+
{
|
|
3213
|
+
code: "rename",
|
|
3214
|
+
severity: "info",
|
|
3215
|
+
message: "",
|
|
3216
|
+
fix: {
|
|
3217
|
+
description: `Rename ${kind} "${oldId}" to "${newId}"`,
|
|
3218
|
+
edits
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
]);
|
|
3222
|
+
if (result.skipped.length > 0) {
|
|
3223
|
+
errors.push(`Some edits were skipped: ${result.skipped[0].reason}`);
|
|
3224
|
+
}
|
|
3225
|
+
for (const r of runScan({ cwd })) writeScanResult(r);
|
|
3226
|
+
return { edits: edits.length, manual, errors };
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3059
3229
|
// src/scanner/scan/cli.ts
|
|
3060
|
-
import * as
|
|
3061
|
-
import * as
|
|
3230
|
+
import * as fs8 from "fs";
|
|
3231
|
+
import * as path11 from "path";
|
|
3062
3232
|
|
|
3063
3233
|
// src/scanner/scan/ai/index.ts
|
|
3064
3234
|
import * as p from "@clack/prompts";
|
|
3065
3235
|
|
|
3066
3236
|
// src/scanner/scan/ai/providers/claude.ts
|
|
3067
|
-
import * as
|
|
3068
|
-
import * as
|
|
3237
|
+
import * as fs7 from "fs";
|
|
3238
|
+
import * as path10 from "path";
|
|
3069
3239
|
|
|
3070
3240
|
// src/scanner/scan/ai/templates.ts
|
|
3071
|
-
import * as
|
|
3072
|
-
import * as
|
|
3241
|
+
import * as fs6 from "fs";
|
|
3242
|
+
import * as path9 from "path";
|
|
3073
3243
|
function templatePath(rel) {
|
|
3074
3244
|
const candidates = [
|
|
3075
|
-
|
|
3076
|
-
// dist/cli/cli.cjs
|
|
3077
|
-
|
|
3078
|
-
// src/scan/ai
|
|
3245
|
+
path9.resolve(__dirname, "../../templates", rel),
|
|
3246
|
+
// dist/cli/cli.cjs → ../../templates
|
|
3247
|
+
path9.resolve(__dirname, "../../../../templates", rel)
|
|
3248
|
+
// src/scanner/scan/ai → ../../../../templates
|
|
3079
3249
|
];
|
|
3080
3250
|
for (const c of candidates) {
|
|
3081
3251
|
try {
|
|
3082
|
-
|
|
3252
|
+
fs6.accessSync(c, fs6.constants.R_OK);
|
|
3083
3253
|
return c;
|
|
3084
3254
|
} catch {
|
|
3085
3255
|
continue;
|
|
@@ -3091,24 +3261,39 @@ function templatePath(rel) {
|
|
|
3091
3261
|
);
|
|
3092
3262
|
}
|
|
3093
3263
|
function readTemplate(rel) {
|
|
3094
|
-
return
|
|
3264
|
+
return fs6.readFileSync(templatePath(rel), "utf8");
|
|
3095
3265
|
}
|
|
3096
3266
|
|
|
3097
3267
|
// src/scanner/scan/ai/providers/claude.ts
|
|
3098
|
-
var
|
|
3099
|
-
{ dest: ".claude/
|
|
3100
|
-
{
|
|
3101
|
-
|
|
3268
|
+
var SKILL_FILES = [
|
|
3269
|
+
{ dest: ".claude/skills/uidex/SKILL.md", template: "claude/SKILL.md" },
|
|
3270
|
+
{
|
|
3271
|
+
dest: ".claude/skills/uidex/references/conventions.md",
|
|
3272
|
+
template: "claude/references/conventions.md"
|
|
3273
|
+
},
|
|
3274
|
+
{
|
|
3275
|
+
dest: ".claude/skills/uidex/references/audit.md",
|
|
3276
|
+
template: "claude/references/audit.md"
|
|
3277
|
+
},
|
|
3278
|
+
{
|
|
3279
|
+
dest: ".claude/skills/uidex/references/api.md",
|
|
3280
|
+
template: "claude/references/api.md"
|
|
3281
|
+
}
|
|
3282
|
+
];
|
|
3283
|
+
var LEGACY_FILES = [
|
|
3284
|
+
".claude/rules/uidex.md",
|
|
3285
|
+
".claude/commands/uidex/audit.md",
|
|
3286
|
+
".claude/commands/uidex/api.md"
|
|
3102
3287
|
];
|
|
3103
3288
|
var claudeProvider = {
|
|
3104
3289
|
id: "claude",
|
|
3105
3290
|
label: "Claude Code",
|
|
3106
|
-
description: "Adds .claude/
|
|
3291
|
+
description: "Adds .claude/skills/uidex/ skill with conventions, audit, and API references.",
|
|
3107
3292
|
async install({ cwd, force }) {
|
|
3108
3293
|
const changes = [];
|
|
3109
|
-
for (const file of
|
|
3110
|
-
const dest =
|
|
3111
|
-
const exists =
|
|
3294
|
+
for (const file of SKILL_FILES) {
|
|
3295
|
+
const dest = path10.join(cwd, file.dest);
|
|
3296
|
+
const exists = fs7.existsSync(dest);
|
|
3112
3297
|
if (exists && !force) {
|
|
3113
3298
|
changes.push({
|
|
3114
3299
|
path: file.dest,
|
|
@@ -3117,36 +3302,56 @@ var claudeProvider = {
|
|
|
3117
3302
|
});
|
|
3118
3303
|
continue;
|
|
3119
3304
|
}
|
|
3120
|
-
|
|
3121
|
-
|
|
3305
|
+
fs7.mkdirSync(path10.dirname(dest), { recursive: true });
|
|
3306
|
+
fs7.writeFileSync(dest, readTemplate(file.template));
|
|
3122
3307
|
changes.push({
|
|
3123
3308
|
path: file.dest,
|
|
3124
3309
|
action: exists ? "overwritten" : "created"
|
|
3125
3310
|
});
|
|
3126
3311
|
}
|
|
3312
|
+
for (const rel of LEGACY_FILES) {
|
|
3313
|
+
const dest = path10.join(cwd, rel);
|
|
3314
|
+
if (fs7.existsSync(dest)) {
|
|
3315
|
+
fs7.unlinkSync(dest);
|
|
3316
|
+
changes.push({ path: rel, action: "removed" });
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
cleanupEmpty(path10.join(cwd, ".claude/commands/uidex"));
|
|
3320
|
+
cleanupEmpty(path10.join(cwd, ".claude/commands"));
|
|
3321
|
+
cleanupEmpty(path10.join(cwd, ".claude/rules"));
|
|
3127
3322
|
return { changes };
|
|
3128
3323
|
},
|
|
3129
3324
|
async uninstall({ cwd }) {
|
|
3130
3325
|
const changes = [];
|
|
3131
|
-
for (const file of
|
|
3132
|
-
const dest =
|
|
3133
|
-
if (!
|
|
3326
|
+
for (const file of SKILL_FILES) {
|
|
3327
|
+
const dest = path10.join(cwd, file.dest);
|
|
3328
|
+
if (!fs7.existsSync(dest)) {
|
|
3134
3329
|
changes.push({ path: file.dest, action: "skipped", reason: "absent" });
|
|
3135
3330
|
continue;
|
|
3136
3331
|
}
|
|
3137
|
-
|
|
3332
|
+
fs7.unlinkSync(dest);
|
|
3138
3333
|
changes.push({ path: file.dest, action: "removed" });
|
|
3139
3334
|
}
|
|
3140
|
-
cleanupEmpty(
|
|
3141
|
-
cleanupEmpty(
|
|
3142
|
-
cleanupEmpty(
|
|
3335
|
+
cleanupEmpty(path10.join(cwd, ".claude/skills/uidex/references"));
|
|
3336
|
+
cleanupEmpty(path10.join(cwd, ".claude/skills/uidex"));
|
|
3337
|
+
cleanupEmpty(path10.join(cwd, ".claude/skills"));
|
|
3338
|
+
for (const rel of LEGACY_FILES) {
|
|
3339
|
+
const dest = path10.join(cwd, rel);
|
|
3340
|
+
if (fs7.existsSync(dest)) {
|
|
3341
|
+
fs7.unlinkSync(dest);
|
|
3342
|
+
changes.push({ path: rel, action: "removed" });
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
cleanupEmpty(path10.join(cwd, ".claude/commands/uidex"));
|
|
3346
|
+
cleanupEmpty(path10.join(cwd, ".claude/commands"));
|
|
3347
|
+
cleanupEmpty(path10.join(cwd, ".claude/rules"));
|
|
3143
3348
|
return { changes };
|
|
3144
3349
|
}
|
|
3145
3350
|
};
|
|
3146
3351
|
function cleanupEmpty(dir) {
|
|
3147
3352
|
try {
|
|
3148
|
-
const entries =
|
|
3149
|
-
if (entries.length === 0)
|
|
3353
|
+
const entries = fs7.readdirSync(dir);
|
|
3354
|
+
if (entries.length === 0) fs7.rmdirSync(dir);
|
|
3150
3355
|
} catch {
|
|
3151
3356
|
}
|
|
3152
3357
|
}
|
|
@@ -3313,6 +3518,8 @@ async function run(opts) {
|
|
|
3313
3518
|
return runScanCommand(cwd, flags, writer);
|
|
3314
3519
|
case "scaffold":
|
|
3315
3520
|
return runScaffold(cwd, positional.slice(1), flags, writer);
|
|
3521
|
+
case "rename":
|
|
3522
|
+
return runRename(cwd, positional.slice(1), flags, writer);
|
|
3316
3523
|
case "ai": {
|
|
3317
3524
|
const result = await runAiCommand({
|
|
3318
3525
|
cwd,
|
|
@@ -3339,7 +3546,8 @@ function helpText2() {
|
|
|
3339
3546
|
"Commands:",
|
|
3340
3547
|
" init Create a .uidex.json",
|
|
3341
3548
|
" scan [flags] Run the scanner pipeline",
|
|
3342
|
-
" scaffold widget <id> Emit a Playwright spec from
|
|
3549
|
+
" scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
|
|
3550
|
+
" rename <element|widget|region> <old-id> <new-id> Rename an id everywhere (DOM attr, flows, exports)",
|
|
3343
3551
|
" ai <install|uninstall|providers> Manage AI assistant integrations",
|
|
3344
3552
|
" api <METHOD> <PATH> Call the uidex API",
|
|
3345
3553
|
" api --list Show available API routes",
|
|
@@ -3348,16 +3556,17 @@ function helpText2() {
|
|
|
3348
3556
|
"",
|
|
3349
3557
|
"Flags:",
|
|
3350
3558
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
3351
|
-
" --lint Run lint diagnostics (missing annotations, scope leak,
|
|
3559
|
+
" --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
|
|
3352
3560
|
" --audit Equivalent to --check --lint (read-only)",
|
|
3561
|
+
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
|
|
3353
3562
|
" --json Emit JSON diagnostics on stdout",
|
|
3354
3563
|
" --force (scaffold) overwrite existing spec",
|
|
3355
3564
|
""
|
|
3356
3565
|
].join("\n");
|
|
3357
3566
|
}
|
|
3358
3567
|
function runInit(cwd, w) {
|
|
3359
|
-
const configPath =
|
|
3360
|
-
if (
|
|
3568
|
+
const configPath = path11.join(cwd, CONFIG_FILENAME);
|
|
3569
|
+
if (fs8.existsSync(configPath)) {
|
|
3361
3570
|
w.err(`.uidex.json already exists at ${configPath}`);
|
|
3362
3571
|
return w.result(1);
|
|
3363
3572
|
}
|
|
@@ -3366,16 +3575,16 @@ function runInit(cwd, w) {
|
|
|
3366
3575
|
sources: [{ rootDir: "src" }],
|
|
3367
3576
|
output: "src/uidex.gen.ts"
|
|
3368
3577
|
};
|
|
3369
|
-
|
|
3578
|
+
fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3370
3579
|
w.out(`Created ${configPath}`);
|
|
3371
|
-
const gitignorePath =
|
|
3580
|
+
const gitignorePath = path11.join(cwd, ".gitignore");
|
|
3372
3581
|
const entry = "*.gen.ts";
|
|
3373
|
-
if (
|
|
3374
|
-
const existing =
|
|
3582
|
+
if (fs8.existsSync(gitignorePath)) {
|
|
3583
|
+
const existing = fs8.readFileSync(gitignorePath, "utf8");
|
|
3375
3584
|
const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
|
|
3376
3585
|
if (!hasEntry) {
|
|
3377
3586
|
const needsNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
3378
|
-
|
|
3587
|
+
fs8.appendFileSync(
|
|
3379
3588
|
gitignorePath,
|
|
3380
3589
|
`${needsNewline ? "\n" : ""}${entry}
|
|
3381
3590
|
`,
|
|
@@ -3384,21 +3593,33 @@ function runInit(cwd, w) {
|
|
|
3384
3593
|
w.out(`Appended ${entry} to ${gitignorePath}`);
|
|
3385
3594
|
}
|
|
3386
3595
|
} else {
|
|
3387
|
-
|
|
3596
|
+
fs8.writeFileSync(gitignorePath, `${entry}
|
|
3388
3597
|
`, "utf8");
|
|
3389
3598
|
w.out(`Created ${gitignorePath} with ${entry}`);
|
|
3390
3599
|
}
|
|
3391
3600
|
return w.result(0);
|
|
3392
3601
|
}
|
|
3393
3602
|
function runScanCommand(cwd, flags, w) {
|
|
3394
|
-
const
|
|
3395
|
-
const
|
|
3603
|
+
const fix = Boolean(flags.fix);
|
|
3604
|
+
const check = !fix && Boolean(flags.check || flags.audit);
|
|
3605
|
+
const lint = Boolean(flags.lint || flags.audit || fix);
|
|
3396
3606
|
const asJson = Boolean(flags.json);
|
|
3397
|
-
|
|
3607
|
+
let configs = discover({ cwd });
|
|
3398
3608
|
if (configs.length === 0) {
|
|
3399
3609
|
w.err(`No ${CONFIG_FILENAME} found under ${cwd}`);
|
|
3400
3610
|
return w.result(1);
|
|
3401
3611
|
}
|
|
3612
|
+
let fixed = [];
|
|
3613
|
+
let fixSkipped = [];
|
|
3614
|
+
if (fix) {
|
|
3615
|
+
const discovery = runScan({ cwd, check: true, lint: true, configs });
|
|
3616
|
+
const result = applyFixes(
|
|
3617
|
+
discovery.flatMap((r) => r.audit?.diagnostics ?? [])
|
|
3618
|
+
);
|
|
3619
|
+
fixed = result.applied;
|
|
3620
|
+
fixSkipped = result.skipped;
|
|
3621
|
+
configs = discover({ cwd });
|
|
3622
|
+
}
|
|
3402
3623
|
const results = runScan({ cwd, check, lint, configs });
|
|
3403
3624
|
if (!check) {
|
|
3404
3625
|
for (const r of results) writeScanResult(r);
|
|
@@ -3413,9 +3634,21 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3413
3634
|
{ errors: 0, warnings: 0 }
|
|
3414
3635
|
);
|
|
3415
3636
|
if (asJson) {
|
|
3416
|
-
const out2 = {
|
|
3637
|
+
const out2 = {
|
|
3638
|
+
diagnostics: allDiagnostics.map(jsonDiagnostic),
|
|
3639
|
+
summary,
|
|
3640
|
+
...fix ? { fixed, fixSkipped } : {}
|
|
3641
|
+
};
|
|
3417
3642
|
w.out(JSON.stringify(out2, null, 2));
|
|
3418
3643
|
} else {
|
|
3644
|
+
for (const f of fixed) {
|
|
3645
|
+
w.out(`FIXED [${f.code}] ${f.file ?? ""} ${f.description}`);
|
|
3646
|
+
}
|
|
3647
|
+
for (const s of fixSkipped) {
|
|
3648
|
+
w.out(
|
|
3649
|
+
`SKIPPED [${s.code}] ${s.file ?? ""} ${s.description} (${s.reason})`
|
|
3650
|
+
);
|
|
3651
|
+
}
|
|
3419
3652
|
for (const r of results) {
|
|
3420
3653
|
if (check) {
|
|
3421
3654
|
w.out(`Checked ${r.outputPath}`);
|
|
@@ -3425,7 +3658,10 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3425
3658
|
for (const d of r.audit?.diagnostics ?? []) {
|
|
3426
3659
|
const loc = d.file ? `${d.file}${d.line ? `:${d.line}` : ""}` : "";
|
|
3427
3660
|
const stream = d.severity === "error" ? w.err : w.out;
|
|
3428
|
-
|
|
3661
|
+
const fixable = d.fix && !fix ? " [fixable: run with --fix]" : "";
|
|
3662
|
+
stream(
|
|
3663
|
+
`${d.severity.toUpperCase()} [${d.code}] ${loc} ${d.message}${fixable}`
|
|
3664
|
+
);
|
|
3429
3665
|
if (d.hint) stream(` hint: ${d.hint}`);
|
|
3430
3666
|
}
|
|
3431
3667
|
}
|
|
@@ -3436,20 +3672,27 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3436
3672
|
const exit = summary.errors > 0 ? 1 : 0;
|
|
3437
3673
|
return w.result(exit);
|
|
3438
3674
|
}
|
|
3675
|
+
function jsonDiagnostic(d) {
|
|
3676
|
+
const { fix, ...rest } = d;
|
|
3677
|
+
return fix ? { ...rest, fixable: true } : rest;
|
|
3678
|
+
}
|
|
3679
|
+
var SCAFFOLD_KINDS = /* @__PURE__ */ new Set(["widget", "page", "feature"]);
|
|
3439
3680
|
function runScaffold(cwd, args, flags, w) {
|
|
3440
3681
|
const [kind, id] = args;
|
|
3441
|
-
if (kind
|
|
3442
|
-
w.err("Usage: uidex scaffold widget <id> [--force]");
|
|
3682
|
+
if (!kind || !SCAFFOLD_KINDS.has(kind) || !id) {
|
|
3683
|
+
w.err("Usage: uidex scaffold <widget|page|feature> <id> [--force]");
|
|
3443
3684
|
return w.result(1);
|
|
3444
3685
|
}
|
|
3686
|
+
const scaffoldKind = kind;
|
|
3445
3687
|
const results = runScan({ cwd });
|
|
3446
3688
|
for (const r of results) {
|
|
3447
|
-
const
|
|
3448
|
-
if (!
|
|
3449
|
-
const outDir =
|
|
3450
|
-
const result =
|
|
3689
|
+
const entity = r.registry.get(scaffoldKind, id);
|
|
3690
|
+
if (!entity) continue;
|
|
3691
|
+
const outDir = path11.resolve(r.configDir, "e2e");
|
|
3692
|
+
const result = scaffoldSpec({
|
|
3451
3693
|
registry: r.registry,
|
|
3452
|
-
|
|
3694
|
+
kind: scaffoldKind,
|
|
3695
|
+
id,
|
|
3453
3696
|
outDir,
|
|
3454
3697
|
force: Boolean(flags.force)
|
|
3455
3698
|
});
|
|
@@ -3460,9 +3703,43 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
3460
3703
|
w.out(`Wrote ${result.outputPath}`);
|
|
3461
3704
|
return w.result(0);
|
|
3462
3705
|
}
|
|
3463
|
-
w.err(
|
|
3706
|
+
w.err(
|
|
3707
|
+
`${scaffoldKind.charAt(0).toUpperCase() + scaffoldKind.slice(1)} "${id}" not found in registry`
|
|
3708
|
+
);
|
|
3464
3709
|
return w.result(1);
|
|
3465
3710
|
}
|
|
3711
|
+
var RENAME_KINDS = /* @__PURE__ */ new Set(["element", "widget", "region"]);
|
|
3712
|
+
function runRename(cwd, args, flags, w) {
|
|
3713
|
+
const [kind, oldId, newId] = args;
|
|
3714
|
+
if (!kind || !RENAME_KINDS.has(kind) || !oldId || !newId) {
|
|
3715
|
+
w.err(
|
|
3716
|
+
"Usage: uidex rename <element|widget|region> <old-id> <new-id> [--force]"
|
|
3717
|
+
);
|
|
3718
|
+
return w.result(1);
|
|
3719
|
+
}
|
|
3720
|
+
const result = renameEntity({
|
|
3721
|
+
cwd,
|
|
3722
|
+
kind,
|
|
3723
|
+
oldId,
|
|
3724
|
+
newId,
|
|
3725
|
+
force: Boolean(flags.force)
|
|
3726
|
+
});
|
|
3727
|
+
for (const e of result.errors) w.err(e);
|
|
3728
|
+
for (const m of result.manual) {
|
|
3729
|
+
w.err(`MANUAL ${m.file}:${m.line} \u2014 ${m.reason}`);
|
|
3730
|
+
}
|
|
3731
|
+
if (result.errors.length > 0) return w.result(1);
|
|
3732
|
+
w.out(
|
|
3733
|
+
`Renamed ${kind} "${oldId}" \u2192 "${newId}" (${result.edits} edit(s)); gen file regenerated`
|
|
3734
|
+
);
|
|
3735
|
+
if (result.manual.length > 0) {
|
|
3736
|
+
w.err(
|
|
3737
|
+
`${result.manual.length} occurrence(s) need manual follow-up (listed above)`
|
|
3738
|
+
);
|
|
3739
|
+
return w.result(1);
|
|
3740
|
+
}
|
|
3741
|
+
return w.result(0);
|
|
3742
|
+
}
|
|
3466
3743
|
function createWriter() {
|
|
3467
3744
|
let stdout = "";
|
|
3468
3745
|
let stderr = "";
|
|
@@ -3482,7 +3759,7 @@ export {
|
|
|
3482
3759
|
CONFIG_FILENAME,
|
|
3483
3760
|
ConfigError,
|
|
3484
3761
|
DEFAULT_CONVENTIONS,
|
|
3485
|
-
|
|
3762
|
+
applyFixes,
|
|
3486
3763
|
audit,
|
|
3487
3764
|
detectRoutes,
|
|
3488
3765
|
discover,
|
|
@@ -3492,10 +3769,12 @@ export {
|
|
|
3492
3769
|
globToRegExp,
|
|
3493
3770
|
parseConfig,
|
|
3494
3771
|
pathToId,
|
|
3772
|
+
renameEntity,
|
|
3495
3773
|
resolve2 as resolve,
|
|
3496
3774
|
resolveGitContext,
|
|
3497
3775
|
run as runCli,
|
|
3498
3776
|
runScan,
|
|
3777
|
+
scaffoldSpec,
|
|
3499
3778
|
scaffoldWidgetSpec,
|
|
3500
3779
|
validateConfig,
|
|
3501
3780
|
walk,
|