@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.
- 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 +611 -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,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
|
+
}
|