lopata 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 (147) hide show
  1. package/README.md +15 -0
  2. package/package.json +51 -0
  3. package/runtime/bindings/ai.ts +132 -0
  4. package/runtime/bindings/analytics-engine.ts +96 -0
  5. package/runtime/bindings/browser.ts +64 -0
  6. package/runtime/bindings/cache.ts +179 -0
  7. package/runtime/bindings/cf-streams.ts +56 -0
  8. package/runtime/bindings/container-docker.ts +225 -0
  9. package/runtime/bindings/container.ts +662 -0
  10. package/runtime/bindings/crypto-extras.ts +89 -0
  11. package/runtime/bindings/d1.ts +315 -0
  12. package/runtime/bindings/do-executor-inprocess.ts +140 -0
  13. package/runtime/bindings/do-executor-worker.ts +368 -0
  14. package/runtime/bindings/do-executor.ts +45 -0
  15. package/runtime/bindings/do-websocket-bridge.ts +70 -0
  16. package/runtime/bindings/do-worker-entry.ts +220 -0
  17. package/runtime/bindings/do-worker-env.ts +74 -0
  18. package/runtime/bindings/durable-object.ts +992 -0
  19. package/runtime/bindings/email.ts +180 -0
  20. package/runtime/bindings/html-rewriter.ts +84 -0
  21. package/runtime/bindings/hyperdrive.ts +130 -0
  22. package/runtime/bindings/images.ts +381 -0
  23. package/runtime/bindings/kv.ts +359 -0
  24. package/runtime/bindings/queue.ts +507 -0
  25. package/runtime/bindings/r2.ts +759 -0
  26. package/runtime/bindings/rpc-stub.ts +267 -0
  27. package/runtime/bindings/scheduled.ts +172 -0
  28. package/runtime/bindings/service-binding.ts +217 -0
  29. package/runtime/bindings/static-assets.ts +481 -0
  30. package/runtime/bindings/websocket-pair.ts +182 -0
  31. package/runtime/bindings/workflow.ts +858 -0
  32. package/runtime/bunflare-config.ts +56 -0
  33. package/runtime/cli/cache.ts +39 -0
  34. package/runtime/cli/context.ts +105 -0
  35. package/runtime/cli/d1.ts +163 -0
  36. package/runtime/cli/dev.ts +392 -0
  37. package/runtime/cli/kv.ts +84 -0
  38. package/runtime/cli/queues.ts +109 -0
  39. package/runtime/cli/r2.ts +140 -0
  40. package/runtime/cli/traces.ts +251 -0
  41. package/runtime/cli.ts +102 -0
  42. package/runtime/config.ts +148 -0
  43. package/runtime/d1-migrate.ts +37 -0
  44. package/runtime/dashboard/api.ts +174 -0
  45. package/runtime/dashboard/app.tsx +220 -0
  46. package/runtime/dashboard/components/breadcrumb.tsx +16 -0
  47. package/runtime/dashboard/components/buttons.tsx +13 -0
  48. package/runtime/dashboard/components/code-block.tsx +5 -0
  49. package/runtime/dashboard/components/detail-field.tsx +8 -0
  50. package/runtime/dashboard/components/empty-state.tsx +8 -0
  51. package/runtime/dashboard/components/filter-input.tsx +11 -0
  52. package/runtime/dashboard/components/index.ts +16 -0
  53. package/runtime/dashboard/components/key-value-table.tsx +23 -0
  54. package/runtime/dashboard/components/modal.tsx +23 -0
  55. package/runtime/dashboard/components/page-header.tsx +11 -0
  56. package/runtime/dashboard/components/pill-button.tsx +14 -0
  57. package/runtime/dashboard/components/refresh-button.tsx +7 -0
  58. package/runtime/dashboard/components/service-info.tsx +45 -0
  59. package/runtime/dashboard/components/status-badge.tsx +7 -0
  60. package/runtime/dashboard/components/table-link.tsx +5 -0
  61. package/runtime/dashboard/components/table.tsx +26 -0
  62. package/runtime/dashboard/components.tsx +19 -0
  63. package/runtime/dashboard/index.html +23 -0
  64. package/runtime/dashboard/lib.ts +45 -0
  65. package/runtime/dashboard/rpc/client.ts +20 -0
  66. package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
  67. package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
  68. package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
  69. package/runtime/dashboard/rpc/handlers/config.ts +137 -0
  70. package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
  71. package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
  72. package/runtime/dashboard/rpc/handlers/do.ts +117 -0
  73. package/runtime/dashboard/rpc/handlers/email.ts +82 -0
  74. package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
  75. package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
  76. package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
  77. package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
  78. package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
  79. package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
  80. package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
  81. package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
  82. package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
  83. package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
  84. package/runtime/dashboard/rpc/hooks.ts +132 -0
  85. package/runtime/dashboard/rpc/server.ts +70 -0
  86. package/runtime/dashboard/rpc/types.ts +396 -0
  87. package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
  88. package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
  89. package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
  90. package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
  91. package/runtime/dashboard/sql-browser/hooks.ts +137 -0
  92. package/runtime/dashboard/sql-browser/index.ts +4 -0
  93. package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
  94. package/runtime/dashboard/sql-browser/modals.tsx +116 -0
  95. package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
  96. package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
  97. package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
  98. package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
  99. package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
  100. package/runtime/dashboard/sql-browser/types.ts +61 -0
  101. package/runtime/dashboard/sql-browser/utils.ts +167 -0
  102. package/runtime/dashboard/style.css +177 -0
  103. package/runtime/dashboard/views/ai.tsx +152 -0
  104. package/runtime/dashboard/views/analytics-engine.tsx +169 -0
  105. package/runtime/dashboard/views/cache.tsx +93 -0
  106. package/runtime/dashboard/views/containers.tsx +197 -0
  107. package/runtime/dashboard/views/d1.tsx +81 -0
  108. package/runtime/dashboard/views/do.tsx +168 -0
  109. package/runtime/dashboard/views/email.tsx +235 -0
  110. package/runtime/dashboard/views/errors.tsx +558 -0
  111. package/runtime/dashboard/views/home.tsx +287 -0
  112. package/runtime/dashboard/views/kv.tsx +273 -0
  113. package/runtime/dashboard/views/queue.tsx +193 -0
  114. package/runtime/dashboard/views/r2.tsx +202 -0
  115. package/runtime/dashboard/views/scheduled.tsx +89 -0
  116. package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
  117. package/runtime/dashboard/views/traces.tsx +768 -0
  118. package/runtime/dashboard/views/workers.tsx +55 -0
  119. package/runtime/dashboard/views/workflows.tsx +473 -0
  120. package/runtime/db.ts +258 -0
  121. package/runtime/env.ts +362 -0
  122. package/runtime/error-page/app.tsx +394 -0
  123. package/runtime/error-page/build.ts +269 -0
  124. package/runtime/error-page/index.html +16 -0
  125. package/runtime/error-page/style.css +31 -0
  126. package/runtime/execution-context.ts +18 -0
  127. package/runtime/file-watcher.ts +57 -0
  128. package/runtime/generation-manager.ts +230 -0
  129. package/runtime/generation.ts +411 -0
  130. package/runtime/plugin.ts +292 -0
  131. package/runtime/request-cf.ts +28 -0
  132. package/runtime/rpc-validate.ts +154 -0
  133. package/runtime/tracing/context.ts +40 -0
  134. package/runtime/tracing/db.ts +73 -0
  135. package/runtime/tracing/frames.ts +75 -0
  136. package/runtime/tracing/instrument.ts +186 -0
  137. package/runtime/tracing/span.ts +138 -0
  138. package/runtime/tracing/store.ts +499 -0
  139. package/runtime/tracing/types.ts +47 -0
  140. package/runtime/vite-plugin/config-plugin.ts +68 -0
  141. package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
  142. package/runtime/vite-plugin/dist/index.mjs +52333 -0
  143. package/runtime/vite-plugin/globals-plugin.ts +94 -0
  144. package/runtime/vite-plugin/index.ts +43 -0
  145. package/runtime/vite-plugin/modules-plugin.ts +88 -0
  146. package/runtime/vite-plugin/react-router-plugin.ts +95 -0
  147. package/runtime/worker-registry.ts +52 -0
