@lariocpt/flawless-react 0.0.0-stage → 0.3.1

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.
Files changed (143) hide show
  1. package/.agents/skills/flawless-a11y/SKILL.md +113 -0
  2. package/.agents/skills/flawless-api/SKILL.md +159 -0
  3. package/.agents/skills/flawless-api-fixtures/SKILL.md +144 -0
  4. package/.agents/skills/flawless-e2e/SKILL.md +155 -0
  5. package/.agents/skills/flawless-expo/SKILL.md +283 -0
  6. package/.agents/skills/flawless-findings/SKILL.md +116 -0
  7. package/.agents/skills/flawless-load/SKILL.md +112 -0
  8. package/.agents/skills/flawless-nextjs/SKILL.md +198 -0
  9. package/.agents/skills/flawless-perf-harness/SKILL.md +158 -0
  10. package/.agents/skills/flawless-perf-static/SKILL.md +131 -0
  11. package/.agents/skills/flawless-react/SKILL.md +403 -0
  12. package/.agents/skills/flawless-react-rules/SKILL.md +96 -0
  13. package/.agents/skills/flawless-regen/SKILL.md +135 -0
  14. package/.agents/skills/flawless-unit-browser/SKILL.md +178 -0
  15. package/LICENSE +21 -0
  16. package/README.md +611 -2
  17. package/bin/flawless-react +4 -0
  18. package/package.json +67 -4
  19. package/scripts/postinstall.d.mts +46 -0
  20. package/scripts/postinstall.mjs +127 -0
  21. package/src/analyse/authShims.ts +220 -0
  22. package/src/analyse/bootability.ts +257 -0
  23. package/src/analyse/childBridge.ts +302 -0
  24. package/src/analyse/childrenIntoSlot.ts +66 -0
  25. package/src/analyse/compoundFamily.ts +100 -0
  26. package/src/analyse/dataExports.ts +200 -0
  27. package/src/analyse/extractChildAttrs.ts +112 -0
  28. package/src/analyse/extractDataDeps.ts +857 -0
  29. package/src/analyse/extractDisclosures.ts +186 -0
  30. package/src/analyse/extractFetchCalls.ts +81 -0
  31. package/src/analyse/extractFlows.ts +239 -0
  32. package/src/analyse/extractGuards.ts +270 -0
  33. package/src/analyse/extractHooks.ts +237 -0
  34. package/src/analyse/extractInteractions.ts +1421 -0
  35. package/src/analyse/extractLogging.ts +45 -0
  36. package/src/analyse/extractNativeA11y.ts +138 -0
  37. package/src/analyse/extractPredicates.ts +784 -0
  38. package/src/analyse/extractPropUsage.ts +162 -0
  39. package/src/analyse/extractProps.ts +1212 -0
  40. package/src/analyse/extractRenderMode.ts +165 -0
  41. package/src/analyse/extractRootTag.ts +117 -0
  42. package/src/analyse/extractState.ts +361 -0
  43. package/src/analyse/extractStaticText.ts +212 -0
  44. package/src/analyse/extractVariants.ts +186 -0
  45. package/src/analyse/harvestApiEndpoints.ts +284 -0
  46. package/src/analyse/hostProps.ts +185 -0
  47. package/src/analyse/hostTag.ts +72 -0
  48. package/src/analyse/htmlContentModel.ts +94 -0
  49. package/src/analyse/index.ts +744 -0
  50. package/src/analyse/mountPathCalls.ts +442 -0
  51. package/src/analyse/nativeContentModel.ts +129 -0
  52. package/src/analyse/nativeDialect.ts +83 -0
  53. package/src/analyse/nativeProps.ts +339 -0
  54. package/src/analyse/ownReturns.ts +31 -0
  55. package/src/analyse/parseFile.ts +919 -0
  56. package/src/analyse/pos.ts +8 -0
  57. package/src/analyse/providerWraps.ts +1977 -0
  58. package/src/analyse/pureModule.ts +88 -0
  59. package/src/analyse/reexportTable.ts +148 -0
  60. package/src/analyse/refs.ts +195 -0
  61. package/src/analyse/renderPath.ts +441 -0
  62. package/src/analyse/resolveDepReturns.ts +370 -0
  63. package/src/analyse/resolveIndexKeys.ts +98 -0
  64. package/src/analyse/resolveModule.ts +431 -0
  65. package/src/analyse/resolveTagRootTags.ts +103 -0
  66. package/src/analyse/serverActionReturns.ts +213 -0
  67. package/src/analyse/sourceCache.ts +263 -0
  68. package/src/analyse/spreadSiblings.ts +98 -0
  69. package/src/analyse/walkChildren.ts +98 -0
  70. package/src/analyse/zodSchema.ts +424 -0
  71. package/src/bin.ts +23 -0
  72. package/src/changed.ts +221 -0
  73. package/src/cli.ts +560 -0
  74. package/src/cliFlags.ts +281 -0
  75. package/src/core.ts +1229 -0
  76. package/src/docLine.ts +122 -0
  77. package/src/emitPaths.ts +51 -0
  78. package/src/expo/detect.ts +184 -0
  79. package/src/expo/refusal.ts +62 -0
  80. package/src/expo/routes.ts +145 -0
  81. package/src/index.ts +95 -0
  82. package/src/mcp/analysisTools.ts +720 -0
  83. package/src/mcp/infoTools.ts +135 -0
  84. package/src/mcp/protocol.ts +130 -0
  85. package/src/mcp/run.ts +269 -0
  86. package/src/mcp/runTools.ts +362 -0
  87. package/src/mcp/server.ts +279 -0
  88. package/src/mcp/spec.ts +86 -0
  89. package/src/mcp/support.ts +268 -0
  90. package/src/mcp/tools.ts +75 -0
  91. package/src/nextjs/apiRoutes.ts +116 -0
  92. package/src/nextjs/detect.ts +397 -0
  93. package/src/nextjs/emit.ts +106 -0
  94. package/src/nextjs/liveSpec.ts +79 -0
  95. package/src/nextjs/mswHandlers.ts +55 -0
  96. package/src/nextjs/providerWrapper.ts +603 -0
  97. package/src/nextjs/routes.ts +86 -0
  98. package/src/nextjs/swagger.ts +32 -0
  99. package/src/packageJson.ts +414 -0
  100. package/src/platform.ts +18 -0
  101. package/src/prompt.ts +132 -0
  102. package/src/reactCompiler.ts +165 -0
  103. package/src/reporters/findings.ts +156 -0
  104. package/src/reporters/report.ts +463 -0
  105. package/src/runner/runTests.ts +402 -0
  106. package/src/scaffold/adoptStyle.ts +181 -0
  107. package/src/scaffold/index.ts +253 -0
  108. package/src/scaffold/openapi.ts +183 -0
  109. package/src/scaffold/templates.ts +2379 -0
  110. package/src/scaffold-tests/index.ts +463 -0
  111. package/src/scaffold-tests/mockValues.ts +208 -0
  112. package/src/scaffold-tests/mocks.ts +794 -0
  113. package/src/scaffold-tests/nativeIndex.ts +52 -0
  114. package/src/scaffold-tests/nativeTemplates.ts +1528 -0
  115. package/src/scaffold-tests/postDispatch.ts +152 -0
  116. package/src/scaffold-tests/registry.ts +443 -0
  117. package/src/scaffold-tests/selectors.ts +493 -0
  118. package/src/scaffold-tests/selectorsNative.ts +273 -0
  119. package/src/scaffold-tests/stamp.ts +79 -0
  120. package/src/scaffold-tests/templates.ts +3578 -0
  121. package/src/skills/cli.ts +248 -0
  122. package/src/skills/compat.ts +166 -0
  123. package/src/skills/data.ts +192 -0
  124. package/src/skills/md.d.ts +7 -0
  125. package/src/skills/menu.ts +55 -0
  126. package/src/skills/spec.ts +227 -0
  127. package/src/suiteScripts.ts +110 -0
  128. package/src/tasks/a11yNative.ts +281 -0
  129. package/src/tasks/a11yScan.ts +67 -0
  130. package/src/tasks/biomeRunner.ts +301 -0
  131. package/src/tasks/biomeSweep.ts +166 -0
  132. package/src/tasks/e2eExpo.ts +436 -0
  133. package/src/tasks/e2eRoutes.ts +756 -0
  134. package/src/tasks/eslintReactRules.ts +67 -0
  135. package/src/tasks/index.ts +24 -0
  136. package/src/tasks/loadExpo.ts +273 -0
  137. package/src/tasks/loadPipes.ts +750 -0
  138. package/src/tasks/perfHarness.ts +145 -0
  139. package/src/tasks/staticPerfAnalysis.ts +510 -0
  140. package/src/tasks/types.ts +1 -0
  141. package/src/tasks/unitAndBrowser.ts +462 -0
  142. package/src/types.ts +1419 -0
  143. package/src/version.ts +12 -0
