wrangler 0.0.13 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/bin/wrangler.js +2 -2
  2. package/package.json +20 -11
  3. package/pages/functions/buildWorker.ts +1 -1
  4. package/pages/functions/filepath-routing.test.ts +112 -28
  5. package/pages/functions/filepath-routing.ts +44 -51
  6. package/pages/functions/routes.ts +11 -18
  7. package/pages/functions/template-worker.ts +3 -9
  8. package/src/__tests__/dev.test.tsx +42 -5
  9. package/src/__tests__/guess-worker-format.test.ts +66 -0
  10. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  11. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  12. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  13. package/src/__tests__/helpers/mock-account-id.ts +30 -0
  14. package/src/__tests__/helpers/mock-bin.ts +36 -0
  15. package/src/__tests__/{mock-cfetch.ts → helpers/mock-cfetch.ts} +43 -9
  16. package/src/__tests__/helpers/mock-console.ts +62 -0
  17. package/src/__tests__/{mock-dialogs.ts → helpers/mock-dialogs.ts} +1 -1
  18. package/src/__tests__/helpers/mock-kv.ts +40 -0
  19. package/src/__tests__/helpers/mock-user.ts +27 -0
  20. package/src/__tests__/helpers/mock-web-socket.ts +37 -0
  21. package/src/__tests__/{run-in-tmp.ts → helpers/run-in-tmp.ts} +1 -1
  22. package/src/__tests__/helpers/run-wrangler.ts +16 -0
  23. package/src/__tests__/helpers/write-wrangler-toml.ts +20 -0
  24. package/src/__tests__/index.test.ts +418 -71
  25. package/src/__tests__/jest.setup.ts +30 -2
  26. package/src/__tests__/kv.test.ts +147 -252
  27. package/src/__tests__/logout.test.ts +50 -0
  28. package/src/__tests__/package-manager.test.ts +206 -0
  29. package/src/__tests__/publish.test.ts +1136 -291
  30. package/src/__tests__/r2.test.ts +206 -0
  31. package/src/__tests__/secret.test.ts +210 -0
  32. package/src/__tests__/sentry.test.ts +146 -0
  33. package/src/__tests__/tail.test.ts +246 -0
  34. package/src/__tests__/whoami.test.tsx +6 -47
  35. package/src/api/form_data.ts +75 -25
  36. package/src/api/preview.ts +2 -2
  37. package/src/api/worker.ts +34 -15
  38. package/src/bundle.ts +127 -0
  39. package/src/cfetch/index.ts +7 -15
  40. package/src/cfetch/internal.ts +41 -6
  41. package/src/cli.ts +10 -0
  42. package/src/config.ts +125 -95
  43. package/src/dev.tsx +300 -193
  44. package/src/dialogs.tsx +2 -2
  45. package/src/guess-worker-format.ts +68 -0
  46. package/src/index.tsx +578 -192
  47. package/src/inspect.ts +29 -10
  48. package/src/kv.tsx +23 -17
  49. package/src/module-collection.ts +32 -12
  50. package/src/open-in-browser.ts +13 -0
  51. package/src/package-manager.ts +120 -0
  52. package/src/pages.tsx +28 -23
  53. package/src/paths.ts +26 -0
  54. package/src/proxy.ts +88 -14
  55. package/src/publish.ts +260 -297
  56. package/src/r2.ts +50 -0
  57. package/src/reporting.ts +115 -0
  58. package/src/sites.tsx +28 -27
  59. package/src/tail.tsx +178 -9
  60. package/src/user.tsx +58 -44
  61. package/templates/new-worker.js +15 -0
  62. package/templates/new-worker.ts +15 -0
  63. package/{static-asset-facade.js → templates/static-asset-facade.js} +0 -0
  64. package/wrangler-dist/cli.js +124315 -104677
  65. package/wrangler-dist/cli.js.map +3 -3
  66. package/src/__tests__/mock-console.ts +0 -34
  67. package/src/__tests__/run-wrangler.ts +0 -8
package/src/publish.ts CHANGED
@@ -1,24 +1,22 @@
1
1
  import assert from "node:assert";
2
+ import { existsSync, readFileSync } from "node:fs";
2
3
  import path from "node:path";
