appflare 0.0.28 → 0.1.0

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 (141) hide show
  1. package/cli/commands/index.ts +140 -0
  2. package/cli/generate.ts +149 -0
  3. package/cli/index.ts +56 -447
  4. package/cli/load-config.ts +182 -0
  5. package/cli/schema-compiler.ts +657 -0
  6. package/cli/templates/auth/README.md +156 -0
  7. package/cli/templates/auth/config.ts +61 -0
  8. package/cli/templates/auth/route-config.ts +18 -0
  9. package/cli/templates/auth/route-handler.ts +18 -0
  10. package/cli/templates/auth/route-request-utils.ts +55 -0
  11. package/cli/templates/auth/route.ts +14 -0
  12. package/cli/templates/core/README.md +266 -0
  13. package/cli/templates/core/app-creation.ts +19 -0
  14. package/cli/templates/core/client/appflare.ts +37 -0
  15. package/cli/templates/core/client/index.ts +6 -0
  16. package/cli/templates/core/client/storage.ts +100 -0
  17. package/cli/templates/core/client/types.ts +54 -0
  18. package/cli/templates/core/client-modules/appflare.ts +112 -0
  19. package/cli/templates/core/client-modules/handlers/index.ts +740 -0
  20. package/cli/templates/core/client-modules/handlers.ts +1 -0
  21. package/cli/templates/core/client-modules/index.ts +7 -0
  22. package/cli/templates/core/client-modules/storage.ts +180 -0
  23. package/cli/templates/core/client-modules/types.ts +145 -0
  24. package/cli/templates/core/client.ts +39 -0
  25. package/cli/templates/core/drizzle.ts +15 -0
  26. package/cli/templates/core/export.ts +14 -0
  27. package/cli/templates/core/handlers-route.ts +23 -0
  28. package/cli/templates/core/handlers.ts +1 -0
  29. package/cli/templates/core/imports.ts +8 -0
  30. package/cli/templates/core/server.ts +38 -0
  31. package/cli/templates/core/types.ts +6 -0
  32. package/cli/templates/core/wrangler.ts +109 -0
  33. package/cli/templates/handlers/README.md +265 -0
  34. package/cli/templates/handlers/auth.ts +36 -0
  35. package/cli/templates/handlers/execution.ts +39 -0
  36. package/cli/templates/handlers/generators/context/context-creation.ts +80 -0
  37. package/cli/templates/handlers/generators/context/error-helpers.ts +11 -0
  38. package/cli/templates/handlers/generators/context/scheduler.ts +24 -0
  39. package/cli/templates/handlers/generators/context/storage-api.ts +112 -0
  40. package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -0
  41. package/cli/templates/handlers/generators/context/types.ts +18 -0
  42. package/cli/templates/handlers/generators/context.ts +43 -0
  43. package/cli/templates/handlers/generators/execution.ts +15 -0
  44. package/cli/templates/handlers/generators/handlers.ts +13 -0
  45. package/cli/templates/handlers/index.ts +43 -0
  46. package/cli/templates/handlers/operations.ts +116 -0
  47. package/cli/templates/handlers/registration.ts +1114 -0
  48. package/cli/templates/handlers/types.ts +960 -0
  49. package/cli/templates/handlers/utils.ts +48 -0
  50. package/cli/types.ts +108 -0
  51. package/cli/utils/handler-discovery.ts +366 -0
  52. package/cli/utils/json-utils.ts +24 -0
  53. package/cli/utils/path-utils.ts +19 -0
  54. package/cli/utils/schema-discovery.ts +390 -0
  55. package/index.ts +27 -4
  56. package/package.json +23 -20
  57. package/react/index.ts +5 -3
  58. package/react/use-infinite-query.ts +190 -0
  59. package/react/use-mutation.ts +54 -0
  60. package/react/use-query.ts +158 -0
  61. package/schema.ts +262 -0
  62. package/tsconfig.json +2 -4
  63. package/cli/README.md +0 -108
  64. package/cli/core/build.ts +0 -187
  65. package/cli/core/config.ts +0 -92
  66. package/cli/core/discover-handlers.ts +0 -143
  67. package/cli/core/handlers.ts +0 -7
  68. package/cli/core/index.ts +0 -205
  69. package/cli/generators/generate-api-client/client.ts +0 -163
  70. package/cli/generators/generate-api-client/extract-configuration.ts +0 -121
  71. package/cli/generators/generate-api-client/index.ts +0 -973
  72. package/cli/generators/generate-api-client/types.ts +0 -164
  73. package/cli/generators/generate-api-client/utils.ts +0 -22
  74. package/cli/generators/generate-api-client.ts +0 -1
  75. package/cli/generators/generate-cloudflare-worker/helpers.ts +0 -24
  76. package/cli/generators/generate-cloudflare-worker/index.ts +0 -2
  77. package/cli/generators/generate-cloudflare-worker/worker.ts +0 -148
  78. package/cli/generators/generate-cloudflare-worker/wrangler.ts +0 -108
  79. package/cli/generators/generate-cloudflare-worker.ts +0 -4
  80. package/cli/generators/generate-cron-handlers/cron-handlers-block.ts +0 -2
  81. package/cli/generators/generate-cron-handlers/handler-entries.ts +0 -29
  82. package/cli/generators/generate-cron-handlers/index.ts +0 -61
  83. package/cli/generators/generate-cron-handlers/runtime-block.ts +0 -49
  84. package/cli/generators/generate-cron-handlers/type-helpers-block.ts +0 -60
  85. package/cli/generators/generate-db-handlers/index.ts +0 -33
  86. package/cli/generators/generate-db-handlers/prepare.ts +0 -24
  87. package/cli/generators/generate-db-handlers/templates.ts +0 -189
  88. package/cli/generators/generate-db-handlers.ts +0 -1
  89. package/cli/generators/generate-hono-server/auth.ts +0 -97
  90. package/cli/generators/generate-hono-server/imports.ts +0 -55
  91. package/cli/generators/generate-hono-server/index.ts +0 -52
  92. package/cli/generators/generate-hono-server/routes.ts +0 -115
  93. package/cli/generators/generate-hono-server/template.ts +0 -371
  94. package/cli/generators/generate-hono-server.ts +0 -1
  95. package/cli/generators/generate-scheduler-handlers/constants.ts +0 -8
  96. package/cli/generators/generate-scheduler-handlers/handler-entries.ts +0 -22
  97. package/cli/generators/generate-scheduler-handlers/index.ts +0 -51
  98. package/cli/generators/generate-scheduler-handlers/runtime-block.ts +0 -68
  99. package/cli/generators/generate-scheduler-handlers/scheduler-handlers-block.ts +0 -2
  100. package/cli/generators/generate-scheduler-handlers/type-helpers-block.ts +0 -68
  101. package/cli/generators/generate-scheduler-handlers.ts +0 -1
  102. package/cli/generators/generate-websocket-durable-object/auth.ts +0 -30
  103. package/cli/generators/generate-websocket-durable-object/imports.ts +0 -55
  104. package/cli/generators/generate-websocket-durable-object/index.ts +0 -41
  105. package/cli/generators/generate-websocket-durable-object/query-handlers.ts +0 -18
  106. package/cli/generators/generate-websocket-durable-object/template.ts +0 -714
  107. package/cli/generators/generate-websocket-durable-object.ts +0 -1
  108. package/cli/schema/schema-static-types.ts +0 -702
  109. package/cli/schema/schema.ts +0 -151
  110. package/cli/utils/tsc.ts +0 -54
  111. package/cli/utils/utils.ts +0 -190
  112. package/cli/utils/zod-utils.ts +0 -121
  113. package/lib/README.md +0 -50
  114. package/lib/db.ts +0 -19
  115. package/lib/location.ts +0 -110
  116. package/lib/values.ts +0 -27
  117. package/react/README.md +0 -67
  118. package/react/hooks/useMutation.ts +0 -89
  119. package/react/hooks/usePaginatedQuery.ts +0 -213
  120. package/react/hooks/useQuery.ts +0 -106
  121. package/react/shared/queryShared.ts +0 -174
  122. package/server/README.md +0 -218
  123. package/server/auth.ts +0 -107
  124. package/server/database/builders.ts +0 -83
  125. package/server/database/context.ts +0 -327
  126. package/server/database/populate.ts +0 -234
  127. package/server/database/query-builder.ts +0 -161
  128. package/server/database/query-utils.ts +0 -25
  129. package/server/db.ts +0 -2
  130. package/server/storage/auth.ts +0 -16
  131. package/server/storage/bucket.ts +0 -22
  132. package/server/storage/context.ts +0 -34
  133. package/server/storage/index.ts +0 -38
  134. package/server/storage/operations.ts +0 -149
  135. package/server/storage/route-handler.ts +0 -60
  136. package/server/storage/types.ts +0 -55
  137. package/server/storage/utils.ts +0 -47
  138. package/server/storage.ts +0 -6
  139. package/server/types/schema-refs.ts +0 -66
  140. package/server/types/types.ts +0 -633
  141. package/server/utils/id-utils.ts +0 -230
