wrangler 2.0.23 → 2.0.26

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 (80) hide show
  1. package/README.md +20 -2
  2. package/bin/wrangler.js +1 -1
  3. package/miniflare-dist/index.mjs +235 -47
  4. package/package.json +11 -6
  5. package/src/__tests__/configuration.test.ts +89 -17
  6. package/src/__tests__/dev.test.tsx +29 -4
  7. package/src/__tests__/generate.test.ts +93 -0
  8. package/src/__tests__/helpers/mock-cfetch.ts +87 -2
  9. package/src/__tests__/index.test.ts +10 -27
  10. package/src/__tests__/init.test.ts +537 -359
  11. package/src/__tests__/jest.setup.ts +34 -1
  12. package/src/__tests__/kv.test.ts +2 -2
  13. package/src/__tests__/metrics.test.ts +5 -0
  14. package/src/__tests__/pages.test.ts +14 -0
  15. package/src/__tests__/publish.test.ts +497 -254
  16. package/src/__tests__/r2.test.ts +173 -71
  17. package/src/__tests__/tail.test.ts +112 -42
  18. package/src/__tests__/user.test.ts +1 -0
  19. package/src/__tests__/validate-dev-props.test.ts +56 -0
  20. package/src/__tests__/whoami.test.tsx +60 -1
  21. package/src/api/dev.ts +7 -0
  22. package/src/bundle.ts +279 -44
  23. package/src/cfetch/internal.ts +73 -2
  24. package/src/config/config.ts +8 -3
  25. package/src/config/environment.ts +40 -8
  26. package/src/config/index.ts +13 -0
  27. package/src/config/validation.ts +102 -8
  28. package/src/create-worker-upload-form.ts +25 -0
  29. package/src/dev/dev.tsx +121 -28
  30. package/src/dev/local.tsx +88 -14
  31. package/src/dev/remote.tsx +39 -8
  32. package/src/dev/use-esbuild.ts +28 -0
  33. package/src/dev/validate-dev-props.ts +31 -0
  34. package/src/dev-registry.tsx +160 -0
  35. package/src/dev.tsx +107 -80
  36. package/src/generate.ts +112 -14
  37. package/src/index.tsx +212 -4
  38. package/src/init.ts +111 -38
  39. package/src/inspect.ts +90 -5
  40. package/src/metrics/index.ts +1 -0
  41. package/src/metrics/metrics-dispatcher.ts +1 -0
  42. package/src/metrics/metrics-usage-headers.ts +24 -0
  43. package/src/metrics/send-event.ts +2 -2
  44. package/src/miniflare-cli/assets.ts +27 -16
  45. package/src/miniflare-cli/index.ts +124 -2
  46. package/src/module-collection.ts +3 -3
  47. package/src/pages/build.tsx +75 -41
  48. package/src/pages/constants.ts +5 -0
  49. package/src/pages/deployments.tsx +10 -10
  50. package/src/pages/dev.tsx +177 -52
  51. package/src/pages/errors.ts +22 -0
  52. package/src/pages/functions/buildPlugin.ts +4 -0
  53. package/src/pages/functions/buildWorker.ts +4 -0
  54. package/src/pages/functions/routes-consolidation.test.ts +250 -0
  55. package/src/pages/functions/routes-consolidation.ts +73 -0
  56. package/src/pages/functions/routes-transformation.test.ts +271 -0
  57. package/src/pages/functions/routes-transformation.ts +122 -0
  58. package/src/pages/functions.tsx +96 -0
  59. package/src/pages/index.tsx +65 -55
  60. package/src/pages/projects.tsx +9 -3
  61. package/src/pages/publish.tsx +76 -23
  62. package/src/pages/types.ts +9 -0
  63. package/src/pages/upload.tsx +38 -21
  64. package/src/publish.ts +126 -112
  65. package/src/r2.ts +81 -0
  66. package/src/tail/filters.ts +3 -1
  67. package/src/tail/index.ts +15 -2
  68. package/src/tail/printing.ts +43 -3
  69. package/src/user/user.tsx +20 -2
  70. package/src/whoami.tsx +79 -1
  71. package/src/worker.ts +12 -0
  72. package/templates/first-party-worker-module-facade.ts +18 -0
  73. package/templates/format-dev-errors.ts +32 -0
  74. package/templates/pages-template-plugin.ts +16 -4
  75. package/templates/pages-template-worker.ts +16 -5
  76. package/templates/{static-asset-facade.js → serve-static-assets.ts} +21 -7
  77. package/templates/service-bindings-module-facade.js +54 -0
  78. package/templates/service-bindings-sw-facade.js +42 -0
  79. package/wrangler-dist/cli.d.ts +7 -0
  80. package/wrangler-dist/cli.js +40851 -15332