3
- import { readFile } from "node:fs/promises";
4
- import * as esbuild from "esbuild";
5
- import type { Metafile } from "esbuild";
6
- import { execa } from "execa";
4
+ import { URLSearchParams } from "node:url";
5
+ import { execaCommand } from "execa";
7
6
  import tmp from "tmp-promise";
8
- import type { CfWorkerInit } from "./api/worker";
9
7
  import { toFormData } from "./api/form_data";
8
+ import { bundleWorker } from "./bundle";
10
9
  import { fetchResult } from "./cfetch";
10
+ import guessWorkerFormat from "./guess-worker-format";
11
+ import { syncAssets } from "./sites";
12
+ import type { CfScriptFormat, CfWorkerInit } from "./api/worker";
11
13
  import type { Config } from "./config";
12
- import makeModuleCollector from "./module-collection";
13
14
  import type { AssetPaths } from "./sites";
14
- import { syncAssets } from "./sites";
15
-
16
- type CfScriptFormat = undefined | "modules" | "service-worker";
17
15
 
18
16
  type Props = {
19
17
  config: Config;
20
18
  format: CfScriptFormat | undefined;
21
- script: string | undefined;
19
+ entry: { file: string; directory: string };
22
20
  name: string | undefined;
23
21
  env: string | undefined;
24
22
  compatibilityDate: string | undefined;
@@ -37,20 +35,10 @@ function sleep(ms: number) {
37
35
  }
38
36
 
39
37
  export default async function publish(props: Props): Promise<void> {
40
- if (props.experimentalPublic && props.format === "service-worker") {
41
- // TODO: check config too
42
- throw new Error(
43
- "You cannot publish in the service worker format with a public directory."
44
- );
45
- }
46
38
  // TODO: warn if git/hg has uncommitted changes
47
39
  const { config } = props;
48
- const {
49
- account_id: accountId,
50
- build,
51
- // @ts-expect-error hidden
52
- __path__,
53
- } = config;
40
+ const { account_id: accountId, workers_dev: deployToWorkersDev = true } =
41
+ config;
54
42
 
55
43
  const envRootObj =
56
44
  props.env && config.env ? config.env[props.env] || {} : config;
@@ -91,309 +79,284 @@ export default async function publish(props: Props): Promise<void> {
91
79
  "A [site] definition requires a `bucket` field with a path to the site's public directory."
92
80
  );
93
81
 
94
- let file: string;
95
- if (props.script) {
96
- // If the script name comes from the command line it is relative to the current working directory.
97
- file = path.resolve(props.script);
98
- } else {
99
- // If the script name comes from the config, then it is relative to the wrangler.toml file.
100
- if (build?.upload?.main === undefined) {
82
+ const destination = await tmp.dir({ unsafeCleanup: true });
83
+ try {
84
+ if (props.legacyEnv) {
85
+ scriptName += props.env ? `-${props.env}` : "";
86
+ }
87
+ const envName = props.env ?? "production";
88
+
89
+ if (props.config.build?.command) {
90
+ // TODO: add a deprecation message here?
91
+ console.log("running:", props.config.build.command);
92
+ await execaCommand(props.config.build.command, {
93
+ shell: true,
94
+ stdout: "inherit",
95
+ stderr: "inherit",
96
+ timeout: 1000 * 30,
97
+ ...(props.config.build?.cwd && { cwd: props.config.build.cwd }),
98
+ });
99
+
100
+ let fileExists = false;
101
+ try {
102
+ // Use require.resolve to use node's resolution algorithm,
103
+ // this lets us use paths without explicit .js extension
104
+ // TODO: we should probably remove this, because it doesn't
105
+ // take into consideration other extensions like .tsx, .ts, .jsx, etc
106
+ fileExists = existsSync(require.resolve(props.entry.file));
107
+ } catch (e) {
108
+ // fail silently, usually means require.resolve threw MODULE_NOT_FOUND
109
+ }
110
+ if (fileExists === false) {
111
+ throw new Error(`Could not resolve "${props.entry.file}".`);
112
+ }
113
+ }
114
+
115
+ const format = await guessWorkerFormat(props.entry, props.format);
116
+
117
+ if (props.experimentalPublic && format === "service-worker") {
118
+ // TODO: check config too
101
119
  throw new Error(
102
- "Missing entry-point: The entry-point should be specified via the command line (e.g. `wrangler publish path/to/script`) or the `build.upload.main` config field."
120
+ "You cannot publish in the service worker format with a public directory."
103
121
  );
104
122
  }
105
- file = path.resolve(path.dirname(__path__), build.upload.main);
106
- }
107
-
108
- if (props.legacyEnv) {
109
- scriptName += props.env ? `-${props.env}` : "";
110
- }
111
- const envName = props.env ?? "production";
112
123
 
113
- const destination = await tmp.dir({ unsafeCleanup: true });
114
- if (props.config.build?.command) {
115
- // TODO: add a deprecation message here?
116
- console.log("running:", props.config.build.command);
117
- const buildCommandPieces = props.config.build.command.split(" ");
118
- await execa(buildCommandPieces[0], buildCommandPieces.slice(1), {
119
- stdout: "inherit",
120
- stderr: "inherit",
121
- ...(props.config.build?.cwd && { cwd: props.config.build.cwd }),
122
- });
123
- }
124
+ if ("wasm_modules" in config && format === "modules") {
125
+ throw new Error(
126
+ "You cannot configure [wasm_modules] with an ES module worker. Instead, import the .wasm module directly in your code"
127
+ );
128
+ }
124
129
 
125
- const moduleCollector = makeModuleCollector();
126
- const result = await esbuild.build({
127
- ...(props.experimentalPublic
128
- ? {
129
- stdin: {
130
- contents: (
131
- await readFile(
132
- path.join(__dirname, "../static-asset-facade.js"),
133
- "utf8"
134
- )
135
- ).replace("__ENTRY_POINT__", file),
136
- sourcefile: "static-asset-facade.js",
137
- resolveDir: path.dirname(file),
138
- },
139
- nodePaths: [path.join(__dirname, "../vendor")],
140
- }
141
- : { entryPoints: [file] }),
142
- bundle: true,
143
- outdir: destination.path,
144
- external: ["__STATIC_CONTENT_MANIFEST"],
145
- format: "esm",
146
- sourcemap: true,
147
- metafile: true,
148
- conditions: ["worker", "browser"],
149
- loader: {
150
- ".js": "jsx",
151
- },
152
- plugins: [moduleCollector.plugin],
153
- ...(jsxFactory && { jsxFactory }),
154
- ...(jsxFragment && { jsxFragment }),
155
- });
156
-
157
- // result.metafile is defined because of the `metafile: true` option above.
158
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
159
- const metafile = result.metafile!;
160
- const entryPoints = Object.entries(metafile.outputs).filter(
161
- ([_path, output]) => output.entryPoint !== undefined
162
- );
163
- assert(
164
- entryPoints.length > 0,
165
- `Cannot find entry-point "${file}" in generated bundle.` +
166
- listEntryPoints(entryPoints)
167
- );
168
- assert(
169
- entryPoints.length < 2,
170
- "More than one entry-point found for generated bundle." +
171
- listEntryPoints(entryPoints)
172
- );
173
- const entryPointExports = entryPoints[0][1].exports;
174
- const resolvedEntryPointPath = entryPoints[0][0];
175
- const { format } = props;
176
- const bundle = {
177
- type: entryPointExports.length > 0 ? "esm" : "commonjs",
178
- exports: entryPointExports,
179
- };
180
-
181
- // TODO: instead of bundling the facade with the worker, we should just bundle the worker and expose it as a module.
182
- // That way we'll be able to accurately tell if this is a service worker or not.
183
-
184
- if (format === "modules" && bundle.type === "commonjs") {
185
- console.error("⎔ Cannot use modules with a commonjs bundle.");
186
- // TODO: a much better error message here, with what to do next
187
- return;
188
- }
189
- if (format === "service-worker" && bundle.type !== "esm") {
190
- console.error("⎔ Cannot use service-worker with a esm bundle.");
191
- // TODO: a much better error message here, with what to do next
192
- return;
193
- }
130
+ const { modules, resolvedEntryPointPath, bundleType } = await bundleWorker(
131
+ props.entry,
132
+ props.experimentalPublic,
133
+ destination.path,
134
+ jsxFactory,
135
+ jsxFragment,
136
+ format
137
+ );
194
138
 
195
- const content = await readFile(resolvedEntryPointPath, { encoding: "utf-8" });
196
- await destination.cleanup();
139
+ let content = readFileSync(resolvedEntryPointPath, {
140
+ encoding: "utf-8",
141
+ });
197
142
 
198
- // if config.migrations
199
- // get current migration tag
200
- let migrations;
201
- if (config.migrations !== undefined) {
202
- const scripts = await fetchResult<{ id: string; migration_tag: string }[]>(
203
- `/accounts/${accountId}/workers/scripts`
204
- );
205
- const script = scripts.find(({ id }) => id === scriptName);
206
- if (script?.migration_tag) {
207
- // was already published once
208
- const foundIndex = config.migrations.findIndex(
209
- (migration) => migration.tag === script.migration_tag
210
- );
211
- if (foundIndex === -1) {
212
- console.warn(
213
- `The published script ${scriptName} has a migration tag "${script.migration_tag}, which was not found in wrangler.toml. You may have already deleted it. Applying all available migrations to the script...`
143
+ // if config.migrations
144
+ // get current migration tag
145
+ let migrations;
146
+ if (config.migrations !== undefined) {
147
+ const scripts = await fetchResult<
148
+ { id: string; migration_tag: string }[]
149
+ >(`/accounts/${accountId}/workers/scripts`);
150
+ const script = scripts.find(({ id }) => id === scriptName);
151
+ if (script?.migration_tag) {
152
+ // was already published once
153
+ const foundIndex = config.migrations.findIndex(
154
+ (migration) => migration.tag === script.migration_tag
214
155
  );
215
- migrations = {
216
- old_tag: script.migration_tag,
217
- new_tag: config.migrations[config.migrations.length - 1].tag,
218
- steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
219
- };
156
+ if (foundIndex === -1) {
157
+ console.warn(
158
+ `The published script ${scriptName} has a migration tag "${script.migration_tag}, which was not found in wrangler.toml. You may have already deleted it. Applying all available migrations to the script...`
159
+ );
160
+ migrations = {
161
+ old_tag: script.migration_tag,
162
+ new_tag: config.migrations[config.migrations.length - 1].tag,
163
+ steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
164
+ };
165
+ } else {
166
+ migrations = {
167
+ old_tag: script.migration_tag,
168
+ new_tag: config.migrations[config.migrations.length - 1].tag,
169
+ steps: config.migrations
170
+ .slice(foundIndex + 1)
171
+ .map(({ tag: _tag, ...rest }) => rest),
172
+ };
173
+ }
220
174
  } else {
221
175
  migrations = {
222
- old_tag: script.migration_tag,
223
176
  new_tag: config.migrations[config.migrations.length - 1].tag,
224
- steps: config.migrations
225
- .slice(foundIndex + 1)
226
- .map(({ tag: _tag, ...rest }) => rest),
177
+ steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
227
178
  };
228
179
  }
229
- } else {
230
- migrations = {
231
- new_tag: config.migrations[config.migrations.length - 1].tag,
232
- steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
233
- };
234
180
  }
235
- }
236
181
 
237
- const assets = await syncAssets(
238
- accountId,
239
- scriptName,
240
- props.assetPaths,
241
- false
242
- );
182
+ const assets = await syncAssets(
183
+ accountId,
184
+ scriptName,
185
+ props.assetPaths,
186
+ false,
187
+ props.env
188
+ );
243
189
 
244
- const bindings: CfWorkerInit["bindings"] = {
245
- kv_namespaces: envRootObj.kv_namespaces?.concat(
246
- assets.namespace
247
- ? { binding: "__STATIC_CONTENT", id: assets.namespace }
248
- : []
249
- ),
250
- vars: envRootObj.vars,
251
- durable_objects: envRootObj.durable_objects,
252
- services: envRootObj.experimental_services,
253
- };
254
-
255
- const worker: CfWorkerInit = {
256
- name: scriptName,
257
- main: {
258
- name: path.basename(resolvedEntryPointPath),
259
- content: content,
260
- type: bundle.type === "esm" ? "esm" : "commonjs",
261
- },
262
- bindings,
263
- ...(migrations && { migrations }),
264
- modules: moduleCollector.modules.concat(
265
- assets.manifest
266
- ? {
267
- name: "__STATIC_CONTENT_MANIFEST",
268
- content: JSON.stringify(assets.manifest),
269
- type: "text",
270
- }
271
- : []
272
- ),
273
- compatibility_date: config.compatibility_date,
274
- compatibility_flags: config.compatibility_flags,
275
- usage_model: config.usage_model,
276
- };
277
-
278
- const start = Date.now();
279
- function formatTime(duration: number) {
280
- return `(${(duration / 1000).toFixed(2)} sec)`;
281
- }
190
+ const bindings: CfWorkerInit["bindings"] = {
191
+ kv_namespaces: (envRootObj.kv_namespaces || []).concat(
192
+ assets.namespace
193
+ ? { binding: "__STATIC_CONTENT", id: assets.namespace }
194
+ : []
195
+ ),
196
+ vars: envRootObj.vars,
197
+ wasm_modules: config.wasm_modules,
198
+ durable_objects: envRootObj.durable_objects,
199
+ r2_buckets: envRootObj.r2_buckets,
200
+ unsafe: envRootObj.unsafe?.bindings,
201
+ };
202
+
203
+ if (assets.manifest) {
204
+ if (bundleType === "esm") {
205
+ modules.push({
206
+ name: "__STATIC_CONTENT_MANIFEST",
207
+ content: JSON.stringify(assets.manifest),
208
+ type: "text",
209
+ });
210
+ } else {
211
+ content = `const __STATIC_CONTENT_MANIFEST = ${JSON.stringify(
212
+ assets.manifest
213
+ )};\n${content}`;
214
+ }
215
+ }
282
216
 
283
- const notProd = !props.legacyEnv && props.env;
284
- const workerName = notProd ? `${scriptName} (${envName})` : scriptName;
285
- const workerUrl = notProd
286
- ? `/accounts/${accountId}/workers/services/${scriptName}/environments/${envName}`
287
- : `/accounts/${accountId}/workers/scripts/${scriptName}`;
288
-
289
- // Upload the script so it has time to propagate.
290
- const { available_on_subdomain } = await fetchResult(
291
- workerUrl,
292
- {
293
- method: "PUT",
294
- // @ts-expect-error: TODO: fix this type error!
295
- body: toFormData(worker),
296
- },
297
- new URLSearchParams({ available_on_subdomains: "true" })
298
- );
217
+ const worker: CfWorkerInit = {
218
+ name: scriptName,
219
+ main: {
220
+ name: path.basename(resolvedEntryPointPath),
221
+ content: content,
222
+ type: bundleType,
223
+ },
224
+ bindings,
225
+ migrations,
226
+ modules,
227
+ compatibility_date: config.compatibility_date,
228
+ compatibility_flags: config.compatibility_flags,
229
+ usage_model: config.usage_model,
230
+ };
231
+
232
+ const start = Date.now();
233
+ const notProd = !props.legacyEnv && props.env;
234
+ const workerName = notProd ? `${scriptName} (${envName})` : scriptName;
235
+ const workerUrl = notProd
236
+ ? `/accounts/${accountId}/workers/services/${scriptName}/environments/${envName}`
237
+ : `/accounts/${accountId}/workers/scripts/${scriptName}`;
238
+
239
+ // Upload the script so it has time to propagate.
240
+ const { available_on_subdomain } = await fetchResult(
241
+ workerUrl,
242
+ {
243
+ method: "PUT",
244
+ body: toFormData(worker),
245
+ },
246
+ new URLSearchParams({ available_on_subdomain: "true" })
247
+ );
299
248
 
300
- const uploadMs = Date.now() - start;
301
- console.log("Uploaded", workerName, formatTime(uploadMs));
302
- const deployments: Promise<string[]>[] = [];
303
-
304
- const userSubdomain = (
305
- await fetchResult<{ subdomain: string }>(
306
- `/accounts/${accountId}/workers/subdomain`
307
- )
308
- ).subdomain;
309
-
310
- const scriptURL =
311
- props.legacyEnv || !props.env
312
- ? `${scriptName}.${userSubdomain}.workers.dev`
313
- : `${envName}.${scriptName}.${userSubdomain}.workers.dev`;
314
-
315
- // Enable the `workers.dev` subdomain.
316
- // TODO: Make this configurable.
317
- if (!available_on_subdomain) {
318
- deployments.push(
249
+ const uploadMs = Date.now() - start;
250
+ console.log("Uploaded", workerName, formatTime(uploadMs));
251
+ const deployments: Promise<string[]>[] = [];
252
+
253
+ const userSubdomain = (
254
+ await fetchResult<{ subdomain: string }>(
255
+ `/accounts/${accountId}/workers/subdomain`
256
+ )
257
+ ).subdomain;
258
+
259
+ if (deployToWorkersDev) {
260
+ // Deploy to a subdomain of `workers.dev`
261
+ const scriptURL =
262
+ props.legacyEnv || !props.env
263
+ ? `${scriptName}.${userSubdomain}.workers.dev`
264
+ : `${envName}.${scriptName}.${userSubdomain}.workers.dev`;
265
+ if (!available_on_subdomain) {
266
+ // Enable the `workers.dev` subdomain.
267
+ deployments.push(
268
+ fetchResult(`${workerUrl}/subdomain`, {
269
+ method: "POST",
270
+ body: JSON.stringify({ enabled: true }),
271
+ headers: {
272
+ "Content-Type": "application/json",
273
+ },
274
+ })
275
+ .then(() => [scriptURL])
276
+ // Add a delay when the subdomain is first created.
277
+ // This is to prevent an issue where a negative cache-hit
278
+ // causes the subdomain to be unavailable for 30 seconds.
279
+ // This is a temporary measure until we fix this on the edge.
280
+ .then(async (url) => {
281
+ await sleep(3000);
282
+ return url;
283
+ })
284
+ );
285
+ } else {
286
+ deployments.push(Promise.resolve([scriptURL]));
287
+ }
288
+ } else {
289
+ // Disable the workers.dev deployment
319
290
  fetchResult(`${workerUrl}/subdomain`, {
320
291
  method: "POST",
321
- body: JSON.stringify({ enabled: true }),
292
+ body: JSON.stringify({ enabled: false }),
322
293
  headers: {
323
294
  "Content-Type": "application/json",
324
295
  },
325
- })
326
- .then(() => [scriptURL])
327
- // Add a delay when the subdomain is first created.
328
- // This is to prevent an issue where a negative cache-hit
329
- // causes the subdomain to be unavailable for 30 seconds.
330
- // This is a temporary measure until we fix this on the edge.
331
- .then(async (url) => {
332
- await sleep(3000);
333
- return url;
334
- })
335
- );
336
- } else {
337
- deployments.push(Promise.resolve([scriptURL]));
338
- }
296
+ });
297
+ }
339
298
 
340
- // Update routing table for the script.
341
- if (routes && routes.length) {
342
- deployments.push(
343
- fetchResult(`${workerUrl}/routes`, {
344
- // TODO: PATCH will not delete previous routes on this script,
345
- // whereas PUT will. We need to decide on the default behaviour
346
- // and how to configure it.
347
- method: "PUT",
348
- body: JSON.stringify(routes.map((pattern) => ({ pattern }))),
349
- headers: {
350
- "Content-Type": "application/json",
351
- },
352
- }).then(() => {
353
- if (routes.length > 10) {
354
- return routes
355
- .slice(0, 9)
356
- .map(String)
357
- .concat([`...and ${routes.length - 10} more routes`]);
358
- }
359
- return routes.map(String);
360
- })
361
- );
362
- }
299
+ // Update routing table for the script.
300
+ if (routes && routes.length) {
301
+ deployments.push(
302
+ fetchResult(`${workerUrl}/routes`, {
303
+ // TODO: PATCH will not delete previous routes on this script,
304
+ // whereas PUT will. We need to decide on the default behaviour
305
+ // and how to configure it.
306
+ method: "PUT",
307
+ body: JSON.stringify(routes.map((pattern) => ({ pattern }))),
308
+ headers: {
309
+ "Content-Type": "application/json",
310
+ },
311
+ }).then(() => {
312
+ if (routes.length > 10) {
313
+ return routes
314
+ .slice(0, 9)
315
+ .map(String)
316
+ .concat([`...and ${routes.length - 10} more routes`]);
317
+ }
318
+ return routes.map(String);
319
+ })
320
+ );
321
+ }
363
322
 
364
- // Configure any schedules for the script.
365
- // TODO: rename this to `schedules`?
366
- if (triggers && triggers.length) {
367
- deployments.push(
368
- fetchResult(`${workerUrl}/schedules`, {
369
- // TODO: Unlike routes, this endpoint does not support PATCH.
370
- // So technically, this will override any previous schedules.
371
- // We should change the endpoint to support PATCH.
372
- method: "PUT",
373
- body: JSON.stringify(triggers.map((cron) => ({ cron }))),
374
- headers: {
375
- "Content-Type": "application/json",
376
- },
377
- }).then(() => triggers.map(String))
378
- );
379
- }
323
+ // Configure any schedules for the script.
324
+ // TODO: rename this to `schedules`?
325
+ if (triggers && triggers.length) {
326
+ deployments.push(
327
+ fetchResult(`${workerUrl}/schedules`, {
328
+ // TODO: Unlike routes, this endpoint does not support PATCH.
329
+ // So technically, this will override any previous schedules.
330
+ // We should change the endpoint to support PATCH.
331
+ method: "PUT",
332
+ body: JSON.stringify(triggers.map((cron) => ({ cron }))),
333
+ headers: {
334
+ "Content-Type": "application/json",
335
+ },
336
+ }).then(() => triggers.map(String))
337
+ );
338
+ }
380
339
 
381
- if (!deployments.length) {
382
- return;
383
- }
340
+ const targets = await Promise.all(deployments);
341
+ const deployMs = Date.now() - start - uploadMs;
384
342
 
385
- const targets = await Promise.all(deployments);
386
- const deployMs = Date.now() - start - uploadMs;
387
- console.log("Deployed", workerName, formatTime(deployMs));
388
- for (const target of targets.flat()) {
389
- console.log(" ", target);
343
+ if (deployments.length > 0) {
344
+ console.log("Deployed", workerName, formatTime(deployMs));
345
+ for (const target of targets.flat()) {
346
+ console.log(" ", target);
347
+ }
348
+ } else {
349
+ console.log(
350
+ "No deployment targets for",
351
+ workerName,
352
+ formatTime(deployMs)
353
+ );
354
+ }
355
+ } finally {
356
+ await destination.cleanup();
390
357
  }
391
358
  }
392
359
 
393
- function listEntryPoints(
394
- outputs: [string, ValueOf<Metafile["outputs"]>][]
395
- ): string {
396
- return outputs.map(([_input, output]) => output.entryPoint).join("\n");
360
+ function formatTime(duration: number) {
361
+ return `(${(duration / 1000).toFixed(2)} sec)`;
397
362
  }
398
-
399
- type ValueOf<T> = T[keyof T];
package/src/r2.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { fetchResult } from "./cfetch";
2
+
3
+ /**
4
+ * Information about a bucket, returned from `listR2Buckets()`.
5
+ */
6
+ export interface R2BucketInfo {
7
+ name: string;
8
+ creation_date: string;
9
+ }
10
+
11
+ /**
12
+ * Fetch a list of all the buckets under the given `accountId`.
13
+ */
14
+ export async function listR2Buckets(
15
+ accountId: string
16
+ ): Promise<R2BucketInfo[]> {
17
+ const results = await fetchResult<{
18
+ buckets: R2BucketInfo[];
19
+ }>(`/accounts/${accountId}/r2/buckets`);
20
+ return results.buckets;
21
+ }
22
+
23
+ /**
24
+ * Create a bucket with the given `bucketName` within the account given by `accountId`.
25
+ *
26
+ * A 400 is returned if the account already owns a bucket with this name.
27
+ * A bucket must be explicitly deleted to be replaced.
28
+ */
29
+ export async function createR2Bucket(
30
+ accountId: string,
31
+ bucketName: string
32
+ ): Promise<void> {
33
+ return await fetchResult<void>(
34
+ `/accounts/${accountId}/r2/buckets/${bucketName}`,
35
+ { method: "PUT" }
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Delete a bucket with the given name
41
+ */
42
+ export async function deleteR2Bucket(
43
+ accountId: string,
44
+ bucketName: string
45
+ ): Promise<void> {
46
+ return await fetchResult<void>(
47
+ `/accounts/${accountId}/r2/buckets/${bucketName}`,
48
+ { method: "DELETE" }
49
+ );
50
+ }