package/cli/index.ts CHANGED
@@ -1,468 +1,77 @@
1
1
  #!/usr/bin/env bun
2
-
3
- import chokidar, { FSWatcher } from "chokidar";
4
2
  import { Command } from "commander";
5
- import { promises as fs } from "node:fs";
6
- import os from "node:os";
7
- import path from "node:path";
8
- import { pathToFileURL } from "node:url";
9
- import {
10
- discoverHandlers,
11
- generateApiClient,
12
- generateDbHandlers,
13
- generateHonoServer,
14
- generateWebsocketDurableObject,
15
- generateSchedulerHandlers,
16
- generateCronHandlers,
17
- } from "./core/handlers";
18
- import {
19
- generateCloudflareWorkerIndex,
20
- generateWranglerJson,
21
- } from "./generators/generate-cloudflare-worker";
22
- import { generateSchemaTypes, getSchemaTableNames } from "./schema/schema";
23
- import { runTscEmit, writeEmitTsconfig } from "./utils/tsc";
24
- import {
25
- AppflareConfig,
26
- assertDirExists,
27
- assertFileExists,
28
- toImportPathFromGeneratedSrc,
29
- } from "./utils/utils";
30
-
31
- type WatchConfig = {
32
- targets: string[];
33
- ignored: string[];
34
- };
3
+ import { runBuild, runDev, runMigrate } from "./commands/index";
35
4
 