@@ -205,11 +205,12 @@ describe("WhoAmI component", () => {
205
205
  );
206
206
  });
207
207
 
208
- it("should display the user's email and accounts", async () => {
208
+ it("should display the user's email, accounts and OAuth scopes", async () => {
209
209
  const user: UserInfo = {
210
210
  authType: "OAuth Token",
211
211
  apiToken: "some-oauth-token",
212
212
  email: "user@example.com",
213
+ tokenPermissions: ["scope1:read", "scope2:write", "scope3"],
213
214
  accounts: [
214
215
  { name: "Account One", id: "account-1" },
215
216
  { name: "Account Two", id: "account-2" },
@@ -226,5 +227,63 @@ describe("WhoAmI component", () => {
226
227
  expect(lastFrame()).toMatch(/Account One .+ account-1/);
227
228
  expect(lastFrame()).toMatch(/Account Two .+ account-2/);
228
229
  expect(lastFrame()).toMatch(/Account Three .+ account-3/);
230
+ expect(lastFrame()).toContain(
231
+ "Token Permissions: If scopes are missing, you may need to logout and re-login."
232
+ );
233
+ expect(lastFrame()).toContain("- scope1 (read)");
234
+ expect(lastFrame()).toContain("- scope2 (write)");
235
+ expect(lastFrame()).toContain("- scope3");
236
+ });
237
+
238
+ // For the case where the cache hasn't updated to include the scopes array
239
+ it("should display the user's email and accounts, but no OAuth scopes if none provided", async () => {
240
+ const user: UserInfo = {
241
+ authType: "OAuth Token",
242
+ apiToken: "some-oauth-token",
243
+ email: "user@example.com",
244
+ tokenPermissions: undefined,
245
+ accounts: [
246
+ { name: "Account One", id: "account-1" },
247
+ { name: "Account Two", id: "account-2" },
248
+ { name: "Account Three", id: "account-3" },
249
+ ],
250
+ };
251
+
252
+ const { lastFrame } = render(<WhoAmI user={user}></WhoAmI>);
253
+
254
+ expect(lastFrame()).toContain(
255
+ "You are logged in with an OAuth Token, associated with the email 'user@example.com'!"
256
+ );
257
+ expect(lastFrame()).toMatch(/Account Name .+ Account ID/);
258
+ expect(lastFrame()).toMatch(/Account One .+ account-1/);
259
+ expect(lastFrame()).toMatch(/Account Two .+ account-2/);
260
+ expect(lastFrame()).toMatch(/Account Three .+ account-3/);
261
+ });
262
+
263
+ it("should display the user's email, accounts and link to view token permissions for non-OAuth tokens", async () => {
264
+ const user: UserInfo = {
265
+ authType: "API Token",
266
+ apiToken: "some-api-token",
267
+ email: "user@example.com",
268
+ tokenPermissions: undefined,
269
+ accounts: [
270
+ { name: "Account One", id: "account-1" },
271
+ { name: "Account Two", id: "account-2" },
272
+ { name: "Account Three", id: "account-3" },
273
+ ],
274
+ };
275
+
276
+ const { lastFrame } = render(<WhoAmI user={user}></WhoAmI>);
277
+
278
+ expect(lastFrame()).toContain(
279
+ "You are logged in with an API Token, associated with the email 'user@example.com'!"
280
+ );
281
+ expect(lastFrame()).toMatch(/Account Name .+ Account ID/);
282
+ expect(lastFrame()).toMatch(/Account One .+ account-1/);
283
+ expect(lastFrame()).toMatch(/Account Two .+ account-2/);
284
+ expect(lastFrame()).toMatch(/Account Three .+ account-3/);
285
+ expect(lastFrame()).toContain(
286
+ "To see token permissions visit https://dash.cloudflare.com/profile/api-tokens"
287
+ );
229
288
  });
230
289
  });
package/src/api/dev.ts CHANGED
@@ -8,6 +8,7 @@ interface DevOptions {
8
8
  env?: string;
9
9
  ip?: string;
10
10
  port?: number;
11
+ inspectorPort?: number;
11
12
  localProtocol?: "http" | "https";
12
13
  assets?: string;
13
14
  site?: string;
@@ -15,6 +16,7 @@ interface DevOptions {
15
16
  siteExclude?: string[];
16
17
  nodeCompat?: boolean;
17
18
  compatibilityDate?: string;
19
+ compatibilityFlags?: string[];
18
20
  experimentalEnableLocalPersistence?: boolean;
19
21
  liveReload?: boolean;
20
22
  watch?: boolean;
@@ -32,6 +34,11 @@ interface DevOptions {
32
34
  script_name?: string | undefined;
33
35
  environment?: string | undefined;
34
36
  }[];
37
+ r2?: {
38
+ binding: string;
39
+ bucket_name: string;
40
+ preview_bucket_name?: string;
41
+ }[];
35
42
  showInteractiveDevSession?: boolean;
36
43
  logLevel?: "none" | "error" | "log" | "warn" | "debug";
37
44
  logPrefix?: string;
package/src/bundle.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import assert from "node:assert";
2
2
  import * as fs from "node:fs";
3
3
  import { builtinModules } from "node:module";
4
- import * as os from "node:os";
5
4
  import * as path from "node:path";
6
5
  import NodeGlobalsPolyfills from "@esbuild-plugins/node-globals-polyfill";
7
6
  import NodeModulesPolyfills from "@esbuild-plugins/node-modules-polyfill";
8
7
  import * as esbuild from "esbuild";
8
+ import tmp from "tmp-promise";
9
9
  import createModuleCollector from "./module-collection";
10
10
  import type { Config } from "./config";
11
+ import type { WorkerRegistry } from "./dev-registry";
11
12
  import type { Entry } from "./entry";
12
13
  import type { CfModule } from "./worker";
13
14
 
@@ -16,8 +17,15 @@ type BundleResult = {
16
17
  resolvedEntryPointPath: string;
17
18
  bundleType: "esm" | "commonjs";
18
19
  stop: (() => void) | undefined;
20
+ sourceMapPath?: string | undefined;
19
21
  };
20
22
 
23
+ type StaticAssetsConfig =
24
+ | (Config["assets"] & {
25
+ bypassCache: boolean | undefined;
26
+ })
27
+ | undefined;
28
+
21
29
  /**
22
30
  * Searches for any uses of node's builtin modules, and throws an error if it
23
31
  * finds anything. This plugin is only used when nodeCompat is not enabled.
@@ -53,6 +61,7 @@ export async function bundleWorker(
53
61
  destination: string,
54
62
  options: {
55
63
  serveAssetsFromWorker: boolean;
64
+ assets: StaticAssetsConfig;
56
65
  jsxFactory: string | undefined;
57
66
  jsxFragment: string | undefined;
58
67
  rules: Config["rules"];
@@ -62,6 +71,9 @@ export async function bundleWorker(
62
71
  nodeCompat: boolean | undefined;
63
72
  define: Config["define"];
64
73
  checkFetch: boolean;
74
+ services: Config["services"];
75
+ workerDefinitions: WorkerRegistry | undefined;
76
+ firstPartyWorkerDevFacade: boolean | undefined;
65
77
  }
66
78
  ): Promise<BundleResult> {
67
79
  const {
@@ -74,7 +86,17 @@ export async function bundleWorker(
74
86
  minify,
75
87
  nodeCompat,
76
88
  checkFetch,
89
+ assets,
90
+ workerDefinitions,
91
+ services,
92
+ firstPartyWorkerDevFacade,
77
93
  } = options;
94
+
95
+ // We create a temporary directory for any oneoff files we
96
+ // need to create. This is separate from the main build
97
+ // directory (`destination`).
98
+ const tmpDir = await tmp.dir({ unsafeCleanup: true });
99
+
78
100
  const entryDirectory = path.dirname(entry.file);
79
101
  const moduleCollector = createModuleCollector({
80
102
  wrangler1xlegacyModuleReferences: {
@@ -98,15 +120,11 @@ export async function bundleWorker(
98
120
  // `checked-fetch.js` to do so. However, with yarn 3 style pnp,
99
121
  // we need to extract that file to an accessible place before injecting
100
122
  // it in, hence this code here.
101
- const osTempDir = os.tmpdir();
102
- const checkedFetchFileToInject = path.join(
103
- osTempDir,
104
- "--temp-wrangler-files--",
105
- "checked-fetch.js"
106
- );
123
+
124
+ const checkedFetchFileToInject = path.join(tmpDir.path, "checked-fetch.js");
107
125
 
108
126
  if (checkFetch && !fs.existsSync(checkedFetchFileToInject)) {
109
- fs.mkdirSync(path.join(osTempDir, "--temp-wrangler-files--"), {
127
+ fs.mkdirSync(tmpDir.path, {
110
128
  recursive: true,
111
129
  });
112
130
  fs.writeFileSync(
@@ -114,12 +132,58 @@ export async function bundleWorker(
114
132
  fs.readFileSync(path.resolve(__dirname, "../templates/checked-fetch.js"))
115
133
  );
116
134
  }
117
- // TODO: we need to have similar logic like above for other files
118
- // like the static asset facade, and other middleware that we
119
- // plan on injecting/referencing.
135
+
136
+ // At this point, we take the opportunity to "wrap" any input workers
137
+ // with any extra functionality we may want to add. This is done by
138
+ // passing the entry point through a pipeline of functions that return
139
+ // a new entry point, that we call "middleware" or "facades".
140
+ // Look at implementations of these functions to learn more.
141
+
142
+ type MiddlewareFn = (arg0: Entry) => Promise<Entry>;
143
+ const middleware: (false | undefined | MiddlewareFn)[] = [
144
+ // serve static assets
145
+ serveAssetsFromWorker &&
146
+ ((currentEntry: Entry) => {
147
+ return applyStaticAssetFacade(currentEntry, tmpDir.path, assets);
148
+ }),
149
+ // format errors nicely
150
+ // We use an env var here because we don't actually
151
+ // want to expose this to the user. It's only used internally to
152
+ // experiment with middleware as a teaching exercise.
153
+ process.env.FORMAT_WRANGLER_ERRORS === "true" &&
154
+ ((currentEntry: Entry) => {
155
+ return applyFormatDevErrorsFacade(currentEntry, tmpDir.path);
156
+ }),
157
+ // bind to other dev instances/service bindings
158
+ workerDefinitions &&
159
+ Object.keys(workerDefinitions).length > 0 &&
160
+ services &&
161
+ services.length > 0 &&
162
+ ((currentEntry: Entry) => {
163
+ return applyMultiWorkerDevFacade(
164
+ currentEntry,
165
+ tmpDir.path,
166
+ services,
167
+ workerDefinitions
168
+ );
169
+ }),
170
+ // Simulate internal environment when using first party workers in dev
171
+ firstPartyWorkerDevFacade === true &&
172
+ ((currentEntry: Entry) => {
173
+ return applyFirstPartyWorkerDevFacade(currentEntry, tmpDir.path);
174
+ }),
175
+ ].filter(Boolean);
176
+
177
+ let inputEntry = entry;
178
+
179
+ for (const middlewareFn of middleware as MiddlewareFn[]) {
180
+ inputEntry = await middlewareFn(inputEntry);
181
+ }
182
+
183
+ // At this point, inputEntry points to the entry point we want to build.
120
184
 
121
185
  const result = await esbuild.build({
122
- ...getEntryPoint(entry.file, serveAssetsFromWorker),
186
+ entryPoints: [inputEntry.file],
123
187
  bundle: true,
124
188
  absWorkingDir: entry.directory,
125
189
  outdir: destination,
@@ -128,6 +192,9 @@ export async function bundleWorker(
128
192
  format: entry.format === "modules" ? "esm" : "iife",
129
193
  target: "es2020",
130
194
  sourcemap: true,
195
+ // Include a reference to the output folder in the sourcemap.
196
+ // This is omitted by default, but we need it to properly resolve source paths in error output.
197
+ sourceRoot: destination,
131
198
  minify,
132
199
  metafile: true,
133
200
  conditions: ["worker", "browser"],
@@ -177,6 +244,10 @@ export async function bundleWorker(
177
244
  const entryPointExports = entryPointOutputs[0][1].exports;
178
245
  const bundleType = entryPointExports.length > 0 ? "esm" : "commonjs";
179
246
 
247
+ const sourceMapPath = Object.keys(result.metafile.outputs).filter((_path) =>
248
+ _path.includes(".map")
249
+ )[0];
250
+
180
251
  return {
181
252
  modules: moduleCollector.modules,
182
253
  resolvedEntryPointPath: path.resolve(
@@ -185,45 +256,209 @@ export async function bundleWorker(
185
256
  ),
186
257
  bundleType,
187
258
  stop: result.stop,
259
+ sourceMapPath,
188
260
  };
189
261
  }
190
262
 
191
- type EntryPoint = { stdin: esbuild.StdinOptions } | { entryPoints: string[] };
263
+ /**
264
+ * A simple plugin to alias modules and mark them as external
265
+ */
266
+ function esbuildAliasExternalPlugin(
267
+ aliases: Record<string, string>
268
+ ): esbuild.Plugin {
269
+ return {
270
+ name: "alias",
271
+ setup(build) {
272
+ build.onResolve({ filter: /.*/g }, (args) => {
273
+ // If it's the entrypoint, let it be as is
274
+ if (args.kind === "entry-point") {
275
+ return {
276
+ path: args.path,
277
+ };
278
+ }
279
+ // If it's not a recognised alias, then throw an error
280
+ if (!Object.keys(aliases).includes(args.path)) {
281
+ throw new Error("unrecognized module: " + args.path);
282
+ }
283
+
284
+ // Otherwise, return the alias
285
+ return {
286
+ path: aliases[args.path as keyof typeof aliases],
287
+ external: true,
288
+ };
289
+ });
290
+ },
291
+ };
292
+ }
192
293
 
193
294
  /**
194
- * Create an object that describes the entry point for esbuild.
195
- *
196
- * If we are using the experimental asset handling, then the entry point is
197
- * actually a shim worker that will either return an asset from a KV store,
198
- * or delegate to the actual worker.
295
+ * A middleware that catches any thrown errors, and instead formats
296
+ * them to be rendered in a browser. This middleware is for demonstration
297
+ * purposes only, and is not intended to be used in production (or even dev!)
199
298
  */
200
- function getEntryPoint(
201
- entryFile: string,
202
- serveAssetsFromWorker: boolean
203
- ): EntryPoint {
204
- if (serveAssetsFromWorker) {
205
- return {
206
- stdin: {
207
- contents: fs
208
- .readFileSync(
209
- path.join(__dirname, "../templates/static-asset-facade.js"),
210
- "utf8"
211
- )
212
- // on windows, escape backslashes in the path (`\`)
213
- .replace("__ENTRY_POINT__", entryFile.replaceAll("\\", "\\\\"))
214
- .replace(
215
- "__KV_ASSET_HANDLER__",
216
- path
217
- .join(__dirname, "../kv-asset-handler.js")
218
- .replaceAll("\\", "\\\\")
219
- ),
220
- sourcefile: "static-asset-facade.js",
221
- resolveDir: path.dirname(entryFile),
222
- },
223
- };
224
- } else {
225
- return { entryPoints: [entryFile] };
299
+ async function applyFormatDevErrorsFacade(
300
+ entry: Entry,
301
+ tmpDirPath: string
302
+ ): Promise<Entry> {
303
+ const targetPath = path.join(tmpDirPath, "format-dev-errors.entry.js");
304
+ await esbuild.build({
305
+ entryPoints: [path.resolve(__dirname, "../templates/format-dev-errors.ts")],
306
+ bundle: true,
307
+ sourcemap: true,
308
+ format: "esm",
309
+ plugins: [
310
+ esbuildAliasExternalPlugin({
311
+ __ENTRY_POINT__: entry.file,
312
+ }),
313
+ ],
314
+ outfile: targetPath,
315
+ });
316
+
317
+ return {
318
+ ...entry,
319
+ file: targetPath,
320
+ };
321
+ }
322
+
323
+ /**
324
+ * A middleware that serves static assets from a worker.
325
+ * This powers --assets / config.assets
326
+ */
327
+
328
+ async function applyStaticAssetFacade(
329
+ entry: Entry,
330
+ tmpDirPath: string,
331
+ assets: StaticAssetsConfig
332
+ ): Promise<Entry> {
333
+ const targetPath = path.join(tmpDirPath, "serve-static-assets.entry.js");
334
+
335
+ await esbuild.build({
336
+ entryPoints: [
337
+ path.resolve(__dirname, "../templates/serve-static-assets.ts"),
338
+ ],
339
+ bundle: true,
340
+ format: "esm",
341
+ sourcemap: true,
342
+ plugins: [
343
+ esbuildAliasExternalPlugin({
344
+ __ENTRY_POINT__: entry.file,
345
+ __KV_ASSET_HANDLER__: path.join(__dirname, "../kv-asset-handler.js"),
346
+ __STATIC_CONTENT_MANIFEST: "__STATIC_CONTENT_MANIFEST",
347
+ }),
348
+ ],
349
+ define: {
350
+ __CACHE_CONTROL_OPTIONS__: JSON.stringify(
351
+ typeof assets === "object"
352
+ ? {
353
+ browserTTL:
354
+ assets.browser_TTL || 172800 /* 2 days: 2* 60 * 60 * 24 */,
355
+ bypassCache: assets.bypassCache,
356
+ }
357
+ : {}
358
+ ),
359
+ __SERVE_SINGLE_PAGE_APP__: JSON.stringify(
360
+ typeof assets === "object" ? assets.serve_single_page_app : false
361
+ ),
362
+ },
363
+ outfile: targetPath,
364
+ });
365
+
366
+ return {
367
+ ...entry,
368
+ file: targetPath,
369
+ };
370
+ }
371
+
372
+ /**
373
+ * A middleware that enables service bindings to be used in dev,
374
+ * binding to other love wrangler dev instances
375
+ */
376
+
377
+ async function applyMultiWorkerDevFacade(
378
+ entry: Entry,
379
+ tmpDirPath: string,
380
+ services: Config["services"],
381
+ workerDefinitions: WorkerRegistry
382
+ ) {
383
+ const targetPath = path.join(tmpDirPath, "serve-static-assets.entry.js");
384
+ const serviceMap = Object.fromEntries(
385
+ (services || []).map((serviceBinding) => [
386
+ serviceBinding.binding,
387
+ workerDefinitions[serviceBinding.service] || null,
388
+ ])
389
+ );
390
+
391
+ await esbuild.build({
392
+ entryPoints: [
393
+ path.join(
394
+ __dirname,
395
+ entry.format === "modules"
396
+ ? "../templates/service-bindings-module-facade.js"
397
+ : "../templates/service-bindings-sw-facade.js"
398
+ ),
399
+ ],
400
+ bundle: true,
401
+ sourcemap: true,
402
+ format: "esm",
403
+ plugins: [
404
+ esbuildAliasExternalPlugin({
405
+ __ENTRY_POINT__: entry.file,
406
+ }),
407
+ ],
408
+ define: {
409
+ __WORKERS__: JSON.stringify(serviceMap),
410
+ },
411
+ outfile: targetPath,
412
+ });
413
+
414
+ return {
415
+ ...entry,
416
+ file: targetPath,
417
+ };
418
+ }
419
+
420
+ /**
421
+ * A middleware that makes first party workers "work" in
422
+ * our dev environments. Is applied during wrangler dev
423
+ * when config.first_party_worker is true
424
+ */
425
+ async function applyFirstPartyWorkerDevFacade(
426
+ entry: Entry,
427
+ tmpDirPath: string
428
+ ) {
429
+ if (entry.format !== "modules") {
430
+ throw new Error(
431
+ "First party workers must be in the modules format. See https://developers.cloudflare.com/workers/learning/migrating-to-module-workers/"
432
+ );
226
433
  }
434
+
435
+ const targetPath = path.join(
436
+ tmpDirPath,
437
+ "first-party-worker-module-facade.entry.js"
438
+ );
439
+
440
+ await esbuild.build({
441
+ entryPoints: [
442
+ path.resolve(
443
+ __dirname,
444
+ "../templates/first-party-worker-module-facade.ts"
445
+ ),
446
+ ],
447
+ bundle: true,
448
+ format: "esm",
449
+ sourcemap: true,
450
+ plugins: [
451
+ esbuildAliasExternalPlugin({
452
+ __ENTRY_POINT__: entry.file,
453
+ }),
454
+ ],
455
+ outfile: targetPath,
456
+ });
457
+
458
+ return {
459
+ ...entry,
460
+ file: targetPath,
461
+ };
227
462
  }
228
463
 
229
464
  /**
@@ -6,7 +6,7 @@ import { ParseError, parseJSON } from "../parse";
6
6
  import { loginOrRefreshIfRequired, requireApiToken } from "../user";
7
7
  import type { ApiCredentials } from "../user";
8
8
  import type { URLSearchParams } from "node:url";
9
- import type { RequestInit, HeadersInit } from "undici";
9
+ import type { RequestInit, HeadersInit, Response } from "undici";
10
10
 
11
11
  /**
12
12
  * Get the URL to use to access the Cloudflare API.
@@ -124,7 +124,6 @@ function addUserAgent(headers: Record<string, string>): void {
124
124
  * Note: any calls to fetchKVGetValue must call encodeURIComponent on key
125
125
  * before passing it
126
126
  */
127
-
128
127
  export async function fetchKVGetValue(
129
128
  accountId: string,
130
129
  namespaceId: string,
@@ -147,3 +146,75 @@ export async function fetchKVGetValue(
147
146
  );
148
147
  }
149
148
  }
149
+
150
+ /**
151
+ * The implementation for fetching a R2 object from Cloudflare API.
152
+ * We have a special implementation to handle the non-standard API response
153
+ * that doesn't return JSON, likely due to the streaming nature.
154
+ *
155
+ * note: The implementation should be called from light wrappers for
156
+ * different methods (GET, PUT)
157
+ */
158
+ type ResponseWithBody = Response & { body: NonNullable<Response["body"]> };
159
+ export async function fetchR2Objects(
160
+ resource: string,
161
+ bodyInit: RequestInit = {}
162
+ ): Promise<ResponseWithBody> {
163
+ await requireLoggedIn();
164
+ const auth = requireApiToken();
165
+ const headers = cloneHeaders(bodyInit.headers);
166
+ addAuthorizationHeaderIfUnspecified(headers, auth);
167
+ addUserAgent(headers);
168
+
169
+ const response = await fetch(`${getCloudflareAPIBaseURL()}${resource}`, {
170
+ ...bodyInit,
171
+ headers,
172
+ });
173
+
174
+ if (response.ok && response.body) {
175
+ return response as ResponseWithBody;
176
+ } else {
177
+ throw new Error(
178
+ `Failed to fetch ${resource} - ${response.status}: ${response.statusText});`
179
+ );
180
+ }
181
+ }
182
+
183
+ /**
184
+ * This is a wrapper STOPGAP for getting the script which returns a raw text response.
185
+ */
186
+ export async function fetchDashboardScript(
187
+ resource: string,
188
+ bodyInit: RequestInit = {}
189
+ ): Promise<string> {
190
+ await requireLoggedIn();
191
+ const auth = requireApiToken();
192
+ const headers = cloneHeaders(bodyInit.headers);
193
+ addAuthorizationHeaderIfUnspecified(headers, auth);
194
+ addUserAgent(headers);
195
+
196
+ const response = await fetch(`${getCloudflareAPIBaseURL()}${resource}`, {
197
+ ...bodyInit,
198
+ headers,
199
+ });
200
+
201
+ if (!response.ok || !response.body) {
202
+ throw new Error(
203
+ `Failed to fetch ${resource} - ${response.status}: ${response.statusText});`
204
+ );
205
+ }
206
+
207
+ const usesModules = response.headers
208
+ .get("content-type")
209
+ ?.startsWith("multipart");
210
+
211
+ if (usesModules) {
212
+ const file = await response.text();
213
+
214
+ // Follow up on issue in Undici about multipart/form-data support & replace the workaround: https://github.com/nodejs/undici/issues/974
215
+ // This should be using a builtin formData() parser pattern.
216
+ return file.split("\n").slice(4, -4).join("\n");
217
+ } else {
218
+ return response.text();
219
+ }
220
+ }
@@ -127,8 +127,13 @@ export interface ConfigFields<Dev extends RawDevConfig> {
127
127
  * This can either be a string, or an object with additional config fields.
128
128
  */
129
129
  assets:
130
- | string
131
- | { bucket: string; include: string[]; exclude: string[] }
130
+ | {
131
+ bucket: string;
132
+ include: string[];
133
+ exclude: string[];
134
+ browser_TTL: number | undefined;
135
+ serve_single_page_app: boolean;
136
+ }
132
137
  | undefined;
133
138
 
134
139
  /**
@@ -169,7 +174,7 @@ export interface DevConfig {
169
174
  /**
170
175
  * IP address for the local dev server to listen on,
171
176
  *
172
- * @default `localhost`
177
+ * @default `0.0.0.0`
173
178
  */
174
179
  ip: string;
175
180