import type { DiscoveredCapability } from "./discover.js"; /** * Generate the app entry module for a set of discovered capabilities. * * The module uses only static imports so that the bundler can bundle the * whole app into a single file. It is written to `.notion/entry.ts` in * the app project, so imports are relative to that location. */ export function generateEntry(capabilities: readonly DiscoveredCapability[]): string { const imports: string[] = []; const registrations: string[] = []; const registryEntries = new Map(); // A workflow's key comes from its file name, so the entry records it for // `context.workflows`, which receives the imported workflow object. const workflowKeyBindings: string[] = []; capabilities .filter((capability) => capability.runtime !== "build") .forEach((capability, index) => { const binding = `capability_${index}`; imports.push( `import ${binding} from ${JSON.stringify(`../${capability.sourcePath}`)};`, ); if (capability.type === "sync") { registrations.push( `bindSyncCapabilityKey(${binding}, ${JSON.stringify(capability.key)});`, ); } if (capability.type === "workflow") { workflowKeyBindings.push( `bindWorkflowKey(${binding}, ${JSON.stringify(capability.key)});`, ); } const entries = registryEntries.get(capability.type) ?? []; entries.push(`\t\t${JSON.stringify(capability.key)}: ${binding},`); registryEntries.set(capability.type, entries); }); const registry = Array.from(registryEntries.entries()) .map(([type, entries]) => `\t${JSON.stringify(type)}: {\n${entries.join("\n")}\n\t},`) .join("\n"); const syncBindingImport = registrations.length === 0 ? "" : 'import { bindSyncCapabilityKey } from "@notionhq/apps/sync";\n'; const workflowBindingImport = workflowKeyBindings.length === 0 ? "" : 'import { bindWorkflowKey } from "@notionhq/apps/workflow";\n'; return `// Generated by \`notion-apps build\`. Do not edit. ${syncBindingImport}${workflowBindingImport}${imports.join("\n")} ${[...registrations, ...workflowKeyBindings].join("\n")} type Handler = (context?: unknown, options?: { concreteOutput?: true }) => Promise; export const capabilities: Record> = { ${registry} }; // The platform dispatches a workflow's synchronous webhook verify run under its // own invocation type. It resolves to the same capability, which tells a verify // invocation from a trigger event by the shape of its input. const invocationTypes: Record = { workflowWebhookVerify: "workflow", }; export async function run( type: string, key: string, context?: unknown, options?: { concreteOutput?: true }, ): Promise { const capabilityType = invocationTypes[type] ?? type; const capability = capabilities[capabilityType]?.[key]; if (!capability) { throw new Error(\`Capability "\${capabilityType}/\${key}" not found\`); } return capability.handler(context, options); } `; } /** * Generate the build-only metadata entry. Dynamic imports run after the * recorder starts and remain isolated from the deployed Worker entry. */ export function generateMetadataEntry(capabilities: readonly DiscoveredCapability[]): string { const imports = [...capabilities] .sort( (left, right) => Number(right.type === "custom_block") - Number(left.type === "custom_block"), ) .map( (capability) => `await withMetadataCapabilityKey(${capability.type === "custom_block" ? JSON.stringify(capability.key) : "undefined"}, () => import(${JSON.stringify(`../${capability.sourcePath}`)}));`, ) .join("\n"); return `// Generated by \`notion-apps build\`. Do not edit. import { finishMetadataRecording, startMetadataRecording, withMetadataCapabilityKey, } from "@notionhq/apps/notion-as-code/recorder"; const recorder = startMetadataRecording(); ${imports} export default finishMetadataRecording(recorder); `; } /** Evaluate block declarations only while building, separately from the Worker. */ export function generateBlocksEntry(capabilities: readonly DiscoveredCapability[]): string { const entries = capabilities.filter((capability) => capability.runtime === "build"); return ( entries .map( (capability, index) => `import block_${index} from ${JSON.stringify(`../${capability.sourcePath}`)};`, ) .join("\n") + `\nexport const capabilities = { custom_block: {${entries.map((capability, index) => `${JSON.stringify(capability.key)}: block_${index}`).join(",")}} };\n` ); }