@lariocpt/flawless-react 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/.agents/skills/flawless-a11y/SKILL.md +113 -0
  2. package/.agents/skills/flawless-api/SKILL.md +159 -0
  3. package/.agents/skills/flawless-api-fixtures/SKILL.md +144 -0
  4. package/.agents/skills/flawless-e2e/SKILL.md +155 -0
  5. package/.agents/skills/flawless-expo/SKILL.md +283 -0
  6. package/.agents/skills/flawless-findings/SKILL.md +116 -0
  7. package/.agents/skills/flawless-load/SKILL.md +112 -0
  8. package/.agents/skills/flawless-nextjs/SKILL.md +198 -0
  9. package/.agents/skills/flawless-perf-harness/SKILL.md +158 -0
  10. package/.agents/skills/flawless-perf-static/SKILL.md +131 -0
  11. package/.agents/skills/flawless-react/SKILL.md +403 -0
  12. package/.agents/skills/flawless-react-rules/SKILL.md +96 -0
  13. package/.agents/skills/flawless-regen/SKILL.md +135 -0
  14. package/.agents/skills/flawless-unit-browser/SKILL.md +178 -0
  15. package/LICENSE +21 -0
  16. package/README.md +611 -2
  17. package/bin/flawless-react +4 -0
  18. package/package.json +67 -4
  19. package/scripts/postinstall.d.mts +46 -0
  20. package/scripts/postinstall.mjs +127 -0
  21. package/src/analyse/authShims.ts +220 -0
  22. package/src/analyse/bootability.ts +257 -0
  23. package/src/analyse/childBridge.ts +302 -0
  24. package/src/analyse/childrenIntoSlot.ts +66 -0
  25. package/src/analyse/compoundFamily.ts +100 -0
  26. package/src/analyse/dataExports.ts +200 -0
  27. package/src/analyse/extractChildAttrs.ts +112 -0
  28. package/src/analyse/extractDataDeps.ts +857 -0
  29. package/src/analyse/extractDisclosures.ts +186 -0
  30. package/src/analyse/extractFetchCalls.ts +81 -0
  31. package/src/analyse/extractFlows.ts +239 -0
  32. package/src/analyse/extractGuards.ts +270 -0
  33. package/src/analyse/extractHooks.ts +237 -0
  34. package/src/analyse/extractInteractions.ts +1421 -0
  35. package/src/analyse/extractLogging.ts +45 -0
  36. package/src/analyse/extractNativeA11y.ts +138 -0
  37. package/src/analyse/extractPredicates.ts +784 -0
  38. package/src/analyse/extractPropUsage.ts +162 -0
  39. package/src/analyse/extractProps.ts +1212 -0
  40. package/src/analyse/extractRenderMode.ts +165 -0
  41. package/src/analyse/extractRootTag.ts +117 -0
  42. package/src/analyse/extractState.ts +361 -0
  43. package/src/analyse/extractStaticText.ts +212 -0
  44. package/src/analyse/extractVariants.ts +186 -0
  45. package/src/analyse/harvestApiEndpoints.ts +284 -0
  46. package/src/analyse/hostProps.ts +185 -0
  47. package/src/analyse/hostTag.ts +72 -0
  48. package/src/analyse/htmlContentModel.ts +94 -0
  49. package/src/analyse/index.ts +744 -0
  50. package/src/analyse/mountPathCalls.ts +442 -0
  51. package/src/analyse/nativeContentModel.ts +129 -0
  52. package/src/analyse/nativeDialect.ts +83 -0
  53. package/src/analyse/nativeProps.ts +339 -0
  54. package/src/analyse/ownReturns.ts +31 -0
  55. package/src/analyse/parseFile.ts +919 -0
  56. package/src/analyse/pos.ts +8 -0
  57. package/src/analyse/providerWraps.ts +1977 -0
  58. package/src/analyse/pureModule.ts +88 -0
  59. package/src/analyse/reexportTable.ts +148 -0
  60. package/src/analyse/refs.ts +195 -0
  61. package/src/analyse/renderPath.ts +441 -0
  62. package/src/analyse/resolveDepReturns.ts +370 -0
  63. package/src/analyse/resolveIndexKeys.ts +98 -0
  64. package/src/analyse/resolveModule.ts +431 -0
  65. package/src/analyse/resolveTagRootTags.ts +103 -0
  66. package/src/analyse/serverActionReturns.ts +213 -0
  67. package/src/analyse/sourceCache.ts +263 -0
  68. package/src/analyse/spreadSiblings.ts +98 -0
  69. package/src/analyse/walkChildren.ts +98 -0
  70. package/src/analyse/zodSchema.ts +424 -0
  71. package/src/bin.ts +23 -0
  72. package/src/changed.ts +221 -0
  73. package/src/cli.ts +560 -0
  74. package/src/cliFlags.ts +281 -0
  75. package/src/core.ts +1229 -0
  76. package/src/docLine.ts +122 -0
  77. package/src/emitPaths.ts +51 -0
  78. package/src/expo/detect.ts +184 -0
  79. package/src/expo/refusal.ts +62 -0
  80. package/src/expo/routes.ts +145 -0
  81. package/src/index.ts +95 -0
  82. package/src/mcp/analysisTools.ts +720 -0
  83. package/src/mcp/infoTools.ts +135 -0
  84. package/src/mcp/protocol.ts +130 -0
  85. package/src/mcp/run.ts +269 -0
  86. package/src/mcp/runTools.ts +362 -0
  87. package/src/mcp/server.ts +279 -0
  88. package/src/mcp/spec.ts +86 -0
  89. package/src/mcp/support.ts +268 -0
  90. package/src/mcp/tools.ts +75 -0
  91. package/src/nextjs/apiRoutes.ts +116 -0
  92. package/src/nextjs/detect.ts +397 -0
  93. package/src/nextjs/emit.ts +106 -0
  94. package/src/nextjs/liveSpec.ts +79 -0
  95. package/src/nextjs/mswHandlers.ts +55 -0
  96. package/src/nextjs/providerWrapper.ts +603 -0
  97. package/src/nextjs/routes.ts +86 -0
  98. package/src/nextjs/swagger.ts +32 -0
  99. package/src/packageJson.ts +414 -0
  100. package/src/platform.ts +18 -0
  101. package/src/prompt.ts +132 -0
  102. package/src/reactCompiler.ts +165 -0
  103. package/src/reporters/findings.ts +156 -0
  104. package/src/reporters/report.ts +463 -0
  105. package/src/runner/runTests.ts +402 -0
  106. package/src/scaffold/adoptStyle.ts +181 -0
  107. package/src/scaffold/index.ts +253 -0
  108. package/src/scaffold/openapi.ts +183 -0
  109. package/src/scaffold/templates.ts +2379 -0
  110. package/src/scaffold-tests/index.ts +463 -0
  111. package/src/scaffold-tests/mockValues.ts +208 -0
  112. package/src/scaffold-tests/mocks.ts +794 -0
  113. package/src/scaffold-tests/nativeIndex.ts +52 -0
  114. package/src/scaffold-tests/nativeTemplates.ts +1528 -0
  115. package/src/scaffold-tests/postDispatch.ts +152 -0
  116. package/src/scaffold-tests/registry.ts +443 -0
  117. package/src/scaffold-tests/selectors.ts +493 -0
  118. package/src/scaffold-tests/selectorsNative.ts +273 -0
  119. package/src/scaffold-tests/stamp.ts +79 -0
  120. package/src/scaffold-tests/templates.ts +3578 -0
  121. package/src/skills/cli.ts +248 -0
  122. package/src/skills/compat.ts +166 -0
  123. package/src/skills/data.ts +192 -0
  124. package/src/skills/md.d.ts +7 -0
  125. package/src/skills/menu.ts +55 -0
  126. package/src/skills/spec.ts +227 -0
  127. package/src/suiteScripts.ts +110 -0
  128. package/src/tasks/a11yNative.ts +281 -0
  129. package/src/tasks/a11yScan.ts +67 -0
  130. package/src/tasks/biomeRunner.ts +301 -0
  131. package/src/tasks/biomeSweep.ts +166 -0
  132. package/src/tasks/e2eExpo.ts +436 -0
  133. package/src/tasks/e2eRoutes.ts +756 -0
  134. package/src/tasks/eslintReactRules.ts +67 -0
  135. package/src/tasks/index.ts +24 -0
  136. package/src/tasks/loadExpo.ts +273 -0
  137. package/src/tasks/loadPipes.ts +750 -0
  138. package/src/tasks/perfHarness.ts +145 -0
  139. package/src/tasks/staticPerfAnalysis.ts +510 -0
  140. package/src/tasks/types.ts +1 -0
  141. package/src/tasks/unitAndBrowser.ts +462 -0
  142. package/src/types.ts +1419 -0
  143. package/src/version.ts +12 -0
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bun
2
+ import { main } from "../src/cli.ts";
3
+
4
+ await main(Bun.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "@lariocpt/flawless-react",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }