Skip to content

Commit

Permalink
Move components to _generated/api.js (#29910)
Browse files Browse the repository at this point in the history
GitOrigin-RevId: eb57cab2ff54a569bd09ac11645050078c0150c7
  • Loading branch information
thomasballinger authored and Convex, Inc. committed Sep 18, 2024
1 parent b1be06d commit b999487
Show file tree
Hide file tree
Showing 3 changed files with 244 additions and 270 deletions.
245 changes: 241 additions & 4 deletions src/cli/codegen_templates/component_api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import path from "path";
import { z } from "zod";
import { Context } from "../../bundler/context.js";
import { entryPoints } from "../../bundler/index.js";
import {
Expand All @@ -12,15 +13,20 @@ import {
ComponentExports,
EvaluatedComponentDefinition,
} from "../lib/deployApi/componentDefinition.js";
import { Identifier } from "../lib/deployApi/types.js";
import { ComponentDefinitionPath } from "../lib/deployApi/paths.js";
import { resolveFunctionReference } from "./component_server.js";
import { Identifier, Reference } from "../lib/deployApi/types.js";
import {
ConvexValidator,
convexValidator,
} from "../lib/deployApi/validator.js";
import { CanonicalizedModulePath } from "../lib/deployApi/paths.js";
import { Value, jsonToConvex } from "../../values/value.js";

export function componentApiJs() {
const lines = [];
lines.push(header("Generated `api` utility."));
lines.push(`
import { anyApi } from "convex/server";
import { anyApi, componentsGeneric } from "convex/server";
/**
* A utility for referencing Convex functions in your app's API.
Expand All @@ -32,6 +38,7 @@ export function componentApiJs() {
*/
export const api = anyApi;
export const internal = anyApi;
export const components = componentsGeneric();
`);
return lines.join("\n");
}
Expand All @@ -50,11 +57,13 @@ export function rootComponentApiCJS() {
export function componentApiStubDTS() {
const lines = [];
lines.push(header("Generated `api` utility."));
lines.push(`import type { AnyApi } from "convex/server";`);
lines.push(`import type { AnyApi, AnyComponents } from "convex/server";`);
lines.push(`
export declare const api: AnyApi;
export declare const internal: AnyApi;
export declare const components: AnyComponents;
`);

return lines.join("\n");
}

Expand Down Expand Up @@ -113,6 +122,38 @@ export async function componentApiDTS(
export declare const api: FilterApi<typeof fullApiWithMounts, FunctionReference<any, "public">>;
export declare const internal: FilterApi<typeof fullApiWithMounts, FunctionReference<any, "internal">>;
`);

lines.push(`
export declare const components: {`);

const analysis = startPush.analysis[definitionPath];
if (!analysis) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `No analysis found for component ${definitionPath} orig: ${definitionPath}\nin\n${Object.keys(startPush.analysis).toString()}`,
});
}
for (const childComponent of analysis.definition.childComponents) {
const childComponentAnalysis = startPush.analysis[childComponent.path];
if (!childComponentAnalysis) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `No analysis found for child component ${childComponent.path}`,
});
}
for await (const line of codegenExports(
ctx,
childComponent.name,
childComponentAnalysis,
)) {
lines.push(line);
}
}

lines.push("};");

return lines.join("\n");
}

Expand Down Expand Up @@ -257,3 +298,199 @@ async function buildComponentMountTree(
}
return nonEmpty ? result : null;
}

async function* codegenExports(
ctx: Context,
name: Identifier,
analysis: EvaluatedComponentDefinition,
): AsyncGenerator<string> {
yield `${name}: {`;
for (const [name, componentExport] of analysis.definition.exports.branch) {
yield `${name}:`;
yield* codegenExport(ctx, analysis, componentExport);
yield ",";
}
yield "},";
}

async function* codegenExport(
ctx: Context,
analysis: EvaluatedComponentDefinition,
componentExport: ComponentExports,
): AsyncGenerator<string> {
if (componentExport.type === "leaf") {
yield await resolveFunctionReference(
ctx,
analysis,
componentExport.leaf,
"internal",
);
} else if (componentExport.type === "branch") {
yield "{";
for (const [name, childExport] of componentExport.branch) {
yield `${name}:`;
yield* codegenExport(ctx, analysis, childExport);
yield ",";
}
yield "}";
}
}

export async function resolveFunctionReference(
ctx: Context,
analysis: EvaluatedComponentDefinition,
reference: Reference,
visibility: "public" | "internal",
) {
if (!reference.startsWith("_reference/function/")) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Invalid function reference: ${reference}`,
});
}
const udfPath = reference.slice("_reference/function/".length);

const [modulePath, functionName] = udfPath.split(":");
const canonicalizedModulePath = canonicalizeModulePath(modulePath);

const analyzedModule = analysis.functions[canonicalizedModulePath];
if (!analyzedModule) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Module not found: ${modulePath}`,
});
}
const analyzedFunction = analyzedModule.functions.find(
(f) => f.name === functionName,
);
if (!analyzedFunction) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Function not found: ${functionName}`,
});
}

