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