appflare 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/cli/README.md +101 -0
  2. package/cli/core/build.ts +136 -0
  3. package/cli/core/config.ts +29 -0
  4. package/cli/core/discover-handlers.ts +61 -0
  5. package/cli/core/handlers.ts +5 -0
  6. package/cli/core/index.ts +157 -0
  7. package/cli/generators/generate-api-client/client.ts +93 -0
  8. package/cli/generators/generate-api-client/index.ts +529 -0
  9. package/cli/generators/generate-api-client/types.ts +59 -0
  10. package/cli/generators/generate-api-client/utils.ts +18 -0
  11. package/cli/generators/generate-api-client.ts +1 -0
  12. package/cli/generators/generate-db-handlers.ts +138 -0
  13. package/cli/generators/generate-hono-server.ts +238 -0
  14. package/cli/generators/generate-websocket-durable-object.ts +537 -0
  15. package/cli/index.ts +157 -0
  16. package/cli/schema/schema-static-types.ts +252 -0
  17. package/cli/schema/schema.ts +105 -0
  18. package/cli/utils/tsc.ts +53 -0
  19. package/cli/utils/utils.ts +126 -0
  20. package/cli/utils/zod-utils.ts +115 -0
  21. package/index.ts +2 -0
  22. package/lib/README.md +43 -0
  23. package/lib/db.ts +9 -0
  24. package/lib/values.ts +23 -0
  25. package/package.json +28 -0
  26. package/react/README.md +67 -0
  27. package/react/hooks/useMutation.ts +89 -0
  28. package/react/hooks/usePaginatedQuery.ts +213 -0
  29. package/react/hooks/useQuery.ts +106 -0
  30. package/react/index.ts +3 -0
  31. package/react/shared/queryShared.ts +169 -0
  32. package/server/README.md +153 -0
  33. package/server/database/builders.ts +83 -0
  34. package/server/database/context.ts +265 -0
  35. package/server/database/populate.ts +160 -0
  36. package/server/database/query-builder.ts +101 -0
  37. package/server/database/query-utils.ts +25 -0
  38. package/server/db.ts +2 -0
  39. package/server/types/schema-refs.ts +66 -0
  40. package/server/types/types.ts +419 -0
  41. package/server/utils/id-utils.ts +123 -0
  42. package/tsconfig.json +7 -0