// The server sends down `udfType` capitalized.
const udfType = analyzedFunction.udfType.toLowerCase();

let argsType = "any";
try {
const argsValidator = parseValidator(analyzedFunction.args);
if (argsValidator) {
if (argsValidator.type === "object" || argsValidator.type === "any") {
argsType = validatorToType(argsValidator);
} else {
// eslint-disable-next-line no-restricted-syntax
throw new Error(
`Unexpected argument validator type: ${argsValidator.type}`,
);
}
}
} catch (e) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Invalid function args: ${analyzedFunction.args}`,
errForSentry: e,
});
}

let returnsType = "any";
try {
const returnsValidator = parseValidator(analyzedFunction.returns);
if (returnsValidator) {
returnsType = validatorToType(returnsValidator);
}
} catch (e) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Invalid function returns: ${analyzedFunction.returns}`,
errForSentry: e,
});
}

return `FunctionReference<"${udfType}", "${visibility}", ${argsType}, ${returnsType}>`;
}

function parseValidator(validator: string | null): ConvexValidator | null {
if (!validator) {
return null;
}
return z.nullable(convexValidator).parse(JSON.parse(validator));
}

function canonicalizeModulePath(modulePath: string): CanonicalizedModulePath {
if (!modulePath.endsWith(".js")) {
return modulePath + ".js";
}
return modulePath;
}

function validatorToType(validator: ConvexValidator): string {
if (validator.type === "null") {
return "null";
} else if (validator.type === "number") {
return "number";
} else if (validator.type === "bigint") {
return "bigint";
} else if (validator.type === "boolean") {
return "boolean";
} else if (validator.type === "string") {
return "string";
} else if (validator.type === "bytes") {
return "ArrayBuffer";
} else if (validator.type === "any") {
return "any";
} else if (validator.type === "literal") {
const convexValue = jsonToConvex(validator.value);
return convexValueToLiteral(convexValue);
} else if (validator.type === "id") {
return "string";
} else if (validator.type === "array") {
return `Array<${validatorToType(validator.value)}>`;
} else if (validator.type === "record") {
return `Record<${validatorToType(validator.keys)}, ${validatorToType(validator.values.fieldType)}>`;
} else if (validator.type === "union") {
return validator.value.map(validatorToType).join(" | ");
} else if (validator.type === "object") {
return objectValidatorToType(validator.value);
} else {
// eslint-disable-next-line no-restricted-syntax
throw new Error(`Unsupported validator type`);
}
}

function objectValidatorToType(
fields: Record<string, { fieldType: ConvexValidator; optional: boolean }>,
): string {
const fieldStrings: string[] = [];
for (const [fieldName, field] of Object.entries(fields)) {
const fieldType = validatorToType(field.fieldType);
fieldStrings.push(`${fieldName}${field.optional ? "?" : ""}: ${fieldType}`);
}
return `{ ${fieldStrings.join(", ")} }`;
}

function convexValueToLiteral(value: Value): string {
if (value === null) {
return "null";
}
if (typeof value === "bigint") {
return `${value}n`;
}
if (typeof value === "number") {
return `${value}`;
}
if (typeof value === "boolean") {
return `${value}`;
}
if (typeof value === "string") {
return `"${value}"`;
}
// eslint-disable-next-line no-restricted-syntax
throw new Error(`Unsupported literal type`);
}
Loading

0 comments on commit b999487

Please sign in to comment.