@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,165 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import type { ReactCompilerProbe } from "./types.ts";
4
+
5
+ /** Expo's own flag, in the app config a person edits. Parsed: it is JSON. */
6
+ const APP_CONFIG = "app.json";
7
+
8
+ /** Next ≥15's stable `reactCompiler` flag, plus its earlier `experimental` home. */
9
+ const NEXT_CONFIGS = ["next.config.ts", "next.config.js", "next.config.mjs", "next.config.cjs"];
10
+
11
+ /** The project's Babel entry — where the plugin goes when Babel runs at all. */
12
+ const BABEL_CONFIGS = ["babel.config.js", "babel.config.cjs", "babel.config.mjs"];
13
+
14
+ /**
15
+ * Bundler configs whose presence means "this project's transform pipeline is
16
+ * declared here, in this file". Deliberately a list of config-file CONVENTIONS
17
+ * we recognise and not a general scan: a bundler we do not know has no honest
18
+ * answer, and says so.
19
+ */
20
+ const BUNDLER_CONFIGS = [
21
+ "vite.config.ts",
22
+ "vite.config.js",
23
+ "vite.config.mjs",
24
+ "webpack.config.js",
25
+ "webpack.config.cjs",
26
+ "webpack.config.mjs",
27
+ "webpack.config.ts",
28
+ "rspack.config.js",
29
+ "rsbuild.config.ts",
30
+ "rsbuild.config.js",
31
+ ];
32
+
33
+ const PLUGIN_NEEDLE = "babel-plugin-react-compiler";
34
+
35
+ /** `reactCompiler: true`, `"reactCompiler": true`, `'react-compiler': false`. */
36
+ const FLAG_ASSIGNMENT = /["']?(reactCompiler|react-compiler)["']?\s*:\s*(true|false)/;
37
+
38
+ /**
39
+ * Read the React Compiler state for a project root.
40
+ *
41
+ * The ladder is the same in every mode, and it is a ladder of authority rather
42
+ * than a search: a positive spelling in any config the project actually uses
43
+ * turns it on, and only the absence of every positive spelling in every config
44
+ * we could read turns it off.
45
+ *
46
+ * Everything read here is a config file, as text, never executed — and never
47
+ * package.json or a lockfile. The dogfood web app's `bun.lock` carries
48
+ * `babel-plugin-react-compiler` because Next ships it as a dependency of its
49
+ * own while the compiler is OFF; a dependency-presence check would have
50
+ * suppressed 127 valid findings on that run. Dependency presence ≠ activation.
51
+ *
52
+ * If a future preset ships the compiler ON by default (bare-RN's
53
+ * `@react-native/babel-preset` and newer Expo SDKs have been moving that way),
54
+ * the version-keyed default table belongs in this file: read the installed
55
+ * preset's SHIPPED DEFAULT as text. Asking the project's babel config that
56
+ * question by executing it is never the answer.
57
+ */
58
+ export function detectReactCompiler(root: string): ReactCompilerProbe {
59
+ const abs = path.resolve(root);
60
+ const positives: string[] = [];
61
+ const read: string[] = [];
62
+
63
+ // Expo's flag first, parsed rather than grepped: app.json is JSON, so the key
64
+ // is decisive in a way no text scan is. `app.config.{js,ts}` is not read at
65
+ // all — executing a project's config to ask it a question is not something a
66
+ // static analyser does, and the babel plugin spelling below still catches a
67
+ // project that turns the compiler on that way.
68
+ const appJson = readJson(path.join(abs, APP_CONFIG));
69
+ if (appJson) {
70
+ read.push(APP_CONFIG);
71
+ const experiments = (appJson.expo as { experiments?: Record<string, unknown> } | undefined)
72
+ ?.experiments;
73
+ if (experiments?.reactCompiler === true) {
74
+ positives.push(`${APP_CONFIG} experiments.reactCompiler`);
75
+ }
76
+ }
77
+
78
+ for (const file of [...NEXT_CONFIGS, ...BABEL_CONFIGS, ...BUNDLER_CONFIGS]) {
79
+ const text = readText(path.join(abs, file));
80
+ if (text === undefined) continue;
81
+ read.push(file);
82
+ const positive = configSaysOn(file, text);
83
+ if (positive !== undefined) positives.push(positive);
84
+ }
85
+
86
+ // The first positive names a sufficient reason, so several spellings in one
87
+ // project cost one evidence line, not a list.
88
+ if (positives.length > 0) return { state: "on", evidence: positives[0] as string };
89
+
90
+ if (read.length === 0) {
91
+ return {
92
+ state: "undetermined",
93
+ evidence: "no app.json, next.config, babel config or recognised bundler config to read",
94
+ };
95
+ }
96
+ const more = read.length > 3 ? `, +${read.length - 3} more` : "";
97
+ return {
98
+ state: "off",
99
+ evidence: `no React Compiler flag in ${read.slice(0, 3).join(", ")}${more}`,
100
+ };
101
+ }
102
+
103
+ /**
104
+ * What one config file says. Undefined means the file does not speak about the
105
+ * compiler at all — which is not the same as it saying no, and is why the caller
106
+ * collects what was READ separately from what was POSITIVE.
107
+ *
108
+ * Order matters. An explicit assignment wins over the plugin name because a
109
+ * preset option is the gate; the plugin name wins over the bare flag word
110
+ * because the bare word can be anything (`isReactCompilerEnabled` in a comment,
111
+ * a local variable) — kept only because it is a real spelling of expo's preset
112
+ * options and the detector has always accepted it.
113
+ */
114
+ function configSaysOn(file: string, text: string): string | undefined {
115
+ const flag = FLAG_ASSIGNMENT.exec(text);
116
+ if (flag) {
117
+ return flag[2] === "true" ? `${file} ${flag[1]}` : undefined;
118
+ }
119
+ if (text.includes(PLUGIN_NEEDLE)) return `${file} ${PLUGIN_NEEDLE}`;
120
+ if (/\breactCompiler\b/.test(text)) return `${file} reactCompiler`;
121
+ return undefined;
122
+ }
123
+
124
+ /**
125
+ * The project root a framework-less run's configs sit in.
126
+ *
127
+ * The same upward walk `ignoreRootFor` makes for the same reason: the tool is
128
+ * run from wherever the person happens to be, and the configs are not
129
+ * necessarily beside the out dir. The first ancestor that is a project — a
130
+ * `.git` or a `package.json` — wins, so a run from a monorepo package reads that
131
+ * package's configs rather than the repo's. Unlike the ignore case there is no
132
+ * last-resort answer: a tree with no package.json anywhere has no configs to
133
+ * read either, so the caller gets its own start back and says undetermined.
134
+ */
135
+ export function configRootFor(outDir: string): string {
136
+ let dir = path.resolve(outDir);
137
+ for (;;) {
138
+ if (existsSync(path.join(dir, ".git")) || existsSync(path.join(dir, "package.json"))) {
139
+ return dir;
140
+ }
141
+ const parent = path.dirname(dir);
142
+ if (parent === dir) return path.resolve(outDir);
143
+ dir = parent;
144
+ }
145
+ }
146
+
147
+ function readText(p: string): string | undefined {
148
+ if (!existsSync(p)) return undefined;
149
+ try {
150
+ return readFileSync(p, "utf8");
151
+ } catch {
152
+ // An unreadable config proves nothing. It is not evidence of an absent flag.
153
+ return undefined;
154
+ }
155
+ }
156
+
157
+ function readJson(p: string): Record<string, unknown> | undefined {
158
+ const text = readText(p);
159
+ if (text === undefined) return undefined;
160
+ try {
161
+ return JSON.parse(text) as Record<string, unknown>;
162
+ } catch {
163
+ return undefined;
164
+ }
165
+ }
@@ -0,0 +1,156 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { relativeToEmitted } from "../emitPaths.ts";
4
+ import { A11Y_REPORT } from "../tasks/a11yScan.ts";
5
+ import { REACT_RULES_REPORT } from "../tasks/eslintReactRules.ts";
6
+ import type { ComponentInfo, ComponentTree, Finding, TaskId, TaskResult } from "../types.ts";
7
+
8
+ export function printFindings(findings: Finding[]): void {
9
+ if (findings.length === 0) return;
10
+ const byFile = new Map<string, Finding[]>();
11
+ for (const f of findings) {
12
+ const list = byFile.get(f.file) ?? [];
13
+ list.push(f);
14
+ byFile.set(f.file, list);
15
+ }
16
+ for (const [file, list] of byFile) {
17
+ process.stdout.write(`\n${path.relative(process.cwd(), file)}\n`);
18
+ for (const f of list) {
19
+ const sev = f.severity === "error" ? "ERROR" : "warn ";
20
+ process.stdout.write(` ${sev} ${f.line}:${f.column} ${f.rule} ${f.message}\n`);
21
+ }
22
+ }
23
+ }
24
+
25
+ export function printSummaryTable(results: TaskResult[]): void {
26
+ process.stdout.write("\nSummary:\n");
27
+ for (const r of results) {
28
+ const status = r.ok ? "ok" : "FAIL";
29
+ const findings = r.findings.length;
30
+ const outputs = r.outputs.length;
31
+ process.stdout.write(
32
+ ` [${status}] ${r.id.padEnd(14)} files:${outputs} findings:${findings} ${r.summary}\n`,
33
+ );
34
+ if (r.error) process.stdout.write(` error: ${r.error}\n`);
35
+ }
36
+ }
37
+
38
+ export async function writeSummaryJson(outDir: string, results: TaskResult[]): Promise<string> {
39
+ const reportsDir = path.join(outDir, "reports");
40
+ await mkdir(reportsDir, { recursive: true });
41
+ const out = path.join(reportsDir, "summary.json");
42
+ const body = {
43
+ ts: new Date().toISOString(),
44
+ results: results.map((r) => ({
45
+ id: r.id,
46
+ ok: r.ok,
47
+ summary: r.summary,
48
+ outputs: r.outputs.length,
49
+ findings: r.findings.length,
50
+ error: r.error,
51
+ })),
52
+ };
53
+ await Bun.write(out, `${JSON.stringify(body, null, 2)}\n`);
54
+ return out;
55
+ }
56
+
57
+ export async function writeInlineSummaries(
58
+ trees: ComponentTree[],
59
+ results: TaskResult[],
60
+ outDir: string,
61
+ ): Promise<string[]> {
62
+ // Keyed on filePath::componentName — the registry.ts dedupe convention.
63
+ // Keyed on the bare name, a/Header.tsx and b/Header.tsx collapse into one
64
+ // entry: the second file never gets its sidecar, and every finding routed to
65
+ // it is silently dropped from the report.
66
+ const byComponent = new Map<string, Finding[]>();
67
+ const components = new Map<string, ComponentTree>();
68
+ for (const t of trees) collectComponents(t, components);
69
+
70
+ for (const r of results) {
71
+ for (const f of r.findings) {
72
+ const owner = findOwner(f.file, components);
73
+ if (!owner) continue;
74
+ const list = byComponent.get(owner) ?? [];
75
+ list.push(f);
76
+ byComponent.set(owner, list);
77
+ }
78
+ }
79
+
80
+ const written: string[] = [];
81
+ for (const [key, tree] of components) {
82
+ const name = tree.entry.componentName;
83
+ const dir = path.dirname(tree.entry.filePath);
84
+ const out = path.join(dir, `${name}.flawless.json`);
85
+ const findings = byComponent.get(key) ?? [];
86
+ const counts: Record<TaskId | "total", number> = {
87
+ "perf-harness": 0,
88
+ "unit-browser": 0,
89
+ "perf-static": 0,
90
+ a11y: 0,
91
+ "react-rules": 0,
92
+ e2e: 0,
93
+ load: 0,
94
+ total: findings.length,
95
+ };
96
+ for (const f of findings) counts[f.source] += 1;
97
+ const top = [...findings]
98
+ .sort((a, b) => severityWeight(b.severity) - severityWeight(a.severity))
99
+ .slice(0, 3)
100
+ .map((f) => ({
101
+ rule: f.rule,
102
+ severity: f.severity,
103
+ line: f.line,
104
+ column: f.column,
105
+ message: f.message,
106
+ }));
107
+ // Every path here is relative to THIS file, not to the machine that wrote
108
+ // it. The sidecar lands beside the analysed source and is committed like any
109
+ // other generated file, so an absolute path in one names the developer who
110
+ // ran the tool — src/emitPaths.ts exists to stop exactly that, and this was
111
+ // the one emitted file its sentinel sweep did not cover. Resolve with
112
+ // path.resolve(dirname(<this file>), value).
113
+ const rel = (target: string): string => relativeToEmitted(dir, target);
114
+ const body = {
115
+ component: name,
116
+ file: rel(tree.entry.filePath),
117
+ counts,
118
+ top,
119
+ // a11y and react-rules are SWEEPS: each writes a single aggregate for the
120
+ // whole run, and only perf-static reports per component. These two used
121
+ // to be spelled `${name}.a11y.json` and `${name}.react-rules.json` —
122
+ // files no run has ever produced — so two thirds of this map pointed at
123
+ // nothing, and the published docs copied the wrong names from it.
124
+ reports: {
125
+ a11y: rel(path.join(outDir, "reports", A11Y_REPORT)),
126
+ reactRules: rel(path.join(outDir, "reports", REACT_RULES_REPORT)),
127
+ perfStatic: rel(path.join(outDir, "reports", `${name}.perf-static.json`)),
128
+ },
129
+ };
130
+ await Bun.write(out, `${JSON.stringify(body, null, 2)}\n`);
131
+ written.push(out);
132
+ }
133
+ return written;
134
+ }
135
+
136
+ /** filePath::componentName — the same key registry.ts flattens and dedupes on. */
137
+ function componentKey(entry: ComponentInfo): string {
138
+ return `${entry.filePath}::${entry.componentName}`;
139
+ }
140
+
141
+ function collectComponents(tree: ComponentTree, into: Map<string, ComponentTree>): void {
142
+ const key = componentKey(tree.entry);
143
+ if (!into.has(key)) into.set(key, tree);
144
+ for (const c of tree.children) collectComponents(c, into);
145
+ }
146
+
147
+ function findOwner(file: string, components: Map<string, ComponentTree>): string | undefined {
148
+ for (const [key, tree] of components) {
149
+ if (path.resolve(tree.entry.filePath) === path.resolve(file)) return key;
150
+ }
151
+ return undefined;
152
+ }
153
+
154
+ function severityWeight(s: "error" | "warn"): number {
155
+ return s === "error" ? 2 : 1;
156
+ }