design-rover 0.2.0 → 0.3.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 -0
- package/dist/bin.js +975 -208
- package/dist/bin.js.map +1 -1
- package/package.json +5 -5
package/dist/bin.js
CHANGED
|
@@ -27,7 +27,7 @@ var init_constants = __esm({
|
|
|
27
27
|
MODEL_VERSION = 1;
|
|
28
28
|
PRODUCT_NAME = "Design Rover";
|
|
29
29
|
CLI_PACKAGE = "design-rover";
|
|
30
|
-
CLI_VERSION = "0.
|
|
30
|
+
CLI_VERSION = "0.3.0";
|
|
31
31
|
SPEC_DIR = ".design-rover";
|
|
32
32
|
MANAGED_START = "<!-- design-rover:start -->";
|
|
33
33
|
MANAGED_END = "<!-- design-rover:end -->";
|
|
@@ -99,7 +99,7 @@ var init_ids = __esm({
|
|
|
99
99
|
|
|
100
100
|
// ../model/src/schema.ts
|
|
101
101
|
import { z } from "zod";
|
|
102
|
-
var KnowledgeStatusSchema, ConfidenceSchema, SourceKindSchema, TokenCategorySchema, EntityKindSchema, DiagnosticSeveritySchema, SyncStateSchema, SourceLocationSchema, EvidenceSchema, KnowledgeMetaSchema, TokenRecordSchema, ComponentPropSchema, ComponentUsageSchema, ComponentRecordSchema, PatternOccurrenceSchema, PatternRecordSchema, ConventionRecordSchema, IntentRecordSchema, ExceptionRecordSchema, RejectionRecordSchema, BaselineFindingSchema, BaselineRecordSchema, ExternalSourceSchema, SourceDiscrepancySchema, GeneratedFileSchema, HistoryEventSchema, FrameworkInfoSchema, StylingInfoSchema, EnforcementInfoSchema, SystemRecordSchema, CanonicalModelSchema, ObservedSnapshotSchema, ChangeKindSchema, ModelChangeSchema, SystemDiffSchema, DiagnosticSchema, CheckReportSchema, StatusReportSchema;
|
|
102
|
+
var KnowledgeStatusSchema, ConfidenceSchema, SourceKindSchema, TokenCategorySchema, EntityKindSchema, DiagnosticSeveritySchema, SyncStateSchema, SourceLocationSchema, EvidenceSchema, KnowledgeMetaSchema, TokenRecordSchema, ComponentPropSchema, ComponentUsageSchema, ComponentPropsResolutionSchema, ComponentRecordSchema, PatternOccurrenceSchema, PatternRecordSchema, ConventionRecordSchema, IntentRecordSchema, ExceptionRecordSchema, RejectionRecordSchema, BaselineFindingSchema, BaselineRecordSchema, ExternalSourceSchema, SourceDiscrepancySchema, GeneratedFileSchema, HistoryEventSchema, FrameworkInfoSchema, StylingInfoSchema, EnforcementInfoSchema, SystemRecordSchema, CanonicalModelSchema, ObservedSnapshotSchema, ChangeKindSchema, ModelChangeSchema, SystemDiffSchema, DiagnosticSchema, CheckReportSchema, StatusReportSchema;
|
|
103
103
|
var init_schema = __esm({
|
|
104
104
|
"../model/src/schema.ts"() {
|
|
105
105
|
"use strict";
|
|
@@ -160,6 +160,10 @@ var init_schema = __esm({
|
|
|
160
160
|
file: z.string(),
|
|
161
161
|
count: z.number().int()
|
|
162
162
|
});
|
|
163
|
+
ComponentPropsResolutionSchema = z.object({
|
|
164
|
+
status: z.enum(["resolved", "partial", "unresolved", "untyped"]),
|
|
165
|
+
issues: z.array(z.string()).default([])
|
|
166
|
+
});
|
|
163
167
|
ComponentRecordSchema = KnowledgeMetaSchema.extend({
|
|
164
168
|
id: z.string(),
|
|
165
169
|
name: z.string(),
|
|
@@ -167,6 +171,7 @@ var init_schema = __esm({
|
|
|
167
171
|
importPath: z.string(),
|
|
168
172
|
exportName: z.string(),
|
|
169
173
|
props: z.array(ComponentPropSchema).default([]),
|
|
174
|
+
propsResolution: ComponentPropsResolutionSchema.optional(),
|
|
170
175
|
variants: z.array(z.string()).default([]),
|
|
171
176
|
sizes: z.array(z.string()).default([]),
|
|
172
177
|
composition: z.array(z.string()).default([]),
|
|
@@ -502,10 +507,189 @@ var init_hash = __esm({
|
|
|
502
507
|
}
|
|
503
508
|
});
|
|
504
509
|
|
|
505
|
-
// ../core/src/
|
|
510
|
+
// ../core/src/local-modules.ts
|
|
506
511
|
import path2 from "path";
|
|
512
|
+
import ts from "typescript";
|
|
513
|
+
function scriptKind(file) {
|
|
514
|
+
if (file.endsWith(".tsx")) return ts.ScriptKind.TSX;
|
|
515
|
+
if (file.endsWith(".jsx")) return ts.ScriptKind.JSX;
|
|
516
|
+
return /\.(?:js|mjs|cjs)$/.test(file) ? ts.ScriptKind.JS : ts.ScriptKind.TS;
|
|
517
|
+
}
|
|
518
|
+
function localModules(evidence) {
|
|
519
|
+
let graph = graphs.get(evidence);
|
|
520
|
+
if (!graph) {
|
|
521
|
+
graph = new LocalModules(evidence);
|
|
522
|
+
graphs.set(evidence, graph);
|
|
523
|
+
}
|
|
524
|
+
return graph;
|
|
525
|
+
}
|
|
526
|
+
var graphs, extensions, normalize, LocalModules;
|
|
527
|
+
var init_local_modules = __esm({
|
|
528
|
+
"../core/src/local-modules.ts"() {
|
|
529
|
+
"use strict";
|
|
530
|
+
init_esm_shims();
|
|
531
|
+
graphs = /* @__PURE__ */ new WeakMap();
|
|
532
|
+
extensions = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx"];
|
|
533
|
+
normalize = (file) => path2.posix.normalize(file.replaceAll("\\", "/"));
|
|
534
|
+
LocalModules = class {
|
|
535
|
+
constructor(evidence) {
|
|
536
|
+
this.evidence = evidence;
|
|
537
|
+
for (const file of evidence.files) if (file.kind === "script") this.files.set(normalize(file.relative), file.content);
|
|
538
|
+
const config = evidence.files.find((file) => file.relative === "tsconfig.json");
|
|
539
|
+
const options = config ? ts.parseConfigFileTextToJson("tsconfig.json", config.content).config?.compilerOptions : void 0;
|
|
540
|
+
this.baseUrl = typeof options?.baseUrl === "string" ? normalize(options.baseUrl) : ".";
|
|
541
|
+
}
|
|
542
|
+
evidence;
|
|
543
|
+
files = /* @__PURE__ */ new Map();
|
|
544
|
+
modules = /* @__PURE__ */ new Map();
|
|
545
|
+
resolutions = /* @__PURE__ */ new Map();
|
|
546
|
+
baseUrl;
|
|
547
|
+
source(file) {
|
|
548
|
+
return this.module(file)?.source;
|
|
549
|
+
}
|
|
550
|
+
resolveModule(from, specifier) {
|
|
551
|
+
const candidates = [];
|
|
552
|
+
if (specifier.startsWith(".")) candidates.push(path2.posix.join(path2.posix.dirname(from), specifier));
|
|
553
|
+
else {
|
|
554
|
+
const aliases = Object.entries(this.evidence.tsconfigPaths).sort(([a], [b]) => Number(a.includes("*")) - Number(b.includes("*")) || b.indexOf("*") - a.indexOf("*") || a.localeCompare(b));
|
|
555
|
+
for (const [alias, values] of aliases) {
|
|
556
|
+
const star = alias.indexOf("*");
|
|
557
|
+
if (star < 0) {
|
|
558
|
+
if (alias === specifier) {
|
|
559
|
+
candidates.push(...values.map((value) => path2.posix.join(this.baseUrl, value)));
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
} else {
|
|
563
|
+
const prefix = alias.slice(0, star);
|
|
564
|
+
const suffix = alias.slice(star + 1);
|
|
565
|
+
if (specifier.startsWith(prefix) && specifier.endsWith(suffix)) {
|
|
566
|
+
const rest = specifier.slice(prefix.length, suffix ? -suffix.length : void 0);
|
|
567
|
+
candidates.push(...values.map((value) => path2.posix.join(this.baseUrl, value.replace("*", rest))));
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (this.baseUrl !== ".") candidates.push(path2.posix.join(this.baseUrl, specifier));
|
|
573
|
+
}
|
|
574
|
+
for (const candidate of candidates) {
|
|
575
|
+
const normalized = normalize(candidate);
|
|
576
|
+
if (normalized === ".." || normalized.startsWith("../") || path2.posix.isAbsolute(normalized)) continue;
|
|
577
|
+
const direct = [];
|
|
578
|
+
if (/\.jsx?$/.test(normalized)) direct.push(normalized.replace(/\.jsx?$/, ".ts"), normalized.replace(/\.jsx?$/, ".tsx"));
|
|
579
|
+
if (normalized.endsWith(".mjs")) direct.push(normalized.replace(/\.mjs$/, ".mts"));
|
|
580
|
+
if (normalized.endsWith(".cjs")) direct.push(normalized.replace(/\.cjs$/, ".cts"));
|
|
581
|
+
direct.push(normalized);
|
|
582
|
+
direct.push(...extensions.map((extension) => `${normalized}${extension}`), ...extensions.map((extension) => `${normalized}/index${extension}`));
|
|
583
|
+
const found = direct.find((file) => this.files.has(file));
|
|
584
|
+
if (found) return found;
|
|
585
|
+
}
|
|
586
|
+
return void 0;
|
|
587
|
+
}
|
|
588
|
+
resolveExport(from, specifier, name, kind) {
|
|
589
|
+
const module = this.resolveModule(from, specifier);
|
|
590
|
+
if (!module) return { targets: [], issues: [`Cannot resolve local ${kind} import ${JSON.stringify(specifier)} from ${from}. External package declarations are not loaded.`] };
|
|
591
|
+
const key = JSON.stringify([module, name, kind]);
|
|
592
|
+
const cached = this.resolutions.get(key);
|
|
593
|
+
if (cached) return cached;
|
|
594
|
+
const result = this.exported(module, name, kind, /* @__PURE__ */ new Set());
|
|
595
|
+
const distinct = [...new Map(result.targets.map((target) => [JSON.stringify(target), target])).values()];
|
|
596
|
+
const issues = [...new Set(result.issues)];
|
|
597
|
+
if (distinct.length > 1) issues.push(`Ambiguous ${kind} export ${name} in ${module}; multiple declarations match.`);
|
|
598
|
+
const resolution = { module, targets: distinct, issues };
|
|
599
|
+
this.resolutions.set(key, resolution);
|
|
600
|
+
return resolution;
|
|
601
|
+
}
|
|
602
|
+
module(file) {
|
|
603
|
+
file = normalize(file);
|
|
604
|
+
const cached = this.modules.get(file);
|
|
605
|
+
if (cached) return cached;
|
|
606
|
+
const content = this.files.get(file);
|
|
607
|
+
if (content === void 0) return void 0;
|
|
608
|
+
const source = ts.createSourceFile(path2.resolve(this.evidence.root || ".", file), content, ts.ScriptTarget.Latest, true, scriptKind(file));
|
|
609
|
+
const module = { source, locals: /* @__PURE__ */ new Map(), imports: /* @__PURE__ */ new Map(), exports: /* @__PURE__ */ new Map(), stars: [] };
|
|
610
|
+
this.modules.set(file, module);
|
|
611
|
+
const addExport = (name, binding) => module.exports.set(name, [...module.exports.get(name) ?? [], binding]);
|
|
612
|
+
const addLocal = (name, declaration) => module.locals.set(name, [...module.locals.get(name) ?? [], declaration]);
|
|
613
|
+
for (const statement of source.statements) {
|
|
614
|
+
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
615
|
+
const clause = statement.importClause;
|
|
616
|
+
const specifier = statement.moduleSpecifier.text;
|
|
617
|
+
if (clause?.name) module.imports.set(clause.name.text, { source: specifier, imported: "default", typeOnly: clause.isTypeOnly });
|
|
618
|
+
const bindings = clause?.namedBindings;
|
|
619
|
+
if (bindings && ts.isNamedImports(bindings)) for (const binding of bindings.elements) {
|
|
620
|
+
module.imports.set(binding.name.text, { source: specifier, imported: binding.propertyName?.text ?? binding.name.text, typeOnly: Boolean(clause?.isTypeOnly || binding.isTypeOnly) });
|
|
621
|
+
}
|
|
622
|
+
if (bindings && ts.isNamespaceImport(bindings)) module.imports.set(bindings.name.text, { source: specifier, imported: "*", typeOnly: Boolean(clause?.isTypeOnly) });
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (ts.isExportDeclaration(statement)) {
|
|
626
|
+
const specifier = statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : void 0;
|
|
627
|
+
if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
628
|
+
for (const binding of statement.exportClause.elements) addExport(binding.name.text, { source: specifier, imported: binding.propertyName?.text ?? binding.name.text, typeOnly: statement.isTypeOnly || binding.isTypeOnly });
|
|
629
|
+
} else if (!statement.exportClause && specifier) module.stars.push({ source: specifier, typeOnly: statement.isTypeOnly });
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (ts.isExportAssignment(statement) && !statement.isExportEquals && ts.isIdentifier(statement.expression)) {
|
|
633
|
+
addExport("default", { imported: statement.expression.text, typeOnly: false });
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : void 0;
|
|
637
|
+
const exported = modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
638
|
+
const isDefault = modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword);
|
|
639
|
+
if (ts.isVariableStatement(statement)) {
|
|
640
|
+
for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name)) {
|
|
641
|
+
addLocal(declaration.name.text, { type: false, value: true });
|
|
642
|
+
if (exported) addExport(declaration.name.text, { imported: declaration.name.text, typeOnly: false });
|
|
643
|
+
}
|
|
644
|
+
} else if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isEnumDeclaration(statement)) && statement.name) {
|
|
645
|
+
const name = statement.name.text;
|
|
646
|
+
const type = !ts.isFunctionDeclaration(statement);
|
|
647
|
+
const value = !ts.isInterfaceDeclaration(statement) && !ts.isTypeAliasDeclaration(statement);
|
|
648
|
+
addLocal(name, { type, value });
|
|
649
|
+
if (exported) addExport(isDefault ? "default" : name, { imported: name, typeOnly: !value });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return module;
|
|
653
|
+
}
|
|
654
|
+
local(file, name, kind, seen) {
|
|
655
|
+
const module = this.module(file);
|
|
656
|
+
if (!module) return { targets: [], issues: [] };
|
|
657
|
+
if (module.locals.get(name)?.some((declaration) => declaration[kind])) return { targets: [{ file, name }], issues: [] };
|
|
658
|
+
const binding = module.imports.get(name);
|
|
659
|
+
if (binding?.source && !(kind === "value" && binding.typeOnly)) return this.binding(file, binding, kind, seen);
|
|
660
|
+
return { targets: [], issues: [] };
|
|
661
|
+
}
|
|
662
|
+
binding(file, binding, kind, seen) {
|
|
663
|
+
if (kind === "value" && binding.typeOnly) return { targets: [], issues: [] };
|
|
664
|
+
if (!binding.source) return this.local(file, binding.imported, kind, seen);
|
|
665
|
+
const target = this.resolveModule(file, binding.source);
|
|
666
|
+
if (!target) return { targets: [], issues: [`Cannot resolve local ${kind} re-export ${JSON.stringify(binding.source)} from ${file}.`] };
|
|
667
|
+
if (binding.imported === "*") return { targets: [], issues: [`Namespace re-export in ${file} requires an explicit exported member.`] };
|
|
668
|
+
return this.exported(target, binding.imported, kind, seen);
|
|
669
|
+
}
|
|
670
|
+
exported(file, name, kind, seen) {
|
|
671
|
+
const key = JSON.stringify([file, name, kind]);
|
|
672
|
+
if (seen.has(key)) return { targets: [], issues: [] };
|
|
673
|
+
const next = new Set(seen).add(key);
|
|
674
|
+
const module = this.module(file);
|
|
675
|
+
if (!module) return { targets: [], issues: [] };
|
|
676
|
+
const explicit = module.exports.get(name)?.filter((binding) => !(kind === "value" && binding.typeOnly));
|
|
677
|
+
if (explicit?.length) {
|
|
678
|
+
const results2 = explicit.map((binding) => this.binding(file, binding, kind, next));
|
|
679
|
+
return { targets: results2.flatMap((result) => result.targets), issues: results2.flatMap((result) => result.issues) };
|
|
680
|
+
}
|
|
681
|
+
if (name === "default") return { targets: [], issues: [] };
|
|
682
|
+
const results = module.stars.filter((star) => !(kind === "value" && star.typeOnly)).map((star) => this.binding(file, { source: star.source, imported: name, typeOnly: star.typeOnly }, kind, next));
|
|
683
|
+
return { targets: results.flatMap((result) => result.targets), issues: results.flatMap((result) => result.issues) };
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// ../core/src/component-resolution.ts
|
|
690
|
+
import path3 from "path";
|
|
507
691
|
function targets(evidence, file, source) {
|
|
508
|
-
if (source.startsWith(".")) return [
|
|
692
|
+
if (source.startsWith(".")) return [path3.posix.normalize(path3.posix.join(path3.posix.dirname(file), source))];
|
|
509
693
|
const found = [];
|
|
510
694
|
for (const [alias, values] of Object.entries(evidence.tsconfigPaths)) {
|
|
511
695
|
const star = alias.indexOf("*");
|
|
@@ -520,7 +704,7 @@ function targets(evidence, file, source) {
|
|
|
520
704
|
}
|
|
521
705
|
}
|
|
522
706
|
}
|
|
523
|
-
return found.map((value) =>
|
|
707
|
+
return found.map((value) => path3.posix.normalize(value.replace(/^\.\//, "")));
|
|
524
708
|
}
|
|
525
709
|
function resolveComponent(evidence, file, tag, components, options = {}) {
|
|
526
710
|
if (tag.split(".").length > 2) return void 0;
|
|
@@ -528,6 +712,7 @@ function resolveComponent(evidence, file, tag, components, options = {}) {
|
|
|
528
712
|
const imports = file.imports.filter((item) => !item.isTypeOnly);
|
|
529
713
|
let bound = false;
|
|
530
714
|
const matches = /* @__PURE__ */ new Set();
|
|
715
|
+
const graph = localModules(evidence);
|
|
531
716
|
for (const item of imports) {
|
|
532
717
|
const bindings = item.bindings ?? [
|
|
533
718
|
...item.names.map((name) => ({ local: name, imported: name })),
|
|
@@ -536,8 +721,16 @@ function resolveComponent(evidence, file, tag, components, options = {}) {
|
|
|
536
721
|
const binding = bindings.find((value) => value.local === local);
|
|
537
722
|
if (!binding) continue;
|
|
538
723
|
bound = true;
|
|
724
|
+
if (binding.isTypeOnly) continue;
|
|
539
725
|
if (binding.imported === "*" !== Boolean(member)) continue;
|
|
540
726
|
const exported = member ?? binding.imported;
|
|
727
|
+
const resolution = graph.resolveExport(file.relative, item.source, exported, "value");
|
|
728
|
+
if (resolution.module) {
|
|
729
|
+
if (resolution.issues.length || resolution.targets.length !== 1) continue;
|
|
730
|
+
const target = resolution.targets[0];
|
|
731
|
+
for (const component of components) if (component.source === target.file && component.name === target.name) matches.add(component);
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
541
734
|
const modules = targets(evidence, file.relative, item.source);
|
|
542
735
|
for (const component of components) {
|
|
543
736
|
const moduleMatch = !item.source.startsWith(".") && component.importPath === item.source || modules.some((target) => {
|
|
@@ -561,12 +754,307 @@ var init_component_resolution = __esm({
|
|
|
561
754
|
"../core/src/component-resolution.ts"() {
|
|
562
755
|
"use strict";
|
|
563
756
|
init_esm_shims();
|
|
757
|
+
init_local_modules();
|
|
564
758
|
withoutExtension = (file) => file.replace(/\.(?:tsx?|jsx?|mts|cts|mjs|cjs)$/, "");
|
|
565
759
|
}
|
|
566
760
|
});
|
|
567
761
|
|
|
762
|
+
// ../core/src/type-resolution.ts
|
|
763
|
+
import path4 from "path";
|
|
764
|
+
import ts2 from "typescript";
|
|
765
|
+
function unwrap(node) {
|
|
766
|
+
if (ts2.isArrowFunction(node) || ts2.isFunctionExpression(node)) return { fn: node };
|
|
767
|
+
if (ts2.isParenthesizedExpression(node) || ts2.isAsExpression(node) || ts2.isSatisfiesExpression(node)) return unwrap(node.expression);
|
|
768
|
+
if (ts2.isCallExpression(node)) {
|
|
769
|
+
const name = ts2.isIdentifier(node.expression) ? node.expression.text : ts2.isPropertyAccessExpression(node.expression) ? node.expression.name.text : "";
|
|
770
|
+
if (name === "memo" || name === "forwardRef") {
|
|
771
|
+
const first = node.arguments[0];
|
|
772
|
+
if (first) return { ...unwrap(first), type: name === "forwardRef" ? node.typeArguments?.[1] : node.typeArguments?.[0] };
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return {};
|
|
776
|
+
}
|
|
777
|
+
function componentInput(source, component) {
|
|
778
|
+
let input;
|
|
779
|
+
const visit = (node) => {
|
|
780
|
+
if (input) return;
|
|
781
|
+
const line2 = source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
|
|
782
|
+
if (ts2.isFunctionDeclaration(node) && node.name?.text === component.name && line2 === component.startLine) {
|
|
783
|
+
input = { fn: node, type: node.parameters[0]?.type };
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (ts2.isVariableDeclaration(node) && ts2.isIdentifier(node.name) && node.name.text === component.name && line2 === component.startLine && node.initializer) {
|
|
787
|
+
const unwrapped = unwrap(node.initializer);
|
|
788
|
+
if (!unwrapped.fn) return;
|
|
789
|
+
let annotation;
|
|
790
|
+
if (node.type && ts2.isTypeReferenceNode(node.type) && /(?:^|\.)(?:FC|FunctionComponent|ComponentType)$/.test(node.type.typeName.getText())) annotation = node.type.typeArguments?.[0];
|
|
791
|
+
if (node.type && ts2.isFunctionTypeNode(node.type)) annotation = node.type.parameters[0]?.type;
|
|
792
|
+
input = { fn: unwrapped.fn, type: unwrapped.fn.parameters[0]?.type ?? unwrapped.type ?? annotation };
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
ts2.forEachChild(node, visit);
|
|
796
|
+
};
|
|
797
|
+
visit(source);
|
|
798
|
+
return input;
|
|
799
|
+
}
|
|
800
|
+
function resolveComponentProps(evidence, component) {
|
|
801
|
+
let resolver = resolvers.get(evidence);
|
|
802
|
+
if (!resolver) {
|
|
803
|
+
resolver = new TypeResolver(evidence);
|
|
804
|
+
resolvers.set(evidence, resolver);
|
|
805
|
+
}
|
|
806
|
+
return resolver.props(component);
|
|
807
|
+
}
|
|
808
|
+
var resolvers, MAX_PROPS, normalize2, nameOf, TypeResolver;
|
|
809
|
+
var init_type_resolution = __esm({
|
|
810
|
+
"../core/src/type-resolution.ts"() {
|
|
811
|
+
"use strict";
|
|
812
|
+
init_esm_shims();
|
|
813
|
+
init_local_modules();
|
|
814
|
+
resolvers = /* @__PURE__ */ new WeakMap();
|
|
815
|
+
MAX_PROPS = 256;
|
|
816
|
+
normalize2 = (file) => path4.resolve(file).replaceAll("\\", "/");
|
|
817
|
+
nameOf = (name) => ts2.isIdentifier(name) || ts2.isStringLiteral(name) || ts2.isNumericLiteral(name) ? name.text : name.getText();
|
|
818
|
+
TypeResolver = class {
|
|
819
|
+
constructor(evidence) {
|
|
820
|
+
this.evidence = evidence;
|
|
821
|
+
this.graph = localModules(evidence);
|
|
822
|
+
this.root = path4.resolve(evidence.root || ".");
|
|
823
|
+
this.libDirectory = normalize2(path4.dirname(ts2.getDefaultLibFilePath({})));
|
|
824
|
+
}
|
|
825
|
+
evidence;
|
|
826
|
+
program;
|
|
827
|
+
graph;
|
|
828
|
+
root;
|
|
829
|
+
libDirectory;
|
|
830
|
+
semantic = /* @__PURE__ */ new Map();
|
|
831
|
+
relative(file) {
|
|
832
|
+
return path4.relative(this.root, file).replaceAll("\\", "/");
|
|
833
|
+
}
|
|
834
|
+
isLibrary(file) {
|
|
835
|
+
return normalize2(file).startsWith(`${this.libDirectory}/`) && /\/lib\.[^/]+\.d\.ts$/.test(normalize2(file));
|
|
836
|
+
}
|
|
837
|
+
compiler() {
|
|
838
|
+
if (this.program) return this.program;
|
|
839
|
+
const options = {
|
|
840
|
+
target: ts2.ScriptTarget.ES2022,
|
|
841
|
+
module: ts2.ModuleKind.ESNext,
|
|
842
|
+
moduleResolution: ts2.ModuleResolutionKind.Bundler,
|
|
843
|
+
jsx: ts2.JsxEmit.Preserve,
|
|
844
|
+
strict: true,
|
|
845
|
+
noEmit: true,
|
|
846
|
+
allowJs: true,
|
|
847
|
+
checkJs: false,
|
|
848
|
+
skipLibCheck: true,
|
|
849
|
+
types: [],
|
|
850
|
+
lib: ["lib.es2022.d.ts"]
|
|
851
|
+
};
|
|
852
|
+
const original = ts2.createCompilerHost(options, true);
|
|
853
|
+
const read = (file) => this.graph.files.get(this.relative(file)) ?? (this.isLibrary(file) ? ts2.sys.readFile(file) : void 0);
|
|
854
|
+
const host = {
|
|
855
|
+
...original,
|
|
856
|
+
getCurrentDirectory: () => this.root,
|
|
857
|
+
getCanonicalFileName: normalize2,
|
|
858
|
+
useCaseSensitiveFileNames: () => true,
|
|
859
|
+
readFile: read,
|
|
860
|
+
fileExists: (file) => read(file) !== void 0,
|
|
861
|
+
getSourceFile: (file, languageVersion) => {
|
|
862
|
+
const source = this.graph.source(this.relative(file));
|
|
863
|
+
if (source) return source;
|
|
864
|
+
const text = this.isLibrary(file) ? ts2.sys.readFile(file) : void 0;
|
|
865
|
+
return text === void 0 ? void 0 : ts2.createSourceFile(file, text, languageVersion, true);
|
|
866
|
+
},
|
|
867
|
+
writeFile: () => {
|
|
868
|
+
throw new Error("Design Rover's type resolver never emits application code.");
|
|
869
|
+
},
|
|
870
|
+
resolveModuleNames: (names, containingFile) => names.map((name) => {
|
|
871
|
+
const target = this.graph.resolveModule(this.relative(containingFile), name);
|
|
872
|
+
if (!target) return void 0;
|
|
873
|
+
return { resolvedFileName: path4.resolve(this.root, target), isExternalLibraryImport: false };
|
|
874
|
+
}),
|
|
875
|
+
resolveTypeReferenceDirectives: (names) => names.map(() => void 0)
|
|
876
|
+
};
|
|
877
|
+
this.program = ts2.createProgram([...this.graph.files.keys()].map((file) => path4.resolve(this.root, file)), options, host);
|
|
878
|
+
return this.program;
|
|
879
|
+
}
|
|
880
|
+
props(component) {
|
|
881
|
+
const source = this.graph.source(component.file);
|
|
882
|
+
const input = source && componentInput(source, component);
|
|
883
|
+
if (!input) return { props: component.props, resolution: { status: "unresolved", issues: ["The component's function declaration could not be matched to its source location."] } };
|
|
884
|
+
const parameter = input.fn.parameters[0];
|
|
885
|
+
if (!parameter && !input.type) return { props: [], resolution: { status: "resolved", issues: [] } };
|
|
886
|
+
const defaults = /* @__PURE__ */ new Map();
|
|
887
|
+
const untyped = [];
|
|
888
|
+
if (parameter && ts2.isObjectBindingPattern(parameter.name)) {
|
|
889
|
+
for (const binding of parameter.name.elements) {
|
|
890
|
+
if (binding.dotDotDotToken) continue;
|
|
891
|
+
const name = binding.propertyName ? nameOf(binding.propertyName) : binding.name.getText();
|
|
892
|
+
if (binding.initializer) defaults.set(name, binding.initializer.getText());
|
|
893
|
+
untyped.push({ name, required: !binding.initializer, defaultValue: binding.initializer?.getText() });
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (!input.type) return { props: untyped, resolution: { status: "untyped", issues: ["The props parameter has no explicit supported type annotation. Destructured bindings are not a complete component API."] } };
|
|
897
|
+
const program = this.compiler();
|
|
898
|
+
const checker = program.getTypeChecker();
|
|
899
|
+
const issues = /* @__PURE__ */ new Set();
|
|
900
|
+
const visited = /* @__PURE__ */ new Set();
|
|
901
|
+
let unsafeDependency = false;
|
|
902
|
+
const diagnostics = (node) => {
|
|
903
|
+
const source2 = node.getSourceFile();
|
|
904
|
+
let all = this.semantic.get(source2.fileName);
|
|
905
|
+
if (!all) {
|
|
906
|
+
all = [...program.getSyntacticDiagnostics(source2), ...program.getSemanticDiagnostics(source2)];
|
|
907
|
+
this.semantic.set(source2.fileName, all);
|
|
908
|
+
}
|
|
909
|
+
const start = node.getStart(source2);
|
|
910
|
+
const end = node.getEnd();
|
|
911
|
+
for (const diagnostic of all) if (diagnostic.category === ts2.DiagnosticCategory.Error && diagnostic.start !== void 0 && diagnostic.start >= start && diagnostic.start < end) {
|
|
912
|
+
issues.add(`${this.relative(source2.fileName)}: ${ts2.flattenDiagnosticMessageText(diagnostic.messageText, " ")}`);
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
const importType = (node) => {
|
|
916
|
+
const argument = node.argument;
|
|
917
|
+
if (!ts2.isLiteralTypeNode(argument) || !ts2.isStringLiteral(argument.literal) || !node.qualifier || !ts2.isIdentifier(node.qualifier)) {
|
|
918
|
+
return { targets: [], issues: ["An import type must identify one explicit local exported member; computed or nested import-type members are unsupported."] };
|
|
919
|
+
}
|
|
920
|
+
return this.graph.resolveExport(this.relative(node.getSourceFile().fileName), argument.literal.text, node.qualifier.text, node.isTypeOf ? "value" : "type");
|
|
921
|
+
};
|
|
922
|
+
const reference = (name) => {
|
|
923
|
+
const current = name.getSourceFile();
|
|
924
|
+
const file = this.relative(current.fileName);
|
|
925
|
+
const parts = name.getText(current).split(".");
|
|
926
|
+
const first = parts[0];
|
|
927
|
+
for (const statement of current.statements) {
|
|
928
|
+
if (!ts2.isImportDeclaration(statement) || !ts2.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
929
|
+
const clause = statement.importClause;
|
|
930
|
+
const bindings = clause?.namedBindings;
|
|
931
|
+
let imported;
|
|
932
|
+
if (parts.length === 1 && clause?.name?.text === first) imported = "default";
|
|
933
|
+
if (parts.length === 1 && bindings && ts2.isNamedImports(bindings)) {
|
|
934
|
+
const binding = bindings.elements.find((item) => item.name.text === first);
|
|
935
|
+
if (binding) imported = binding.propertyName?.text ?? binding.name.text;
|
|
936
|
+
}
|
|
937
|
+
if (parts.length === 2 && bindings && ts2.isNamespaceImport(bindings) && bindings.name.text === first) imported = parts[1];
|
|
938
|
+
if (imported) {
|
|
939
|
+
const resolution = this.graph.resolveExport(file, statement.moduleSpecifier.text, imported, "type");
|
|
940
|
+
for (const issue of resolution.issues) issues.add(issue);
|
|
941
|
+
if (resolution.targets.length !== 1 && !resolution.issues.length) issues.add(`Cannot resolve a unique local type export ${imported} from ${statement.moduleSpecifier.text}.`);
|
|
942
|
+
if (resolution.issues.length || resolution.targets.length !== 1) unsafeDependency = true;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
let symbol = checker.getSymbolAtLocation(name);
|
|
946
|
+
if (symbol?.flags && symbol.flags & ts2.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
|
|
947
|
+
if (!symbol || !symbol.declarations?.length) {
|
|
948
|
+
issues.add(`Cannot resolve type ${name.getText(current)} in ${file}.`);
|
|
949
|
+
unsafeDependency = true;
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
for (const declaration of symbol.declarations) {
|
|
953
|
+
if (this.isLibrary(declaration.getSourceFile().fileName)) continue;
|
|
954
|
+
if (!this.graph.files.has(this.relative(declaration.getSourceFile().fileName))) {
|
|
955
|
+
issues.add(`Type ${name.getText(current)} is declared outside scanned repository files.`);
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
if (ts2.isTypeAliasDeclaration(declaration) || ts2.isInterfaceDeclaration(declaration) || ts2.isClassDeclaration(declaration)) walk(declaration);
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
const walk = (node) => {
|
|
962
|
+
if (visited.has(node)) return;
|
|
963
|
+
visited.add(node);
|
|
964
|
+
diagnostics(node);
|
|
965
|
+
if (ts2.isClassDeclaration(node)) issues.add("Class-based props contracts are only partially supported; inherited and runtime-initialized members require inspection.");
|
|
966
|
+
if (ts2.isTypeReferenceNode(node)) reference(node.typeName);
|
|
967
|
+
if (ts2.isExpressionWithTypeArguments(node)) reference(node.expression);
|
|
968
|
+
if (ts2.isImportTypeNode(node)) {
|
|
969
|
+
const resolution = importType(node);
|
|
970
|
+
for (const issue of resolution.issues) issues.add(issue);
|
|
971
|
+
if (resolution.issues.length || resolution.targets.length !== 1) {
|
|
972
|
+
unsafeDependency = true;
|
|
973
|
+
if (!resolution.issues.length) issues.add("The import type does not identify a unique local exported declaration.");
|
|
974
|
+
} else if (node.qualifier) {
|
|
975
|
+
let symbol = checker.getSymbolAtLocation(node.qualifier);
|
|
976
|
+
if (symbol?.flags && symbol.flags & ts2.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
|
|
977
|
+
for (const declaration of symbol?.declarations ?? []) if (ts2.isTypeAliasDeclaration(declaration) || ts2.isInterfaceDeclaration(declaration) || ts2.isClassDeclaration(declaration)) walk(declaration);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
ts2.forEachChild(node, walk);
|
|
981
|
+
};
|
|
982
|
+
walk(input.type);
|
|
983
|
+
const type = checker.getTypeFromTypeNode(input.type);
|
|
984
|
+
const primitive = ts2.TypeFlags.StringLike | ts2.TypeFlags.NumberLike | ts2.TypeFlags.BooleanLike | ts2.TypeFlags.BigIntLike | ts2.TypeFlags.ESSymbolLike | ts2.TypeFlags.Null | ts2.TypeFlags.Undefined | ts2.TypeFlags.Void;
|
|
985
|
+
const nonObject = Boolean(type.flags & primitive) || type.isUnion() && type.types.some((member) => Boolean(member.flags & primitive));
|
|
986
|
+
if (nonObject) issues.add("The props annotation includes a non-object type; primitive members are not a component property API.");
|
|
987
|
+
if (type.flags & (ts2.TypeFlags.Any | ts2.TypeFlags.Unknown | ts2.TypeFlags.TypeParameter | ts2.TypeFlags.Conditional | ts2.TypeFlags.Never)) issues.add("The props object type could not be resolved to a concrete local contract.");
|
|
988
|
+
if (type.isUnion()) issues.add("Union props expose only common properties; branch-specific contracts remain unresolved.");
|
|
989
|
+
if (checker.getIndexInfosOfType(type).length) issues.add("The props type has an index signature; arbitrary property names are not enumerated.");
|
|
990
|
+
const proven = (node, seen) => {
|
|
991
|
+
if (seen.has(node)) return [];
|
|
992
|
+
const next = new Set(seen).add(node);
|
|
993
|
+
if (ts2.isParenthesizedTypeNode(node)) return proven(node.type, next);
|
|
994
|
+
if (ts2.isIntersectionTypeNode(node)) return node.types.flatMap((member) => proven(member, next));
|
|
995
|
+
if (ts2.isTypeLiteralNode(node)) return checker.getPropertiesOfType(checker.getTypeFromTypeNode(node));
|
|
996
|
+
if (ts2.isInterfaceDeclaration(node) || ts2.isClassDeclaration(node)) {
|
|
997
|
+
const own = node.members.flatMap((member) => member.name && !(ts2.canHaveModifiers(member) && ts2.getModifiers(member)?.some((modifier) => modifier.kind === ts2.SyntaxKind.StaticKeyword)) ? [checker.getSymbolAtLocation(member.name)].filter((symbol) => Boolean(symbol)) : []);
|
|
998
|
+
return [...own, ...(node.heritageClauses ?? []).flatMap((clause) => clause.types.flatMap((base) => proven(base, next)))];
|
|
999
|
+
}
|
|
1000
|
+
if (ts2.isTypeAliasDeclaration(node)) return proven(node.type, next);
|
|
1001
|
+
if (ts2.isImportTypeNode(node)) {
|
|
1002
|
+
const resolution = importType(node);
|
|
1003
|
+
if (resolution.issues.length || resolution.targets.length !== 1) return [];
|
|
1004
|
+
let symbol = node.qualifier && checker.getSymbolAtLocation(node.qualifier);
|
|
1005
|
+
if (symbol?.flags && symbol.flags & ts2.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
|
|
1006
|
+
return (symbol?.declarations ?? []).filter((declaration) => !this.isLibrary(declaration.getSourceFile().fileName)).flatMap((declaration) => proven(declaration, next));
|
|
1007
|
+
}
|
|
1008
|
+
if (ts2.isTypeReferenceNode(node) || ts2.isExpressionWithTypeArguments(node)) {
|
|
1009
|
+
const name = ts2.isTypeReferenceNode(node) ? node.typeName : node.expression;
|
|
1010
|
+
const file = this.relative(node.getSourceFile().fileName);
|
|
1011
|
+
const parts = name.getText().split(".");
|
|
1012
|
+
for (const statement of node.getSourceFile().statements) {
|
|
1013
|
+
if (!ts2.isImportDeclaration(statement) || !ts2.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
1014
|
+
const clause = statement.importClause;
|
|
1015
|
+
const bindings = clause?.namedBindings;
|
|
1016
|
+
let imported = parts.length === 1 && clause?.name?.text === parts[0] ? "default" : void 0;
|
|
1017
|
+
if (parts.length === 1 && bindings && ts2.isNamedImports(bindings)) {
|
|
1018
|
+
const binding = bindings.elements.find((member) => member.name.text === parts[0]);
|
|
1019
|
+
if (binding) imported = binding.propertyName?.text ?? binding.name.text;
|
|
1020
|
+
}
|
|
1021
|
+
if (parts.length === 2 && bindings && ts2.isNamespaceImport(bindings) && bindings.name.text === parts[0]) imported = parts[1];
|
|
1022
|
+
if (imported) {
|
|
1023
|
+
const resolution = this.graph.resolveExport(file, statement.moduleSpecifier.text, imported, "type");
|
|
1024
|
+
if (resolution.issues.length || resolution.targets.length !== 1) return [];
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
let symbol = checker.getSymbolAtLocation(name);
|
|
1028
|
+
if (symbol?.flags && symbol.flags & ts2.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
|
|
1029
|
+
return (symbol?.declarations ?? []).filter((declaration) => !this.isLibrary(declaration.getSourceFile().fileName)).flatMap((declaration) => proven(declaration, next));
|
|
1030
|
+
}
|
|
1031
|
+
return [];
|
|
1032
|
+
};
|
|
1033
|
+
const candidates = nonObject ? [] : unsafeDependency || type.flags & (ts2.TypeFlags.Any | ts2.TypeFlags.Unknown) ? proven(input.type, /* @__PURE__ */ new Set()) : checker.getPropertiesOfType(type);
|
|
1034
|
+
const symbols = [...new Map(candidates.map((symbol) => [symbol.name, symbol])).values()];
|
|
1035
|
+
if (symbols.length > MAX_PROPS) issues.add(`The props contract exceeds ${MAX_PROPS} properties; only the first ${MAX_PROPS} are included.`);
|
|
1036
|
+
const props = symbols.slice(0, MAX_PROPS).map((symbol) => {
|
|
1037
|
+
const propertyType = checker.getTypeOfSymbolAtLocation(symbol, input.type);
|
|
1038
|
+
const required = !(symbol.flags & ts2.SymbolFlags.Optional);
|
|
1039
|
+
const members = propertyType.isUnion() ? propertyType.types.filter((member) => required || !(member.flags & ts2.TypeFlags.Undefined)) : [propertyType];
|
|
1040
|
+
if (members.some((member) => member.flags & (ts2.TypeFlags.TypeParameter | ts2.TypeFlags.Conditional | ts2.TypeFlags.IndexedAccess))) issues.add(`Property ${symbol.name} depends on unresolved generic type information.`);
|
|
1041
|
+
const values = members.length && members.every((member) => member.isStringLiteral()) ? members.map((member) => member.value).sort() : void 0;
|
|
1042
|
+
return {
|
|
1043
|
+
name: symbol.name,
|
|
1044
|
+
type: members.map((member) => checker.typeToString(member, input.type, ts2.TypeFormatFlags.NoTruncation | ts2.TypeFormatFlags.InTypeAlias)).join(" | ") || "undefined",
|
|
1045
|
+
required,
|
|
1046
|
+
values,
|
|
1047
|
+
defaultValue: defaults.get(symbol.name)
|
|
1048
|
+
};
|
|
1049
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
1050
|
+
return { props, resolution: { status: issues.size ? props.length ? "partial" : "unresolved" : "resolved", issues: [...issues].sort() } };
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
|
|
568
1056
|
// ../core/src/analyzers/components.ts
|
|
569
|
-
import
|
|
1057
|
+
import path5 from "path";
|
|
570
1058
|
function isUiPath(file) {
|
|
571
1059
|
return /(^|\/)(components\/ui|ui\/|components\/primitives)\//.test(file);
|
|
572
1060
|
}
|
|
@@ -579,7 +1067,7 @@ function isPageFile(file) {
|
|
|
579
1067
|
function importPathFor(evidence, file, name) {
|
|
580
1068
|
const aliases = evidence.componentsJson?.aliases ?? {};
|
|
581
1069
|
if (aliases.ui && isUiPath(file)) {
|
|
582
|
-
const base =
|
|
1070
|
+
const base = path5.posix.basename(file).replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
583
1071
|
return `${aliases.ui}/${base}`;
|
|
584
1072
|
}
|
|
585
1073
|
if (aliases.components && file.includes("components/")) {
|
|
@@ -646,18 +1134,21 @@ function analyzeComponents(evidence, tokenNames) {
|
|
|
646
1134
|
return records.sort((a, b) => a.id.localeCompare(b.id));
|
|
647
1135
|
}
|
|
648
1136
|
function toRecord(evidence, component, usage, architecture, local, id, tokenNames) {
|
|
1137
|
+
const api = resolveComponentProps(evidence, component);
|
|
649
1138
|
return {
|
|
650
1139
|
id,
|
|
651
1140
|
name: component.name,
|
|
652
1141
|
source: component.file,
|
|
653
1142
|
importPath: importPathFor(evidence, component.file, component.name),
|
|
654
1143
|
exportName: component.isDefault ? "default" : component.exportName,
|
|
655
|
-
props:
|
|
1144
|
+
props: api.props.map((prop) => ({
|
|
656
1145
|
name: prop.name,
|
|
657
1146
|
type: prop.type,
|
|
658
1147
|
required: prop.required,
|
|
659
|
-
values: prop.values
|
|
1148
|
+
values: prop.values,
|
|
1149
|
+
defaultValue: prop.defaultValue
|
|
660
1150
|
})),
|
|
1151
|
+
propsResolution: api.resolution,
|
|
661
1152
|
variants: component.variants,
|
|
662
1153
|
sizes: component.sizes,
|
|
663
1154
|
composition: component.composition,
|
|
@@ -668,7 +1159,7 @@ function toRecord(evidence, component, usage, architecture, local, id, tokenName
|
|
|
668
1159
|
architecture,
|
|
669
1160
|
local,
|
|
670
1161
|
status: "observed",
|
|
671
|
-
confidence: "high",
|
|
1162
|
+
confidence: api.resolution.status === "resolved" ? "high" : api.resolution.status === "partial" ? "medium" : "low",
|
|
672
1163
|
originKind: "implementation",
|
|
673
1164
|
evidence: [
|
|
674
1165
|
{
|
|
@@ -677,7 +1168,13 @@ function toRecord(evidence, component, usage, architecture, local, id, tokenName
|
|
|
677
1168
|
description: `${component.name} declared in ${component.file}`,
|
|
678
1169
|
location: { file: component.file, startLine: component.startLine },
|
|
679
1170
|
count: usage.length
|
|
680
|
-
}
|
|
1171
|
+
},
|
|
1172
|
+
...api.resolution.issues.map((description, index) => ({
|
|
1173
|
+
id: `${id}:api-${index}`,
|
|
1174
|
+
kind: "component-api-resolution",
|
|
1175
|
+
description,
|
|
1176
|
+
location: { file: component.file, startLine: component.startLine }
|
|
1177
|
+
}))
|
|
681
1178
|
]
|
|
682
1179
|
};
|
|
683
1180
|
}
|
|
@@ -688,6 +1185,7 @@ var init_components = __esm({
|
|
|
688
1185
|
init_src();
|
|
689
1186
|
init_hash();
|
|
690
1187
|
init_component_resolution();
|
|
1188
|
+
init_type_resolution();
|
|
691
1189
|
}
|
|
692
1190
|
});
|
|
693
1191
|
|
|
@@ -1444,37 +1942,37 @@ var init_tokens = __esm({
|
|
|
1444
1942
|
});
|
|
1445
1943
|
|
|
1446
1944
|
// ../core/src/paths.ts
|
|
1447
|
-
import
|
|
1945
|
+
import path6 from "path";
|
|
1448
1946
|
function specDir(root) {
|
|
1449
|
-
return
|
|
1947
|
+
return path6.join(root, SPEC_DIR);
|
|
1450
1948
|
}
|
|
1451
1949
|
function specPaths(root) {
|
|
1452
1950
|
const dir = specDir(root);
|
|
1453
1951
|
return {
|
|
1454
1952
|
dir,
|
|
1455
|
-
system:
|
|
1456
|
-
tokens:
|
|
1457
|
-
components:
|
|
1458
|
-
patterns:
|
|
1459
|
-
intents:
|
|
1460
|
-
conventions:
|
|
1461
|
-
exceptions:
|
|
1462
|
-
generated:
|
|
1463
|
-
history:
|
|
1464
|
-
historyLog:
|
|
1465
|
-
rejections:
|
|
1466
|
-
baseline:
|
|
1467
|
-
sources:
|
|
1468
|
-
discrepancies:
|
|
1469
|
-
cache:
|
|
1470
|
-
gitignore:
|
|
1953
|
+
system: path6.join(dir, "system.json"),
|
|
1954
|
+
tokens: path6.join(dir, "tokens"),
|
|
1955
|
+
components: path6.join(dir, "components"),
|
|
1956
|
+
patterns: path6.join(dir, "patterns"),
|
|
1957
|
+
intents: path6.join(dir, "intents"),
|
|
1958
|
+
conventions: path6.join(dir, "conventions"),
|
|
1959
|
+
exceptions: path6.join(dir, "exceptions"),
|
|
1960
|
+
generated: path6.join(dir, "generated"),
|
|
1961
|
+
history: path6.join(dir, "history"),
|
|
1962
|
+
historyLog: path6.join(dir, "history", "events.jsonl"),
|
|
1963
|
+
rejections: path6.join(dir, "rejections.json"),
|
|
1964
|
+
baseline: path6.join(dir, "baseline.json"),
|
|
1965
|
+
sources: path6.join(dir, "sources.json"),
|
|
1966
|
+
discrepancies: path6.join(dir, "discrepancies.json"),
|
|
1967
|
+
cache: path6.join(dir, "cache"),
|
|
1968
|
+
gitignore: path6.join(dir, ".gitignore")
|
|
1471
1969
|
};
|
|
1472
1970
|
}
|
|
1473
1971
|
function toPosix(filePath) {
|
|
1474
|
-
return filePath.split(
|
|
1972
|
+
return filePath.split(path6.sep).join("/");
|
|
1475
1973
|
}
|
|
1476
1974
|
function relativePosix(root, filePath) {
|
|
1477
|
-
return toPosix(
|
|
1975
|
+
return toPosix(path6.relative(root, filePath));
|
|
1478
1976
|
}
|
|
1479
1977
|
var init_paths = __esm({
|
|
1480
1978
|
"../core/src/paths.ts"() {
|
|
@@ -1499,7 +1997,7 @@ __export(fs_exports, {
|
|
|
1499
1997
|
});
|
|
1500
1998
|
import { execFile } from "child_process";
|
|
1501
1999
|
import { promisify } from "util";
|
|
1502
|
-
import
|
|
2000
|
+
import path7 from "path";
|
|
1503
2001
|
import fs from "fs/promises";
|
|
1504
2002
|
async function isGitRepo(root) {
|
|
1505
2003
|
try {
|
|
@@ -1592,7 +2090,7 @@ async function readText(filePath) {
|
|
|
1592
2090
|
}
|
|
1593
2091
|
}
|
|
1594
2092
|
async function writeText(filePath, content) {
|
|
1595
|
-
await fs.mkdir(
|
|
2093
|
+
await fs.mkdir(path7.dirname(filePath), { recursive: true });
|
|
1596
2094
|
await fs.writeFile(filePath, content, "utf8");
|
|
1597
2095
|
}
|
|
1598
2096
|
async function readJsonFile(filePath, fallback) {
|
|
@@ -1607,7 +2105,7 @@ async function readJsonFile(filePath, fallback) {
|
|
|
1607
2105
|
async function listJsonFiles(dir) {
|
|
1608
2106
|
try {
|
|
1609
2107
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1610
|
-
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) =>
|
|
2108
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => path7.join(dir, entry.name)).sort();
|
|
1611
2109
|
} catch {
|
|
1612
2110
|
return [];
|
|
1613
2111
|
}
|
|
@@ -1624,37 +2122,37 @@ var init_fs = __esm({
|
|
|
1624
2122
|
});
|
|
1625
2123
|
|
|
1626
2124
|
// ../core/src/parse.ts
|
|
1627
|
-
import
|
|
2125
|
+
import ts3 from "typescript";
|
|
1628
2126
|
import postcss from "postcss";
|
|
1629
|
-
function
|
|
1630
|
-
if (file.endsWith(".tsx")) return
|
|
1631
|
-
if (file.endsWith(".jsx")) return
|
|
2127
|
+
function scriptKind2(file) {
|
|
2128
|
+
if (file.endsWith(".tsx")) return ts3.ScriptKind.TSX;
|
|
2129
|
+
if (file.endsWith(".jsx")) return ts3.ScriptKind.JSX;
|
|
1632
2130
|
if (file.endsWith(".js") || file.endsWith(".mjs") || file.endsWith(".cjs")) {
|
|
1633
|
-
return
|
|
2131
|
+
return ts3.ScriptKind.JS;
|
|
1634
2132
|
}
|
|
1635
|
-
return
|
|
2133
|
+
return ts3.ScriptKind.TS;
|
|
1636
2134
|
}
|
|
1637
2135
|
function lineOf(source, node) {
|
|
1638
2136
|
return source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
|
|
1639
2137
|
}
|
|
1640
2138
|
function isJsx(node) {
|
|
1641
|
-
return
|
|
2139
|
+
return ts3.isJsxElement(node) || ts3.isJsxSelfClosingElement(node) || ts3.isJsxFragment(node);
|
|
1642
2140
|
}
|
|
1643
2141
|
function nodeReturnsJsx(node) {
|
|
1644
2142
|
let found = false;
|
|
1645
2143
|
const visit = (child) => {
|
|
1646
2144
|
if (found) return;
|
|
1647
|
-
if (
|
|
2145
|
+
if (ts3.isReturnStatement(child) && child.expression && containsJsx(child.expression)) {
|
|
1648
2146
|
found = true;
|
|
1649
2147
|
return;
|
|
1650
2148
|
}
|
|
1651
|
-
if (
|
|
2149
|
+
if (ts3.isArrowFunction(child) || ts3.isFunctionExpression(child) || ts3.isFunctionDeclaration(child)) {
|
|
1652
2150
|
return;
|
|
1653
2151
|
}
|
|
1654
|
-
|
|
2152
|
+
ts3.forEachChild(child, visit);
|
|
1655
2153
|
};
|
|
1656
|
-
|
|
1657
|
-
if (
|
|
2154
|
+
ts3.forEachChild(node, visit);
|
|
2155
|
+
if (ts3.isArrowFunction(node) && node.body && containsJsx(node.body)) return true;
|
|
1658
2156
|
return found;
|
|
1659
2157
|
}
|
|
1660
2158
|
function containsJsx(node) {
|
|
@@ -1666,9 +2164,9 @@ function containsJsx(node) {
|
|
|
1666
2164
|
found = true;
|
|
1667
2165
|
return;
|
|
1668
2166
|
}
|
|
1669
|
-
|
|
2167
|
+
ts3.forEachChild(child, visit);
|
|
1670
2168
|
};
|
|
1671
|
-
|
|
2169
|
+
ts3.forEachChild(node, visit);
|
|
1672
2170
|
return found;
|
|
1673
2171
|
}
|
|
1674
2172
|
function isComponentName(name) {
|
|
@@ -1676,18 +2174,18 @@ function isComponentName(name) {
|
|
|
1676
2174
|
}
|
|
1677
2175
|
function collectClassesFromExpr(expr) {
|
|
1678
2176
|
if (!expr) return { classes: [], dynamic: false };
|
|
1679
|
-
if (
|
|
2177
|
+
if (ts3.isStringLiteral(expr) || ts3.isNoSubstitutionTemplateLiteral(expr)) {
|
|
1680
2178
|
return { classes: splitClasses(expr.text), dynamic: false };
|
|
1681
2179
|
}
|
|
1682
|
-
if (
|
|
2180
|
+
if (ts3.isTemplateExpression(expr)) {
|
|
1683
2181
|
const parts = [expr.head.text, ...expr.templateSpans.map((span) => span.literal.text)];
|
|
1684
2182
|
return { classes: splitClasses(parts.join(" ")), dynamic: true };
|
|
1685
2183
|
}
|
|
1686
|
-
if (
|
|
2184
|
+
if (ts3.isCallExpression(expr)) {
|
|
1687
2185
|
const name = callName(expr);
|
|
1688
2186
|
if (name && CLASS_FNS.has(name)) {
|
|
1689
2187
|
const merged = expr.arguments.map(
|
|
1690
|
-
(arg) =>
|
|
2188
|
+
(arg) => ts3.isExpression(arg) ? collectClassesFromExpr(arg) : { classes: [], dynamic: false }
|
|
1691
2189
|
);
|
|
1692
2190
|
return {
|
|
1693
2191
|
classes: merged.flatMap((item) => item.classes),
|
|
@@ -1696,7 +2194,7 @@ function collectClassesFromExpr(expr) {
|
|
|
1696
2194
|
}
|
|
1697
2195
|
return { classes: [], dynamic: true };
|
|
1698
2196
|
}
|
|
1699
|
-
if (
|
|
2197
|
+
if (ts3.isConditionalExpression(expr)) {
|
|
1700
2198
|
const left = collectClassesFromExpr(expr.whenTrue);
|
|
1701
2199
|
const right = collectClassesFromExpr(expr.whenFalse);
|
|
1702
2200
|
return {
|
|
@@ -1704,7 +2202,7 @@ function collectClassesFromExpr(expr) {
|
|
|
1704
2202
|
dynamic: true
|
|
1705
2203
|
};
|
|
1706
2204
|
}
|
|
1707
|
-
if (
|
|
2205
|
+
if (ts3.isBinaryExpression(expr)) {
|
|
1708
2206
|
const left = collectClassesFromExpr(expr.left);
|
|
1709
2207
|
const right = collectClassesFromExpr(expr.right);
|
|
1710
2208
|
return {
|
|
@@ -1712,13 +2210,13 @@ function collectClassesFromExpr(expr) {
|
|
|
1712
2210
|
dynamic: left.dynamic || right.dynamic
|
|
1713
2211
|
};
|
|
1714
2212
|
}
|
|
1715
|
-
if (
|
|
2213
|
+
if (ts3.isParenthesizedExpression(expr)) {
|
|
1716
2214
|
return collectClassesFromExpr(expr.expression);
|
|
1717
2215
|
}
|
|
1718
|
-
if (
|
|
2216
|
+
if (ts3.isObjectLiteralExpression(expr)) {
|
|
1719
2217
|
const classes = [];
|
|
1720
2218
|
for (const prop of expr.properties) {
|
|
1721
|
-
if (
|
|
2219
|
+
if (ts3.isPropertyAssignment(prop) && ts3.isStringLiteral(prop.initializer)) {
|
|
1722
2220
|
classes.push(...splitClasses(prop.initializer.text));
|
|
1723
2221
|
}
|
|
1724
2222
|
}
|
|
@@ -1727,17 +2225,17 @@ function collectClassesFromExpr(expr) {
|
|
|
1727
2225
|
return { classes: [], dynamic: true };
|
|
1728
2226
|
}
|
|
1729
2227
|
function callName(expr) {
|
|
1730
|
-
if (
|
|
1731
|
-
if (
|
|
2228
|
+
if (ts3.isIdentifier(expr.expression)) return expr.expression.text;
|
|
2229
|
+
if (ts3.isPropertyAccessExpression(expr.expression)) return expr.expression.name.text;
|
|
1732
2230
|
return void 0;
|
|
1733
2231
|
}
|
|
1734
2232
|
function unionValues(typeNode) {
|
|
1735
2233
|
if (!typeNode) return void 0;
|
|
1736
|
-
if (
|
|
1737
|
-
const values = typeNode.types.map((item) =>
|
|
2234
|
+
if (ts3.isUnionTypeNode(typeNode)) {
|
|
2235
|
+
const values = typeNode.types.map((item) => ts3.isLiteralTypeNode(item) && ts3.isStringLiteral(item.literal) ? item.literal.text : void 0).filter((item) => Boolean(item));
|
|
1738
2236
|
return values.length ? values : void 0;
|
|
1739
2237
|
}
|
|
1740
|
-
if (
|
|
2238
|
+
if (ts3.isLiteralTypeNode(typeNode) && ts3.isStringLiteral(typeNode.literal)) {
|
|
1741
2239
|
return [typeNode.literal.text];
|
|
1742
2240
|
}
|
|
1743
2241
|
return void 0;
|
|
@@ -1745,7 +2243,7 @@ function unionValues(typeNode) {
|
|
|
1745
2243
|
function propsFromTypeLiteral(literal) {
|
|
1746
2244
|
const props = [];
|
|
1747
2245
|
for (const member of literal.members) {
|
|
1748
|
-
if (!
|
|
2246
|
+
if (!ts3.isPropertySignature(member) || !member.name) continue;
|
|
1749
2247
|
const name = member.name.getText();
|
|
1750
2248
|
props.push({
|
|
1751
2249
|
name,
|
|
@@ -1758,22 +2256,22 @@ function propsFromTypeLiteral(literal) {
|
|
|
1758
2256
|
}
|
|
1759
2257
|
function findInterface(source, name) {
|
|
1760
2258
|
for (const stmt of source.statements) {
|
|
1761
|
-
if (
|
|
1762
|
-
if (
|
|
2259
|
+
if (ts3.isInterfaceDeclaration(stmt) && stmt.name.text === name) return stmt;
|
|
2260
|
+
if (ts3.isTypeAliasDeclaration(stmt) && stmt.name.text === name) return stmt;
|
|
1763
2261
|
}
|
|
1764
2262
|
return void 0;
|
|
1765
2263
|
}
|
|
1766
2264
|
function propsFromType(source, typeNode) {
|
|
1767
2265
|
if (!typeNode) return [];
|
|
1768
|
-
if (
|
|
1769
|
-
if (
|
|
2266
|
+
if (ts3.isTypeLiteralNode(typeNode)) return propsFromTypeLiteral(typeNode);
|
|
2267
|
+
if (ts3.isTypeReferenceNode(typeNode) && ts3.isIdentifier(typeNode.typeName)) {
|
|
1770
2268
|
const decl = findInterface(source, typeNode.typeName.text);
|
|
1771
2269
|
if (!decl) return [];
|
|
1772
|
-
if (
|
|
1773
|
-
const fake =
|
|
2270
|
+
if (ts3.isInterfaceDeclaration(decl)) {
|
|
2271
|
+
const fake = ts3.factory.createTypeLiteralNode(decl.members);
|
|
1774
2272
|
return propsFromTypeLiteral(fake);
|
|
1775
2273
|
}
|
|
1776
|
-
if (
|
|
2274
|
+
if (ts3.isTypeAliasDeclaration(decl) && decl.type && ts3.isTypeLiteralNode(decl.type)) {
|
|
1777
2275
|
return propsFromTypeLiteral(decl.type);
|
|
1778
2276
|
}
|
|
1779
2277
|
}
|
|
@@ -1783,7 +2281,7 @@ function propsFromFunction(source, fn) {
|
|
|
1783
2281
|
const first = fn.parameters[0];
|
|
1784
2282
|
if (!first) return [];
|
|
1785
2283
|
if (first.type) return propsFromType(source, first.type);
|
|
1786
|
-
if (
|
|
2284
|
+
if (ts3.isObjectBindingPattern(first.name)) {
|
|
1787
2285
|
return first.name.elements.map((el) => ({
|
|
1788
2286
|
name: el.name.getText(),
|
|
1789
2287
|
required: !el.dotDotDotToken
|
|
@@ -1791,15 +2289,15 @@ function propsFromFunction(source, fn) {
|
|
|
1791
2289
|
}
|
|
1792
2290
|
return [];
|
|
1793
2291
|
}
|
|
1794
|
-
function
|
|
1795
|
-
if (
|
|
2292
|
+
function unwrap2(node) {
|
|
2293
|
+
if (ts3.isCallExpression(node)) {
|
|
1796
2294
|
const name = callName(node);
|
|
1797
2295
|
if (name === "forwardRef" || name === "memo") {
|
|
1798
2296
|
const arg = node.arguments[0];
|
|
1799
|
-
if (arg &&
|
|
2297
|
+
if (arg && ts3.isExpression(arg)) return unwrap2(arg);
|
|
1800
2298
|
}
|
|
1801
2299
|
}
|
|
1802
|
-
if (
|
|
2300
|
+
if (ts3.isParenthesizedExpression(node)) return unwrap2(node.expression);
|
|
1803
2301
|
return node;
|
|
1804
2302
|
}
|
|
1805
2303
|
function extractCva(node) {
|
|
@@ -1807,24 +2305,24 @@ function extractCva(node) {
|
|
|
1807
2305
|
const sizes = [];
|
|
1808
2306
|
const classes = [];
|
|
1809
2307
|
const base = node.arguments[0];
|
|
1810
|
-
if (base &&
|
|
2308
|
+
if (base && ts3.isExpression(base)) {
|
|
1811
2309
|
classes.push(...collectClassesFromExpr(base).classes);
|
|
1812
2310
|
}
|
|
1813
2311
|
const config = node.arguments[1];
|
|
1814
|
-
if (config &&
|
|
2312
|
+
if (config && ts3.isObjectLiteralExpression(config)) {
|
|
1815
2313
|
for (const prop of config.properties) {
|
|
1816
|
-
if (!
|
|
1817
|
-
if (prop.name.text !== "variants" || !
|
|
2314
|
+
if (!ts3.isPropertyAssignment(prop) || !ts3.isIdentifier(prop.name)) continue;
|
|
2315
|
+
if (prop.name.text !== "variants" || !ts3.isObjectLiteralExpression(prop.initializer)) continue;
|
|
1818
2316
|
for (const group of prop.initializer.properties) {
|
|
1819
|
-
if (!
|
|
2317
|
+
if (!ts3.isPropertyAssignment(group) || !ts3.isIdentifier(group.name)) continue;
|
|
1820
2318
|
const groupName = group.name.text;
|
|
1821
|
-
if (!
|
|
1822
|
-
const keys = group.initializer.properties.map((item) =>
|
|
2319
|
+
if (!ts3.isObjectLiteralExpression(group.initializer)) continue;
|
|
2320
|
+
const keys = group.initializer.properties.map((item) => ts3.isPropertyAssignment(item) ? item.name.getText().replace(/['"]/g, "") : void 0).filter((item) => Boolean(item));
|
|
1823
2321
|
if (groupName === "size") sizes.push(...keys);
|
|
1824
2322
|
else if (groupName === "variant") variants.push(...keys);
|
|
1825
2323
|
else variants.push(...keys.map((key) => `${groupName}:${key}`));
|
|
1826
2324
|
for (const item of group.initializer.properties) {
|
|
1827
|
-
if (
|
|
2325
|
+
if (ts3.isPropertyAssignment(item) && ts3.isExpression(item.initializer)) {
|
|
1828
2326
|
classes.push(...collectClassesFromExpr(item.initializer).classes);
|
|
1829
2327
|
}
|
|
1830
2328
|
}
|
|
@@ -1839,11 +2337,11 @@ function tagName(node) {
|
|
|
1839
2337
|
function jsxAttributes(node) {
|
|
1840
2338
|
const attrs = {};
|
|
1841
2339
|
for (const attr of node.properties) {
|
|
1842
|
-
if (!
|
|
2340
|
+
if (!ts3.isJsxAttribute(attr) || !attr.initializer) continue;
|
|
1843
2341
|
const name = attr.name.getText();
|
|
1844
|
-
if (
|
|
1845
|
-
else if (
|
|
1846
|
-
if (
|
|
2342
|
+
if (ts3.isStringLiteral(attr.initializer)) attrs[name] = attr.initializer.text;
|
|
2343
|
+
else if (ts3.isJsxExpression(attr.initializer) && attr.initializer.expression) {
|
|
2344
|
+
if (ts3.isStringLiteral(attr.initializer.expression)) {
|
|
1847
2345
|
attrs[name] = attr.initializer.expression.text;
|
|
1848
2346
|
}
|
|
1849
2347
|
}
|
|
@@ -1851,28 +2349,28 @@ function jsxAttributes(node) {
|
|
|
1851
2349
|
return attrs;
|
|
1852
2350
|
}
|
|
1853
2351
|
function opening(node) {
|
|
1854
|
-
return
|
|
2352
|
+
return ts3.isJsxElement(node) ? node.openingElement : node;
|
|
1855
2353
|
}
|
|
1856
2354
|
function walkJsxTree(node, tags, components, families, depth) {
|
|
1857
2355
|
let childCount = 0;
|
|
1858
2356
|
let maxDepth = depth;
|
|
1859
|
-
const children =
|
|
1860
|
-
if (!
|
|
2357
|
+
const children = ts3.isJsxFragment(node) ? node.children : ts3.isJsxElement(node) ? node.children : [];
|
|
2358
|
+
if (!ts3.isJsxFragment(node)) {
|
|
1861
2359
|
const open = opening(node);
|
|
1862
2360
|
const name = tagName(open.tagName);
|
|
1863
2361
|
if (isComponentName(name)) components.push(name);
|
|
1864
2362
|
else tags.push(name);
|
|
1865
2363
|
const classAttr = open.attributes.properties.find(
|
|
1866
|
-
(prop) =>
|
|
2364
|
+
(prop) => ts3.isJsxAttribute(prop) && prop.name.getText() === "className"
|
|
1867
2365
|
);
|
|
1868
|
-
if (classAttr &&
|
|
2366
|
+
if (classAttr && ts3.isJsxAttribute(classAttr)) {
|
|
1869
2367
|
const expr = classAttr.initializer;
|
|
1870
|
-
const collected = expr &&
|
|
2368
|
+
const collected = expr && ts3.isJsxExpression(expr) ? collectClassesFromExpr(expr.expression) : expr && ts3.isStringLiteral(expr) ? { classes: splitClasses(expr.text), dynamic: false } : { classes: [], dynamic: false };
|
|
1871
2369
|
for (const cls of collected.classes) families.add(classFamily(cls));
|
|
1872
2370
|
}
|
|
1873
2371
|
}
|
|
1874
2372
|
for (const child of children) {
|
|
1875
|
-
if (
|
|
2373
|
+
if (ts3.isJsxElement(child) || ts3.isJsxSelfClosingElement(child) || ts3.isJsxFragment(child)) {
|
|
1876
2374
|
childCount += 1;
|
|
1877
2375
|
const nested = walkJsxTree(child, tags, components, families, depth + 1);
|
|
1878
2376
|
childCount += nested.childCount;
|
|
@@ -1887,25 +2385,25 @@ function variantsOwnerName(name) {
|
|
|
1887
2385
|
return `${base[0].toUpperCase()}${base.slice(1)}`;
|
|
1888
2386
|
}
|
|
1889
2387
|
function findComponent(components, name) {
|
|
1890
|
-
const
|
|
2388
|
+
const owner2 = variantsOwnerName(name);
|
|
1891
2389
|
return components.find(
|
|
1892
|
-
(item) => item.name === name || item.name ===
|
|
2390
|
+
(item) => item.name === name || item.name === owner2 || item.name.toLowerCase() === name.toLowerCase()
|
|
1893
2391
|
);
|
|
1894
2392
|
}
|
|
1895
2393
|
function headingFrom(node) {
|
|
1896
2394
|
let heading;
|
|
1897
2395
|
const visit = (child) => {
|
|
1898
2396
|
if (heading) return;
|
|
1899
|
-
if (
|
|
2397
|
+
if (ts3.isJsxElement(child)) {
|
|
1900
2398
|
const name = tagName(child.openingElement.tagName);
|
|
1901
2399
|
if (/^h[1-3]$/.test(name)) {
|
|
1902
|
-
heading = child.children.map((item) =>
|
|
2400
|
+
heading = child.children.map((item) => ts3.isJsxText(item) ? item.getText() : "").join("").trim();
|
|
1903
2401
|
return;
|
|
1904
2402
|
}
|
|
1905
2403
|
}
|
|
1906
|
-
|
|
2404
|
+
ts3.forEachChild(child, visit);
|
|
1907
2405
|
};
|
|
1908
|
-
|
|
2406
|
+
ts3.forEachChild(node, visit);
|
|
1909
2407
|
return heading || void 0;
|
|
1910
2408
|
}
|
|
1911
2409
|
function isPlaceholderSrc(src) {
|
|
@@ -1931,14 +2429,14 @@ function cssVariables(content, file) {
|
|
|
1931
2429
|
}
|
|
1932
2430
|
function commentsFromSource(source, relative) {
|
|
1933
2431
|
const hits = [];
|
|
1934
|
-
const trivia =
|
|
2432
|
+
const trivia = ts3.getLeadingCommentRanges(source.text, 0) ?? [];
|
|
1935
2433
|
const ranges = [...trivia];
|
|
1936
2434
|
const visit = (node) => {
|
|
1937
|
-
const leading =
|
|
2435
|
+
const leading = ts3.getLeadingCommentRanges(source.text, node.pos) ?? [];
|
|
1938
2436
|
ranges.push(...leading);
|
|
1939
|
-
|
|
2437
|
+
ts3.forEachChild(node, visit);
|
|
1940
2438
|
};
|
|
1941
|
-
|
|
2439
|
+
ts3.forEachChild(source, visit);
|
|
1942
2440
|
const seen = /* @__PURE__ */ new Set();
|
|
1943
2441
|
for (const range of ranges) {
|
|
1944
2442
|
if (seen.has(range.pos)) continue;
|
|
@@ -1973,12 +2471,12 @@ function extractFileEvidence(input) {
|
|
|
1973
2471
|
empty.comments = input.content.split("\n").map((text, index) => ({ file: input.relative, text, startLine: index + 1 })).filter((item) => /TODO|FIXME|placeholder/i.test(item.text));
|
|
1974
2472
|
return empty;
|
|
1975
2473
|
}
|
|
1976
|
-
const source =
|
|
2474
|
+
const source = ts3.createSourceFile(
|
|
1977
2475
|
input.relative,
|
|
1978
2476
|
input.content,
|
|
1979
|
-
|
|
2477
|
+
ts3.ScriptTarget.Latest,
|
|
1980
2478
|
true,
|
|
1981
|
-
|
|
2479
|
+
scriptKind2(input.relative)
|
|
1982
2480
|
);
|
|
1983
2481
|
const components = [];
|
|
1984
2482
|
const classes = [];
|
|
@@ -1989,29 +2487,29 @@ function extractFileEvidence(input) {
|
|
|
1989
2487
|
const exportedClassConstants = [];
|
|
1990
2488
|
const interfaces = /* @__PURE__ */ new Map();
|
|
1991
2489
|
for (const stmt of source.statements) {
|
|
1992
|
-
if (
|
|
1993
|
-
interfaces.set(stmt.name.text, propsFromTypeLiteral(
|
|
2490
|
+
if (ts3.isInterfaceDeclaration(stmt)) {
|
|
2491
|
+
interfaces.set(stmt.name.text, propsFromTypeLiteral(ts3.factory.createTypeLiteralNode(stmt.members)));
|
|
1994
2492
|
}
|
|
1995
2493
|
}
|
|
1996
2494
|
function maybeComponent(name, node, exported, isDefault, start) {
|
|
1997
2495
|
if (!isComponentName(name)) return;
|
|
1998
|
-
const fn =
|
|
1999
|
-
const expr = !fn &&
|
|
2496
|
+
const fn = ts3.isFunctionLike(node) ? node : void 0;
|
|
2497
|
+
const expr = !fn && ts3.isExpression(node) ? unwrap2(node) : node;
|
|
2000
2498
|
const bodyNode = fn ?? expr;
|
|
2001
|
-
if (!nodeReturnsJsx(bodyNode) && !(
|
|
2002
|
-
if (!(
|
|
2499
|
+
if (!nodeReturnsJsx(bodyNode) && !(ts3.isFunctionLike(bodyNode) && bodyNode.body && containsJsx(bodyNode.body))) {
|
|
2500
|
+
if (!(ts3.isArrowFunction(expr) && expr.body && containsJsx(expr.body))) return;
|
|
2003
2501
|
}
|
|
2004
|
-
const props = fn ? propsFromFunction(source, fn) :
|
|
2502
|
+
const props = fn ? propsFromFunction(source, fn) : ts3.isFunctionLike(expr) ? propsFromFunction(source, expr) : [];
|
|
2005
2503
|
const composition = /* @__PURE__ */ new Set();
|
|
2006
2504
|
const classNames = [];
|
|
2007
2505
|
const visitComp = (child) => {
|
|
2008
|
-
if (
|
|
2506
|
+
if (ts3.isJsxOpeningElement(child) || ts3.isJsxSelfClosingElement(child)) {
|
|
2009
2507
|
const tag = tagName(child.tagName);
|
|
2010
2508
|
if (isComponentName(tag)) composition.add(tag);
|
|
2011
2509
|
}
|
|
2012
|
-
|
|
2510
|
+
ts3.forEachChild(child, visitComp);
|
|
2013
2511
|
};
|
|
2014
|
-
|
|
2512
|
+
ts3.forEachChild(bodyNode, visitComp);
|
|
2015
2513
|
components.push({
|
|
2016
2514
|
name,
|
|
2017
2515
|
file: input.relative,
|
|
@@ -2028,7 +2526,7 @@ function extractFileEvidence(input) {
|
|
|
2028
2526
|
}
|
|
2029
2527
|
const cvaPending = /* @__PURE__ */ new Map();
|
|
2030
2528
|
function visit(node) {
|
|
2031
|
-
if (
|
|
2529
|
+
if (ts3.isImportDeclaration(node) && ts3.isStringLiteral(node.moduleSpecifier)) {
|
|
2032
2530
|
const names = [];
|
|
2033
2531
|
const importedBindings = [];
|
|
2034
2532
|
let defaultName;
|
|
@@ -2038,13 +2536,13 @@ function extractFileEvidence(input) {
|
|
|
2038
2536
|
importedBindings.push({ local: defaultName, imported: "default" });
|
|
2039
2537
|
}
|
|
2040
2538
|
const bindings = clause?.namedBindings;
|
|
2041
|
-
if (bindings &&
|
|
2539
|
+
if (bindings && ts3.isNamedImports(bindings)) {
|
|
2042
2540
|
for (const el of bindings.elements) {
|
|
2043
2541
|
names.push(el.name.text);
|
|
2044
|
-
importedBindings.push({ local: el.name.text, imported: el.propertyName?.text ?? el.name.text });
|
|
2542
|
+
importedBindings.push({ local: el.name.text, imported: el.propertyName?.text ?? el.name.text, isTypeOnly: el.isTypeOnly || void 0 });
|
|
2045
2543
|
}
|
|
2046
2544
|
}
|
|
2047
|
-
if (bindings &&
|
|
2545
|
+
if (bindings && ts3.isNamespaceImport(bindings)) importedBindings.push({ local: bindings.name.text, imported: "*" });
|
|
2048
2546
|
imports.push({
|
|
2049
2547
|
file: input.relative,
|
|
2050
2548
|
source: node.moduleSpecifier.text,
|
|
@@ -2054,16 +2552,16 @@ function extractFileEvidence(input) {
|
|
|
2054
2552
|
isTypeOnly: Boolean(clause?.isTypeOnly)
|
|
2055
2553
|
});
|
|
2056
2554
|
}
|
|
2057
|
-
if (
|
|
2058
|
-
const exported = Boolean(node.modifiers?.some((m) => m.kind ===
|
|
2059
|
-
maybeComponent(node.name.text, node, exported, Boolean(node.modifiers?.some((m) => m.kind ===
|
|
2555
|
+
if (ts3.isFunctionDeclaration(node) && node.name) {
|
|
2556
|
+
const exported = Boolean(node.modifiers?.some((m) => m.kind === ts3.SyntaxKind.ExportKeyword));
|
|
2557
|
+
maybeComponent(node.name.text, node, exported, Boolean(node.modifiers?.some((m) => m.kind === ts3.SyntaxKind.DefaultKeyword)), node);
|
|
2060
2558
|
}
|
|
2061
|
-
if (
|
|
2062
|
-
const exported = Boolean(node.modifiers?.some((m) => m.kind ===
|
|
2559
|
+
if (ts3.isVariableStatement(node)) {
|
|
2560
|
+
const exported = Boolean(node.modifiers?.some((m) => m.kind === ts3.SyntaxKind.ExportKeyword));
|
|
2063
2561
|
for (const decl of node.declarationList.declarations) {
|
|
2064
|
-
if (!
|
|
2562
|
+
if (!ts3.isIdentifier(decl.name) || !decl.initializer) continue;
|
|
2065
2563
|
const name = decl.name.text;
|
|
2066
|
-
if (/class|classes|className/i.test(name) && (
|
|
2564
|
+
if (/class|classes|className/i.test(name) && (ts3.isStringLiteral(decl.initializer) || ts3.isNoSubstitutionTemplateLiteral(decl.initializer))) {
|
|
2067
2565
|
exportedClassConstants.push(name);
|
|
2068
2566
|
classes.push({
|
|
2069
2567
|
file: input.relative,
|
|
@@ -2074,39 +2572,39 @@ function extractFileEvidence(input) {
|
|
|
2074
2572
|
startLine: lineOf(source, decl)
|
|
2075
2573
|
});
|
|
2076
2574
|
}
|
|
2077
|
-
const init =
|
|
2078
|
-
if (
|
|
2575
|
+
const init = unwrap2(decl.initializer);
|
|
2576
|
+
if (ts3.isCallExpression(decl.initializer) && callName(decl.initializer) && CVA_NAMES.has(callName(decl.initializer))) {
|
|
2079
2577
|
const extracted = extractCva(decl.initializer);
|
|
2080
|
-
const
|
|
2578
|
+
const owner2 = variantsOwnerName(name);
|
|
2081
2579
|
classes.push({
|
|
2082
2580
|
file: input.relative,
|
|
2083
2581
|
classes: extracted.classes,
|
|
2084
2582
|
dynamic: false,
|
|
2085
2583
|
exported,
|
|
2086
2584
|
kind: "cva",
|
|
2087
|
-
componentName:
|
|
2585
|
+
componentName: owner2,
|
|
2088
2586
|
startLine: lineOf(source, decl)
|
|
2089
2587
|
});
|
|
2090
|
-
const existing = findComponent(components,
|
|
2588
|
+
const existing = findComponent(components, owner2);
|
|
2091
2589
|
if (existing) {
|
|
2092
2590
|
existing.variants = extracted.variants;
|
|
2093
2591
|
existing.sizes = extracted.sizes;
|
|
2094
2592
|
existing.classNames.push(...extracted.classes);
|
|
2095
2593
|
} else {
|
|
2096
|
-
cvaPending.set(
|
|
2594
|
+
cvaPending.set(owner2, extracted);
|
|
2097
2595
|
}
|
|
2098
2596
|
}
|
|
2099
2597
|
maybeComponent(name, init, exported, false, decl);
|
|
2100
2598
|
}
|
|
2101
2599
|
}
|
|
2102
|
-
if (
|
|
2600
|
+
if (ts3.isExportAssignment(node) && ts3.isIdentifier(node.expression)) {
|
|
2103
2601
|
const target = components.find((item) => item.name === node.expression.getText());
|
|
2104
2602
|
if (target) {
|
|
2105
2603
|
target.isDefault = true;
|
|
2106
2604
|
target.exported = true;
|
|
2107
2605
|
}
|
|
2108
2606
|
}
|
|
2109
|
-
if (
|
|
2607
|
+
if (ts3.isCallExpression(node) && callName(node) && CVA_NAMES.has(callName(node))) {
|
|
2110
2608
|
const extracted = extractCva(node);
|
|
2111
2609
|
classes.push({
|
|
2112
2610
|
file: input.relative,
|
|
@@ -2117,8 +2615,8 @@ function extractFileEvidence(input) {
|
|
|
2117
2615
|
startLine: lineOf(source, node)
|
|
2118
2616
|
});
|
|
2119
2617
|
}
|
|
2120
|
-
if (
|
|
2121
|
-
const collected = node.initializer &&
|
|
2618
|
+
if (ts3.isJsxAttribute(node) && node.name.getText() === "className") {
|
|
2619
|
+
const collected = node.initializer && ts3.isJsxExpression(node.initializer) ? collectClassesFromExpr(node.initializer.expression) : node.initializer && ts3.isStringLiteral(node.initializer) ? { classes: splitClasses(node.initializer.text), dynamic: false } : { classes: [], dynamic: false };
|
|
2122
2620
|
if (collected.classes.length) {
|
|
2123
2621
|
classes.push({
|
|
2124
2622
|
file: input.relative,
|
|
@@ -2130,14 +2628,14 @@ function extractFileEvidence(input) {
|
|
|
2130
2628
|
});
|
|
2131
2629
|
}
|
|
2132
2630
|
}
|
|
2133
|
-
if (
|
|
2631
|
+
if (ts3.isJsxElement(node) || ts3.isJsxSelfClosingElement(node)) {
|
|
2134
2632
|
const open = opening(node);
|
|
2135
2633
|
const tag = tagName(open.tagName);
|
|
2136
2634
|
const attrs = jsxAttributes(open.attributes);
|
|
2137
2635
|
const classAttr = open.attributes.properties.find(
|
|
2138
|
-
(prop) =>
|
|
2636
|
+
(prop) => ts3.isJsxAttribute(prop) && prop.name.getText() === "className"
|
|
2139
2637
|
);
|
|
2140
|
-
const collected = classAttr &&
|
|
2638
|
+
const collected = classAttr && ts3.isJsxAttribute(classAttr) ? classAttr.initializer && ts3.isJsxExpression(classAttr.initializer) ? collectClassesFromExpr(classAttr.initializer.expression) : classAttr.initializer && ts3.isStringLiteral(classAttr.initializer) ? { classes: splitClasses(classAttr.initializer.text), dynamic: false } : { classes: [], dynamic: false } : { classes: [], dynamic: false };
|
|
2141
2639
|
jsx.push({
|
|
2142
2640
|
file: input.relative,
|
|
2143
2641
|
tag,
|
|
@@ -2145,7 +2643,7 @@ function extractFileEvidence(input) {
|
|
|
2145
2643
|
classes: collected.classes,
|
|
2146
2644
|
attributes: attrs,
|
|
2147
2645
|
startLine: lineOf(source, node),
|
|
2148
|
-
selfClosing:
|
|
2646
|
+
selfClosing: ts3.isJsxSelfClosingElement(node)
|
|
2149
2647
|
});
|
|
2150
2648
|
if (tag === "img" || tag === "Image") {
|
|
2151
2649
|
images.push({
|
|
@@ -2168,7 +2666,7 @@ function extractFileEvidence(input) {
|
|
|
2168
2666
|
});
|
|
2169
2667
|
}
|
|
2170
2668
|
const structural = !isComponentName(tag) && (tag === "section" || tag === "article" || tag === "header" || tag === "aside" || tag === "main" || tag === "div" && collected.classes.some((cls) => /^(flex|grid|space-y|rounded|border)/.test(cls)));
|
|
2171
|
-
if (structural &&
|
|
2669
|
+
if (structural && ts3.isJsxElement(node)) {
|
|
2172
2670
|
const tags = [];
|
|
2173
2671
|
const comps = [];
|
|
2174
2672
|
const families = /* @__PURE__ */ new Set();
|
|
@@ -2187,7 +2685,7 @@ function extractFileEvidence(input) {
|
|
|
2187
2685
|
}
|
|
2188
2686
|
}
|
|
2189
2687
|
}
|
|
2190
|
-
|
|
2688
|
+
ts3.forEachChild(node, visit);
|
|
2191
2689
|
}
|
|
2192
2690
|
visit(source);
|
|
2193
2691
|
for (const [name, extracted] of cvaPending) {
|
|
@@ -2199,8 +2697,8 @@ function extractFileEvidence(input) {
|
|
|
2199
2697
|
}
|
|
2200
2698
|
}
|
|
2201
2699
|
for (const stmt of source.statements) {
|
|
2202
|
-
if (
|
|
2203
|
-
const exported = Boolean(stmt.modifiers?.some((m) => m.kind ===
|
|
2700
|
+
if (ts3.isFunctionDeclaration(stmt) && stmt.name && isComponentName(stmt.name.text) && nodeReturnsJsx(stmt)) {
|
|
2701
|
+
const exported = Boolean(stmt.modifiers?.some((m) => m.kind === ts3.SyntaxKind.ExportKeyword));
|
|
2204
2702
|
if (!exported && /page|layout|route/i.test(input.relative)) {
|
|
2205
2703
|
const tags = [];
|
|
2206
2704
|
const comps = [];
|
|
@@ -2237,13 +2735,13 @@ function extractFileEvidence(input) {
|
|
|
2237
2735
|
}
|
|
2238
2736
|
function walkJsxTreeFromBody(body, tags, components, families) {
|
|
2239
2737
|
const visit = (node) => {
|
|
2240
|
-
if (
|
|
2738
|
+
if (ts3.isJsxElement(node) || ts3.isJsxSelfClosingElement(node) || ts3.isJsxFragment(node)) {
|
|
2241
2739
|
walkJsxTree(node, tags, components, families, 1);
|
|
2242
2740
|
return;
|
|
2243
2741
|
}
|
|
2244
|
-
|
|
2742
|
+
ts3.forEachChild(node, visit);
|
|
2245
2743
|
};
|
|
2246
|
-
|
|
2744
|
+
ts3.forEachChild(body, visit);
|
|
2247
2745
|
}
|
|
2248
2746
|
var CVA_NAMES, CLASS_FNS, PASCAL;
|
|
2249
2747
|
var init_parse = __esm({
|
|
@@ -2259,13 +2757,14 @@ var init_parse = __esm({
|
|
|
2259
2757
|
|
|
2260
2758
|
// ../core/src/scan.ts
|
|
2261
2759
|
import fs2 from "fs/promises";
|
|
2262
|
-
import
|
|
2760
|
+
import path8 from "path";
|
|
2263
2761
|
import fg from "fast-glob";
|
|
2264
2762
|
import ignore from "ignore";
|
|
2763
|
+
import ts4 from "typescript";
|
|
2265
2764
|
async function discoverFiles(root) {
|
|
2266
2765
|
const ig = ignore();
|
|
2267
2766
|
ig.add(DEFAULT_IGNORE);
|
|
2268
|
-
const gitignore = await readText(
|
|
2767
|
+
const gitignore = await readText(path8.join(root, ".gitignore"));
|
|
2269
2768
|
if (gitignore) ig.add(gitignore);
|
|
2270
2769
|
const found = await fg(
|
|
2271
2770
|
[
|
|
@@ -2291,7 +2790,7 @@ async function discoverFiles(root) {
|
|
|
2291
2790
|
return found.map((file) => toPosix(file)).filter((file) => !ig.ignores(file)).sort();
|
|
2292
2791
|
}
|
|
2293
2792
|
async function loadPackageJson(root) {
|
|
2294
|
-
const raw = await readText(
|
|
2793
|
+
const raw = await readText(path8.join(root, "package.json"));
|
|
2295
2794
|
if (!raw) return void 0;
|
|
2296
2795
|
try {
|
|
2297
2796
|
return JSON.parse(raw);
|
|
@@ -2300,7 +2799,7 @@ async function loadPackageJson(root) {
|
|
|
2300
2799
|
}
|
|
2301
2800
|
}
|
|
2302
2801
|
async function loadComponentsJson(root) {
|
|
2303
|
-
const raw = await readText(
|
|
2802
|
+
const raw = await readText(path8.join(root, "components.json"));
|
|
2304
2803
|
if (!raw) return void 0;
|
|
2305
2804
|
try {
|
|
2306
2805
|
return JSON.parse(raw);
|
|
@@ -2309,11 +2808,13 @@ async function loadComponentsJson(root) {
|
|
|
2309
2808
|
}
|
|
2310
2809
|
}
|
|
2311
2810
|
async function loadTsconfigPaths(root) {
|
|
2312
|
-
const raw = await readText(
|
|
2811
|
+
const raw = await readText(path8.join(root, "tsconfig.json"));
|
|
2313
2812
|
if (!raw) return {};
|
|
2314
2813
|
try {
|
|
2315
|
-
const parsed =
|
|
2316
|
-
|
|
2814
|
+
const parsed = ts4.parseConfigFileTextToJson("tsconfig.json", raw).config;
|
|
2815
|
+
const paths = parsed?.compilerOptions?.paths;
|
|
2816
|
+
if (!paths || typeof paths !== "object" || Array.isArray(paths)) return {};
|
|
2817
|
+
return Object.fromEntries(Object.entries(paths).filter((entry) => Array.isArray(entry[1]) && entry[1].every((value) => typeof value === "string")));
|
|
2317
2818
|
} catch {
|
|
2318
2819
|
return {};
|
|
2319
2820
|
}
|
|
@@ -2329,9 +2830,9 @@ async function collectEvidence(root, options = {}) {
|
|
|
2329
2830
|
const files = [];
|
|
2330
2831
|
const excluded = ignore().add(DEFAULT_IGNORE);
|
|
2331
2832
|
for (const relative of relativeFiles) {
|
|
2332
|
-
const abs =
|
|
2333
|
-
const fromRoot =
|
|
2334
|
-
if (fromRoot === ".." || fromRoot.startsWith(`..${
|
|
2833
|
+
const abs = path8.join(root, relative);
|
|
2834
|
+
const fromRoot = path8.relative(path8.resolve(root), path8.resolve(abs));
|
|
2835
|
+
if (fromRoot === ".." || fromRoot.startsWith(`..${path8.sep}`) || path8.isAbsolute(fromRoot)) {
|
|
2335
2836
|
throw new Error(`Source path is outside the repository: ${relative}`);
|
|
2336
2837
|
}
|
|
2337
2838
|
if (excluded.ignores(relative)) continue;
|
|
@@ -2346,7 +2847,7 @@ async function collectEvidence(root, options = {}) {
|
|
|
2346
2847
|
content = content.replace(/<!-- design-rover:start -->[\s\S]*?<!-- design-rover:end -->/g, (block) => "\n".repeat(block.split("\n").length - 1));
|
|
2347
2848
|
if (!content.trim()) continue;
|
|
2348
2849
|
}
|
|
2349
|
-
const ext =
|
|
2850
|
+
const ext = path8.extname(relative);
|
|
2350
2851
|
const kind = SCRIPT_EXT.has(ext) ? "script" : STYLE_EXT.has(ext) || relative.endsWith(".module.css") ? "style" : relative.endsWith(".json") || relative.endsWith(".md") || relative.endsWith(".mdc") ? "json" : "other";
|
|
2351
2852
|
const normalizedKind = kind === "script" || kind === "style" ? kind : "other";
|
|
2352
2853
|
const hash = sha256(content);
|
|
@@ -2404,12 +2905,12 @@ var init_scan = __esm({
|
|
|
2404
2905
|
});
|
|
2405
2906
|
|
|
2406
2907
|
// ../core/src/cache.ts
|
|
2407
|
-
import
|
|
2908
|
+
import path9 from "path";
|
|
2408
2909
|
import fs3 from "fs/promises";
|
|
2409
2910
|
import { randomUUID } from "crypto";
|
|
2410
2911
|
import { z as z2 } from "zod";
|
|
2411
2912
|
async function loadCache(root) {
|
|
2412
|
-
const file =
|
|
2913
|
+
const file = path9.join(specPaths(root).cache, "evidence.json");
|
|
2413
2914
|
const raw = await readText(file);
|
|
2414
2915
|
if (!raw) return void 0;
|
|
2415
2916
|
try {
|
|
@@ -2417,7 +2918,7 @@ async function loadCache(root) {
|
|
|
2417
2918
|
if (!parsed.success) return void 0;
|
|
2418
2919
|
const paths = parsed.data.files.map((item) => item.relative);
|
|
2419
2920
|
if (new Set(paths).size !== paths.length) return void 0;
|
|
2420
|
-
if (paths.some((relative) =>
|
|
2921
|
+
if (paths.some((relative) => path9.isAbsolute(relative) || relative.split(/[\\/]/).includes(".."))) return void 0;
|
|
2421
2922
|
if (parsed.data.files.some((item) => item.hash !== sha256(item.content))) return void 0;
|
|
2422
2923
|
if (parsed.data.hash !== sha256(parsed.data.files.map((item) => `${item.relative}\0${item.hash}`).join("\0"))) return void 0;
|
|
2423
2924
|
return parsed.data;
|
|
@@ -2427,9 +2928,9 @@ async function loadCache(root) {
|
|
|
2427
2928
|
}
|
|
2428
2929
|
async function saveCache(root, cache) {
|
|
2429
2930
|
const directory = specPaths(root).cache;
|
|
2430
|
-
const file =
|
|
2931
|
+
const file = path9.join(directory, "evidence.json");
|
|
2431
2932
|
await fs3.mkdir(directory, { recursive: true });
|
|
2432
|
-
const temporary =
|
|
2933
|
+
const temporary = path9.join(directory, `evidence-${randomUUID()}.tmp`);
|
|
2433
2934
|
try {
|
|
2434
2935
|
await fs3.writeFile(temporary, `${JSON.stringify(cache)}
|
|
2435
2936
|
`, "utf8");
|
|
@@ -2476,7 +2977,7 @@ var init_cache = __esm({
|
|
|
2476
2977
|
cssVariables: z2.array(z2.object({ name: z2.string(), value: z2.string(), line: z2.number().int().optional(), scope: z2.string().optional(), mode: z2.string().optional(), important: z2.boolean().optional() }).passthrough()),
|
|
2477
2978
|
exportedClassConstants: strings
|
|
2478
2979
|
}).passthrough();
|
|
2479
|
-
cacheSchema = z2.object({ version: z2.literal(
|
|
2980
|
+
cacheSchema = z2.object({ version: z2.literal(4), hash: z2.string(), files: z2.array(evidenceSchema) });
|
|
2480
2981
|
}
|
|
2481
2982
|
});
|
|
2482
2983
|
|
|
@@ -2521,7 +3022,7 @@ async function analyzeRepository(root, options = {}) {
|
|
|
2521
3022
|
documentation: framework.documentation
|
|
2522
3023
|
};
|
|
2523
3024
|
const nextCache = {
|
|
2524
|
-
version:
|
|
3025
|
+
version: 4,
|
|
2525
3026
|
hash: sha256(merged.files.map((file) => `${file.relative}\0${file.hash}`).join("\0")),
|
|
2526
3027
|
files: merged.files
|
|
2527
3028
|
};
|
|
@@ -2548,7 +3049,7 @@ var init_analyze = __esm({
|
|
|
2548
3049
|
});
|
|
2549
3050
|
|
|
2550
3051
|
// ../core/src/context.ts
|
|
2551
|
-
import
|
|
3052
|
+
import path10 from "path";
|
|
2552
3053
|
function renderContext(model, options = {}) {
|
|
2553
3054
|
const directory = options.directory ?? SPEC_DIR;
|
|
2554
3055
|
const body = [
|
|
@@ -2556,7 +3057,7 @@ function renderContext(model, options = {}) {
|
|
|
2556
3057
|
"",
|
|
2557
3058
|
"Before changing UI:",
|
|
2558
3059
|
"1. Run `design-rover status --json` to inspect freshness and pending review.",
|
|
2559
|
-
"2. Use `design-rover
|
|
3060
|
+
"2. Use `design-rover context <task> --path <file-or-directory> --json` for bounded, relevant components, tokens and approved decisions. Inspect coverage and omission warnings; raise --max-chars or narrow the request when approved decisions are omitted. Use `design-rover query <id> --json` for whole records or `design-rover explain <id>` for readable details and source locations.",
|
|
2560
3061
|
"3. Read the relevant implementation before using its imports, props and variants. A component name can exist in several packages; choose by ID and source.",
|
|
2561
3062
|
"4. Use approved conventions and exceptions in their documented scope. Observed code is evidence; inferred or suggested entries need review before becoming policy.",
|
|
2562
3063
|
"5. Match the relevant token scope and theme. Missing or unresolved information is not permission to invent an API or claim a rule was checked.",
|
|
@@ -2651,7 +3152,7 @@ async function writeGenerated(root, model, options = {}) {
|
|
|
2651
3152
|
const writes = [];
|
|
2652
3153
|
for (const entry of entries) {
|
|
2653
3154
|
if (options.writeRepoFiles === false && !entry.rel.startsWith(`${directory}/generated/`)) continue;
|
|
2654
|
-
const current = await readText(
|
|
3155
|
+
const current = await readText(path10.join(root, entry.rel));
|
|
2655
3156
|
if (entry.wrap) {
|
|
2656
3157
|
writes.push({ rel: entry.rel, content: wrapManaged(current, entry.content) });
|
|
2657
3158
|
continue;
|
|
@@ -2664,10 +3165,10 @@ async function writeGenerated(root, model, options = {}) {
|
|
|
2664
3165
|
}
|
|
2665
3166
|
const files = [];
|
|
2666
3167
|
for (const write of writes) {
|
|
2667
|
-
if (!options.dryRun) await writeText(
|
|
3168
|
+
if (!options.dryRun) await writeText(path10.join(root, write.rel), write.content);
|
|
2668
3169
|
files.push({ path: write.rel, hash: sha256(write.content), managed: true });
|
|
2669
3170
|
}
|
|
2670
|
-
if (!options.dryRun) await writeText(
|
|
3171
|
+
if (!options.dryRun) await writeText(path10.join(specPaths(root).generated, "projections.json"), `${JSON.stringify({ files }, null, 2)}
|
|
2671
3172
|
`);
|
|
2672
3173
|
return { ...rendered, files };
|
|
2673
3174
|
}
|
|
@@ -2681,7 +3182,7 @@ async function generatedHashes(root, model) {
|
|
|
2681
3182
|
stale.push(entry.rel);
|
|
2682
3183
|
continue;
|
|
2683
3184
|
}
|
|
2684
|
-
const current = await readText(
|
|
3185
|
+
const current = await readText(path10.join(root, entry.rel));
|
|
2685
3186
|
if (current === void 0) {
|
|
2686
3187
|
stale.push(entry.rel);
|
|
2687
3188
|
continue;
|
|
@@ -2732,7 +3233,8 @@ function componentSig(record) {
|
|
|
2732
3233
|
})).sort((a, b) => a.name.localeCompare(b.name)),
|
|
2733
3234
|
source: record.source,
|
|
2734
3235
|
importPath: record.importPath,
|
|
2735
|
-
exportName: record.exportName
|
|
3236
|
+
exportName: record.exportName,
|
|
3237
|
+
propsResolution: record.propsResolution
|
|
2736
3238
|
};
|
|
2737
3239
|
}
|
|
2738
3240
|
function tokenSig(record) {
|
|
@@ -3197,6 +3699,221 @@ var init_check = __esm({
|
|
|
3197
3699
|
}
|
|
3198
3700
|
});
|
|
3199
3701
|
|
|
3702
|
+
// ../core/src/retrieval.ts
|
|
3703
|
+
import path11 from "path";
|
|
3704
|
+
import ignore3 from "ignore";
|
|
3705
|
+
function normalizePath(value) {
|
|
3706
|
+
const input = value.trim().replace(/\\/g, "/");
|
|
3707
|
+
if (!input || input.length > 512 || input.includes("\0") || input.startsWith("/") || /^[a-z]:/i.test(input)) throw new Error("Context path must be a repository-relative file or directory (at most 512 characters).");
|
|
3708
|
+
const normalized = path11.posix.normalize(input).replace(/\/$/, "");
|
|
3709
|
+
if (normalized === ".." || normalized.startsWith("../")) throw new Error("Context path cannot leave the repository.");
|
|
3710
|
+
return normalized;
|
|
3711
|
+
}
|
|
3712
|
+
function owner(file) {
|
|
3713
|
+
const parts = file.split("/");
|
|
3714
|
+
return (parts[0] === "apps" || parts[0] === "packages") && parts[1] ? `${parts[0]}/${parts[1]}` : void 0;
|
|
3715
|
+
}
|
|
3716
|
+
function under(file, selected) {
|
|
3717
|
+
return selected === "." || file === selected || file.startsWith(`${selected}/`);
|
|
3718
|
+
}
|
|
3719
|
+
function sourceFiles(record) {
|
|
3720
|
+
const files = record.evidence.flatMap((item) => item.location?.file ? [item.location.file] : []);
|
|
3721
|
+
if ("source" in record) files.push(record.source);
|
|
3722
|
+
if ("origin" in record) files.push(record.origin);
|
|
3723
|
+
if ("occurrences" in record) files.push(...record.occurrences.map((item) => item.file));
|
|
3724
|
+
if ("sourceRefs" in record) files.push(...record.sourceRefs.flatMap((item) => item.location?.file ? [item.location.file] : []));
|
|
3725
|
+
return [...new Set(files.map((file) => file.replace(/\\/g, "/")).filter(Boolean))].sort(compare);
|
|
3726
|
+
}
|
|
3727
|
+
function lexicalScore(record, query, words) {
|
|
3728
|
+
const name = "name" in record ? record.name : record.subject;
|
|
3729
|
+
if (query.toLowerCase() === record.id.toLowerCase()) return 300;
|
|
3730
|
+
const title = normalizeText(`${record.id} ${name}`);
|
|
3731
|
+
const text = normalizeText(JSON.stringify(record));
|
|
3732
|
+
let score = query.toLowerCase() === name.toLowerCase() ? 150 : 0;
|
|
3733
|
+
for (const word of words) {
|
|
3734
|
+
if (title.includes(word)) score += 20;
|
|
3735
|
+
else if (text.includes(word)) score += 4;
|
|
3736
|
+
}
|
|
3737
|
+
return score;
|
|
3738
|
+
}
|
|
3739
|
+
function approvedDecision(match) {
|
|
3740
|
+
return match.record.status === "approved" && match.kind !== "component" && match.kind !== "token";
|
|
3741
|
+
}
|
|
3742
|
+
function visible(kind, record, includePending) {
|
|
3743
|
+
return record.status === "approved" || kind !== "exception" && record.status === "observed" || Boolean(includePending && (record.status === "inferred" || record.status === "suggested"));
|
|
3744
|
+
}
|
|
3745
|
+
function pathScopeMatches(patterns, selected, allowDescendants = true) {
|
|
3746
|
+
if (selected === ".") return "potential";
|
|
3747
|
+
const result = ignore3().add(patterns).test(selected);
|
|
3748
|
+
if (result.ignored) return "direct";
|
|
3749
|
+
if (result.unignored) return void 0;
|
|
3750
|
+
if (allowDescendants && patterns.some((pattern) => {
|
|
3751
|
+
if (pattern.startsWith("!")) return false;
|
|
3752
|
+
const rawPrefix = pattern.replace(/^\//, "").split(/[?*\[]/, 1)[0];
|
|
3753
|
+
const prefix = rawPrefix.replace(/\/$/, "");
|
|
3754
|
+
return !prefix || under(prefix, selected) || under(selected, prefix) || !rawPrefix.endsWith("/") && selected.startsWith(rawPrefix);
|
|
3755
|
+
})) return "potential";
|
|
3756
|
+
return void 0;
|
|
3757
|
+
}
|
|
3758
|
+
function retrieveContext(model, options = {}) {
|
|
3759
|
+
const query = options.query?.trim() ?? "";
|
|
3760
|
+
if (query.length > 512) throw new Error("Context query must be at most 512 characters.");
|
|
3761
|
+
const selected = options.path === void 0 ? void 0 : normalizePath(options.path);
|
|
3762
|
+
if (!query && selected === void 0) throw new Error("Provide a context query or a repository-relative path.");
|
|
3763
|
+
const limit = options.limit ?? 20;
|
|
3764
|
+
const maxChars = options.maxChars ?? 12e3;
|
|
3765
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) throw new Error("Context limit must be an integer from 1 to 100.");
|
|
3766
|
+
if (!Number.isInteger(maxChars) || maxChars < 1024 || maxChars > 1e5) throw new Error("Context maxChars must be an integer from 1024 to 100000.");
|
|
3767
|
+
const includePending = options.includePending ?? false;
|
|
3768
|
+
const words = [...new Set(lexicalWords(query))].slice(0, 32);
|
|
3769
|
+
const matches = [];
|
|
3770
|
+
const relevantComponents = /* @__PURE__ */ new Set();
|
|
3771
|
+
const relevantPaths = /* @__PURE__ */ new Set();
|
|
3772
|
+
const relevantOwners = /* @__PURE__ */ new Set();
|
|
3773
|
+
const relevantComponentSources = /* @__PURE__ */ new Set();
|
|
3774
|
+
const tokenNames = /* @__PURE__ */ new Set();
|
|
3775
|
+
function relevance(kind, record) {
|
|
3776
|
+
if (!visible(kind, record, includePending)) return void 0;
|
|
3777
|
+
const files = sourceFiles(record);
|
|
3778
|
+
const lexical = query ? lexicalScore(record, query, words) : 0;
|
|
3779
|
+
let pathScore = selected !== void 0 && files.some((file) => under(file, selected)) ? 100 : 0;
|
|
3780
|
+
const reasons = [];
|
|
3781
|
+
if (kind === "component" && selected !== void 0 && "usage" in record && record.usage.some((item) => under(item.file, selected))) {
|
|
3782
|
+
pathScore = Math.max(pathScore, 120);
|
|
3783
|
+
reasons.push("used-in-path");
|
|
3784
|
+
}
|
|
3785
|
+
const connected = kind === "token" && "origin" in record && (tokenNames.has(record.id) || tokenNames.has(record.name) && (owner(record.origin) === void 0 || relevantOwners.has(owner(record.origin)) || selected !== void 0 && owner(record.origin) === owner(selected)));
|
|
3786
|
+
if (connected) {
|
|
3787
|
+
pathScore = Math.max(pathScore, 80);
|
|
3788
|
+
reasons.push("component-token");
|
|
3789
|
+
}
|
|
3790
|
+
const globalDecision = (kind === "convention" || kind === "pattern" || kind === "intent") && !files.length;
|
|
3791
|
+
if (kind !== "component" && kind !== "token" && files.some((file) => relevantComponentSources.has(file))) {
|
|
3792
|
+
pathScore = Math.max(pathScore, 110);
|
|
3793
|
+
reasons.push("component-source-decision");
|
|
3794
|
+
}
|
|
3795
|
+
let applicable = false;
|
|
3796
|
+
if (kind === "exception" && "scope" in record && typeof record.scope === "object") {
|
|
3797
|
+
const scope = record.scope;
|
|
3798
|
+
const componentsMatch = !scope.components.length || scope.components.some((item) => relevantComponents.has(item));
|
|
3799
|
+
let pathsMatch;
|
|
3800
|
+
let potentialScope = false;
|
|
3801
|
+
try {
|
|
3802
|
+
const matched = selected !== void 0 ? pathScopeMatches(scope.paths, selected) : [...relevantPaths].map((file) => pathScopeMatches(scope.paths, file, false)).find(Boolean);
|
|
3803
|
+
pathsMatch = !scope.paths.length || Boolean(matched);
|
|
3804
|
+
potentialScope = scope.paths.length > 0 && matched === "potential";
|
|
3805
|
+
} catch (error) {
|
|
3806
|
+
throw new Error(`Cannot safely resolve exception scope ${record.id}: ${String(error)}`);
|
|
3807
|
+
}
|
|
3808
|
+
const pathRelevant = scope.paths.length > 0 && pathsMatch;
|
|
3809
|
+
applicable = Boolean((scope.paths.length || scope.components.length) && componentsMatch && pathsMatch);
|
|
3810
|
+
if (selected !== void 0 && !applicable && !pathRelevant) return void 0;
|
|
3811
|
+
if (applicable || pathRelevant) {
|
|
3812
|
+
pathScore = Math.max(pathScore, 140);
|
|
3813
|
+
reasons.push(potentialScope ? "path-exception-may-apply" : applicable ? "applicable-exception" : "path-exception-component-scope-unresolved");
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
if (selected !== void 0) {
|
|
3817
|
+
const selectedOwner = owner(selected);
|
|
3818
|
+
const foreign = files.length > 0 && files.every((file) => owner(file) !== void 0 && owner(file) !== selectedOwner);
|
|
3819
|
+
if (foreign && !pathScore && !applicable) return void 0;
|
|
3820
|
+
if (files.length && !pathScore && lexical < 20 && !globalDecision && !applicable) return void 0;
|
|
3821
|
+
}
|
|
3822
|
+
if (!lexical && !pathScore && !globalDecision) return void 0;
|
|
3823
|
+
if (lexical) reasons.push("query-match");
|
|
3824
|
+
if (pathScore && !reasons.length) reasons.push("source-in-path");
|
|
3825
|
+
if (globalDecision) reasons.push("repository-decision");
|
|
3826
|
+
const name = "name" in record ? record.name : record.subject;
|
|
3827
|
+
const explicitMatch = query.toLowerCase() === record.id.toLowerCase() || query.toLowerCase() === name.toLowerCase();
|
|
3828
|
+
const nameWords = lexicalWords(name);
|
|
3829
|
+
const requestedComponent = kind === "component" && nameWords.length > 0 && nameWords.every((word) => words.includes(word));
|
|
3830
|
+
const requestBoost = explicitMatch ? 3e3 : requestedComponent ? 2e3 : 0;
|
|
3831
|
+
if (requestBoost) reasons.push("requested-record");
|
|
3832
|
+
const decisionBoost = record.status === "approved" && kind !== "component" && kind !== "token" ? 1e3 : 0;
|
|
3833
|
+
return { kind, score: requestBoost + decisionBoost + pathScore + lexical, reasons, sourceRefs: files, record };
|
|
3834
|
+
}
|
|
3835
|
+
for (const component of model.components) {
|
|
3836
|
+
const match = relevance("component", component);
|
|
3837
|
+
if (!match) continue;
|
|
3838
|
+
matches.push(match);
|
|
3839
|
+
relevantComponents.add(component.id);
|
|
3840
|
+
relevantComponents.add(component.name);
|
|
3841
|
+
relevantPaths.add(component.source);
|
|
3842
|
+
relevantOwners.add(owner(component.source));
|
|
3843
|
+
relevantComponentSources.add(component.source);
|
|
3844
|
+
for (const usage of component.usage) relevantPaths.add(usage.file);
|
|
3845
|
+
for (const token of component.relatedTokens) tokenNames.add(token);
|
|
3846
|
+
}
|
|
3847
|
+
const collections = { token: model.tokens, pattern: model.patterns, convention: model.conventions, intent: model.intents, exception: model.exceptions };
|
|
3848
|
+
for (const kind of ["token", "pattern", "convention", "intent", "exception"]) {
|
|
3849
|
+
for (const record of collections[kind]) {
|
|
3850
|
+
const match = relevance(kind, record);
|
|
3851
|
+
if (match) matches.push(match);
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
matches.sort((left, right) => right.score - left.score || compare(left.kind, right.kind) || compare(left.record.id, right.record.id));
|
|
3855
|
+
const decisionCount = matches.filter(approvedDecision).length;
|
|
3856
|
+
function packet(items, omittedByBudget2, omittedByLimit2) {
|
|
3857
|
+
const returnedDecisions = items.filter(approvedDecision).length;
|
|
3858
|
+
const omittedDecisions = decisionCount - returnedDecisions;
|
|
3859
|
+
const result2 = {
|
|
3860
|
+
version: 1,
|
|
3861
|
+
repository: model.system.repository.name,
|
|
3862
|
+
scope: { query: query || null, path: selected ?? null, includePending },
|
|
3863
|
+
limits: { limit, maxChars, outputChars: 0, unit: "UTF-16 code units including JSON formatting and final newline" },
|
|
3864
|
+
freshness: { checked: false, canonicalUpdatedAt: model.system.updatedAt, lastAnalyzedAt: model.system.lastAnalyzedAt ?? null, recordedSyncStatus: model.system.syncStatus },
|
|
3865
|
+
coverage: { matched: matches.length, returned: items.length, complete: items.length === matches.length, omittedByLimit: omittedByLimit2, omittedByBudget: omittedByBudget2, approvedDecisions: { matched: decisionCount, returned: returnedDecisions, omitted: omittedDecisions } },
|
|
3866
|
+
warnings: ["Canonical snapshot only; run status to check source freshness."],
|
|
3867
|
+
matches: items
|
|
3868
|
+
};
|
|
3869
|
+
if (omittedDecisions) result2.warnings.push("Applicable approved decisions were omitted. Retrieve them before treating this context as complete.");
|
|
3870
|
+
if (items.length < matches.length) result2.warnings.push("Whole records were omitted by the stated limits; raise the bounds or narrow the query/path. No record was sliced.");
|
|
3871
|
+
if (items.some((item) => item.reasons.some((reason) => reason === "path-exception-may-apply" || reason === "path-exception-component-scope-unresolved"))) result2.warnings.push("Some exception scopes may apply to descendants or unresolved components. Inspect their recorded scope before applying them.");
|
|
3872
|
+
for (let pass = 0; pass < 4; pass += 1) {
|
|
3873
|
+
const length = serializeContext(result2).length;
|
|
3874
|
+
if (length === result2.limits.outputChars) break;
|
|
3875
|
+
result2.limits.outputChars = length;
|
|
3876
|
+
}
|
|
3877
|
+
return result2;
|
|
3878
|
+
}
|
|
3879
|
+
const selectedMatches = [];
|
|
3880
|
+
let omittedByBudget = 0;
|
|
3881
|
+
let omittedByLimit = 0;
|
|
3882
|
+
if (serializeContext(packet([], matches.length, 0)).length > maxChars) throw new Error("Context metadata exceeds maxChars. Increase the bound or shorten the query/path.");
|
|
3883
|
+
for (const match of matches) {
|
|
3884
|
+
if (selectedMatches.length >= limit) {
|
|
3885
|
+
omittedByLimit += 1;
|
|
3886
|
+
continue;
|
|
3887
|
+
}
|
|
3888
|
+
const candidate = packet([...selectedMatches, match], omittedByBudget, matches.length - selectedMatches.length - 1 - omittedByBudget);
|
|
3889
|
+
if (serializeContext(candidate).length <= maxChars) selectedMatches.push(match);
|
|
3890
|
+
else omittedByBudget += 1;
|
|
3891
|
+
}
|
|
3892
|
+
let result = packet(selectedMatches, omittedByBudget, omittedByLimit);
|
|
3893
|
+
while (serializeContext(result).length > maxChars && selectedMatches.length) {
|
|
3894
|
+
selectedMatches.pop();
|
|
3895
|
+
omittedByBudget += 1;
|
|
3896
|
+
result = packet(selectedMatches, omittedByBudget, omittedByLimit);
|
|
3897
|
+
}
|
|
3898
|
+
if (serializeContext(result).length > maxChars) throw new Error("Context metadata exceeds maxChars. Increase the bound.");
|
|
3899
|
+
return result;
|
|
3900
|
+
}
|
|
3901
|
+
function serializeContext(packet) {
|
|
3902
|
+
return `${JSON.stringify(packet, null, 2)}
|
|
3903
|
+
`;
|
|
3904
|
+
}
|
|
3905
|
+
var STOP_WORDS, normalizeText, lexicalWords, compare;
|
|
3906
|
+
var init_retrieval = __esm({
|
|
3907
|
+
"../core/src/retrieval.ts"() {
|
|
3908
|
+
"use strict";
|
|
3909
|
+
init_esm_shims();
|
|
3910
|
+
STOP_WORDS = /* @__PURE__ */ new Set(["a", "an", "the", "to", "for", "with", "on", "in", "and", "or", "of", "this", "that", "please", "build", "create", "edit", "update", "change", "add", "implement", "make", "use", "using"]);
|
|
3911
|
+
normalizeText = (value) => value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
|
|
3912
|
+
lexicalWords = (value) => normalizeText(value).split(/[^a-z0-9_-]+/).filter((word) => word && !STOP_WORDS.has(word));
|
|
3913
|
+
compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
3914
|
+
}
|
|
3915
|
+
});
|
|
3916
|
+
|
|
3200
3917
|
// ../core/src/explain.ts
|
|
3201
3918
|
function explain(model, query) {
|
|
3202
3919
|
const q = query.trim().toLowerCase();
|
|
@@ -3221,6 +3938,8 @@ ${componentMatches.map((item) => `${item.id} \u2014 ${item.source}`).join("\n")}
|
|
|
3221
3938
|
`Variants: ${component.variants.join(", ") || "none"}`,
|
|
3222
3939
|
`Sizes: ${component.sizes.join(", ") || "none"}`,
|
|
3223
3940
|
`Props: ${component.props.map((prop) => `${prop.name}${prop.required ? "" : "?"}: ${prop.type ?? "unresolved"}${prop.defaultValue ? ` = ${prop.defaultValue}` : ""}`).join(", ") || "none"}`,
|
|
3941
|
+
`Props resolution: ${component.propsResolution?.status ?? "not recorded"}`,
|
|
3942
|
+
...component.propsResolution?.issues.map((issue) => ` ${issue}`) ?? [],
|
|
3224
3943
|
`Architecture: ${component.architecture}`,
|
|
3225
3944
|
`Usage: ${component.usage.reduce((sum, item) => sum + item.count, 0)} call sites in ${component.usage.length} files`,
|
|
3226
3945
|
`Status: ${component.status}`
|
|
@@ -3310,11 +4029,11 @@ var init_explain = __esm({
|
|
|
3310
4029
|
|
|
3311
4030
|
// ../core/src/store.ts
|
|
3312
4031
|
import fs4 from "fs/promises";
|
|
3313
|
-
import
|
|
4032
|
+
import path12 from "path";
|
|
3314
4033
|
async function listJsonFiles2(dir) {
|
|
3315
4034
|
try {
|
|
3316
4035
|
const entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
3317
|
-
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) =>
|
|
4036
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => path12.join(dir, entry.name)).sort();
|
|
3318
4037
|
} catch (error) {
|
|
3319
4038
|
if (error.code === "ENOENT") return [];
|
|
3320
4039
|
throw new Error(`Cannot read canonical directory ${dir}: ${String(error)}`);
|
|
@@ -3394,7 +4113,7 @@ async function isInitialized(root) {
|
|
|
3394
4113
|
}
|
|
3395
4114
|
async function loadCanonicalUnlocked(root) {
|
|
3396
4115
|
const paths = specPaths(root);
|
|
3397
|
-
const recoveryMarker =
|
|
4116
|
+
const recoveryMarker = path12.join(paths.dir, ".recovery-required.json");
|
|
3398
4117
|
if (await pathExists(recoveryMarker)) throw new Error(`Canonical storage needs recovery. Inspect ${recoveryMarker} before continuing.`);
|
|
3399
4118
|
if (!await pathExists(paths.system)) return void 0;
|
|
3400
4119
|
const system = await readJsonStrict(paths.system, void 0);
|
|
@@ -3423,7 +4142,7 @@ async function loadCanonicalUnlocked(root) {
|
|
|
3423
4142
|
async function loadCanonical(root) {
|
|
3424
4143
|
const paths = specPaths(root);
|
|
3425
4144
|
if (!await pathExists(paths.dir)) return void 0;
|
|
3426
|
-
const lockFile =
|
|
4145
|
+
const lockFile = path12.join(paths.dir, ".write.lock");
|
|
3427
4146
|
let lock;
|
|
3428
4147
|
try {
|
|
3429
4148
|
lock = await fs4.open(lockFile, "wx");
|
|
@@ -3444,7 +4163,7 @@ async function saveCanonical(root, model) {
|
|
|
3444
4163
|
model = validateCanonical(model);
|
|
3445
4164
|
const paths = specPaths(root);
|
|
3446
4165
|
await fs4.mkdir(paths.dir, { recursive: true });
|
|
3447
|
-
const lockFile =
|
|
4166
|
+
const lockFile = path12.join(paths.dir, ".write.lock");
|
|
3448
4167
|
let lock;
|
|
3449
4168
|
try {
|
|
3450
4169
|
lock = await fs4.open(lockFile, "wx");
|
|
@@ -3476,9 +4195,9 @@ async function saveCanonical(root, model) {
|
|
|
3476
4195
|
[paths.intents, model.intents],
|
|
3477
4196
|
[paths.exceptions, model.exceptions]
|
|
3478
4197
|
]) {
|
|
3479
|
-
const keep = new Set(records.map((record) =>
|
|
4198
|
+
const keep = new Set(records.map((record) => path12.join(dir, recordFileName(record.id))));
|
|
3480
4199
|
for (const file of await listJsonFiles2(dir)) if (!keep.has(file)) obsolete.push(file);
|
|
3481
|
-
for (const record of records) writes.set(
|
|
4200
|
+
for (const record of records) writes.set(path12.join(dir, recordFileName(record.id)), stableStringify(record));
|
|
3482
4201
|
}
|
|
3483
4202
|
let currentIgnore = "";
|
|
3484
4203
|
try {
|
|
@@ -3505,16 +4224,16 @@ async function saveCanonical(root, model) {
|
|
|
3505
4224
|
}
|
|
3506
4225
|
writes.set(paths.historyLog, history2.map((event) => JSON.stringify(event)).join("\n") + (history2.length ? "\n" : ""));
|
|
3507
4226
|
writes.set(paths.system, stableStringify(model.system));
|
|
3508
|
-
staging = await fs4.mkdtemp(
|
|
4227
|
+
staging = await fs4.mkdtemp(path12.join(paths.dir, ".write-"));
|
|
3509
4228
|
const staged = [];
|
|
3510
4229
|
for (const [destination, content] of writes) {
|
|
3511
|
-
const temporary =
|
|
4230
|
+
const temporary = path12.join(staging, `new-${staged.length}`);
|
|
3512
4231
|
await fs4.writeFile(temporary, content, "utf8");
|
|
3513
4232
|
staged.push([temporary, destination]);
|
|
3514
4233
|
}
|
|
3515
4234
|
const recovery = [];
|
|
3516
4235
|
for (const destination of /* @__PURE__ */ new Set([...writes.keys(), ...obsolete])) {
|
|
3517
|
-
const backup =
|
|
4236
|
+
const backup = path12.join(staging, `old-${recovery.length}`);
|
|
3518
4237
|
try {
|
|
3519
4238
|
await fs4.copyFile(destination, backup);
|
|
3520
4239
|
recovery.push({ destination, backup });
|
|
@@ -3523,13 +4242,13 @@ async function saveCanonical(root, model) {
|
|
|
3523
4242
|
recovery.push({ destination, backup: null });
|
|
3524
4243
|
}
|
|
3525
4244
|
}
|
|
3526
|
-
await fs4.writeFile(
|
|
4245
|
+
await fs4.writeFile(path12.join(staging, "recovery.json"), JSON.stringify({
|
|
3527
4246
|
canonicalDirectory: paths.dir,
|
|
3528
|
-
files: recovery.map((item) => ({ path:
|
|
4247
|
+
files: recovery.map((item) => ({ path: path12.relative(paths.dir, item.destination), backup: item.backup ? path12.basename(item.backup) : null }))
|
|
3529
4248
|
}, null, 2));
|
|
3530
4249
|
try {
|
|
3531
4250
|
for (const [temporary, destination] of staged) {
|
|
3532
|
-
await fs4.mkdir(
|
|
4251
|
+
await fs4.mkdir(path12.dirname(destination), { recursive: true });
|
|
3533
4252
|
await fs4.rename(temporary, destination);
|
|
3534
4253
|
}
|
|
3535
4254
|
for (const file of obsolete) await fs4.rm(file);
|
|
@@ -3540,9 +4259,9 @@ async function saveCanonical(root, model) {
|
|
|
3540
4259
|
try {
|
|
3541
4260
|
if (item.backup === null) await fs4.rm(item.destination, { force: true });
|
|
3542
4261
|
else {
|
|
3543
|
-
const temporary =
|
|
4262
|
+
const temporary = path12.join(staging, `restore-${index}`);
|
|
3544
4263
|
await fs4.copyFile(item.backup, temporary);
|
|
3545
|
-
await fs4.mkdir(
|
|
4264
|
+
await fs4.mkdir(path12.dirname(item.destination), { recursive: true });
|
|
3546
4265
|
await fs4.rename(temporary, item.destination);
|
|
3547
4266
|
}
|
|
3548
4267
|
} catch (rollbackError) {
|
|
@@ -3551,8 +4270,8 @@ async function saveCanonical(root, model) {
|
|
|
3551
4270
|
}
|
|
3552
4271
|
if (failures.length) {
|
|
3553
4272
|
retainRecovery = true;
|
|
3554
|
-
const marker =
|
|
3555
|
-
await fs4.writeFile(marker, JSON.stringify({ backup: staging, manifest:
|
|
4273
|
+
const marker = path12.join(paths.dir, ".recovery-required.json");
|
|
4274
|
+
await fs4.writeFile(marker, JSON.stringify({ backup: staging, manifest: path12.join(staging, "recovery.json") }, null, 2));
|
|
3556
4275
|
throw new AggregateError([error, ...failures], `Canonical save and rollback failed. Retained recovery backup at ${staging}; inspect ${marker}.`);
|
|
3557
4276
|
}
|
|
3558
4277
|
throw error;
|
|
@@ -3564,12 +4283,12 @@ async function saveCanonical(root, model) {
|
|
|
3564
4283
|
}
|
|
3565
4284
|
}
|
|
3566
4285
|
function emptyCanonical(root, name) {
|
|
3567
|
-
const
|
|
4286
|
+
const ts5 = nowIso();
|
|
3568
4287
|
return {
|
|
3569
4288
|
system: {
|
|
3570
4289
|
version: 1,
|
|
3571
|
-
createdAt:
|
|
3572
|
-
updatedAt:
|
|
4290
|
+
createdAt: ts5,
|
|
4291
|
+
updatedAt: ts5,
|
|
3573
4292
|
repository: { name, root },
|
|
3574
4293
|
framework: { name: "unknown" },
|
|
3575
4294
|
styling: { systems: [] },
|
|
@@ -3609,7 +4328,7 @@ var init_store = __esm({
|
|
|
3609
4328
|
});
|
|
3610
4329
|
|
|
3611
4330
|
// ../core/src/integrations-local.ts
|
|
3612
|
-
import
|
|
4331
|
+
import path13 from "path";
|
|
3613
4332
|
import fs5 from "fs/promises";
|
|
3614
4333
|
import { execFile as execFile2 } from "child_process";
|
|
3615
4334
|
import { promisify as promisify2 } from "util";
|
|
@@ -3651,7 +4370,7 @@ async function installGitHook(root) {
|
|
|
3651
4370
|
} catch {
|
|
3652
4371
|
throw new Error("Cannot install a Git hook outside a Git repository.");
|
|
3653
4372
|
}
|
|
3654
|
-
const dest =
|
|
4373
|
+
const dest = path13.resolve(root, hooksPath);
|
|
3655
4374
|
const existing = await fs5.readFile(dest, "utf8").catch((error) => {
|
|
3656
4375
|
if (error.code === "ENOENT") return void 0;
|
|
3657
4376
|
throw error;
|
|
@@ -3667,7 +4386,7 @@ async function installGitHook(root) {
|
|
|
3667
4386
|
if (error.code !== "ENOENT") throw error;
|
|
3668
4387
|
}
|
|
3669
4388
|
}
|
|
3670
|
-
await fs5.mkdir(
|
|
4389
|
+
await fs5.mkdir(path13.dirname(dest), { recursive: true });
|
|
3671
4390
|
const temporary = `${dest}.${randomUUID2()}.tmp`;
|
|
3672
4391
|
let moved = false;
|
|
3673
4392
|
try {
|
|
@@ -3714,7 +4433,7 @@ jobs:
|
|
|
3714
4433
|
}
|
|
3715
4434
|
async function installGithubWorkflow(root) {
|
|
3716
4435
|
const { writeText: writeText2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
3717
|
-
const current =
|
|
4436
|
+
const current = path13.join(root, ".github/workflows/design-rover.yml");
|
|
3718
4437
|
const read = async (file) => fs5.readFile(file, "utf8").catch((error) => {
|
|
3719
4438
|
if (error.code === "ENOENT") return void 0;
|
|
3720
4439
|
throw error;
|
|
@@ -3905,9 +4624,9 @@ var init_sync = __esm({
|
|
|
3905
4624
|
});
|
|
3906
4625
|
|
|
3907
4626
|
// ../core/src/engine.ts
|
|
3908
|
-
import
|
|
4627
|
+
import path14 from "path";
|
|
3909
4628
|
function repoName(root) {
|
|
3910
|
-
return
|
|
4629
|
+
return path14.basename(root);
|
|
3911
4630
|
}
|
|
3912
4631
|
async function createEngine(root) {
|
|
3913
4632
|
const initialized = await isInitialized(root);
|
|
@@ -4050,6 +4769,11 @@ async function createEngine(root) {
|
|
|
4050
4769
|
if (!model) return void 0;
|
|
4051
4770
|
return searchModel(model, query);
|
|
4052
4771
|
},
|
|
4772
|
+
context: async (options = {}) => {
|
|
4773
|
+
const model = await loadCanonical(root);
|
|
4774
|
+
if (!model) throw new Error("Design Rover is not initialized. Run `design-rover init` before retrieving context.");
|
|
4775
|
+
return retrieveContext(model, options);
|
|
4776
|
+
},
|
|
4053
4777
|
history: () => history(root),
|
|
4054
4778
|
checkSnippet: async (code, fileName) => {
|
|
4055
4779
|
const model = await loadCanonical(root);
|
|
@@ -4071,6 +4795,7 @@ var init_engine = __esm({
|
|
|
4071
4795
|
"use strict";
|
|
4072
4796
|
init_esm_shims();
|
|
4073
4797
|
init_src();
|
|
4798
|
+
init_retrieval();
|
|
4074
4799
|
init_analyze();
|
|
4075
4800
|
init_check();
|
|
4076
4801
|
init_usage_check();
|
|
@@ -4111,9 +4836,11 @@ __export(src_exports, {
|
|
|
4111
4836
|
pathExists: () => pathExists,
|
|
4112
4837
|
readText: () => readText,
|
|
4113
4838
|
renderContext: () => renderContext,
|
|
4839
|
+
retrieveContext: () => retrieveContext,
|
|
4114
4840
|
saveCanonical: () => saveCanonical,
|
|
4115
4841
|
searchModel: () => searchModel,
|
|
4116
4842
|
seedCanonical: () => seedCanonical,
|
|
4843
|
+
serializeContext: () => serializeContext,
|
|
4117
4844
|
specDir: () => specDir,
|
|
4118
4845
|
specPaths: () => specPaths,
|
|
4119
4846
|
usageDiagnostics: () => usageDiagnostics,
|
|
@@ -4127,6 +4854,7 @@ var init_src2 = __esm({
|
|
|
4127
4854
|
init_analyze();
|
|
4128
4855
|
init_check();
|
|
4129
4856
|
init_context();
|
|
4857
|
+
init_retrieval();
|
|
4130
4858
|
init_diff();
|
|
4131
4859
|
init_engine();
|
|
4132
4860
|
init_explain();
|
|
@@ -4213,7 +4941,7 @@ async function createMcpServer(root) {
|
|
|
4213
4941
|
async ({ includePending }) => {
|
|
4214
4942
|
const model = await requireModel();
|
|
4215
4943
|
return {
|
|
4216
|
-
content: [{ type: "text", text: JSON.stringify(model.conventions.filter((item) =>
|
|
4944
|
+
content: [{ type: "text", text: JSON.stringify(model.conventions.filter((item) => visible2(item, includePending)), null, 2) }]
|
|
4217
4945
|
};
|
|
4218
4946
|
}
|
|
4219
4947
|
);
|
|
@@ -4224,7 +4952,7 @@ async function createMcpServer(root) {
|
|
|
4224
4952
|
async ({ includePending }) => {
|
|
4225
4953
|
const model = await requireModel();
|
|
4226
4954
|
return {
|
|
4227
|
-
content: [{ type: "text", text: JSON.stringify(model.intents.filter((item) =>
|
|
4955
|
+
content: [{ type: "text", text: JSON.stringify(model.intents.filter((item) => visible2(item, includePending)), null, 2) }]
|
|
4228
4956
|
};
|
|
4229
4957
|
}
|
|
4230
4958
|
);
|
|
@@ -4246,7 +4974,7 @@ async function createMcpServer(root) {
|
|
|
4246
4974
|
async ({ query, includePending }) => {
|
|
4247
4975
|
await requireModel();
|
|
4248
4976
|
const result = await engine.search(query);
|
|
4249
|
-
const filtered = Object.fromEntries(Object.entries(result ?? {}).map(([kind, records]) => [kind, records.filter((item) =>
|
|
4977
|
+
const filtered = Object.fromEntries(Object.entries(result ?? {}).map(([kind, records]) => [kind, records.filter((item) => visible2(item, includePending))]));
|
|
4250
4978
|
return {
|
|
4251
4979
|
content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }]
|
|
4252
4980
|
};
|
|
@@ -4264,6 +4992,25 @@ async function createMcpServer(root) {
|
|
|
4264
4992
|
};
|
|
4265
4993
|
}
|
|
4266
4994
|
);
|
|
4995
|
+
server.registerTool(
|
|
4996
|
+
"get_context",
|
|
4997
|
+
{
|
|
4998
|
+
description: "Retrieve bounded canonical design context for a task/query and/or repository-relative path. Whole records are ranked deterministically; inspect coverage and omission warnings before relying on it. Does not scan source or check freshness.",
|
|
4999
|
+
inputSchema: {
|
|
5000
|
+
query: z3.string().max(512).optional(),
|
|
5001
|
+
path: z3.string().max(512).optional(),
|
|
5002
|
+
includePending: z3.boolean().optional(),
|
|
5003
|
+
limit: z3.number().int().min(1).max(100).optional(),
|
|
5004
|
+
maxChars: z3.number().int().min(1024).max(1e5).optional()
|
|
5005
|
+
},
|
|
5006
|
+
annotations: { readOnlyHint: true }
|
|
5007
|
+
},
|
|
5008
|
+
async (options) => {
|
|
5009
|
+
const model = await requireModel();
|
|
5010
|
+
const packet = retrieveContext(model, options);
|
|
5011
|
+
return { content: [{ type: "text", text: serializeContext(packet) }] };
|
|
5012
|
+
}
|
|
5013
|
+
);
|
|
4267
5014
|
server.tool(
|
|
4268
5015
|
"explain_violation",
|
|
4269
5016
|
"Explain a component, pattern, convention, or diagnostic in context.",
|
|
@@ -4279,7 +5026,7 @@ async function createMcpServer(root) {
|
|
|
4279
5026
|
function trusted(item) {
|
|
4280
5027
|
return item.status === "approved" || item.status === "observed";
|
|
4281
5028
|
}
|
|
4282
|
-
function
|
|
5029
|
+
function visible2(item, includePending) {
|
|
4283
5030
|
return trusted(item) || Boolean(includePending && (item.status === "inferred" || item.status === "suggested"));
|
|
4284
5031
|
}
|
|
4285
5032
|
function approvedModel(model) {
|
|
@@ -4312,7 +5059,7 @@ init_esm_shims();
|
|
|
4312
5059
|
init_src2();
|
|
4313
5060
|
init_src();
|
|
4314
5061
|
import { parseArgs } from "util";
|
|
4315
|
-
import
|
|
5062
|
+
import path15 from "path";
|
|
4316
5063
|
|
|
4317
5064
|
// ../integrations/src/index.ts
|
|
4318
5065
|
init_esm_shims();
|
|
@@ -4762,6 +5509,7 @@ Usage
|
|
|
4762
5509
|
design-rover diff Show canonical system changes
|
|
4763
5510
|
design-rover explain <name> Explain a component, pattern, convention, or intent
|
|
4764
5511
|
design-rover query <q> Search the canonical model
|
|
5512
|
+
design-rover context [task] Retrieve bounded design context for a task and/or path
|
|
4765
5513
|
design-rover generate Write AGENTS.md / CLAUDE.md / Cursor rules / skill
|
|
4766
5514
|
design-rover history Show how the canonical system changed
|
|
4767
5515
|
design-rover baseline Capture current findings as existing debt
|
|
@@ -4783,6 +5531,10 @@ Options
|
|
|
4783
5531
|
--version Print the CLI version
|
|
4784
5532
|
--cwd <path> Repository root
|
|
4785
5533
|
--base <ref> Git base for incremental / PR / CI
|
|
5534
|
+
--path <path> Context: repository-relative file or directory
|
|
5535
|
+
--limit <n> Context: maximum whole records (default 20, maximum 100)
|
|
5536
|
+
--max-chars <n> Context: maximum formatted JSON characters (default 12000)
|
|
5537
|
+
--include-pending Context: include inferences and suggestions for review
|
|
4786
5538
|
--fail-on <level> CI: error (default) | warning | none
|
|
4787
5539
|
`;
|
|
4788
5540
|
async function main() {
|
|
@@ -4796,6 +5548,10 @@ async function main() {
|
|
|
4796
5548
|
version: { type: "boolean", short: "v", default: false },
|
|
4797
5549
|
cwd: { type: "string" },
|
|
4798
5550
|
base: { type: "string" },
|
|
5551
|
+
path: { type: "string" },
|
|
5552
|
+
limit: { type: "string" },
|
|
5553
|
+
"max-chars": { type: "string" },
|
|
5554
|
+
"include-pending": { type: "boolean", default: false },
|
|
4799
5555
|
"fail-on": { type: "string", default: "error" },
|
|
4800
5556
|
approve: { type: "boolean", default: false },
|
|
4801
5557
|
generate: { type: "boolean", default: false },
|
|
@@ -4814,7 +5570,7 @@ async function main() {
|
|
|
4814
5570
|
`);
|
|
4815
5571
|
return;
|
|
4816
5572
|
}
|
|
4817
|
-
const root =
|
|
5573
|
+
const root = path15.resolve(parsed.values.cwd ?? process.cwd());
|
|
4818
5574
|
const command = parsed.positionals[0] ?? "status";
|
|
4819
5575
|
const rest = parsed.positionals.slice(1);
|
|
4820
5576
|
const engine = await createEngine(root);
|
|
@@ -4830,7 +5586,7 @@ async function main() {
|
|
|
4830
5586
|
generate: Boolean(parsed.values.generate)
|
|
4831
5587
|
});
|
|
4832
5588
|
printInitSummary({
|
|
4833
|
-
directory:
|
|
5589
|
+
directory: path15.relative(root, engine.specDir),
|
|
4834
5590
|
framework: result.snapshot.framework.name,
|
|
4835
5591
|
styling: result.snapshot.styling.systems,
|
|
4836
5592
|
counts: {
|
|
@@ -4953,6 +5709,17 @@ async function main() {
|
|
|
4953
5709
|
}
|
|
4954
5710
|
return;
|
|
4955
5711
|
}
|
|
5712
|
+
if (command === "context") {
|
|
5713
|
+
const packet = await engine.context({
|
|
5714
|
+
query: rest.join(" ") || void 0,
|
|
5715
|
+
path: parsed.values.path,
|
|
5716
|
+
includePending: Boolean(parsed.values["include-pending"]),
|
|
5717
|
+
limit: parsed.values.limit === void 0 ? void 0 : Number(parsed.values.limit),
|
|
5718
|
+
maxChars: parsed.values["max-chars"] === void 0 ? void 0 : Number(parsed.values["max-chars"])
|
|
5719
|
+
});
|
|
5720
|
+
process.stdout.write(serializeContext(packet));
|
|
5721
|
+
return;
|
|
5722
|
+
}
|
|
4956
5723
|
if (command === "query") {
|
|
4957
5724
|
const query = rest.join(" ");
|
|
4958
5725
|
if (!query) fail("Usage: design-rover query <text>");
|
|
@@ -5131,7 +5898,7 @@ async function main() {
|
|
|
5131
5898
|
const mapping = mapShadcnLint(model);
|
|
5132
5899
|
if (rest[0] === "install") {
|
|
5133
5900
|
const { writeText: writeText2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
|
|
5134
|
-
const dest =
|
|
5901
|
+
const dest = path15.join(engine.specDir, "generated/oxlintrc.shadcn.json");
|
|
5135
5902
|
await writeText2(dest, oxlintFromMap(mapping));
|
|
5136
5903
|
header();
|
|
5137
5904
|
line(` Wrote ${dest}`);
|