package/cli/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Appflare CLI
2
+
3
+ This folder contains the build toolchain that turns an Appflare project (schema + query/mutation handlers) into a fully generated API surface (typed client, Hono server, websocket durable object, and optional JS/.d.ts emit). The CLI is Bun-based and is intended to run inside a project that exports `appflare.config.ts`.
4
+
5
+ ## Command surface
6
+
7
+ - **build**: entrypoint defined in [packages/appflare/cli/index.ts](packages/appflare/cli/index.ts). Generates all artifacts into the configured `outDir` and optionally emits compiled output.
8
+ - `-c, --config <path>`: path to the config file (defaults to `appflare.config.ts`).
9
+ - `--emit`: after generation, run `bunx tsc` with a temporary tsconfig to emit JS and .d.ts into `outDir/dist`.
10
+
11
+ ### Config shape
12
+
13
+ `appflare.config.ts` must default-export an object with:
14
+
15
+ ```ts
16
+ export default {
17
+ dir: "./app", // Root folder containing your handlers
18
+ schema: "./schema.ts", // Path to the Zod schema file
19
+ outDir: "./_generated", // Where generated files are written
20
+ };
21
+ ```
22
+
23
+ The loader in [packages/appflare/cli/core/config.ts](packages/appflare/cli/core/config.ts) validates presence and types of `dir`, `schema`, and `outDir` and resolves paths relative to the config file location.
24
+
25
+ ## Build pipeline
26
+
27
+ The build orchestrator in [packages/appflare/cli/core/build.ts](packages/appflare/cli/core/build.ts) performs the following steps when `build` runs:
28
+
29
+ 1. Resolve absolute paths for `dir`, `schema`, and `outDir`; ensure `dir` and `schema` exist.
30
+ 2. Create `outDir/src` and `outDir/server` if missing.
31
+ 3. Generate typed schema helpers into `outDir/src/schema-types.ts` using [packages/appflare/cli/schema/schema.ts](packages/appflare/cli/schema/schema.ts). This produces table doc interfaces, `TableNames`, `Id`, query helpers, and convenience types from [packages/appflare/cli/schema/schema-static-types.ts](packages/appflare/cli/schema/schema-static-types.ts).
32
+ 4. Generate built-in CRUD handlers for every table into `outDir/src/handlers/<table>.ts` plus an index via [packages/appflare/cli/generators/generate-db-handlers.ts](packages/appflare/cli/generators/generate-db-handlers.ts). These include `find*`, `findOne*`, `insert*`, `update*`, and `delete*` operations backed by the generated schema types.
33
+ 5. Discover user-defined handlers under `dir` with [packages/appflare/cli/core/discover-handlers.ts](packages/appflare/cli/core/discover-handlers.ts):
34
+ - Recurses through `.ts` files (excluding `node_modules`, `.git`, `dist`, `build`, and the configured `outDir`).
35
+ - Recognizes handlers declared as `export const <name> = query(` or `export const <name> = mutation(`.
36
+ - Skips the schema file and the config file; deduplicates by kind/file/name.
37
+ 6. Generate a typed client at `outDir/src/api.ts` via [packages/appflare/cli/generators/generate-api-client.ts](packages/appflare/cli/generators/generate-api-client.ts):
38
+ - Produces `createAppflareApi()` with `queries` and `mutations` collections keyed by `<file>/<handler>`.
39
+ - Each handler function wraps `fetch` (default `better-fetch`) and carries metadata: Zod schema, websocket helper, and route path.
40
+ - Realtime helpers build websocket URLs for subscriptions, normalizing `ws`/`wss` bases and providing hooks (`onOpen`, `onMessage`, `onData`, etc.).
41
+ 7. Generate a Hono server at `outDir/server/server.ts` with [packages/appflare/cli/generators/generate-hono-server.ts](packages/appflare/cli/generators/generate-hono-server.ts):
42
+ - Routes: `GET /queries/<file>/<name>` and `POST /mutations/<file>/<name>`.
43
+ - Uses `@hono/standard-validator` + Zod arg schemas and wraps Mongo via `createMongoDbContext` from `appflare/server/db`.
44
+ - Supports optional mutation notifications for realtime (custom notifier or Durable Object hook).
45
+ 8. Generate a websocket Durable Object shim at `outDir/server/websocket-hibernation-server.ts` via [packages/appflare/cli/generators/generate-websocket-durable-object.ts](packages/appflare/cli/generators/generate-websocket-durable-object.ts):
46
+ - Implements `WebSocketHibernationServer` to handle subscriptions at `/ws` and mutation notifications at `/notify`.
47
+ - Selects a default query handler per table (or a specific handler) and re-fetches data on mutation notifications, emitting `data` messages to subscribers.
48
+ 9. If `--emit` is set, remove any previous `outDir/dist`, write a temporary tsconfig (includes generated schema types and handlers only), and run `bunx tsc` to emit JS + .d.ts into `outDir/dist` (logic in [packages/appflare/cli/utils/tsc.ts](packages/appflare/cli/utils/tsc.ts)).
49
+
50
+ ## Handler authoring guidelines
51
+
52
+ - Handlers must be exported as `query({ args, handler })` or `mutation({ args, handler })` objects.
53
+ - Filenames become the first route/path segment and grouping key in the client (`<file>/<handler>`).
54
+ - Arguments are validated with Zod; the client infers optional vs required keys and provides typed `args` for both client and server.
55
+ - The discovery step ignores `.d.ts` files and anything outside the configured `dir`.
56
+
57
+ ## Generated layout (relative to `outDir`)
58
+
59
+ ```
60
+ src/
61
+ schema-types.ts # Typed schema exports, helpers, and Zod-powered validator types
62
+ handlers/ # Auto CRUD handlers per table + index
63
+ <table>.ts
64
+ index.ts
65
+ api.ts # Typed queries/mutations client with realtime helpers
66
+ server/
67
+ server.ts # Hono server that wires handlers + Mongo context
68
+ websocket-hibernation-server.ts # Durable Object websocket bridge
69
+ ```
70
+
71
+ ## Realtime and Durable Object flow
72
+
73
+ - Client websockets are created via `handler.websocket(args?, options?)`, building URLs against `realtime.baseUrl` (or handler override) and defaulting the `table` and `handler` params based on the handler’s file/name.
74
+ - The Durable Object handles `/ws` upgrades, parses subscription params (`table`, `handler`, `where`, `orderBy`, `take`, `skip`, `select`, `include`, `args`), and caches subscriptions. On `/notify` payloads, it re-runs the relevant query or table fetch and pushes a `data` message to connected sockets.
75
+ - Mutation notifications can be sent by the generated Hono server (if `realtime.notify` or `realtime.durableObject` is provided) so subscriptions stay in sync.
76
+
77
+ ## Error handling and safeguards
78
+
79
+ - Config, schema, and project directories are validated before generation; missing paths throw with readable errors.
80
+ - Build de-duplicates discovered handlers to avoid duplicate route generation.
81
+ - `--emit` uses a scoped tsconfig that only references generated files to prevent user code outside `rootDir` from breaking emit.
82
+ - Generated files include `/* eslint-disable */` headers to avoid lint noise.
83
+
84
+ ## Typical usage
85
+
86
+ ```sh
87
+ # From the repo root (config defaults to ./appflare.config.ts)
88
+ bunx appflare build
89
+
90
+ # Custom config location and emit compiled JS
91
+ bunx appflare build --config ./config/appflare.config.ts --emit
92
+ ```
93
+
94
+ After running, import the generated client/server:
95
+
96
+ ```ts
97
+ import { createAppflareApi } from "./_generated/src/api";
98
+ import server from "./_generated/server/server";
99
+ ```
100
+
101
+ Use the client in web/React or server contexts, and deploy the generated Hono server + Durable Object to your runtime of choice (e.g., Cloudflare Workers with Mongo).
@@ -0,0 +1,136 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ import {
5
+ AppflareConfig,
6
+ assertDirExists,
7
+ assertFileExists,
8
+ } from "../utils/utils";
9
+ import { getSchemaTableNames, generateSchemaTypes } from "../schema/schema";
10
+ import {
11
+ generateDbHandlers,
12
+ discoverHandlers,
13
+ generateApiClient,
14
+ generateHonoServer,
15
+ generateWebsocketDurableObject,
16
+ } from "./handlers";
17
+
18
+ export async function buildFromConfig(params: {
19
+ config: AppflareConfig;
20
+ configDirAbs: string;
21
+ configPathAbs: string;
22
+ emit: boolean;
23
+ }): Promise<void> {
24
+ const { config, configDirAbs, emit, configPathAbs } = params;
25
+
26
+ const projectDirAbs = path.resolve(configDirAbs, config.dir);
27
+ const schemaPathAbs = path.resolve(configDirAbs, config.schema);
28
+ const outDirAbs = path.resolve(configDirAbs, config.outDir);
29
+
30
+ await assertDirExists(
31
+ projectDirAbs,
32
+ `Project dir not found: ${projectDirAbs}`
33
+ );
34
+ await assertFileExists(schemaPathAbs, `Schema not found: ${schemaPathAbs}`);
35
+
36
+ await fs.mkdir(path.join(outDirAbs, "src"), { recursive: true });
37
+ await fs.mkdir(path.join(outDirAbs, "server"), { recursive: true });
38
+
39
+ const schemaTypesTs = await generateSchemaTypes({ schemaPathAbs });
40
+ await fs.writeFile(
41
+ path.join(outDirAbs, "src", "schema-types.ts"),
42
+ schemaTypesTs
43
+ );
44
+
45
+ // (Re)generate built-in DB handlers based on the schema tables.
46
+ const schemaTableNames = await getSchemaTableNames(schemaPathAbs);
47
+ await generateDbHandlers({ outDirAbs, tableNames: schemaTableNames });
48
+
49
+ const handlers = await discoverHandlers({
50
+ projectDirAbs,
51
+ schemaPathAbs,
52
+ outDirAbs,
53
+ configPathAbs,
54
+ });
55
+
56
+ const apiTs = generateApiClient({ handlers, outDirAbs });
57
+ await fs.writeFile(path.join(outDirAbs, "src", "api.ts"), apiTs);
58
+
59
+ const serverTs = generateHonoServer({
60
+ handlers,
61
+ outDirAbs,
62
+ schemaPathAbs,
63
+ });
64
+ await fs.writeFile(path.join(outDirAbs, "server", "server.ts"), serverTs);
65
+
66
+ const websocketDoTs = generateWebsocketDurableObject({
67
+ handlers,
68
+ outDirAbs,
69
+ schemaPathAbs,
70
+ });
71
+ await fs.writeFile(
72
+ path.join(outDirAbs, "server", "websocket-hibernation-server.ts"),
73
+ websocketDoTs
74
+ );
75
+
76
+ if (emit) {
77
+ // Remove previous emit output to avoid stale files lingering.
78
+ await fs.rm(path.join(outDirAbs, "dist"), { recursive: true, force: true });
79
+
80
+ // Emit only the files that don't pull in user code outside rootDir.
81
+ // This avoids TS rootDir issues and dist overwrite issues caused by user modules.
82
+ const emitTsconfigAbs = await writeEmitTsconfig({
83
+ configDirAbs,
84
+ outDirAbs,
85
+ });
86
+ await runTscEmit(emitTsconfigAbs);
87
+ }
88
+ }
89
+
90
+ async function writeEmitTsconfig(params: {
91
+ configDirAbs: string;
92
+ outDirAbs: string;
93
+ }): Promise<string> {
94
+ const outDirRel =
95
+ path.relative(params.configDirAbs, params.outDirAbs).replace(/\\/g, "/") ||
96
+ "./_generated";
97
+ const tsconfigPathAbs = path.join(
98
+ params.configDirAbs,
99
+ ".appflare.tsconfig.emit.json"
100
+ );
101
+ const content = {
102
+ compilerOptions: {
103
+ noEmit: false,
104
+ declaration: true,
105
+ emitDeclarationOnly: false,
106
+ outDir: `./${outDirRel}/dist`,
107
+ rootDir: `./${outDirRel}/src`,
108
+ sourceMap: false,
109
+ declarationMap: false,
110
+ skipLibCheck: true,
111
+ target: "ES2022",
112
+ module: "ES2022",
113
+ moduleResolution: "Bundler",
114
+ types: [],
115
+ },
116
+ include: [
117
+ `./${outDirRel}/src/schema-types.ts`,
118
+ `./${outDirRel}/src/handlers/**/*`,
119
+ ],
120
+ };
121
+ await fs.writeFile(tsconfigPathAbs, JSON.stringify(content, null, 2));
122
+ return tsconfigPathAbs;
123
+ }
124
+
125
+ async function runTscEmit(tsconfigPathAbs: string): Promise<void> {
126
+ await new Promise<void>((resolve, reject) => {
127
+ const child = spawn("bunx", ["tsc", "-p", tsconfigPathAbs], {
128
+ stdio: "inherit",
129
+ });
130
+ child.on("error", reject);
131
+ child.on("exit", (code) => {
132
+ if (code === 0) resolve();
133
+ reject(new Error(`tsc exited with code ${code}`));
134
+ });
135
+ });
136
+ }
@@ -0,0 +1,29 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { AppflareConfig, assertFileExists } from "../utils/utils";
5
+
6
+ export async function loadConfig(
7
+ configPathAbs: string
8
+ ): Promise<{ config: AppflareConfig; configDirAbs: string }> {
9
+ await assertFileExists(configPathAbs, `Config not found: ${configPathAbs}`);
10
+ const configDirAbs = path.dirname(configPathAbs);
11
+
12
+ const mod = await import(pathToFileURL(configPathAbs).href);
13
+ const config = (mod?.default ?? mod) as Partial<AppflareConfig>;
14
+ if (!config || typeof config !== "object") {
15
+ throw new Error(
16
+ `Invalid config export in ${configPathAbs} (expected default export object)`
17
+ );
18
+ }
19
+ if (typeof config.dir !== "string" || !config.dir) {
20
+ throw new Error(`Invalid config.dir in ${configPathAbs}`);
21
+ }
22
+ if (typeof config.schema !== "string" || !config.schema) {
23
+ throw new Error(`Invalid config.schema in ${configPathAbs}`);
24
+ }
25
+ if (typeof config.outDir !== "string" || !config.outDir) {
26
+ throw new Error(`Invalid config.outDir in ${configPathAbs}`);
27
+ }
28
+ return { config: config as AppflareConfig, configDirAbs };
29
+ }
@@ -0,0 +1,61 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ DiscoveredHandler,
5
+ HandlerKind,
6
+ walkTsFiles,
7
+ groupBy,
8
+ } from "../utils/utils";
9
+
10
+ export async function discoverHandlers(params: {
11
+ projectDirAbs: string;
12
+ schemaPathAbs: string;
13
+ outDirAbs: string;
14
+ configPathAbs: string;
15
+ }): Promise<DiscoveredHandler[]> {
16
+ const ignoreDirs = new Set([
17
+ "node_modules",
18
+ ".git",
19
+ "dist",
20
+ "build",
21
+ path.basename(params.outDirAbs),
22
+ ]);
23
+
24
+ const files = await walkTsFiles(params.projectDirAbs, ignoreDirs);
25
+
26
+ const handlers: DiscoveredHandler[] = [];
27
+ for (const fileAbs of files) {
28
+ if (path.resolve(fileAbs) === path.resolve(params.schemaPathAbs)) continue;
29
+ if (path.resolve(fileAbs) === path.resolve(params.configPathAbs)) continue;
30
+
31
+ const content = await fs.readFile(fileAbs, "utf8");
32
+ const regex =
33
+ /export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(query|mutation)\s*\(/g;
34
+ let match: RegExpExecArray | null;
35
+ while ((match = regex.exec(content)) !== null) {
36
+ handlers.push({
37
+ fileName: path.basename(fileAbs, ".ts"),
38
+ name: match[1],
39
+ kind: match[2] as HandlerKind,
40
+ sourceFileAbs: fileAbs,
41
+ });
42
+ }
43
+ }
44
+
45
+ // De-dupe: keep first occurrence
46
+ const seen = new Set<string>();
47
+ const unique = handlers.filter((h) => {
48
+ const key = `${h.kind}:${h.fileName}:${h.name}`;
49
+ if (seen.has(key)) return false;
50
+ seen.add(key);
51
+ return true;
52
+ });
53
+
54
+ unique.sort((a, b) => {
55
+ if (a.kind !== b.kind) return a.kind.localeCompare(b.kind);
56
+ if (a.fileName !== b.fileName) return a.fileName.localeCompare(b.fileName);
57
+ return a.name.localeCompare(b.name);
58
+ });
59
+
60
+ return unique;
61
+ }
@@ -0,0 +1,5 @@
1
+ export { discoverHandlers } from "./discover-handlers";
2
+ export { generateDbHandlers } from "../generators/generate-db-handlers";
3
+ export { generateApiClient } from "../generators/generate-api-client";
4
+ export { generateHonoServer } from "../generators/generate-hono-server";
5
+ export { generateWebsocketDurableObject } from "../generators/generate-websocket-durable-object";
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { Command } from "commander";
4
+ import { promises as fs } from "node:fs";
5
+ import path from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import {
8
+ discoverHandlers,
9
+ generateApiClient,
10
+ generateDbHandlers,
11
+ generateHonoServer,
12
+ generateWebsocketDurableObject,
13
+ } from "./handlers";
14
+ import { generateSchemaTypes, getSchemaTableNames } from "../schema/schema";
15
+ import { runTscEmit, writeEmitTsconfig } from "../utils/tsc";
16
+ import { assertDirExists, assertFileExists } from "../utils/utils";
17
+
18
+ type AppflareConfig = {
19
+ dir: string;
20
+ schema: string;
21
+ outDir: string;
22
+ };
23
+
24
+ const program = new Command();
25
+
26
+ program.name("appflare").description("Appflare CLI").version("0.0.0");
27
+
28
+ program
29
+ .command("build")
30
+ .description(
31
+ "Generate typed schema + query/mutation client/server into outDir"
32
+ )
33
+ .option(
34
+ "-c, --config <path>",
35
+ "Path to appflare.config.ts",
36
+ "appflare.config.ts"
37
+ )
38
+ .option("--emit", "Also run tsc to emit JS + .d.ts into outDir/dist")
39
+ .action(async (options: { config: string; emit?: boolean }) => {
40
+ try {
41
+ const configPath = path.resolve(process.cwd(), options.config);
42
+ const { config, configDirAbs } = await loadConfig(configPath);
43
+ await buildFromConfig({
44
+ config,
45
+ configDirAbs,
46
+ configPathAbs: configPath,
47
+ emit: Boolean(options.emit),
48
+ });
49
+ } catch (err) {
50
+ const message = err instanceof Error ? err.message : String(err);
51
+ console.error(message);
52
+ process.exitCode = 1;
53
+ }
54
+ });
55
+
56
+ void main();
57
+
58
+ async function main(): Promise<void> {
59
+ await program.parseAsync(process.argv);
60
+ }
61
+
62
+ async function loadConfig(
63
+ configPathAbs: string
64
+ ): Promise<{ config: AppflareConfig; configDirAbs: string }> {
65
+ await assertFileExists(configPathAbs, `Config not found: ${configPathAbs}`);
66
+ const configDirAbs = path.dirname(configPathAbs);
67
+
68
+ const mod = await import(pathToFileURL(configPathAbs).href);
69
+ const config = (mod?.default ?? mod) as Partial<AppflareConfig>;
70
+ if (!config || typeof config !== "object") {
71
+ throw new Error(
72
+ `Invalid config export in ${configPathAbs} (expected default export object)`
73
+ );
74
+ }
75
+ if (typeof config.dir !== "string" || !config.dir) {
76
+ throw new Error(`Invalid config.dir in ${configPathAbs}`);
77
+ }
78
+ if (typeof config.schema !== "string" || !config.schema) {
79
+ throw new Error(`Invalid config.schema in ${configPathAbs}`);
80
+ }
81
+ if (typeof config.outDir !== "string" || !config.outDir) {
82
+ throw new Error(`Invalid config.outDir in ${configPathAbs}`);
83
+ }
84
+ return { config: config as AppflareConfig, configDirAbs };
85
+ }
86
+
87
+ async function buildFromConfig(params: {
88
+ config: AppflareConfig;
89
+ configDirAbs: string;
90
+ configPathAbs: string;
91
+ emit: boolean;
92
+ }): Promise<void> {
93
+ const { config, configDirAbs, emit, configPathAbs } = params;
94
+
95
+ const projectDirAbs = path.resolve(configDirAbs, config.dir);
96
+ const schemaPathAbs = path.resolve(configDirAbs, config.schema);
97
+ const outDirAbs = path.resolve(configDirAbs, config.outDir);
98
+
99
+ await assertDirExists(
100
+ projectDirAbs,
101
+ `Project dir not found: ${projectDirAbs}`
102
+ );
103
+ await assertFileExists(schemaPathAbs, `Schema not found: ${schemaPathAbs}`);
104
+
105
+ await fs.mkdir(path.join(outDirAbs, "src"), { recursive: true });
106
+ await fs.mkdir(path.join(outDirAbs, "server"), { recursive: true });
107
+
108
+ const schemaTypesTs = await generateSchemaTypes({ schemaPathAbs });
109
+ await fs.writeFile(
110
+ path.join(outDirAbs, "src", "schema-types.ts"),
111
+ schemaTypesTs
112
+ );
113
+
114
+ // (Re)generate built-in DB handlers based on the schema tables.
115
+ const schemaTableNames = await getSchemaTableNames(schemaPathAbs);
116
+ await generateDbHandlers({ outDirAbs, tableNames: schemaTableNames });
117
+
118
+ const handlers = await discoverHandlers({
119
+ projectDirAbs,
120
+ schemaPathAbs,
121
+ outDirAbs,
122
+ configPathAbs,
123
+ });
124
+
125
+ const apiTs = generateApiClient({ handlers, outDirAbs });
126
+ await fs.writeFile(path.join(outDirAbs, "src", "api.ts"), apiTs);
127
+
128
+ const serverTs = generateHonoServer({
129
+ handlers,
130
+ outDirAbs,
131
+ schemaPathAbs,
132
+ });
133
+ await fs.writeFile(path.join(outDirAbs, "server", "server.ts"), serverTs);
134
+
135
+ const websocketDoTs = generateWebsocketDurableObject({
136
+ handlers,
137
+ outDirAbs,
138
+ schemaPathAbs,
139
+ });
140
+ await fs.writeFile(
141
+ path.join(outDirAbs, "server", "websocket-hibernation-server.ts"),
142
+ websocketDoTs
143
+ );
144
+
145
+ if (emit) {
146
+ // Remove previous emit output to avoid stale files lingering.
147
+ await fs.rm(path.join(outDirAbs, "dist"), { recursive: true, force: true });
148
+
149
+ // Emit only the files that don't pull in user code outside rootDir.
150
+ // This avoids TS rootDir issues and dist overwrite issues caused by user modules.
151
+ const emitTsconfigAbs = await writeEmitTsconfig({
152
+ configDirAbs,
153
+ outDirAbs,
154
+ });
155
+ await runTscEmit(emitTsconfigAbs);
156
+ }
157
+ }
@@ -0,0 +1,93 @@
1
+ import { DiscoveredHandler } from "../../utils/utils";
2
+ import {
3
+ handlerTypePrefix,
4
+ normalizeTableName,
5
+ renderObjectKey,
6
+ sortedEntries,
7
+ } from "./utils";
8
+
9
+ export function generateQueriesClientLines(
10
+ queriesByFile: Map<string, DiscoveredHandler[]>,
11
+ importAliasBySource: Map<string, string>
12
+ ): string {
13
+ return sortedEntries(queriesByFile)
14
+ .map(([fileName, list]) => {
15
+ const fileKey = renderObjectKey(fileName);
16
+ const inner = list
17
+ .slice()
18
+ .sort((a, b) => a.name.localeCompare(b.name))
19
+ .map((h) => {
20
+ const pascal = handlerTypePrefix(h);
21
+ const route = `/queries/${fileName}/${h.name}`;
22
+ const importAlias = importAliasBySource.get(h.sourceFileAbs)!;
23
+ const handlerAccessor = `${importAlias}.${h.name}`;
24
+ return (
25
+ `\t\t${h.name}: withHandlerMetadata<${pascal}Definition>(\n` +
26
+ `\t\t\tasync (args: ${pascal}Args, init) => {\n` +
27
+ `\t\t\t\tconst url = buildQueryUrl(baseUrl, ${JSON.stringify(route)}, args);\n` +
28
+ `\t\t\t\tconst response = await request(url, {\n` +
29
+ `\t\t\t\t\t...(init ?? {}),\n` +
30
+ `\t\t\t\t\tmethod: "GET",\n` +
31
+ `\t\t\t\t});\n` +
32
+ `\t\t\t\treturn parseJson<${pascal}Result>(response);\n` +
33
+ `\t\t\t},\n` +
34
+ `\t\t\t{\n` +
35
+ `\t\t\t\tschema: createHandlerSchema(${handlerAccessor}.args),\n` +
36
+ `\t\t\t\twebsocket: createHandlerWebsocket<${pascal}Args, ${pascal}Result>(realtime, {\n` +
37
+ `\t\t\t\t\tdefaultTable: ${JSON.stringify(normalizeTableName(fileName))},\n` +
38
+ `\t\t\t\t\tdefaultHandler: { file: ${JSON.stringify(fileName)}, name: ${JSON.stringify(h.name)} },\n` +
39
+ `\t\t\t\t}),\n` +
40
+ `\t\t\t\tpath: ${JSON.stringify(route)},\n` +
41
+ `\t\t\t}\n` +
42
+ `\t\t),`
43
+ );
44
+ })
45
+ .join("\n");
46
+ return `\t${fileKey}: {\n${inner || "\t\t// (none)"}\n\t},`;
47
+ })
48
+ .join("\n");
49
+ }
50
+
51
+ export function generateMutationsClientLines(
52
+ mutationsByFile: Map<string, DiscoveredHandler[]>,
53
+ importAliasBySource: Map<string, string>
54
+ ): string {
55
+ return sortedEntries(mutationsByFile)
56
+ .map(([fileName, list]) => {
57
+ const fileKey = renderObjectKey(fileName);
58
+ const inner = list
59
+ .slice()
60
+ .sort((a, b) => a.name.localeCompare(b.name))
61
+ .map((h) => {
62
+ const pascal = handlerTypePrefix(h);
63
+ const route = `/mutations/${fileName}/${h.name}`;
64
+ const importAlias = importAliasBySource.get(h.sourceFileAbs)!;
65
+ const handlerAccessor = `${importAlias}.${h.name}`;
66
+ return (
67
+ `\t\t${h.name}: withHandlerMetadata<${pascal}Definition>(\n` +
68
+ `\t\t\tasync (args: ${pascal}Args, init) => {\n` +
69
+ `\t\t\t\tconst url = buildUrl(baseUrl, ${JSON.stringify(route)});\n` +
70
+ `\t\t\t\tconst response = await request(url, {\n` +
71
+ `\t\t\t\t\t...(init ?? {}),\n` +
72
+ `\t\t\t\t\tmethod: "POST",\n` +
73
+ `\t\t\t\t\theaders: ensureJsonHeaders(init?.headers),\n` +
74
+ `\t\t\t\t\tbody: JSON.stringify(args),\n` +
75
+ `\t\t\t\t});\n` +
76
+ `\t\t\t\treturn parseJson<${pascal}Result>(response);\n` +
77
+ `\t\t\t},\n` +
78
+ `\t\t\t{\n` +
79
+ `\t\t\t\tschema: createHandlerSchema(${handlerAccessor}.args),\n` +
80
+ `\t\t\t\twebsocket: createHandlerWebsocket<${pascal}Args, ${pascal}Result>(realtime, {\n` +
81
+ `\t\t\t\t\tdefaultTable: ${JSON.stringify(normalizeTableName(fileName))},\n` +
82
+ `\t\t\t\t\tdefaultHandler: { file: ${JSON.stringify(fileName)}, name: ${JSON.stringify(h.name)} },\n` +
83
+ `\t\t\t\t}),\n` +
84
+ `\t\t\t\tpath: ${JSON.stringify(route)},\n` +
85
+ `\t\t\t}\n` +
86
+ `\t\t),`
87
+ );
88
+ })
89
+ .join("\n");
90
+ return `\t${fileKey}: {\n${inner || "\t\t// (none)"}\n\t},`;
91
+ })
92
+ .join("\n");
93
+ }