@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/package.json
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lariocpt/flawless-react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Multi-task quality kit for React components: scaffolds Playwright perf harness + Vitest unit + browser tests, runs static perf analysis, an accessibility scan, and Biome-driven React correctness rules. Point it at a .tsx file or glob and pick what to run.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "Lario Borges",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/lariocpt/flawless-react.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/lariocpt/flawless-react/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://lariocpt.github.io/flawless-react-website/",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"react",
|
|
17
|
+
"testing",
|
|
18
|
+
"playwright",
|
|
19
|
+
"vitest",
|
|
20
|
+
"accessibility",
|
|
21
|
+
"performance",
|
|
22
|
+
"biome",
|
|
23
|
+
"code-generation"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"bin": {
|
|
27
|
+
"flawless-react": "./bin/flawless-react"
|
|
28
|
+
},
|
|
29
|
+
"module": "./src/index.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./src/index.ts",
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"bin",
|
|
36
|
+
"src",
|
|
37
|
+
"scripts",
|
|
38
|
+
".agents/skills",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "bun test",
|
|
44
|
+
"test:watch": "bun test --watch",
|
|
45
|
+
"dev": "bun src/cli.ts",
|
|
46
|
+
"build": "bun build --compile --minify --outfile bin/flawless-react-bin src/bin.ts",
|
|
47
|
+
"lint": "biome lint",
|
|
48
|
+
"format": "biome format --write",
|
|
49
|
+
"check": "biome check",
|
|
50
|
+
"check:fix": "biome check --write",
|
|
51
|
+
"postinstall": "node scripts/postinstall.mjs"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@clack/prompts": "^0.7.0",
|
|
55
|
+
"@biomejs/biome": "2.5.1",
|
|
56
|
+
"typescript": "^5.6.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/bun": "latest",
|
|
60
|
+
"@types/react": "^18.3.0"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"bun": ">=1.1.0"
|
|
64
|
+
},
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"registry": "https://registry.npmjs.org",
|
|
67
|
+
"access": "public"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Types for postinstall.mjs, which stays plain JavaScript ON PURPOSE: npm runs it
|
|
2
|
+
// before anything is installed, so it may not need a transpile step, a bun runtime,
|
|
3
|
+
// or a dependency that is not in node_modules yet. That also means tsc never saw it
|
|
4
|
+
// — the file was shipped untyped until tests/skills-prompt.test.ts imported it.
|
|
5
|
+
// This declaration is the seam: the implementation keeps its runtime shape, and the
|
|
6
|
+
// gate's contract is written down where a change to it has to be stated.
|
|
7
|
+
|
|
8
|
+
/** What `hasBun` injects to probe for the runtime: a `spawnSync`-shaped call. */
|
|
9
|
+
type RunProbe = (
|
|
10
|
+
command: string,
|
|
11
|
+
args: readonly string[],
|
|
12
|
+
) => { error?: unknown; status: number | null };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The answer from the install gate. `reason` is present on every "no" and
|
|
16
|
+
* `dir` on every "yes" plus the "already installed" case — the caller that
|
|
17
|
+
* spawns the picker needs the directory, the tests need the reason.
|
|
18
|
+
*/
|
|
19
|
+
export interface SkillGate {
|
|
20
|
+
ask: boolean;
|
|
21
|
+
dir?: string;
|
|
22
|
+
reason?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export declare function hasBun(run?: RunProbe): boolean;
|
|
26
|
+
|
|
27
|
+
export declare function shouldAsk(options: {
|
|
28
|
+
env: Record<string, string | undefined>;
|
|
29
|
+
stdinTty: boolean;
|
|
30
|
+
stdoutTty: boolean;
|
|
31
|
+
bunAvailable?: boolean;
|
|
32
|
+
exists?: (path: string) => boolean;
|
|
33
|
+
}): SkillGate;
|
|
34
|
+
|
|
35
|
+
export declare function askSkills(options: {
|
|
36
|
+
bin: string;
|
|
37
|
+
bun?: string;
|
|
38
|
+
spawnFn?: (
|
|
39
|
+
command: string,
|
|
40
|
+
args: readonly string[],
|
|
41
|
+
options: { cwd?: string; stdio?: string },
|
|
42
|
+
) => {
|
|
43
|
+
on: (event: string, listener: (argument: unknown) => void) => void;
|
|
44
|
+
};
|
|
45
|
+
log?: (line: string) => void;
|
|
46
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// flawless-react — npm postinstall.
|
|
2
|
+
//
|
|
3
|
+
// Asks, once, in a terminal: want the agent skills? Everything here is a
|
|
4
|
+
// best-effort fast path. Install scripts are unreliable by design (npm 12 blocks
|
|
5
|
+
// them by default, `--ignore-scripts` skips them, a proxy can swallow stdout), so
|
|
6
|
+
// the real surface is the CLI itself: an interactive run makes the same offer, and
|
|
7
|
+
// `flawless-react skills install` is always there. What this file owns is the
|
|
8
|
+
// moment a user has just decided to use the tool and has not run it yet.
|
|
9
|
+
//
|
|
10
|
+
// Kept deliberately small: node built-ins only, and it can never fail an install.
|
|
11
|
+
// The gate is `shouldAsk` below — a pure function, so tests cover every reason to
|
|
12
|
+
// stay silent rather than only the one that is easy to trigger.
|
|
13
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
14
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
/** Keep in step with SKILL_PREFIX in src/skills/data.ts — the prefix IS ownership. */
|
|
18
|
+
const SKILL_PREFIX = "flawless-";
|
|
19
|
+
|
|
20
|
+
/** Whether the picker can actually be run: the CLI is a bun script. */
|
|
21
|
+
export function hasBun(run = spawnSync) {
|
|
22
|
+
const probe = run("bun", ["--version"]);
|
|
23
|
+
return !probe.error && probe.status === 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Should this install ask? Returns `{ ask, dir }` to ask in, or `{ ask, reason }`.
|
|
28
|
+
*
|
|
29
|
+
* The order matters and so does each test. `CI` and a missing TTY come first,
|
|
30
|
+
* because an unanswered question in a build pipeline is a hung job. A global
|
|
31
|
+
* install asks nothing: the cwd is wherever npm happened to run, not a project.
|
|
32
|
+
* `INIT_CWD` is npm's own "where the user ran this" and the only reliable answer,
|
|
33
|
+
* because a dependency's postinstall runs with cwd set to the package directory,
|
|
34
|
+
* which is inside node_modules — writing `.agents/skills` there would bury the
|
|
35
|
+
* files where no agent looks. Anything under node_modules is refused outright.
|
|
36
|
+
* The bun probe is last because it costs a subprocess: the checks before it are
|
|
37
|
+
* free, and if bun is absent the picker cannot run anyway.
|
|
38
|
+
*/
|
|
39
|
+
export function shouldAsk({
|
|
40
|
+
env,
|
|
41
|
+
stdinTty,
|
|
42
|
+
stdoutTty,
|
|
43
|
+
bunAvailable = true,
|
|
44
|
+
exists = existsSync,
|
|
45
|
+
}) {
|
|
46
|
+
if (env.FLAWLESS_NO_SKILLS) return { ask: false, reason: "FLAWLESS_NO_SKILLS is set" };
|
|
47
|
+
if (env.CI) return { ask: false, reason: "CI" };
|
|
48
|
+
if (env.npm_config_global === "true") return { ask: false, reason: "global install" };
|
|
49
|
+
if (!stdinTty || !stdoutTty) return { ask: false, reason: "not a terminal" };
|
|
50
|
+
|
|
51
|
+
const dir = env.INIT_CWD && path.resolve(env.INIT_CWD);
|
|
52
|
+
if (!dir) return { ask: false, reason: "no INIT_CWD" };
|
|
53
|
+
if (dir.includes(`${path.sep}node_modules${path.sep}`) || dir.endsWith("node_modules")) {
|
|
54
|
+
return { ask: false, reason: "inside node_modules" };
|
|
55
|
+
}
|
|
56
|
+
if (!exists(dir)) return { ask: false, reason: "project directory is gone" };
|
|
57
|
+
|
|
58
|
+
// A project that already has one of ours was set up on purpose; the skills
|
|
59
|
+
// themselves are the marker, so neither question needs to store a decline.
|
|
60
|
+
for (const base of [".agents/skills", ".claude/skills"]) {
|
|
61
|
+
let entries = [];
|
|
62
|
+
try {
|
|
63
|
+
entries = readdirSync(path.join(dir, base), { withFileTypes: true });
|
|
64
|
+
} catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (entries.some((e) => e.name.startsWith(SKILL_PREFIX) && (e.isDirectory() || e.isSymbolicLink()))) {
|
|
68
|
+
return { ask: false, reason: "already installed", dir };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!bunAvailable) return { ask: false, reason: "no bun on PATH to run the picker" };
|
|
73
|
+
return { ask: true, dir };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Hand the terminal to the picker. Nothing is decided here about WHICH skills: the
|
|
78
|
+
* menu owns that, and running it in-process would mean duplicating the installer in
|
|
79
|
+
* plain JavaScript. So this spawns the CLI with inherited stdio — which also means
|
|
80
|
+
* the one question in the whole install is clack's, not a second prompt that then
|
|
81
|
+
* has to hand a half-used stdin to a child.
|
|
82
|
+
*/
|
|
83
|
+
export function askSkills({ bin, bun = "bun", spawnFn = spawn, log = console.error }) {
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
const gate = shouldAsk({
|
|
86
|
+
env: process.env,
|
|
87
|
+
stdinTty: Boolean(process.stdin.isTTY),
|
|
88
|
+
stdoutTty: Boolean(process.stdout.isTTY),
|
|
89
|
+
bunAvailable: hasBun(),
|
|
90
|
+
});
|
|
91
|
+
if (!gate.ask) {
|
|
92
|
+
resolve(gate.reason ?? "not asked");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
log("");
|
|
96
|
+
log(" flawless-react ships agent skills: guides that teach Claude Code, Codex,");
|
|
97
|
+
log(" Cursor and OpenCode how to drive this tool instead of guessing at it.");
|
|
98
|
+
log(` Pick what to install into ${gate.dir}/.agents/skills (Esc installs nothing):`);
|
|
99
|
+
|
|
100
|
+
const child = spawnFn(bun, [bin, "skills", "install"], {
|
|
101
|
+
cwd: gate.dir,
|
|
102
|
+
stdio: "inherit",
|
|
103
|
+
});
|
|
104
|
+
child.on("error", (err) =>
|
|
105
|
+
log(` flawless-react: could not run the skills picker (${err.message}). Later:`),
|
|
106
|
+
);
|
|
107
|
+
child.on("close", (code) => {
|
|
108
|
+
if (code) {
|
|
109
|
+
log(` flawless-react: the skills picker exited ${code}. Run it yourself any time:`);
|
|
110
|
+
}
|
|
111
|
+
log(" bunx flawless-react skills install");
|
|
112
|
+
resolve(code === null ? "killed" : `exit ${code}`);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// npm runs this file directly. Errors are swallowed on purpose: a broken prompt is
|
|
118
|
+
// not a reason to fail an install, and a postinstall that throws takes the user's
|
|
119
|
+
// dependency tree down with it.
|
|
120
|
+
if (import.meta.main) {
|
|
121
|
+
try {
|
|
122
|
+
await askSkills({ bin: new URL("../bin/flawless-react", import.meta.url).pathname });
|
|
123
|
+
} catch {
|
|
124
|
+
// Declining on the user's behalf is better than aborting their install.
|
|
125
|
+
}
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-bound auth libraries — the ones that THROW when a component renders
|
|
3
|
+
* without their provider mounted. `@clerk/nextjs` refuses on a missing
|
|
4
|
+
* publishableKey before anything else runs; its hooks refuse outside
|
|
5
|
+
* ClerkProvider; next-auth's useSession refuses outside SessionProvider. A
|
|
6
|
+
* browser host bundles every analysed component into ONE module graph (the
|
|
7
|
+
* kiranism dogfood run: Accordion's profile spec died on "Missing
|
|
8
|
+
* publishableKey" because AppSidebar's Clerk import poisoned the shared
|
|
9
|
+
* bundle), and jsdom units mount components bare, with no app layout to
|
|
10
|
+
* supply the provider.
|
|
11
|
+
*
|
|
12
|
+
* Neither plane can meaningfully run a real auth handshake, so the whole
|
|
13
|
+
* library is replaced with a signed-out stub — same philosophy as the global
|
|
14
|
+
* next/navigation mock in setup.ts. The stub is generated from the export
|
|
15
|
+
* names the analysis actually saw imported, because ESM named imports have to
|
|
16
|
+
* bind: a catch-all default export cannot satisfy `import { useUser }`.
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import ts from "typescript";
|
|
20
|
+
|
|
21
|
+
/** Package names matched exactly or by subpath ("next-auth/react", "@clerk/nextjs/server"). */
|
|
22
|
+
const AUTH_LIB_PREFIXES = [
|
|
23
|
+
"@clerk/nextjs",
|
|
24
|
+
"@clerk/clerk-react",
|
|
25
|
+
"next-auth",
|
|
26
|
+
// The Supabase auth layer, in both its generations: `auth-helpers-*` is the
|
|
27
|
+
// tutorial stack (talkformai's pages/_app builds a client in a useState
|
|
28
|
+
// initializer, so with no env file the CONSTRUCTOR throws before a single
|
|
29
|
+
// spec runs — 17 files, 26 tests, all dead on
|
|
30
|
+
// "either NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY env
|
|
31
|
+
// variables or supabaseUrl and supabaseKey are required!"), and `@supabase/ssr`
|
|
32
|
+
// is its replacement. Both are provider-bound: the session context they
|
|
33
|
+
// supply is exactly what a unit mount cannot have. `@supabase/supabase-js`
|
|
34
|
+
// itself stays unstubbed — it is the SDK rather than the provider, and a
|
|
35
|
+
// stubbed `from().select()` chain would turn a named failure into undefined
|
|
36
|
+
// results quietly flowing through an assertion.
|
|
37
|
+
"@supabase/auth-helpers-nextjs",
|
|
38
|
+
"@supabase/auth-helpers-react",
|
|
39
|
+
"@supabase/ssr",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export function isAuthLibSpecifier(specifier: string): boolean {
|
|
43
|
+
return AUTH_LIB_PREFIXES.some((p) => specifier === p || specifier.startsWith(`${p}/`));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AuthStub {
|
|
47
|
+
/** Module-side export names to bind (the "cn" of `import { cn as cx }`), sorted, no "default". */
|
|
48
|
+
names: string[];
|
|
49
|
+
hasDefault: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The part of ModuleImportInfo the collector reads, so a raw file can feed it too. */
|
|
53
|
+
interface AuthImportEvidence {
|
|
54
|
+
specifier: string;
|
|
55
|
+
names: readonly string[];
|
|
56
|
+
importedNames?: readonly string[];
|
|
57
|
+
isTypeOnly?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Import declarations of a file the analysis never visits. The app resolver is
|
|
62
|
+
* mounted by every spec without being an analysed component, so its auth
|
|
63
|
+
* imports would stay unstubbed while its module graph runs for real:
|
|
64
|
+
* talkformai's pages/_app.tsx is the only file importing
|
|
65
|
+
* `createPagesBrowserClient` / `SessionContextProvider`, and with no env file in
|
|
66
|
+
* the repo the real client threw from its constructor — 17 spec files, 26
|
|
67
|
+
* tests, none of them reaching an assertion. The import table is all the
|
|
68
|
+
* collector needs; the shim binds the names.
|
|
69
|
+
*/
|
|
70
|
+
function fileImportEvidence(file: string): AuthImportEvidence[] {
|
|
71
|
+
let sf: ts.SourceFile;
|
|
72
|
+
try {
|
|
73
|
+
sf = ts.createSourceFile(
|
|
74
|
+
file,
|
|
75
|
+
readFileSync(file, "utf8"),
|
|
76
|
+
ts.ScriptTarget.ES2022,
|
|
77
|
+
true,
|
|
78
|
+
ts.ScriptKind.TSX,
|
|
79
|
+
);
|
|
80
|
+
} catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const out: AuthImportEvidence[] = [];
|
|
84
|
+
for (const stmt of sf.statements) {
|
|
85
|
+
if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
86
|
+
const clause = stmt.importClause;
|
|
87
|
+
if (!clause || clause.isTypeOnly) continue;
|
|
88
|
+
const names: string[] = [];
|
|
89
|
+
const importedNames: string[] = [];
|
|
90
|
+
if (clause.name) {
|
|
91
|
+
names.push(clause.name.text);
|
|
92
|
+
importedNames.push("default");
|
|
93
|
+
}
|
|
94
|
+
const bindings = clause.namedBindings;
|
|
95
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
96
|
+
for (const el of bindings.elements) {
|
|
97
|
+
names.push(el.name.text);
|
|
98
|
+
importedNames.push(el.propertyName?.text ?? el.name.text);
|
|
99
|
+
}
|
|
100
|
+
} else if (bindings && ts.isNamespaceImport(bindings)) {
|
|
101
|
+
names.push(bindings.name.text);
|
|
102
|
+
importedNames.push(bindings.name.text);
|
|
103
|
+
}
|
|
104
|
+
if (names.length === 0) continue;
|
|
105
|
+
out.push({ specifier: stmt.moduleSpecifier.text, names, importedNames });
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Specifier → the export names any analysed file imports from it. Collect from
|
|
112
|
+
* EVERY analysed component, not just the mountable ones: an excluded component
|
|
113
|
+
* is still imported transitively by a hosted neighbour, and one unstubbed
|
|
114
|
+
* throw poisons the whole bundle. Namespace imports are indistinguishable
|
|
115
|
+
* here (the analyser records the local name as the module-side name); a repo
|
|
116
|
+
* that does `import * as clerk from "@clerk/nextjs"` will see undefined
|
|
117
|
+
* members and the dogfood failure will name the specifier.
|
|
118
|
+
*/
|
|
119
|
+
export function collectAuthStubs(
|
|
120
|
+
infos: Iterable<{ moduleImports?: readonly AuthImportEvidence[] }>,
|
|
121
|
+
extraFiles: readonly string[] = [],
|
|
122
|
+
): Map<string, AuthStub> {
|
|
123
|
+
const acc = new Map<string, { names: Set<string>; hasDefault: boolean }>();
|
|
124
|
+
const absorb = (mi: AuthImportEvidence): void => {
|
|
125
|
+
if (mi.isTypeOnly || !isAuthLibSpecifier(mi.specifier)) return;
|
|
126
|
+
let entry = acc.get(mi.specifier);
|
|
127
|
+
if (!entry) {
|
|
128
|
+
entry = { names: new Set(), hasDefault: false };
|
|
129
|
+
acc.set(mi.specifier, entry);
|
|
130
|
+
}
|
|
131
|
+
const e = entry;
|
|
132
|
+
mi.names.forEach((local, i) => {
|
|
133
|
+
const moduleSide = mi.importedNames?.[i] ?? local;
|
|
134
|
+
if (moduleSide === "default") e.hasDefault = true;
|
|
135
|
+
else e.names.add(moduleSide);
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
for (const info of infos) for (const mi of info.moduleImports ?? []) absorb(mi);
|
|
139
|
+
for (const file of extraFiles) for (const mi of fileImportEvidence(file)) absorb(mi);
|
|
140
|
+
return new Map(
|
|
141
|
+
[...acc.entries()]
|
|
142
|
+
.sort(([a], [b]) => (a < b ? -1 : 1))
|
|
143
|
+
.map(([spec, e]) => [spec, { names: [...e.names].sort(), hasDefault: e.hasDefault }]),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A signed-out snapshot: everything falsy-but-present so `user?.firstName`
|
|
149
|
+
* renders nothing instead of throwing. Unknown property reads follow shape
|
|
150
|
+
* heuristics — verb names get an async no-op (awaiting undefined throws),
|
|
151
|
+
* `*List` gets an empty array (mapping undefined throws) — and otherwise
|
|
152
|
+
* undefined. Regenerated per call so no test can mutate another's view.
|
|
153
|
+
*/
|
|
154
|
+
const SNAPSHOT = `const __snap = () => new Proxy({ isLoaded: true, isLoading: false, isSignedIn: false, userId: null, sessionId: null, orgId: null, orgRole: null, user: null, organization: null, session: null, data: null, status: "unauthenticated" }, { get: (t, k) => k in t ? t[k] : typeof k === "string" && (/^(sign|set|get|reload|redirect|create)/i.test(k) || k === "auth" || k === "currentUser") ? async () => __snap() : typeof k === "string" && k.endsWith("List") ? [] : undefined });`;
|
|
155
|
+
|
|
156
|
+
/** Render children — for providers (hooks need their subtree mounted) and unknown widgets (empty is safe). */
|
|
157
|
+
const AS_CHILD = `const __asChild = (props) => (props && props.children !== undefined ? props.children : null);`;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Per-export stub expression. The signed-out state decides the three named
|
|
161
|
+
* conditionals; hooks get the snapshot; the known async API surface gets an
|
|
162
|
+
* awaited snapshot (components `await auth()` and destructure it); providers
|
|
163
|
+
* must mount children; unknown widgets render children too — no children,
|
|
164
|
+
* nothing renders, which is the honest signed-out picture for a UserButton.
|
|
165
|
+
*/
|
|
166
|
+
function stubExpr(name: string): string {
|
|
167
|
+
if (name === "Show") return "(props) => (props && props.when ? props.children : null)";
|
|
168
|
+
// Signed-out means SignedIn renders nothing and SignedOut renders its
|
|
169
|
+
// children; `<Show when>` is the generic conditional.
|
|
170
|
+
if (name === "SignedIn") return "() => null";
|
|
171
|
+
if (name === "SignedOut") return "__asChild";
|
|
172
|
+
// Hooks return the snapshot, not a renderable — destructuring a component
|
|
173
|
+
// stub (`const { userId } = useAuth()`) would hand back a function.
|
|
174
|
+
if (/^use[A-Z]/.test(name)) return "() => __snap()";
|
|
175
|
+
// Known async API surface: components `await auth()` and destructure it.
|
|
176
|
+
if (
|
|
177
|
+
/^(signIn|signUp|signOut|reload|redirect|setActive|setSession|getSession|getServerSession|getAuth|getCurrentUser|currentUser|createOrganization)$/.test(
|
|
178
|
+
name,
|
|
179
|
+
) ||
|
|
180
|
+
name === "auth" ||
|
|
181
|
+
name === "clerkClient"
|
|
182
|
+
) {
|
|
183
|
+
return "async () => __snap()";
|
|
184
|
+
}
|
|
185
|
+
// Providers must mount children or nothing under them renders; unknown
|
|
186
|
+
// names are treated the same way — a widget with no children renders null.
|
|
187
|
+
return "__asChild";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function preamble(specifier: string): string {
|
|
191
|
+
return [
|
|
192
|
+
`// flawlessly-generated signed-out stub for "${specifier}": the real library throws`,
|
|
193
|
+
"// without its provider/keys, which neither the perf host nor a bare unit mount",
|
|
194
|
+
"// can supply. Export names come from the analysis of this repo's components.",
|
|
195
|
+
SNAPSHOT,
|
|
196
|
+
AS_CHILD,
|
|
197
|
+
].join("\n");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** ESM source for the perf-harness host bundle (Bun plugin onLoad body). */
|
|
201
|
+
export function authShimEsm(specifier: string, stub: AuthStub): string {
|
|
202
|
+
const lines = [preamble(specifier)];
|
|
203
|
+
for (const n of stub.names) lines.push(`export const ${n} = ${stubExpr(n)};`);
|
|
204
|
+
if (stub.hasDefault) lines.push(`export default __asChild;`);
|
|
205
|
+
return lines.join("\n");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** A vi.mock block for the unit plane's setup.ts (factory-local: vitest hoists the call). */
|
|
209
|
+
export function authShimVitestBlock(specifier: string, stub: AuthStub): string {
|
|
210
|
+
const exports = stub.names.map((n) => `${JSON.stringify(n)}: ${stubExpr(n)}`);
|
|
211
|
+
if (stub.hasDefault) exports.push(`default: __asChild`);
|
|
212
|
+
return `
|
|
213
|
+
vi.mock(${JSON.stringify(specifier)}, () => {
|
|
214
|
+
${preamble(specifier)
|
|
215
|
+
.split("\n")
|
|
216
|
+
.map((l) => ` ${l}`)
|
|
217
|
+
.join("\n")}
|
|
218
|
+
return { ${exports.join(", ")} };
|
|
219
|
+
});`;
|
|
220
|
+
}
|