package/runtime/db.ts ADDED
@@ -0,0 +1,258 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { mkdirSync } from "node:fs";
3
+ import { join, dirname } from "node:path";
4
+
5
+ const DATA_DIR = join(process.cwd(), ".bunflare");
6
+ const DB_PATH = join(DATA_DIR, "data.sqlite");
7
+
8
+ let instance: Database | null = null;
9
+
10
+ /**
11
+ * Returns the shared SQLite database singleton for bunflare runtime data.
12
+ * Creates the .bunflare/ directory and database file on first call.
13
+ */
14
+ export function getDatabase(): Database {
15
+ if (instance) return instance;
16
+
17
+ mkdirSync(DATA_DIR, { recursive: true });
18
+ mkdirSync(join(DATA_DIR, "r2"), { recursive: true });
19
+ mkdirSync(join(DATA_DIR, "d1"), { recursive: true });
20
+
21
+ instance = new Database(DB_PATH, { create: true });
22
+ instance.run("PRAGMA journal_mode=WAL");
23
+ runMigrations(instance);
24
+ return instance;
25
+ }
26
+
27
+ /**
28
+ * Initialize schema on the given database. Exported so tests and
29
+ * external callers can run migrations on an arbitrary Database instance (e.g. :memory:).
30
+ */
31
+ export function runMigrations(db: Database): void {
32
+ db.run(`
33
+ CREATE TABLE IF NOT EXISTS kv (
34
+ namespace TEXT NOT NULL,
35
+ key TEXT NOT NULL,
36
+ value BLOB NOT NULL,
37
+ metadata TEXT,
38
+ expiration INTEGER,
39
+ PRIMARY KEY (namespace, key)
40
+ )
41
+ `);
42
+
43
+ db.run(`
44
+ CREATE TABLE IF NOT EXISTS r2_objects (
45
+ bucket TEXT NOT NULL,
46
+ key TEXT NOT NULL,
47
+ size INTEGER NOT NULL,
48
+ etag TEXT NOT NULL,
49
+ version TEXT NOT NULL DEFAULT '',
50
+ uploaded TEXT NOT NULL,
51
+ http_metadata TEXT,
52
+ custom_metadata TEXT,
53
+ checksums TEXT,
54
+ PRIMARY KEY (bucket, key)
55
+ )
56
+ `);
57
+
58
+ db.run(`
59
+ CREATE TABLE IF NOT EXISTS r2_multipart_uploads (
60
+ upload_id TEXT PRIMARY KEY,
61
+ bucket TEXT NOT NULL,
62
+ key TEXT NOT NULL,
63
+ http_metadata TEXT,
64
+ custom_metadata TEXT,
65
+ created_at TEXT NOT NULL
66
+ )
67
+ `);
68
+
69
+ db.run(`
70
+ CREATE TABLE IF NOT EXISTS r2_multipart_parts (
71
+ upload_id TEXT NOT NULL,
72
+ part_number INTEGER NOT NULL,
73
+ etag TEXT NOT NULL,
74
+ size INTEGER NOT NULL,
75
+ file_path TEXT NOT NULL,
76
+ PRIMARY KEY (upload_id, part_number)
77
+ )
78
+ `);
79
+
80
+ db.run(`
81
+ CREATE TABLE IF NOT EXISTS do_instances (
82
+ namespace TEXT NOT NULL,
83
+ id TEXT NOT NULL,
84
+ name TEXT,
85
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
86
+ PRIMARY KEY (namespace, id)
87
+ )
88
+ `);
89
+
90
+ db.run(`
91
+ CREATE TABLE IF NOT EXISTS do_storage (
92
+ namespace TEXT NOT NULL,
93
+ id TEXT NOT NULL,
94
+ key TEXT NOT NULL,
95
+ value TEXT NOT NULL,
96
+ PRIMARY KEY (namespace, id, key)
97
+ )
98
+ `);
99
+
100
+ db.run(`
101
+ CREATE TABLE IF NOT EXISTS do_alarms (
102
+ namespace TEXT NOT NULL,
103
+ id TEXT NOT NULL,
104
+ alarm_time INTEGER NOT NULL,
105
+ PRIMARY KEY (namespace, id)
106
+ )
107
+ `);
108
+
109
+ db.run(`
110
+ CREATE TABLE IF NOT EXISTS queue_messages (
111
+ id TEXT PRIMARY KEY,
112
+ queue TEXT NOT NULL,
113
+ body BLOB NOT NULL,
114
+ content_type TEXT NOT NULL DEFAULT 'json',
115
+ attempts INTEGER NOT NULL DEFAULT 0,
116
+ status TEXT NOT NULL DEFAULT 'pending',
117
+ visible_at INTEGER NOT NULL,
118
+ created_at INTEGER NOT NULL,
119
+ completed_at INTEGER
120
+ )
121
+ `);
122
+
123
+ // Migrate: add status column if missing (existing databases)
124
+ {
125
+ const cols = db.query<{ name: string }, []>("PRAGMA table_info(queue_messages)").all();
126
+ if (!cols.some(c => c.name === "status")) {
127
+ db.run("ALTER TABLE queue_messages ADD COLUMN status TEXT NOT NULL DEFAULT 'pending'");
128
+ }
129
+ if (!cols.some(c => c.name === "completed_at")) {
130
+ db.run("ALTER TABLE queue_messages ADD COLUMN completed_at INTEGER");
131
+ }
132
+ }
133
+
134
+ db.run(`CREATE INDEX IF NOT EXISTS idx_queue_visible ON queue_messages(queue, visible_at)`);
135
+
136
+ db.run(`
137
+ CREATE TABLE IF NOT EXISTS queue_leases (
138
+ lease_id TEXT PRIMARY KEY,
139
+ message_id TEXT NOT NULL,
140
+ queue TEXT NOT NULL,
141
+ expires_at INTEGER NOT NULL
142
+ )
143
+ `);
144
+
145
+ db.run(`CREATE INDEX IF NOT EXISTS idx_queue_leases_queue ON queue_leases(queue)`);
146
+
147
+ db.run(`
148
+ CREATE TABLE IF NOT EXISTS workflow_instances (
149
+ id TEXT PRIMARY KEY,
150
+ workflow_name TEXT NOT NULL,
151
+ class_name TEXT NOT NULL,
152
+ params TEXT,
153
+ status TEXT NOT NULL DEFAULT 'running',
154
+ output TEXT,
155
+ error TEXT,
156
+ error_name TEXT,
157
+ created_at INTEGER NOT NULL,
158
+ updated_at INTEGER NOT NULL
159
+ )
160
+ `);
161
+
162
+ db.run(`
163
+ CREATE TABLE IF NOT EXISTS workflow_events (
164
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
165
+ instance_id TEXT NOT NULL,
166
+ event_type TEXT NOT NULL,
167
+ payload TEXT,
168
+ created_at INTEGER NOT NULL
169
+ )
170
+ `);
171
+
172
+ db.run(`
173
+ CREATE TABLE IF NOT EXISTS workflow_steps (
174
+ instance_id TEXT NOT NULL,
175
+ step_name TEXT NOT NULL,
176
+ output TEXT,
177
+ completed_at INTEGER NOT NULL,
178
+ PRIMARY KEY (instance_id, step_name)
179
+ )
180
+ `);
181
+
182
+ db.run(`
183
+ CREATE TABLE IF NOT EXISTS workflow_step_attempts (
184
+ instance_id TEXT NOT NULL,
185
+ step_name TEXT NOT NULL,
186
+ failed_attempts INTEGER NOT NULL DEFAULT 0,
187
+ last_error TEXT,
188
+ last_error_name TEXT,
189
+ last_error_id TEXT,
190
+ updated_at INTEGER,
191
+ PRIMARY KEY (instance_id, step_name)
192
+ )
193
+ `);
194
+
195
+ db.run(`
196
+ CREATE TABLE IF NOT EXISTS cache_entries (
197
+ cache_name TEXT NOT NULL,
198
+ url TEXT NOT NULL,
199
+ status INTEGER NOT NULL,
200
+ headers TEXT NOT NULL,
201
+ body BLOB NOT NULL,
202
+ expires_at INTEGER,
203
+ PRIMARY KEY (cache_name, url)
204
+ )
205
+ `);
206
+
207
+ db.run(`
208
+ CREATE TABLE IF NOT EXISTS email_messages (
209
+ id TEXT PRIMARY KEY,
210
+ binding TEXT NOT NULL,
211
+ from_addr TEXT NOT NULL,
212
+ to_addr TEXT NOT NULL,
213
+ raw BLOB NOT NULL,
214
+ raw_size INTEGER NOT NULL,
215
+ status TEXT NOT NULL DEFAULT 'sent',
216
+ reject_reason TEXT,
217
+ created_at INTEGER NOT NULL
218
+ )
219
+ `);
220
+
221
+ db.run(`
222
+ CREATE TABLE IF NOT EXISTS ai_requests (
223
+ id TEXT PRIMARY KEY,
224
+ model TEXT NOT NULL,
225
+ input_summary TEXT,
226
+ output_summary TEXT,
227
+ duration_ms INTEGER NOT NULL,
228
+ status TEXT NOT NULL DEFAULT 'ok',
229
+ error TEXT,
230
+ is_streaming INTEGER NOT NULL DEFAULT 0,
231
+ created_at INTEGER NOT NULL
232
+ )
233
+ `);
234
+
235
+ db.run(`
236
+ CREATE TABLE IF NOT EXISTS analytics_engine (
237
+ id TEXT PRIMARY KEY,
238
+ dataset TEXT NOT NULL,
239
+ timestamp INTEGER NOT NULL,
240
+ _sample_interval INTEGER NOT NULL DEFAULT 1,
241
+ index1 TEXT,
242
+ blob1 TEXT, blob2 TEXT, blob3 TEXT, blob4 TEXT, blob5 TEXT,
243
+ blob6 TEXT, blob7 TEXT, blob8 TEXT, blob9 TEXT, blob10 TEXT,
244
+ blob11 TEXT, blob12 TEXT, blob13 TEXT, blob14 TEXT, blob15 TEXT,
245
+ blob16 TEXT, blob17 TEXT, blob18 TEXT, blob19 TEXT, blob20 TEXT,
246
+ double1 REAL, double2 REAL, double3 REAL, double4 REAL, double5 REAL,
247
+ double6 REAL, double7 REAL, double8 REAL, double9 REAL, double10 REAL,
248
+ double11 REAL, double12 REAL, double13 REAL, double14 REAL, double15 REAL,
249
+ double16 REAL, double17 REAL, double18 REAL, double19 REAL, double20 REAL
250
+ )
251
+ `);
252
+ db.run(`CREATE INDEX IF NOT EXISTS idx_analytics_engine_dataset_ts ON analytics_engine(dataset, timestamp)`);
253
+ }
254
+
255
+ /** Returns the path to the .bunflare data directory. */
256
+ export function getDataDir(): string {
257
+ return DATA_DIR;
258
+ }
package/runtime/env.ts ADDED
@@ -0,0 +1,362 @@
1
+ import path from "node:path";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import type { WranglerConfig } from "./config";
4
+ import { SqliteKVNamespace } from "./bindings/kv";
5
+ import { FileR2Bucket } from "./bindings/r2";
6
+ import { DurableObjectNamespaceImpl } from "./bindings/durable-object";
7
+ import { SqliteWorkflowBinding } from "./bindings/workflow";
8
+ import { openD1Database } from "./bindings/d1";
9
+ import { SqliteQueueProducer, QueueConsumer } from "./bindings/queue";
10
+ import { SendEmailBinding } from "./bindings/email";
11
+ import { HyperdriveBinding } from "./bindings/hyperdrive";
12
+ import { AiBinding } from "./bindings/ai";
13
+ import { createServiceBinding } from "./bindings/service-binding";
14
+ import { StaticAssets } from "./bindings/static-assets";
15
+ import { ImagesBinding } from "./bindings/images";
16
+ import { BrowserBinding } from "./bindings/browser";
17
+ import { SqliteAnalyticsEngine } from "./bindings/analytics-engine";
18
+ import { DockerManager } from "./bindings/container-docker";
19
+ import { ContainerBase } from "./bindings/container";
20
+ import type { DOExecutorFactory } from "./bindings/do-executor";
21
+ import type { WorkerRegistry } from "./worker-registry";
22
+ import { getDatabase, getDataDir } from "./db";
23
+ import { instrumentBinding, instrumentD1, instrumentDONamespace, instrumentServiceBinding } from "./tracing/instrument";
24
+
25
+ /**
26
+ * Global reference to the built env object. Used by cloudflare:workers `env` export.
27
+ * Must remain the same object reference — we mutate it in place so that
28
+ * `import { env } from "cloudflare:workers"` always sees current bindings.
29
+ */
30
+ export const globalEnv: Record<string, unknown> = {};
31
+
32
+ export function setGlobalEnv(env: Record<string, unknown>) {
33
+ for (const key of Object.keys(globalEnv)) {
34
+ delete globalEnv[key];
35
+ }
36
+ Object.assign(globalEnv, env);
37
+ }
38
+
39
+ export function parseDevVars(content: string): Record<string, string> {
40
+ const vars: Record<string, string> = {};
41
+ for (const line of content.split("\n")) {
42
+ const trimmed = line.trim();
43
+ if (!trimmed || trimmed.startsWith("#")) continue;
44
+ const eqIndex = trimmed.indexOf("=");
45
+ if (eqIndex === -1) continue;
46
+ const key = trimmed.slice(0, eqIndex).trim();
47
+ let value = trimmed.slice(eqIndex + 1).trim();
48
+ // Strip surrounding quotes
49
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
50
+ value = value.slice(1, -1);
51
+ }
52
+ vars[key] = value;
53
+ }
54
+ return vars;
55
+ }
56
+
57
+ interface ConsumerConfig {
58
+ queue: string;
59
+ maxBatchSize: number;
60
+ maxBatchTimeout: number;
61
+ maxRetries: number;
62
+ deadLetterQueue: string | null;
63
+ }
64
+
65
+ interface ServiceBindingEntry {
66
+ bindingName: string;
67
+ serviceName: string;
68
+ entrypoint?: string;
69
+ proxy: Record<string, unknown>;
70
+ }
71
+
72
+ interface ClassRegistry {
73
+ durableObjects: { bindingName: string; className: string; namespace: DurableObjectNamespaceImpl }[];
74
+ workflows: { bindingName: string; className: string; binding: SqliteWorkflowBinding }[];
75
+ containers: { className: string; image: string; maxInstances?: number; namespace: DurableObjectNamespaceImpl }[];
76
+ queueConsumers: ConsumerConfig[];
77
+ serviceBindings: ServiceBindingEntry[];
78
+ staticAssets: StaticAssets | null;
79
+ }
80
+
81
+ export function buildEnv(config: WranglerConfig, devVarsDir?: string, executorFactory?: DOExecutorFactory, browserConfig?: { wsEndpoint?: string; executablePath?: string; headless?: boolean }): { env: Record<string, unknown>; registry: ClassRegistry } {
82
+ const env: Record<string, unknown> = {};
83
+ const registry: ClassRegistry = { durableObjects: [], workflows: [], containers: [], queueConsumers: [], serviceBindings: [], staticAssets: null };
84
+
85
+ // Environment variables from config
86
+ if (config.vars) {
87
+ for (const [key, value] of Object.entries(config.vars)) {
88
+ env[key] = value;
89
+ }
90
+ }
91
+
92
+ // Override with .dev.vars or .env file (if exists)
93
+ // .dev.vars takes priority over .env (matching CF behavior)
94
+ if (devVarsDir) {
95
+ const devVarsPath = path.join(devVarsDir, ".dev.vars");
96
+ const envPath = path.join(devVarsDir, ".env");
97
+ const filePath = existsSync(devVarsPath) ? devVarsPath : existsSync(envPath) ? envPath : null;
98
+ if (filePath) {
99
+ const content = readFileSync(filePath, "utf-8");
100
+ const devVars = parseDevVars(content);
101
+ for (const [key, value] of Object.entries(devVars)) {
102
+ env[key] = value;
103
+ }
104
+ }
105
+ }
106
+
107
+ // KV namespaces
108
+ const db = getDatabase();
109
+ for (const kv of config.kv_namespaces ?? []) {
110
+ console.log(`[bunflare] KV namespace: ${kv.binding}`);
111
+ env[kv.binding] = instrumentBinding(new SqliteKVNamespace(db, kv.id), {
112
+ type: "kv", name: kv.binding,
113
+ methods: ["get", "getWithMetadata", "put", "delete", "list"],
114
+ });
115
+ }
116
+
117
+ // R2 buckets
118
+ for (const r2 of config.r2_buckets ?? []) {
119
+ console.log(`[bunflare] R2 bucket: ${r2.binding} (${r2.bucket_name})`);
120
+ env[r2.binding] = instrumentBinding(new FileR2Bucket(db, r2.bucket_name, getDataDir()), {
121
+ type: "r2", name: r2.binding,
122
+ methods: ["get", "put", "delete", "list", "head", "createMultipartUpload"],
123
+ });
124
+ }
125
+
126
+ // Durable Objects
127
+ for (const doBinding of config.durable_objects?.bindings ?? []) {
128
+ console.log(`[bunflare] Durable Object: ${doBinding.name} -> ${doBinding.class_name}`);
129
+ const namespace = new DurableObjectNamespaceImpl(db, doBinding.class_name, getDataDir(), undefined, executorFactory);
130
+ env[doBinding.name] = instrumentDONamespace(namespace, doBinding.class_name);
131
+ registry.durableObjects.push({
132
+ bindingName: doBinding.name,
133
+ className: doBinding.class_name,
134
+ namespace,
135
+ });
136
+ }
137
+
138
+ // Workflows
139
+ for (const wf of config.workflows ?? []) {
140
+ console.log(`[bunflare] Workflow: ${wf.binding} -> ${wf.class_name}`);
141
+ const binding = new SqliteWorkflowBinding(db, wf.binding, wf.class_name, wf.limits);
142
+ env[wf.binding] = instrumentBinding(binding, {
143
+ type: "workflow", name: wf.binding,
144
+ methods: ["create", "get"],
145
+ });
146
+ registry.workflows.push({
147
+ bindingName: wf.binding,
148
+ className: wf.class_name,
149
+ binding,
150
+ });
151
+ }
152
+
153
+ // D1 databases
154
+ for (const d1 of config.d1_databases ?? []) {
155
+ console.log(`[bunflare] D1 database: ${d1.binding} (${d1.database_name})`);
156
+ env[d1.binding] = instrumentD1(openD1Database(getDataDir(), d1.database_name), d1.binding);
157
+ }
158
+
159
+ // Queue producers
160
+ for (const producer of config.queues?.producers ?? []) {
161
+ console.log(`[bunflare] Queue producer: ${producer.binding} -> ${producer.queue}`);
162
+ env[producer.binding] = instrumentBinding(new SqliteQueueProducer(db, producer.queue, producer.delivery_delay ?? 0), {
163
+ type: "queue", name: producer.binding,
164
+ methods: ["send", "sendBatch"],
165
+ });
166
+ }
167
+
168
+ // Queue consumers (configs — actual consumers started in dev.ts after worker import)
169
+ for (const consumer of config.queues?.consumers ?? []) {
170
+ console.log(`[bunflare] Queue consumer: ${consumer.queue}`);
171
+ registry.queueConsumers.push({
172
+ queue: consumer.queue,
173
+ maxBatchSize: consumer.max_batch_size ?? 10,
174
+ maxBatchTimeout: consumer.max_batch_timeout ?? 5,
175
+ maxRetries: consumer.max_retries ?? 3,
176
+ deadLetterQueue: consumer.dead_letter_queue ?? null,
177
+ });
178
+ }
179
+
180
+ // Service bindings
181
+ for (const svc of config.services ?? []) {
182
+ console.log(`[bunflare] Service binding: ${svc.binding} -> ${svc.service}${svc.entrypoint ? ` (${svc.entrypoint})` : ""}`);
183
+ const proxy = createServiceBinding(svc.service, svc.entrypoint);
184
+ env[svc.binding] = instrumentServiceBinding(proxy as object, svc.service) as Record<string, unknown>;
185
+ registry.serviceBindings.push({
186
+ bindingName: svc.binding,
187
+ serviceName: svc.service,
188
+ entrypoint: svc.entrypoint,
189
+ proxy,
190
+ });
191
+ }
192
+
193
+ // Images binding
194
+ if (config.images) {
195
+ console.log(`[bunflare] Images binding: ${config.images.binding}`);
196
+ env[config.images.binding] = instrumentBinding(new ImagesBinding(), {
197
+ type: "images", name: config.images.binding,
198
+ methods: ["info"],
199
+ });
200
+ }
201
+
202
+ // Send email bindings
203
+ for (const email of config.send_email ?? []) {
204
+ console.log(`[bunflare] Send email binding: ${email.name}`);
205
+ env[email.name] = instrumentBinding(
206
+ new SendEmailBinding(db, email.name, email.destination_address, email.allowed_destination_addresses),
207
+ { type: "email", name: email.name, methods: ["send"] },
208
+ );
209
+ }
210
+
211
+ // Hyperdrive
212
+ for (const hd of config.hyperdrive ?? []) {
213
+ const connStr = hd.localConnectionString ?? "";
214
+ console.log(`[bunflare] Hyperdrive: ${hd.binding}`);
215
+ env[hd.binding] = new HyperdriveBinding(connStr);
216
+ }
217
+
218
+ // Workers AI
219
+ if (config.ai) {
220
+ const accountId = (env.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID) as string | undefined;
221
+ const apiToken = (env.CLOUDFLARE_API_TOKEN ?? process.env.CLOUDFLARE_API_TOKEN) as string | undefined;
222
+ console.log(`[bunflare] AI binding: ${config.ai.binding}`);
223
+ env[config.ai.binding] = instrumentBinding(
224
+ new AiBinding(db, accountId, apiToken),
225
+ { type: "ai", name: config.ai.binding, methods: ["run", "models"] },
226
+ );
227
+ }
228
+
229
+ // Analytics Engine datasets
230
+ for (const ae of config.analytics_engine_datasets ?? []) {
231
+ console.log(`[bunflare] Analytics Engine: ${ae.binding} (dataset: ${ae.dataset ?? ae.binding})`);
232
+ env[ae.binding] = instrumentBinding(
233
+ new SqliteAnalyticsEngine(db, ae.dataset ?? ae.binding),
234
+ { type: "analytics_engine", name: ae.binding, methods: ["writeDataPoint"] },
235
+ );
236
+ }
237
+
238
+ // Containers — create DO namespaces for container classes
239
+ const doClassNames = new Set((config.durable_objects?.bindings ?? []).map(b => b.class_name));
240
+ for (const container of config.containers ?? []) {
241
+ // Skip if this class is already defined as a DO binding (avoid double-creating)
242
+ if (doClassNames.has(container.class_name)) {
243
+ // Find the existing namespace and register container config on it
244
+ const existing = registry.durableObjects.find(d => d.className === container.class_name);
245
+ if (existing) {
246
+ registry.containers.push({
247
+ className: container.class_name,
248
+ image: container.image,
249
+ maxInstances: container.max_instances,
250
+ namespace: existing.namespace,
251
+ });
252
+ console.log(`[bunflare] Container: ${container.class_name} (reusing DO binding, image: ${container.image})`);
253
+ }
254
+ } else {
255
+ // Create a new DO namespace for this container
256
+ const bindingName = container.name ?? container.class_name;
257
+ console.log(`[bunflare] Container: ${bindingName} -> ${container.class_name} (image: ${container.image})`);
258
+ const namespace = new DurableObjectNamespaceImpl(db, container.class_name, getDataDir(), undefined, executorFactory);
259
+ env[bindingName] = instrumentDONamespace(namespace, container.class_name);
260
+ registry.durableObjects.push({
261
+ bindingName,
262
+ className: container.class_name,
263
+ namespace,
264
+ });
265
+ registry.containers.push({
266
+ className: container.class_name,
267
+ image: container.image,
268
+ maxInstances: container.max_instances,
269
+ namespace,
270
+ });
271
+ }
272
+ }
273
+
274
+ // Static assets
275
+ if (config.assets) {
276
+ const assetsDir = path.resolve(config.assets.directory);
277
+ const assets = new StaticAssets(assetsDir, config.assets.html_handling, config.assets.not_found_handling);
278
+ registry.staticAssets = assets;
279
+ if (config.assets.binding) {
280
+ console.log(`[bunflare] Static assets: ${config.assets.binding} -> ${config.assets.directory}`);
281
+ env[config.assets.binding] = instrumentBinding(assets, {
282
+ type: "assets", name: config.assets.binding,
283
+ methods: ["fetch"],
284
+ });
285
+ } else {
286
+ console.log(`[bunflare] Static assets: ${config.assets.directory} (auto-serve)`);
287
+ }
288
+ }
289
+
290
+ // Browser Rendering binding
291
+ if (config.browser) {
292
+ console.log(`[bunflare] Browser binding: ${config.browser.binding}`);
293
+ env[config.browser.binding] = instrumentBinding(
294
+ new BrowserBinding(browserConfig ?? {}),
295
+ { type: "browser", name: config.browser.binding, methods: ["launch", "connect", "sessions"] },
296
+ );
297
+ }
298
+
299
+ // Version metadata binding
300
+ if (config.version_metadata) {
301
+ const binding = config.version_metadata.binding;
302
+ env[binding] = {
303
+ id: "local-dev",
304
+ tag: "",
305
+ timestamp: new Date().toISOString(),
306
+ };
307
+ }
308
+
309
+ // Store reference for cloudflare:workers env export
310
+ setGlobalEnv(env);
311
+
312
+ return { env, registry };
313
+ }
314
+
315
+ export function wireClassRefs(
316
+ registry: ClassRegistry,
317
+ workerModule: Record<string, unknown>,
318
+ env: Record<string, unknown>,
319
+ workerRegistry?: WorkerRegistry,
320
+ ) {
321
+ for (const entry of registry.durableObjects) {
322
+ const cls = workerModule[entry.className];
323
+ if (!cls) throw new Error(`Durable Object class "${entry.className}" not exported from worker module`);
324
+ entry.namespace._setClass(cls as any, env);
325
+ console.log(`[bunflare] Wired DO class: ${entry.className}`);
326
+ }
327
+
328
+ for (const entry of registry.workflows) {
329
+ const cls = workerModule[entry.className];
330
+ if (!cls) throw new Error(`Workflow class "${entry.className}" not exported from worker module`);
331
+ entry.binding._setClass(cls as any, env);
332
+ entry.binding.resumeInterrupted();
333
+ console.log(`[bunflare] Wired Workflow class: ${entry.className}`);
334
+ }
335
+
336
+ // Wire container configs onto namespaces
337
+ const dockerManager = new DockerManager();
338
+ for (const entry of registry.containers) {
339
+ entry.namespace._setContainerConfig({
340
+ className: entry.className,
341
+ image: entry.image,
342
+ maxInstances: entry.maxInstances,
343
+ dockerManager,
344
+ });
345
+ console.log(`[bunflare] Wired container config: ${entry.className} (image: ${entry.image})`);
346
+ }
347
+
348
+ // Wire service bindings
349
+ for (const entry of registry.serviceBindings) {
350
+ const wire = entry.proxy._wire as ((resolver: () => { workerModule: Record<string, unknown>; env: Record<string, unknown> }) => void) | undefined;
351
+ if (wire) {
352
+ if (workerRegistry) {
353
+ // Resolve through registry (handles both self-ref and cross-worker)
354
+ wire(() => workerRegistry.resolveTarget(entry.serviceName));
355
+ } else {
356
+ // Backward compat: self-reference
357
+ wire(() => ({ workerModule, env }));
358
+ }
359
+ console.log(`[bunflare] Wired service binding: ${entry.bindingName} -> ${entry.serviceName}${entry.entrypoint ? ` (${entry.entrypoint})` : ""}`);
360
+ }
361
+ }
362
+ }