36
5
  const program = new Command();
37
6
 
38
- program.name("appflare").description("Appflare CLI").version("0.0.0");
7
+ program
8
+ .name("appflare")
9
+ .description(
10
+ "Appflare compiler/bundler for Cloudflare-native backends and SDK generation",
11
+ )
12
+ .version("0.0.28");
39
13
 
40
14
  program
41
15
  .command("build")
42
16
  .description(
43
- "Generate typed schema + query/mutation client/server into outDir",
17
+ "Generate server.ts, client.ts, auth.config.ts, drizzle.config.ts, and wrangler.json artifacts",
44
18
  )
45
19
  .option(
46
20
  "-c, --config <path>",
47
21
  "Path to appflare.config.ts",
48
22
  "appflare.config.ts",
49
23
  )
50
- .option("--emit", "Also run tsc to emit JS + .d.ts into outDir/dist")
51
- .option("-w, --watch", "Watch for changes and rebuild")
52
- .action(
53
- async (options: { config: string; emit?: boolean; watch?: boolean }) => {
54
- try {
55
- const configPath = path.resolve(process.cwd(), options.config);
56
-
57
- if (options.watch) {
58
- await watchAndBuild({
59
- configPathAbs: configPath,
60
- emit: Boolean(options.emit),
61
- });
62
- return;
63
- }
64
-
65
- const { config, configDirAbs } = await loadConfig(configPath);
66
- await buildFromConfig({
67
- config,
68
- configDirAbs,
69
- configPathAbs: configPath,
70
- emit: Boolean(options.emit),
71
- });
72
- } catch (err) {
73
- const message = err instanceof Error ? err.message : String(err);
74
- console.error(message);
75
- process.exitCode = 1;
76
- }
77
- },
78
- );
79
-
80
- void main();
81
-
82
- async function main(): Promise<void> {
83
- await program.parseAsync(process.argv);
84
- }
85
-
86
- /**
87
- * Regex that matches ES import lines pulling in React Native / Expo native
88
- * modules (e.g. `import * as SecureStore from "expo-secure-store"`).
89
- * These cannot be transpiled by Bun and are only needed at client runtime.
90
- */
91
- const NATIVE_IMPORT_RE =
92
- /^import\s+(?:(?:\*\s+as\s+(\w+))|(?:\{[^}]*\})|(?:(\w+)(?:\s*,\s*\{[^}]*\})?))?\s*from\s*["'](expo-[^"']+)["'];?\s*$/gm;
93
-
94
- /**
95
- * Strip native-module imports from a config source and replace them with
96
- * harmless stub declarations so the CLI can evaluate the config object
97
- * without triggering Bun transpilation errors on native code.
98
- */
99
- function sanitizeConfigSource(source: string): string {
100
- const stubs: string[] = [];
101
- const sanitized = source.replace(
102
- NATIVE_IMPORT_RE,
103
- (_match, starAs, defaultImport, _mod) => {
104
- const name = starAs || defaultImport;
105
- if (name) {
106
- stubs.push(`const ${name} = {} as any;`);
107
- }
108
- return ""; // remove the original import line
109
- },
110
- );
111
- return stubs.length > 0 ? stubs.join("\n") + "\n" + sanitized : sanitized;
112
- }
113
-
114
- async function loadConfig(
115
- configPathAbs: string,
116
- ): Promise<{ config: AppflareConfig; configDirAbs: string }> {
117
- await assertFileExists(configPathAbs, `Config not found: ${configPathAbs}`);
118
- const configDirAbs = path.dirname(configPathAbs);
119
-
120
- // Read the config source and strip native-module imports (e.g. expo-secure-store)
121
- // that Bun cannot transpile. Write the sanitized source to a temp file and import that.
122
- const raw = await fs.readFile(configPathAbs, "utf-8");
123
- const sanitized = sanitizeConfigSource(raw);
124
-
125
- let mod: Record<string, unknown>;
126
- if (sanitized !== raw) {
127
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "appflare-cfg-"));
128
- const tmpFile = path.join(tmpDir, path.basename(configPathAbs));
129
- await fs.writeFile(tmpFile, sanitized);
130
- try {
131
- mod = await import(pathToFileURL(tmpFile).href);
132
- } finally {
133
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
134
- }
135
- } else {
136
- mod = await import(pathToFileURL(configPathAbs).href);
137
- }
138
-
139
- const config = (mod?.default ?? mod) as Partial<AppflareConfig>;
140
- if (!config || typeof config !== "object") {
141
- throw new Error(
142
- `Invalid config export in ${configPathAbs} (expected default export object)`,
143
- );
144
- }
145
- if (typeof config.dir !== "string" || !config.dir) {
146
- throw new Error(`Invalid config.dir in ${configPathAbs}`);
147
- }
148
- if (typeof config.schema !== "string" || !config.schema) {
149
- throw new Error(`Invalid config.schema in ${configPathAbs}`);
150
- }
151
- if (typeof config.outDir !== "string" || !config.outDir) {
152
- throw new Error(`Invalid config.outDir in ${configPathAbs}`);
153
- }
154
- if (
155
- config.wranglerOutPath !== undefined &&
156
- (typeof config.wranglerOutPath !== "string" || !config.wranglerOutPath)
157
- ) {
158
- throw new Error(`Invalid config.wranglerOutPath in ${configPathAbs}`);
159
- }
160
- if (
161
- config.wranglerMain !== undefined &&
162
- (typeof config.wranglerMain !== "string" || !config.wranglerMain)
163
- ) {
164
- throw new Error(`Invalid config.wranglerMain in ${configPathAbs}`);
165
- }
166
- return { config: config as AppflareConfig, configDirAbs };
167
- }
168
-
169
- async function buildFromConfig(params: {
170
- config: AppflareConfig;
171
- configDirAbs: string;
172
- configPathAbs: string;
173
- emit: boolean;
174
- }): Promise<void> {
175
- const { config, configDirAbs, emit, configPathAbs } = params;
176
-
177
- const projectDirAbs = path.resolve(configDirAbs, config.dir);
178
- const schemaPathAbs = path.resolve(configDirAbs, config.schema);
179
- const outDirAbs = path.resolve(configDirAbs, config.outDir);
180
-
181
- await assertDirExists(
182
- projectDirAbs,
183
- `Project dir not found: ${projectDirAbs}`,
184
- );
185
- await assertFileExists(schemaPathAbs, `Schema not found: ${schemaPathAbs}`);
186
-
187
- await fs.mkdir(path.join(outDirAbs, "src"), { recursive: true });
188
- await fs.mkdir(path.join(outDirAbs, "server"), { recursive: true });
189
-
190
- // Re-export the user schema inside the generated output so downstream code can import it from the build directory.
191
- const schemaImportPathForGeneratedSrc = toImportPathFromGeneratedSrc(
192
- outDirAbs,
193
- schemaPathAbs,
194
- );
195
- const schemaReexport = `import schema from ${JSON.stringify(schemaImportPathForGeneratedSrc)};
196
- export type AppflareGeneratedSchema = typeof schema;
197
- export default schema;
198
- `;
199
- await fs.writeFile(path.join(outDirAbs, "src", "schema.ts"), schemaReexport);
200
-
201
- const schemaTypesTs = await generateSchemaTypes({
202
- schemaPathAbs,
203
- configPathAbs,
204
- outDirAbs,
205
- });
206
- await fs.writeFile(
207
- path.join(outDirAbs, "src", "schema-types.ts"),
208
- schemaTypesTs,
209
- );
210
-
211
- // (Re)generate built-in DB handlers based on the schema tables.
212
- const schemaTableNames = await getSchemaTableNames(schemaPathAbs);
213
- await generateDbHandlers({ outDirAbs, tableNames: schemaTableNames });
214
-
215
- const handlers = await discoverHandlers({
216
- projectDirAbs,
217
- schemaPathAbs,
218
- outDirAbs,
219
- configPathAbs,
220
- });
221
-
222
- const { apiTs, clientConfigTs } = generateApiClient({
223
- handlers,
224
- outDirAbs,
225
- authBasePath:
226
- config.auth && config.auth.enabled === false
227
- ? undefined
228
- : (config.auth?.basePath ?? "/auth"),
229
- authEnabled: config.auth?.enabled !== false,
230
- configPathAbs,
231
- });
232
- await fs.writeFile(path.join(outDirAbs, "src", "api.ts"), apiTs);
233
- if (clientConfigTs) {
234
- await fs.writeFile(
235
- path.join(outDirAbs, "src", "client.config.ts"),
236
- clientConfigTs,
237
- );
238
- }
239
-
240
- const serverTs = generateHonoServer({
241
- handlers,
242
- outDirAbs,
243
- schemaPathAbs,
244
- configPathAbs,
245
- config,
246
- });
247
- await fs.writeFile(path.join(outDirAbs, "server", "server.ts"), serverTs);
248
-
249
- const websocketDoTs = generateWebsocketDurableObject({
250
- handlers,
251
- outDirAbs,
252
- schemaPathAbs,
253
- configPathAbs,
254
- config,
255
- });
256
- await fs.writeFile(
257
- path.join(outDirAbs, "server", "websocket-hibernation-server.ts"),
258
- websocketDoTs,
259
- );
260
-
261
- const schedulerTs = generateSchedulerHandlers({
262
- handlers,
263
- outDirAbs,
264
- schemaPathAbs,
265
- configPathAbs,
266
- });
267
- await fs.writeFile(
268
- path.join(outDirAbs, "server", "scheduler.ts"),
269
- schedulerTs,
270
- );
271
-
272
- const cronHandlersPresent = handlers.some(
273
- (handler) => handler.kind === "cron",
274
- );
275
- const { code: cronTs, cronTriggers } = generateCronHandlers({
276
- handlers,
277
- outDirAbs,
278
- schemaPathAbs,
279
- configPathAbs,
280
- });
281
- await fs.writeFile(path.join(outDirAbs, "server", "cron.ts"), cronTs);
282
-
283
- const allowedOrigins = normalizeAllowedOrigins(
284
- process.env.APPFLARE_ALLOWED_ORIGINS ??
285
- config.corsOrigin ??
286
- "http://localhost:3000",
287
- );
288
- const workerIndexTs = generateCloudflareWorkerIndex({
289
- allowedOrigins,
290
- hasCronHandlers: cronHandlersPresent,
24
+ .action(async (options: { config: string }) => {
25
+ await runBuild(options.config);
291
26
  });
292
- await fs.writeFile(path.join(outDirAbs, "server", "index.ts"), workerIndexTs);
293
27
 
294
- const wranglerJson = generateWranglerJson({
295
- config,
296
- configDirAbs,
297
- allowedOrigins,
298
- cronTriggers,
28
+ program
29
+ .command("dev")
30
+ .description("Run generator in development mode")
31
+ .option(
32
+ "-c, --config <path>",
33
+ "Path to appflare.config.ts",
34
+ "appflare.config.ts",
35
+ )
36
+ .option("-w, --watch", "Watch scanDir and regenerate on changes", false)
37
+ .action(async (options: { config: string; watch: boolean }) => {
38
+ await runDev(options.config, options.watch);
299
39
  });
300
- const wranglerOutPath =
301
- config.wranglerOutPath ?? path.join(config.outDir, "wrangler.json");
302
- const wranglerOutAbs = path.resolve(configDirAbs, wranglerOutPath);
303
- await fs.mkdir(path.dirname(wranglerOutAbs), { recursive: true });
304
- await fs.writeFile(wranglerOutAbs, wranglerJson);
305
-
306
- if (emit) {
307
- // Remove previous emit output to avoid stale files lingering.
308
- await fs.rm(path.join(outDirAbs, "dist"), { recursive: true, force: true });
309
40
 
310
- // Emit only the files that don't pull in user code outside rootDir.
311
- // This avoids TS rootDir issues and dist overwrite issues caused by user modules.
312
- const emitTsconfigAbs = await writeEmitTsconfig({
313
- configDirAbs,
314
- outDirAbs,
315
- });
316
- await runTscEmit(emitTsconfigAbs);
317
- }
318
- }
319
-
320
- async function watchAndBuild(params: {
321
- configPathAbs: string;
322
- emit: boolean;
323
- }): Promise<void> {
324
- let watcher: FSWatcher | undefined;
325
- let lastWatchConfig: WatchConfig | undefined;
326
- let isBuilding = false;
327
- let pendingBuild = false;
328
- let closed = false;
329
-
330
- const closeWatcher = async () => {
331
- if (!watcher) return;
332
- await watcher.close();
333
- watcher = undefined;
334
- };
335
-
336
- const applyWatchConfig = async (config: WatchConfig) => {
337
- const normalized = normalizeWatchConfig(config);
338
- if (watchConfigsEqual(lastWatchConfig, normalized)) return;
339
-
340
- await closeWatcher();
341
- watcher = chokidar.watch(normalized.targets, {
342
- ignored: normalized.ignored,
343
- ignoreInitial: true,
344
- persistent: true,
345
- });
346
-
347
- watcher.on("all", (_event, filePath) => {
348
- const rel = path.relative(process.cwd(), filePath) || filePath;
349
- console.log(`[appflare] change detected: ${rel}`);
350
- scheduleBuild();
351
- });
352
-
353
- lastWatchConfig = normalized;
354
- console.log(
355
- `[appflare] watching ${normalized.targets.length} path(s) (ignoring ${normalized.ignored.length})`,
356
- );
357
- };
358
-
359
- const scheduleBuild = () => {
360
- if (isBuilding) {
361
- pendingBuild = true;
362
- return;
363
- }
364
- void runBuild();
365
- };
366
-
367
- const runBuild = async () => {
368
- isBuilding = true;
369
- const startedAt = Date.now();
370
- try {
371
- const { config, configDirAbs } = await loadConfig(params.configPathAbs);
372
- await applyWatchConfig(
373
- computeWatchConfig({
374
- config,
375
- configDirAbs,
376
- configPathAbs: params.configPathAbs,
377
- }),
378
- );
379
- console.log("[appflare] build started");
380
- await buildFromConfig({
381
- config,
382
- configDirAbs,
383
- configPathAbs: params.configPathAbs,
384
- emit: params.emit,
41
+ program
42
+ .command("migrate")
43
+ .description(
44
+ "Generate drizzle migration files from outDir/auth.schema.ts and apply them to the configured D1 database",
45
+ )
46
+ .option(
47
+ "-c, --config <path>",
48
+ "Path to appflare.config.ts",
49
+ "appflare.config.ts",
50
+ )
51
+ .option(
52
+ "--local",
53
+ "Execute commands/files against a local DB for use with wrangler dev",
54
+ false,
55
+ )
56
+ .option(
57
+ "--remote",
58
+ "Execute commands/files against a remote DB for use with wrangler dev --remote",
59
+ false,
60
+ )
61
+ .option("--preview", "Execute commands/files against a preview D1 DB", false)
62
+ .action(
63
+ async (options: {
64
+ config: string;
65
+ local: boolean;
66
+ remote: boolean;
67
+ preview: boolean;
68
+ }) => {
69
+ await runMigrate(options.config, {
70
+ local: options.local,
71
+ remote: options.remote,
72
+ preview: options.preview,
385
73
  });
386
- const elapsed = Date.now() - startedAt;
387
- console.log(`[appflare] build finished in ${elapsed}ms`);
388
- } catch (err) {
389
- const message =
390
- err instanceof Error ? (err.stack ?? err.message) : String(err);
391
- console.error(`[appflare] build failed: ${message}`);
392
- } finally {
393
- isBuilding = false;
394
- if (pendingBuild && !closed) {
395
- pendingBuild = false;
396
- scheduleBuild();
397
- }
398
- }
399
- };
400
-
401
- const handleExit = async () => {
402
- closed = true;
403
- await closeWatcher();
404
- };
405
-
406
- process.once("SIGINT", handleExit);
407
- process.once("SIGTERM", handleExit);
408
-
409
- await runBuild();
410
- }
411
-
412
- function computeWatchConfig(params: {
413
- config: AppflareConfig;
414
- configDirAbs: string;
415
- configPathAbs: string;
416
- }): WatchConfig {
417
- const { config, configDirAbs, configPathAbs } = params;
418
- const projectDirAbs = path.resolve(configDirAbs, config.dir);
419
- const schemaPathAbs = path.resolve(configDirAbs, config.schema);
420
- const outDirAbs = path.resolve(configDirAbs, config.outDir);
421
-
422
- return {
423
- targets: [projectDirAbs, schemaPathAbs, configPathAbs],
424
- ignored: [
425
- outDirAbs,
426
- path.join(outDirAbs, "**"),
427
- path.join(projectDirAbs, "node_modules/**"),
428
- path.join(projectDirAbs, "dist/**"),
429
- path.join(projectDirAbs, "build/**"),
430
- "**/node_modules/**",
431
- "**/.git/**",
432
- "**/dist/**",
433
- "**/build/**",
434
- ],
435
- };
436
- }
437
-
438
- function normalizeWatchConfig(config: WatchConfig): WatchConfig {
439
- const normalizeList = (list: string[]): string[] =>
440
- [
441
- ...new Set(
442
- list.map((item) => (hasGlob(item) ? item : path.resolve(item))),
443
- ),
444
- ].sort();
445
-
446
- return {
447
- targets: normalizeList(config.targets),
448
- ignored: normalizeList(config.ignored),
449
- };
450
- }
451
-
452
- function watchConfigsEqual(a?: WatchConfig, b?: WatchConfig): boolean {
453
- if (!a || !b) return false;
454
- return arraysEqual(a.targets, b.targets) && arraysEqual(a.ignored, b.ignored);
455
- }
456
-
457
- function arraysEqual(a: string[], b: string[]): boolean {
458
- return a.length === b.length && a.every((value, index) => value === b[index]);
459
- }
460
-
461
- function hasGlob(value: string): boolean {
462
- return value.includes("*") || value.includes("?") || value.includes("[");
463
- }
74
+ },
75
+ );
464
76
 
465
- function normalizeAllowedOrigins(origins: string | string[]): string[] {
466
- const list = Array.isArray(origins) ? origins : origins.split(",");
467
- return list.map((origin) => origin.trim()).filter(Boolean);
468
- }
77
+ await program.parseAsync(process.argv);
@@ -0,0 +1,182 @@
1
+ import { dirname, isAbsolute, resolve } from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+ import { z } from "zod";
4
+ import type {
5
+ AppflareConfig,
6
+ LoadedAppflareConfig,
7
+ NormalizedAppflareConfig,
8
+ } from "./types";
9
+
10
+ const databaseSchema = z
11
+ .object({
12
+ binding: z.string().min(1),
13
+ databaseName: z.string().min(1),
14
+ databaseId: z.string().min(1),
15
+ previewDatabaseId: z.string().min(1).optional(),
16
+ migrationsDir: z.string().min(1).optional(),
17
+ })
18
+ .strict();
19
+
20
+ const kvSchema = z
21
+ .object({
22
+ binding: z.string().min(1),
23
+ id: z.string().min(1),
24
+ previewId: z.string().min(1).optional(),
25
+ })
26
+ .strict();
27
+
28
+ const r2Schema = z
29
+ .object({
30
+ binding: z.string().min(1),
31
+ bucketName: z.string().min(1),
32
+ previewBucketName: z.string().min(1).optional(),
33
+ jurisdiction: z.string().min(1).optional(),
34
+ })
35
+ .strict();
36
+
37
+ const schedulerConfigSchema = z
38
+ .object({
39
+ enabled: z.boolean().optional(),
40
+ binding: z.string().min(1).optional(),
41
+ queue: z.string().min(1).optional(),
42
+ })
43
+ .strict();
44
+
45
+ const realtimeConfigSchema = z
46
+ .object({
47
+ enabled: z.boolean().optional(),
48
+ binding: z.string().min(1).optional(),
49
+ className: z.string().min(1).optional(),
50
+ objectName: z.string().min(1).optional(),
51
+ subscribePath: z.string().min(1).optional(),
52
+ websocketPath: z.string().min(1).optional(),
53
+ protocol: z.string().min(1).optional(),
54
+ })
55
+ .strict();
56
+
57
+ const appflareConfigSchema = z
58
+ .object({
59
+ scanDir: z.string().min(1),
60
+ outDir: z.string().min(1),
61
+ wranglerOutDir: z.string().min(1).optional(),
62
+ wranglerOutPath: z.string().min(1).optional(),
63
+ schema: z.array(z.string()).min(1),
64
+ schemaDsl: z
65
+ .object({
66
+ entry: z.string().min(1),
67
+ exportName: z.string().min(1).optional(),
68
+ outFile: z.string().min(1).optional(),
69
+ typesOutFile: z.string().min(1).optional(),
70
+ zodOutFile: z.string().min(1).optional(),
71
+ namingStrategy: z.literal("camelToSnake").optional(),
72
+ })
73
+ .strict()
74
+ .optional(),
75
+ database: z.union([databaseSchema, z.array(databaseSchema).min(1)]),
76
+ kv: z.union([kvSchema, z.array(kvSchema)]).optional(),
77
+ r2: z.union([r2Schema, z.array(r2Schema)]).optional(),
78
+ auth: z
79
+ .object({
80
+ enabled: z.boolean(),
81
+ basePath: z.string().min(1),
82
+ options: z.custom<AppflareConfig["auth"]["options"]>((value) => {
83
+ return typeof value === "object" && value !== null;
84
+ }),
85
+ clientOptions: z.custom<AppflareConfig["auth"]["clientOptions"]>(
86
+ (value) => {
87
+ return typeof value === "object" && value !== null;
88
+ },
89
+ ),
90
+ })
91
+ .strict(),
92
+ scheduler: schedulerConfigSchema.optional(),
93
+ realtime: realtimeConfigSchema.optional(),
94
+ wranglerOverrides: z.record(z.string(), z.unknown()).optional(),
95
+ })
96
+ .strict();
97
+
98
+ function isRecord(value: unknown): value is Record<string, unknown> {
99
+ return typeof value === "object" && value !== null;
100
+ }
101
+
102
+ function readLegacySchedulerConfig(
103
+ input: AppflareConfig,
104
+ ): Partial<NonNullable<AppflareConfig["scheduler"]>> {
105
+ const raw = isRecord(input.wranglerOverrides)
106
+ ? input.wranglerOverrides.scheduler
107
+ : undefined;
108
+ const parsed = schedulerConfigSchema.safeParse(raw);
109
+ if (!parsed.success) {
110
+ return {};
111
+ }
112
+
113
+ return parsed.data;
114
+ }
115
+
116
+ function removeLegacySchedulerOverride(
117
+ overrides: AppflareConfig["wranglerOverrides"],
118
+ ): AppflareConfig["wranglerOverrides"] {
119
+ if (!isRecord(overrides) || !("scheduler" in overrides)) {
120
+ return overrides;
121
+ }
122
+
123
+ const { scheduler: _scheduler, ...rest } = overrides;
124
+ return rest;
125
+ }
126
+
127
+ function normalizeConfig(input: AppflareConfig): NormalizedAppflareConfig {
128
+ const legacyScheduler = readLegacySchedulerConfig(input);
129
+ const scheduler = {
130
+ ...(legacyScheduler ?? {}),
131
+ ...(input.scheduler ?? {}),
132
+ };
133
+ const realtime = input.realtime ?? {};
134
+
135
+ return {
136
+ ...input,
137
+ database: Array.isArray(input.database) ? input.database : [input.database],
138
+ kv: input.kv ? (Array.isArray(input.kv) ? input.kv : [input.kv]) : [],
139
+ r2: input.r2 ? (Array.isArray(input.r2) ? input.r2 : [input.r2]) : [],
140
+ scheduler: {
141
+ enabled: scheduler.enabled ?? true,
142
+ binding: scheduler.binding ?? "APPFLARE_SCHEDULER_QUEUE",
143
+ queue: scheduler.queue,
144
+ },
145
+ realtime: {
146
+ enabled: realtime.enabled ?? true,
147
+ binding: realtime.binding ?? "APPFLARE_REALTIME",
148
+ className: realtime.className ?? "AppflareRealtimeDurableObject",
149
+ objectName: realtime.objectName ?? "global",
150
+ subscribePath: realtime.subscribePath ?? "/realtime/subscribe",
151
+ websocketPath: realtime.websocketPath ?? "/realtime/ws",
152
+ protocol: realtime.protocol ?? "appflare.realtime.v1",
153
+ },
154
+ wranglerOverrides: removeLegacySchedulerOverride(input.wranglerOverrides),
155
+ wranglerOutDir:
156
+ input.wranglerOutDir ?? input.wranglerOutPath ?? input.outDir,
157
+ };
158
+ }
159
+
160
+ export async function loadConfig(
161
+ configPathArg?: string,
162
+ ): Promise<LoadedAppflareConfig> {
163
+ const configPath = isAbsolute(configPathArg ?? "")
164
+ ? (configPathArg as string)
165
+ : resolve(process.cwd(), configPathArg ?? "appflare.config.ts");
166
+
167
+ const configDir = dirname(configPath);
168
+ const moduleUrl = pathToFileURL(configPath).href;
169
+ const configModule = await import(moduleUrl);
170
+ const raw = configModule.default;
171
+ const parsed = appflareConfigSchema.parse(raw) as AppflareConfig;
172
+ const config = normalizeConfig(parsed);
173
+
174
+ return {
175
+ configPath,
176
+ configDir,
177
+ scanDirAbs: resolve(configDir, config.scanDir),
178
+ outDirAbs: resolve(configDir, config.outDir),
179
+ wranglerOutDirAbs: resolve(configDir, config.wranglerOutDir),
180
+ config,
181
+ };
182
+ }