@@ -0,0 +1,100 @@
1
+ import ts from "typescript";
2
+ import type { ComponentInfo } from "../types.ts";
3
+ import type { ImportBinding, ParsedComponentUnit } from "./parseFile.ts";
4
+ import { emptyEnv, initialRenderExpression } from "./renderPath.ts";
5
+
6
+ /**
7
+ * The compound family a part's OWN file exports, if it does: shadcn-style
8
+ * wrappers put Dialog (rendering DialogPrimitive.Root), DialogContent and
9
+ * every part in one module, so the wrapper a spec needs is a same-file
10
+ * import away. Derivation is same-file only — a cross-file family would
11
+ * need the child tree, and chasing it is exactly the guessing v1 avoids.
12
+ *
13
+ * The caller decides WHEN to derive (only for compound-part units) and the
14
+ * emitters decide whether the family is wrappable at all (open-state Roots
15
+ * only) — this module just reports what the file says.
16
+ */
17
+
18
+ /** Headless-UI packages whose parts require a Root ancestor (extractRenderMode's list). */
19
+ const COMPOUND_PACKAGES = /^@(base-ui|radix-ui|ariakit)(\/|$)/;
20
+
21
+ const CONTENT_MEMBERS = new Set(["Content", "Popup"]);
22
+ const ROOT_MEMBERS = new Set(["Root", "Provider"]);
23
+
24
+ export function deriveCompoundFamily(
25
+ unit: ParsedComponentUnit,
26
+ siblings: ParsedComponentUnit[],
27
+ imports: Map<string, ImportBinding>,
28
+ ): ComponentInfo["compoundFamily"] {
29
+ const ownAll = compoundMembers(unit, imports);
30
+ // A package whose Root the unit renders itself is already provided for —
31
+ // only the UNROOTED members are what made this a compound part, and the
32
+ // family must be theirs (mirroring extractRenderMode's rootsProvided).
33
+ const selfRooted = new Set(
34
+ ownAll.filter((m) => ROOT_MEMBERS.has(m.member)).map((m) => m.specifier),
35
+ );
36
+ const unrooted = ownAll.filter((m) => !selfRooted.has(m.specifier));
37
+ const specifiers = new Set(unrooted.map((m) => m.specifier));
38
+ // Zero: a local-hop part with no direct member of its own — v2 territory.
39
+ // Two+: one wrap cannot fix both packages, and wrapping one leaves the
40
+ // other throwing — the honest skip was the right output.
41
+ if (specifiers.size !== 1) return undefined;
42
+ const specifier = [...specifiers][0] as string;
43
+ const own = unrooted.filter((m) => m.specifier === specifier);
44
+ // Sub-family members (SubTrigger, SubContent, Sub itself) additionally
45
+ // need a <Sub> ancestor that Root+Content cannot supply — confirmed red
46
+ // against real radix dropdown-menu. They stay honest skips.
47
+ if (own.some((m) => m.member.startsWith("Sub"))) return undefined;
48
+
49
+ let root: string | undefined;
50
+ let content: string | undefined;
51
+ for (const sibling of siblings) {
52
+ // Named exports only: both emitters import the wrappers by name, and a
53
+ // named import of a default-only export is TS2305 — in the host registry
54
+ // it takes the whole bundle down. The unit is never its own wrapper.
55
+ if (sibling.exportKind !== "named") continue;
56
+ if (sibling.componentName === unit.componentName) continue;
57
+ const members = compoundMembers(sibling, imports).filter((m) => m.specifier === specifier);
58
+ if (members.length === 0) continue;
59
+ if (members.some((m) => ROOT_MEMBERS.has(m.member))) {
60
+ root ??= sibling.componentName;
61
+ } else if (members.some((m) => CONTENT_MEMBERS.has(m.member))) {
62
+ content ??= sibling.componentName;
63
+ }
64
+ }
65
+ if (!root) return undefined;
66
+
67
+ const part = own.some((m) => m.member === "Trigger")
68
+ ? ("trigger" as const)
69
+ : own.some((m) => CONTENT_MEMBERS.has(m.member))
70
+ ? ("content" as const)
71
+ : ("other" as const);
72
+ return { root, ...(content !== undefined ? { content } : {}), part, specifier };
73
+ }
74
+
75
+ /** Every `<X.Member>` in the unit's initial render where X is a compound package. */
76
+ function compoundMembers(
77
+ unit: ParsedComponentUnit,
78
+ imports: Map<string, ImportBinding>,
79
+ ): Array<{ member: string; specifier: string }> {
80
+ const expr = initialRenderExpression(unit.bodyNode, emptyEnv())?.expr;
81
+ if (!expr) return [];
82
+ const out: Array<{ member: string; specifier: string }> = [];
83
+ const visit = (n: ts.Node): void => {
84
+ let tagName: ts.JsxTagNameExpression | undefined;
85
+ if (ts.isJsxOpeningElement(n) || ts.isJsxSelfClosingElement(n)) tagName = n.tagName;
86
+ if (tagName && ts.isPropertyAccessExpression(tagName)) {
87
+ let rootExpr: ts.Expression = tagName.expression;
88
+ while (ts.isPropertyAccessExpression(rootExpr)) rootExpr = rootExpr.expression;
89
+ if (ts.isIdentifier(rootExpr)) {
90
+ const binding = imports.get(rootExpr.text);
91
+ if (binding && COMPOUND_PACKAGES.test(binding.specifier)) {
92
+ out.push({ member: tagName.name.text, specifier: binding.specifier });
93
+ }
94
+ }
95
+ }
96
+ ts.forEachChild(n, visit);
97
+ };
98
+ visit(expr);
99
+ return out;
100
+ }
@@ -0,0 +1,200 @@
1
+ import ts from "typescript";
2
+ import type { ModuleResolver } from "./resolveModule.ts";
3
+ import type { SourceFileCache } from "./sourceCache.ts";
4
+ import { findExportedDecl } from "./sourceCache.ts";
5
+
6
+ /**
7
+ * Module-side exports whose declarations are DATA constants — the evidence
8
+ * the mock emitter needs to leave a config-style module real in RSC mode.
9
+ *
10
+ * When EVERYTHING a component imports from a module is one of these, the
11
+ * module must not be mocked at all. A mock factory replaces the module for
12
+ * the whole graph, and a wholesale replacement only ever enumerates the one
13
+ * component's import list: stubbing `@/config/site` to satisfy a component's
14
+ * `SOURCE_CODE_GITHUB_REPO` left json-ld.ts's `SITE_INFO` undefined, and the
15
+ * chanhdai dogfood buried 150+ specs at collection under
16
+ * `No "SITE_INFO" export is defined on the "@/config/site" mock`.
17
+ *
18
+ * Conservative on purpose — "declared" STOPS the barrel walk, so a module can
19
+ * never be unmocked on the strength of a name that is not really its data
20
+ * export. Two verdicts per name found:
21
+ *
22
+ * data — initializers that cannot compute at module scope: literals,
23
+ * substitution-free templates, property-access chains
24
+ * (`USER.bio`, `process.env.X || "https://…"`), object/array
25
+ * literals built only of those, and logical/binary/ternary
26
+ * compositions of those.
27
+ * declared — anything else that declares the name: a function, class or
28
+ * enum, an initializer containing a call, `new`, `await`, or a
29
+ * bare identifier whose binding the scan cannot see. An env
30
+ * module exporting `envSchema.parse(…)` lands here — those
31
+ * stay mocked, exactly as before.
32
+ *
33
+ * A name never located at all yields nothing: the mock's normal honest {}
34
+ * fallback covers it, and unmocking on an absence would import blind.
35
+ *
36
+ * "default" names the default export (`export default { … }`), same
37
+ * convention as importedReturns/serverActionReturns.
38
+ */
39
+ export function dataConstantExports(
40
+ mi: { names: string[]; importedNames?: string[]; isTypeOnly: boolean; hasDefault: boolean },
41
+ resolvedPath: string,
42
+ resolver: ModuleResolver,
43
+ cache: SourceFileCache,
44
+ ): string[] {
45
+ if (mi.isTypeOnly) return [];
46
+ const out: string[] = [];
47
+ for (const [i, local] of mi.names.entries()) {
48
+ const exported = mi.hasDefault && i === 0 ? "default" : (mi.importedNames?.[i] ?? local);
49
+ if (!exported) continue;
50
+ const located = findExportedDecl(
51
+ resolvedPath,
52
+ exported,
53
+ cache,
54
+ resolver,
55
+ selectDeclaration,
56
+ true,
57
+ );
58
+ if (located?.decl === "data") out.push(exported);
59
+ }
60
+ return out;
61
+ }
62
+
63
+ type DeclHit = "data" | "declared";
64
+
65
+ /**
66
+ * Verdict for one statement list: "data" for an exported data constant,
67
+ * "declared" for any other executable declaration of the name, undefined to
68
+ * keep walking (not here — a barrel arm may hold it; type-only declarations
69
+ * are erased at runtime and never claim a value binding).
70
+ */
71
+ function selectDeclaration(
72
+ sf: ts.SourceFile,
73
+ name: string,
74
+ viaReexport: boolean,
75
+ ): DeclHit | undefined {
76
+ for (const stmt of sf.statements) {
77
+ if (ts.isVariableStatement(stmt)) {
78
+ const exported =
79
+ ts.canHaveModifiers(stmt) &&
80
+ (ts.getModifiers(stmt)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false);
81
+ // Reached through a re-export chain, only an exported declaration can
82
+ // vouch for the name; a local `export { X as Y }` clause re-queries
83
+ // this file with viaReexport=false and binds the private one.
84
+ if (viaReexport && !exported) continue;
85
+ for (const d of stmt.declarationList.declarations) {
86
+ if (!ts.isIdentifier(d.name) || d.name.text !== name) continue;
87
+ return d.initializer !== undefined && isDataExpr(d.initializer) ? "data" : "declared";
88
+ }
89
+ continue;
90
+ }
91
+ if (ts.isExportAssignment(stmt)) {
92
+ if (name === "default" && !stmt.isExportEquals) {
93
+ return isDataExpr(stmt.expression) ? "data" : "declared";
94
+ }
95
+ continue;
96
+ }
97
+ if (ts.isFunctionDeclaration(stmt) && stmt.name?.text === name) return "declared";
98
+ if (ts.isClassDeclaration(stmt) && stmt.name?.text === name) return "declared";
99
+ if (ts.isEnumDeclaration(stmt) && stmt.name.text === name) return "declared";
100
+ }
101
+ return undefined;
102
+ }
103
+
104
+ /** Depth bound on literal nesting — config files stop at 2-3; beyond this, bail. */
105
+ const MAX_DATA_DEPTH = 8;
106
+
107
+ /**
108
+ * Is this initializer shaped as data — read-only composition of literals and
109
+ * property reads, never a call, a construction, or a function value?
110
+ *
111
+ * `export const fn = obj.method` passes: the chain is a read, and on the
112
+ * skip path the module loads REAL, so even a function-valued constant keeps
113
+ * its real shape. The question the answer feeds is only whether the module
114
+ * SCOPE can compute or throw — calls, `new`, `await` and function literals
115
+ * disqualify precisely because they do.
116
+ */
117
+ function isDataExpr(e: ts.Expression, depth = 0): boolean {
118
+ if (depth > MAX_DATA_DEPTH) return false;
119
+ for (;;) {
120
+ if (
121
+ ts.isParenthesizedExpression(e) ||
122
+ ts.isAsExpression(e) ||
123
+ ts.isSatisfiesExpression(e) ||
124
+ ts.isTypeAssertionExpression(e) ||
125
+ ts.isNonNullExpression(e)
126
+ ) {
127
+ e = e.expression;
128
+ continue;
129
+ }
130
+ break;
131
+ }
132
+ if (
133
+ ts.isStringLiteral(e) ||
134
+ ts.isNumericLiteral(e) ||
135
+ ts.isBigIntLiteral(e) ||
136
+ ts.isNoSubstitutionTemplateLiteral(e)
137
+ ) {
138
+ return true;
139
+ }
140
+ if (
141
+ e.kind === ts.SyntaxKind.TrueKeyword ||
142
+ e.kind === ts.SyntaxKind.FalseKeyword ||
143
+ e.kind === ts.SyntaxKind.NullKeyword
144
+ ) {
145
+ return true;
146
+ }
147
+ if (ts.isIdentifier(e)) {
148
+ // Only `export const X = undefined` — a bare identifier could be bound
149
+ // to a call result two statements up.
150
+ return e.text === "undefined";
151
+ }
152
+ if (ts.isPropertyAccessExpression(e)) {
153
+ // Chain root is an opaque read — an import (USER, process) or a file
154
+ // const. Property access itself cannot compute.
155
+ return ts.isIdentifier(e.expression) || isDataExpr(e.expression, depth + 1);
156
+ }
157
+ if (ts.isElementAccessExpression(e)) {
158
+ return (
159
+ (ts.isIdentifier(e.expression) || isDataExpr(e.expression, depth + 1)) &&
160
+ isDataExpr(e.argumentExpression, depth + 1)
161
+ );
162
+ }
163
+ if (ts.isTemplateExpression(e)) {
164
+ return e.head.text === "" && e.templateSpans.every((s) => s.literal.text === "");
165
+ }
166
+ if (ts.isObjectLiteralExpression(e)) {
167
+ return e.properties.every((p) => {
168
+ if (ts.isPropertyAssignment(p)) {
169
+ if (p.name !== undefined && ts.isComputedPropertyName(p.name)) return false;
170
+ return isDataExpr(p.initializer, depth + 1);
171
+ }
172
+ if (ts.isSpreadAssignment(p)) return isDataExpr(p.expression, depth + 1);
173
+ // Shorthand binds an unknown local; methods and accessors are functions.
174
+ return false;
175
+ });
176
+ }
177
+ if (ts.isArrayLiteralExpression(e)) {
178
+ return e.elements.every(
179
+ (el) => el.kind === ts.SyntaxKind.OmittedExpression || isDataExpr(el, depth + 1),
180
+ );
181
+ }
182
+ // isBinaryExpression covers the logical forms (&&, ||, ??) too.
183
+ if (ts.isBinaryExpression(e)) {
184
+ return isDataExpr(e.left, depth + 1) && isDataExpr(e.right, depth + 1);
185
+ }
186
+ if (ts.isConditionalExpression(e)) {
187
+ return (
188
+ isDataExpr(e.condition, depth + 1) &&
189
+ isDataExpr(e.whenTrue, depth + 1) &&
190
+ isDataExpr(e.whenFalse, depth + 1)
191
+ );
192
+ }
193
+ if (ts.isPrefixUnaryExpression(e)) {
194
+ return (
195
+ (e.operator === ts.SyntaxKind.MinusToken || e.operator === ts.SyntaxKind.PlusToken) &&
196
+ isDataExpr(e.operand, depth + 1)
197
+ );
198
+ }
199
+ return false;
200
+ }
@@ -0,0 +1,112 @@
1
+ import ts from "typescript";
2
+ import type { AttrValueKind, ChildAttrInfo } from "../types.ts";
3
+ import { posOf } from "./pos.ts";
4
+
5
+ interface ClassifiedValue {
6
+ valueKind: AttrValueKind;
7
+ /** Root identifier for identifier / member kinds. */
8
+ identifier?: string;
9
+ }
10
+
11
+ /**
12
+ * Attribute shapes passed to uppercase (component) JSX children under
13
+ * `bodyNode`. Lowercase host elements (div, button…) are skipped entirely.
14
+ */
15
+ export function extractChildAttrs(bodyNode: ts.Node, sourceFile: ts.SourceFile): ChildAttrInfo[] {
16
+ const out: ChildAttrInfo[] = [];
17
+
18
+ const visit = (n: ts.Node): void => {
19
+ if (ts.isJsxOpeningElement(n) || ts.isJsxSelfClosingElement(n)) {
20
+ const tag = componentTagName(n.tagName);
21
+ if (tag !== undefined) {
22
+ for (const attr of n.attributes.properties) {
23
+ if (ts.isJsxSpreadAttribute(attr)) {
24
+ out.push({
25
+ tag,
26
+ attr: "...spread",
27
+ valueKind: "spread",
28
+ pos: posOf(attr, sourceFile),
29
+ });
30
+ continue;
31
+ }
32
+ if (!ts.isJsxAttribute(attr)) continue;
33
+ const name = ts.isIdentifier(attr.name)
34
+ ? attr.name.text
35
+ : `${attr.name.namespace.text}:${attr.name.name.text}`;
36
+ const classified = classifyInitializer(attr.initializer);
37
+ const entry: ChildAttrInfo = {
38
+ tag,
39
+ attr: name,
40
+ valueKind: classified.valueKind,
41
+ pos: posOf(attr, sourceFile),
42
+ };
43
+ if (classified.identifier !== undefined) entry.identifier = classified.identifier;
44
+ out.push(entry);
45
+ }
46
+ }
47
+ }
48
+ ts.forEachChild(n, visit);
49
+ };
50
+ ts.forEachChild(bodyNode, visit);
51
+
52
+ return out;
53
+ }
54
+
55
+ /** Full dotted tag text ("Card", "Menu.Item") when the root identifier is uppercase. */
56
+ function componentTagName(tag: ts.JsxTagNameExpression): string | undefined {
57
+ if (ts.isIdentifier(tag)) {
58
+ return /^[A-Z]/.test(tag.text) ? tag.text : undefined;
59
+ }
60
+ if (ts.isPropertyAccessExpression(tag)) {
61
+ let root: ts.Expression = tag.expression;
62
+ while (ts.isPropertyAccessExpression(root)) root = root.expression;
63
+ if (ts.isIdentifier(root) && /^[A-Z]/.test(root.text)) return readPropertyAccess(tag);
64
+ }
65
+ return undefined;
66
+ }
67
+
68
+ function readPropertyAccess(p: ts.PropertyAccessExpression): string {
69
+ const head = ts.isIdentifier(p.expression)
70
+ ? p.expression.text
71
+ : ts.isPropertyAccessExpression(p.expression)
72
+ ? readPropertyAccess(p.expression)
73
+ : "<dynamic>";
74
+ return `${head}.${p.name.text}`;
75
+ }
76
+
77
+ function classifyInitializer(init: ts.JsxAttribute["initializer"]): ClassifiedValue {
78
+ if (!init) return { valueKind: "boolean-true" };
79
+ if (ts.isStringLiteral(init)) return { valueKind: "string" };
80
+ if (ts.isJsxExpression(init)) {
81
+ return init.expression ? classifyExpression(init.expression) : { valueKind: "other" };
82
+ }
83
+ // TS 5.x also allows JsxElement/JsxFragment attribute values directly.
84
+ return { valueKind: "jsx" };
85
+ }
86
+
87
+ function classifyExpression(expr: ts.Expression): ClassifiedValue {
88
+ if (ts.isStringLiteral(expr)) return { valueKind: "string" };
89
+ if (ts.isNumericLiteral(expr)) return { valueKind: "number" };
90
+ if (expr.kind === ts.SyntaxKind.TrueKeyword || expr.kind === ts.SyntaxKind.FalseKeyword) {
91
+ // The approved AttrValueKind union has no "boolean-false"; a literal
92
+ // `false` value is deliberately reported as "boolean-true" too.
93
+ return { valueKind: "boolean-true" };
94
+ }
95
+ if (expr.kind === ts.SyntaxKind.NullKeyword) return { valueKind: "null" };
96
+ if (ts.isIdentifier(expr)) return { valueKind: "identifier", identifier: expr.text };
97
+ if (ts.isPropertyAccessExpression(expr)) {
98
+ let root: ts.Expression = expr.expression;
99
+ while (ts.isPropertyAccessExpression(root)) root = root.expression;
100
+ if (ts.isIdentifier(root)) return { valueKind: "member", identifier: root.text };
101
+ return { valueKind: "member" };
102
+ }
103
+ if (ts.isObjectLiteralExpression(expr)) return { valueKind: "object-literal" };
104
+ if (ts.isArrayLiteralExpression(expr)) return { valueKind: "array-literal" };
105
+ if (ts.isArrowFunction(expr)) return { valueKind: "arrow" };
106
+ if (ts.isFunctionExpression(expr)) return { valueKind: "function" };
107
+ if (ts.isCallExpression(expr)) return { valueKind: "call" };
108
+ if (ts.isJsxElement(expr) || ts.isJsxSelfClosingElement(expr) || ts.isJsxFragment(expr)) {
109
+ return { valueKind: "jsx" };
110
+ }
111
+ return { valueKind: "other" };
112
+ }