Title
codegen: in-project api.d.ts drags entire backend module graph into every client TS program
Body
Problem
packages/.../convex/_generated/api.d.ts is generated as:
import type * as moduleA from "../moduleA.js";
import type * as moduleB from "../moduleB.js";
// ...one line per backend module
declare const fullApi: ApiFromModules<{
"moduleA": typeof moduleA,
"moduleB": typeof moduleB,
// ...
}>;
export declare const api: FilterApi<typeof fullApi, FunctionReference<any, "public">>;
export declare const internal: FilterApi<typeof fullApi, FunctionReference<any, "internal">>;
This is codegenDynamicApiObjects in dist/esm/cli/codegen_templates/component_api.js (and the equivalent in codegen_templates/api.js for the project root).
Any client that imports api (which every Convex client must, to get end-to-end typed function references) forces TypeScript to load and check every backend module to derive the api type. import type * as does not prevent module loading for type resolution — TS still parses each source file to compute its exported types, then ApiFromModules<> and FilterApi<> walk the result via recursive conditional+mapped types.
Measured impact (real production monorepo)
A monorepo with a non-trivial backend (1,103 convex modules, 3,721 registered functions, ~503k lines of .d.ts defs):
| Metric |
mobile-rn (Expo/RN) |
web-react (Vite) |
| Backend files pulled into client TS program |
1,199 |
1,200 |
Memory used (tsc --extendedDiagnostics) |
3.43 GB |
3.66 GB |
Strict subtype cache size |
439,040 |
21,560 |
Total time |
47.22 s |
58.58 s |
Both clients sit just under the V8 default 4 GB heap. Any growth tips them over — intermittent OOMs in CI/Vercel/EAS that do not reproduce on developer machines with --max-old-space-size=8192. Devs spend hours bisecting "what changed in the type system" when the answer is "the backend grew, the type-checker noticed."
Precedent: convex already does the right thing for components
codegenStaticApiObjects (same file) emits each component's api/internal as inlined FunctionReference<udfType, visibility, args, returns> declarations — fully self-contained, zero source-file dependencies. It uses parseValidator + validatorToType from validator_helpers.js against the analyzed function metadata.
The static path is gated on opts.staticApi and currently only triggers for components, because in-project codegen runs locally without the server-side function analysis that components have via startPush.analysis.
Proposed change
Make in-project api.d.ts codegen produce the inlined-FunctionReference shape too. The function metadata (args validator JSON, returns validator JSON, visibility, udfType) needed by validatorToType is statically derivable from the .ts source files: every registered function is export const X = (mutation|query|action|internalMutation|internalQuery|internalAction)({args: v.object({...}), returns: v...., handler: ...}). The CLI can parse each backend file's AST during codegen, extract those validator literals, and synthesize the same FunctionReference strings that the component path emits today.
Concretely:
- Add
codegenStaticApiFromSources(modulePaths) that:
- For each module, walk top-level
export const declarations
- Match call expressions whose callee is a registration wrapper (configurable list — convex's own +
secureMutation/secureQuery from convex-helpers)
- Extract the
args and returns validator object literals; convert via validatorToType
- Emit
FunctionReference<udfType, visibility, argsType, returnsType> per leaf
- Build the api/internal trees the same way
buildApiTree does for components
- Default
--codegen=static for new projects (gated by a flag for backward compat); existing projects opt in.
Workaround we're shipping while waiting
Local post-codegen step using the TS Compiler API: node scripts/flatten-api-dts.mjs. Walks the convex/ module graph, identifies registered functions via RegisteredQuery|Mutation|Action alias-symbol detection (with AST fallback for files where wrapper imports are broken or use convex-helpers secure*), and rewrites api.d.ts in the inlined shape. Runs after convex deploy and is enforced by a pre-commit gate that rejects unflattened api.d.ts.
Result on the same monorepo: mobile-rn drops from 1,199 → 120 backend files, 3.43 GB → 2.13 GB, 47s → 11s. web-react drops from 1,200 → 113 files, 3.66 GB → 2.36 GB, 58s → 21s. Zero new errors caused by the flatten itself; 13 errors surfaced that the dynamic shape was hiding by typing missing functions as any (real codebase bugs we wanted to find).
Happy to open the PR if there's interest in landing this upstream — the script we wrote is the prototype; a CLI-native version would be cleaner because it has access to convex's own parseValidator/validatorToType and doesn't need to spin up a TS Program just to walk types.
Title
codegen: in-project
api.d.tsdrags entire backend module graph into every client TS programBody
Problem
packages/.../convex/_generated/api.d.tsis generated as:This is
codegenDynamicApiObjectsindist/esm/cli/codegen_templates/component_api.js(and the equivalent incodegen_templates/api.jsfor the project root).Any client that imports
api(which every Convex client must, to get end-to-end typed function references) forces TypeScript to load and check every backend module to derive theapitype.import type * asdoes not prevent module loading for type resolution — TS still parses each source file to compute its exported types, thenApiFromModules<>andFilterApi<>walk the result via recursive conditional+mapped types.Measured impact (real production monorepo)
A monorepo with a non-trivial backend (1,103 convex modules, 3,721 registered functions, ~503k lines of
.d.tsdefs):Memory used(tsc --extendedDiagnostics)Strict subtype cache sizeTotal timeBoth clients sit just under the V8 default 4 GB heap. Any growth tips them over — intermittent OOMs in CI/Vercel/EAS that do not reproduce on developer machines with
--max-old-space-size=8192. Devs spend hours bisecting "what changed in the type system" when the answer is "the backend grew, the type-checker noticed."Precedent: convex already does the right thing for components
codegenStaticApiObjects(same file) emits each component'sapi/internalas inlinedFunctionReference<udfType, visibility, args, returns>declarations — fully self-contained, zero source-file dependencies. It usesparseValidator+validatorToTypefromvalidator_helpers.jsagainst the analyzed function metadata.The static path is gated on
opts.staticApiand currently only triggers for components, because in-project codegen runs locally without the server-side function analysis that components have viastartPush.analysis.Proposed change
Make in-project
api.d.tscodegen produce the inlined-FunctionReferenceshape too. The function metadata (args validator JSON, returns validator JSON, visibility, udfType) needed byvalidatorToTypeis statically derivable from the.tssource files: every registered function isexport const X = (mutation|query|action|internalMutation|internalQuery|internalAction)({args: v.object({...}), returns: v...., handler: ...}). The CLI can parse each backend file's AST during codegen, extract those validator literals, and synthesize the sameFunctionReferencestrings that the component path emits today.Concretely:
codegenStaticApiFromSources(modulePaths)that:export constdeclarationssecureMutation/secureQueryfrom convex-helpers)argsandreturnsvalidator object literals; convert viavalidatorToTypeFunctionReference<udfType, visibility, argsType, returnsType>per leafbuildApiTreedoes for components--codegen=staticfor new projects (gated by a flag for backward compat); existing projects opt in.Workaround we're shipping while waiting
Local post-codegen step using the TS Compiler API:
node scripts/flatten-api-dts.mjs. Walks the convex/ module graph, identifies registered functions viaRegisteredQuery|Mutation|Actionalias-symbol detection (with AST fallback for files where wrapper imports are broken or use convex-helperssecure*), and rewrites api.d.ts in the inlined shape. Runs afterconvex deployand is enforced by a pre-commit gate that rejects unflattened api.d.ts.Result on the same monorepo: mobile-rn drops from 1,199 → 120 backend files, 3.43 GB → 2.13 GB, 47s → 11s. web-react drops from 1,200 → 113 files, 3.66 GB → 2.36 GB, 58s → 21s. Zero new errors caused by the flatten itself; 13 errors surfaced that the dynamic shape was hiding by typing missing functions as
any(real codebase bugs we wanted to find).Happy to open the PR if there's interest in landing this upstream — the script we wrote is the prototype; a CLI-native version would be cleaner because it has access to convex's own
parseValidator/validatorToTypeand doesn't need to spin up a TS Program just to walk types.