better-cf 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.
@@ -0,0 +1,1065 @@
1
+ import { Command } from 'commander';
2
+ import pc from 'picocolors';
3
+ import { spawn } from 'child_process';
4
+ import fs7 from 'fs';
5
+ import path3 from 'path';
6
+ import { Project, SyntaxKind, Node } from 'ts-morph';
7
+ import { applyEdits, modify, parse } from 'jsonc-parser';
8
+ import chokidar from 'chokidar';
9
+
10
+ // src/cli/index.ts
11
+ function printLine(message) {
12
+ process.stdout.write(`${message}
13
+ `);
14
+ }
15
+ function printErrLine(message) {
16
+ process.stderr.write(`${message}
17
+ `);
18
+ }
19
+ var logger = {
20
+ header(title) {
21
+ printLine(pc.bold(pc.cyan(`== ${title} ==`)));
22
+ },
23
+ info(message) {
24
+ printLine(`${pc.blue("[*]")} ${message}`);
25
+ },
26
+ success(message) {
27
+ printLine(`${pc.green("[+]")} ${message}`);
28
+ },
29
+ warn(message) {
30
+ printLine(`${pc.yellow("[!]")} ${message}`);
31
+ },
32
+ error(message) {
33
+ printErrLine(`${pc.red("[x]")} ${message}`);
34
+ },
35
+ section(message) {
36
+ printLine("");
37
+ printLine(pc.bold(pc.white(`-- ${message} --`)));
38
+ },
39
+ item(label, value) {
40
+ printLine(` -> ${pc.bold(label)}${value ? `: ${value}` : ""}`);
41
+ }
42
+ };
43
+ function runCommand(command, args, cwd, stdio = "inherit") {
44
+ return new Promise((resolve, reject) => {
45
+ const child = spawn(command, args, {
46
+ cwd,
47
+ stdio,
48
+ env: process.env
49
+ });
50
+ child.once("error", (error) => reject(error));
51
+ child.once("close", (code) => resolve(code ?? 0));
52
+ });
53
+ }
54
+ function spawnCommand(command, args, cwd) {
55
+ return spawn(command, args, {
56
+ cwd,
57
+ stdio: "inherit",
58
+ env: process.env
59
+ });
60
+ }
61
+ var DEFAULT_IGNORE = ["node_modules", ".better-cf", "dist", ".wrangler"];
62
+ function loadCliConfig(rootDir = process.cwd()) {
63
+ const defaults = {
64
+ rootDir,
65
+ ignore: [...DEFAULT_IGNORE],
66
+ workerEntry: void 0,
67
+ legacyServiceWorker: false
68
+ };
69
+ const configPath = path3.join(rootDir, "better-cf.config.ts");
70
+ if (!fs7.existsSync(configPath)) {
71
+ return defaults;
72
+ }
73
+ const project = new Project({
74
+ compilerOptions: {
75
+ target: 99,
76
+ module: 99,
77
+ moduleResolution: 99
78
+ }
79
+ });
80
+ const sourceFile = project.addSourceFileAtPath(configPath);
81
+ const variable = sourceFile.getVariableDeclarations().find((decl) => decl.getName() === "betterCfConfig" && decl.getVariableStatement()?.isExported());
82
+ if (!variable) {
83
+ return defaults;
84
+ }
85
+ const initializer = variable.getInitializer();
86
+ if (!initializer || initializer.getKind() !== SyntaxKind.ObjectLiteralExpression) {
87
+ return defaults;
88
+ }
89
+ const configObject = initializer;
90
+ const workerEntry = readString(configObject, "workerEntry");
91
+ const legacyServiceWorker = readBoolean(configObject, "legacyServiceWorker");
92
+ const ignore = readStringArray(configObject, "ignore");
93
+ return {
94
+ rootDir,
95
+ workerEntry,
96
+ legacyServiceWorker: legacyServiceWorker ?? defaults.legacyServiceWorker,
97
+ ignore: ignore ? [.../* @__PURE__ */ new Set([...defaults.ignore, ...ignore])] : defaults.ignore
98
+ };
99
+ }
100
+ function readString(node, key) {
101
+ const property = node.getProperties().find((prop) => {
102
+ return Node.isPropertyAssignment(prop) && prop.getName() === key;
103
+ });
104
+ if (!property || !Node.isPropertyAssignment(property)) {
105
+ return void 0;
106
+ }
107
+ const initializer = property.getInitializer();
108
+ if (!initializer || !Node.isStringLiteral(initializer)) {
109
+ return void 0;
110
+ }
111
+ return initializer.getLiteralText();
112
+ }
113
+ function readBoolean(node, key) {
114
+ const property = node.getProperties().find((prop) => {
115
+ return Node.isPropertyAssignment(prop) && prop.getName() === key;
116
+ });
117
+ if (!property || !Node.isPropertyAssignment(property)) {
118
+ return void 0;
119
+ }
120
+ const initializer = property.getInitializer();
121
+ if (!initializer) {
122
+ return void 0;
123
+ }
124
+ if (initializer.getKind() === SyntaxKind.TrueKeyword) {
125
+ return true;
126
+ }
127
+ if (initializer.getKind() === SyntaxKind.FalseKeyword) {
128
+ return false;
129
+ }
130
+ return void 0;
131
+ }
132
+ function readStringArray(node, key) {
133
+ const property = node.getProperties().find((prop) => {
134
+ return Node.isPropertyAssignment(prop) && prop.getName() === key;
135
+ });
136
+ if (!property || !Node.isPropertyAssignment(property)) {
137
+ return void 0;
138
+ }
139
+ const initializer = property.getInitializer();
140
+ if (!initializer || !Node.isArrayLiteralExpression(initializer)) {
141
+ return void 0;
142
+ }
143
+ const values = initializer.getElements().filter((element) => Node.isStringLiteral(element)).map((element) => element.getLiteralText());
144
+ return values.length > 0 ? values : void 0;
145
+ }
146
+ function resolveWorkerEntry(config) {
147
+ const candidates = [
148
+ config.workerEntry,
149
+ "worker.ts",
150
+ "src/worker.ts",
151
+ "index.ts",
152
+ "src/index.ts"
153
+ ].filter((value) => Boolean(value));
154
+ for (const candidate of candidates) {
155
+ const absolutePath = path3.isAbsolute(candidate) ? candidate : path3.join(config.rootDir, candidate);
156
+ if (fs7.existsSync(absolutePath)) {
157
+ return absolutePath;
158
+ }
159
+ }
160
+ throw new Error(
161
+ "Could not find worker entry. Provide betterCfConfig.workerEntry in better-cf.config.ts or create worker.ts."
162
+ );
163
+ }
164
+
165
+ // src/cli/codegen.ts
166
+ function generateCode(discovery, config) {
167
+ const outputDir = path3.join(config.rootDir, ".better-cf");
168
+ fs7.mkdirSync(outputDir, { recursive: true });
169
+ const workerEntryAbsolute = resolveWorkerEntry(config);
170
+ const entryContents = renderEntryFile(discovery, workerEntryAbsolute, outputDir, config);
171
+ const typesContents = renderTypesFile(discovery);
172
+ const entryPath = path3.join(outputDir, "entry.ts");
173
+ const typesPath = path3.join(outputDir, "types.d.ts");
174
+ fs7.writeFileSync(entryPath, entryContents, "utf8");
175
+ fs7.writeFileSync(typesPath, typesContents, "utf8");
176
+ return {
177
+ entryPath,
178
+ typesPath
179
+ };
180
+ }
181
+ function renderEntryFile(discovery, workerEntryAbsolute, outDir, config) {
182
+ const imports = [];
183
+ const bindings = [];
184
+ const queueMap = [];
185
+ imports.push(`import workerDefault, * as workerModule from '${toImportPath(outDir, workerEntryAbsolute)}';`);
186
+ imports.push(`import { getQueueInternals, resolveWorkerHandlers } from 'better-cf/queue/internal';`);
187
+ for (const queue of discovery.queues) {
188
+ const queueImportPath = toImportPath(outDir, queue.absoluteFilePath);
189
+ if (queue.isDefaultExport) {
190
+ imports.push(`import ${queue.importName} from '${queueImportPath}';`);
191
+ } else {
192
+ imports.push(`import { ${queue.exportName} as ${queue.importName} } from '${queueImportPath}';`);
193
+ }
194
+ bindings.push(`getQueueInternals(${queue.importName}).setBinding('${queue.bindingName}');`);
195
+ queueMap.push(` '${queue.queueName}': ${queue.importName}`);
196
+ }
197
+ const scheduledBlock = `
198
+ const __workerHandlers = resolveWorkerHandlers({ default: workerDefault, ...workerModule });
199
+
200
+ export default {
201
+ async fetch(request: Request, env: unknown, ctx: ExecutionContext): Promise<Response> {
202
+ return __workerHandlers.fetch(request, env, ctx);
203
+ },
204
+
205
+ async queue(batch: MessageBatch<unknown>, env: unknown, ctx: ExecutionContext): Promise<void> {
206
+ const consumer = __queues[batch.queue];
207
+ if (!consumer) {
208
+ console.error(\`[better-cf] No queue consumer for "\${batch.queue}". Acking batch to avoid infinite retries.\`);
209
+ batch.ackAll();
210
+ return;
211
+ }
212
+
213
+ await getQueueInternals(consumer).consume(batch, env, ctx);
214
+ },
215
+
216
+ ...(__workerHandlers.scheduled
217
+ ? {
218
+ async scheduled(event: ScheduledEvent, env: unknown, ctx: ExecutionContext): Promise<void> {
219
+ await __workerHandlers.scheduled?.(event, env, ctx);
220
+ }
221
+ }
222
+ : {})
223
+ };`;
224
+ const legacyWarning = config.legacyServiceWorker ? "console.warn('[better-cf] legacyServiceWorker mode is compatibility-only. Consider migrating to module workers.');" : "";
225
+ return `// Auto-generated by better-cf. Do not edit.
226
+ ${imports.join("\n")}
227
+
228
+ ${bindings.join("\n")}
229
+
230
+ const __queues: Record<string, unknown> = {
231
+ ${queueMap.join(",\n")}
232
+ };
233
+
234
+ ${legacyWarning}
235
+ ${scheduledBlock}
236
+ `;
237
+ }
238
+ function renderTypesFile(discovery) {
239
+ const lines = discovery.queues.map((queue) => {
240
+ return ` ${queue.bindingName}: Queue;`;
241
+ });
242
+ return `// Auto-generated by better-cf. Do not edit.
243
+ import type { Queue } from '@cloudflare/workers-types';
244
+
245
+ declare module 'better-cf/queue' {
246
+ interface BetterCfGeneratedBindings {
247
+ ${lines.join("\n")}
248
+ }
249
+ }
250
+
251
+ export {};
252
+ `;
253
+ }
254
+ function toImportPath(fromDir, targetFile) {
255
+ const relative = path3.relative(fromDir, targetFile).replace(/\\/g, "/");
256
+ if (relative.startsWith(".")) {
257
+ return relative.replace(/\.tsx?$/, "");
258
+ }
259
+ return `./${relative.replace(/\.tsx?$/, "")}`;
260
+ }
261
+
262
+ // src/cli/discovery/naming.ts
263
+ function deriveQueueName(input) {
264
+ const withoutSuffix = input.replace(/Queue$/, "");
265
+ const kebab = withoutSuffix.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
266
+ return kebab || input.toLowerCase();
267
+ }
268
+ function deriveBindingName(queueName) {
269
+ return `QUEUE_${queueName.replace(/[^a-zA-Z0-9]/g, "_").replace(/_+/g, "_").toUpperCase()}`;
270
+ }
271
+ function makeImportName(queueName, isDefaultExport, exportName) {
272
+ if (!isDefaultExport) {
273
+ return exportName;
274
+ }
275
+ return `__queue_${queueName.replace(/-/g, "_")}`;
276
+ }
277
+
278
+ // src/cli/discovery/scanner.ts
279
+ var RESERVED_KEYS = /* @__PURE__ */ new Set([
280
+ "retry",
281
+ "retryDelay",
282
+ "deadLetter",
283
+ "visibilityTimeout",
284
+ "batch",
285
+ "message",
286
+ "process",
287
+ "processBatch",
288
+ "onFailure"
289
+ ]);
290
+ async function scanQueues(config) {
291
+ const diagnostics = [];
292
+ const candidates = collectSourceFiles(config.rootDir, config.ignore);
293
+ const project = createProject(config.rootDir);
294
+ const queues = [];
295
+ for (const absolutePath of candidates) {
296
+ try {
297
+ const sourceFile = project.addSourceFileAtPath(absolutePath);
298
+ const localDefineQueueNames = getDefineQueueLocalNames(sourceFile);
299
+ if (localDefineQueueNames.size === 0) {
300
+ project.removeSourceFile(sourceFile);
301
+ continue;
302
+ }
303
+ for (const declaration of sourceFile.getVariableDeclarations()) {
304
+ const variableStatement = declaration.getVariableStatement();
305
+ if (!variableStatement || !variableStatement.isExported()) {
306
+ continue;
307
+ }
308
+ const call = declaration.getInitializerIfKind(SyntaxKind.CallExpression);
309
+ if (!call || !isDefineQueueCall(call, localDefineQueueNames)) {
310
+ continue;
311
+ }
312
+ const exportName = declaration.getName();
313
+ const queueName = deriveQueueName(exportName);
314
+ const extracted = extractQueueConfig(call, absolutePath, diagnostics, config.rootDir);
315
+ queues.push({
316
+ exportName,
317
+ queueName,
318
+ bindingName: deriveBindingName(queueName),
319
+ filePath: path3.relative(config.rootDir, absolutePath),
320
+ absoluteFilePath: absolutePath,
321
+ isDefaultExport: false,
322
+ importName: makeImportName(queueName, false, exportName),
323
+ config: extracted
324
+ });
325
+ }
326
+ for (const exportAssignment of sourceFile.getExportAssignments()) {
327
+ if (exportAssignment.isExportEquals()) {
328
+ continue;
329
+ }
330
+ const call = resolveCallExpressionFromExportAssignment(exportAssignment.getExpression());
331
+ if (!call || !isDefineQueueCall(call, localDefineQueueNames)) {
332
+ continue;
333
+ }
334
+ const basename = path3.basename(absolutePath, path3.extname(absolutePath));
335
+ const queueName = deriveQueueName(basename);
336
+ const extracted = extractQueueConfig(call, absolutePath, diagnostics, config.rootDir);
337
+ queues.push({
338
+ exportName: "default",
339
+ queueName,
340
+ bindingName: deriveBindingName(queueName),
341
+ filePath: path3.relative(config.rootDir, absolutePath),
342
+ absoluteFilePath: absolutePath,
343
+ isDefaultExport: true,
344
+ importName: makeImportName(queueName, true, "default"),
345
+ config: extracted
346
+ });
347
+ }
348
+ project.removeSourceFile(sourceFile);
349
+ } catch (error) {
350
+ diagnostics.push({
351
+ level: "error",
352
+ code: "SCANNER_FILE_ERROR",
353
+ message: `Failed to parse ${absolutePath}: ${toErrorMessage(error)}`,
354
+ filePath: path3.relative(config.rootDir, absolutePath)
355
+ });
356
+ }
357
+ }
358
+ addConflictDiagnostics(queues, diagnostics);
359
+ if (queues.length === 0) {
360
+ diagnostics.push({
361
+ level: "warning",
362
+ code: "NO_QUEUES_FOUND",
363
+ message: "No defineQueue exports found in this project."
364
+ });
365
+ }
366
+ return {
367
+ queues,
368
+ diagnostics
369
+ };
370
+ }
371
+ function createProject(rootDir) {
372
+ const tsConfigPath = path3.join(rootDir, "tsconfig.json");
373
+ if (fs7.existsSync(tsConfigPath)) {
374
+ return new Project({
375
+ tsConfigFilePath: tsConfigPath,
376
+ skipAddingFilesFromTsConfig: true
377
+ });
378
+ }
379
+ return new Project({
380
+ compilerOptions: {
381
+ target: 99,
382
+ module: 99,
383
+ moduleResolution: 99,
384
+ allowJs: false
385
+ }
386
+ });
387
+ }
388
+ function collectSourceFiles(rootDir, ignore) {
389
+ const files = [];
390
+ const ignoreSet = new Set(ignore);
391
+ function walk(currentPath) {
392
+ const entries = fs7.readdirSync(currentPath, { withFileTypes: true });
393
+ for (const entry of entries) {
394
+ if (entry.name.startsWith(".git")) {
395
+ continue;
396
+ }
397
+ if (ignoreSet.has(entry.name)) {
398
+ continue;
399
+ }
400
+ const absolutePath = path3.join(currentPath, entry.name);
401
+ if (entry.isDirectory()) {
402
+ walk(absolutePath);
403
+ continue;
404
+ }
405
+ if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
406
+ const content = fs7.readFileSync(absolutePath, "utf8");
407
+ if (content.includes("defineQueue")) {
408
+ files.push(absolutePath);
409
+ }
410
+ }
411
+ }
412
+ }
413
+ walk(rootDir);
414
+ return files;
415
+ }
416
+ function getDefineQueueLocalNames(sourceFile) {
417
+ const names = /* @__PURE__ */ new Set();
418
+ for (const importDecl of sourceFile.getImportDeclarations()) {
419
+ const specifier = importDecl.getModuleSpecifierValue();
420
+ if (!specifier.includes("better-cf.config")) {
421
+ continue;
422
+ }
423
+ for (const namedImport of importDecl.getNamedImports()) {
424
+ if (namedImport.getName() !== "defineQueue") {
425
+ continue;
426
+ }
427
+ names.add(namedImport.getAliasNode()?.getText() ?? namedImport.getName());
428
+ }
429
+ }
430
+ return names;
431
+ }
432
+ function isDefineQueueCall(call, localNames) {
433
+ const expression = call.getExpression();
434
+ if (expression.getKind() !== SyntaxKind.Identifier) {
435
+ return false;
436
+ }
437
+ return localNames.has(expression.getText());
438
+ }
439
+ function resolveCallExpressionFromExportAssignment(expression) {
440
+ if (expression.getKind() === SyntaxKind.CallExpression) {
441
+ return expression;
442
+ }
443
+ if (expression.getKind() !== SyntaxKind.Identifier) {
444
+ return void 0;
445
+ }
446
+ const symbol = expression.getSymbol();
447
+ const declaration = symbol?.getValueDeclaration();
448
+ if (!declaration || !Node.isVariableDeclaration(declaration)) {
449
+ return void 0;
450
+ }
451
+ return declaration.getInitializerIfKind(SyntaxKind.CallExpression) ?? void 0;
452
+ }
453
+ function extractQueueConfig(call, absolutePath, diagnostics, rootDir) {
454
+ const firstArg = call.getArguments()[0];
455
+ if (!firstArg || firstArg.getKind() !== SyntaxKind.ObjectLiteralExpression) {
456
+ return {
457
+ hasProcess: false,
458
+ hasProcessBatch: false,
459
+ isMultiJob: false
460
+ };
461
+ }
462
+ const objectLiteral = firstArg;
463
+ const hasProcess = hasProperty(objectLiteral, "process");
464
+ const hasProcessBatch = hasProperty(objectLiteral, "processBatch");
465
+ if (hasProcess && hasProcessBatch) {
466
+ diagnostics.push({
467
+ level: "error",
468
+ code: "INVALID_PROCESS_MODE",
469
+ message: "Queue config cannot include both process and processBatch.",
470
+ filePath: path3.relative(rootDir, absolutePath)
471
+ });
472
+ }
473
+ const retry = readNumberProperty(objectLiteral, "retry", diagnostics, absolutePath, rootDir);
474
+ const retryDelay = readNumberOrStringProperty(
475
+ objectLiteral,
476
+ "retryDelay",
477
+ diagnostics,
478
+ absolutePath,
479
+ rootDir
480
+ );
481
+ const deadLetter = readStringProperty(objectLiteral, "deadLetter", diagnostics, absolutePath, rootDir);
482
+ const visibilityTimeout = readNumberOrStringProperty(
483
+ objectLiteral,
484
+ "visibilityTimeout",
485
+ diagnostics,
486
+ absolutePath,
487
+ rootDir
488
+ );
489
+ const batchObject = getObjectLiteralProperty(objectLiteral, "batch");
490
+ const batchMaxSize = batchObject ? readNumberProperty(batchObject, "maxSize", diagnostics, absolutePath, rootDir) : void 0;
491
+ const batchTimeout = batchObject ? readNumberOrStringProperty(batchObject, "timeout", diagnostics, absolutePath, rootDir) : void 0;
492
+ const maxConcurrency = batchObject ? readNumberProperty(batchObject, "maxConcurrency", diagnostics, absolutePath, rootDir) : void 0;
493
+ let isMultiJob = false;
494
+ if (!hasProcess && !hasProcessBatch) {
495
+ for (const property of objectLiteral.getProperties()) {
496
+ if (!Node.isPropertyAssignment(property)) {
497
+ continue;
498
+ }
499
+ const propertyName = property.getName();
500
+ if (RESERVED_KEYS.has(propertyName)) {
501
+ continue;
502
+ }
503
+ const initializer = property.getInitializer();
504
+ if (!initializer || initializer.getKind() !== SyntaxKind.ObjectLiteralExpression) {
505
+ continue;
506
+ }
507
+ const jobObject = initializer;
508
+ if (hasProperty(jobObject, "message") && hasProperty(jobObject, "process")) {
509
+ isMultiJob = true;
510
+ break;
511
+ }
512
+ }
513
+ }
514
+ return {
515
+ retry,
516
+ retryDelay,
517
+ deadLetter,
518
+ batchMaxSize,
519
+ batchTimeout,
520
+ maxConcurrency,
521
+ visibilityTimeout,
522
+ hasProcess,
523
+ hasProcessBatch,
524
+ isMultiJob
525
+ };
526
+ }
527
+ function hasProperty(objectLiteral, name) {
528
+ return objectLiteral.getProperties().some((property) => {
529
+ return Node.isPropertyAssignment(property) && property.getName() === name;
530
+ });
531
+ }
532
+ function getObjectLiteralProperty(objectLiteral, name) {
533
+ for (const property of objectLiteral.getProperties()) {
534
+ if (!Node.isPropertyAssignment(property) || property.getName() !== name) {
535
+ continue;
536
+ }
537
+ const initializer = property.getInitializer();
538
+ if (initializer && initializer.getKind() === SyntaxKind.ObjectLiteralExpression) {
539
+ return initializer;
540
+ }
541
+ }
542
+ return void 0;
543
+ }
544
+ function readNumberProperty(objectLiteral, name, diagnostics, absolutePath, rootDir) {
545
+ const prop = objectLiteral.getProperties().find((property) => {
546
+ return Node.isPropertyAssignment(property) && property.getName() === name;
547
+ });
548
+ if (!prop || !Node.isPropertyAssignment(prop)) {
549
+ return void 0;
550
+ }
551
+ const initializer = prop.getInitializer();
552
+ if (!initializer) {
553
+ return void 0;
554
+ }
555
+ if (initializer.getKind() !== SyntaxKind.NumericLiteral) {
556
+ diagnostics.push({
557
+ level: "warning",
558
+ code: "NON_STATIC_CONFIG",
559
+ message: `Config key ${name} in ${path3.relative(rootDir, absolutePath)} is not a static number literal.`,
560
+ filePath: path3.relative(rootDir, absolutePath)
561
+ });
562
+ return void 0;
563
+ }
564
+ return Number(initializer.getText());
565
+ }
566
+ function readStringProperty(objectLiteral, name, diagnostics, absolutePath, rootDir) {
567
+ const prop = objectLiteral.getProperties().find((property) => {
568
+ return Node.isPropertyAssignment(property) && property.getName() === name;
569
+ });
570
+ if (!prop || !Node.isPropertyAssignment(prop)) {
571
+ return void 0;
572
+ }
573
+ const initializer = prop.getInitializer();
574
+ if (!initializer) {
575
+ return void 0;
576
+ }
577
+ if (initializer.getKind() !== SyntaxKind.StringLiteral) {
578
+ diagnostics.push({
579
+ level: "warning",
580
+ code: "NON_STATIC_CONFIG",
581
+ message: `Config key ${name} in ${path3.relative(rootDir, absolutePath)} is not a static string literal.`,
582
+ filePath: path3.relative(rootDir, absolutePath)
583
+ });
584
+ return void 0;
585
+ }
586
+ return initializer.getText().slice(1, -1);
587
+ }
588
+ function readNumberOrStringProperty(objectLiteral, name, diagnostics, absolutePath, rootDir) {
589
+ const prop = objectLiteral.getProperties().find((property) => {
590
+ return Node.isPropertyAssignment(property) && property.getName() === name;
591
+ });
592
+ if (!prop || !Node.isPropertyAssignment(prop)) {
593
+ return void 0;
594
+ }
595
+ const initializer = prop.getInitializer();
596
+ if (!initializer) {
597
+ return void 0;
598
+ }
599
+ if (initializer.getKind() === SyntaxKind.NumericLiteral) {
600
+ return Number(initializer.getText());
601
+ }
602
+ if (initializer.getKind() === SyntaxKind.StringLiteral) {
603
+ return initializer.getText().slice(1, -1);
604
+ }
605
+ diagnostics.push({
606
+ level: "warning",
607
+ code: "NON_STATIC_CONFIG",
608
+ message: `Config key ${name} in ${path3.relative(rootDir, absolutePath)} is not static.`,
609
+ filePath: path3.relative(rootDir, absolutePath)
610
+ });
611
+ return void 0;
612
+ }
613
+ function addConflictDiagnostics(queues, diagnostics) {
614
+ const queueNameMap = /* @__PURE__ */ new Map();
615
+ const bindingNameMap = /* @__PURE__ */ new Map();
616
+ for (const queue of queues) {
617
+ queueNameMap.set(queue.queueName, [...queueNameMap.get(queue.queueName) ?? [], queue.filePath]);
618
+ bindingNameMap.set(
619
+ queue.bindingName,
620
+ [...bindingNameMap.get(queue.bindingName) ?? [], queue.filePath]
621
+ );
622
+ }
623
+ for (const [queueName, files] of queueNameMap.entries()) {
624
+ if (files.length < 2) {
625
+ continue;
626
+ }
627
+ diagnostics.push({
628
+ level: "error",
629
+ code: "QUEUE_NAME_CONFLICT",
630
+ message: `Queue name conflict for ${queueName}: ${files.join(", ")}`
631
+ });
632
+ }
633
+ for (const [bindingName, files] of bindingNameMap.entries()) {
634
+ if (files.length < 2) {
635
+ continue;
636
+ }
637
+ diagnostics.push({
638
+ level: "error",
639
+ code: "BINDING_NAME_CONFLICT",
640
+ message: `Binding name conflict for ${bindingName}: ${files.join(", ")}`
641
+ });
642
+ }
643
+ }
644
+ function toErrorMessage(value) {
645
+ if (value instanceof Error) {
646
+ return value.message;
647
+ }
648
+ return String(value);
649
+ }
650
+ function patchJsoncConfig(filePath, discovery) {
651
+ let text = fs7.readFileSync(filePath, "utf8");
652
+ text = applyEdits(
653
+ text,
654
+ modify(text, ["main"], ".better-cf/entry.ts", {
655
+ formattingOptions: { insertSpaces: true, tabSize: 2 }
656
+ })
657
+ );
658
+ const producers = discovery.queues.map((queue) => ({
659
+ queue: queue.queueName,
660
+ binding: queue.bindingName
661
+ }));
662
+ const consumers = discovery.queues.map((queue) => ({
663
+ queue: queue.queueName,
664
+ ...queue.config.batchMaxSize !== void 0 ? { max_batch_size: queue.config.batchMaxSize } : {},
665
+ ...queue.config.batchTimeout !== void 0 ? { max_batch_timeout: parseDurationSeconds(queue.config.batchTimeout) } : {},
666
+ ...queue.config.retry !== void 0 ? { max_retries: queue.config.retry } : {},
667
+ ...queue.config.deadLetter !== void 0 ? { dead_letter_queue: queue.config.deadLetter } : {},
668
+ ...queue.config.maxConcurrency !== void 0 ? { max_concurrency: queue.config.maxConcurrency } : {},
669
+ ...queue.config.retryDelay !== void 0 ? { retry_delay: parseDurationSeconds(queue.config.retryDelay) } : {}
670
+ }));
671
+ text = applyEdits(
672
+ text,
673
+ modify(text, ["queues"], { producers, consumers, better_cf_managed: true }, {
674
+ formattingOptions: { insertSpaces: true, tabSize: 2 }
675
+ })
676
+ );
677
+ fs7.writeFileSync(filePath, text, "utf8");
678
+ }
679
+ function ensureJsoncExists(rootDir) {
680
+ const filePath = path3.join(rootDir, "wrangler.jsonc");
681
+ if (!fs7.existsSync(filePath)) {
682
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
683
+ const content = `{
684
+ "$schema": "node_modules/wrangler/config-schema.json",
685
+ "name": "my-worker",
686
+ "main": ".better-cf/entry.ts",
687
+ "compatibility_date": "${date}",
688
+ "queues": {
689
+ "producers": [],
690
+ "consumers": []
691
+ }
692
+ }
693
+ `;
694
+ fs7.writeFileSync(filePath, content, "utf8");
695
+ } else {
696
+ const parsed = parse(fs7.readFileSync(filePath, "utf8"));
697
+ if (!parsed.queues) {
698
+ patchJsoncConfig(filePath, { queues: []});
699
+ }
700
+ }
701
+ return filePath;
702
+ }
703
+ function parseDurationSeconds(value) {
704
+ if (typeof value === "number") {
705
+ return value;
706
+ }
707
+ const match = value.match(/^(\d+)(s|m|h)$/);
708
+ if (!match) {
709
+ return Number(value) || 0;
710
+ }
711
+ const amount = Number.parseInt(match[1], 10);
712
+ const unit = match[2];
713
+ if (unit === "s") {
714
+ return amount;
715
+ }
716
+ if (unit === "m") {
717
+ return amount * 60;
718
+ }
719
+ return amount * 3600;
720
+ }
721
+ var START_MARKER = "# --- better-cf:start ---";
722
+ var END_MARKER = "# --- better-cf:end ---";
723
+ function patchTomlConfig(filePath, discovery) {
724
+ let content = fs7.readFileSync(filePath, "utf8");
725
+ content = ensureMainEntry(content);
726
+ const generatedSection = renderQueueSection(discovery);
727
+ const startIndex = content.indexOf(START_MARKER);
728
+ const endIndex = content.indexOf(END_MARKER);
729
+ if (startIndex >= 0 && endIndex > startIndex) {
730
+ const head = content.slice(0, startIndex + START_MARKER.length);
731
+ const tail = content.slice(endIndex);
732
+ content = `${head}
733
+ ${generatedSection}
734
+ ${tail}`;
735
+ } else {
736
+ content = `${content.trimEnd()}
737
+
738
+ ${START_MARKER}
739
+ ${generatedSection}
740
+ ${END_MARKER}
741
+ `;
742
+ }
743
+ fs7.writeFileSync(filePath, content, "utf8");
744
+ }
745
+ function ensureTomlExists(rootDir) {
746
+ const filePath = path3.join(rootDir, "wrangler.toml");
747
+ if (!fs7.existsSync(filePath)) {
748
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
749
+ const initial = `name = "my-worker"
750
+ main = ".better-cf/entry.ts"
751
+ compatibility_date = "${date}"
752
+
753
+ ${START_MARKER}
754
+ ${END_MARKER}
755
+ `;
756
+ fs7.writeFileSync(filePath, initial, "utf8");
757
+ }
758
+ return filePath;
759
+ }
760
+ function ensureMainEntry(content) {
761
+ if (/^main\s*=\s*".*"/m.test(content)) {
762
+ return content.replace(/^main\s*=\s*".*"/m, 'main = ".better-cf/entry.ts"');
763
+ }
764
+ return `main = ".better-cf/entry.ts"
765
+ ${content}`;
766
+ }
767
+ function renderQueueSection(discovery) {
768
+ const lines = [];
769
+ for (const queue of discovery.queues) {
770
+ lines.push("[[queues.producers]]");
771
+ lines.push(`queue = "${queue.queueName}"`);
772
+ lines.push(`binding = "${queue.bindingName}"`);
773
+ lines.push("");
774
+ lines.push("[[queues.consumers]]");
775
+ lines.push(`queue = "${queue.queueName}"`);
776
+ if (queue.config.batchMaxSize !== void 0) {
777
+ lines.push(`max_batch_size = ${queue.config.batchMaxSize}`);
778
+ }
779
+ if (queue.config.batchTimeout !== void 0) {
780
+ lines.push(`max_batch_timeout = ${parseDurationSeconds2(queue.config.batchTimeout)}`);
781
+ }
782
+ if (queue.config.retry !== void 0) {
783
+ lines.push(`max_retries = ${queue.config.retry}`);
784
+ }
785
+ if (queue.config.deadLetter !== void 0) {
786
+ lines.push(`dead_letter_queue = "${queue.config.deadLetter}"`);
787
+ }
788
+ if (queue.config.maxConcurrency !== void 0) {
789
+ lines.push(`max_concurrency = ${queue.config.maxConcurrency}`);
790
+ }
791
+ if (queue.config.retryDelay !== void 0) {
792
+ lines.push(`retry_delay = ${parseDurationSeconds2(queue.config.retryDelay)}`);
793
+ }
794
+ lines.push("");
795
+ }
796
+ return lines.join("\n").trimEnd();
797
+ }
798
+ function parseDurationSeconds2(value) {
799
+ if (typeof value === "number") {
800
+ return value;
801
+ }
802
+ const match = value.match(/^(\d+)(s|m|h)$/);
803
+ if (!match) {
804
+ return Number(value) || 0;
805
+ }
806
+ const amount = Number.parseInt(match[1], 10);
807
+ const unit = match[2];
808
+ if (unit === "s") {
809
+ return amount;
810
+ }
811
+ if (unit === "m") {
812
+ return amount * 60;
813
+ }
814
+ return amount * 3600;
815
+ }
816
+
817
+ // src/cli/wrangler/index.ts
818
+ function patchWranglerConfig(config, discovery) {
819
+ const existing = detectWranglerConfig(config.rootDir);
820
+ if (existing && (existing.endsWith(".jsonc") || existing.endsWith(".json"))) {
821
+ patchJsoncConfig(existing, discovery);
822
+ return existing;
823
+ }
824
+ if (existing && existing.endsWith(".toml")) {
825
+ patchTomlConfig(existing, discovery);
826
+ return existing;
827
+ }
828
+ const created = ensureTomlExists(config.rootDir);
829
+ patchTomlConfig(created, discovery);
830
+ return created;
831
+ }
832
+ function detectWranglerConfig(rootDir) {
833
+ const preferred = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
834
+ for (const fileName of preferred) {
835
+ const absolutePath = path3.join(rootDir, fileName);
836
+ if (fs7.existsSync(absolutePath)) {
837
+ return absolutePath;
838
+ }
839
+ }
840
+ if (fs7.existsSync(path3.join(rootDir, "package.json")) && fs7.existsSync(path3.join(rootDir, "src"))) {
841
+ return ensureJsoncExists(rootDir);
842
+ }
843
+ return void 0;
844
+ }
845
+
846
+ // src/cli/commands/generate.ts
847
+ async function runGenerate(rootDir = process.cwd()) {
848
+ const config = loadCliConfig(rootDir);
849
+ const discovery = await scanQueues(config);
850
+ for (const diagnostic of discovery.diagnostics) {
851
+ if (diagnostic.level === "error") {
852
+ logger.error(diagnostic.message);
853
+ } else {
854
+ logger.warn(diagnostic.message);
855
+ }
856
+ }
857
+ const hasErrors = discovery.diagnostics.some((diag) => diag.level === "error");
858
+ if (hasErrors) {
859
+ throw new Error("Queue discovery failed due to configuration errors.");
860
+ }
861
+ const generated = generateCode(discovery, config);
862
+ const wranglerConfigPath = patchWranglerConfig(config, discovery);
863
+ return {
864
+ discovery,
865
+ generatedEntryPath: generated.entryPath,
866
+ generatedTypesPath: generated.typesPath,
867
+ wranglerConfigPath
868
+ };
869
+ }
870
+ async function generateCommand(rootDir = process.cwd()) {
871
+ const result = await runGenerate(rootDir);
872
+ logger.success(`Generated ${result.discovery.queues.length} queue(s)`);
873
+ logger.item("entry", result.generatedEntryPath);
874
+ logger.item("types", result.generatedTypesPath);
875
+ logger.item("wrangler", result.wranglerConfigPath);
876
+ }
877
+
878
+ // src/cli/commands/deploy.ts
879
+ async function deployCommand(rootDir = process.cwd()) {
880
+ const result = await runGenerate(rootDir);
881
+ logger.section("Deploying with wrangler");
882
+ const code = await runCommand("npx", ["wrangler", "deploy"], rootDir, "inherit");
883
+ if (code !== 0) {
884
+ throw new Error(`wrangler deploy failed with code ${code}`);
885
+ }
886
+ logger.success("Deployment complete");
887
+ logger.section("Active queue bindings");
888
+ for (const queue of result.discovery.queues) {
889
+ logger.item(queue.queueName, queue.bindingName);
890
+ }
891
+ }
892
+ function createProjectWatcher(rootDir, options) {
893
+ const watcher = chokidar.watch(["**/*.ts", "**/*.tsx"], {
894
+ cwd: rootDir,
895
+ ignored: options.ignored.map((entry) => `${entry}/**`),
896
+ ignoreInitial: true
897
+ });
898
+ const handler = async (filePath) => {
899
+ if (!filePath.endsWith(".ts") && !filePath.endsWith(".tsx")) {
900
+ return;
901
+ }
902
+ await options.onRelevantChange(filePath);
903
+ };
904
+ watcher.on("add", handler);
905
+ watcher.on("change", handler);
906
+ watcher.on("unlink", handler);
907
+ return watcher;
908
+ }
909
+
910
+ // src/cli/commands/dev.ts
911
+ async function devCommand(options, rootDir = process.cwd()) {
912
+ if (options.remote) {
913
+ throw new Error("Cloudflare Queues do not support wrangler dev --remote. Use local mode only.");
914
+ }
915
+ let wranglerProcess = null;
916
+ let isRebuilding = false;
917
+ const buildAndRestart = async (reason) => {
918
+ if (isRebuilding) {
919
+ return;
920
+ }
921
+ isRebuilding = true;
922
+ try {
923
+ const result = await runGenerate(rootDir);
924
+ logger.success(`Regenerated project (${reason}) with ${result.discovery.queues.length} queue(s)`);
925
+ if (wranglerProcess) {
926
+ wranglerProcess.kill();
927
+ }
928
+ wranglerProcess = spawnCommand("npx", ["wrangler", "dev", "--port", options.port], rootDir);
929
+ wranglerProcess.once("error", (error) => {
930
+ logger.error(`Failed to start wrangler dev: ${error.message}`);
931
+ });
932
+ } finally {
933
+ isRebuilding = false;
934
+ }
935
+ };
936
+ await buildAndRestart("initial build");
937
+ if (options.watch) {
938
+ const watcher = createProjectWatcher(rootDir, {
939
+ ignored: ["node_modules", ".better-cf", "dist"],
940
+ onRelevantChange: async (filePath) => {
941
+ await buildAndRestart(`file changed: ${filePath}`);
942
+ }
943
+ });
944
+ process.on("SIGINT", async () => {
945
+ await watcher.close();
946
+ wranglerProcess?.kill();
947
+ process.exit(0);
948
+ });
949
+ } else {
950
+ process.on("SIGINT", () => {
951
+ wranglerProcess?.kill();
952
+ process.exit(0);
953
+ });
954
+ }
955
+ }
956
+ async function initCommand(rootDir = process.cwd()) {
957
+ const configPath = path3.join(rootDir, "better-cf.config.ts");
958
+ if (!fs7.existsSync(configPath)) {
959
+ fs7.writeFileSync(configPath, defaultConfigTemplate(), "utf8");
960
+ logger.success("Created better-cf.config.ts");
961
+ }
962
+ const workerPath = path3.join(rootDir, "worker.ts");
963
+ const srcWorkerPath = path3.join(rootDir, "src", "worker.ts");
964
+ if (!fs7.existsSync(workerPath) && !fs7.existsSync(srcWorkerPath)) {
965
+ fs7.writeFileSync(workerPath, defaultWorkerTemplate(), "utf8");
966
+ logger.success("Created worker.ts");
967
+ }
968
+ const outputDir = path3.join(rootDir, ".better-cf");
969
+ fs7.mkdirSync(outputDir, { recursive: true });
970
+ const gitignorePath = path3.join(rootDir, ".gitignore");
971
+ if (!fs7.existsSync(gitignorePath)) {
972
+ fs7.writeFileSync(gitignorePath, ".better-cf/\nnode_modules/\n", "utf8");
973
+ logger.success("Created .gitignore");
974
+ } else {
975
+ const existing = fs7.readFileSync(gitignorePath, "utf8");
976
+ if (!existing.includes(".better-cf/")) {
977
+ fs7.appendFileSync(gitignorePath, "\n.better-cf/\n", "utf8");
978
+ logger.success("Updated .gitignore");
979
+ }
980
+ }
981
+ const packageJsonPath = path3.join(rootDir, "package.json");
982
+ if (fs7.existsSync(packageJsonPath)) {
983
+ const packageJson = JSON.parse(fs7.readFileSync(packageJsonPath, "utf8"));
984
+ packageJson.scripts = packageJson.scripts ?? {};
985
+ packageJson.scripts.dev = packageJson.scripts.dev ?? "better-cf dev";
986
+ packageJson.scripts.deploy = packageJson.scripts.deploy ?? "better-cf deploy";
987
+ packageJson.scripts.generate = "better-cf generate";
988
+ fs7.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}
989
+ `, "utf8");
990
+ logger.success("Updated package.json scripts");
991
+ }
992
+ const wranglerTomlPath = path3.join(rootDir, "wrangler.toml");
993
+ const wranglerJsoncPath = path3.join(rootDir, "wrangler.jsonc");
994
+ const wranglerJsonPath = path3.join(rootDir, "wrangler.json");
995
+ if (!fs7.existsSync(wranglerTomlPath) && !fs7.existsSync(wranglerJsoncPath) && !fs7.existsSync(wranglerJsonPath)) {
996
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
997
+ fs7.writeFileSync(
998
+ wranglerTomlPath,
999
+ `name = "my-worker"
1000
+ main = ".better-cf/entry.ts"
1001
+ compatibility_date = "${date}"
1002
+
1003
+ # --- better-cf:start ---
1004
+ # --- better-cf:end ---
1005
+ `,
1006
+ "utf8"
1007
+ );
1008
+ logger.success("Created wrangler.toml");
1009
+ }
1010
+ logger.info("Next steps: create a queue export and run `better-cf dev`.");
1011
+ }
1012
+ function defaultConfigTemplate() {
1013
+ return `import { createSDK } from 'better-cf/queue';
1014
+
1015
+ export type Env = {
1016
+ // DB: D1Database;
1017
+ };
1018
+
1019
+ export const { defineQueue, defineWorker } = createSDK<Env>();
1020
+
1021
+ export const betterCfConfig = {
1022
+ // workerEntry: 'worker.ts',
1023
+ // ignore: ['coverage'],
1024
+ legacyServiceWorker: false,
1025
+ };
1026
+ `;
1027
+ }
1028
+ function defaultWorkerTemplate() {
1029
+ return `import { defineWorker } from './better-cf.config';
1030
+
1031
+ export default defineWorker({
1032
+ async fetch() {
1033
+ return new Response('better-cf ready');
1034
+ },
1035
+ });
1036
+ `;
1037
+ }
1038
+
1039
+ // src/cli/index.ts
1040
+ async function run(argv = process.argv.slice(2)) {
1041
+ const program = new Command();
1042
+ program.name("better-cf").description("better-cf queue SDK CLI").version("0.1.0");
1043
+ program.command("init").description("Initialize better-cf in the current project").action(async () => {
1044
+ await initCommand();
1045
+ });
1046
+ program.command("generate").description("Scan queues and regenerate .better-cf files").action(async () => {
1047
+ await generateCommand();
1048
+ });
1049
+ program.command("dev").description("Run local development with queue codegen and wrangler dev").option("-p, --port <port>", "Port to pass to wrangler dev", "8787").option("--no-watch", "Disable file watcher").option("--remote", "Pass through remote mode (blocked for queues)").action(async (options) => {
1050
+ await devCommand(options);
1051
+ });
1052
+ program.command("deploy").description("Generate and deploy via wrangler deploy").action(async () => {
1053
+ await deployCommand();
1054
+ });
1055
+ try {
1056
+ await program.parseAsync(argv, { from: "user" });
1057
+ } catch (error) {
1058
+ logger.error(error instanceof Error ? error.message : String(error));
1059
+ process.exitCode = 1;
1060
+ }
1061
+ }
1062
+
1063
+ export { run };
1064
+ //# sourceMappingURL=index.js.map
1065
+ //# sourceMappingURL=index.js.map