@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
package/src/cli.ts
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
// core.ts (via the analyser) imports the TypeScript compiler — ~110ms of
|
|
2
|
+
// startup nobody running --help/--version should pay. Only type-only imports
|
|
3
|
+
// of that graph may appear here; the value imports of runCore and TASKS happen
|
|
4
|
+
// inside main(), after parseArgs and its fast paths have had their chance to
|
|
5
|
+
// exit. UsageError comes from types.ts, a leaf, precisely so this file never
|
|
6
|
+
// needs core.ts at load time.
|
|
7
|
+
import { FLAG_NAMES, renderHelp } from "./cliFlags.ts";
|
|
8
|
+
import type { CoreResult } from "./core.ts";
|
|
9
|
+
import { flawlessArgv, stripTrailingComment } from "./docLine.ts";
|
|
10
|
+
import { isMcpArgv, parseMcpCommand } from "./mcp/spec.ts";
|
|
11
|
+
import { confirmRunTests, outro, promptInteractive } from "./prompt.ts";
|
|
12
|
+
import { printSummaryTable } from "./reporters/findings.ts";
|
|
13
|
+
import {
|
|
14
|
+
isSkillsArgv,
|
|
15
|
+
parseSkillsCommand,
|
|
16
|
+
renderSkillsHelp,
|
|
17
|
+
type SkillsCommand,
|
|
18
|
+
} from "./skills/spec.ts";
|
|
19
|
+
import {
|
|
20
|
+
type CliArgs,
|
|
21
|
+
type HeapMode,
|
|
22
|
+
type PropsShape,
|
|
23
|
+
type RootMode,
|
|
24
|
+
type TaskId,
|
|
25
|
+
UsageError,
|
|
26
|
+
} from "./types.ts";
|
|
27
|
+
import { VERSION } from "./version.ts";
|
|
28
|
+
|
|
29
|
+
const VALID_TASK_IDS: ReadonlySet<TaskId> = new Set<TaskId>([
|
|
30
|
+
"perf-harness",
|
|
31
|
+
"unit-browser",
|
|
32
|
+
"perf-static",
|
|
33
|
+
"a11y",
|
|
34
|
+
"react-rules",
|
|
35
|
+
"e2e",
|
|
36
|
+
"load",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/** A closed set, so a typo fails at the CLI rather than silently profiling everything. */
|
|
40
|
+
function readHeapMode(raw: string | undefined): HeapMode {
|
|
41
|
+
if (raw === "off" || raw === "stress" || raw === "all") return raw;
|
|
42
|
+
throw new Error(`--heaps must be off, stress or all (got ${JSON.stringify(raw ?? "")})`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function parseArgs(argv: string[]): CliArgs {
|
|
46
|
+
let entry: string | undefined;
|
|
47
|
+
let out: string | undefined;
|
|
48
|
+
let depth = 3;
|
|
49
|
+
let variants = 5;
|
|
50
|
+
let heaps: HeapMode = "stress";
|
|
51
|
+
let changed: string | true | undefined;
|
|
52
|
+
let includeChildren = true;
|
|
53
|
+
let force = false;
|
|
54
|
+
let report = false;
|
|
55
|
+
let propsOverride: PropsShape | undefined;
|
|
56
|
+
let provider: string | undefined;
|
|
57
|
+
let apiSpec: string | undefined;
|
|
58
|
+
let tasks: TaskId[] | undefined;
|
|
59
|
+
let noInteractive = false;
|
|
60
|
+
let json = false;
|
|
61
|
+
let runTests = false;
|
|
62
|
+
let inlineSummary = true;
|
|
63
|
+
let nextjs: string | undefined;
|
|
64
|
+
let expo: string | undefined;
|
|
65
|
+
let appResolver: string | undefined;
|
|
66
|
+
let swaggerUrl: string | undefined;
|
|
67
|
+
let colocate = false;
|
|
68
|
+
let ci = false;
|
|
69
|
+
let roots: RootMode = "primary";
|
|
70
|
+
const exclude: string[] = [];
|
|
71
|
+
let useDefaultExcludes = true;
|
|
72
|
+
let help = false;
|
|
73
|
+
let version = false;
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < argv.length; i++) {
|
|
76
|
+
const a = argv[i];
|
|
77
|
+
if (a === undefined) continue;
|
|
78
|
+
if (a === "--out") {
|
|
79
|
+
const v = argv[++i];
|
|
80
|
+
if (v === undefined) throw new Error("--out requires a value");
|
|
81
|
+
out = v;
|
|
82
|
+
} else if (a.startsWith("--out=")) {
|
|
83
|
+
out = a.slice("--out=".length);
|
|
84
|
+
} else if (a === "--depth") {
|
|
85
|
+
const v = argv[++i];
|
|
86
|
+
if (v === undefined) throw new Error("--depth requires a value");
|
|
87
|
+
depth = Number(v);
|
|
88
|
+
} else if (a.startsWith("--depth=")) {
|
|
89
|
+
depth = Number(a.slice("--depth=".length));
|
|
90
|
+
} else if (a === "--changed") {
|
|
91
|
+
// Optional value: `--changed` alone means HEAD, `--changed main` a ref.
|
|
92
|
+
const next = argv[i + 1];
|
|
93
|
+
changed = next !== undefined && !next.startsWith("-") ? (argv[++i] as string) : true;
|
|
94
|
+
} else if (a.startsWith("--changed=")) {
|
|
95
|
+
changed = a.slice("--changed=".length) || true;
|
|
96
|
+
} else if (a === "--heaps") {
|
|
97
|
+
heaps = readHeapMode(argv[++i]);
|
|
98
|
+
} else if (a.startsWith("--heaps=")) {
|
|
99
|
+
heaps = readHeapMode(a.slice("--heaps=".length));
|
|
100
|
+
} else if (a === "--variants") {
|
|
101
|
+
const v = argv[++i];
|
|
102
|
+
if (v === undefined) throw new Error("--variants requires a value");
|
|
103
|
+
variants = Number(v);
|
|
104
|
+
} else if (a.startsWith("--variants=")) {
|
|
105
|
+
variants = Number(a.slice("--variants=".length));
|
|
106
|
+
} else if (a === "--include-children") {
|
|
107
|
+
includeChildren = true;
|
|
108
|
+
} else if (a.startsWith("--include-children=")) {
|
|
109
|
+
const v = a.slice("--include-children=".length).toLowerCase();
|
|
110
|
+
includeChildren = !(v === "false" || v === "0" || v === "no");
|
|
111
|
+
} else if (a === "--props") {
|
|
112
|
+
const v = argv[++i];
|
|
113
|
+
if (v === undefined) throw new Error("--props requires a JSON value");
|
|
114
|
+
propsOverride = parsePropsJson(v);
|
|
115
|
+
} else if (a.startsWith("--props=")) {
|
|
116
|
+
propsOverride = parsePropsJson(a.slice("--props=".length));
|
|
117
|
+
} else if (a === "--provider") {
|
|
118
|
+
const v = argv[++i];
|
|
119
|
+
if (v === undefined) throw new Error("--provider requires a file path");
|
|
120
|
+
provider = v;
|
|
121
|
+
} else if (a.startsWith("--provider=")) {
|
|
122
|
+
provider = a.slice("--provider=".length);
|
|
123
|
+
} else if (a === "--api") {
|
|
124
|
+
const v = argv[++i];
|
|
125
|
+
if (v === undefined) throw new Error("--api requires a file path");
|
|
126
|
+
apiSpec = v;
|
|
127
|
+
} else if (a.startsWith("--api=")) {
|
|
128
|
+
apiSpec = a.slice("--api=".length);
|
|
129
|
+
} else if (a === "--tasks") {
|
|
130
|
+
const v = argv[++i];
|
|
131
|
+
if (v === undefined) throw new Error("--tasks requires a csv value");
|
|
132
|
+
tasks = parseTaskList(v);
|
|
133
|
+
} else if (a.startsWith("--tasks=")) {
|
|
134
|
+
tasks = parseTaskList(a.slice("--tasks=".length));
|
|
135
|
+
} else if (a === "--nextjs") {
|
|
136
|
+
const peek = argv[i + 1];
|
|
137
|
+
if (peek !== undefined && !peek.startsWith("-")) {
|
|
138
|
+
nextjs = argv[++i] as string;
|
|
139
|
+
} else {
|
|
140
|
+
nextjs = ".";
|
|
141
|
+
}
|
|
142
|
+
} else if (a.startsWith("--nextjs=")) {
|
|
143
|
+
nextjs = a.slice("--nextjs=".length) || ".";
|
|
144
|
+
} else if (a === "--expo") {
|
|
145
|
+
const peek = argv[i + 1];
|
|
146
|
+
if (peek !== undefined && !peek.startsWith("-")) {
|
|
147
|
+
expo = argv[++i] as string;
|
|
148
|
+
} else {
|
|
149
|
+
expo = ".";
|
|
150
|
+
}
|
|
151
|
+
} else if (a.startsWith("--expo=")) {
|
|
152
|
+
expo = a.slice("--expo=".length) || ".";
|
|
153
|
+
} else if (a === "--app-resolver") {
|
|
154
|
+
const v = argv[++i];
|
|
155
|
+
if (v === undefined) throw new Error("--app-resolver requires a file path");
|
|
156
|
+
appResolver = v;
|
|
157
|
+
} else if (a.startsWith("--app-resolver=")) {
|
|
158
|
+
appResolver = a.slice("--app-resolver=".length);
|
|
159
|
+
} else if (a === "--swagger-url") {
|
|
160
|
+
const v = argv[++i];
|
|
161
|
+
if (v === undefined) throw new Error("--swagger-url requires a value");
|
|
162
|
+
swaggerUrl = v;
|
|
163
|
+
} else if (a.startsWith("--swagger-url=")) {
|
|
164
|
+
swaggerUrl = a.slice("--swagger-url=".length);
|
|
165
|
+
} else if (a === "--roots" || a.startsWith("--roots=")) {
|
|
166
|
+
const v = a.startsWith("--roots=") ? a.slice("--roots=".length) : argv[++i];
|
|
167
|
+
if (v !== "primary" && v !== "exported") {
|
|
168
|
+
throw new Error(`--roots must be "primary" or "exported", got: ${v ?? "<missing>"}`);
|
|
169
|
+
}
|
|
170
|
+
roots = v;
|
|
171
|
+
} else if (a === "--exclude" || a.startsWith("--exclude=")) {
|
|
172
|
+
const v = a.startsWith("--exclude=") ? a.slice("--exclude=".length) : argv[++i];
|
|
173
|
+
if (v === undefined) throw new Error("--exclude requires a glob");
|
|
174
|
+
exclude.push(v);
|
|
175
|
+
} else if (a === "--no-default-excludes") {
|
|
176
|
+
useDefaultExcludes = false;
|
|
177
|
+
} else if (a === "--colocate") {
|
|
178
|
+
colocate = true;
|
|
179
|
+
} else if (a === "--ci") {
|
|
180
|
+
ci = true;
|
|
181
|
+
noInteractive = true;
|
|
182
|
+
json = true;
|
|
183
|
+
} else if (a === "--no-interactive") {
|
|
184
|
+
noInteractive = true;
|
|
185
|
+
} else if (a === "--run-tests") {
|
|
186
|
+
runTests = true;
|
|
187
|
+
} else if (a === "--no-inline-summary") {
|
|
188
|
+
inlineSummary = false;
|
|
189
|
+
} else if (a === "--json") {
|
|
190
|
+
json = true;
|
|
191
|
+
} else if (a === "--force") {
|
|
192
|
+
force = true;
|
|
193
|
+
} else if (a === "--report") {
|
|
194
|
+
report = true;
|
|
195
|
+
} else if (a === "-h" || a === "--help") {
|
|
196
|
+
// `break`, not a return: --help used to process.exit(0) right here, so
|
|
197
|
+
// everything after it went unread and the post-loop validation never ran.
|
|
198
|
+
// Both are preserved deliberately — `--depth=-1 --help` printed help then,
|
|
199
|
+
// and prints help now, rather than becoming a usage error.
|
|
200
|
+
help = true;
|
|
201
|
+
break;
|
|
202
|
+
} else if (a === "-V" || a === "--version") {
|
|
203
|
+
version = true;
|
|
204
|
+
break;
|
|
205
|
+
} else if (a.startsWith("--")) {
|
|
206
|
+
throw new Error(`unknown flag: ${a}`);
|
|
207
|
+
} else if (entry === undefined) {
|
|
208
|
+
entry = a;
|
|
209
|
+
} else {
|
|
210
|
+
throw new Error(`unexpected positional: ${a}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!help && !version) {
|
|
215
|
+
// A project is one or the other. Checked here rather than in either detector
|
|
216
|
+
// so the message names the conflict instead of whichever ran first, and so
|
|
217
|
+
// it costs nothing on the --help path.
|
|
218
|
+
if (nextjs !== undefined && expo !== undefined) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
"--nextjs and --expo are mutually exclusive: a project is a Next.js app or an Expo app, not both.",
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (!Number.isFinite(depth) || depth < 0) throw new Error(`--depth must be >= 0`);
|
|
224
|
+
if (!Number.isFinite(variants) || variants < 0) throw new Error(`--variants must be >= 0`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
entry,
|
|
229
|
+
out,
|
|
230
|
+
depth,
|
|
231
|
+
variants,
|
|
232
|
+
heaps,
|
|
233
|
+
changed,
|
|
234
|
+
includeChildren,
|
|
235
|
+
force,
|
|
236
|
+
report,
|
|
237
|
+
propsOverride,
|
|
238
|
+
provider,
|
|
239
|
+
apiSpec,
|
|
240
|
+
tasks,
|
|
241
|
+
noInteractive,
|
|
242
|
+
json,
|
|
243
|
+
runTests,
|
|
244
|
+
inlineSummary,
|
|
245
|
+
nextjs,
|
|
246
|
+
expo,
|
|
247
|
+
appResolver,
|
|
248
|
+
swaggerUrl,
|
|
249
|
+
colocate,
|
|
250
|
+
ci,
|
|
251
|
+
roots,
|
|
252
|
+
exclude,
|
|
253
|
+
useDefaultExcludes,
|
|
254
|
+
help,
|
|
255
|
+
version,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** A raw JSON.parse error ("JSON Parse error: …") names no flag; keep the
|
|
260
|
+
* diagnostic but say which flag carried the bad value. */
|
|
261
|
+
function parsePropsJson(raw: string): PropsShape {
|
|
262
|
+
try {
|
|
263
|
+
return JSON.parse(raw) as PropsShape;
|
|
264
|
+
} catch (err) {
|
|
265
|
+
throw new Error(`--props is not valid JSON: ${(err as Error).message}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function parseTaskList(s: string): TaskId[] {
|
|
270
|
+
const parts = s
|
|
271
|
+
.split(",")
|
|
272
|
+
.map((p) => p.trim())
|
|
273
|
+
.filter(Boolean);
|
|
274
|
+
const ids: TaskId[] = [];
|
|
275
|
+
for (const p of parts) {
|
|
276
|
+
if (!VALID_TASK_IDS.has(p as TaskId)) {
|
|
277
|
+
throw new Error(`unknown task id: ${p}. Available: ${[...VALID_TASK_IDS].join(", ")}`);
|
|
278
|
+
}
|
|
279
|
+
ids.push(p as TaskId);
|
|
280
|
+
}
|
|
281
|
+
return ids;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export interface LineProblem {
|
|
285
|
+
line: string;
|
|
286
|
+
problem: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export interface CheckReport {
|
|
290
|
+
/** flawless-react invocations examined, whether or not they parsed. */
|
|
291
|
+
checked: number;
|
|
292
|
+
/** Lines that invoke something else. Not a failure, but see the note below. */
|
|
293
|
+
skipped: number;
|
|
294
|
+
problems: LineProblem[];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Parse every documented flawless-react invocation in `text` without running it.
|
|
299
|
+
*
|
|
300
|
+
* The lint for shipped examples. Parsing is pure — parseArgs opens no file and
|
|
301
|
+
* spawns nothing — so an example naming ./src/Dashboard.tsx or a seeded database
|
|
302
|
+
* checks clean on a machine that has neither.
|
|
303
|
+
*
|
|
304
|
+
* `checked === 0` is reported, never smoothed over. A run that examined nothing
|
|
305
|
+
* and said "ok" is the exact false pass this tool exists to eliminate, and it is
|
|
306
|
+
* the likeliest failure of a doc lint: the extractor breaks, no line survives to
|
|
307
|
+
* be checked, and the gate stays green while the docs rot behind it.
|
|
308
|
+
*/
|
|
309
|
+
export function checkLines(text: string): CheckReport {
|
|
310
|
+
const report: CheckReport = { checked: 0, skipped: 0, problems: [] };
|
|
311
|
+
for (const raw of text.split("\n")) {
|
|
312
|
+
const line = stripTrailingComment(raw).trim();
|
|
313
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
314
|
+
let argv: string[] | null;
|
|
315
|
+
try {
|
|
316
|
+
argv = flawlessArgv(line);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
report.checked++;
|
|
319
|
+
report.problems.push({ line, problem: (err as Error).message });
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (argv === null) {
|
|
323
|
+
report.skipped++;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
report.checked++;
|
|
327
|
+
try {
|
|
328
|
+
// Two other grammars, neither of which parseArgs speaks: `skills …` is a
|
|
329
|
+
// subcommand and `--flags` a bare switch. Left to parseArgs, a working
|
|
330
|
+
// `flawless-react skills install --all` would be reported broken (its verb
|
|
331
|
+
// read as an entry glob) and a stale flag in prose would pass uncaught —
|
|
332
|
+
// both directions of the same silence. `mcp …` is a third: without it a
|
|
333
|
+
// documented `flawless-react mcp` line passes as an entry glob named "mcp",
|
|
334
|
+
// which is the wrong-reason pass skills/spec.ts exists to prevent.
|
|
335
|
+
if (isSkillsArgv(argv)) parseSkillsCommand(argv.slice(1));
|
|
336
|
+
else if (isMcpArgv(argv)) parseMcpCommand(argv.slice(1));
|
|
337
|
+
else if (argv[0] === "--flags") {
|
|
338
|
+
if (argv.length > 1) throw new Error("--flags takes no arguments");
|
|
339
|
+
} else parseArgs(argv);
|
|
340
|
+
} catch (err) {
|
|
341
|
+
report.problems.push({ line, problem: (err as Error).message });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return report;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export async function main(argv: string[]): Promise<void> {
|
|
348
|
+
// The skills subcommand, ahead of parseArgs for the same reason as the two
|
|
349
|
+
// surfaces below it: `skills` is not an entry glob, and falling through would
|
|
350
|
+
// mean parsing it as one. The bytes stay behind a dynamic import — the grammar
|
|
351
|
+
// here costs a few hundred bytes, the ~70 KB of embedded SKILL.md comes in only
|
|
352
|
+
// once a verb is actually being run.
|
|
353
|
+
if (isSkillsArgv(argv)) {
|
|
354
|
+
let cmd: SkillsCommand;
|
|
355
|
+
try {
|
|
356
|
+
cmd = parseSkillsCommand(argv.slice(1));
|
|
357
|
+
} catch (err) {
|
|
358
|
+
process.stderr.write(`${(err as Error).message}\n`);
|
|
359
|
+
process.exit(2);
|
|
360
|
+
}
|
|
361
|
+
if (cmd.verb === "help") {
|
|
362
|
+
process.stdout.write(renderSkillsHelp());
|
|
363
|
+
process.exit(0);
|
|
364
|
+
}
|
|
365
|
+
const { runSkillsCommand } = await import("./skills/cli.ts");
|
|
366
|
+
process.exit(await runSkillsCommand(cmd));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// The MCP server, dispatched for the same reason as `skills` above: `mcp` is a
|
|
370
|
+
// subcommand, not an entry glob. Falling through would start a generation run
|
|
371
|
+
// over a project called "mcp" — and the docs lint would still call that line
|
|
372
|
+
// clean. The whole server (the analyser, the rules, the child plane) stays
|
|
373
|
+
// behind a dynamic import so `--help` and `--check` never pay for the TypeScript
|
|
374
|
+
// compiler it reaches through (FR-031); only a host that actually speaks the
|
|
375
|
+
// protocol loads it.
|
|
376
|
+
if (isMcpArgv(argv)) {
|
|
377
|
+
const { runMcpCli } = await import("./mcp/server.ts");
|
|
378
|
+
process.exit(await runMcpCli(argv.slice(1)));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Two parse-only surfaces, ahead of parseArgs and well ahead of the core
|
|
382
|
+
// graph. They exist for consumers that CANNOT import this module: the website
|
|
383
|
+
// repo keeps hand-copied examples and a "every flag flawless-react accepts"
|
|
384
|
+
// table, and its CI can run the published binary even though it can have no
|
|
385
|
+
// opinion about this source tree. Both must stay clear of core.ts, or --help
|
|
386
|
+
// and --check start paying for the TypeScript compiler (FR-031).
|
|
387
|
+
if (argv[0] === "--flags") {
|
|
388
|
+
if (argv.length > 1) {
|
|
389
|
+
process.stderr.write("--flags takes no arguments\n");
|
|
390
|
+
process.exit(2);
|
|
391
|
+
}
|
|
392
|
+
process.stdout.write(`${FLAG_NAMES.join("\n")}\n`);
|
|
393
|
+
process.exit(0);
|
|
394
|
+
}
|
|
395
|
+
if (argv[0] === "--check") {
|
|
396
|
+
if (argv.length < 2) {
|
|
397
|
+
process.stderr.write("--check requires a line to check\n");
|
|
398
|
+
process.exit(2);
|
|
399
|
+
}
|
|
400
|
+
// Several operands are joined rather than rejected: a caller feeding a whole
|
|
401
|
+
// doc page one block per argument is the normal shape, and an unquoted line
|
|
402
|
+
// would otherwise be silently truncated to its first word.
|
|
403
|
+
const report = checkLines(argv.slice(1).join("\n"));
|
|
404
|
+
for (const p of report.problems) {
|
|
405
|
+
process.stderr.write(`${p.problem}\n in: ${p.line}\n`);
|
|
406
|
+
}
|
|
407
|
+
if (report.problems.length > 0) process.exit(1);
|
|
408
|
+
if (report.checked === 0) {
|
|
409
|
+
process.stderr.write(
|
|
410
|
+
`--check found no flawless-react invocation in ${report.skipped} line(s)\n`,
|
|
411
|
+
);
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
process.stdout.write(
|
|
415
|
+
`ok: ${report.checked} invocation(s) parse (${report.skipped} line(s) skipped)\n`,
|
|
416
|
+
);
|
|
417
|
+
process.exit(0);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let args: CliArgs;
|
|
421
|
+
try {
|
|
422
|
+
args = parseArgs(argv);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
process.stderr.write(`${(err as Error).message}\n`);
|
|
425
|
+
process.exit(2);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Printed here rather than inside parseArgs, so that parsing a documented
|
|
429
|
+
// line stays a pure function call. It used to process.exit(0) mid-loop, which
|
|
430
|
+
// meant no test — and no lint — could parse `flawless-react --help` in-process.
|
|
431
|
+
if (args.help) {
|
|
432
|
+
process.stdout.write(renderHelp());
|
|
433
|
+
process.exit(0);
|
|
434
|
+
}
|
|
435
|
+
if (args.version) {
|
|
436
|
+
process.stdout.write(`flawless-react ${VERSION}\n`);
|
|
437
|
+
process.exit(0);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
let pattern = args.entry;
|
|
441
|
+
let outDir = args.out;
|
|
442
|
+
let taskIds = args.tasks;
|
|
443
|
+
|
|
444
|
+
const wantInteractive = !args.noInteractive && pattern === undefined && process.stdin.isTTY;
|
|
445
|
+
if (wantInteractive) {
|
|
446
|
+
// Deferred with core.ts: tasks/index.ts pulls every task module, and only
|
|
447
|
+
// the interactive menu needs the registry before runCore does.
|
|
448
|
+
const { TASKS } = await import("./tasks/index.ts");
|
|
449
|
+
const picked = await promptInteractive({
|
|
450
|
+
taskChoices: TASKS.map((t) => ({ id: t.id, label: t.label, description: t.description })),
|
|
451
|
+
defaultTaskIds: ["perf-harness", "perf-static"],
|
|
452
|
+
});
|
|
453
|
+
if (!picked) {
|
|
454
|
+
process.exit(0);
|
|
455
|
+
}
|
|
456
|
+
pattern = picked.pattern;
|
|
457
|
+
outDir = picked.outDir;
|
|
458
|
+
taskIds = picked.tasks;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (pattern === undefined && args.nextjs === undefined && args.expo === undefined) {
|
|
462
|
+
process.stderr.write(
|
|
463
|
+
"missing entry path or glob. Pass a .tsx path, a glob, or run in a TTY for the interactive menu. See --help.\n",
|
|
464
|
+
);
|
|
465
|
+
process.exit(2);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// First value-touch of the core graph (and with it the TypeScript compiler):
|
|
469
|
+
// every exit path above must stay reachable without paying for it.
|
|
470
|
+
const { runCore } = await import("./core.ts");
|
|
471
|
+
|
|
472
|
+
let result: CoreResult;
|
|
473
|
+
try {
|
|
474
|
+
result = await runCore({
|
|
475
|
+
pattern: pattern as string,
|
|
476
|
+
outDir,
|
|
477
|
+
taskIds,
|
|
478
|
+
depth: args.depth,
|
|
479
|
+
variants: args.variants,
|
|
480
|
+
heaps: args.heaps,
|
|
481
|
+
...(args.changed === undefined ? {} : { changed: args.changed }),
|
|
482
|
+
includeChildren: args.includeChildren,
|
|
483
|
+
force: args.force,
|
|
484
|
+
report: args.report,
|
|
485
|
+
propsOverride: args.propsOverride,
|
|
486
|
+
provider: args.provider ?? args.appResolver,
|
|
487
|
+
apiSpec: args.apiSpec,
|
|
488
|
+
inlineSummary: args.inlineSummary,
|
|
489
|
+
nextjs: args.nextjs,
|
|
490
|
+
expo: args.expo,
|
|
491
|
+
swaggerUrl: args.swaggerUrl,
|
|
492
|
+
colocate: args.colocate,
|
|
493
|
+
roots: args.roots,
|
|
494
|
+
exclude: args.exclude,
|
|
495
|
+
useDefaultExcludes: args.useDefaultExcludes,
|
|
496
|
+
// --json (and --ci, which implies it) asks for a machine-readable run, so
|
|
497
|
+
// the progress and finding lines have to stay off stdout or the blob is
|
|
498
|
+
// preceded by prose and the whole thing fails to parse.
|
|
499
|
+
log: !args.json,
|
|
500
|
+
runTests: args.runTests || (wantInteractive && (await confirmRunTests())),
|
|
501
|
+
});
|
|
502
|
+
} catch (err) {
|
|
503
|
+
process.stderr.write(`${(err as Error).message}\n`);
|
|
504
|
+
process.exit(err instanceof UsageError ? 2 : 1);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (!args.json) printSummaryTable(result.results);
|
|
508
|
+
if (args.json) {
|
|
509
|
+
// exitCode rides inside the blob: suite failures under --run-tests fold
|
|
510
|
+
// into it, and a --ci consumer parsing stdout alone must see them without
|
|
511
|
+
// shell plumbing around $?.
|
|
512
|
+
//
|
|
513
|
+
// testRun and errors are what make the exit code EXPLICABLE rather than
|
|
514
|
+
// merely correct. Without testRun, exit 0 conflates "the suites passed"
|
|
515
|
+
// with "no suite ran" (B-14); without errors, a failure belonging to no
|
|
516
|
+
// task gives exit 1 with every result ok:true and no reason anywhere
|
|
517
|
+
// (FR-041). Both are omitted from the JSON when empty/absent — `testRun`
|
|
518
|
+
// because absent genuinely means "no run was asked for".
|
|
519
|
+
process.stdout.write(
|
|
520
|
+
`${JSON.stringify(
|
|
521
|
+
{
|
|
522
|
+
exitCode: result.exitCode,
|
|
523
|
+
outDir: result.outDir,
|
|
524
|
+
results: result.results,
|
|
525
|
+
keptScripts: result.keptScripts,
|
|
526
|
+
testRun: result.testRun,
|
|
527
|
+
...(result.errors.length > 0 ? { errors: result.errors } : {}),
|
|
528
|
+
},
|
|
529
|
+
null,
|
|
530
|
+
2,
|
|
531
|
+
)}\n`,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
if (wantInteractive) {
|
|
535
|
+
// The skills question, asked here rather than never: an interactive run is a
|
|
536
|
+
// person who has just watched the tool work, with a terminal in front of them.
|
|
537
|
+
// It goes BEFORE the outro because clack owns the session, and the gate is the
|
|
538
|
+
// project having no `flawless-*` skill yet — so answering it is what stops it
|
|
539
|
+
// being asked, with no marker file trailing the user. npm's postinstall asks
|
|
540
|
+
// the same question at install time; `FLAWLESS_NO_SKILLS` silences both.
|
|
541
|
+
const { defaultSkillsIO, offerSkills } = await import("./skills/cli.ts");
|
|
542
|
+
await offerSkills(defaultSkillsIO());
|
|
543
|
+
await outro(result.exitCode !== 0 ? "done (with failures)" : "done");
|
|
544
|
+
}
|
|
545
|
+
process.exit(result.exitCode);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Harmless when imported (bin.ts, the compiled binary, tests) and the
|
|
549
|
+
// difference between a run and a silent exit-0 no-op when someone invokes
|
|
550
|
+
// `bun src/cli.ts …` directly — the trap bin.ts's header documents.
|
|
551
|
+
if (import.meta.main) {
|
|
552
|
+
try {
|
|
553
|
+
await main(Bun.argv.slice(2));
|
|
554
|
+
} catch (err) {
|
|
555
|
+
// A rejection escaping main() must land on the documented exit-1 contract,
|
|
556
|
+
// not surface as the runtime's uncaught-error dump.
|
|
557
|
+
process.stderr.write(`${String((err as Error | undefined)?.stack ?? err)}\n`);
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
}
|