better-cf 0.1.0 → 0.2.2
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.
- package/README.md +87 -113
- package/dist/cli/index.js +1057 -129
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +55 -9
- package/dist/index.js.map +1 -1
- package/dist/queue/index.d.ts +11 -14
- package/dist/queue/index.js +55 -9
- package/dist/queue/index.js.map +1 -1
- package/dist/queue/internal.d.ts +19 -1
- package/dist/queue/internal.js.map +1 -1
- package/dist/testing/index.d.ts +25 -3
- package/dist/testing/index.js +3 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/types-D44i92Zf.d.ts +309 -0
- package/package.json +21 -11
- package/dist/types-zED9gDCd.d.ts +0 -118
- package/docs/api/queue.md +0 -24
- package/docs/api/testing.md +0 -17
- package/docs/getting-started.md +0 -40
- package/docs/guides/hono.md +0 -18
- package/docs/guides/legacy-cloudflare.md +0 -15
- package/docs/limitations.md +0 -16
- package/docs/reference/errors.md +0 -21
- package/docs/reference/wrangler-mapping.md +0 -18
package/dist/cli/index.js
CHANGED
|
@@ -1,13 +1,54 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
|
-
import
|
|
4
|
+
import fs8 from 'fs';
|
|
5
5
|
import path3 from 'path';
|
|
6
6
|
import { Project, SyntaxKind, Node } from 'ts-morph';
|
|
7
7
|
import { applyEdits, modify, parse } from 'jsonc-parser';
|
|
8
8
|
import chokidar from 'chokidar';
|
|
9
|
+
import { createInterface } from 'readline/promises';
|
|
10
|
+
import { stdin, stdout } from 'process';
|
|
9
11
|
|
|
10
12
|
// src/cli/index.ts
|
|
13
|
+
|
|
14
|
+
// src/cli/errors.ts
|
|
15
|
+
var CliError = class extends Error {
|
|
16
|
+
code;
|
|
17
|
+
summary;
|
|
18
|
+
file;
|
|
19
|
+
details;
|
|
20
|
+
hint;
|
|
21
|
+
docsUrl;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
super(options.summary);
|
|
24
|
+
this.name = "CliError";
|
|
25
|
+
this.code = options.code;
|
|
26
|
+
this.summary = options.summary;
|
|
27
|
+
this.file = options.file;
|
|
28
|
+
this.details = options.details;
|
|
29
|
+
this.hint = options.hint;
|
|
30
|
+
this.docsUrl = options.docsUrl;
|
|
31
|
+
if (options.cause !== void 0) {
|
|
32
|
+
this.cause = options.cause;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
function toCliError(error) {
|
|
37
|
+
if (error instanceof CliError) {
|
|
38
|
+
return error;
|
|
39
|
+
}
|
|
40
|
+
if (error instanceof Error) {
|
|
41
|
+
return new CliError({
|
|
42
|
+
code: "UNEXPECTED_ERROR",
|
|
43
|
+
summary: error.message,
|
|
44
|
+
details: error.stack
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return new CliError({
|
|
48
|
+
code: "UNEXPECTED_ERROR",
|
|
49
|
+
summary: String(error)
|
|
50
|
+
});
|
|
51
|
+
}
|
|
11
52
|
function printLine(message) {
|
|
12
53
|
process.stdout.write(`${message}
|
|
13
54
|
`);
|
|
@@ -38,6 +79,33 @@ var logger = {
|
|
|
38
79
|
},
|
|
39
80
|
item(label, value) {
|
|
40
81
|
printLine(` -> ${pc.bold(label)}${value ? `: ${value}` : ""}`);
|
|
82
|
+
},
|
|
83
|
+
diagnostic(diag) {
|
|
84
|
+
const levelBadge = diag.level === "error" ? pc.red("[error]") : pc.yellow("[warn]");
|
|
85
|
+
printLine(`${levelBadge} ${pc.bold(diag.code)} ${diag.message}`);
|
|
86
|
+
if (diag.filePath) {
|
|
87
|
+
printLine(` file: ${diag.filePath}`);
|
|
88
|
+
}
|
|
89
|
+
if (diag.hint) {
|
|
90
|
+
printLine(` hint: ${diag.hint}`);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
cliError(payload) {
|
|
94
|
+
printErrLine(pc.red(pc.bold(`
|
|
95
|
+
ERROR ${payload.code}`)));
|
|
96
|
+
printErrLine(pc.red(payload.summary));
|
|
97
|
+
if (payload.file) {
|
|
98
|
+
printErrLine(` file: ${payload.file}`);
|
|
99
|
+
}
|
|
100
|
+
if (payload.details) {
|
|
101
|
+
printErrLine(` details: ${payload.details}`);
|
|
102
|
+
}
|
|
103
|
+
if (payload.hint) {
|
|
104
|
+
printErrLine(` hint: ${payload.hint}`);
|
|
105
|
+
}
|
|
106
|
+
if (payload.docsUrl) {
|
|
107
|
+
printErrLine(` docs: ${payload.docsUrl}`);
|
|
108
|
+
}
|
|
41
109
|
}
|
|
42
110
|
};
|
|
43
111
|
function runCommand(command, args, cwd, stdio = "inherit") {
|
|
@@ -51,6 +119,31 @@ function runCommand(command, args, cwd, stdio = "inherit") {
|
|
|
51
119
|
child.once("close", (code) => resolve(code ?? 0));
|
|
52
120
|
});
|
|
53
121
|
}
|
|
122
|
+
function runCommandCapture(command, args, cwd) {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
const child = spawn(command, args, {
|
|
125
|
+
cwd,
|
|
126
|
+
stdio: "pipe",
|
|
127
|
+
env: process.env
|
|
128
|
+
});
|
|
129
|
+
let stdout = "";
|
|
130
|
+
let stderr = "";
|
|
131
|
+
child.stdout?.on("data", (chunk) => {
|
|
132
|
+
stdout += chunk.toString();
|
|
133
|
+
});
|
|
134
|
+
child.stderr?.on("data", (chunk) => {
|
|
135
|
+
stderr += chunk.toString();
|
|
136
|
+
});
|
|
137
|
+
child.once("error", (error) => reject(error));
|
|
138
|
+
child.once("close", (code) => {
|
|
139
|
+
resolve({
|
|
140
|
+
code: code ?? 0,
|
|
141
|
+
stdout,
|
|
142
|
+
stderr
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
54
147
|
function spawnCommand(command, args, cwd) {
|
|
55
148
|
return spawn(command, args, {
|
|
56
149
|
cwd,
|
|
@@ -58,16 +151,241 @@ function spawnCommand(command, args, cwd) {
|
|
|
58
151
|
env: process.env
|
|
59
152
|
});
|
|
60
153
|
}
|
|
154
|
+
|
|
155
|
+
// src/cli/commands/admin.ts
|
|
156
|
+
async function runWranglerQueueSubcommand(args, rootDir = process.cwd(), summary = "Wrangler queue command failed.") {
|
|
157
|
+
logger.section(`wrangler ${args.join(" ")}`);
|
|
158
|
+
const code = await runCommand("npx", ["wrangler", ...args], rootDir, "inherit");
|
|
159
|
+
if (code !== 0) {
|
|
160
|
+
throw new CliError({
|
|
161
|
+
code: "WRANGLER_QUEUE_COMMAND_FAILED",
|
|
162
|
+
summary,
|
|
163
|
+
details: `Command "wrangler ${args.join(" ")}" exited with code ${code}.`,
|
|
164
|
+
hint: "Verify wrangler auth/project configuration and command arguments.",
|
|
165
|
+
docsUrl: "https://developers.cloudflare.com/queues/reference/wrangler-commands/"
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function queueListCommand(rootDir = process.cwd()) {
|
|
170
|
+
await runWranglerQueueSubcommand(["queues", "list"], rootDir, "Failed to list queues.");
|
|
171
|
+
}
|
|
172
|
+
async function queueCreateCommand(name, options = {}, rootDir = process.cwd()) {
|
|
173
|
+
assertWranglerToken("queue name", name);
|
|
174
|
+
const args = ["queues", "create", name];
|
|
175
|
+
pushNumberOption(args, "--delivery-delay-secs", options.deliveryDelaySecs);
|
|
176
|
+
pushNumberOption(args, "--message-retention-period-secs", options.messageRetentionPeriodSecs);
|
|
177
|
+
await runWranglerQueueSubcommand(
|
|
178
|
+
args,
|
|
179
|
+
rootDir,
|
|
180
|
+
`Failed to create queue "${name}".`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
async function queueUpdateCommand(name, options = {}, rootDir = process.cwd()) {
|
|
184
|
+
assertWranglerToken("queue name", name);
|
|
185
|
+
const args = ["queues", "update", name];
|
|
186
|
+
pushNumberOption(args, "--delivery-delay-secs", options.deliveryDelaySecs);
|
|
187
|
+
pushNumberOption(args, "--message-retention-period-secs", options.messageRetentionPeriodSecs);
|
|
188
|
+
await runWranglerQueueSubcommand(
|
|
189
|
+
args,
|
|
190
|
+
rootDir,
|
|
191
|
+
`Failed to update queue "${name}".`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
async function queueDeleteCommand(name, rootDir = process.cwd()) {
|
|
195
|
+
assertWranglerToken("queue name", name);
|
|
196
|
+
await runWranglerQueueSubcommand(
|
|
197
|
+
["queues", "delete", name],
|
|
198
|
+
rootDir,
|
|
199
|
+
`Failed to delete queue "${name}".`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
async function queueInfoCommand(name, rootDir = process.cwd()) {
|
|
203
|
+
assertWranglerToken("queue name", name);
|
|
204
|
+
await runWranglerQueueSubcommand(["queues", "info", name], rootDir, `Failed to get queue "${name}".`);
|
|
205
|
+
}
|
|
206
|
+
async function queuePauseCommand(name, rootDir = process.cwd()) {
|
|
207
|
+
assertWranglerToken("queue name", name);
|
|
208
|
+
await runWranglerQueueSubcommand(
|
|
209
|
+
["queues", "pause-delivery", name],
|
|
210
|
+
rootDir,
|
|
211
|
+
`Failed to pause queue "${name}".`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
async function queueResumeCommand(name, rootDir = process.cwd()) {
|
|
215
|
+
assertWranglerToken("queue name", name);
|
|
216
|
+
await runWranglerQueueSubcommand(
|
|
217
|
+
["queues", "resume-delivery", name],
|
|
218
|
+
rootDir,
|
|
219
|
+
`Failed to resume queue "${name}".`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
async function queuePurgeCommand(name, rootDir = process.cwd()) {
|
|
223
|
+
assertWranglerToken("queue name", name);
|
|
224
|
+
await runWranglerQueueSubcommand(
|
|
225
|
+
["queues", "purge", name],
|
|
226
|
+
rootDir,
|
|
227
|
+
`Failed to purge queue "${name}".`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
async function queueConsumerHttpAddCommand(queue, options = {}, rootDir = process.cwd()) {
|
|
231
|
+
assertWranglerToken("queue name", queue);
|
|
232
|
+
const args = ["queues", "consumer", "http", "add", queue];
|
|
233
|
+
pushNumberOption(args, "--batch-size", options.batchSize);
|
|
234
|
+
pushNumberOption(args, "--message-retries", options.messageRetries);
|
|
235
|
+
pushStringOption(args, "--dead-letter-queue", options.deadLetterQueue);
|
|
236
|
+
pushNumberOption(args, "--visibility-timeout-secs", options.visibilityTimeoutSecs);
|
|
237
|
+
pushNumberOption(args, "--retry-delay-secs", options.retryDelaySecs);
|
|
238
|
+
await runWranglerQueueSubcommand(
|
|
239
|
+
args,
|
|
240
|
+
rootDir,
|
|
241
|
+
`Failed to add HTTP consumer for queue "${queue}".`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
async function queueConsumerHttpRemoveCommand(queue, rootDir = process.cwd()) {
|
|
245
|
+
assertWranglerToken("queue name", queue);
|
|
246
|
+
await runWranglerQueueSubcommand(
|
|
247
|
+
["queues", "consumer", "http", "remove", queue],
|
|
248
|
+
rootDir,
|
|
249
|
+
`Failed to remove HTTP consumer for queue "${queue}".`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
async function queueConsumerWorkerAddCommand(queue, script, options = {}, rootDir = process.cwd()) {
|
|
253
|
+
assertWranglerToken("queue name", queue);
|
|
254
|
+
assertWranglerToken("worker script name", script);
|
|
255
|
+
const args = ["queues", "consumer", "worker", "add", queue, script];
|
|
256
|
+
pushNumberOption(args, "--batch-size", options.batchSize);
|
|
257
|
+
pushNumberOption(args, "--batch-timeout", options.batchTimeout);
|
|
258
|
+
pushNumberOption(args, "--message-retries", options.messageRetries);
|
|
259
|
+
pushStringOption(args, "--dead-letter-queue", options.deadLetterQueue);
|
|
260
|
+
pushNumberOption(args, "--max-concurrency", options.maxConcurrency);
|
|
261
|
+
pushNumberOption(args, "--retry-delay-secs", options.retryDelaySecs);
|
|
262
|
+
await runWranglerQueueSubcommand(
|
|
263
|
+
args,
|
|
264
|
+
rootDir,
|
|
265
|
+
`Failed to add worker consumer for queue "${queue}".`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
async function queueConsumerWorkerRemoveCommand(queue, script, rootDir = process.cwd()) {
|
|
269
|
+
assertWranglerToken("queue name", queue);
|
|
270
|
+
assertWranglerToken("worker script name", script);
|
|
271
|
+
await runWranglerQueueSubcommand(
|
|
272
|
+
["queues", "consumer", "worker", "remove", queue, script],
|
|
273
|
+
rootDir,
|
|
274
|
+
`Failed to remove worker consumer for queue "${queue}".`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
async function subscriptionListCommand(queue, options = {}, rootDir = process.cwd()) {
|
|
278
|
+
assertWranglerToken("queue name", queue);
|
|
279
|
+
const args = ["queues", "subscription", "list", queue];
|
|
280
|
+
pushNumberOption(args, "--page", options.page);
|
|
281
|
+
pushNumberOption(args, "--per-page", options.perPage);
|
|
282
|
+
pushBooleanFlag(args, "--json", options.json);
|
|
283
|
+
await runWranglerQueueSubcommand(
|
|
284
|
+
args,
|
|
285
|
+
rootDir,
|
|
286
|
+
"Failed to list queue subscriptions."
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
async function subscriptionCreateCommand(queue, options, rootDir = process.cwd()) {
|
|
290
|
+
assertWranglerToken("queue name", queue);
|
|
291
|
+
assertWranglerToken("subscription source", options.source);
|
|
292
|
+
const args = ["queues", "subscription", "create", queue, "--source", options.source, "--events", options.events];
|
|
293
|
+
pushStringOption(args, "--name", options.name);
|
|
294
|
+
pushBooleanOption(args, "--enabled", options.enabled);
|
|
295
|
+
pushStringOption(args, "--model-name", options.modelName);
|
|
296
|
+
pushStringOption(args, "--worker-name", options.workerName);
|
|
297
|
+
pushStringOption(args, "--workflow-name", options.workflowName);
|
|
298
|
+
await runWranglerQueueSubcommand(
|
|
299
|
+
args,
|
|
300
|
+
rootDir,
|
|
301
|
+
"Failed to create queue subscription."
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
async function subscriptionGetCommand(queue, id, options = {}, rootDir = process.cwd()) {
|
|
305
|
+
assertWranglerToken("queue name", queue);
|
|
306
|
+
assertWranglerToken("subscription id", id);
|
|
307
|
+
const args = ["queues", "subscription", "get", queue, "--id", id];
|
|
308
|
+
pushBooleanFlag(args, "--json", options.json);
|
|
309
|
+
await runWranglerQueueSubcommand(
|
|
310
|
+
args,
|
|
311
|
+
rootDir,
|
|
312
|
+
`Failed to get queue subscription "${id}" from "${queue}".`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
async function subscriptionUpdateCommand(queue, id, options, rootDir = process.cwd()) {
|
|
316
|
+
assertWranglerToken("queue name", queue);
|
|
317
|
+
assertWranglerToken("subscription id", id);
|
|
318
|
+
const args = ["queues", "subscription", "update", queue, "--id", id];
|
|
319
|
+
pushStringOption(args, "--name", options.name);
|
|
320
|
+
pushStringOption(args, "--events", options.events);
|
|
321
|
+
pushBooleanOption(args, "--enabled", options.enabled);
|
|
322
|
+
pushBooleanFlag(args, "--json", options.json);
|
|
323
|
+
await runWranglerQueueSubcommand(
|
|
324
|
+
args,
|
|
325
|
+
rootDir,
|
|
326
|
+
`Failed to update queue subscription "${id}" on "${queue}".`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
async function subscriptionDeleteCommand(queue, id, options = {}, rootDir = process.cwd()) {
|
|
330
|
+
assertWranglerToken("queue name", queue);
|
|
331
|
+
assertWranglerToken("subscription id", id);
|
|
332
|
+
const args = ["queues", "subscription", "delete", queue, "--id", id];
|
|
333
|
+
pushBooleanFlag(args, "--force", options.force);
|
|
334
|
+
await runWranglerQueueSubcommand(
|
|
335
|
+
args,
|
|
336
|
+
rootDir,
|
|
337
|
+
`Failed to delete queue subscription "${id}" from "${queue}".`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
function pushStringOption(target, flag, value) {
|
|
341
|
+
if (value && value.length > 0) {
|
|
342
|
+
target.push(flag, value);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function pushNumberOption(target, flag, value) {
|
|
346
|
+
if (value !== void 0) {
|
|
347
|
+
target.push(flag, String(value));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function pushBooleanOption(target, flag, value) {
|
|
351
|
+
if (value !== void 0) {
|
|
352
|
+
target.push(flag, String(value));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function pushBooleanFlag(target, flag, value) {
|
|
356
|
+
if (value) {
|
|
357
|
+
target.push(flag);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function assertWranglerToken(label, value) {
|
|
361
|
+
if (!value || value.trim().length === 0) {
|
|
362
|
+
throw new CliError({
|
|
363
|
+
code: "INVALID_WRANGLER_ARGUMENT",
|
|
364
|
+
summary: `Invalid ${label}.`,
|
|
365
|
+
details: "Value cannot be empty.",
|
|
366
|
+
hint: `Provide a non-empty ${label}.`
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (value.startsWith("-")) {
|
|
370
|
+
throw new CliError({
|
|
371
|
+
code: "INVALID_WRANGLER_ARGUMENT",
|
|
372
|
+
summary: `Invalid ${label}.`,
|
|
373
|
+
details: `Value "${value}" starts with "-" and may be interpreted as a CLI flag.`,
|
|
374
|
+
hint: `Use a ${label} that does not start with "-".`
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
61
378
|
var DEFAULT_IGNORE = ["node_modules", ".better-cf", "dist", ".wrangler"];
|
|
62
379
|
function loadCliConfig(rootDir = process.cwd()) {
|
|
63
380
|
const defaults = {
|
|
64
381
|
rootDir,
|
|
65
382
|
ignore: [...DEFAULT_IGNORE],
|
|
66
383
|
workerEntry: void 0,
|
|
67
|
-
legacyServiceWorker: false
|
|
384
|
+
legacyServiceWorker: false,
|
|
385
|
+
inferEnvTypes: true
|
|
68
386
|
};
|
|
69
387
|
const configPath = path3.join(rootDir, "better-cf.config.ts");
|
|
70
|
-
if (!
|
|
388
|
+
if (!fs8.existsSync(configPath)) {
|
|
71
389
|
return defaults;
|
|
72
390
|
}
|
|
73
391
|
const project = new Project({
|
|
@@ -89,11 +407,13 @@ function loadCliConfig(rootDir = process.cwd()) {
|
|
|
89
407
|
const configObject = initializer;
|
|
90
408
|
const workerEntry = readString(configObject, "workerEntry");
|
|
91
409
|
const legacyServiceWorker = readBoolean(configObject, "legacyServiceWorker");
|
|
410
|
+
const inferEnvTypes = readBoolean(configObject, "inferEnvTypes");
|
|
92
411
|
const ignore = readStringArray(configObject, "ignore");
|
|
93
412
|
return {
|
|
94
413
|
rootDir,
|
|
95
414
|
workerEntry,
|
|
96
415
|
legacyServiceWorker: legacyServiceWorker ?? defaults.legacyServiceWorker,
|
|
416
|
+
inferEnvTypes: inferEnvTypes ?? defaults.inferEnvTypes,
|
|
97
417
|
ignore: ignore ? [.../* @__PURE__ */ new Set([...defaults.ignore, ...ignore])] : defaults.ignore
|
|
98
418
|
};
|
|
99
419
|
}
|
|
@@ -153,7 +473,7 @@ function resolveWorkerEntry(config) {
|
|
|
153
473
|
].filter((value) => Boolean(value));
|
|
154
474
|
for (const candidate of candidates) {
|
|
155
475
|
const absolutePath = path3.isAbsolute(candidate) ? candidate : path3.join(config.rootDir, candidate);
|
|
156
|
-
if (
|
|
476
|
+
if (fs8.existsSync(absolutePath)) {
|
|
157
477
|
return absolutePath;
|
|
158
478
|
}
|
|
159
479
|
}
|
|
@@ -165,14 +485,14 @@ function resolveWorkerEntry(config) {
|
|
|
165
485
|
// src/cli/codegen.ts
|
|
166
486
|
function generateCode(discovery, config) {
|
|
167
487
|
const outputDir = path3.join(config.rootDir, ".better-cf");
|
|
168
|
-
|
|
488
|
+
fs8.mkdirSync(outputDir, { recursive: true });
|
|
169
489
|
const workerEntryAbsolute = resolveWorkerEntry(config);
|
|
170
490
|
const entryContents = renderEntryFile(discovery, workerEntryAbsolute, outputDir, config);
|
|
171
491
|
const typesContents = renderTypesFile(discovery);
|
|
172
492
|
const entryPath = path3.join(outputDir, "entry.ts");
|
|
173
493
|
const typesPath = path3.join(outputDir, "types.d.ts");
|
|
174
|
-
|
|
175
|
-
|
|
494
|
+
fs8.writeFileSync(entryPath, entryContents, "utf8");
|
|
495
|
+
fs8.writeFileSync(typesPath, typesContents, "utf8");
|
|
176
496
|
return {
|
|
177
497
|
entryPath,
|
|
178
498
|
typesPath
|
|
@@ -182,14 +502,18 @@ function renderEntryFile(discovery, workerEntryAbsolute, outDir, config) {
|
|
|
182
502
|
const imports = [];
|
|
183
503
|
const bindings = [];
|
|
184
504
|
const queueMap = [];
|
|
185
|
-
imports.push(
|
|
505
|
+
imports.push(
|
|
506
|
+
`import workerDefault, * as workerModule from ${JSON.stringify(toImportPath(outDir, workerEntryAbsolute))};`
|
|
507
|
+
);
|
|
186
508
|
imports.push(`import { getQueueInternals, resolveWorkerHandlers } from 'better-cf/queue/internal';`);
|
|
187
509
|
for (const queue of discovery.queues) {
|
|
188
510
|
const queueImportPath = toImportPath(outDir, queue.absoluteFilePath);
|
|
189
511
|
if (queue.isDefaultExport) {
|
|
190
|
-
imports.push(`import ${queue.importName} from
|
|
512
|
+
imports.push(`import ${queue.importName} from ${JSON.stringify(queueImportPath)};`);
|
|
191
513
|
} else {
|
|
192
|
-
imports.push(
|
|
514
|
+
imports.push(
|
|
515
|
+
`import { ${queue.exportName} as ${queue.importName} } from ${JSON.stringify(queueImportPath)};`
|
|
516
|
+
);
|
|
193
517
|
}
|
|
194
518
|
bindings.push(`getQueueInternals(${queue.importName}).setBinding('${queue.bindingName}');`);
|
|
195
519
|
queueMap.push(` '${queue.queueName}': ${queue.importName}`);
|
|
@@ -246,6 +570,8 @@ declare module 'better-cf/queue' {
|
|
|
246
570
|
interface BetterCfGeneratedBindings {
|
|
247
571
|
${lines.join("\n")}
|
|
248
572
|
}
|
|
573
|
+
|
|
574
|
+
interface BetterCfAutoEnv extends BetterCfGeneratedBindings {}
|
|
249
575
|
}
|
|
250
576
|
|
|
251
577
|
export {};
|
|
@@ -260,10 +586,10 @@ function toImportPath(fromDir, targetFile) {
|
|
|
260
586
|
}
|
|
261
587
|
|
|
262
588
|
// src/cli/discovery/naming.ts
|
|
263
|
-
function deriveQueueName(
|
|
264
|
-
const withoutSuffix =
|
|
589
|
+
function deriveQueueName(input2) {
|
|
590
|
+
const withoutSuffix = input2.replace(/Queue$/, "");
|
|
265
591
|
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 ||
|
|
592
|
+
return kebab || input2.toLowerCase();
|
|
267
593
|
}
|
|
268
594
|
function deriveBindingName(queueName) {
|
|
269
595
|
return `QUEUE_${queueName.replace(/[^a-zA-Z0-9]/g, "_").replace(/_+/g, "_").toUpperCase()}`;
|
|
@@ -280,8 +606,10 @@ var RESERVED_KEYS = /* @__PURE__ */ new Set([
|
|
|
280
606
|
"retry",
|
|
281
607
|
"retryDelay",
|
|
282
608
|
"deadLetter",
|
|
609
|
+
"deliveryDelay",
|
|
283
610
|
"visibilityTimeout",
|
|
284
611
|
"batch",
|
|
612
|
+
"consumer",
|
|
285
613
|
"message",
|
|
286
614
|
"process",
|
|
287
615
|
"processBatch",
|
|
@@ -370,7 +698,7 @@ async function scanQueues(config) {
|
|
|
370
698
|
}
|
|
371
699
|
function createProject(rootDir) {
|
|
372
700
|
const tsConfigPath = path3.join(rootDir, "tsconfig.json");
|
|
373
|
-
if (
|
|
701
|
+
if (fs8.existsSync(tsConfigPath)) {
|
|
374
702
|
return new Project({
|
|
375
703
|
tsConfigFilePath: tsConfigPath,
|
|
376
704
|
skipAddingFilesFromTsConfig: true
|
|
@@ -389,7 +717,7 @@ function collectSourceFiles(rootDir, ignore) {
|
|
|
389
717
|
const files = [];
|
|
390
718
|
const ignoreSet = new Set(ignore);
|
|
391
719
|
function walk(currentPath) {
|
|
392
|
-
const entries =
|
|
720
|
+
const entries = fs8.readdirSync(currentPath, { withFileTypes: true });
|
|
393
721
|
for (const entry of entries) {
|
|
394
722
|
if (entry.name.startsWith(".git")) {
|
|
395
723
|
continue;
|
|
@@ -403,7 +731,7 @@ function collectSourceFiles(rootDir, ignore) {
|
|
|
403
731
|
continue;
|
|
404
732
|
}
|
|
405
733
|
if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
|
|
406
|
-
const content =
|
|
734
|
+
const content = fs8.readFileSync(absolutePath, "utf8");
|
|
407
735
|
if (content.includes("defineQueue")) {
|
|
408
736
|
files.push(absolutePath);
|
|
409
737
|
}
|
|
@@ -467,7 +795,8 @@ function extractQueueConfig(call, absolutePath, diagnostics, rootDir) {
|
|
|
467
795
|
level: "error",
|
|
468
796
|
code: "INVALID_PROCESS_MODE",
|
|
469
797
|
message: "Queue config cannot include both process and processBatch.",
|
|
470
|
-
filePath: path3.relative(rootDir, absolutePath)
|
|
798
|
+
filePath: path3.relative(rootDir, absolutePath),
|
|
799
|
+
hint: "Pick exactly one processing mode for worker consumers."
|
|
471
800
|
});
|
|
472
801
|
}
|
|
473
802
|
const retry = readNumberProperty(objectLiteral, "retry", diagnostics, absolutePath, rootDir);
|
|
@@ -479,13 +808,23 @@ function extractQueueConfig(call, absolutePath, diagnostics, rootDir) {
|
|
|
479
808
|
rootDir
|
|
480
809
|
);
|
|
481
810
|
const deadLetter = readStringProperty(objectLiteral, "deadLetter", diagnostics, absolutePath, rootDir);
|
|
482
|
-
const
|
|
811
|
+
const deliveryDelay = readNumberOrStringProperty(
|
|
812
|
+
objectLiteral,
|
|
813
|
+
"deliveryDelay",
|
|
814
|
+
diagnostics,
|
|
815
|
+
absolutePath,
|
|
816
|
+
rootDir
|
|
817
|
+
);
|
|
818
|
+
const topLevelVisibilityTimeout = readNumberOrStringProperty(
|
|
483
819
|
objectLiteral,
|
|
484
820
|
"visibilityTimeout",
|
|
485
821
|
diagnostics,
|
|
486
822
|
absolutePath,
|
|
487
823
|
rootDir
|
|
488
824
|
);
|
|
825
|
+
const consumer = readConsumerConfig(objectLiteral, diagnostics, absolutePath, rootDir);
|
|
826
|
+
const consumerType = consumer?.type;
|
|
827
|
+
const visibilityTimeout = consumer?.visibilityTimeout ?? topLevelVisibilityTimeout;
|
|
489
828
|
const batchObject = getObjectLiteralProperty(objectLiteral, "batch");
|
|
490
829
|
const batchMaxSize = batchObject ? readNumberProperty(batchObject, "maxSize", diagnostics, absolutePath, rootDir) : void 0;
|
|
491
830
|
const batchTimeout = batchObject ? readNumberOrStringProperty(batchObject, "timeout", diagnostics, absolutePath, rootDir) : void 0;
|
|
@@ -511,14 +850,35 @@ function extractQueueConfig(call, absolutePath, diagnostics, rootDir) {
|
|
|
511
850
|
}
|
|
512
851
|
}
|
|
513
852
|
}
|
|
853
|
+
const relativeFile = path3.relative(rootDir, absolutePath);
|
|
854
|
+
if (consumerType === "http_pull" && (hasProcess || hasProcessBatch)) {
|
|
855
|
+
diagnostics.push({
|
|
856
|
+
level: "error",
|
|
857
|
+
code: "INVALID_PULL_MODE_HANDLER",
|
|
858
|
+
message: 'Queue with consumer.type="http_pull" cannot include process/processBatch.',
|
|
859
|
+
filePath: relativeFile,
|
|
860
|
+
hint: "Remove process handlers for pull consumers and consume via HTTP pull APIs."
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
if (consumerType === "http_pull" && isMultiJob) {
|
|
864
|
+
diagnostics.push({
|
|
865
|
+
level: "error",
|
|
866
|
+
code: "UNSUPPORTED_PULL_MULTIJOB",
|
|
867
|
+
message: 'Multi-job queue mode is not supported when consumer.type="http_pull".',
|
|
868
|
+
filePath: relativeFile,
|
|
869
|
+
hint: "Split jobs into separate queues when using http_pull."
|
|
870
|
+
});
|
|
871
|
+
}
|
|
514
872
|
return {
|
|
515
873
|
retry,
|
|
516
874
|
retryDelay,
|
|
517
875
|
deadLetter,
|
|
876
|
+
deliveryDelay,
|
|
518
877
|
batchMaxSize,
|
|
519
878
|
batchTimeout,
|
|
520
879
|
maxConcurrency,
|
|
521
880
|
visibilityTimeout,
|
|
881
|
+
consumerType,
|
|
522
882
|
hasProcess,
|
|
523
883
|
hasProcessBatch,
|
|
524
884
|
isMultiJob
|
|
@@ -610,6 +970,36 @@ function readNumberOrStringProperty(objectLiteral, name, diagnostics, absolutePa
|
|
|
610
970
|
});
|
|
611
971
|
return void 0;
|
|
612
972
|
}
|
|
973
|
+
function readConsumerConfig(objectLiteral, diagnostics, absolutePath, rootDir) {
|
|
974
|
+
const consumerObject = getObjectLiteralProperty(objectLiteral, "consumer");
|
|
975
|
+
if (!consumerObject) {
|
|
976
|
+
return void 0;
|
|
977
|
+
}
|
|
978
|
+
const typeValue = readStringProperty(consumerObject, "type", diagnostics, absolutePath, rootDir);
|
|
979
|
+
if (!typeValue) {
|
|
980
|
+
return void 0;
|
|
981
|
+
}
|
|
982
|
+
if (typeValue !== "worker" && typeValue !== "http_pull") {
|
|
983
|
+
diagnostics.push({
|
|
984
|
+
level: "warning",
|
|
985
|
+
code: "NON_STATIC_CONFIG",
|
|
986
|
+
message: `Unknown consumer.type "${typeValue}" in ${path3.relative(rootDir, absolutePath)}.`,
|
|
987
|
+
filePath: path3.relative(rootDir, absolutePath)
|
|
988
|
+
});
|
|
989
|
+
return void 0;
|
|
990
|
+
}
|
|
991
|
+
const visibilityTimeout = readNumberOrStringProperty(
|
|
992
|
+
consumerObject,
|
|
993
|
+
"visibilityTimeout",
|
|
994
|
+
diagnostics,
|
|
995
|
+
absolutePath,
|
|
996
|
+
rootDir
|
|
997
|
+
);
|
|
998
|
+
return {
|
|
999
|
+
type: typeValue,
|
|
1000
|
+
visibilityTimeout
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
613
1003
|
function addConflictDiagnostics(queues, diagnostics) {
|
|
614
1004
|
const queueNameMap = /* @__PURE__ */ new Map();
|
|
615
1005
|
const bindingNameMap = /* @__PURE__ */ new Map();
|
|
@@ -647,8 +1037,105 @@ function toErrorMessage(value) {
|
|
|
647
1037
|
}
|
|
648
1038
|
return String(value);
|
|
649
1039
|
}
|
|
1040
|
+
var WRANGLER_ENV_INTERFACE = "BetterCfWranglerEnv";
|
|
1041
|
+
async function generateEnvTypes(config) {
|
|
1042
|
+
const outDir = path3.join(config.rootDir, ".better-cf");
|
|
1043
|
+
fs8.mkdirSync(outDir, { recursive: true });
|
|
1044
|
+
const wranglerEnvPath = path3.join(outDir, "wrangler-env.d.ts");
|
|
1045
|
+
const autoEnvPath = path3.join(outDir, "auto-env.d.ts");
|
|
1046
|
+
const skipWrangler = process.env.BETTER_CF_SKIP_WRANGLER_TYPES === "1" || config.inferEnvTypes === false;
|
|
1047
|
+
if (skipWrangler) {
|
|
1048
|
+
fs8.writeFileSync(
|
|
1049
|
+
wranglerEnvPath,
|
|
1050
|
+
`// Auto-generated fallback when wrangler types is skipped.
|
|
1051
|
+
interface ${WRANGLER_ENV_INTERFACE} {
|
|
1052
|
+
[key: string]: unknown;
|
|
1053
|
+
}
|
|
1054
|
+
`,
|
|
1055
|
+
"utf8"
|
|
1056
|
+
);
|
|
1057
|
+
} else {
|
|
1058
|
+
const command = await runCommandCapture(
|
|
1059
|
+
"npx",
|
|
1060
|
+
["wrangler", "types", "--path", wranglerEnvPath, "--env-interface", WRANGLER_ENV_INTERFACE],
|
|
1061
|
+
config.rootDir
|
|
1062
|
+
);
|
|
1063
|
+
if (command.code !== 0) {
|
|
1064
|
+
throw new CliError({
|
|
1065
|
+
code: "WRANGLER_TYPES_FAILED",
|
|
1066
|
+
summary: "Failed to infer env types with wrangler.",
|
|
1067
|
+
details: command.stderr || command.stdout || `Exit code ${command.code}`,
|
|
1068
|
+
hint: "Install wrangler >=3.91 and verify wrangler config is valid.",
|
|
1069
|
+
docsUrl: "https://developers.cloudflare.com/workers/wrangler/commands/#types"
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
if (!fs8.existsSync(wranglerEnvPath)) {
|
|
1073
|
+
throw new CliError({
|
|
1074
|
+
code: "WRANGLER_TYPES_OUTPUT_MISSING",
|
|
1075
|
+
summary: "Wrangler types command succeeded but output file was not created.",
|
|
1076
|
+
file: wranglerEnvPath,
|
|
1077
|
+
hint: "Check wrangler CLI version and project permissions."
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
fs8.writeFileSync(
|
|
1082
|
+
autoEnvPath,
|
|
1083
|
+
`// Auto-generated by better-cf. Do not edit.
|
|
1084
|
+
/// <reference path="./wrangler-env.d.ts" />
|
|
1085
|
+
|
|
1086
|
+
declare module 'better-cf/queue' {
|
|
1087
|
+
interface BetterCfAutoEnv extends BetterCfGeneratedBindings, ${WRANGLER_ENV_INTERFACE} {}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
export {};
|
|
1091
|
+
`,
|
|
1092
|
+
"utf8"
|
|
1093
|
+
);
|
|
1094
|
+
return {
|
|
1095
|
+
wranglerEnvPath,
|
|
1096
|
+
autoEnvPath
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// src/cli/wrangler/duration.ts
|
|
1101
|
+
function parseDurationSecondsStrict(value, context) {
|
|
1102
|
+
if (typeof value === "number") {
|
|
1103
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1104
|
+
throw new CliError({
|
|
1105
|
+
code: "INVALID_DURATION",
|
|
1106
|
+
summary: `Invalid duration for ${context}.`,
|
|
1107
|
+
details: `Expected non-negative finite number, received ${String(value)}.`,
|
|
1108
|
+
hint: 'Use values like 30 or "30s", "5m", "1h".'
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
return value;
|
|
1112
|
+
}
|
|
1113
|
+
const match = value.match(/^(\d+)(s|m|h)$/);
|
|
1114
|
+
if (!match) {
|
|
1115
|
+
throw new CliError({
|
|
1116
|
+
code: "INVALID_DURATION",
|
|
1117
|
+
summary: `Invalid duration string for ${context}.`,
|
|
1118
|
+
details: `Received "${value}".`,
|
|
1119
|
+
hint: 'Use formats like "30s", "5m", "1h".'
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
const amount = Number.parseInt(match[1], 10);
|
|
1123
|
+
const unit = match[2];
|
|
1124
|
+
if (unit === "s") {
|
|
1125
|
+
return amount;
|
|
1126
|
+
}
|
|
1127
|
+
if (unit === "m") {
|
|
1128
|
+
return amount * 60;
|
|
1129
|
+
}
|
|
1130
|
+
return amount * 3600;
|
|
1131
|
+
}
|
|
1132
|
+
function parseDurationMsStrict(value, context) {
|
|
1133
|
+
return parseDurationSecondsStrict(value, context) * 1e3;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// src/cli/wrangler/jsonc.ts
|
|
650
1137
|
function patchJsoncConfig(filePath, discovery) {
|
|
651
|
-
let text =
|
|
1138
|
+
let text = fs8.readFileSync(filePath, "utf8");
|
|
652
1139
|
text = applyEdits(
|
|
653
1140
|
text,
|
|
654
1141
|
modify(text, ["main"], ".better-cf/entry.ts", {
|
|
@@ -657,28 +1144,46 @@ function patchJsoncConfig(filePath, discovery) {
|
|
|
657
1144
|
);
|
|
658
1145
|
const producers = discovery.queues.map((queue) => ({
|
|
659
1146
|
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) } : {}
|
|
1147
|
+
binding: queue.bindingName,
|
|
1148
|
+
...queue.config.deliveryDelay !== void 0 ? { delivery_delay: parseDurationSecondsStrict(queue.config.deliveryDelay, `${queue.queueName}.deliveryDelay`) } : {}
|
|
670
1149
|
}));
|
|
1150
|
+
const consumers = discovery.queues.map((queue) => {
|
|
1151
|
+
const base = {
|
|
1152
|
+
queue: queue.queueName,
|
|
1153
|
+
...queue.config.retry !== void 0 ? { max_retries: queue.config.retry } : {},
|
|
1154
|
+
...queue.config.deadLetter !== void 0 ? { dead_letter_queue: queue.config.deadLetter } : {},
|
|
1155
|
+
...queue.config.retryDelay !== void 0 ? { retry_delay: parseDurationSecondsStrict(queue.config.retryDelay, `${queue.queueName}.retryDelay`) } : {}
|
|
1156
|
+
};
|
|
1157
|
+
if (queue.config.consumerType === "http_pull") {
|
|
1158
|
+
return {
|
|
1159
|
+
...base,
|
|
1160
|
+
type: "http_pull",
|
|
1161
|
+
...queue.config.visibilityTimeout !== void 0 ? {
|
|
1162
|
+
visibility_timeout_ms: parseDurationMsStrict(
|
|
1163
|
+
queue.config.visibilityTimeout,
|
|
1164
|
+
`${queue.queueName}.visibilityTimeout`
|
|
1165
|
+
)
|
|
1166
|
+
} : {}
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
return {
|
|
1170
|
+
...base,
|
|
1171
|
+
...queue.config.batchMaxSize !== void 0 ? { max_batch_size: queue.config.batchMaxSize } : {},
|
|
1172
|
+
...queue.config.batchTimeout !== void 0 ? { max_batch_timeout: parseDurationSecondsStrict(queue.config.batchTimeout, `${queue.queueName}.batch.timeout`) } : {},
|
|
1173
|
+
...queue.config.maxConcurrency !== void 0 ? { max_concurrency: queue.config.maxConcurrency } : {}
|
|
1174
|
+
};
|
|
1175
|
+
});
|
|
671
1176
|
text = applyEdits(
|
|
672
1177
|
text,
|
|
673
1178
|
modify(text, ["queues"], { producers, consumers, better_cf_managed: true }, {
|
|
674
1179
|
formattingOptions: { insertSpaces: true, tabSize: 2 }
|
|
675
1180
|
})
|
|
676
1181
|
);
|
|
677
|
-
|
|
1182
|
+
fs8.writeFileSync(filePath, text, "utf8");
|
|
678
1183
|
}
|
|
679
1184
|
function ensureJsoncExists(rootDir) {
|
|
680
1185
|
const filePath = path3.join(rootDir, "wrangler.jsonc");
|
|
681
|
-
if (!
|
|
1186
|
+
if (!fs8.existsSync(filePath)) {
|
|
682
1187
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
683
1188
|
const content = `{
|
|
684
1189
|
"$schema": "node_modules/wrangler/config-schema.json",
|
|
@@ -691,37 +1196,19 @@ function ensureJsoncExists(rootDir) {
|
|
|
691
1196
|
}
|
|
692
1197
|
}
|
|
693
1198
|
`;
|
|
694
|
-
|
|
1199
|
+
fs8.writeFileSync(filePath, content, "utf8");
|
|
695
1200
|
} else {
|
|
696
|
-
const parsed = parse(
|
|
1201
|
+
const parsed = parse(fs8.readFileSync(filePath, "utf8"));
|
|
697
1202
|
if (!parsed.queues) {
|
|
698
1203
|
patchJsoncConfig(filePath, { queues: []});
|
|
699
1204
|
}
|
|
700
1205
|
}
|
|
701
1206
|
return filePath;
|
|
702
1207
|
}
|
|
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
1208
|
var START_MARKER = "# --- better-cf:start ---";
|
|
722
1209
|
var END_MARKER = "# --- better-cf:end ---";
|
|
723
1210
|
function patchTomlConfig(filePath, discovery) {
|
|
724
|
-
let content =
|
|
1211
|
+
let content = fs8.readFileSync(filePath, "utf8");
|
|
725
1212
|
content = ensureMainEntry(content);
|
|
726
1213
|
const generatedSection = renderQueueSection(discovery);
|
|
727
1214
|
const startIndex = content.indexOf(START_MARKER);
|
|
@@ -740,11 +1227,11 @@ ${generatedSection}
|
|
|
740
1227
|
${END_MARKER}
|
|
741
1228
|
`;
|
|
742
1229
|
}
|
|
743
|
-
|
|
1230
|
+
fs8.writeFileSync(filePath, content, "utf8");
|
|
744
1231
|
}
|
|
745
1232
|
function ensureTomlExists(rootDir) {
|
|
746
1233
|
const filePath = path3.join(rootDir, "wrangler.toml");
|
|
747
|
-
if (!
|
|
1234
|
+
if (!fs8.existsSync(filePath)) {
|
|
748
1235
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
749
1236
|
const initial = `name = "my-worker"
|
|
750
1237
|
main = ".better-cf/entry.ts"
|
|
@@ -753,7 +1240,7 @@ compatibility_date = "${date}"
|
|
|
753
1240
|
${START_MARKER}
|
|
754
1241
|
${END_MARKER}
|
|
755
1242
|
`;
|
|
756
|
-
|
|
1243
|
+
fs8.writeFileSync(filePath, initial, "utf8");
|
|
757
1244
|
}
|
|
758
1245
|
return filePath;
|
|
759
1246
|
}
|
|
@@ -768,50 +1255,62 @@ function renderQueueSection(discovery) {
|
|
|
768
1255
|
const lines = [];
|
|
769
1256
|
for (const queue of discovery.queues) {
|
|
770
1257
|
lines.push("[[queues.producers]]");
|
|
771
|
-
lines.push(`queue =
|
|
772
|
-
lines.push(`binding =
|
|
1258
|
+
lines.push(`queue = ${toTomlString(queue.queueName)}`);
|
|
1259
|
+
lines.push(`binding = ${toTomlString(queue.bindingName)}`);
|
|
1260
|
+
if (queue.config.deliveryDelay !== void 0) {
|
|
1261
|
+
lines.push(
|
|
1262
|
+
`delivery_delay = ${parseDurationSecondsStrict(
|
|
1263
|
+
queue.config.deliveryDelay,
|
|
1264
|
+
`${queue.queueName}.deliveryDelay`
|
|
1265
|
+
)}`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
773
1268
|
lines.push("");
|
|
774
1269
|
lines.push("[[queues.consumers]]");
|
|
775
|
-
lines.push(`queue =
|
|
776
|
-
if (queue.config.
|
|
777
|
-
lines.push(`
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1270
|
+
lines.push(`queue = ${toTomlString(queue.queueName)}`);
|
|
1271
|
+
if (queue.config.consumerType === "http_pull") {
|
|
1272
|
+
lines.push(`type = ${toTomlString("http_pull")}`);
|
|
1273
|
+
if (queue.config.visibilityTimeout !== void 0) {
|
|
1274
|
+
lines.push(
|
|
1275
|
+
`visibility_timeout_ms = ${parseDurationMsStrict(
|
|
1276
|
+
queue.config.visibilityTimeout,
|
|
1277
|
+
`${queue.queueName}.visibilityTimeout`
|
|
1278
|
+
)}`
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
} else {
|
|
1282
|
+
if (queue.config.batchMaxSize !== void 0) {
|
|
1283
|
+
lines.push(`max_batch_size = ${queue.config.batchMaxSize}`);
|
|
1284
|
+
}
|
|
1285
|
+
if (queue.config.batchTimeout !== void 0) {
|
|
1286
|
+
lines.push(
|
|
1287
|
+
`max_batch_timeout = ${parseDurationSecondsStrict(
|
|
1288
|
+
queue.config.batchTimeout,
|
|
1289
|
+
`${queue.queueName}.batch.timeout`
|
|
1290
|
+
)}`
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
if (queue.config.maxConcurrency !== void 0) {
|
|
1294
|
+
lines.push(`max_concurrency = ${queue.config.maxConcurrency}`);
|
|
1295
|
+
}
|
|
781
1296
|
}
|
|
782
1297
|
if (queue.config.retry !== void 0) {
|
|
783
1298
|
lines.push(`max_retries = ${queue.config.retry}`);
|
|
784
1299
|
}
|
|
785
1300
|
if (queue.config.deadLetter !== void 0) {
|
|
786
|
-
lines.push(`dead_letter_queue =
|
|
787
|
-
}
|
|
788
|
-
if (queue.config.maxConcurrency !== void 0) {
|
|
789
|
-
lines.push(`max_concurrency = ${queue.config.maxConcurrency}`);
|
|
1301
|
+
lines.push(`dead_letter_queue = ${toTomlString(queue.config.deadLetter)}`);
|
|
790
1302
|
}
|
|
791
1303
|
if (queue.config.retryDelay !== void 0) {
|
|
792
|
-
lines.push(
|
|
1304
|
+
lines.push(
|
|
1305
|
+
`retry_delay = ${parseDurationSecondsStrict(queue.config.retryDelay, `${queue.queueName}.retryDelay`)}`
|
|
1306
|
+
);
|
|
793
1307
|
}
|
|
794
1308
|
lines.push("");
|
|
795
1309
|
}
|
|
796
1310
|
return lines.join("\n").trimEnd();
|
|
797
1311
|
}
|
|
798
|
-
function
|
|
799
|
-
|
|
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;
|
|
1312
|
+
function toTomlString(value) {
|
|
1313
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
|
|
815
1314
|
}
|
|
816
1315
|
|
|
817
1316
|
// src/cli/wrangler/index.ts
|
|
@@ -833,11 +1332,11 @@ function detectWranglerConfig(rootDir) {
|
|
|
833
1332
|
const preferred = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
834
1333
|
for (const fileName of preferred) {
|
|
835
1334
|
const absolutePath = path3.join(rootDir, fileName);
|
|
836
|
-
if (
|
|
1335
|
+
if (fs8.existsSync(absolutePath)) {
|
|
837
1336
|
return absolutePath;
|
|
838
1337
|
}
|
|
839
1338
|
}
|
|
840
|
-
if (
|
|
1339
|
+
if (fs8.existsSync(path3.join(rootDir, "package.json")) && fs8.existsSync(path3.join(rootDir, "src"))) {
|
|
841
1340
|
return ensureJsoncExists(rootDir);
|
|
842
1341
|
}
|
|
843
1342
|
return void 0;
|
|
@@ -848,23 +1347,26 @@ async function runGenerate(rootDir = process.cwd()) {
|
|
|
848
1347
|
const config = loadCliConfig(rootDir);
|
|
849
1348
|
const discovery = await scanQueues(config);
|
|
850
1349
|
for (const diagnostic of discovery.diagnostics) {
|
|
851
|
-
|
|
852
|
-
logger.error(diagnostic.message);
|
|
853
|
-
} else {
|
|
854
|
-
logger.warn(diagnostic.message);
|
|
855
|
-
}
|
|
1350
|
+
logger.diagnostic(diagnostic);
|
|
856
1351
|
}
|
|
857
1352
|
const hasErrors = discovery.diagnostics.some((diag) => diag.level === "error");
|
|
858
1353
|
if (hasErrors) {
|
|
859
|
-
throw new
|
|
1354
|
+
throw new CliError({
|
|
1355
|
+
code: "QUEUE_DISCOVERY_FAILED",
|
|
1356
|
+
summary: "Queue discovery failed due to configuration errors.",
|
|
1357
|
+
hint: "Fix the diagnostics above and re-run `better-cf generate`.",
|
|
1358
|
+
docsUrl: "https://github.com/better-cf/better-cf#errors"
|
|
1359
|
+
});
|
|
860
1360
|
}
|
|
861
1361
|
const generated = generateCode(discovery, config);
|
|
862
1362
|
const wranglerConfigPath = patchWranglerConfig(config, discovery);
|
|
1363
|
+
const envTypeResult = await generateEnvTypes(config);
|
|
863
1364
|
return {
|
|
864
1365
|
discovery,
|
|
865
1366
|
generatedEntryPath: generated.entryPath,
|
|
866
1367
|
generatedTypesPath: generated.typesPath,
|
|
867
|
-
wranglerConfigPath
|
|
1368
|
+
wranglerConfigPath,
|
|
1369
|
+
autoEnvPath: envTypeResult.autoEnvPath
|
|
868
1370
|
};
|
|
869
1371
|
}
|
|
870
1372
|
async function generateCommand(rootDir = process.cwd()) {
|
|
@@ -872,6 +1374,7 @@ async function generateCommand(rootDir = process.cwd()) {
|
|
|
872
1374
|
logger.success(`Generated ${result.discovery.queues.length} queue(s)`);
|
|
873
1375
|
logger.item("entry", result.generatedEntryPath);
|
|
874
1376
|
logger.item("types", result.generatedTypesPath);
|
|
1377
|
+
logger.item("auto-env", result.autoEnvPath);
|
|
875
1378
|
logger.item("wrangler", result.wranglerConfigPath);
|
|
876
1379
|
}
|
|
877
1380
|
|
|
@@ -881,7 +1384,12 @@ async function deployCommand(rootDir = process.cwd()) {
|
|
|
881
1384
|
logger.section("Deploying with wrangler");
|
|
882
1385
|
const code = await runCommand("npx", ["wrangler", "deploy"], rootDir, "inherit");
|
|
883
1386
|
if (code !== 0) {
|
|
884
|
-
throw new
|
|
1387
|
+
throw new CliError({
|
|
1388
|
+
code: "WRANGLER_DEPLOY_FAILED",
|
|
1389
|
+
summary: "wrangler deploy failed.",
|
|
1390
|
+
details: `wrangler exited with code ${code}`,
|
|
1391
|
+
hint: "Run wrangler deploy manually to inspect environment-specific failures."
|
|
1392
|
+
});
|
|
885
1393
|
}
|
|
886
1394
|
logger.success("Deployment complete");
|
|
887
1395
|
logger.section("Active queue bindings");
|
|
@@ -889,28 +1397,48 @@ async function deployCommand(rootDir = process.cwd()) {
|
|
|
889
1397
|
logger.item(queue.queueName, queue.bindingName);
|
|
890
1398
|
}
|
|
891
1399
|
}
|
|
1400
|
+
var WATCH_GLOBS = [
|
|
1401
|
+
"**/*.ts",
|
|
1402
|
+
"**/*.tsx",
|
|
1403
|
+
"better-cf.config.ts",
|
|
1404
|
+
"wrangler.toml",
|
|
1405
|
+
"wrangler.json",
|
|
1406
|
+
"wrangler.jsonc",
|
|
1407
|
+
"tsconfig.json"
|
|
1408
|
+
];
|
|
892
1409
|
function createProjectWatcher(rootDir, options) {
|
|
893
|
-
const watcher = chokidar.watch(
|
|
1410
|
+
const watcher = chokidar.watch(WATCH_GLOBS, {
|
|
894
1411
|
cwd: rootDir,
|
|
895
1412
|
ignored: options.ignored.map((entry) => `${entry}/**`),
|
|
896
|
-
ignoreInitial: true
|
|
1413
|
+
ignoreInitial: true,
|
|
1414
|
+
usePolling: true,
|
|
1415
|
+
interval: 100,
|
|
1416
|
+
awaitWriteFinish: {
|
|
1417
|
+
stabilityThreshold: 150,
|
|
1418
|
+
pollInterval: 25
|
|
1419
|
+
}
|
|
897
1420
|
});
|
|
898
1421
|
const handler = async (filePath) => {
|
|
899
|
-
if (!filePath.endsWith(".ts") && !filePath.endsWith(".tsx")) {
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
1422
|
await options.onRelevantChange(filePath);
|
|
903
1423
|
};
|
|
904
1424
|
watcher.on("add", handler);
|
|
905
1425
|
watcher.on("change", handler);
|
|
906
1426
|
watcher.on("unlink", handler);
|
|
1427
|
+
watcher.on("ready", () => {
|
|
1428
|
+
void options.onRelevantChange("__watcher_ready__");
|
|
1429
|
+
});
|
|
907
1430
|
return watcher;
|
|
908
1431
|
}
|
|
909
1432
|
|
|
910
1433
|
// src/cli/commands/dev.ts
|
|
911
1434
|
async function devCommand(options, rootDir = process.cwd()) {
|
|
912
1435
|
if (options.remote) {
|
|
913
|
-
throw new
|
|
1436
|
+
throw new CliError({
|
|
1437
|
+
code: "REMOTE_QUEUE_DEV_UNSUPPORTED",
|
|
1438
|
+
summary: "Cloudflare Queues do not support wrangler dev --remote.",
|
|
1439
|
+
hint: "Run better-cf dev without --remote for queue consumer development.",
|
|
1440
|
+
docsUrl: "https://developers.cloudflare.com/queues/configuration/local-development/"
|
|
1441
|
+
});
|
|
914
1442
|
}
|
|
915
1443
|
let wranglerProcess = null;
|
|
916
1444
|
let isRebuilding = false;
|
|
@@ -936,7 +1464,8 @@ async function devCommand(options, rootDir = process.cwd()) {
|
|
|
936
1464
|
await buildAndRestart("initial build");
|
|
937
1465
|
if (options.watch) {
|
|
938
1466
|
const watcher = createProjectWatcher(rootDir, {
|
|
939
|
-
|
|
1467
|
+
// Ignore Wrangler's local build output to avoid self-triggered rebuild loops.
|
|
1468
|
+
ignored: ["node_modules", ".better-cf", ".wrangler", "dist"],
|
|
940
1469
|
onRelevantChange: async (filePath) => {
|
|
941
1470
|
await buildAndRestart(`file changed: ${filePath}`);
|
|
942
1471
|
}
|
|
@@ -955,46 +1484,53 @@ async function devCommand(options, rootDir = process.cwd()) {
|
|
|
955
1484
|
}
|
|
956
1485
|
async function initCommand(rootDir = process.cwd()) {
|
|
957
1486
|
const configPath = path3.join(rootDir, "better-cf.config.ts");
|
|
958
|
-
if (!
|
|
959
|
-
|
|
1487
|
+
if (!fs8.existsSync(configPath)) {
|
|
1488
|
+
fs8.writeFileSync(configPath, defaultConfigTemplate(), "utf8");
|
|
960
1489
|
logger.success("Created better-cf.config.ts");
|
|
961
1490
|
}
|
|
962
1491
|
const workerPath = path3.join(rootDir, "worker.ts");
|
|
963
1492
|
const srcWorkerPath = path3.join(rootDir, "src", "worker.ts");
|
|
964
|
-
if (!
|
|
965
|
-
|
|
1493
|
+
if (!fs8.existsSync(workerPath) && !fs8.existsSync(srcWorkerPath)) {
|
|
1494
|
+
fs8.writeFileSync(workerPath, defaultWorkerTemplate(), "utf8");
|
|
966
1495
|
logger.success("Created worker.ts");
|
|
967
1496
|
}
|
|
968
1497
|
const outputDir = path3.join(rootDir, ".better-cf");
|
|
969
|
-
|
|
1498
|
+
fs8.mkdirSync(outputDir, { recursive: true });
|
|
970
1499
|
const gitignorePath = path3.join(rootDir, ".gitignore");
|
|
971
|
-
if (!
|
|
972
|
-
|
|
1500
|
+
if (!fs8.existsSync(gitignorePath)) {
|
|
1501
|
+
fs8.writeFileSync(gitignorePath, ".better-cf/\nnode_modules/\n", "utf8");
|
|
973
1502
|
logger.success("Created .gitignore");
|
|
974
1503
|
} else {
|
|
975
|
-
const existing =
|
|
1504
|
+
const existing = fs8.readFileSync(gitignorePath, "utf8");
|
|
976
1505
|
if (!existing.includes(".better-cf/")) {
|
|
977
|
-
|
|
1506
|
+
fs8.appendFileSync(gitignorePath, "\n.better-cf/\n", "utf8");
|
|
978
1507
|
logger.success("Updated .gitignore");
|
|
979
1508
|
}
|
|
980
1509
|
}
|
|
981
1510
|
const packageJsonPath = path3.join(rootDir, "package.json");
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1511
|
+
const packageJsonExists = fs8.existsSync(packageJsonPath);
|
|
1512
|
+
const packageJson = packageJsonExists ? JSON.parse(fs8.readFileSync(packageJsonPath, "utf8")) : {
|
|
1513
|
+
name: derivePackageName(rootDir),
|
|
1514
|
+
version: "0.0.0",
|
|
1515
|
+
private: true,
|
|
1516
|
+
scripts: {}
|
|
1517
|
+
};
|
|
1518
|
+
if (!packageJsonExists) {
|
|
1519
|
+
logger.success("Created package.json");
|
|
1520
|
+
}
|
|
1521
|
+
packageJson.scripts = packageJson.scripts ?? {};
|
|
1522
|
+
packageJson.scripts.dev = packageJson.scripts.dev ?? "better-cf dev";
|
|
1523
|
+
packageJson.scripts.deploy = packageJson.scripts.deploy ?? "better-cf deploy";
|
|
1524
|
+
packageJson.scripts.generate = "better-cf generate";
|
|
1525
|
+
fs8.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}
|
|
989
1526
|
`, "utf8");
|
|
990
|
-
|
|
991
|
-
}
|
|
1527
|
+
logger.success("Updated package.json scripts");
|
|
992
1528
|
const wranglerTomlPath = path3.join(rootDir, "wrangler.toml");
|
|
993
1529
|
const wranglerJsoncPath = path3.join(rootDir, "wrangler.jsonc");
|
|
994
1530
|
const wranglerJsonPath = path3.join(rootDir, "wrangler.json");
|
|
995
|
-
if (!
|
|
1531
|
+
if (!fs8.existsSync(wranglerTomlPath) && !fs8.existsSync(wranglerJsoncPath) && !fs8.existsSync(wranglerJsonPath)) {
|
|
996
1532
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
997
|
-
|
|
1533
|
+
fs8.writeFileSync(
|
|
998
1534
|
wranglerTomlPath,
|
|
999
1535
|
`name = "my-worker"
|
|
1000
1536
|
main = ".better-cf/entry.ts"
|
|
@@ -1009,19 +1545,23 @@ compatibility_date = "${date}"
|
|
|
1009
1545
|
}
|
|
1010
1546
|
logger.info("Next steps: create a queue export and run `better-cf dev`.");
|
|
1011
1547
|
}
|
|
1548
|
+
function derivePackageName(rootDir) {
|
|
1549
|
+
const raw = path3.basename(path3.resolve(rootDir)).toLowerCase().trim();
|
|
1550
|
+
const normalized = raw.replace(/[^a-z0-9._-]+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
1551
|
+
return normalized || "better-cf-app";
|
|
1552
|
+
}
|
|
1012
1553
|
function defaultConfigTemplate() {
|
|
1013
1554
|
return `import { createSDK } from 'better-cf/queue';
|
|
1014
1555
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
};
|
|
1018
|
-
|
|
1019
|
-
export const { defineQueue, defineWorker } = createSDK<Env>();
|
|
1556
|
+
// Auto-inferred env types are generated under .better-cf/*.d.ts
|
|
1557
|
+
// You can still switch to createSDK<Env>() when you need explicit overrides.
|
|
1558
|
+
export const { defineQueue, defineWorker } = createSDK();
|
|
1020
1559
|
|
|
1021
1560
|
export const betterCfConfig = {
|
|
1022
1561
|
// workerEntry: 'worker.ts',
|
|
1023
1562
|
// ignore: ['coverage'],
|
|
1024
1563
|
legacyServiceWorker: false,
|
|
1564
|
+
inferEnvTypes: true,
|
|
1025
1565
|
};
|
|
1026
1566
|
`;
|
|
1027
1567
|
}
|
|
@@ -1035,11 +1575,231 @@ export default defineWorker({
|
|
|
1035
1575
|
});
|
|
1036
1576
|
`;
|
|
1037
1577
|
}
|
|
1578
|
+
var DEFAULT_PROJECT_DIR = "better-cf-app";
|
|
1579
|
+
var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
1580
|
+
var INSTALL_PLANS = {
|
|
1581
|
+
npm: {
|
|
1582
|
+
command: "npm",
|
|
1583
|
+
runtimeArgs: ["install", "better-cf", "zod"],
|
|
1584
|
+
devArgs: ["install", "-D", "wrangler", "@cloudflare/workers-types", "typescript"],
|
|
1585
|
+
devRunCommand: "npm run dev"
|
|
1586
|
+
},
|
|
1587
|
+
pnpm: {
|
|
1588
|
+
command: "pnpm",
|
|
1589
|
+
runtimeArgs: ["add", "better-cf", "zod"],
|
|
1590
|
+
devArgs: ["add", "-D", "wrangler", "@cloudflare/workers-types", "typescript"],
|
|
1591
|
+
devRunCommand: "pnpm dev"
|
|
1592
|
+
},
|
|
1593
|
+
yarn: {
|
|
1594
|
+
command: "yarn",
|
|
1595
|
+
runtimeArgs: ["add", "better-cf", "zod"],
|
|
1596
|
+
devArgs: ["add", "-D", "wrangler", "@cloudflare/workers-types", "typescript"],
|
|
1597
|
+
devRunCommand: "yarn dev"
|
|
1598
|
+
},
|
|
1599
|
+
bun: {
|
|
1600
|
+
command: "bun",
|
|
1601
|
+
runtimeArgs: ["add", "better-cf", "zod"],
|
|
1602
|
+
devArgs: ["add", "-d", "wrangler", "@cloudflare/workers-types", "typescript"],
|
|
1603
|
+
devRunCommand: "bun run dev"
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
async function createCommand(projectDirectoryArg, options = {}, rootDir = process.cwd()) {
|
|
1607
|
+
const isYes = options.yes ?? false;
|
|
1608
|
+
const shouldInstallByDefault = options.install ?? true;
|
|
1609
|
+
const isInteractive = Boolean(stdin.isTTY && stdout.isTTY && !isYes);
|
|
1610
|
+
const prompts = isInteractive ? createPrompts() : void 0;
|
|
1611
|
+
try {
|
|
1612
|
+
const projectDirectory = await resolveProjectDirectory(projectDirectoryArg, isYes, prompts);
|
|
1613
|
+
const targetDir = path3.resolve(rootDir, projectDirectory);
|
|
1614
|
+
ensureTargetDirectory(targetDir, options.force ?? false);
|
|
1615
|
+
await initCommand(targetDir);
|
|
1616
|
+
const detectedPackageManager = detectPackageManager(rootDir);
|
|
1617
|
+
const packageManager = options.packageManager ?? (prompts ? await prompts.selectPackageManager(detectedPackageManager) : detectedPackageManager);
|
|
1618
|
+
const shouldInstall = shouldInstallByDefault && (prompts ? await prompts.confirmInstall(true) : true);
|
|
1619
|
+
if (shouldInstall) {
|
|
1620
|
+
await installDependencies(packageManager, targetDir);
|
|
1621
|
+
}
|
|
1622
|
+
const relativePath = path3.relative(rootDir, targetDir) || ".";
|
|
1623
|
+
const plan = INSTALL_PLANS[packageManager];
|
|
1624
|
+
logger.section("Project ready");
|
|
1625
|
+
logger.item("path", targetDir);
|
|
1626
|
+
if (relativePath !== ".") {
|
|
1627
|
+
logger.item("cd", `cd ${relativePath}`);
|
|
1628
|
+
}
|
|
1629
|
+
if (!shouldInstall) {
|
|
1630
|
+
logger.item("install", `${plan.command} ${plan.runtimeArgs.join(" ")}`);
|
|
1631
|
+
logger.item("install (dev)", `${plan.command} ${plan.devArgs.join(" ")}`);
|
|
1632
|
+
}
|
|
1633
|
+
logger.item("dev", plan.devRunCommand);
|
|
1634
|
+
} finally {
|
|
1635
|
+
prompts?.close();
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
function detectPackageManager(rootDir = process.cwd()) {
|
|
1639
|
+
if (fs8.existsSync(path3.join(rootDir, "pnpm-lock.yaml"))) {
|
|
1640
|
+
return "pnpm";
|
|
1641
|
+
}
|
|
1642
|
+
if (fs8.existsSync(path3.join(rootDir, "yarn.lock"))) {
|
|
1643
|
+
return "yarn";
|
|
1644
|
+
}
|
|
1645
|
+
if (fs8.existsSync(path3.join(rootDir, "bun.lock")) || fs8.existsSync(path3.join(rootDir, "bun.lockb"))) {
|
|
1646
|
+
return "bun";
|
|
1647
|
+
}
|
|
1648
|
+
const userAgent = process.env.npm_config_user_agent?.toLowerCase() ?? "";
|
|
1649
|
+
if (userAgent.startsWith("pnpm")) {
|
|
1650
|
+
return "pnpm";
|
|
1651
|
+
}
|
|
1652
|
+
if (userAgent.startsWith("yarn")) {
|
|
1653
|
+
return "yarn";
|
|
1654
|
+
}
|
|
1655
|
+
if (userAgent.startsWith("bun")) {
|
|
1656
|
+
return "bun";
|
|
1657
|
+
}
|
|
1658
|
+
if (userAgent.startsWith("npm")) {
|
|
1659
|
+
return "npm";
|
|
1660
|
+
}
|
|
1661
|
+
return "npm";
|
|
1662
|
+
}
|
|
1663
|
+
function ensureTargetDirectory(targetDir, force) {
|
|
1664
|
+
if (!fs8.existsSync(targetDir)) {
|
|
1665
|
+
fs8.mkdirSync(targetDir, { recursive: true });
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
const stats = fs8.statSync(targetDir);
|
|
1669
|
+
if (!stats.isDirectory()) {
|
|
1670
|
+
throw new CliError({
|
|
1671
|
+
code: "CREATE_TARGET_INVALID",
|
|
1672
|
+
summary: `Target path exists and is not a directory: ${targetDir}.`,
|
|
1673
|
+
hint: "Choose a different project directory."
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
const contents = fs8.readdirSync(targetDir).filter((entry) => entry !== ".DS_Store");
|
|
1677
|
+
if (contents.length > 0 && !force) {
|
|
1678
|
+
throw new CliError({
|
|
1679
|
+
code: "CREATE_TARGET_NOT_EMPTY",
|
|
1680
|
+
summary: `Target directory is not empty: ${targetDir}.`,
|
|
1681
|
+
hint: "Use --force to scaffold in a non-empty directory."
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
async function resolveProjectDirectory(projectDirectoryArg, isYes, prompts) {
|
|
1686
|
+
if (projectDirectoryArg && projectDirectoryArg.trim().length > 0) {
|
|
1687
|
+
return projectDirectoryArg.trim();
|
|
1688
|
+
}
|
|
1689
|
+
if (isYes) {
|
|
1690
|
+
return DEFAULT_PROJECT_DIR;
|
|
1691
|
+
}
|
|
1692
|
+
if (!prompts) {
|
|
1693
|
+
throw new CliError({
|
|
1694
|
+
code: "CREATE_TARGET_REQUIRED",
|
|
1695
|
+
summary: "Project directory is required in non-interactive mode.",
|
|
1696
|
+
hint: "Pass a directory name, for example: `better-cf create my-worker`."
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
return prompts.askProjectDirectory(DEFAULT_PROJECT_DIR);
|
|
1700
|
+
}
|
|
1701
|
+
async function installDependencies(packageManager, targetDir) {
|
|
1702
|
+
const plan = INSTALL_PLANS[packageManager];
|
|
1703
|
+
logger.section(`Installing dependencies with ${plan.command}`);
|
|
1704
|
+
await runInstallCommand(plan.command, plan.runtimeArgs, targetDir);
|
|
1705
|
+
await runInstallCommand(plan.command, plan.devArgs, targetDir);
|
|
1706
|
+
}
|
|
1707
|
+
async function runInstallCommand(command, args, cwd) {
|
|
1708
|
+
try {
|
|
1709
|
+
const code = await runCommand(command, args, cwd, "inherit");
|
|
1710
|
+
if (code !== 0) {
|
|
1711
|
+
throw new CliError({
|
|
1712
|
+
code: "DEPENDENCY_INSTALL_FAILED",
|
|
1713
|
+
summary: `Dependency install failed: ${command} ${args.join(" ")}`,
|
|
1714
|
+
details: `Command exited with code ${code}.`,
|
|
1715
|
+
hint: `Run this command manually in ${cwd}.`
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
} catch (error) {
|
|
1719
|
+
if (error instanceof CliError) {
|
|
1720
|
+
throw error;
|
|
1721
|
+
}
|
|
1722
|
+
throw new CliError({
|
|
1723
|
+
code: "DEPENDENCY_INSTALL_FAILED",
|
|
1724
|
+
summary: `Dependency install failed: ${command} ${args.join(" ")}`,
|
|
1725
|
+
details: error instanceof Error ? error.message : String(error),
|
|
1726
|
+
hint: `Install dependencies manually in ${cwd}.`
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
function createPrompts() {
|
|
1731
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
1732
|
+
return {
|
|
1733
|
+
async askProjectDirectory(defaultValue) {
|
|
1734
|
+
while (true) {
|
|
1735
|
+
const raw = await rl.question(`Project directory (${defaultValue}): `);
|
|
1736
|
+
const value = raw.trim() || defaultValue;
|
|
1737
|
+
if (value.length > 0) {
|
|
1738
|
+
return value;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
},
|
|
1742
|
+
async selectPackageManager(defaultValue) {
|
|
1743
|
+
const defaultIndex = PACKAGE_MANAGERS.indexOf(defaultValue);
|
|
1744
|
+
while (true) {
|
|
1745
|
+
stdout.write("\nSelect a package manager:\n");
|
|
1746
|
+
PACKAGE_MANAGERS.forEach((manager, index) => {
|
|
1747
|
+
stdout.write(` ${index + 1}) ${manager}${manager === defaultValue ? " (default)" : ""}
|
|
1748
|
+
`);
|
|
1749
|
+
});
|
|
1750
|
+
const raw = await rl.question(`Package manager [${defaultIndex + 1}]: `);
|
|
1751
|
+
const normalized = raw.trim().toLowerCase();
|
|
1752
|
+
if (!normalized) {
|
|
1753
|
+
return defaultValue;
|
|
1754
|
+
}
|
|
1755
|
+
if (PACKAGE_MANAGERS.includes(normalized)) {
|
|
1756
|
+
return normalized;
|
|
1757
|
+
}
|
|
1758
|
+
const asNumber = Number(normalized);
|
|
1759
|
+
if (Number.isInteger(asNumber) && asNumber >= 1 && asNumber <= PACKAGE_MANAGERS.length) {
|
|
1760
|
+
return PACKAGE_MANAGERS[asNumber - 1];
|
|
1761
|
+
}
|
|
1762
|
+
logger.warn(`Unsupported package manager input: ${raw}`);
|
|
1763
|
+
}
|
|
1764
|
+
},
|
|
1765
|
+
async confirmInstall(defaultValue) {
|
|
1766
|
+
const hint = defaultValue ? "Y/n" : "y/N";
|
|
1767
|
+
while (true) {
|
|
1768
|
+
const raw = await rl.question(`Install dependencies now? (${hint}): `);
|
|
1769
|
+
const normalized = raw.trim().toLowerCase();
|
|
1770
|
+
if (!normalized) {
|
|
1771
|
+
return defaultValue;
|
|
1772
|
+
}
|
|
1773
|
+
if (normalized === "y" || normalized === "yes") {
|
|
1774
|
+
return true;
|
|
1775
|
+
}
|
|
1776
|
+
if (normalized === "n" || normalized === "no") {
|
|
1777
|
+
return false;
|
|
1778
|
+
}
|
|
1779
|
+
logger.warn(`Unsupported confirmation input: ${raw}`);
|
|
1780
|
+
}
|
|
1781
|
+
},
|
|
1782
|
+
close() {
|
|
1783
|
+
rl.close();
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1038
1787
|
|
|
1039
1788
|
// src/cli/index.ts
|
|
1040
1789
|
async function run(argv = process.argv.slice(2)) {
|
|
1041
1790
|
const program = new Command();
|
|
1042
|
-
program.name("better-cf").description("better-cf queue SDK CLI").version("0.1
|
|
1791
|
+
program.name("better-cf").description("better-cf queue SDK CLI").version("0.2.1");
|
|
1792
|
+
program.command("create [project-directory]").description("Create a new better-cf project").option("-y, --yes", "Use defaults and skip prompts").option("--no-install", "Skip dependency installation").option("--force", "Allow creating in a non-empty directory").option("--use-npm", "Use npm to install dependencies").option("--use-pnpm", "Use pnpm to install dependencies").option("--use-yarn", "Use yarn to install dependencies").option("--use-bun", "Use bun to install dependencies").action(
|
|
1793
|
+
async (projectDirectory, options) => {
|
|
1794
|
+
const packageManager = resolveCreatePackageManager(options);
|
|
1795
|
+
await createCommand(projectDirectory, {
|
|
1796
|
+
yes: options.yes,
|
|
1797
|
+
install: options.install,
|
|
1798
|
+
force: options.force,
|
|
1799
|
+
packageManager
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
);
|
|
1043
1803
|
program.command("init").description("Initialize better-cf in the current project").action(async () => {
|
|
1044
1804
|
await initCommand();
|
|
1045
1805
|
});
|
|
@@ -1052,13 +1812,181 @@ async function run(argv = process.argv.slice(2)) {
|
|
|
1052
1812
|
program.command("deploy").description("Generate and deploy via wrangler deploy").action(async () => {
|
|
1053
1813
|
await deployCommand();
|
|
1054
1814
|
});
|
|
1815
|
+
program.command("queue:list").description("List queues").action(async () => {
|
|
1816
|
+
await queueListCommand();
|
|
1817
|
+
});
|
|
1818
|
+
program.command("queue:create").description("Create a queue").requiredOption("--name <name>", "Queue name").option("--delivery-delay-secs <seconds>", "Default delivery delay in seconds", parseNumberOption).option(
|
|
1819
|
+
"--message-retention-period-secs <seconds>",
|
|
1820
|
+
"Message retention period in seconds",
|
|
1821
|
+
parseNumberOption
|
|
1822
|
+
).action(
|
|
1823
|
+
async (options) => {
|
|
1824
|
+
await queueCreateCommand(options.name, {
|
|
1825
|
+
deliveryDelaySecs: options.deliveryDelaySecs,
|
|
1826
|
+
messageRetentionPeriodSecs: options.messageRetentionPeriodSecs
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
);
|
|
1830
|
+
program.command("queue:update").description("Update a queue").requiredOption("--name <name>", "Queue name").option("--delivery-delay-secs <seconds>", "Default delivery delay in seconds", parseNumberOption).option(
|
|
1831
|
+
"--message-retention-period-secs <seconds>",
|
|
1832
|
+
"Message retention period in seconds",
|
|
1833
|
+
parseNumberOption
|
|
1834
|
+
).action(
|
|
1835
|
+
async (options) => {
|
|
1836
|
+
await queueUpdateCommand(options.name, {
|
|
1837
|
+
deliveryDelaySecs: options.deliveryDelaySecs,
|
|
1838
|
+
messageRetentionPeriodSecs: options.messageRetentionPeriodSecs
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
);
|
|
1842
|
+
program.command("queue:delete").description("Delete a queue").requiredOption("--name <name>", "Queue name").action(async (options) => {
|
|
1843
|
+
await queueDeleteCommand(options.name);
|
|
1844
|
+
});
|
|
1845
|
+
program.command("queue:info").description("Describe a queue").requiredOption("--name <name>", "Queue name").action(async (options) => {
|
|
1846
|
+
await queueInfoCommand(options.name);
|
|
1847
|
+
});
|
|
1848
|
+
program.command("queue:pause").description("Pause queue delivery").requiredOption("--name <name>", "Queue name").action(async (options) => {
|
|
1849
|
+
await queuePauseCommand(options.name);
|
|
1850
|
+
});
|
|
1851
|
+
program.command("queue:resume").description("Resume queue delivery").requiredOption("--name <name>", "Queue name").action(async (options) => {
|
|
1852
|
+
await queueResumeCommand(options.name);
|
|
1853
|
+
});
|
|
1854
|
+
program.command("queue:purge").description("Purge queue messages").requiredOption("--name <name>", "Queue name").action(async (options) => {
|
|
1855
|
+
await queuePurgeCommand(options.name);
|
|
1856
|
+
});
|
|
1857
|
+
program.command("queue:consumer:http:add").description("Add HTTP pull consumer for a queue").requiredOption("--queue <queue>", "Queue name").option("--batch-size <size>", "Batch size", parseNumberOption).option("--message-retries <retries>", "Message retries", parseNumberOption).option("--dead-letter-queue <queue>", "Dead letter queue name").option("--visibility-timeout-secs <seconds>", "Visibility timeout for pull consumer", parseNumberOption).option("--retry-delay-secs <seconds>", "Retry delay in seconds", parseNumberOption).action(
|
|
1858
|
+
async (options) => {
|
|
1859
|
+
await queueConsumerHttpAddCommand(options.queue, {
|
|
1860
|
+
batchSize: options.batchSize,
|
|
1861
|
+
messageRetries: options.messageRetries,
|
|
1862
|
+
deadLetterQueue: options.deadLetterQueue,
|
|
1863
|
+
visibilityTimeoutSecs: options.visibilityTimeoutSecs,
|
|
1864
|
+
retryDelaySecs: options.retryDelaySecs
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
);
|
|
1868
|
+
program.command("queue:consumer:http:remove").description("Remove HTTP pull consumer for a queue").requiredOption("--queue <queue>", "Queue name").action(async (options) => {
|
|
1869
|
+
await queueConsumerHttpRemoveCommand(options.queue);
|
|
1870
|
+
});
|
|
1871
|
+
program.command("queue:consumer:worker:add").description("Add worker consumer for a queue").requiredOption("--queue <queue>", "Queue name").requiredOption("--script <script>", "Worker script name").option("--batch-size <size>", "Batch size", parseNumberOption).option("--batch-timeout <seconds>", "Batch timeout in seconds", parseNumberOption).option("--message-retries <retries>", "Message retries", parseNumberOption).option("--dead-letter-queue <queue>", "Dead letter queue name").option("--max-concurrency <count>", "Max consumer concurrency", parseNumberOption).option("--retry-delay-secs <seconds>", "Retry delay in seconds", parseNumberOption).action(
|
|
1872
|
+
async (options) => {
|
|
1873
|
+
await queueConsumerWorkerAddCommand(
|
|
1874
|
+
options.queue,
|
|
1875
|
+
options.script,
|
|
1876
|
+
{
|
|
1877
|
+
batchSize: options.batchSize,
|
|
1878
|
+
batchTimeout: options.batchTimeout,
|
|
1879
|
+
messageRetries: options.messageRetries,
|
|
1880
|
+
deadLetterQueue: options.deadLetterQueue,
|
|
1881
|
+
maxConcurrency: options.maxConcurrency,
|
|
1882
|
+
retryDelaySecs: options.retryDelaySecs
|
|
1883
|
+
}
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
);
|
|
1887
|
+
program.command("queue:consumer:worker:remove").description("Remove worker consumer for a queue").requiredOption("--queue <queue>", "Queue name").requiredOption("--script <script>", "Worker script name").action(async (options) => {
|
|
1888
|
+
await queueConsumerWorkerRemoveCommand(options.queue, options.script);
|
|
1889
|
+
});
|
|
1890
|
+
program.command("subscription:list").description("List queue subscriptions").requiredOption("--queue <queue>", "Queue name").option("--page <page>", "Page number", parseNumberOption).option("--per-page <count>", "Results per page", parseNumberOption).option("--json", "JSON output").action(async (options) => {
|
|
1891
|
+
await subscriptionListCommand(
|
|
1892
|
+
options.queue,
|
|
1893
|
+
{ page: options.page, perPage: options.perPage, json: options.json }
|
|
1894
|
+
);
|
|
1895
|
+
});
|
|
1896
|
+
program.command("subscription:create").description("Create queue subscription").requiredOption("--queue <queue>", "Queue name").requiredOption("--source <source>", "Source id (same as queue name in most setups)").requiredOption("--events <events>", 'Event list (for example "message.acked")').option("--name <name>", "Subscription name").option("--enabled <enabled>", "Subscription enabled state (true/false)", parseBooleanOption).option("--model-name <modelName>", "AI model name (for AI gateway subscriptions)").option("--worker-name <workerName>", "Worker destination name").option("--workflow-name <workflowName>", "Workflow destination name").action(
|
|
1897
|
+
async (options) => {
|
|
1898
|
+
await subscriptionCreateCommand(options.queue, {
|
|
1899
|
+
source: options.source,
|
|
1900
|
+
events: options.events,
|
|
1901
|
+
name: options.name,
|
|
1902
|
+
enabled: options.enabled,
|
|
1903
|
+
modelName: options.modelName,
|
|
1904
|
+
workerName: options.workerName,
|
|
1905
|
+
workflowName: options.workflowName
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
);
|
|
1909
|
+
program.command("subscription:get").description("Get queue subscription").requiredOption("--queue <queue>", "Queue name").requiredOption("--id <id>", "Subscription ID").option("--json", "JSON output").action(async (options) => {
|
|
1910
|
+
await subscriptionGetCommand(options.queue, options.id, { json: options.json });
|
|
1911
|
+
});
|
|
1912
|
+
program.command("subscription:update").description("Update queue subscription destination").requiredOption("--queue <queue>", "Queue name").requiredOption("--id <id>", "Subscription ID").option("--name <name>", "Subscription name").option("--events <events>", "Event list").option("--enabled <enabled>", "Subscription enabled state (true/false)", parseBooleanOption).option("--json", "JSON output").action(
|
|
1913
|
+
async (options) => {
|
|
1914
|
+
await subscriptionUpdateCommand(options.queue, options.id, {
|
|
1915
|
+
name: options.name,
|
|
1916
|
+
events: options.events,
|
|
1917
|
+
enabled: options.enabled,
|
|
1918
|
+
json: options.json
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
);
|
|
1922
|
+
program.command("subscription:delete").description("Delete queue subscription").requiredOption("--queue <queue>", "Queue name").requiredOption("--id <id>", "Subscription ID").option("--force", "Skip confirmation where supported").action(async (options) => {
|
|
1923
|
+
await subscriptionDeleteCommand(options.queue, options.id, { force: options.force });
|
|
1924
|
+
});
|
|
1055
1925
|
try {
|
|
1056
1926
|
await program.parseAsync(argv, { from: "user" });
|
|
1057
1927
|
} catch (error) {
|
|
1058
|
-
|
|
1928
|
+
const cliError = toCliError(error);
|
|
1929
|
+
logger.cliError({
|
|
1930
|
+
code: cliError.code,
|
|
1931
|
+
summary: cliError.summary,
|
|
1932
|
+
file: cliError.file,
|
|
1933
|
+
details: cliError.details,
|
|
1934
|
+
hint: cliError.hint,
|
|
1935
|
+
docsUrl: cliError.docsUrl
|
|
1936
|
+
});
|
|
1059
1937
|
process.exitCode = 1;
|
|
1060
1938
|
}
|
|
1061
1939
|
}
|
|
1940
|
+
function parseNumberOption(value) {
|
|
1941
|
+
const parsed = Number(value);
|
|
1942
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
1943
|
+
throw new CliError({
|
|
1944
|
+
code: "INVALID_CLI_OPTION",
|
|
1945
|
+
summary: `Invalid numeric option: ${value}.`,
|
|
1946
|
+
details: "Expected a non-negative integer.",
|
|
1947
|
+
hint: "Use values like 0, 1, 30, 120."
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
return parsed;
|
|
1951
|
+
}
|
|
1952
|
+
function parseBooleanOption(value) {
|
|
1953
|
+
const normalized = value.toLowerCase();
|
|
1954
|
+
if (normalized === "true") {
|
|
1955
|
+
return true;
|
|
1956
|
+
}
|
|
1957
|
+
if (normalized === "false") {
|
|
1958
|
+
return false;
|
|
1959
|
+
}
|
|
1960
|
+
throw new CliError({
|
|
1961
|
+
code: "INVALID_CLI_OPTION",
|
|
1962
|
+
summary: `Invalid boolean option: ${value}.`,
|
|
1963
|
+
details: "Expected true or false.",
|
|
1964
|
+
hint: "Use --enabled true or --enabled false."
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
function resolveCreatePackageManager(options) {
|
|
1968
|
+
const selected = [];
|
|
1969
|
+
if (options.useNpm) {
|
|
1970
|
+
selected.push("npm");
|
|
1971
|
+
}
|
|
1972
|
+
if (options.usePnpm) {
|
|
1973
|
+
selected.push("pnpm");
|
|
1974
|
+
}
|
|
1975
|
+
if (options.useYarn) {
|
|
1976
|
+
selected.push("yarn");
|
|
1977
|
+
}
|
|
1978
|
+
if (options.useBun) {
|
|
1979
|
+
selected.push("bun");
|
|
1980
|
+
}
|
|
1981
|
+
if (selected.length > 1) {
|
|
1982
|
+
throw new CliError({
|
|
1983
|
+
code: "INVALID_CLI_OPTION",
|
|
1984
|
+
summary: "Conflicting package manager flags.",
|
|
1985
|
+
details: "Use only one of --use-npm, --use-pnpm, --use-yarn, or --use-bun."
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
return selected[0];
|
|
1989
|
+
}
|
|
1062
1990
|
|
|
1063
1991
|
export { run };
|
|
1064
1992
|
//# sourceMappingURL=index.js.map
|