@lariocpt/flawless-react 0.0.0-stage → 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/.agents/skills/flawless-a11y/SKILL.md +113 -0
- package/.agents/skills/flawless-api/SKILL.md +159 -0
- package/.agents/skills/flawless-api-fixtures/SKILL.md +144 -0
- package/.agents/skills/flawless-e2e/SKILL.md +155 -0
- package/.agents/skills/flawless-expo/SKILL.md +283 -0
- package/.agents/skills/flawless-findings/SKILL.md +116 -0
- package/.agents/skills/flawless-load/SKILL.md +112 -0
- package/.agents/skills/flawless-nextjs/SKILL.md +198 -0
- package/.agents/skills/flawless-perf-harness/SKILL.md +158 -0
- package/.agents/skills/flawless-perf-static/SKILL.md +131 -0
- package/.agents/skills/flawless-react/SKILL.md +403 -0
- package/.agents/skills/flawless-react-rules/SKILL.md +96 -0
- package/.agents/skills/flawless-regen/SKILL.md +135 -0
- package/.agents/skills/flawless-unit-browser/SKILL.md +178 -0
- package/LICENSE +21 -0
- package/README.md +608 -2
- package/bin/flawless-react +4 -0
- package/package.json +67 -4
- package/scripts/postinstall.d.mts +46 -0
- package/scripts/postinstall.mjs +127 -0
- package/src/analyse/authShims.ts +220 -0
- package/src/analyse/bootability.ts +257 -0
- package/src/analyse/childBridge.ts +302 -0
- package/src/analyse/childrenIntoSlot.ts +66 -0
- package/src/analyse/compoundFamily.ts +100 -0
- package/src/analyse/dataExports.ts +200 -0
- package/src/analyse/extractChildAttrs.ts +112 -0
- package/src/analyse/extractDataDeps.ts +857 -0
- package/src/analyse/extractDisclosures.ts +186 -0
- package/src/analyse/extractFetchCalls.ts +81 -0
- package/src/analyse/extractFlows.ts +239 -0
- package/src/analyse/extractGuards.ts +270 -0
- package/src/analyse/extractHooks.ts +237 -0
- package/src/analyse/extractInteractions.ts +1421 -0
- package/src/analyse/extractLogging.ts +45 -0
- package/src/analyse/extractNativeA11y.ts +138 -0
- package/src/analyse/extractPredicates.ts +784 -0
- package/src/analyse/extractPropUsage.ts +162 -0
- package/src/analyse/extractProps.ts +1212 -0
- package/src/analyse/extractRenderMode.ts +165 -0
- package/src/analyse/extractRootTag.ts +117 -0
- package/src/analyse/extractState.ts +361 -0
- package/src/analyse/extractStaticText.ts +212 -0
- package/src/analyse/extractVariants.ts +186 -0
- package/src/analyse/harvestApiEndpoints.ts +284 -0
- package/src/analyse/hostProps.ts +185 -0
- package/src/analyse/hostTag.ts +72 -0
- package/src/analyse/htmlContentModel.ts +94 -0
- package/src/analyse/index.ts +744 -0
- package/src/analyse/mountPathCalls.ts +442 -0
- package/src/analyse/nativeContentModel.ts +129 -0
- package/src/analyse/nativeDialect.ts +83 -0
- package/src/analyse/nativeProps.ts +339 -0
- package/src/analyse/ownReturns.ts +31 -0
- package/src/analyse/parseFile.ts +919 -0
- package/src/analyse/pos.ts +8 -0
- package/src/analyse/providerWraps.ts +1977 -0
- package/src/analyse/pureModule.ts +88 -0
- package/src/analyse/reexportTable.ts +148 -0
- package/src/analyse/refs.ts +195 -0
- package/src/analyse/renderPath.ts +441 -0
- package/src/analyse/resolveDepReturns.ts +370 -0
- package/src/analyse/resolveIndexKeys.ts +98 -0
- package/src/analyse/resolveModule.ts +431 -0
- package/src/analyse/resolveTagRootTags.ts +103 -0
- package/src/analyse/serverActionReturns.ts +213 -0
- package/src/analyse/sourceCache.ts +263 -0
- package/src/analyse/spreadSiblings.ts +98 -0
- package/src/analyse/walkChildren.ts +98 -0
- package/src/analyse/zodSchema.ts +424 -0
- package/src/bin.ts +23 -0
- package/src/changed.ts +221 -0
- package/src/cli.ts +560 -0
- package/src/cliFlags.ts +281 -0
- package/src/core.ts +1229 -0
- package/src/docLine.ts +122 -0
- package/src/emitPaths.ts +51 -0
- package/src/expo/detect.ts +184 -0
- package/src/expo/refusal.ts +62 -0
- package/src/expo/routes.ts +145 -0
- package/src/index.ts +95 -0
- package/src/mcp/analysisTools.ts +720 -0
- package/src/mcp/infoTools.ts +135 -0
- package/src/mcp/protocol.ts +130 -0
- package/src/mcp/run.ts +269 -0
- package/src/mcp/runTools.ts +362 -0
- package/src/mcp/server.ts +279 -0
- package/src/mcp/spec.ts +86 -0
- package/src/mcp/support.ts +268 -0
- package/src/mcp/tools.ts +75 -0
- package/src/nextjs/apiRoutes.ts +116 -0
- package/src/nextjs/detect.ts +397 -0
- package/src/nextjs/emit.ts +106 -0
- package/src/nextjs/liveSpec.ts +79 -0
- package/src/nextjs/mswHandlers.ts +55 -0
- package/src/nextjs/providerWrapper.ts +603 -0
- package/src/nextjs/routes.ts +86 -0
- package/src/nextjs/swagger.ts +32 -0
- package/src/packageJson.ts +414 -0
- package/src/platform.ts +18 -0
- package/src/prompt.ts +132 -0
- package/src/reactCompiler.ts +165 -0
- package/src/reporters/findings.ts +156 -0
- package/src/reporters/report.ts +463 -0
- package/src/runner/runTests.ts +402 -0
- package/src/scaffold/adoptStyle.ts +181 -0
- package/src/scaffold/index.ts +253 -0
- package/src/scaffold/openapi.ts +183 -0
- package/src/scaffold/templates.ts +2379 -0
- package/src/scaffold-tests/index.ts +463 -0
- package/src/scaffold-tests/mockValues.ts +208 -0
- package/src/scaffold-tests/mocks.ts +794 -0
- package/src/scaffold-tests/nativeIndex.ts +52 -0
- package/src/scaffold-tests/nativeTemplates.ts +1528 -0
- package/src/scaffold-tests/postDispatch.ts +152 -0
- package/src/scaffold-tests/registry.ts +443 -0
- package/src/scaffold-tests/selectors.ts +493 -0
- package/src/scaffold-tests/selectorsNative.ts +273 -0
- package/src/scaffold-tests/stamp.ts +79 -0
- package/src/scaffold-tests/templates.ts +3578 -0
- package/src/skills/cli.ts +248 -0
- package/src/skills/compat.ts +166 -0
- package/src/skills/data.ts +192 -0
- package/src/skills/md.d.ts +7 -0
- package/src/skills/menu.ts +55 -0
- package/src/skills/spec.ts +227 -0
- package/src/suiteScripts.ts +110 -0
- package/src/tasks/a11yNative.ts +281 -0
- package/src/tasks/a11yScan.ts +67 -0
- package/src/tasks/biomeRunner.ts +301 -0
- package/src/tasks/biomeSweep.ts +166 -0
- package/src/tasks/e2eExpo.ts +436 -0
- package/src/tasks/e2eRoutes.ts +756 -0
- package/src/tasks/eslintReactRules.ts +67 -0
- package/src/tasks/index.ts +24 -0
- package/src/tasks/loadExpo.ts +273 -0
- package/src/tasks/loadPipes.ts +750 -0
- package/src/tasks/perfHarness.ts +145 -0
- package/src/tasks/staticPerfAnalysis.ts +510 -0
- package/src/tasks/types.ts +1 -0
- package/src/tasks/unitAndBrowser.ts +462 -0
- package/src/types.ts +1419 -0
- package/src/version.ts +12 -0
|
@@ -0,0 +1,1212 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import {
|
|
3
|
+
DATE_PROP_SENTINEL,
|
|
4
|
+
type PropsShape,
|
|
5
|
+
type PropValue,
|
|
6
|
+
type SourcePosition,
|
|
7
|
+
} from "../types.ts";
|
|
8
|
+
import { extractVariantDefs, type VariantDefinition } from "./extractVariants.ts";
|
|
9
|
+
import { hostPropsFor, isHostTagRecognised } from "./hostProps.ts";
|
|
10
|
+
import type { ImportBinding } from "./parseFile.ts";
|
|
11
|
+
import { posOf } from "./pos.ts";
|
|
12
|
+
import type { ModuleResolver } from "./resolveModule.ts";
|
|
13
|
+
import { findExportedDecl, type SourceFileCache } from "./sourceCache.ts";
|
|
14
|
+
import {
|
|
15
|
+
asZodInferRef,
|
|
16
|
+
resolveZodSchemaConst,
|
|
17
|
+
type ZodSchemaResult,
|
|
18
|
+
zodResultToValue,
|
|
19
|
+
} from "./zodSchema.ts";
|
|
20
|
+
|
|
21
|
+
export interface ExtractedProps {
|
|
22
|
+
propsShape: PropsShape;
|
|
23
|
+
variants: PropsShape[];
|
|
24
|
+
/** Props seen but not synthesisable — real prop names only. */
|
|
25
|
+
unresolvedProps: string[];
|
|
26
|
+
/** Type expressions we gave up on: "Omit", "ButtonPrimitive.Props". */
|
|
27
|
+
unresolvedTypes: string[];
|
|
28
|
+
/** Declaration site of every member seen (resolved or not). */
|
|
29
|
+
propPositions: Record<string, SourcePosition>;
|
|
30
|
+
/** Props declared as Promise<...> — synthesised unwrapped; wrap at render time. */
|
|
31
|
+
promiseProps: string[];
|
|
32
|
+
/** prop → full literal option list. */
|
|
33
|
+
variantOptions: Record<string, PropValue[]>;
|
|
34
|
+
/** prop → option → its cva class string, for distinctness checks. */
|
|
35
|
+
variantClasses: Record<string, Record<string, string>>;
|
|
36
|
+
/** Host element the props type anchors to, when unambiguous. */
|
|
37
|
+
hostElement?: string;
|
|
38
|
+
acceptsChildren: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Where a member came from. Higher precedence wins a name conflict when
|
|
43
|
+
* intersection operands are merged: the type literal written at the
|
|
44
|
+
* declaration site is the author's most specific statement, and the curated
|
|
45
|
+
* DOM table is the least.
|
|
46
|
+
*/
|
|
47
|
+
type MemberOrigin = "literal" | "variant" | "interface" | "component" | "host";
|
|
48
|
+
|
|
49
|
+
const ORIGIN_RANK: Record<MemberOrigin, number> = {
|
|
50
|
+
literal: 5,
|
|
51
|
+
variant: 4,
|
|
52
|
+
interface: 3,
|
|
53
|
+
component: 2,
|
|
54
|
+
host: 1,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
interface PartialProps {
|
|
58
|
+
shape: PropsShape;
|
|
59
|
+
variantOptions: Record<string, PropValue[]>;
|
|
60
|
+
variantClasses: Record<string, Record<string, string>>;
|
|
61
|
+
origin: Record<string, MemberOrigin>;
|
|
62
|
+
propPositions: Record<string, SourcePosition>;
|
|
63
|
+
promiseProps: string[];
|
|
64
|
+
unresolvedProps: string[];
|
|
65
|
+
unresolvedTypes: string[];
|
|
66
|
+
hostElement?: string;
|
|
67
|
+
acceptsChildren: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface PropsResolveCtx {
|
|
71
|
+
sf: ts.SourceFile;
|
|
72
|
+
/** Absolute path of `sf`. */
|
|
73
|
+
file: string;
|
|
74
|
+
imports: Map<string, ImportBinding>;
|
|
75
|
+
resolver: ModuleResolver | undefined;
|
|
76
|
+
cache: SourceFileCache | undefined;
|
|
77
|
+
/** Remaining cross-file / cross-component hops. */
|
|
78
|
+
hops: number;
|
|
79
|
+
/** `${file}::${name}` cycle guard. */
|
|
80
|
+
visited: Set<string>;
|
|
81
|
+
/** cva/tv definitions declared in `sf`, built lazily. */
|
|
82
|
+
variantDefs?: Map<string, VariantDefinition>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Intersections beyond this are machine-generated; resolving them is guessing. */
|
|
86
|
+
const MAX_INTERSECTION_OPERANDS = 8;
|
|
87
|
+
const MAX_HOPS = 2;
|
|
88
|
+
|
|
89
|
+
function emptyPartial(): PartialProps {
|
|
90
|
+
return {
|
|
91
|
+
shape: {},
|
|
92
|
+
variantOptions: {},
|
|
93
|
+
variantClasses: {},
|
|
94
|
+
origin: {},
|
|
95
|
+
propPositions: {},
|
|
96
|
+
promiseProps: [],
|
|
97
|
+
unresolvedProps: [],
|
|
98
|
+
unresolvedTypes: [],
|
|
99
|
+
acceptsChildren: false,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const EMPTY: ExtractedProps = {
|
|
104
|
+
propsShape: {},
|
|
105
|
+
variants: [],
|
|
106
|
+
unresolvedProps: [],
|
|
107
|
+
unresolvedTypes: [],
|
|
108
|
+
propPositions: {},
|
|
109
|
+
promiseProps: [],
|
|
110
|
+
variantOptions: {},
|
|
111
|
+
variantClasses: {},
|
|
112
|
+
acceptsChildren: false,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export function extractProps(
|
|
116
|
+
sourceFile: ts.SourceFile,
|
|
117
|
+
propsTypeNode: ts.TypeNode | undefined,
|
|
118
|
+
propsTypeLiteral?: ts.TypeLiteralNode,
|
|
119
|
+
imports?: Map<string, ImportBinding>,
|
|
120
|
+
entryFile?: string,
|
|
121
|
+
resolver?: ModuleResolver,
|
|
122
|
+
cache?: SourceFileCache,
|
|
123
|
+
): ExtractedProps {
|
|
124
|
+
const node = propsTypeNode ?? propsTypeLiteral;
|
|
125
|
+
if (!node) return EMPTY;
|
|
126
|
+
|
|
127
|
+
const ctx: PropsResolveCtx = {
|
|
128
|
+
sf: sourceFile,
|
|
129
|
+
file: entryFile ?? sourceFile.fileName,
|
|
130
|
+
imports: imports ?? new Map(),
|
|
131
|
+
resolver,
|
|
132
|
+
cache,
|
|
133
|
+
hops: MAX_HOPS,
|
|
134
|
+
visited: new Set(),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const partial = resolveTypeNode(node, ctx);
|
|
138
|
+
return finalise(partial);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* B-1 Path A: a declared type → ONE concrete fixture value, or undefined.
|
|
143
|
+
*
|
|
144
|
+
* Deliberately NOT a pass over resolveTypeNode/mapType: the props machinery
|
|
145
|
+
* synthesises DEFAULTS and silently drops what it cannot resolve, and both
|
|
146
|
+
* habits are poison for a data fixture. A post-hoc restyle cannot tell a
|
|
147
|
+
* keyword `boolean`'s false from a declared literal `ok: false` (restyling
|
|
148
|
+
* the latter emits a value the type FORBIDS), and a silently dropped member
|
|
149
|
+
* ships a fixture with a hole the component crashes on — reported as a real
|
|
150
|
+
* app failure, because an enriched dep loses the RSC skip escape. So this
|
|
151
|
+
* resolver keeps provenance (keyword leaves restyle to the truthy house
|
|
152
|
+
* values, literal types are preserved exactly) and bails on ANY member it
|
|
153
|
+
* cannot fully resolve, at every depth.
|
|
154
|
+
*
|
|
155
|
+
* Structure: arrays (`T[]`, `Array<T>`) → ONE-element arrays at every depth
|
|
156
|
+
* (an empty list lets a spec pass without rendering the row markup); unions
|
|
157
|
+
* of literals → the first literal; type references resolve through local and
|
|
158
|
+
* one-hop imported interface/type-alias declarations only — no React special
|
|
159
|
+
* heads, so a data type shadowing `ButtonHTMLAttributes` means the LOCAL
|
|
160
|
+
* declaration, never the DOM table. Generics, intersections, heritage
|
|
161
|
+
* clauses and cycles all bail.
|
|
162
|
+
*/
|
|
163
|
+
export function resolveTypeNodeValue(
|
|
164
|
+
node: ts.TypeNode,
|
|
165
|
+
sf: ts.SourceFile,
|
|
166
|
+
file: string,
|
|
167
|
+
resolver: ModuleResolver | undefined,
|
|
168
|
+
cache: SourceFileCache | undefined,
|
|
169
|
+
): PropValue | undefined {
|
|
170
|
+
return fixtureValue(node, "value", {
|
|
171
|
+
sf,
|
|
172
|
+
file,
|
|
173
|
+
imports: collectImportsSync(sf),
|
|
174
|
+
resolver,
|
|
175
|
+
cache,
|
|
176
|
+
hops: MAX_HOPS,
|
|
177
|
+
visited: new Set(),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function fixtureValue(
|
|
182
|
+
node: ts.TypeNode,
|
|
183
|
+
seed: string,
|
|
184
|
+
ctx: PropsResolveCtx,
|
|
185
|
+
): PropValue | undefined {
|
|
186
|
+
if (ts.isParenthesizedTypeNode(node)) return fixtureValue(node.type, seed, ctx);
|
|
187
|
+
|
|
188
|
+
const element = fixtureArrayElement(node);
|
|
189
|
+
if (element) {
|
|
190
|
+
const el = fixtureValue(element, seed, ctx);
|
|
191
|
+
return el === undefined ? undefined : [el];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
switch (node.kind) {
|
|
195
|
+
case ts.SyntaxKind.StringKeyword:
|
|
196
|
+
return `flawless-${seed}`;
|
|
197
|
+
case ts.SyntaxKind.NumberKeyword:
|
|
198
|
+
return 1;
|
|
199
|
+
case ts.SyntaxKind.BooleanKeyword:
|
|
200
|
+
return true;
|
|
201
|
+
case ts.SyntaxKind.NullKeyword:
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (ts.isLiteralTypeNode(node)) return literalTypeValue(node);
|
|
206
|
+
|
|
207
|
+
if (ts.isUnionTypeNode(node)) {
|
|
208
|
+
// null/undefined arms are DISCOUNTED, not chosen between: the DB-backed
|
|
209
|
+
// types real apps annotate with are `X | null` throughout (dogfood round
|
|
210
|
+
// 5's single biggest inference blocker), and the non-null arm's value is
|
|
211
|
+
// one of the union's own members — type-legal, unlike a guess between
|
|
212
|
+
// two real alternatives, which still bails below.
|
|
213
|
+
const arms = node.types.filter(
|
|
214
|
+
(t) =>
|
|
215
|
+
t.kind !== ts.SyntaxKind.UndefinedKeyword &&
|
|
216
|
+
!(ts.isLiteralTypeNode(t) && t.literal.kind === ts.SyntaxKind.NullKeyword),
|
|
217
|
+
);
|
|
218
|
+
const first = arms[0];
|
|
219
|
+
if (!first) return undefined;
|
|
220
|
+
if (arms.every((t) => ts.isLiteralTypeNode(t))) {
|
|
221
|
+
return ts.isLiteralTypeNode(first) ? literalTypeValue(first) : undefined;
|
|
222
|
+
}
|
|
223
|
+
if (arms.length === 1) return fixtureValue(first, seed, ctx);
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (ts.isTypeLiteralNode(node)) return fixtureMembers(node.members, ctx);
|
|
228
|
+
|
|
229
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
230
|
+
// B-9: z.infer<typeof Schema> / z.input — read the schema's own AST.
|
|
231
|
+
// Once the reference is provably zod's, a resolution failure is a real
|
|
232
|
+
// bail, never a fall-through to the generic guards below.
|
|
233
|
+
const zi = asZodInferRef(node, ctx.imports);
|
|
234
|
+
if (zi) {
|
|
235
|
+
const r = resolveZodSchemaConst(zi.schemaName, seed, ctx);
|
|
236
|
+
return r === undefined ? undefined : zodResultToValue(r);
|
|
237
|
+
}
|
|
238
|
+
// Two generic instantiations whose EMPTY OBJECT is a complete inhabitant,
|
|
239
|
+
// not a partial one — no substitution needed to know that. `read_by:
|
|
240
|
+
// Record<string, number>` was enough to bail MessageSummary entirely, and
|
|
241
|
+
// with it every mock of the three api functions that return one.
|
|
242
|
+
//
|
|
243
|
+
// Deliberately just these two. `Pick`/`Omit`/`Readonly<T>` all need the
|
|
244
|
+
// argument resolved before anything can be said about their members, and a
|
|
245
|
+
// guess there is the partial fixture this function refuses to produce.
|
|
246
|
+
if (ts.isIdentifier(node.typeName) && (node.typeArguments?.length ?? 0) > 0) {
|
|
247
|
+
const g = node.typeName.text;
|
|
248
|
+
if (g === "Record" || g === "Partial") return {};
|
|
249
|
+
}
|
|
250
|
+
// Any other generic instantiation would need type-parameter substitution.
|
|
251
|
+
if ((node.typeArguments?.length ?? 0) > 0) return undefined;
|
|
252
|
+
if (!ts.isIdentifier(node.typeName)) return undefined;
|
|
253
|
+
return fixtureReference(node.typeName.text, ctx);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function literalTypeValue(node: ts.LiteralTypeNode): PropValue | undefined {
|
|
260
|
+
const lit = node.literal;
|
|
261
|
+
if (ts.isStringLiteral(lit)) return lit.text;
|
|
262
|
+
if (ts.isNumericLiteral(lit)) return Number(lit.text);
|
|
263
|
+
if (lit.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
264
|
+
if (lit.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
265
|
+
if (lit.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function fixtureArrayElement(node: ts.TypeNode): ts.TypeNode | undefined {
|
|
270
|
+
if (ts.isArrayTypeNode(node)) return node.elementType;
|
|
271
|
+
if (
|
|
272
|
+
ts.isTypeReferenceNode(node) &&
|
|
273
|
+
ts.isIdentifier(node.typeName) &&
|
|
274
|
+
(node.typeName.text === "Array" || node.typeName.text === "ReadonlyArray")
|
|
275
|
+
) {
|
|
276
|
+
return node.typeArguments?.[0];
|
|
277
|
+
}
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Every member resolves or the whole object bails — no silent holes. */
|
|
282
|
+
function fixtureMembers(
|
|
283
|
+
members: ts.NodeArray<ts.TypeElement>,
|
|
284
|
+
ctx: PropsResolveCtx,
|
|
285
|
+
): PropValue | undefined {
|
|
286
|
+
const out: { [k: string]: PropValue } = {};
|
|
287
|
+
let any = false;
|
|
288
|
+
for (const m of members) {
|
|
289
|
+
if (!ts.isPropertySignature(m)) return undefined;
|
|
290
|
+
if (!ts.isIdentifier(m.name) && !ts.isStringLiteral(m.name)) return undefined;
|
|
291
|
+
if (!m.type) return undefined;
|
|
292
|
+
// Optional members are included: a value for `name?: string` is
|
|
293
|
+
// type-legal, and richer beats sparser as long as it is never wrong.
|
|
294
|
+
const v = fixtureValue(m.type, m.name.text, ctx);
|
|
295
|
+
if (v === undefined) return undefined;
|
|
296
|
+
out[m.name.text] = v;
|
|
297
|
+
any = true;
|
|
298
|
+
}
|
|
299
|
+
return any ? out : undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** A named type, resolved through local then one-hop imported declarations. */
|
|
303
|
+
function fixtureReference(name: string, ctx: PropsResolveCtx): PropValue | undefined {
|
|
304
|
+
const key = `${ctx.file}::${name}`;
|
|
305
|
+
if (ctx.visited.has(key)) return undefined;
|
|
306
|
+
const next: PropsResolveCtx = { ...ctx, visited: new Set([...ctx.visited, key]) };
|
|
307
|
+
|
|
308
|
+
const local = findTypeDecl(ctx.sf, name);
|
|
309
|
+
if (local) return fixtureDecl(local, next);
|
|
310
|
+
|
|
311
|
+
// `createdAt: Date` — a live Date answers getTime/toISOString/
|
|
312
|
+
// toLocaleTimeString; nothing else does. The sentinel rides through to
|
|
313
|
+
// defaultsJson, which restores the real `new Date(...)`. Checked AFTER the
|
|
314
|
+
// local lookup on purpose: a file declaring its own Date shadows the lib.
|
|
315
|
+
if (name === "Date") return DATE_PROP_SENTINEL;
|
|
316
|
+
|
|
317
|
+
const binding = ctx.imports.get(name);
|
|
318
|
+
if (!binding || ctx.hops <= 0) return undefined;
|
|
319
|
+
const target = ctx.resolver?.resolve(binding.specifier, ctx.file);
|
|
320
|
+
if (!target || !ctx.cache) return undefined;
|
|
321
|
+
const wanted = binding.importedName ?? name;
|
|
322
|
+
// The resolved module may be a barrel that only re-exports the name; the
|
|
323
|
+
// chase to the DECLARING file rides its own visited set, not the hops.
|
|
324
|
+
const located = findExportedDecl(
|
|
325
|
+
target,
|
|
326
|
+
wanted,
|
|
327
|
+
ctx.cache,
|
|
328
|
+
ctx.resolver,
|
|
329
|
+
findExportedTypeDecl,
|
|
330
|
+
true,
|
|
331
|
+
);
|
|
332
|
+
if (!located) return undefined;
|
|
333
|
+
return fixtureDecl(located.decl, {
|
|
334
|
+
...next,
|
|
335
|
+
sf: located.sf,
|
|
336
|
+
file: located.file,
|
|
337
|
+
imports: collectImportsSync(located.sf),
|
|
338
|
+
hops: ctx.hops - 1,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function fixtureDecl(
|
|
343
|
+
decl: ts.InterfaceDeclaration | ts.TypeAliasDeclaration,
|
|
344
|
+
ctx: PropsResolveCtx,
|
|
345
|
+
): PropValue | undefined {
|
|
346
|
+
if (ts.isTypeAliasDeclaration(decl)) {
|
|
347
|
+
if (decl.typeParameters?.length) return undefined;
|
|
348
|
+
return fixtureValue(decl.type, decl.name.text, ctx);
|
|
349
|
+
}
|
|
350
|
+
if (decl.typeParameters?.length) return undefined;
|
|
351
|
+
const own = fixtureMembers(decl.members, ctx);
|
|
352
|
+
if (own === undefined) return undefined;
|
|
353
|
+
if (!decl.heritageClauses?.length) return own;
|
|
354
|
+
// `interface PollDetail extends PollSummary { … }` — resolve the base and
|
|
355
|
+
// merge, own members last because they are the more specific.
|
|
356
|
+
//
|
|
357
|
+
// This used to bail, on the reasoning that a partial fixture is worse than
|
|
358
|
+
// none. The reasoning is right and the bail was too wide: a base that DOES
|
|
359
|
+
// resolve is not partial, it is complete. Seven of the dogfood app's api
|
|
360
|
+
// functions mocked to `{}` for this, and one of them poisoned a whole screen —
|
|
361
|
+
// `const [{ poll: p }] = await Promise.all([getPoll(…), …])` bound `p` to
|
|
362
|
+
// undefined, `p.my_vote_option_id` threw into the catch, and the screen
|
|
363
|
+
// rendered its error state while the test looked for a row.
|
|
364
|
+
//
|
|
365
|
+
// If ANY base fails to resolve the whole thing still bails, which is the
|
|
366
|
+
// original invariant kept where it belongs.
|
|
367
|
+
const merged: { [k: string]: PropValue } = {};
|
|
368
|
+
for (const heritage of decl.heritageClauses) {
|
|
369
|
+
if (heritage.token !== ts.SyntaxKind.ExtendsKeyword) return undefined;
|
|
370
|
+
for (const t of heritage.types) {
|
|
371
|
+
// A heritage entry is an ExpressionWithTypeArguments, NOT a TypeReference
|
|
372
|
+
// — `ts.isTypeReferenceNode` is false for it however it is cast, so
|
|
373
|
+
// routing it through fixtureValue silently produced undefined and bailed
|
|
374
|
+
// every interface with an `extends`. Resolve the name directly.
|
|
375
|
+
if (!ts.isIdentifier(t.expression) || (t.typeArguments?.length ?? 0) > 0) return undefined;
|
|
376
|
+
const base = fixtureReference(t.expression.text, ctx);
|
|
377
|
+
if (base === null || typeof base !== "object" || Array.isArray(base)) return undefined;
|
|
378
|
+
Object.assign(merged, base);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (own === null || typeof own !== "object" || Array.isArray(own)) return undefined;
|
|
382
|
+
return { ...merged, ...own };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function finalise(p: PartialProps): ExtractedProps {
|
|
386
|
+
// `children` is a boolean capability, never a prop value: propsShape is
|
|
387
|
+
// JSON-stringified into the spec's DEFAULTS and spread, so a `children`
|
|
388
|
+
// entry would emit <X children="…" /> instead of real JSX.
|
|
389
|
+
const acceptsChildren = p.acceptsChildren || "children" in p.propPositions;
|
|
390
|
+
delete p.shape.children;
|
|
391
|
+
delete p.variantOptions.children;
|
|
392
|
+
const unresolvedProps = p.unresolvedProps.filter((n) => n !== "children" && !(n in p.shape));
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
propsShape: p.shape,
|
|
396
|
+
variants: cartesian(p.variantOptions, p.shape),
|
|
397
|
+
unresolvedProps: [...new Set(unresolvedProps)],
|
|
398
|
+
unresolvedTypes: [...new Set(p.unresolvedTypes)],
|
|
399
|
+
propPositions: p.propPositions,
|
|
400
|
+
promiseProps: [...new Set(p.promiseProps)],
|
|
401
|
+
variantOptions: p.variantOptions,
|
|
402
|
+
variantClasses: p.variantClasses,
|
|
403
|
+
...(p.hostElement ? { hostElement: p.hostElement } : {}),
|
|
404
|
+
acceptsChildren,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
// Dispatcher
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
function resolveTypeNode(node: ts.TypeNode, ctx: PropsResolveCtx): PartialProps {
|
|
413
|
+
if (ts.isParenthesizedTypeNode(node)) return resolveTypeNode(node.type, ctx);
|
|
414
|
+
|
|
415
|
+
if (ts.isIntersectionTypeNode(node)) {
|
|
416
|
+
const operands = flattenIntersection(node);
|
|
417
|
+
if (operands.length > MAX_INTERSECTION_OPERANDS) {
|
|
418
|
+
const p = emptyPartial();
|
|
419
|
+
p.unresolvedTypes.push(`<intersection:${MAX_INTERSECTION_OPERANDS + 1}+>`);
|
|
420
|
+
return p;
|
|
421
|
+
}
|
|
422
|
+
return mergePartials(operands.map((o) => resolveTypeNode(o, ctx)));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (ts.isTypeLiteralNode(node)) {
|
|
426
|
+
return walkMembers(node.members, ctx, "literal");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
430
|
+
return resolveTypeReference(node, ctx);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const p = emptyPartial();
|
|
434
|
+
p.unresolvedTypes.push(printHead(node, ctx.sf));
|
|
435
|
+
return p;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function flattenIntersection(node: ts.IntersectionTypeNode): ts.TypeNode[] {
|
|
439
|
+
const out: ts.TypeNode[] = [];
|
|
440
|
+
const walk = (n: ts.TypeNode): void => {
|
|
441
|
+
if (ts.isParenthesizedTypeNode(n)) {
|
|
442
|
+
walk(n.type);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (ts.isIntersectionTypeNode(n)) {
|
|
446
|
+
for (const t of n.types) walk(t);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
out.push(n);
|
|
450
|
+
};
|
|
451
|
+
walk(node);
|
|
452
|
+
return out;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** A short, stable label for a type we could not resolve. */
|
|
456
|
+
function printHead(node: ts.TypeNode, sf: ts.SourceFile): string {
|
|
457
|
+
if (ts.isTypeReferenceNode(node)) return entityNameText(node.typeName);
|
|
458
|
+
let text: string;
|
|
459
|
+
try {
|
|
460
|
+
text = node.getText(sf);
|
|
461
|
+
} catch {
|
|
462
|
+
return "<type>";
|
|
463
|
+
}
|
|
464
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 60);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function entityNameText(name: ts.EntityName): string {
|
|
468
|
+
if (ts.isIdentifier(name)) return name.text;
|
|
469
|
+
return `${entityNameText(name.left)}.${name.right.text}`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function lastSegment(name: ts.EntityName): string {
|
|
473
|
+
return ts.isIdentifier(name) ? name.text : name.right.text;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
// Merge
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
|
|
480
|
+
function mergePartials(parts: PartialProps[]): PartialProps {
|
|
481
|
+
const acc = emptyPartial();
|
|
482
|
+
let hostSeen: string | undefined;
|
|
483
|
+
let hostConflict = false;
|
|
484
|
+
|
|
485
|
+
for (const part of parts) {
|
|
486
|
+
for (const [name, value] of Object.entries(part.shape)) {
|
|
487
|
+
const incoming = part.origin[name] ?? "literal";
|
|
488
|
+
const current = acc.origin[name];
|
|
489
|
+
// Later operand wins ties; higher-precedence origin always wins.
|
|
490
|
+
const wins = current === undefined || ORIGIN_RANK[incoming] >= ORIGIN_RANK[current];
|
|
491
|
+
if (!wins) continue;
|
|
492
|
+
acc.shape[name] = value;
|
|
493
|
+
acc.origin[name] = incoming;
|
|
494
|
+
const pos = part.propPositions[name];
|
|
495
|
+
if (pos) acc.propPositions[name] = pos;
|
|
496
|
+
// Option lists are replaced wholesale — unioning two literal sets would
|
|
497
|
+
// invent a union the author never wrote.
|
|
498
|
+
const opts = part.variantOptions[name];
|
|
499
|
+
if (opts) acc.variantOptions[name] = opts;
|
|
500
|
+
else delete acc.variantOptions[name];
|
|
501
|
+
const cls = part.variantClasses[name];
|
|
502
|
+
if (cls) acc.variantClasses[name] = cls;
|
|
503
|
+
else delete acc.variantClasses[name];
|
|
504
|
+
}
|
|
505
|
+
for (const [name, pos] of Object.entries(part.propPositions)) {
|
|
506
|
+
acc.propPositions[name] ??= pos;
|
|
507
|
+
}
|
|
508
|
+
acc.promiseProps.push(...part.promiseProps);
|
|
509
|
+
acc.unresolvedProps.push(...part.unresolvedProps);
|
|
510
|
+
acc.unresolvedTypes.push(...part.unresolvedTypes);
|
|
511
|
+
acc.acceptsChildren ||= part.acceptsChildren;
|
|
512
|
+
|
|
513
|
+
if (part.hostElement) {
|
|
514
|
+
if (hostSeen && hostSeen !== part.hostElement) hostConflict = true;
|
|
515
|
+
hostSeen ??= part.hostElement;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (hostConflict) {
|
|
520
|
+
// Keep the merged members; drop only the tag claim.
|
|
521
|
+
acc.unresolvedTypes.push("<host-conflict>");
|
|
522
|
+
} else if (hostSeen) {
|
|
523
|
+
acc.hostElement = hostSeen;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// A name resolved by one operand is not unresolved just because another failed.
|
|
527
|
+
acc.unresolvedProps = acc.unresolvedProps.filter((n) => !(n in acc.shape));
|
|
528
|
+
return acc;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ---------------------------------------------------------------------------
|
|
532
|
+
// Type references
|
|
533
|
+
// ---------------------------------------------------------------------------
|
|
534
|
+
|
|
535
|
+
const COMPONENT_PROPS_HEADS = new Set([
|
|
536
|
+
"ComponentProps",
|
|
537
|
+
"ComponentPropsWithoutRef",
|
|
538
|
+
"ComponentPropsWithRef",
|
|
539
|
+
]);
|
|
540
|
+
|
|
541
|
+
/** `ButtonHTMLAttributes<…>` names its own tag; the type argument is noise. */
|
|
542
|
+
const HTML_ATTR_TO_TAG: Record<string, string> = {
|
|
543
|
+
HTMLAttributes: "div",
|
|
544
|
+
ButtonHTMLAttributes: "button",
|
|
545
|
+
InputHTMLAttributes: "input",
|
|
546
|
+
AnchorHTMLAttributes: "a",
|
|
547
|
+
FormHTMLAttributes: "form",
|
|
548
|
+
TextareaHTMLAttributes: "textarea",
|
|
549
|
+
SelectHTMLAttributes: "select",
|
|
550
|
+
LabelHTMLAttributes: "label",
|
|
551
|
+
ImgHTMLAttributes: "img",
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
function resolveTypeReference(node: ts.TypeReferenceNode, ctx: PropsResolveCtx): PartialProps {
|
|
555
|
+
const head = lastSegment(node.typeName);
|
|
556
|
+
|
|
557
|
+
if (head === "PropsWithChildren") {
|
|
558
|
+
const inner = node.typeArguments?.[0];
|
|
559
|
+
const p = inner ? resolveTypeNode(inner, ctx) : emptyPartial();
|
|
560
|
+
p.acceptsChildren = true;
|
|
561
|
+
return p;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (COMPONENT_PROPS_HEADS.has(head) && isReactProvenance(node.typeName, ctx)) {
|
|
565
|
+
return resolveComponentProps(node, ctx);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const attrTag = HTML_ATTR_TO_TAG[head];
|
|
569
|
+
if (attrTag && isReactProvenance(node.typeName, ctx)) {
|
|
570
|
+
return hostPartial(attrTag, node, ctx);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (head === "VariantProps") {
|
|
574
|
+
return resolveVariantProps(node, ctx);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// B-9: props typed AS a z.infer alias (inline here, or an alias RHS via
|
|
578
|
+
// resolveTypeDecl). A z.object schema IS a props shape; its enum fields
|
|
579
|
+
// are variant axes with the first literal as the default. A non-object
|
|
580
|
+
// result stays a bail — props must be object-shaped, and inventing a
|
|
581
|
+
// wrapper would be a guess — keeping the "z.infer" unresolvedTypes label.
|
|
582
|
+
const zi = asZodInferRef(node, ctx.imports);
|
|
583
|
+
if (zi) {
|
|
584
|
+
const r = resolveZodSchemaConst(zi.schemaName, "props", ctx);
|
|
585
|
+
if (r?.kind === "object") {
|
|
586
|
+
const p = emptyPartial();
|
|
587
|
+
for (const f of r.fields) {
|
|
588
|
+
p.shape[f.name] = f.value;
|
|
589
|
+
p.origin[f.name] = "interface";
|
|
590
|
+
p.propPositions[f.name] = f.pos;
|
|
591
|
+
if (f.options) p.variantOptions[f.name] = f.options;
|
|
592
|
+
}
|
|
593
|
+
return p;
|
|
594
|
+
}
|
|
595
|
+
const p = emptyPartial();
|
|
596
|
+
p.unresolvedTypes.push(entityNameText(node.typeName));
|
|
597
|
+
return p;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// A local interface / type alias.
|
|
601
|
+
if (ts.isIdentifier(node.typeName)) {
|
|
602
|
+
const local = findTypeDecl(ctx.sf, node.typeName.text);
|
|
603
|
+
if (local) return resolveTypeDecl(local, ctx);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// An imported props type — one hop through the resolver.
|
|
607
|
+
const imported = resolveImportedTypeDecl(node.typeName, ctx);
|
|
608
|
+
if (imported) return imported;
|
|
609
|
+
|
|
610
|
+
const p = emptyPartial();
|
|
611
|
+
p.unresolvedTypes.push(entityNameText(node.typeName));
|
|
612
|
+
return p;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* `ComponentProps` must actually be React's. A project-local type of the same
|
|
617
|
+
* name would make the host table a lie, so anything we cannot attribute to
|
|
618
|
+
* react is refused.
|
|
619
|
+
*/
|
|
620
|
+
function isReactProvenance(name: ts.EntityName, ctx: PropsResolveCtx): boolean {
|
|
621
|
+
if (ts.isQualifiedName(name)) {
|
|
622
|
+
const root = ts.isIdentifier(name.left) ? name.left.text : undefined;
|
|
623
|
+
if (!root) return false;
|
|
624
|
+
if (root === "React") return true;
|
|
625
|
+
return ctx.imports.get(root)?.specifier === "react";
|
|
626
|
+
}
|
|
627
|
+
const binding = ctx.imports.get(name.text);
|
|
628
|
+
// Not imported at all → the global JSX namespace. Imported → must be react.
|
|
629
|
+
return binding === undefined || binding.specifier === "react";
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function resolveComponentProps(node: ts.TypeReferenceNode, ctx: PropsResolveCtx): PartialProps {
|
|
633
|
+
const arg = node.typeArguments?.[0];
|
|
634
|
+
if (!arg) {
|
|
635
|
+
const p = emptyPartial();
|
|
636
|
+
p.unresolvedTypes.push(entityNameText(node.typeName));
|
|
637
|
+
return p;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ComponentProps<"button">
|
|
641
|
+
if (ts.isLiteralTypeNode(arg) && ts.isStringLiteral(arg.literal)) {
|
|
642
|
+
const tag = arg.literal.text;
|
|
643
|
+
if (/^[a-z][a-z0-9]*$/.test(tag)) return hostPartial(tag, node, ctx);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ComponentProps<typeof LocalComponent>
|
|
647
|
+
if (ts.isTypeQueryNode(arg)) {
|
|
648
|
+
return resolveComponentPropsOfEntity(arg.exprName, node, ctx);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const p = emptyPartial();
|
|
652
|
+
p.unresolvedTypes.push(printHead(node, ctx.sf));
|
|
653
|
+
return p;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function hostPartial(tag: string, node: ts.TypeNode, ctx: PropsResolveCtx): PartialProps {
|
|
657
|
+
const host = hostPropsFor(tag);
|
|
658
|
+
const p = emptyPartial();
|
|
659
|
+
const pos = posOf(node, ctx.sf);
|
|
660
|
+
for (const [k, v] of Object.entries(host.shape)) {
|
|
661
|
+
p.shape[k] = v;
|
|
662
|
+
p.origin[k] = "host";
|
|
663
|
+
p.propPositions[k] = pos;
|
|
664
|
+
}
|
|
665
|
+
for (const [k, opts] of Object.entries(host.variantOptions ?? {})) {
|
|
666
|
+
p.variantOptions[k] = opts;
|
|
667
|
+
}
|
|
668
|
+
p.hostElement = tag;
|
|
669
|
+
p.acceptsChildren = host.acceptsChildren;
|
|
670
|
+
if (!isHostTagRecognised(tag)) {
|
|
671
|
+
// Still safe: className/id exist on every element and the void-element
|
|
672
|
+
// list is a closed HTML fact. Recorded so the limit stays visible.
|
|
673
|
+
p.unresolvedTypes.push(`<host:${tag}>`);
|
|
674
|
+
}
|
|
675
|
+
return p;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// ---------------------------------------------------------------------------
|
|
679
|
+
// ComponentProps<typeof X>
|
|
680
|
+
// ---------------------------------------------------------------------------
|
|
681
|
+
|
|
682
|
+
function resolveComponentPropsOfEntity(
|
|
683
|
+
exprName: ts.EntityName,
|
|
684
|
+
node: ts.TypeReferenceNode,
|
|
685
|
+
ctx: PropsResolveCtx,
|
|
686
|
+
): PartialProps {
|
|
687
|
+
const bail = (): PartialProps => {
|
|
688
|
+
const p = emptyPartial();
|
|
689
|
+
p.unresolvedTypes.push(printHead(node, ctx.sf));
|
|
690
|
+
return p;
|
|
691
|
+
};
|
|
692
|
+
if (ctx.hops <= 0) return bail();
|
|
693
|
+
|
|
694
|
+
const localName = ts.isIdentifier(exprName) ? exprName.text : lastSegment(exprName);
|
|
695
|
+
const rootName = ts.isIdentifier(exprName)
|
|
696
|
+
? exprName.text
|
|
697
|
+
: ts.isIdentifier(exprName.left)
|
|
698
|
+
? exprName.left.text
|
|
699
|
+
: undefined;
|
|
700
|
+
if (!rootName) return bail();
|
|
701
|
+
|
|
702
|
+
// Same file first.
|
|
703
|
+
if (ts.isIdentifier(exprName)) {
|
|
704
|
+
const key = `${ctx.file}::${localName}`;
|
|
705
|
+
if (ctx.visited.has(key)) return bail();
|
|
706
|
+
const typeNode = findComponentPropsTypeNode(ctx.sf, localName);
|
|
707
|
+
if (typeNode) {
|
|
708
|
+
const next: PropsResolveCtx = {
|
|
709
|
+
...ctx,
|
|
710
|
+
hops: ctx.hops - 1,
|
|
711
|
+
visited: new Set([...ctx.visited, key]),
|
|
712
|
+
};
|
|
713
|
+
return withOrigin(resolveTypeNode(typeNode, next), "component");
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Imported component.
|
|
718
|
+
const binding = ctx.imports.get(rootName);
|
|
719
|
+
if (!binding || binding.isTypeOnly) return bail();
|
|
720
|
+
const target = ctx.resolver?.resolve(binding.specifier, ctx.file);
|
|
721
|
+
if (!target) return bail();
|
|
722
|
+
const targetSf = ctx.cache?.get(target);
|
|
723
|
+
if (!targetSf) return bail();
|
|
724
|
+
|
|
725
|
+
const wanted = ts.isIdentifier(exprName) ? (binding.importedName ?? localName) : localName;
|
|
726
|
+
const key = `${target}::${wanted}`;
|
|
727
|
+
if (ctx.visited.has(key)) return bail();
|
|
728
|
+
|
|
729
|
+
const typeNode =
|
|
730
|
+
findComponentPropsTypeNode(targetSf, wanted) ??
|
|
731
|
+
(binding.kind === "default" ? findPrimaryComponentPropsTypeNode(targetSf) : undefined);
|
|
732
|
+
if (!typeNode) return bail();
|
|
733
|
+
|
|
734
|
+
const next: PropsResolveCtx = {
|
|
735
|
+
sf: targetSf,
|
|
736
|
+
file: target,
|
|
737
|
+
imports: collectImportsSync(targetSf),
|
|
738
|
+
resolver: ctx.resolver,
|
|
739
|
+
cache: ctx.cache,
|
|
740
|
+
hops: ctx.hops - 1,
|
|
741
|
+
visited: new Set([...ctx.visited, key]),
|
|
742
|
+
};
|
|
743
|
+
return withOrigin(resolveTypeNode(typeNode, next), "component");
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function withOrigin(p: PartialProps, origin: MemberOrigin): PartialProps {
|
|
747
|
+
for (const name of Object.keys(p.shape)) p.origin[name] = origin;
|
|
748
|
+
return p;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** First-param (or FC arg) type node of a component named `name` in `sf`. */
|
|
752
|
+
function findComponentPropsTypeNode(sf: ts.SourceFile, name: string): ts.TypeNode | undefined {
|
|
753
|
+
for (const stmt of sf.statements) {
|
|
754
|
+
if (ts.isFunctionDeclaration(stmt) && stmt.name?.text === name) {
|
|
755
|
+
return stmt.parameters[0]?.type;
|
|
756
|
+
}
|
|
757
|
+
if (ts.isVariableStatement(stmt)) {
|
|
758
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
759
|
+
if (!ts.isIdentifier(decl.name) || decl.name.text !== name) continue;
|
|
760
|
+
const typeRef = decl.type && ts.isTypeReferenceNode(decl.type) ? decl.type : undefined;
|
|
761
|
+
const fcArg = typeRef?.typeArguments?.[0];
|
|
762
|
+
if (fcArg) return fcArg;
|
|
763
|
+
const init = decl.initializer;
|
|
764
|
+
if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) {
|
|
765
|
+
return init.parameters[0]?.type;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return undefined;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** Props of the file's default-exported component, for `import X from "…"`. */
|
|
774
|
+
function findPrimaryComponentPropsTypeNode(sf: ts.SourceFile): ts.TypeNode | undefined {
|
|
775
|
+
for (const stmt of sf.statements) {
|
|
776
|
+
if (!ts.isFunctionDeclaration(stmt) || !stmt.name) continue;
|
|
777
|
+
const mods = stmt.modifiers ?? [];
|
|
778
|
+
const isDefault =
|
|
779
|
+
mods.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) &&
|
|
780
|
+
mods.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword);
|
|
781
|
+
if (isDefault) return stmt.parameters[0]?.type;
|
|
782
|
+
}
|
|
783
|
+
return undefined;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** Synchronous import map for a cached secondary file. */
|
|
787
|
+
function collectImportsSync(sf: ts.SourceFile): Map<string, ImportBinding> {
|
|
788
|
+
const out = new Map<string, ImportBinding>();
|
|
789
|
+
for (const stmt of sf.statements) {
|
|
790
|
+
if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
791
|
+
const specifier = stmt.moduleSpecifier.text;
|
|
792
|
+
const clause = stmt.importClause;
|
|
793
|
+
if (!clause) continue;
|
|
794
|
+
const pos = posOf(stmt, sf);
|
|
795
|
+
if (clause.name) {
|
|
796
|
+
out.set(clause.name.text, {
|
|
797
|
+
specifier,
|
|
798
|
+
kind: "default",
|
|
799
|
+
isTypeOnly: clause.isTypeOnly,
|
|
800
|
+
pos,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
804
|
+
for (const el of clause.namedBindings.elements) {
|
|
805
|
+
out.set(el.name.text, {
|
|
806
|
+
specifier,
|
|
807
|
+
kind: "named",
|
|
808
|
+
importedName: el.propertyName?.text ?? el.name.text,
|
|
809
|
+
isTypeOnly: clause.isTypeOnly || el.isTypeOnly,
|
|
810
|
+
pos,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) {
|
|
815
|
+
out.set(clause.namedBindings.name.text, {
|
|
816
|
+
specifier,
|
|
817
|
+
kind: "namespace",
|
|
818
|
+
isTypeOnly: clause.isTypeOnly,
|
|
819
|
+
pos,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return out;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// ---------------------------------------------------------------------------
|
|
827
|
+
// VariantProps<typeof xVariants>
|
|
828
|
+
// ---------------------------------------------------------------------------
|
|
829
|
+
|
|
830
|
+
const VARIANT_SPECIFIERS = new Set(["class-variance-authority", "tailwind-variants"]);
|
|
831
|
+
|
|
832
|
+
function resolveVariantProps(node: ts.TypeReferenceNode, ctx: PropsResolveCtx): PartialProps {
|
|
833
|
+
const bail = (): PartialProps => {
|
|
834
|
+
const p = emptyPartial();
|
|
835
|
+
p.unresolvedTypes.push("VariantProps");
|
|
836
|
+
return p;
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
// Provenance: a project-local `VariantProps` would make this a name guess.
|
|
840
|
+
if (ts.isQualifiedName(node.typeName)) {
|
|
841
|
+
const root = ts.isIdentifier(node.typeName.left) ? node.typeName.left.text : undefined;
|
|
842
|
+
const spec = root ? ctx.imports.get(root)?.specifier : undefined;
|
|
843
|
+
if (!spec || !VARIANT_SPECIFIERS.has(spec)) return bail();
|
|
844
|
+
} else {
|
|
845
|
+
const binding = ctx.imports.get(node.typeName.text);
|
|
846
|
+
if (!binding || !VARIANT_SPECIFIERS.has(binding.specifier)) return bail();
|
|
847
|
+
if (binding.importedName && binding.importedName !== "VariantProps") return bail();
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const arg = node.typeArguments?.[0];
|
|
851
|
+
if (!arg || !ts.isTypeQueryNode(arg) || !ts.isIdentifier(arg.exprName)) return bail();
|
|
852
|
+
const constName = arg.exprName.text;
|
|
853
|
+
|
|
854
|
+
ctx.variantDefs ??= extractVariantDefs(ctx.sf, ctx.imports);
|
|
855
|
+
let def = ctx.variantDefs.get(constName);
|
|
856
|
+
|
|
857
|
+
if (!def && ctx.hops > 0) {
|
|
858
|
+
// `import { buttonVariants } from "@/components/ui/button"` is real.
|
|
859
|
+
const binding = ctx.imports.get(constName);
|
|
860
|
+
const target = binding ? ctx.resolver?.resolve(binding.specifier, ctx.file) : undefined;
|
|
861
|
+
const targetSf = target ? ctx.cache?.get(target) : undefined;
|
|
862
|
+
if (targetSf) {
|
|
863
|
+
const defs = extractVariantDefs(targetSf, collectImportsSync(targetSf));
|
|
864
|
+
def = defs.get(binding?.importedName ?? constName);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (!def) return bail();
|
|
868
|
+
|
|
869
|
+
const p = emptyPartial();
|
|
870
|
+
for (const [key, options] of Object.entries(def.options)) {
|
|
871
|
+
const first = options[0];
|
|
872
|
+
if (first === undefined) continue;
|
|
873
|
+
p.shape[key] = def.defaults[key] ?? first;
|
|
874
|
+
p.variantOptions[key] = options;
|
|
875
|
+
const cls = def.classes[key];
|
|
876
|
+
if (cls) p.variantClasses[key] = cls;
|
|
877
|
+
p.origin[key] = "variant";
|
|
878
|
+
const pos = def.keyPositions[key];
|
|
879
|
+
if (pos) p.propPositions[key] = pos;
|
|
880
|
+
}
|
|
881
|
+
return p;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// ---------------------------------------------------------------------------
|
|
885
|
+
// Declarations
|
|
886
|
+
// ---------------------------------------------------------------------------
|
|
887
|
+
|
|
888
|
+
function findTypeDecl(
|
|
889
|
+
sf: ts.SourceFile,
|
|
890
|
+
name: string,
|
|
891
|
+
): ts.InterfaceDeclaration | ts.TypeAliasDeclaration | undefined {
|
|
892
|
+
for (const stmt of sf.statements) {
|
|
893
|
+
if (ts.isInterfaceDeclaration(stmt) && stmt.name.text === name) return stmt;
|
|
894
|
+
if (ts.isTypeAliasDeclaration(stmt) && stmt.name.text === name) return stmt;
|
|
895
|
+
}
|
|
896
|
+
return undefined;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* findTypeDecl for the barrel chase: a file reached THROUGH a re-export
|
|
901
|
+
* chain vouches only for its exported declarations — a private same-named
|
|
902
|
+
* interface in an earlier star arm must not shadow the real one (that emits
|
|
903
|
+
* a wrong-shape fixture on valid TS). The directly-resolved file keeps
|
|
904
|
+
* findTypeDecl's pre-B-8 looseness.
|
|
905
|
+
*/
|
|
906
|
+
function findExportedTypeDecl(
|
|
907
|
+
sf: ts.SourceFile,
|
|
908
|
+
name: string,
|
|
909
|
+
viaReexport: boolean,
|
|
910
|
+
): ts.InterfaceDeclaration | ts.TypeAliasDeclaration | undefined {
|
|
911
|
+
const decl = findTypeDecl(sf, name);
|
|
912
|
+
if (!decl) return undefined;
|
|
913
|
+
if (
|
|
914
|
+
viaReexport &&
|
|
915
|
+
!(ts.getModifiers(decl)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false)
|
|
916
|
+
) {
|
|
917
|
+
return undefined;
|
|
918
|
+
}
|
|
919
|
+
return decl;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function resolveTypeDecl(
|
|
923
|
+
decl: ts.InterfaceDeclaration | ts.TypeAliasDeclaration,
|
|
924
|
+
ctx: PropsResolveCtx,
|
|
925
|
+
): PartialProps {
|
|
926
|
+
if (ts.isTypeAliasDeclaration(decl)) {
|
|
927
|
+
// `type Props = A & B` now works, where it used to bail.
|
|
928
|
+
return resolveTypeNode(decl.type, ctx);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// `interface Props extends ComponentProps<"button"> { … }` is an implicit
|
|
932
|
+
// intersection: bases first, own members last (they are more specific).
|
|
933
|
+
const parts: PartialProps[] = [];
|
|
934
|
+
for (const heritage of decl.heritageClauses ?? []) {
|
|
935
|
+
if (heritage.token !== ts.SyntaxKind.ExtendsKeyword) continue;
|
|
936
|
+
for (const t of heritage.types) {
|
|
937
|
+
const synthetic = asTypeNode(t);
|
|
938
|
+
if (synthetic) parts.push(resolveTypeNode(synthetic, ctx));
|
|
939
|
+
else {
|
|
940
|
+
const p = emptyPartial();
|
|
941
|
+
p.unresolvedTypes.push(printHead(t.expression as unknown as ts.TypeNode, ctx.sf));
|
|
942
|
+
parts.push(p);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
parts.push(walkMembers(decl.members, ctx, "interface"));
|
|
947
|
+
return mergePartials(parts);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/** An `extends` entry is a call-ish node; reuse it as a type reference. */
|
|
951
|
+
/**
|
|
952
|
+
* A member's type as a zod schema result: the reference is z.infer directly,
|
|
953
|
+
* or a named alias (local or imported, barrel-chased) whose RHS is z.infer
|
|
954
|
+
* under the DECLARING file's imports. undefined for everything else — the
|
|
955
|
+
* member falls through to mapType and behaves exactly as before B-9.
|
|
956
|
+
*/
|
|
957
|
+
function zodMemberSchema(
|
|
958
|
+
t: ts.TypeReferenceNode,
|
|
959
|
+
seed: string,
|
|
960
|
+
ctx: PropsResolveCtx,
|
|
961
|
+
): ZodSchemaResult | undefined {
|
|
962
|
+
const direct = asZodInferRef(t, ctx.imports);
|
|
963
|
+
if (direct) return resolveZodSchemaConst(direct.schemaName, seed, ctx);
|
|
964
|
+
|
|
965
|
+
if (!ts.isIdentifier(t.typeName) || (t.typeArguments?.length ?? 0) > 0) return undefined;
|
|
966
|
+
const name = t.typeName.text;
|
|
967
|
+
|
|
968
|
+
const aliasRef = (
|
|
969
|
+
decl: ts.InterfaceDeclaration | ts.TypeAliasDeclaration,
|
|
970
|
+
): ts.TypeReferenceNode | undefined =>
|
|
971
|
+
ts.isTypeAliasDeclaration(decl) && ts.isTypeReferenceNode(decl.type) ? decl.type : undefined;
|
|
972
|
+
|
|
973
|
+
const local = findTypeDecl(ctx.sf, name);
|
|
974
|
+
if (local) {
|
|
975
|
+
const rhs = aliasRef(local);
|
|
976
|
+
if (!rhs) return undefined;
|
|
977
|
+
const zi = asZodInferRef(rhs, ctx.imports);
|
|
978
|
+
return zi ? resolveZodSchemaConst(zi.schemaName, seed, ctx) : undefined;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const binding = ctx.imports.get(name);
|
|
982
|
+
if (!binding || ctx.hops <= 0 || !ctx.cache) return undefined;
|
|
983
|
+
const target = ctx.resolver?.resolve(binding.specifier, ctx.file);
|
|
984
|
+
if (!target) return undefined;
|
|
985
|
+
const located = findExportedDecl(
|
|
986
|
+
target,
|
|
987
|
+
binding.importedName ?? name,
|
|
988
|
+
ctx.cache,
|
|
989
|
+
ctx.resolver,
|
|
990
|
+
findExportedTypeDecl,
|
|
991
|
+
true,
|
|
992
|
+
);
|
|
993
|
+
if (!located) return undefined;
|
|
994
|
+
const rhs = aliasRef(located.decl);
|
|
995
|
+
if (!rhs) return undefined;
|
|
996
|
+
const declImports = collectImportsSync(located.sf);
|
|
997
|
+
const zi = asZodInferRef(rhs, declImports);
|
|
998
|
+
if (!zi) return undefined;
|
|
999
|
+
return resolveZodSchemaConst(zi.schemaName, seed, {
|
|
1000
|
+
...ctx,
|
|
1001
|
+
sf: located.sf,
|
|
1002
|
+
file: located.file,
|
|
1003
|
+
imports: declImports,
|
|
1004
|
+
hops: ctx.hops - 1,
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function asTypeNode(t: ts.ExpressionWithTypeArguments): ts.TypeNode | undefined {
|
|
1009
|
+
const expr = t.expression;
|
|
1010
|
+
if (!ts.isIdentifier(expr) && !ts.isPropertyAccessExpression(expr)) return undefined;
|
|
1011
|
+
return t as unknown as ts.TypeNode;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function walkMembers(
|
|
1015
|
+
members: ts.NodeArray<ts.TypeElement>,
|
|
1016
|
+
ctx: PropsResolveCtx,
|
|
1017
|
+
origin: MemberOrigin,
|
|
1018
|
+
): PartialProps {
|
|
1019
|
+
const p = emptyPartial();
|
|
1020
|
+
|
|
1021
|
+
for (const m of members) {
|
|
1022
|
+
if (!ts.isPropertySignature(m)) continue;
|
|
1023
|
+
// String-literal names ('"data-variant": string') are real members and map
|
|
1024
|
+
// like identifiers; the emitted DEFAULTS is JSON.stringify'd, so the quoted
|
|
1025
|
+
// key survives emission. Computed/numeric/private names stay out.
|
|
1026
|
+
if (!ts.isIdentifier(m.name) && !ts.isStringLiteral(m.name)) continue;
|
|
1027
|
+
const name = m.name.text;
|
|
1028
|
+
p.propPositions[name] = posOf(m, ctx.sf);
|
|
1029
|
+
const t = m.type;
|
|
1030
|
+
if (!t) {
|
|
1031
|
+
p.unresolvedProps.push(name);
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Promise<T> (Next.js 15 params/searchParams): synthesise T, mark for wrapping.
|
|
1036
|
+
if (ts.isTypeReferenceNode(t) && ts.isIdentifier(t.typeName) && t.typeName.text === "Promise") {
|
|
1037
|
+
const inner = t.typeArguments?.[0];
|
|
1038
|
+
const r = inner ? mapType(inner) : { kind: "unresolved" as const };
|
|
1039
|
+
if (r.kind === "scalar" || r.kind === "variants") {
|
|
1040
|
+
p.shape[name] = r.value;
|
|
1041
|
+
p.origin[name] = origin;
|
|
1042
|
+
p.promiseProps.push(name);
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
// Promise<object-shaped> — `params: Promise<{ id: string }>` or a named
|
|
1046
|
+
// type (`Promise<Page>`). mapType cannot synthesise the inner shape, but
|
|
1047
|
+
// the OUTER fact is certain: this prop is a Promise. Marking it keeps
|
|
1048
|
+
// `params.then` / `use(page)` off a bare object; synthesiseDefaults
|
|
1049
|
+
// floors the missing value at {}.
|
|
1050
|
+
p.unresolvedProps.push(name);
|
|
1051
|
+
p.promiseProps.push(name);
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// B-9: a member typed as a z.infer alias (directly, or via a named
|
|
1056
|
+
// alias whose RHS is one) resolves from the schema — enum fields become
|
|
1057
|
+
// variant axes. Undefined = not zod OR zod-bailed; both fall through to
|
|
1058
|
+
// mapType, which lands unresolved for named types exactly as before.
|
|
1059
|
+
if (ts.isTypeReferenceNode(t)) {
|
|
1060
|
+
const zr = zodMemberSchema(t, name, ctx);
|
|
1061
|
+
if (zr) {
|
|
1062
|
+
p.shape[name] = zodResultToValue(zr);
|
|
1063
|
+
p.origin[name] = origin;
|
|
1064
|
+
if (zr.kind === "enum") p.variantOptions[name] = zr.values;
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const r = mapType(t);
|
|
1070
|
+
if (r.kind === "unresolved") {
|
|
1071
|
+
// mapType is syntactic: it cannot follow `kind: NotificationKind`
|
|
1072
|
+
// into a declared literal union, nor `drop: DropItem` into another
|
|
1073
|
+
// file — and a dropped member is exactly the hole the mount dies
|
|
1074
|
+
// on (NotificationList indexes ICONS by n.kind). The strict fixture
|
|
1075
|
+
// resolver can: it walks declarations within the hop budget, keeps
|
|
1076
|
+
// literal-union arms verbatim, maps Date to the sentinel — and it
|
|
1077
|
+
// returns a COMPLETE fixture or bails, so no hole is invented
|
|
1078
|
+
// either way.
|
|
1079
|
+
const strict = fixtureValue(t, name, ctx);
|
|
1080
|
+
if (strict !== undefined) {
|
|
1081
|
+
p.shape[name] = strict;
|
|
1082
|
+
p.origin[name] = origin;
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
p.unresolvedProps.push(name);
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
p.shape[name] = r.value;
|
|
1089
|
+
p.origin[name] = origin;
|
|
1090
|
+
if (r.kind === "variants") p.variantOptions[name] = r.values;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
return p;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/** Props type imported from another module — one hop, no recursion past it. */
|
|
1097
|
+
function resolveImportedTypeDecl(
|
|
1098
|
+
name: ts.EntityName,
|
|
1099
|
+
ctx: PropsResolveCtx,
|
|
1100
|
+
): PartialProps | undefined {
|
|
1101
|
+
if (!ts.isIdentifier(name)) return undefined;
|
|
1102
|
+
if (ctx.hops <= 0) return undefined;
|
|
1103
|
+
const binding = ctx.imports.get(name.text);
|
|
1104
|
+
if (!binding) return undefined;
|
|
1105
|
+
const target = ctx.resolver?.resolve(binding.specifier, ctx.file);
|
|
1106
|
+
if (!target || !ctx.cache) return undefined;
|
|
1107
|
+
// Barrel-aware: the resolved module may only re-export the name; chase to
|
|
1108
|
+
// the declaring file on its own visited set (never the hops budget).
|
|
1109
|
+
const located = findExportedDecl(
|
|
1110
|
+
target,
|
|
1111
|
+
binding.importedName ?? name.text,
|
|
1112
|
+
ctx.cache,
|
|
1113
|
+
ctx.resolver,
|
|
1114
|
+
findExportedTypeDecl,
|
|
1115
|
+
true,
|
|
1116
|
+
);
|
|
1117
|
+
if (!located) return undefined;
|
|
1118
|
+
|
|
1119
|
+
const key = `${located.file}::${located.decl.name.text}`;
|
|
1120
|
+
if (ctx.visited.has(key)) return undefined;
|
|
1121
|
+
|
|
1122
|
+
return resolveTypeDecl(located.decl, {
|
|
1123
|
+
sf: located.sf,
|
|
1124
|
+
file: located.file,
|
|
1125
|
+
imports: collectImportsSync(located.sf),
|
|
1126
|
+
resolver: ctx.resolver,
|
|
1127
|
+
cache: ctx.cache,
|
|
1128
|
+
hops: ctx.hops - 1,
|
|
1129
|
+
visited: new Set([...ctx.visited, key]),
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// ---------------------------------------------------------------------------
|
|
1134
|
+
// Type → default value
|
|
1135
|
+
// ---------------------------------------------------------------------------
|
|
1136
|
+
|
|
1137
|
+
type MapResult =
|
|
1138
|
+
| { kind: "scalar"; value: PropValue }
|
|
1139
|
+
| { kind: "variants"; value: PropValue; values: PropValue[] }
|
|
1140
|
+
| { kind: "unresolved" };
|
|
1141
|
+
|
|
1142
|
+
function mapType(t: ts.TypeNode): MapResult {
|
|
1143
|
+
switch (t.kind) {
|
|
1144
|
+
case ts.SyntaxKind.StringKeyword:
|
|
1145
|
+
return { kind: "scalar", value: "" };
|
|
1146
|
+
case ts.SyntaxKind.NumberKeyword:
|
|
1147
|
+
// 1, not 0 — the house number value everywhere else (fixtureValue,
|
|
1148
|
+
// mockLiteral). A 0 in DEFAULTS silently degenerates the arithmetic a
|
|
1149
|
+
// click test drives: `setN(n + step)` with step=0 re-renders NOTHING and
|
|
1150
|
+
// the spec times out waiting for a render that can never happen.
|
|
1151
|
+
return { kind: "scalar", value: 1 };
|
|
1152
|
+
case ts.SyntaxKind.BooleanKeyword:
|
|
1153
|
+
return { kind: "scalar", value: false };
|
|
1154
|
+
case ts.SyntaxKind.NullKeyword:
|
|
1155
|
+
return { kind: "scalar", value: null };
|
|
1156
|
+
}
|
|
1157
|
+
if (ts.isArrayTypeNode(t)) return { kind: "scalar", value: [] };
|
|
1158
|
+
// `date: Date` — a live Date answers getTime/toISOString/
|
|
1159
|
+
// toLocaleTimeString; a string or {} answers none of them. PropValue is
|
|
1160
|
+
// JSON-safe, so carry the sentinel and let `defaultsJson` put the real
|
|
1161
|
+
// `new Date(...)` back wherever a PropsShape becomes spec source.
|
|
1162
|
+
if (ts.isTypeReferenceNode(t) && ts.isIdentifier(t.typeName) && t.typeName.text === "Date")
|
|
1163
|
+
return { kind: "scalar", value: DATE_PROP_SENTINEL };
|
|
1164
|
+
if (ts.isTypeLiteralNode(t)) {
|
|
1165
|
+
const inner: PropsShape = {};
|
|
1166
|
+
for (const m of t.members) {
|
|
1167
|
+
if (!ts.isPropertySignature(m) || !ts.isIdentifier(m.name) || !m.type) continue;
|
|
1168
|
+
const r = mapType(m.type);
|
|
1169
|
+
if (r.kind === "scalar" || r.kind === "variants") inner[m.name.text] = r.value;
|
|
1170
|
+
}
|
|
1171
|
+
return { kind: "scalar", value: inner };
|
|
1172
|
+
}
|
|
1173
|
+
if (ts.isLiteralTypeNode(t)) {
|
|
1174
|
+
const lit = t.literal;
|
|
1175
|
+
if (ts.isStringLiteral(lit)) return { kind: "scalar", value: lit.text };
|
|
1176
|
+
if (ts.isNumericLiteral(lit)) return { kind: "scalar", value: Number(lit.text) };
|
|
1177
|
+
if (lit.kind === ts.SyntaxKind.TrueKeyword) return { kind: "scalar", value: true };
|
|
1178
|
+
if (lit.kind === ts.SyntaxKind.FalseKeyword) return { kind: "scalar", value: false };
|
|
1179
|
+
if (lit.kind === ts.SyntaxKind.NullKeyword) return { kind: "scalar", value: null };
|
|
1180
|
+
return { kind: "unresolved" };
|
|
1181
|
+
}
|
|
1182
|
+
if (ts.isUnionTypeNode(t)) {
|
|
1183
|
+
const literals: PropValue[] = [];
|
|
1184
|
+
for (const u of t.types) {
|
|
1185
|
+
const r = mapType(u);
|
|
1186
|
+
if (r.kind === "scalar") literals.push(r.value);
|
|
1187
|
+
else return { kind: "unresolved" };
|
|
1188
|
+
}
|
|
1189
|
+
if (literals.length === 0) return { kind: "unresolved" };
|
|
1190
|
+
// `string | Date` — a Date instance also passes for the string half
|
|
1191
|
+
// (toString/toISOString), a string never survives the Date half.
|
|
1192
|
+
const date = literals.find((v) => v === DATE_PROP_SENTINEL);
|
|
1193
|
+
const first = date ?? (literals[0] as PropValue);
|
|
1194
|
+
return { kind: "variants", value: first, values: literals };
|
|
1195
|
+
}
|
|
1196
|
+
return { kind: "unresolved" };
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function cartesian(options: Record<string, PropValue[]>, base: PropsShape): PropsShape[] {
|
|
1200
|
+
const keys = Object.keys(options);
|
|
1201
|
+
if (keys.length === 0) return [];
|
|
1202
|
+
// Pairwise: hold all-but-one at defaults, vary the chosen key across its options.
|
|
1203
|
+
const out: PropsShape[] = [];
|
|
1204
|
+
for (const k of keys) {
|
|
1205
|
+
const values = options[k];
|
|
1206
|
+
if (!values) continue;
|
|
1207
|
+
for (const v of values) {
|
|
1208
|
+
out.push({ ...base, [k]: v });
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return out;
|
|
1212
|
+
}
|