@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,185 @@
1
+ import type { PropsShape, PropValue } from "../types.ts";
2
+
3
+ /**
4
+ * A curated, closed table of host-element props — deliberately NOT a model of
5
+ * the DOM. `ComponentProps<"div">` is the dominant props shape in shadcn/base-ui
6
+ * codebases, and without this every such component synthesises zero props and
7
+ * gets an assertion-free smoke test.
8
+ *
9
+ * Three rules are baked in, each because breaking them produces *false test
10
+ * failures* rather than missing coverage:
11
+ *
12
+ * 1. Never synthesise `value`/`checked` — a value without an onChange makes
13
+ * React warn about a controlled input and turns `userEvent.type` into a
14
+ * silent no-op. Always `defaultValue`.
15
+ * 2. Never synthesise `placeholder`/`name`/`required` — an empty-string default
16
+ * is useless to the scaffolder and `getByPlaceholderText("")` throws.
17
+ * 3. `type` is always a valid literal, never `""` — `<button type="">` is
18
+ * invalid HTML.
19
+ */
20
+
21
+ export interface HostElementProps {
22
+ shape: PropsShape;
23
+ variantOptions?: Record<string, PropValue[]>;
24
+ acceptsChildren: boolean;
25
+ /** Implicit ARIA role, only where unambiguous. */
26
+ role?: string;
27
+ /** Handler props the scaffolder stubs with vi.fn(). */
28
+ handlers: string[];
29
+ }
30
+
31
+ /** Present on every HTML element — safe for any tag, known or not. */
32
+ const BASE: PropsShape = { className: "", id: "" };
33
+
34
+ export const VOID_ELEMENTS: ReadonlySet<string> = new Set([
35
+ "area",
36
+ "base",
37
+ "br",
38
+ "col",
39
+ "embed",
40
+ "hr",
41
+ "img",
42
+ "input",
43
+ "link",
44
+ "meta",
45
+ "param",
46
+ "source",
47
+ "track",
48
+ "wbr",
49
+ ]);
50
+
51
+ const CONTAINER_TAGS = [
52
+ "div",
53
+ "span",
54
+ "p",
55
+ "section",
56
+ "article",
57
+ "header",
58
+ "footer",
59
+ "main",
60
+ "aside",
61
+ "figure",
62
+ "blockquote",
63
+ "table",
64
+ "thead",
65
+ "tbody",
66
+ "tr",
67
+ "td",
68
+ "th",
69
+ "dl",
70
+ "dt",
71
+ "dd",
72
+ "pre",
73
+ "code",
74
+ "em",
75
+ "strong",
76
+ "small",
77
+ "fieldset",
78
+ "legend",
79
+ ];
80
+
81
+ function container(role?: string): HostElementProps {
82
+ return { shape: { ...BASE }, acceptsChildren: true, handlers: [], ...(role ? { role } : {}) };
83
+ }
84
+
85
+ const TABLE: Record<string, HostElementProps> = {};
86
+ for (const tag of CONTAINER_TAGS) TABLE[tag] = container();
87
+
88
+ TABLE.nav = container("navigation");
89
+ TABLE.ul = container("list");
90
+ TABLE.ol = container("list");
91
+ TABLE.li = container("listitem");
92
+ for (const h of ["h1", "h2", "h3", "h4", "h5", "h6"]) TABLE[h] = container("heading");
93
+
94
+ TABLE.label = {
95
+ shape: { ...BASE, htmlFor: "" },
96
+ acceptsChildren: true,
97
+ handlers: [],
98
+ };
99
+
100
+ TABLE.button = {
101
+ shape: { ...BASE, type: "button", disabled: false },
102
+ variantOptions: { type: ["button", "submit", "reset"] },
103
+ acceptsChildren: true,
104
+ role: "button",
105
+ handlers: ["onClick"],
106
+ };
107
+
108
+ TABLE.a = {
109
+ shape: { ...BASE, href: "/" },
110
+ acceptsChildren: true,
111
+ role: "link",
112
+ handlers: ["onClick"],
113
+ };
114
+
115
+ TABLE.input = {
116
+ shape: { ...BASE, type: "text", defaultValue: "", disabled: false },
117
+ variantOptions: { type: ["text", "email", "password", "number"] },
118
+ acceptsChildren: false,
119
+ role: "textbox",
120
+ handlers: ["onChange"],
121
+ };
122
+
123
+ TABLE.textarea = {
124
+ shape: { ...BASE, defaultValue: "", disabled: false, rows: 3 },
125
+ acceptsChildren: false,
126
+ role: "textbox",
127
+ handlers: ["onChange"],
128
+ };
129
+
130
+ TABLE.select = {
131
+ shape: { ...BASE, defaultValue: "", disabled: false },
132
+ acceptsChildren: true,
133
+ role: "combobox",
134
+ handlers: ["onChange"],
135
+ };
136
+
137
+ TABLE.form = {
138
+ shape: { ...BASE },
139
+ acceptsChildren: true,
140
+ role: "form",
141
+ handlers: ["onSubmit"],
142
+ };
143
+
144
+ TABLE.img = {
145
+ shape: { ...BASE, src: "/flawless.png", alt: "placeholder" },
146
+ acceptsChildren: false,
147
+ role: "img",
148
+ handlers: [],
149
+ };
150
+
151
+ for (const tag of ["svg", "path", "circle", "rect", "g"]) {
152
+ TABLE[tag] = {
153
+ shape: { ...BASE },
154
+ acceptsChildren: !VOID_ELEMENTS.has(tag),
155
+ handlers: [],
156
+ };
157
+ }
158
+
159
+ export const HOST_ELEMENT_PROPS: Readonly<Record<string, HostElementProps>> = TABLE;
160
+
161
+ /** Tag → implicit ARIA role, shared with the selector emitters so they cannot drift. */
162
+ export const TAG_TO_ROLE: Readonly<Record<string, string>> = Object.fromEntries(
163
+ Object.entries(TABLE)
164
+ .filter(([, v]) => v.role !== undefined)
165
+ .map(([k, v]) => [k, v.role as string]),
166
+ );
167
+
168
+ export function isHostTagRecognised(tag: string): boolean {
169
+ return tag in TABLE;
170
+ }
171
+
172
+ /**
173
+ * Never undefined. An unknown tag still gets `className`/`id` (present on every
174
+ * HTML element) and a children flag from the closed void-element list — both
175
+ * are facts about HTML, not guesses about this component.
176
+ */
177
+ export function hostPropsFor(tag: string): HostElementProps {
178
+ const known = TABLE[tag];
179
+ if (known) return { ...known, shape: { ...known.shape } };
180
+ return {
181
+ shape: { ...BASE },
182
+ acceptsChildren: !VOID_ELEMENTS.has(tag),
183
+ handlers: [],
184
+ };
185
+ }
@@ -0,0 +1,72 @@
1
+ import { detectHostDialect, type HostDialect } from "./nativeDialect.ts";
2
+ import { nativeElementFor } from "./nativeProps.ts";
3
+ import type { ImportBinding } from "./parseFile.ts";
4
+
5
+ /**
6
+ * Whether a JSX tag names a host element, and which one.
7
+ *
8
+ * ONE predicate, replacing the `/^[a-z][a-z0-9-]*$/` test that was written out
9
+ * five times — extractRootTag, extractRenderMode, extractFlows, extractChildAttrs
10
+ * and selectors.ts. Five copies of a rule is the defect class this project keeps
11
+ * finding in its own docs, and it was about to become ten.
12
+ *
13
+ * The rule is EVIDENCE-BASED in both directions:
14
+ *
15
+ * - lowercase -> a DOM intrinsic. Byte-identical to the old test.
16
+ * - Capitalised -> a host element IF AND ONLY IF this file imports that
17
+ * name from a specifier in the closed native table.
18
+ * - anything else -> undefined, meaning "this is a component" — exactly
19
+ * the state every uppercase tag is in today.
20
+ *
21
+ * Never a guess from the name. `<IconButton>` is not a button, `<TicketRow>` is
22
+ * not a row, and shadcn's `<ButtonPrimitive>` must keep answering undefined on
23
+ * the web path or the DOM regression this predicate was extracted from returns.
24
+ */
25
+ export interface ResolvedHostTag {
26
+ /**
27
+ * The canonical element name: the MODULE-SIDE export, never the local alias.
28
+ *
29
+ * `import { View as Box }` gives tag "Box" and name "View". This is the same
30
+ * refusal extractRootTag already makes for `<UI.Badge/>` — "Badge" and
31
+ * "UI.Badge" are both lies about the emitted element — applied in the other
32
+ * direction, and it is what makes rootTag comparable across files.
33
+ */
34
+ name: string;
35
+ dialect: HostDialect;
36
+ }
37
+
38
+ export type HostTagResolver = (tag: string) => ResolvedHostTag | undefined;
39
+
40
+ const LOWERCASE_INTRINSIC = /^[a-z][a-z0-9-]*$/;
41
+
42
+ /**
43
+ * Today's rule, exactly: lowercase intrinsics only.
44
+ *
45
+ * The default everywhere, so every existing call site behaves byte-identically
46
+ * until it is handed a native resolver on purpose.
47
+ */
48
+ export const domOnlyResolver: HostTagResolver = (tag) =>
49
+ LOWERCASE_INTRINSIC.test(tag) ? { name: tag, dialect: "dom" } : undefined;
50
+
51
+ /**
52
+ * A resolver for one file, built from that file's own imports.
53
+ *
54
+ * Returns `domOnlyResolver` unchanged for a DOM file — not a wrapper around it,
55
+ * the same function object — so the web path cannot pick up so much as an extra
56
+ * closure.
57
+ */
58
+ export function makeHostTagResolver(imports: Map<string, ImportBinding>): HostTagResolver {
59
+ if (detectHostDialect(imports) === "dom") return domOnlyResolver;
60
+ return (tag) => {
61
+ // A lowercase tag in a React Native file is still a DOM intrinsic as far as
62
+ // this predicate is concerned. It cannot render on a device, but saying so
63
+ // is extractRenderMode's job, not this one's.
64
+ if (LOWERCASE_INTRINSIC.test(tag)) return { name: tag, dialect: "dom" };
65
+ if (!/^[A-Z]/.test(tag)) return undefined;
66
+ const binding = imports.get(tag);
67
+ if (!binding || binding.isTypeOnly || binding.kind === "namespace") return undefined;
68
+ const exported = binding.importedName ?? tag;
69
+ if (nativeElementFor(binding.specifier, exported) === undefined) return undefined;
70
+ return { name: exported, dialect: "native" };
71
+ };
72
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Facts about HTML's content model that a generated harness has to respect.
3
+ *
4
+ * This is not inference about the analysed app — it is the specification. A
5
+ * `<tbody>` is only valid inside a `<table>`, so mounting `TableBody` straight
6
+ * into the host's `#root` div made React 19 log
7
+ *
8
+ * In HTML, <tbody> cannot be a child of <div>. This will cause a hydration error.
9
+ *
10
+ * on seven of the dogfood app's table primitives. The spec then failed them for
11
+ * a warning the harness itself provoked. Wrapping the mount in the required
12
+ * ancestors keeps those components covered instead of excluding them, and it is
13
+ * a lookup rather than a guess.
14
+ */
15
+
16
+ /** Ancestors a tag must be nested in, outermost first. */
17
+ const REQUIRED_ANCESTORS: Record<string, string[]> = {
18
+ thead: ["table"],
19
+ tbody: ["table"],
20
+ tfoot: ["table"],
21
+ caption: ["table"],
22
+ colgroup: ["table"],
23
+ col: ["table", "colgroup"],
24
+ tr: ["table", "tbody"],
25
+ td: ["table", "tbody", "tr"],
26
+ th: ["table", "tbody", "tr"],
27
+ option: ["select"],
28
+ optgroup: ["select"],
29
+ li: ["ul"],
30
+ dt: ["dl"],
31
+ dd: ["dl"],
32
+ figcaption: ["figure"],
33
+ legend: ["fieldset"],
34
+ source: ["video"],
35
+ track: ["video"],
36
+ area: ["map"],
37
+ param: ["object"],
38
+ summary: ["details"],
39
+ };
40
+
41
+ /**
42
+ * The chain a component's root tag must be mounted inside, or [] when it can
43
+ * stand alone. Unknown tags (every component name, every custom element) return
44
+ * [] — the harness only wraps where HTML demands it.
45
+ */
46
+ export function requiredAncestors(rootTag: string | undefined): string[] {
47
+ if (!rootTag) return [];
48
+ return REQUIRED_ANCESTORS[rootTag] ?? [];
49
+ }
50
+
51
+ /** Elements a user can type into. Anything else rejects .fill()/.type() outright. */
52
+ const FILLABLE_TAGS = new Set(["input", "textarea", "select"]);
53
+
54
+ /**
55
+ * Whether a change/input event target is something a test can type into.
56
+ *
57
+ * React's onChange bubbles, so it is idiomatic to put it on the `<form>` and
58
+ * read `event.target`. Driving that literally emitted
59
+ * `page.locator("form").fill("x")`, which Playwright rejects with "Element is
60
+ * not an <input>, <textarea>, <select> or [contenteditable]" — the generator
61
+ * describing event delegation as if it were a text box.
62
+ */
63
+ export function isFillableTag(tag: string): boolean {
64
+ return FILLABLE_TAGS.has(tag);
65
+ }
66
+
67
+ /** Input types that hold no user-suppliable text (or none a blind fill can supply). */
68
+ const UNFILLABLE_INPUT_TYPES = new Set([
69
+ "hidden",
70
+ "checkbox",
71
+ "radio",
72
+ "file",
73
+ "image",
74
+ "submit",
75
+ "reset",
76
+ "button",
77
+ "range",
78
+ "color",
79
+ ]);
80
+
81
+ export type FillKind = "text" | "toggle" | "select" | "skip";
82
+
83
+ /** How (or whether) a form field can be driven, from its tag and type alone. */
84
+ export function fillKind(tag: string, type: string | undefined): FillKind {
85
+ if (tag === "select") return "select";
86
+ if (tag === "textarea") return "text";
87
+ if (tag !== "input") return "skip";
88
+ if (!type) return "text";
89
+ if (type === "checkbox" || type === "radio") return "toggle";
90
+ // A hidden input carries a value the component chose; typing into it asserts
91
+ // nothing and, when several share a name, cannot even be addressed uniquely.
92
+ if (UNFILLABLE_INPUT_TYPES.has(type)) return "skip";
93
+ return "text";
94
+ }