Skip to content

Commit 8bd2cd3

Browse files
thomasballingerConvex, Inc.
authored andcommitted
TypeScript codegen (#42662)
Support for coden using .ts file extensions - components now always generate .ts files in their _generated/ directories - new `legacyJavaScriptFileType` property in convex.json (defaults to true) can be set to false to generate .ts files in `convex/_generated/*` - rename `useComponentApiImports` to `legacyComponentApi` in convex.json (defaults to true) - updated codegen for .d.ts and .js files, some unused imports have been removed and inline component interfaces are now sorted within a file GitOrigin-RevId: d5217ade9c737bfaa7b028c852270ad867d2436a
1 parent efd304e commit 8bd2cd3

File tree

12 files changed

+1211
-405
lines changed

12 files changed

+1211
-405
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@
196196
"test-not-silent": "vitest",
197197
"typecheck": "tsc --noEmit --emitDeclarationOnly false",
198198
"test-esm": "node ./scripts/test-esm.mjs && ./scripts/checkdeps.mjs && ./scripts/checkimports.mjs",
199+
"compare-codegen": "node scripts/compare-codegen.mjs",
199200
"generateManagementApiSpec": "openapi-typescript ./management-openapi.json --output ./src/cli/generatedApi.ts --root-types --root-types-no-schema-prefix",
200201
"checkManagementApiSpec": "openapi-typescript ./management-openapi.json --output ./src/cli/generatedApi.ts --root-types --root-types-no-schema-prefix --check",
201202
"generateFunctionLogsApiSpec": "openapi-typescript ./function-logs-openapi.json --output ./src/cli/lib/generatedFunctionLogsApi.ts --root-types --root-types-no-schema-prefix",

schemas/convex.schema.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@
5454
"type": "object",
5555
"description": "Configuration for Convex code generation.",
5656
"properties": {
57+
"legacyJavaScriptFileType": {
58+
"type": "boolean",
59+
"description": "When true (or omitted, default is true), generates separate .js and .d.ts files for the Convex API. When false generates combined .ts files instead. The .ts format is more modern and recommended for TypeScript projects.\n\nNote: This option must be true when using generateCommonJSApi.",
60+
"default": true
61+
},
62+
"legacyComponentApi": {
63+
"type": "boolean",
64+
"description": "When true (or omitted, default is true), generates inline expanded types for component APIs. When false, generates import-based component API references instead. The import-based format is more modern and recommended for projects using components.\n\nNote: When false, component.ts files are automatically generated for each component.",
65+
"default": true
66+
},
5767
"staticApi": {
5868
"type": "boolean",
5969
"description": "When true, generates static `api.d.ts` files, which improves autocomplete and incremental typechecking performance in large codebases.\n\nDocumentation: https://docs.convex.dev/production/project-configuration#using-static-code-generation-beta",

scripts/compare-codegen.mjs

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Script to compare directly-generated JS/DTS files with TS-compiled outputs.
4+
*
5+
* This creates two directories with the different codegen approaches and
6+
* automatically opens the diffs in VS Code.
7+
*/
8+
9+
import { apiCodegen } from "../dist/esm/cli/codegen_templates/api.js";
10+
import { serverCodegen } from "../dist/esm/cli/codegen_templates/server.js";
11+
import { noSchemaDataModelDTS } from "../dist/esm/cli/codegen_templates/dataModel.js";
12+
import * as fs from "fs";
13+
import * as path from "path";
14+
import * as os from "os";
15+
import { execSync } from "child_process";
16+
import prettier from "prettier";
17+
18+
const modulePaths = ["foo.ts", "bar/baz.ts"];
19+
20+
console.log("Generating codegen outputs for comparison...\n");
21+
22+
// Generate all versions of our files
23+
const apiJsDts = apiCodegen(modulePaths, { generateJavaScriptApi: true });
24+
const apiJs = apiJsDts.JS;
25+
const apiDts = apiJsDts.DTS;
26+
const apiTsResult = apiCodegen(modulePaths, { generateJavaScriptApi: false });
27+
const apiTs = apiTsResult.TS;
28+
29+
const serverJsDts = serverCodegen({ generateJavaScriptApi: true });
30+
const serverJs = serverJsDts.JS;
31+
const serverDts = serverJsDts.DTS;
32+
const serverTsResult = serverCodegen({ generateJavaScriptApi: false });
33+
const serverTs = serverTsResult.TS;
34+
35+
const dataModelDts = noSchemaDataModelDTS();
36+
37+
const tmpDir = fs.mkdtempSync(
38+
path.join(os.tmpdir(), "convex-codegen-compare-"),
39+
);
40+
41+
try {
42+
// Create directory structure
43+
const directDir = path.join(tmpDir, "direct");
44+
const compiledDir = path.join(tmpDir, "compiled");
45+
const tsSourceDir = path.join(tmpDir, "ts_source");
46+
47+
fs.mkdirSync(directDir, { recursive: true });
48+
fs.mkdirSync(compiledDir, { recursive: true });
49+
fs.mkdirSync(tsSourceDir);
50+
51+
// Write directly-generated JS/DTS files (formatted with prettier)
52+
const formattedApiJs = await prettier.format(apiJs, {
53+
parser: "typescript",
54+
pluginSearchDirs: false,
55+
});
56+
const formattedApiDts = await prettier.format(apiDts, {
57+
parser: "typescript",
58+
pluginSearchDirs: false,
59+
});
60+
const formattedServerJs = await prettier.format(serverJs, {
61+
parser: "typescript",
62+
pluginSearchDirs: false,
63+
});
64+
const formattedServerDts = await prettier.format(serverDts, {
65+
parser: "typescript",
66+
pluginSearchDirs: false,
67+
});
68+
const formattedDataModelDts = await prettier.format(dataModelDts, {
69+
parser: "typescript",
70+
pluginSearchDirs: false,
71+
});
72+
73+
fs.writeFileSync(path.join(directDir, "api.js"), formattedApiJs);
74+
fs.writeFileSync(path.join(directDir, "api.d.ts"), formattedApiDts);
75+
fs.writeFileSync(path.join(directDir, "server.js"), formattedServerJs);
76+
fs.writeFileSync(path.join(directDir, "server.d.ts"), formattedServerDts);
77+
fs.writeFileSync(
78+
path.join(directDir, "dataModel.d.ts"),
79+
formattedDataModelDts,
80+
);
81+
82+
// Format TS files with prettier BEFORE compilation (matching runtime behavior)
83+
const formattedApiTs = await prettier.format(apiTs, {
84+
parser: "typescript",
85+
pluginSearchDirs: false,
86+
});
87+
const formattedServerTs = await prettier.format(serverTs, {
88+
parser: "typescript",
89+
pluginSearchDirs: false,
90+
});
91+
const formattedDataModelTs = await prettier.format(dataModelDts, {
92+
parser: "typescript",
93+
pluginSearchDirs: false,
94+
});
95+
96+
// Write the formatted TS files we'll compile
97+
fs.writeFileSync(path.join(tsSourceDir, "api.ts"), formattedApiTs);
98+
fs.writeFileSync(path.join(tsSourceDir, "server.ts"), formattedServerTs);
99+
fs.writeFileSync(
100+
path.join(tsSourceDir, "dataModel.ts"),
101+
formattedDataModelTs,
102+
);
103+
104+
// Create tsconfig.json for compilation
105+
const tsconfig = {
106+
compilerOptions: {
107+
target: "ES2020",
108+
module: "ES2020",
109+
declaration: true,
110+
emitDeclarationOnly: false,
111+
skipLibCheck: true,
112+
esModuleInterop: true,
113+
moduleResolution: "bundler",
114+
outDir: compiledDir,
115+
rootDir: tsSourceDir,
116+
paths: {
117+
"convex/server": [
118+
path.join(
119+
tmpDir,
120+
"node_modules/convex/dist/esm-types/server/index.d.ts",
121+
),
122+
],
123+
"convex/values": [
124+
path.join(
125+
tmpDir,
126+
"node_modules/convex/dist/esm-types/values/index.d.ts",
127+
),
128+
],
129+
},
130+
},
131+
include: [path.join(tsSourceDir, "*.ts")],
132+
};
133+
fs.writeFileSync(
134+
path.join(tmpDir, "tsconfig.json"),
135+
JSON.stringify(tsconfig, null, 2),
136+
);
137+
138+
// Set up the convex package
139+
const convexPackageDir = path.join(tmpDir, "node_modules", "convex");
140+
const convexDistDir = path.join(convexPackageDir, "dist");
141+
fs.mkdirSync(convexDistDir, { recursive: true });
142+
143+
const projectRoot = process.cwd();
144+
const distDir = path.join(projectRoot, "dist");
145+
146+
// Copy package.json
147+
fs.copyFileSync(
148+
path.join(projectRoot, "package.json"),
149+
path.join(convexPackageDir, "package.json"),
150+
);
151+
152+
// Symlink dist directories
153+
fs.symlinkSync(path.join(distDir, "esm"), path.join(convexDistDir, "esm"));
154+
fs.symlinkSync(
155+
path.join(distDir, "esm-types"),
156+
path.join(convexDistDir, "esm-types"),
157+
);
158+
159+
// Create stub user modules that are imported by api.ts
160+
for (const modulePath of modulePaths) {
161+
const fullPath = path.join(tmpDir, modulePath);
162+
const dir = path.dirname(fullPath);
163+
fs.mkdirSync(dir, { recursive: true });
164+
fs.writeFileSync(fullPath.replace(/\.ts$/, ".js"), "export const foo = 1;");
165+
fs.writeFileSync(
166+
fullPath.replace(/\.ts$/, ".d.ts"),
167+
"export declare const foo: number;",
168+
);
169+
}
170+
171+
// Compile the TS files
172+
const tscPath = path.join(projectRoot, "node_modules", ".bin", "tsc");
173+
try {
174+
execSync(`${tscPath} --project ${path.join(tmpDir, "tsconfig.json")}`, {
175+
cwd: tmpDir,
176+
stdio: "pipe",
177+
});
178+
} catch (error) {
179+
console.error("TypeScript compilation failed:");
180+
console.error(error.stdout?.toString());
181+
console.error(error.stderr?.toString());
182+
process.exit(1);
183+
}
184+
185+
// Format the compiled outputs with prettier
186+
for (const file of [
187+
"api.js",
188+
"api.d.ts",
189+
"server.js",
190+
"server.d.ts",
191+
"dataModel.d.ts",
192+
]) {
193+
const filePath = path.join(compiledDir, file);
194+
const content = fs.readFileSync(filePath, "utf-8");
195+
const formatted = await prettier.format(content, {
196+
parser: "typescript",
197+
pluginSearchDirs: false,
198+
});
199+
fs.writeFileSync(filePath, formatted);
200+
}
201+
202+
console.log("✓ Generated codegen outputs\n");
203+
console.log("Directories created:");
204+
console.log(` Direct (JS/DTS): ${directDir}`);
205+
console.log(` Compiled (TS): ${compiledDir}`);
206+
console.log(` TS Source: ${tsSourceDir}`);
207+
console.log();
208+
console.log("To compare with diff:");
209+
console.log(` diff -r ${directDir} ${compiledDir}`);
210+
console.log();
211+
console.log("To open diffs in VS Code:");
212+
const filesToCompare = [
213+
"api.d.ts",
214+
"api.js",
215+
"server.d.ts",
216+
"server.js",
217+
"dataModel.d.ts",
218+
];
219+
for (const file of filesToCompare) {
220+
console.log(` code --diff ${directDir}/${file} ${compiledDir}/${file}`);
221+
}
222+
console.log();
223+
} catch (error) {
224+
console.error("Error:", error);
225+
// Clean up on error
226+
fs.rmSync(tmpDir, { recursive: true, force: true });
227+
process.exit(1);
228+
}

src/cli/codegen_templates/api.ts

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,15 @@ export function moduleIdentifier(modulePath: string) {
7575
return safeModulePath;
7676
}
7777

78-
export function apiCodegen(modulePaths: string[]) {
79-
const apiDTS = `${header("Generated `api` utility.")}
78+
export function apiCodegen(
79+
modulePaths: string[],
80+
opts?: { useTypeScript?: boolean },
81+
) {
82+
const useTypeScript = opts?.useTypeScript ?? false;
83+
84+
if (!useTypeScript) {
85+
// Generate separate .js and .d.ts files
86+
const apiDTS = `${header("Generated `api` utility.")}
8087
import type { ApiFromModules, FilterApi, FunctionReference } from "convex/server";
8188
${modulePaths
8289
.map(
@@ -107,7 +114,7 @@ export function apiCodegen(modulePaths: string[]) {
107114
export declare const internal: FilterApi<typeof fullApi, FunctionReference<any, "internal">>;
108115
`;
109116

110-
const apiJS = `${header("Generated `api` utility.")}
117+
const apiJS = `${header("Generated `api` utility.")}
111118
import { anyApi } from "convex/server";
112119
113120
/**
@@ -121,8 +128,55 @@ export function apiCodegen(modulePaths: string[]) {
121128
export const api = anyApi;
122129
export const internal = anyApi;
123130
`;
124-
return {
125-
DTS: apiDTS,
126-
JS: apiJS,
127-
};
131+
return {
132+
DTS: apiDTS,
133+
JS: apiJS,
134+
};
135+
} else {
136+
// Generate combined .ts file
137+
const apiTS = `${header("Generated `api` utility.")}
138+
import type { ApiFromModules, FilterApi, FunctionReference } from "convex/server";
139+
import { anyApi } from "convex/server";
140+
${modulePaths
141+
.map(
142+
(modulePath) =>
143+
`import type * as ${moduleIdentifier(modulePath)} from "../${importPath(
144+
modulePath,
145+
)}.js";`,
146+
)
147+
.join("\n")}
148+
149+
const fullApi: ApiFromModules<{
150+
${modulePaths
151+
.map(
152+
(modulePath) =>
153+
`"${importPath(modulePath)}": typeof ${moduleIdentifier(modulePath)},`,
154+
)
155+
.join("\n")}
156+
}> = anyApi as any;
157+
158+
/**
159+
* A utility for referencing Convex functions in your app's public API.
160+
*
161+
* Usage:
162+
* \`\`\`js
163+
* const myFunctionReference = api.myModule.myFunction;
164+
* \`\`\`
165+
*/
166+
export const api: FilterApi<typeof fullApi, FunctionReference<any, "public">> = anyApi as any;
167+
168+
/**
169+
* A utility for referencing Convex functions in your app's internal API.
170+
*
171+
* Usage:
172+
* \`\`\`js
173+
* const myFunctionReference = internal.myModule.myFunction;
174+
* \`\`\`
175+
*/
176+
export const internal: FilterApi<typeof fullApi, FunctionReference<any, "internal">> = anyApi as any;
177+
`;
178+
return {
179+
TS: apiTS,
180+
};
181+
}
128182
}

src/cli/codegen_templates/common.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,14 @@ export function apiComment(
2525
* \`\`\`
2626
*/`;
2727
}
28+
29+
/**
30+
* Comparison function for sorting strings alphabetically.
31+
* Uses localeCompare for consistent, locale-aware sorting.
32+
*
33+
* Usage: array.sort(compareStrings)
34+
* or with entries: Object.entries(obj).sort(([a], [b]) => compareStrings(a, b))
35+
*/
36+
export function compareStrings(a: string, b: string): number {
37+
return a.localeCompare(b);
38+
}

0 commit comments

Comments
 (0)