Skip to main content
Glama
codegen.ts24.8 kB
import path from "path"; import prettier from "prettier"; import { withTmpDir, TempDir } from "../../bundler/fs.js"; import { entryPoints } from "../../bundler/index.js"; import { apiCodegen } from "../codegen_templates/api.js"; import { apiCjsCodegen } from "../codegen_templates/api_cjs.js"; import { dynamicDataModelDTS, dynamicDataModelTS, noSchemaDataModelDTS, noSchemaDataModelTS, staticDataModelDTS, staticDataModelTS, } from "../codegen_templates/dataModel.js"; import { readmeCodegen } from "../codegen_templates/readme.js"; import { serverCodegen } from "../codegen_templates/server.js"; import { tsconfigCodegen } from "../codegen_templates/tsconfig.js"; import { Context } from "../../bundler/context.js"; import { logError, logMessage, logOutput, logVerbose, } from "../../bundler/log.js"; import { typeCheckFunctionsInMode, TypeCheckMode } from "./typecheck.js"; import { configFilepath, readProjectConfig, usesTypeScriptCodegen, usesComponentApiImports, } from "./config.js"; import { recursivelyDelete } from "./fsUtils.js"; import { componentServerTS } from "../codegen_templates/component_server.js"; import { ComponentDirectory, isComponentDirectory, } from "./components/definition/directoryStructure.js"; import { StartPushResponse } from "./deployApi/startPush.js"; import { componentApiDTS, componentApiJs, componentApiStubDTS, componentApiStubTS, componentApiTSWithTypes, componentTS, rootComponentApiCJS, } from "../codegen_templates/component_api.js"; import { functionsDir } from "./utils/utils.js"; import { LargeIndexDeletionCheck } from "./indexes.js"; export type CodegenOptions = { url?: string | undefined; adminKey?: string | undefined; dryRun: boolean; debug: boolean; typecheck: TypeCheckMode; init: boolean; commonjs: boolean; liveComponentSources: boolean; debugNodeApis: boolean; systemUdfs: boolean; largeIndexDeletionCheck: LargeIndexDeletionCheck; codegenOnlyThisComponent?: string | undefined; }; export async function doInitialCodegen( ctx: Context, options: { init: boolean }, ) { const { projectConfig: existingProjectConfig } = await readProjectConfig(ctx); const configPath = await configFilepath(ctx); const functionsPath = functionsDir(configPath, existingProjectConfig); if (options.init) { await doInitCodegen(ctx, functionsPath, true); } const componentDir = isComponentDirectory(ctx, functionsPath, true); if (componentDir.kind === "err") { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: `Invalid component directory: ${componentDir.why}`, }); } if (componentDir.component.isRootWithoutConfig) { // Disable typechecking since there isn't any code yet. await doCodegen(ctx, functionsPath, "disable"); } else { await withTmpDir(async (tmpDir) => { await doInitialComponentCodegen(ctx, tmpDir, componentDir.component); }); } } export async function doInitCodegen( ctx: Context, functionsDir: string, skipIfExists: boolean, opts?: { dryRun?: boolean; debug?: boolean }, ): Promise<void> { await prepareForCodegen(ctx, functionsDir, opts); await withTmpDir(async (tmpDir) => { await doReadmeCodegen(ctx, tmpDir, functionsDir, skipIfExists, opts); await doTsconfigCodegen(ctx, tmpDir, functionsDir, skipIfExists, opts); }); } async function prepareForCodegen( ctx: Context, functionsDir: string, opts?: { dryRun?: boolean }, ) { // Delete the old _generated.ts because v0.1.2 used to put the react generated // code there const legacyCodegenPath = path.join(functionsDir, "_generated.ts"); if (ctx.fs.exists(legacyCodegenPath)) { if (opts?.dryRun) { logError( `Command would delete legacy codegen file: ${legacyCodegenPath}}`, ); } else { logError(`Deleting legacy codegen file: ${legacyCodegenPath}}`); ctx.fs.unlink(legacyCodegenPath); } } // Create the codegen dir if it doesn't already exist. const codegenDir = path.join(functionsDir, "_generated"); ctx.fs.mkdir(codegenDir, { allowExisting: true, recursive: true }); return codegenDir; } /** Codegen only for an application (a root component) */ export async function doCodegen( ctx: Context, functionsDir: string, typeCheckMode: TypeCheckMode, opts?: { dryRun?: boolean; generateCommonJSApi?: boolean; debug?: boolean }, ) { const { projectConfig } = await readProjectConfig(ctx); const codegenDir = await prepareForCodegen(ctx, functionsDir, opts); await withTmpDir(async (tmpDir) => { // Write files in dependency order so a watching dev server doesn't // see inconsistent results where a file we write imports from a // file that doesn't exist yet. We'll collect all the paths we write // and then delete any remaining paths at the end. const writtenFiles = []; const useTypeScript = usesTypeScriptCodegen(projectConfig); const generateCommonJSApi = opts?.generateCommonJSApi || projectConfig.generateCommonJSApi; // First, `dataModel.d.ts` imports from the developer's `schema.js` file. const schemaFiles = await doDataModelCodegen( ctx, tmpDir, functionsDir, codegenDir, useTypeScript, opts, ); writtenFiles.push(...schemaFiles); // Next, the `server.d.ts` file imports from `dataModel.d.ts`. const serverFiles = await writeServerFiles( ctx, tmpDir, codegenDir, useTypeScript, opts, ); writtenFiles.push(...serverFiles); // The `api.d.ts` file imports from the developer's modules, which then // import from `server.d.ts`. Note that there's a cycle here, since the // developer's modules could also import from the `api.{js,d.ts}` files. const apiFiles = await doApiCodegen( ctx, tmpDir, functionsDir, codegenDir, useTypeScript, generateCommonJSApi, opts, ); writtenFiles.push(...apiFiles); // Cleanup any files that weren't written in this run. // Skip cleanup in debug mode since we don't actually write files in that mode. if (!opts?.debug) { for (const file of ctx.fs.listDir(codegenDir)) { if (!writtenFiles.includes(file.name)) { recursivelyDelete(ctx, path.join(codegenDir, file.name), opts); } } } // Generated code is updated, typecheck the query and mutation functions. await typeCheckFunctionsInMode(ctx, typeCheckMode, functionsDir); }); } // Just enough to be able to bundle code for analysis: we need an api proxy object // so that imports aren't broken, we need basics in server, we need something in // data model. export async function doInitialComponentCodegen( ctx: Context, tmpDir: TempDir, componentDirectory: ComponentDirectory, opts?: { dryRun?: boolean; generateCommonJSApi?: boolean; debug?: boolean; verbose?: boolean; }, ) { const { projectConfig } = await readProjectConfig(ctx); if (isPublishedPackage(componentDirectory)) { if (opts?.verbose) { logMessage( `skipping initial codegen for installed package ${componentDirectory.path}`, ); } return; } const codegenDir = await prepareForCodegen( ctx, componentDirectory.path, opts, ); // Write files in dependency order so a watching dev server doesn't // see inconsistent results where a file we write imports from a // file that doesn't exist yet. We'll collect all the paths we write // and then delete any remaining paths at the end. const writtenFiles = []; // Non-root components always use .ts files; root components respect the config // But for initial (placeholder, stub) codegen we don't care, just use .d.ts and .js. const useTypeScript = !componentDirectory.isRoot || usesTypeScriptCodegen(projectConfig); const generateCommonJSApi = opts?.generateCommonJSApi || projectConfig.generateCommonJSApi; // First, `dataModel.d.ts` imports from the developer's `schema.js` file. const dataModelFiles = await doInitialComponentDataModelCodegen( ctx, tmpDir, componentDirectory, codegenDir, useTypeScript, opts, ); writtenFiles.push(...dataModelFiles); // Next, the `server.d.ts` file imports from `dataModel.d.ts`. const serverFiles = await doInitialComponentServerCodegen( ctx, componentDirectory.isRoot, tmpDir, codegenDir, useTypeScript, opts, ); writtenFiles.push(...serverFiles); // The `api.d.ts` file imports from the developer's modules, which then // import from `server.d.ts`. Note that there's a cycle here, since the // developer's modules could also import from the `api.{js,d.ts}` files. const apiFiles = await doInitialComponentApiCodegen( ctx, componentDirectory.isRoot, tmpDir, codegenDir, useTypeScript, generateCommonJSApi, opts, ); writtenFiles.push(...apiFiles); // component.ts is generated in doFinalComponentCodegen, but don't delete // if it already exists from a previous full codegen run. if (!componentDirectory.isRoot) { const componentTSPath = path.join(codegenDir, "component.ts"); if (ctx.fs.exists(componentTSPath)) { writtenFiles.push("component.ts"); } } // Cleanup any files that weren't written in this run. // Skip cleanup in debug mode since we don't actually write files in that mode. if (!opts?.debug) { for (const file of ctx.fs.listDir(codegenDir)) { if (!writtenFiles.includes(file.name)) { recursivelyDelete(ctx, path.join(codegenDir, file.name), opts); } } } } /* This component defined in a dist directory; it is probably in a node_module * directory, installed from a package. It is stuck with the files it has. * Heuristics for this: * - component definition has a dist/ directory as an ancestor * - component definition is a .js file * - presence of .js.map files * We may improve this heuristic. */ export function isPublishedPackage(componentDirectory: ComponentDirectory) { return ( componentDirectory.definitionPath.endsWith(".js") && !componentDirectory.isRoot ); } // Handles root and non-root components; it's "component" codegen because // it's not the old legacy path. export async function doFinalComponentCodegen( ctx: Context, tmpDir: TempDir, rootComponent: ComponentDirectory, componentDirectory: ComponentDirectory, startPushResponse: StartPushResponse, componentsMap: Map<string, ComponentDirectory>, opts?: { dryRun?: boolean; debug?: boolean; generateCommonJSApi?: boolean; }, ) { const { projectConfig } = await readProjectConfig(ctx); const isPublishedPackage = componentDirectory.definitionPath.endsWith(".js") && !componentDirectory.isRoot; // We never codegen for a published package (you need to link to the convex.config.ts file instead). if (isPublishedPackage) { return; } const codegenDir = path.join(componentDirectory.path, "_generated"); ctx.fs.mkdir(codegenDir, { allowExisting: true, recursive: true }); // Non-root components always use .ts files; root components respect the config const useTypeScript = !componentDirectory.isRoot || usesTypeScriptCodegen(projectConfig); // `dataModel` and `api` files depend on analyze results so will get replaced // in the later post-analysis codegen phase, but `server` files don't need // analysis info so the stubs from initial codegen are sufficient. // dataModel const hasSchemaFile = schemaFileExists(ctx, componentDirectory.path); let dataModelContents: string; if (hasSchemaFile) { if (projectConfig.codegen.staticDataModel) { dataModelContents = useTypeScript ? await staticDataModelTS( ctx, startPushResponse, rootComponent, componentDirectory, ) : await staticDataModelDTS( ctx, startPushResponse, rootComponent, componentDirectory, ); } else { dataModelContents = useTypeScript ? dynamicDataModelTS() : dynamicDataModelDTS(); } } else { dataModelContents = useTypeScript ? noSchemaDataModelTS() : noSchemaDataModelDTS(); } const dataModelPath = path.join( codegenDir, useTypeScript ? "dataModel.ts" : "dataModel.d.ts", ); await writeFormattedFile( ctx, tmpDir, dataModelContents, "typescript", dataModelPath, opts, ); // component.ts if (!componentDirectory.isRoot) { const componentTSPath = path.join(codegenDir, "component.ts"); const componentTSContents = await componentTS( ctx, startPushResponse, rootComponent, componentDirectory, ); await writeFormattedFile( ctx, tmpDir, componentTSContents, "typescript", componentTSPath, opts, ); } // server.ts - regenerate it in final codegen for consistency, even though // the stub from initial codegen would be sufficient. await writeServerFilesForComponent( ctx, componentDirectory.isRoot, tmpDir, codegenDir, useTypeScript, opts, ); // api if (!useTypeScript) { const apiDTSPath = path.join(codegenDir, "api.d.ts"); const apiContents = await componentApiDTS( ctx, startPushResponse, rootComponent, componentDirectory, componentsMap, { staticApi: projectConfig.codegen.staticApi, useComponentApiImports: usesComponentApiImports(projectConfig), }, ); await writeFormattedFile( ctx, tmpDir, apiContents, "typescript", apiDTSPath, opts, ); if (opts?.generateCommonJSApi || projectConfig.generateCommonJSApi) { const apiCjsDTSPath = path.join(codegenDir, "api_cjs.d.cts"); await writeFormattedFile( ctx, tmpDir, apiContents, "typescript", apiCjsDTSPath, opts, ); } } else { const apiTSPath = path.join(codegenDir, "api.ts"); const apiContents = await componentApiTSWithTypes( ctx, startPushResponse, rootComponent, componentDirectory, componentsMap, { staticApi: projectConfig.codegen.staticApi, useComponentApiImports: usesComponentApiImports(projectConfig), }, ); await writeFormattedFile( ctx, tmpDir, apiContents, "typescript", apiTSPath, opts, ); } } async function doReadmeCodegen( ctx: Context, tmpDir: TempDir, functionsDir: string, skipIfExists: boolean, opts?: { dryRun?: boolean; debug?: boolean }, ) { const readmePath = path.join(functionsDir, "README.md"); if (skipIfExists && ctx.fs.exists(readmePath)) { logVerbose(`Not overwriting README.md.`); return; } await writeFormattedFile( ctx, tmpDir, readmeCodegen(), "markdown", readmePath, opts, ); } async function doTsconfigCodegen( ctx: Context, tmpDir: TempDir, functionsDir: string, skipIfExists: boolean, opts?: { dryRun?: boolean; debug?: boolean }, ) { const tsconfigPath = path.join(functionsDir, "tsconfig.json"); if (skipIfExists && ctx.fs.exists(tsconfigPath)) { logVerbose(`Not overwriting tsconfig.json.`); return; } await writeFormattedFile( ctx, tmpDir, tsconfigCodegen(), "json", tsconfigPath, opts, ); } function schemaFileExists(ctx: Context, functionsDir: string) { let schemaPath = path.join(functionsDir, "schema.ts"); let hasSchemaFile = ctx.fs.exists(schemaPath); if (!hasSchemaFile) { schemaPath = path.join(functionsDir, "schema.js"); hasSchemaFile = ctx.fs.exists(schemaPath); } return hasSchemaFile; } async function doDataModelCodegen( ctx: Context, tmpDir: TempDir, functionsDir: string, codegenDir: string, useTypeScript: boolean, opts?: { dryRun?: boolean; debug?: boolean }, ) { const hasSchemaFile = schemaFileExists(ctx, functionsDir); const schemaContent = hasSchemaFile ? useTypeScript ? dynamicDataModelTS() : dynamicDataModelDTS() : useTypeScript ? noSchemaDataModelTS() : noSchemaDataModelDTS(); const filename = useTypeScript ? "dataModel.ts" : "dataModel.d.ts"; await writeFormattedFile( ctx, tmpDir, schemaContent, "typescript", path.join(codegenDir, filename), opts, ); return [filename]; } /** * Write server.ts/.js/.d.ts files for root components. * Returns list of filenames written. */ async function writeServerFiles( ctx: Context, tmpDir: TempDir, codegenDir: string, useTypeScript: boolean, opts?: { dryRun?: boolean; debug?: boolean }, ): Promise<string[]> { if (!useTypeScript) { const serverContent = serverCodegen({ useTypeScript: false }); await writeFormattedFile( ctx, tmpDir, serverContent.JS!, "typescript", path.join(codegenDir, "server.js"), opts, ); await writeFormattedFile( ctx, tmpDir, serverContent.DTS!, "typescript", path.join(codegenDir, "server.d.ts"), opts, ); return ["server.js", "server.d.ts"]; } else { const serverContent = serverCodegen({ useTypeScript: true }); await writeFormattedFile( ctx, tmpDir, serverContent.TS!, "typescript", path.join(codegenDir, "server.ts"), opts, ); return ["server.ts"]; } } /** * Write server.ts file for non-root components. * Returns list of filenames written. */ async function writeComponentServerFile( ctx: Context, tmpDir: TempDir, codegenDir: string, opts?: { dryRun?: boolean; debug?: boolean }, ): Promise<string[]> { const serverTSPath = path.join(codegenDir, "server.ts"); const serverTSContents = componentServerTS(false); await writeFormattedFile( ctx, tmpDir, serverTSContents, "typescript", serverTSPath, opts, ); return ["server.ts"]; } /** * Write server files for either root or non-root components. * Root components get server.ts/server.js/server.d.ts based on useTypeScript. * Non-root components always get server.ts. * Returns list of filenames written. */ async function writeServerFilesForComponent( ctx: Context, isRoot: boolean, tmpDir: TempDir, codegenDir: string, useTypeScript: boolean, opts?: { dryRun?: boolean; debug?: boolean }, ): Promise<string[]> { if (isRoot) { return await writeServerFiles(ctx, tmpDir, codegenDir, useTypeScript, opts); } else { return await writeComponentServerFile(ctx, tmpDir, codegenDir, opts); } } async function doInitialComponentServerCodegen( ctx: Context, isRoot: boolean, tmpDir: TempDir, codegenDir: string, useTypeScript: boolean, opts?: { dryRun?: boolean; debug?: boolean }, ) { return await writeServerFilesForComponent( ctx, isRoot, tmpDir, codegenDir, useTypeScript, opts, ); } async function doInitialComponentDataModelCodegen( ctx: Context, tmpDir: TempDir, componentDirectory: ComponentDirectory, codegenDir: string, useTypeScript: boolean, opts?: { dryRun?: boolean; debug?: boolean }, ) { const hasSchemaFile = schemaFileExists(ctx, componentDirectory.path); const dataModelContent = hasSchemaFile ? useTypeScript ? dynamicDataModelTS() : dynamicDataModelDTS() : useTypeScript ? noSchemaDataModelTS() : noSchemaDataModelDTS(); const filename = useTypeScript ? "dataModel.ts" : "dataModel.d.ts"; const dataModelPath = path.join(codegenDir, filename); // Don't write our stub if the file already exists, since it may have // better type information from `doFinalComponentDataModelCodegen`. if (!ctx.fs.exists(dataModelPath)) { await writeFormattedFile( ctx, tmpDir, dataModelContent, "typescript", dataModelPath, opts, ); } return [filename]; } async function doInitialComponentApiCodegen( ctx: Context, isRoot: boolean, tmpDir: TempDir, codegenDir: string, useTypeScript: boolean, generateCommonJSApi: boolean, opts?: { dryRun?: boolean; debug?: boolean }, ) { const writtenFiles: string[] = []; if (!useTypeScript) { const apiJS = componentApiJs(); await writeFormattedFile( ctx, tmpDir, apiJS, "typescript", path.join(codegenDir, "api.js"), opts, ); // Don't write the `.d.ts` stub if it already exists. const apiDTSPath = path.join(codegenDir, "api.d.ts"); const apiStubDTS = componentApiStubDTS(); if (!ctx.fs.exists(apiDTSPath)) { await writeFormattedFile( ctx, tmpDir, apiStubDTS, "typescript", apiDTSPath, opts, ); } writtenFiles.push("api.js", "api.d.ts"); if (generateCommonJSApi && isRoot) { const apiCjsJS = rootComponentApiCJS(); await writeFormattedFile( ctx, tmpDir, apiCjsJS, "typescript", path.join(codegenDir, "api_cjs.cjs"), opts, ); const cjsStubPath = path.join(codegenDir, "api_cjs.d.cts"); if (!ctx.fs.exists(cjsStubPath)) { await writeFormattedFile( ctx, tmpDir, apiStubDTS, "typescript", cjsStubPath, opts, ); } writtenFiles.push("api_cjs.cjs", "api_cjs.d.cts"); } } else { const apiTSPath = path.join(codegenDir, "api.ts"); const apiTS = componentApiStubTS(); // Don't write the `.ts` stub if it already exists. if (!ctx.fs.exists(apiTSPath)) { await writeFormattedFile( ctx, tmpDir, apiTS, "typescript", apiTSPath, opts, ); } writtenFiles.push("api.ts"); } return writtenFiles; } async function doApiCodegen( ctx: Context, tmpDir: TempDir, functionsDir: string, codegenDir: string, useTypeScript: boolean, generateCommonJSApi: boolean, opts?: { dryRun?: boolean; debug?: boolean }, ) { const absModulePaths = await entryPoints(ctx, functionsDir); const modulePaths = absModulePaths .map((p) => path.relative(functionsDir, p)) .sort(); const writtenFiles: string[] = []; if (!useTypeScript) { const apiContent = apiCodegen(modulePaths, { useTypeScript: false }); await writeFormattedFile( ctx, tmpDir, apiContent.JS!, "typescript", path.join(codegenDir, "api.js"), opts, ); await writeFormattedFile( ctx, tmpDir, apiContent.DTS!, "typescript", path.join(codegenDir, "api.d.ts"), opts, ); writtenFiles.push("api.js", "api.d.ts"); if (generateCommonJSApi) { const apiCjsContent = apiCjsCodegen(modulePaths); await writeFormattedFile( ctx, tmpDir, apiCjsContent.JS!, "typescript", path.join(codegenDir, "api_cjs.cjs"), opts, ); await writeFormattedFile( ctx, tmpDir, apiCjsContent.DTS!, "typescript", path.join(codegenDir, "api_cjs.d.cts"), opts, ); writtenFiles.push("api_cjs.cjs", "api_cjs.d.cts"); } } else { const apiContent = apiCodegen(modulePaths, { useTypeScript: true }); await writeFormattedFile( ctx, tmpDir, apiContent.TS!, "typescript", path.join(codegenDir, "api.ts"), opts, ); writtenFiles.push("api.ts"); } return writtenFiles; } async function writeFormattedFile( ctx: Context, tmpDir: TempDir, contents: string, filetype: string, destination: string, options?: { dryRun?: boolean; debug?: boolean; }, ) { // Run prettier so we don't have to think about formatting! // // This is a little sketchy because we are using the default prettier config // (not our user's one) but it's better than nothing. const formattedContents = await prettier.format(contents, { parser: filetype, pluginSearchDirs: false, }); if (options?.debug) { // NB: The `test_codegen_projects_are_up_to_date` smoke test depends // on this output format. logOutput(`# ${path.resolve(destination)}`); logOutput(formattedContents); return; } try { const existing = ctx.fs.readUtf8File(destination); if (existing === formattedContents) { return; } } catch (err: any) { if (err.code !== "ENOENT") { // eslint-disable-next-line no-restricted-syntax throw err; } } if (options?.dryRun) { logOutput(`Command would write file: ${destination}`); return; } const tmpPath = tmpDir.writeUtf8File(formattedContents); ctx.fs.swapTmpFile(tmpPath, destination); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server