@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,67 @@
|
|
|
1
|
+
import type { RunContext, Task, TaskResult } from "../types.ts";
|
|
2
|
+
import { runBiomeSweep } from "./biomeSweep.ts";
|
|
3
|
+
|
|
4
|
+
/** One aggregate for the whole sweep — see A11Y_REPORT. */
|
|
5
|
+
export const REACT_RULES_REPORT = "react-rules.json";
|
|
6
|
+
|
|
7
|
+
export const eslintReactRules: Task = {
|
|
8
|
+
id: "react-rules",
|
|
9
|
+
label: "Functionally important React rules (Biome)",
|
|
10
|
+
description: "Curated bug-catching React + hooks rules via Biome",
|
|
11
|
+
needsTree: true,
|
|
12
|
+
async run(ctx: RunContext): Promise<TaskResult> {
|
|
13
|
+
return runBiomeSweep({
|
|
14
|
+
id: "react-rules",
|
|
15
|
+
ctx,
|
|
16
|
+
rules: reactRules(),
|
|
17
|
+
aggregateFile: REACT_RULES_REPORT,
|
|
18
|
+
note: "Curated React correctness rules via Biome. Local biome.json is bypassed.",
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function reactRules(): unknown {
|
|
24
|
+
return {
|
|
25
|
+
recommended: false,
|
|
26
|
+
correctness: {
|
|
27
|
+
noChildrenProp: "error",
|
|
28
|
+
noConstAssign: "error",
|
|
29
|
+
noConstantCondition: "error",
|
|
30
|
+
noEmptyPattern: "error",
|
|
31
|
+
noInvalidUseBeforeDeclaration: "error",
|
|
32
|
+
noPrecisionLoss: "error",
|
|
33
|
+
noRenderReturnValue: "error",
|
|
34
|
+
noVoidElementsWithChildren: "error",
|
|
35
|
+
noVoidTypeReturn: "error",
|
|
36
|
+
useExhaustiveDependencies: "error",
|
|
37
|
+
useHookAtTopLevel: "error",
|
|
38
|
+
useIsNan: "error",
|
|
39
|
+
useJsxKeyInIterable: "error",
|
|
40
|
+
useValidForDirection: "error",
|
|
41
|
+
},
|
|
42
|
+
suspicious: {
|
|
43
|
+
noArrayIndexKey: "warn",
|
|
44
|
+
noAssignInExpressions: "error",
|
|
45
|
+
noAsyncPromiseExecutor: "error",
|
|
46
|
+
noCompareNegZero: "error",
|
|
47
|
+
noConfusingVoidType: "warn",
|
|
48
|
+
noControlCharactersInRegex: "error",
|
|
49
|
+
noDoubleEquals: "warn",
|
|
50
|
+
noDuplicateCase: "error",
|
|
51
|
+
noDuplicateClassMembers: "error",
|
|
52
|
+
noDuplicateJsxProps: "error",
|
|
53
|
+
noDuplicateObjectKeys: "error",
|
|
54
|
+
noDuplicateParameters: "error",
|
|
55
|
+
noEmptyBlockStatements: "warn",
|
|
56
|
+
noExplicitAny: "warn",
|
|
57
|
+
noExtraNonNullAssertion: "warn",
|
|
58
|
+
noFallthroughSwitchClause: "error",
|
|
59
|
+
noGlobalIsFinite: "error",
|
|
60
|
+
noGlobalIsNan: "error",
|
|
61
|
+
noImplicitAnyLet: "warn",
|
|
62
|
+
noLabelVar: "error",
|
|
63
|
+
noMisleadingCharacterClass: "error",
|
|
64
|
+
noMisleadingInstantiator: "error",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Task, TaskId } from "../types.ts";
|
|
2
|
+
import { a11yScan } from "./a11yScan.ts";
|
|
3
|
+
import { e2eRoutes } from "./e2eRoutes.ts";
|
|
4
|
+
import { eslintReactRules } from "./eslintReactRules.ts";
|
|
5
|
+
import { loadPipes } from "./loadPipes.ts";
|
|
6
|
+
import { perfHarness } from "./perfHarness.ts";
|
|
7
|
+
import { staticPerfAnalysis } from "./staticPerfAnalysis.ts";
|
|
8
|
+
import { unitAndBrowser } from "./unitAndBrowser.ts";
|
|
9
|
+
|
|
10
|
+
export const TASKS: Task[] = [
|
|
11
|
+
perfHarness,
|
|
12
|
+
unitAndBrowser,
|
|
13
|
+
staticPerfAnalysis,
|
|
14
|
+
a11yScan,
|
|
15
|
+
eslintReactRules,
|
|
16
|
+
e2eRoutes,
|
|
17
|
+
loadPipes,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function findTask(id: TaskId): Task | undefined {
|
|
21
|
+
return TASKS.find((t) => t.id === id);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const ALL_TASK_IDS: TaskId[] = TASKS.map((t) => t.id);
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { outDirRel, underOutDir } from "../emitPaths.ts";
|
|
5
|
+
import type { ComponentTree, ExpoContext, RunContext, TaskResult } from "../types.ts";
|
|
6
|
+
import { ensureParamDefaults } from "./e2eRoutes.ts";
|
|
7
|
+
import {
|
|
8
|
+
countPipes,
|
|
9
|
+
describeTally,
|
|
10
|
+
type LoadTarget,
|
|
11
|
+
loadEnvScript,
|
|
12
|
+
mergePipes,
|
|
13
|
+
readFilledParams,
|
|
14
|
+
renderPipes,
|
|
15
|
+
slugify,
|
|
16
|
+
} from "./loadPipes.ts";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* API-only crust load pipes for an Expo app — B-16 wave 7, the sibling of
|
|
20
|
+
* loadPipes.ts (one TaskId, one engine per platform).
|
|
21
|
+
*
|
|
22
|
+
* An Expo app has no server-rendered page routes to GET, so the page-route half
|
|
23
|
+
* of the Next flavour drops away and the pipes are the GET endpoints the screens
|
|
24
|
+
* reach through the app's fetch wrapper (ComponentInfo.apiCalls, harvested one
|
|
25
|
+
* hop at analysis time). Everything downstream is shared verbatim — the pipe
|
|
26
|
+
* body, the merge-kept routes.pipes, params.json, env.sh — because none of it is
|
|
27
|
+
* platform-specific. Only two things differ and both are here: what a target IS
|
|
28
|
+
* (an api endpoint, not a page), and how the auth probe knows the session is
|
|
29
|
+
* dead (a 401 from the API, not a redirect to a login page).
|
|
30
|
+
*/
|
|
31
|
+
export async function runExpoLoad(ctx: RunContext): Promise<TaskResult> {
|
|
32
|
+
const expo = ctx.expo as ExpoContext;
|
|
33
|
+
const loadDir = path.join(ctx.outDir, "load");
|
|
34
|
+
await mkdir(loadDir, { recursive: true });
|
|
35
|
+
const written: string[] = [];
|
|
36
|
+
|
|
37
|
+
const targets = collectExpoTargets(ctx.trees);
|
|
38
|
+
const rel = outDirRel(ctx.outDir, expo.root);
|
|
39
|
+
|
|
40
|
+
// A run that harvested nothing is reported as such, not as an empty success:
|
|
41
|
+
// 0 endpoints on an app that makes API calls means the harvest could not see
|
|
42
|
+
// through its client, which the reader needs to know.
|
|
43
|
+
if (targets.length === 0) {
|
|
44
|
+
return {
|
|
45
|
+
id: "load",
|
|
46
|
+
ok: true,
|
|
47
|
+
outputs: [],
|
|
48
|
+
findings: [],
|
|
49
|
+
summary:
|
|
50
|
+
"load: no GET endpoints harvested — the screens reach no api function whose body " +
|
|
51
|
+
"calls a fetch wrapper with a literal path (calls mediated by a store, or a non-literal URL, are not traced)",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const paramsPath = path.join(ctx.outDir, "e2e", "params.json");
|
|
56
|
+
const needed = [...new Set(targets.flatMap((t) => t.params))].sort();
|
|
57
|
+
if (await ensureParamDefaults(paramsPath, needed)) written.push(paramsPath);
|
|
58
|
+
const filled = await readFilledParams(paramsPath);
|
|
59
|
+
|
|
60
|
+
const pipesPath = path.join(loadDir, "routes.pipes");
|
|
61
|
+
let keptPipes = false;
|
|
62
|
+
let pipesText: string;
|
|
63
|
+
if (!existsSync(pipesPath)) {
|
|
64
|
+
pipesText = renderPipes(targets, ctx.outDir, expo.root, filled);
|
|
65
|
+
await Bun.write(pipesPath, pipesText);
|
|
66
|
+
written.push(pipesPath);
|
|
67
|
+
} else if (ctx.force) {
|
|
68
|
+
pipesText = mergePipes(
|
|
69
|
+
await Bun.file(pipesPath).text(),
|
|
70
|
+
targets,
|
|
71
|
+
ctx.outDir,
|
|
72
|
+
expo.root,
|
|
73
|
+
filled,
|
|
74
|
+
);
|
|
75
|
+
await Bun.write(pipesPath, pipesText);
|
|
76
|
+
written.push(pipesPath);
|
|
77
|
+
} else {
|
|
78
|
+
pipesText = await Bun.file(pipesPath).text();
|
|
79
|
+
keptPipes = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const setupPath = path.join(loadDir, "routes.setup.ts");
|
|
83
|
+
if (!existsSync(setupPath)) {
|
|
84
|
+
await Bun.write(setupPath, renderExpoLoadSetup(targets, rel));
|
|
85
|
+
written.push(setupPath);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const envPath = path.join(loadDir, "env.sh");
|
|
89
|
+
await Bun.write(envPath, loadEnvScript(rel));
|
|
90
|
+
written.push(envPath);
|
|
91
|
+
|
|
92
|
+
const readmePath = path.join(loadDir, "README.md");
|
|
93
|
+
if (!existsSync(readmePath) || ctx.force) {
|
|
94
|
+
await Bun.write(readmePath, expoLoadReadme());
|
|
95
|
+
written.push(readmePath);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const crustPresent = Bun.which("crust") !== null;
|
|
99
|
+
const tally = countPipes(pipesText);
|
|
100
|
+
return {
|
|
101
|
+
id: "load",
|
|
102
|
+
ok: true,
|
|
103
|
+
outputs: written,
|
|
104
|
+
findings: [],
|
|
105
|
+
summary: keptPipes
|
|
106
|
+
? `kept existing load/routes.pipes (--force to merge in ${targets.length} API target(s))`
|
|
107
|
+
: `load pipes for ${targets.length} API endpoint(s) — ${describeTally(tally, filled, targets)}${
|
|
108
|
+
crustPresent ? " — run with bun run flawless:load" : " — crust not found on PATH"
|
|
109
|
+
}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The GET endpoints the screens hit, as load targets. Mutations are excluded by
|
|
115
|
+
* default (load-testing a POST writes junk into the target DB); the dynamic
|
|
116
|
+
* segments become $LOAD_<PARAM> exactly as the Next flavour renders a page
|
|
117
|
+
* route's, so the shared pipe/param machinery cannot tell the two apart.
|
|
118
|
+
*/
|
|
119
|
+
export function collectExpoTargets(trees: ComponentTree[]): LoadTarget[] {
|
|
120
|
+
const out: LoadTarget[] = [];
|
|
121
|
+
const seen = new Set<string>();
|
|
122
|
+
const usedNames = new Set<string>();
|
|
123
|
+
const walk = (t: ComponentTree): void => {
|
|
124
|
+
for (const f of t.entry.apiCalls ?? []) {
|
|
125
|
+
if (f.method !== "GET") continue;
|
|
126
|
+
let p = f.urlTemplate;
|
|
127
|
+
for (const hole of f.holes) p = p.replace(`:${hole}`, `$LOAD_${hole.toUpperCase()}`);
|
|
128
|
+
if (seen.has(p)) continue;
|
|
129
|
+
seen.add(p);
|
|
130
|
+
let name = slugify(p);
|
|
131
|
+
// slugify collapses every $VAR to `x`, so two distinct endpoints can
|
|
132
|
+
// slug to one name and would then share one last-*.json — the merge keys
|
|
133
|
+
// on that name and the second would never emit. First claimant keeps it.
|
|
134
|
+
let unique = name;
|
|
135
|
+
for (let n = 2; usedNames.has(unique); n++) unique = `${name}-${n}`;
|
|
136
|
+
name = unique;
|
|
137
|
+
usedNames.add(name);
|
|
138
|
+
out.push({ name, urlPath: p, params: f.holes });
|
|
139
|
+
}
|
|
140
|
+
for (const c of t.children) walk(c);
|
|
141
|
+
};
|
|
142
|
+
for (const tree of trees) walk(tree);
|
|
143
|
+
// Stable order so regeneration is byte-deterministic — endpoints are
|
|
144
|
+
// harvested in screen-analysis order, which is not sorted.
|
|
145
|
+
out.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The parameterless GET endpoints, which the auth probe can hit without a filled id. */
|
|
150
|
+
function probeTargets(targets: LoadTarget[]): string[] {
|
|
151
|
+
return targets.filter((t) => t.params.length === 0).map((t) => t.urlPath);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The setup module crust imports before any pipe runs — the Expo-web variant.
|
|
156
|
+
*
|
|
157
|
+
* The Next flavour probes for a REDIRECT to a login page (crust follows
|
|
158
|
+
* redirects, so a dead session silently measures the login form at 200). An API
|
|
159
|
+
* endpoint does not redirect: an expired LOAD_AUTH gets a 401 at the SAME url,
|
|
160
|
+
* which the redirect check would pass. So this probes a parameterless GET
|
|
161
|
+
* endpoint and fails on a 401/403 — the sharper signal the redirect check could
|
|
162
|
+
* never see, and exactly the "an authenticated /api/me answering 401" case the
|
|
163
|
+
* Next setup's own comment wishes for.
|
|
164
|
+
*
|
|
165
|
+
* WRITE-ONCE, like every setup stub: a hand-written check that knows the app is
|
|
166
|
+
* better than this one, and no regeneration overwrites it.
|
|
167
|
+
*/
|
|
168
|
+
export function renderExpoLoadSetup(targets: LoadTarget[], rel: string): string {
|
|
169
|
+
const probes = probeTargets(targets);
|
|
170
|
+
return `// Setup module for routes.pipes (Expo, API-only). test-pipes imports the
|
|
171
|
+
// sibling <name>.setup.ts and awaits its default export before any line runs.
|
|
172
|
+
//
|
|
173
|
+
// Why an API-specific check: the Next.js flavour probes for a redirect to a
|
|
174
|
+
// login page, because a dead cookie makes a page route serve /login at 200. An
|
|
175
|
+
// API endpoint does not redirect — an expired LOAD_AUTH answers 401 at the same
|
|
176
|
+
// URL — so a redirect check would pass on a dead session. This fails on the 401
|
|
177
|
+
// instead, before a single measurement.
|
|
178
|
+
//
|
|
179
|
+
// This file is written once and never regenerated. Replace it with a check that
|
|
180
|
+
// knows your app if you have a sharper one; nothing will overwrite yours.
|
|
181
|
+
|
|
182
|
+
/** Parameterless GET endpoints — hittable without a filled id, so they make honest auth probes. */
|
|
183
|
+
const PROBES: string[] = ${JSON.stringify(probes)};
|
|
184
|
+
|
|
185
|
+
export default async () => {
|
|
186
|
+
const base = process.env.LOAD_BASE;
|
|
187
|
+
const auth = process.env.LOAD_AUTH;
|
|
188
|
+
if (!base || !auth) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
"routes.setup: LOAD_BASE and LOAD_AUTH are required — export them or use \`crust --env-file ${underOutDir(rel, "load/.env.load")}\`",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const sep = auth.indexOf(":");
|
|
194
|
+
if (sep < 0) {
|
|
195
|
+
throw new Error(\`routes.setup: LOAD_AUTH must be a header line, e.g. "authorization: Bearer <token>" or "cookie: session=…" — got \${auth}\`);
|
|
196
|
+
}
|
|
197
|
+
const headers = { [auth.slice(0, sep).trim()]: auth.slice(sep + 1).trim() };
|
|
198
|
+
|
|
199
|
+
// Seed LOAD_<PARAM> from params.json (the e2e task's fixture), so a dynamic
|
|
200
|
+
// pipe requests a real record rather than a URL with an empty segment.
|
|
201
|
+
const paramsFile = Bun.file("${underOutDir(rel, "e2e/params.json")}");
|
|
202
|
+
if (await paramsFile.exists()) {
|
|
203
|
+
const params = (await paramsFile.json()) as {
|
|
204
|
+
defaults?: Record<string, string>;
|
|
205
|
+
routes?: Record<string, Record<string, string>>;
|
|
206
|
+
};
|
|
207
|
+
const merged: Record<string, string> = { ...(params.defaults ?? {}) };
|
|
208
|
+
for (const route of Object.values(params.routes ?? {})) {
|
|
209
|
+
for (const [k, v] of Object.entries(route)) if (v !== "") merged[k] = v;
|
|
210
|
+
}
|
|
211
|
+
for (const [k, v] of Object.entries(merged)) {
|
|
212
|
+
if (v !== "") process.env[\`LOAD_\${k.toUpperCase()}\`] = v;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (PROBES.length === 0) {
|
|
217
|
+
// Every endpoint is parameterised; nothing can be probed without trusting a
|
|
218
|
+
// fixture id, and a wrong id reads as an auth failure. The pipes' own
|
|
219
|
+
// \`expect 200\` is the only gate left — noted, not silently skipped.
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const route of PROBES) {
|
|
224
|
+
const res = await fetch(new URL(route, base), { headers });
|
|
225
|
+
if (res.status === 401 || res.status === 403) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
\`routes.setup: \${route} answered \${res.status} — LOAD_AUTH does not authenticate. \` +
|
|
228
|
+
\`Every pipe below would measure the API's auth rejection, not the endpoint. Refresh the token/cookie.\`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
if (res.status >= 500) {
|
|
232
|
+
throw new Error(\`routes.setup: \${route} answered \${res.status} — the API is unhealthy; fix it before load-testing.\`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function expoLoadReadme(): string {
|
|
240
|
+
return `# flawless-react load pipes (Expo, API-only)
|
|
241
|
+
|
|
242
|
+
Generated crust pipelines for the GET API endpoints the app's screens reach
|
|
243
|
+
through its fetch wrapper (\`lib/api\`). An Expo app has no server-rendered page
|
|
244
|
+
routes, so — unlike the Next.js flavour — these pipes are API endpoints only.
|
|
245
|
+
Mutating endpoints are excluded; add them by hand against a safe target.
|
|
246
|
+
|
|
247
|
+
Run: \`bun run flawless:load\` (crust must be installed — see the repo's crust
|
|
248
|
+
setup or https://github.com/lariocpt/crust).
|
|
249
|
+
|
|
250
|
+
- \`LOAD_BASE\` — the API origin, e.g. \`http://localhost:3001\`. Point it at a
|
|
251
|
+
real API (a production build for real numbers), NOT at Metro: \`expo start
|
|
252
|
+
--web\` serves the UI, not the endpoints these pipes hit.
|
|
253
|
+
- \`LOAD_AUTH\` — a header line. On React Native the app authenticates with a
|
|
254
|
+
bearer token (\`authorization: Bearer <token>\`); on its web build, a cookie
|
|
255
|
+
(\`cookie: session=…\`). Either works — the API accepts both. \`routes.setup.ts\`
|
|
256
|
+
fails loudly if it no longer authenticates (a dead token answers 401, and the
|
|
257
|
+
pipes would otherwise measure the rejection).
|
|
258
|
+
- \`LOAD_<PARAM>\` — one per dynamic segment (\`LOAD_BUILDINGID\`, \`LOAD_ID\`,
|
|
259
|
+
\`LOAD_INVOICEID\`). \`routes.setup.ts\` seeds these from \`../e2e/params.json\`;
|
|
260
|
+
fill them there (the same ids the e2e task uses) or source \`env.sh\`.
|
|
261
|
+
|
|
262
|
+
Note two names for the building id: some api functions take \`id\`, others
|
|
263
|
+
\`buildingId\`, so params.json carries both — fill each with the same building.
|
|
264
|
+
|
|
265
|
+
Baselines: after a good run, copy \`last-<name>.json\` to \`baseline-<name>.json\`.
|
|
266
|
+
Subsequent runs fail when p95 exceeds 2x the baseline.
|
|
267
|
+
|
|
268
|
+
\`routes.pipes\` is yours after generation. \`--force\` merges this run's endpoints
|
|
269
|
+
into it rather than replacing it: a rate you tuned, a pipe you commented out and
|
|
270
|
+
a POST flow you added by hand all survive. This README is regenerated by
|
|
271
|
+
\`--force\`; put your own notes in \`routes.pipes\`, below its header.
|
|
272
|
+
`;
|
|
273
|
+
}
|