wrangler 2.0.27 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/bin/wrangler.js +1 -1
  2. package/miniflare-dist/index.mjs +1141 -369
  3. package/package.json +6 -4
  4. package/src/__tests__/api-dev.test.ts +19 -0
  5. package/src/__tests__/configuration.test.ts +27 -27
  6. package/src/__tests__/dev.test.tsx +8 -6
  7. package/src/__tests__/helpers/hello-world-worker.js +5 -0
  8. package/src/__tests__/helpers/mock-cfetch.ts +4 -4
  9. package/src/__tests__/helpers/mock-console.ts +11 -2
  10. package/src/__tests__/helpers/mock-get-zone-from-host.ts +8 -0
  11. package/src/__tests__/helpers/mock-known-routes.ts +7 -0
  12. package/src/__tests__/index.test.ts +37 -37
  13. package/src/__tests__/init.test.ts +356 -5
  14. package/src/__tests__/jest.setup.ts +13 -0
  15. package/src/__tests__/middleware.test.ts +768 -0
  16. package/src/__tests__/pages.test.ts +829 -104
  17. package/src/__tests__/paths.test.ts +17 -0
  18. package/src/__tests__/publish.test.ts +512 -445
  19. package/src/__tests__/tail.test.ts +79 -72
  20. package/src/__tests__/test-old-node-version.js +3 -3
  21. package/src/__tests__/worker-namespace.test.ts +37 -35
  22. package/src/api/dev.ts +93 -28
  23. package/src/bundle.ts +239 -12
  24. package/src/cfetch/internal.ts +64 -3
  25. package/src/cli.ts +1 -1
  26. package/src/config/environment.ts +1 -1
  27. package/src/config/index.ts +4 -4
  28. package/src/config/validation.ts +3 -3
  29. package/src/create-worker-upload-form.ts +29 -26
  30. package/src/dev/dev.tsx +3 -1
  31. package/src/dev/local.tsx +319 -171
  32. package/src/dev/remote.tsx +16 -4
  33. package/src/dev/start-server.ts +416 -0
  34. package/src/dev/use-esbuild.ts +4 -0
  35. package/src/dev.tsx +340 -166
  36. package/src/dialogs.tsx +12 -0
  37. package/src/{worker-namespace.ts → dispatch-namespace.ts} +18 -18
  38. package/src/entry.ts +2 -1
  39. package/src/index.tsx +59 -12
  40. package/src/init.ts +291 -16
  41. package/src/metrics/send-event.ts +6 -5
  42. package/src/miniflare-cli/assets.ts +130 -476
  43. package/src/miniflare-cli/index.ts +39 -33
  44. package/src/pages/constants.ts +3 -0
  45. package/src/pages/dev.tsx +8 -3
  46. package/src/pages/functions/buildPlugin.ts +2 -1
  47. package/src/pages/functions/buildWorker.ts +2 -1
  48. package/src/pages/functions/routes-transformation.test.ts +12 -1
  49. package/src/pages/functions/routes-transformation.ts +7 -1
  50. package/src/pages/hash.tsx +13 -0
  51. package/src/pages/publish.tsx +82 -38
  52. package/src/pages/upload.tsx +3 -18
  53. package/src/paths.ts +20 -1
  54. package/src/publish.ts +49 -8
  55. package/src/tail/filters.ts +1 -5
  56. package/src/tail/index.ts +6 -3
  57. package/src/worker.ts +10 -9
  58. package/src/zones.ts +91 -0
  59. package/templates/middleware/common.ts +62 -0
  60. package/templates/middleware/loader-modules.ts +84 -0
  61. package/templates/middleware/loader-sw.ts +213 -0
  62. package/templates/middleware/middleware-pretty-error.ts +40 -0
  63. package/templates/middleware/middleware-scheduled.ts +14 -0
  64. package/wrangler-dist/cli.d.ts +22 -8
  65. package/wrangler-dist/cli.js +71020 -65212
@@ -1,5 +1,8 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { chdir } from "node:process";
3
+ import { ROUTES_SPEC_VERSION } from "../pages/constants";
4
+ import { isRoutesJSONSpec } from "../pages/functions/routes-transformation";
5
+ import { version } from "./../../package.json";
3
6
  import { mockAccountId, mockApiToken } from "./helpers/mock-account-id";
4
7
  import {
5
8
  createFetchResult,
@@ -57,23 +60,23 @@ describe("pages", () => {
57
60
  await endEventLoop();
58
61
 
59
62
  expect(std.out).toMatchInlineSnapshot(`
60
- "wrangler pages
63
+ "wrangler pages
61
64
 
62
- ⚡️ Configure Cloudflare Pages
65
+ ⚡️ Configure Cloudflare Pages
63
66
 
64
- Commands:
65
- wrangler pages dev [directory] [-- command..] 🧑‍💻 Develop your full-stack Pages application locally
66
- wrangler pages project ⚡️ Interact with your Pages projects
67
- wrangler pages deployment 🚀 Interact with the deployments of a project
68
- wrangler pages publish [directory] 🆙 Publish a directory of static assets as a Pages deployment
67
+ Commands:
68
+ wrangler pages dev [directory] [-- command..] 🧑‍💻 Develop your full-stack Pages application locally
69
+ wrangler pages project ⚡️ Interact with your Pages projects
70
+ wrangler pages deployment 🚀 Interact with the deployments of a project
71
+ wrangler pages publish [directory] 🆙 Publish a directory of static assets as a Pages deployment
69
72
 
70
- Flags:
71
- -c, --config Path to .toml configuration file [string]
72
- -h, --help Show help [boolean]
73
- -v, --version Show version number [boolean]
73
+ Flags:
74
+ -c, --config Path to .toml configuration file [string]
75
+ -h, --help Show help [boolean]
76
+ -v, --version Show version number [boolean]
74
77
 
75
- 🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose"
76
- `);
78
+ 🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose"
79
+ `);
77
80
  });
78
81
 
79
82
  describe("beta message for subcommands", () => {
@@ -81,24 +84,24 @@ describe("pages", () => {
81
84
  await expect(
82
85
  runWrangler("pages dev")
83
86
  ).rejects.toThrowErrorMatchingInlineSnapshot(
84
- `"Must specify a directory of static assets to serve or a command to run."`
87
+ `"Must specify a directory of static assets to serve or a command to run or a proxy port."`
85
88
  );
86
89
 
87
90
  expect(std.out).toMatchInlineSnapshot(`
88
- "🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose
91
+ "🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose
89
92
 
90
- If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose"
91
- `);
93
+ If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose"
94
+ `);
92
95
  });
93
96
 
94
97
  it("should display for pages:functions:build", async () => {
95
98
  await expect(runWrangler("pages functions build")).rejects.toThrowError();
96
99
 
97
100
  expect(std.out).toMatchInlineSnapshot(`
98
- "🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose
101
+ "🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose
99
102
 
100
- If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose"
101
- `);
103
+ If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose"
104
+ `);
102
105
  });
103
106
 
104
107
  it("should display for pages:functions:optimize-routes", async () => {
@@ -109,10 +112,10 @@ describe("pages", () => {
109
112
  ).rejects.toThrowError();
110
113
 
111
114
  expect(std.out).toMatchInlineSnapshot(`
112
- "🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose
115
+ "🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose
113
116
 
114
- If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose"
115
- `);
117
+ If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose"
118
+ `);
116
119
  });
117
120
  });
118
121
 
@@ -234,9 +237,9 @@ describe("pages", () => {
234
237
  "pages project create a-new-project --production-branch=main"
235
238
  );
236
239
  expect(std.out).toMatchInlineSnapshot(`
237
- "✨ Successfully created the 'a-new-project' project. It will be available at https://a-new-project.pages.dev/ once you create your first deployment.
238
- To deploy a folder of assets, run 'wrangler pages publish [directory]'."
239
- `);
240
+ "✨ Successfully created the 'a-new-project' project. It will be available at https://a-new-project.pages.dev/ once you create your first deployment.
241
+ To deploy a folder of assets, run 'wrangler pages publish [directory]'."
242
+ `);
240
243
  });
241
244
  });
242
245
 
@@ -312,26 +315,26 @@ describe("pages", () => {
312
315
  await endEventLoop();
313
316
 
314
317
  expect(std.out).toMatchInlineSnapshot(`
315
- "wrangler pages publish [directory]
318
+ "wrangler pages publish [directory]
316
319
 
317
- 🆙 Publish a directory of static assets as a Pages deployment
320
+ 🆙 Publish a directory of static assets as a Pages deployment
318
321
 
319
- Positionals:
320
- directory The directory of static files to upload [string]
322
+ Positionals:
323
+ directory The directory of static files to upload [string]
321
324
 
322
- Flags:
323
- -h, --help Show help [boolean]
324
- -v, --version Show version number [boolean]
325
+ Flags:
326
+ -h, --help Show help [boolean]
327
+ -v, --version Show version number [boolean]
325
328
 
326
- Options:
327
- --project-name The name of the project you want to deploy to [string]
328
- --branch The name of the branch you want to deploy to [string]
329
- --commit-hash The SHA to attach to this deployment [string]
330
- --commit-message The commit message to attach to this deployment [string]
331
- --commit-dirty Whether or not the workspace should be considered dirty for this deployment [boolean]
329
+ Options:
330
+ --project-name The name of the project you want to deploy to [string]
331
+ --branch The name of the branch you want to deploy to [string]
332
+ --commit-hash The SHA to attach to this deployment [string]
333
+ --commit-message The commit message to attach to this deployment [string]
334
+ --commit-dirty Whether or not the workspace should be considered dirty for this deployment [boolean]
332
335
 
333
- 🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose"
334
- `);
336
+ 🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose"
337
+ `);
335
338
  });
336
339
 
337
340
  it("should upload a directory of files", async () => {
@@ -384,10 +387,10 @@ describe("pages", () => {
384
387
  const body = init.body as FormData;
385
388
  const manifest = JSON.parse(body.get("manifest") as string);
386
389
  expect(manifest).toMatchInlineSnapshot(`
387
- Object {
388
- "/logo.png": "2082190357cfd3617ccfe04f340c6247",
389
- }
390
- `);
390
+ Object {
391
+ "/logo.png": "2082190357cfd3617ccfe04f340c6247",
392
+ }
393
+ `);
391
394
  });
392
395
 
393
396
  return {
@@ -398,13 +401,11 @@ describe("pages", () => {
398
401
 
399
402
  await runWrangler("pages publish . --project-name=foo");
400
403
 
401
- // TODO: Unmounting somehow loses this output
402
-
403
- // expect(std.out).toMatchInlineSnapshot(`
404
- // "✨ Success! Uploaded 1 files (TIMINGS)
404
+ expect(std.out).toMatchInlineSnapshot(`
405
+ "✨ Success! Uploaded 1 files (TIMINGS)
405
406
 
406
- // ✨ Deployment complete! Take a peek over at https://abcxyz.foo.pages.dev/"
407
- // `);
407
+ ✨ Deployment complete! Take a peek over at https://abcxyz.foo.pages.dev/"
408
+ `);
408
409
  });
409
410
 
410
411
  it("should retry uploads", async () => {
@@ -455,10 +456,10 @@ describe("pages", () => {
455
456
  const body = init.body as FormData;
456
457
  const manifest = JSON.parse(body.get("manifest") as string);
457
458
  expect(manifest).toMatchInlineSnapshot(`
458
- Object {
459
- "/logo.txt": "1a98fb08af91aca4a7df1764a2c4ddb0",
460
- }
461
- `);
459
+ Object {
460
+ "/logo.txt": "1a98fb08af91aca4a7df1764a2c4ddb0",
461
+ }
462
+ `);
462
463
  });
463
464
 
464
465
  return {
@@ -492,10 +493,10 @@ describe("pages", () => {
492
493
  }
493
494
 
494
495
  expect(std.out).toMatchInlineSnapshot(`
495
- "✨ Success! Uploaded 1 files (TIMINGS)
496
+ "✨ Success! Uploaded 1 files (TIMINGS)
496
497
 
497
- ✨ Deployment complete! Take a peek over at https://abcxyz.foo.pages.dev/"
498
- `);
498
+ ✨ Deployment complete! Take a peek over at https://abcxyz.foo.pages.dev/"
499
+ `);
499
500
  });
500
501
 
501
502
  it("should refetch a JWT if it expires while uploading", async () => {
@@ -549,10 +550,10 @@ describe("pages", () => {
549
550
  const body = init.body as FormData;
550
551
  const manifest = JSON.parse(body.get("manifest") as string);
551
552
  expect(manifest).toMatchInlineSnapshot(`
552
- Object {
553
- "/logo.txt": "1a98fb08af91aca4a7df1764a2c4ddb0",
554
- }
555
- `);
553
+ Object {
554
+ "/logo.txt": "1a98fb08af91aca4a7df1764a2c4ddb0",
555
+ }
556
+ `);
556
557
  });
557
558
 
558
559
  return {
@@ -589,10 +590,10 @@ describe("pages", () => {
589
590
  }
590
591
 
591
592
  expect(std.out).toMatchInlineSnapshot(`
592
- "✨ Success! Uploaded 1 files (TIMINGS)
593
+ "✨ Success! Uploaded 1 files (TIMINGS)
593
594
 
594
- ✨ Deployment complete! Take a peek over at https://abcxyz.foo.pages.dev/"
595
- `);
595
+ ✨ Deployment complete! Take a peek over at https://abcxyz.foo.pages.dev/"
596
+ `);
596
597
  });
597
598
 
598
599
  it("should try to use multiple buckets (up to the max concurrency)", async () => {
@@ -640,13 +641,13 @@ describe("pages", () => {
640
641
  const body = init.body as FormData;
641
642
  const manifest = JSON.parse(body.get("manifest") as string);
642
643
  expect(manifest).toMatchInlineSnapshot(`
643
- Object {
644
- "/logo.html": "d96fef225537c9f5e44a3cb27fd0b492",
645
- "/logo.js": "6be321bef99e758250dac034474ddbb8",
646
- "/logo.png": "2082190357cfd3617ccfe04f340c6247",
647
- "/logo.txt": "1a98fb08af91aca4a7df1764a2c4ddb0",
648
- }
649
- `);
644
+ Object {
645
+ "/logo.html": "d96fef225537c9f5e44a3cb27fd0b492",
646
+ "/logo.js": "6be321bef99e758250dac034474ddbb8",
647
+ "/logo.png": "2082190357cfd3617ccfe04f340c6247",
648
+ "/logo.txt": "1a98fb08af91aca4a7df1764a2c4ddb0",
649
+ }
650
+ `);
650
651
  });
651
652
 
652
653
  return {
@@ -699,10 +700,10 @@ describe("pages", () => {
699
700
  );
700
701
 
701
702
  expect(std.out).toMatchInlineSnapshot(`
702
- "✨ Success! Uploaded 4 files (TIMINGS)
703
+ "✨ Success! Uploaded 4 files (TIMINGS)
703
704
 
704
- ✨ Deployment complete! Take a peek over at https://abcxyz.foo.pages.dev/"
705
- `);
705
+ ✨ Deployment complete! Take a peek over at https://abcxyz.foo.pages.dev/"
706
+ `);
706
707
  });
707
708
 
708
709
  it("should resolve child directories correctly", async () => {
@@ -754,13 +755,13 @@ describe("pages", () => {
754
755
  const body = init.body as FormData;
755
756
  const manifest = JSON.parse(body.get("manifest") as string);
756
757
  expect(manifest).toMatchInlineSnapshot(`
757
- Object {
758
- "/imgs/logo.png": "2082190357cfd3617ccfe04f340c6247",
759
- "/logo.html": "d96fef225537c9f5e44a3cb27fd0b492",
760
- "/logo.js": "6be321bef99e758250dac034474ddbb8",
761
- "/logo.txt": "1a98fb08af91aca4a7df1764a2c4ddb0",
762
- }
763
- `);
758
+ Object {
759
+ "/imgs/logo.png": "2082190357cfd3617ccfe04f340c6247",
760
+ "/logo.html": "d96fef225537c9f5e44a3cb27fd0b492",
761
+ "/logo.js": "6be321bef99e758250dac034474ddbb8",
762
+ "/logo.txt": "1a98fb08af91aca4a7df1764a2c4ddb0",
763
+ }
764
+ `);
764
765
  });
765
766
 
766
767
  return {
@@ -813,10 +814,10 @@ describe("pages", () => {
813
814
  );
814
815
 
815
816
  expect(std.out).toMatchInlineSnapshot(`
816
- "✨ Success! Uploaded 4 files (TIMINGS)
817
+ "✨ Success! Uploaded 4 files (TIMINGS)
817
818
 
818
- ✨ Deployment complete! Take a peek over at https://abcxyz.foo.pages.dev/"
819
- `);
819
+ ✨ Deployment complete! Take a peek over at https://abcxyz.foo.pages.dev/"
820
+ `);
820
821
  });
821
822
 
822
823
  it("should resolve the current directory correctly", async () => {
@@ -868,13 +869,13 @@ describe("pages", () => {
868
869
  const body = init.body as FormData;
869
870
  const manifest = JSON.parse(body.get("manifest") as string);
870
871
  expect(manifest).toMatchInlineSnapshot(`
871
- Object {
872
- "/imgs/logo.png": "2082190357cfd3617ccfe04f340c6247",
873
- "/logo.html": "d96fef225537c9f5e44a3cb27fd0b492",
874
- "/logo.js": "6be321bef99e758250dac034474ddbb8",
875
- "/logo.txt": "1a98fb08af91aca4a7df1764a2c4ddb0",
876
- }
877
- `);
872
+ Object {
873
+ "/imgs/logo.png": "2082190357cfd3617ccfe04f340c6247",
874
+ "/logo.html": "d96fef225537c9f5e44a3cb27fd0b492",
875
+ "/logo.js": "6be321bef99e758250dac034474ddbb8",
876
+ "/logo.txt": "1a98fb08af91aca4a7df1764a2c4ddb0",
877
+ }
878
+ `);
878
879
  });
879
880
 
880
881
  return {
@@ -928,10 +929,10 @@ describe("pages", () => {
928
929
  );
929
930
 
930
931
  expect(std.out).toMatchInlineSnapshot(`
931
- "✨ Success! Uploaded 4 files (TIMINGS)
932
+ "✨ Success! Uploaded 4 files (TIMINGS)
932
933
 
933
- ✨ Deployment complete! Take a peek over at https://abcxyz.foo.pages.dev/"
934
- `);
934
+ ✨ Deployment complete! Take a peek over at https://abcxyz.foo.pages.dev/"
935
+ `);
935
936
  });
936
937
 
937
938
  it("should not error when directory names contain periods and houses a extensionless file", async () => {
@@ -1002,6 +1003,730 @@ describe("pages", () => {
1002
1003
  `"Pages does not support wrangler.toml"`
1003
1004
  );
1004
1005
  });
1006
+
1007
+ it("should upload a Functions project", async () => {
1008
+ // set up the directory of static files to upload.
1009
+ mkdirSync("public");
1010
+ writeFileSync("public/README.md", "This is a readme");
1011
+
1012
+ // set up /functions
1013
+ mkdirSync("functions");
1014
+ writeFileSync(
1015
+ "functions/hello.js",
1016
+ `
1017
+ export async function onRequest() {
1018
+ return new Response("Hello, world!");
1019
+ }
1020
+ `
1021
+ );
1022
+
1023
+ mockGetToken("<<funfetti-auth-jwt>>");
1024
+
1025
+ setMockResponse(
1026
+ "/pages/assets/check-missing",
1027
+ "POST",
1028
+ async (_, init) => {
1029
+ const body = JSON.parse(init.body as string) as { hashes: string[] };
1030
+ assertLater(() => {
1031
+ expect(init.headers).toMatchObject({
1032
+ Authorization: "Bearer <<funfetti-auth-jwt>>",
1033
+ });
1034
+ expect(body).toMatchObject({
1035
+ hashes: ["13a03eaf24ae98378acd36ea00f77f2f"],
1036
+ });
1037
+ });
1038
+ return body.hashes;
1039
+ }
1040
+ );
1041
+
1042
+ setMockResponse("/pages/assets/upload", "POST", async (_, init) => {
1043
+ assertLater(() => {
1044
+ expect(init.headers).toMatchObject({
1045
+ Authorization: "Bearer <<funfetti-auth-jwt>>",
1046
+ });
1047
+ const body = JSON.parse(init.body as string) as UploadPayloadFile[];
1048
+ expect(body).toMatchObject([
1049
+ {
1050
+ key: "13a03eaf24ae98378acd36ea00f77f2f",
1051
+ value: Buffer.from("This is a readme").toString("base64"),
1052
+ metadata: {
1053
+ contentType: "text/markdown",
1054
+ },
1055
+ base64: true,
1056
+ },
1057
+ ]);
1058
+ });
1059
+ });
1060
+
1061
+ setMockResponse(
1062
+ `/pages/assets/upsert-hashes`,
1063
+ "POST",
1064
+ async (_, init) => {
1065
+ assertLater(() => {
1066
+ expect(init.headers).toMatchObject({
1067
+ Authorization: "Bearer <<funfetti-auth-jwt>>",
1068
+ });
1069
+ const body = JSON.parse(init.body as string) as UploadPayloadFile[];
1070
+ expect(body).toMatchObject({
1071
+ hashes: ["13a03eaf24ae98378acd36ea00f77f2f"],
1072
+ });
1073
+ });
1074
+
1075
+ return Promise.resolve(true);
1076
+ }
1077
+ );
1078
+
1079
+ setMockResponse(
1080
+ "/accounts/:accountId/pages/projects/foo/deployments",
1081
+ async ([_url, accountId], init) => {
1082
+ assertLater(async () => {
1083
+ expect(accountId).toEqual("some-account-id");
1084
+ expect(init.method).toEqual("POST");
1085
+ const body = init.body as FormData;
1086
+ const manifest = JSON.parse(body.get("manifest") as string);
1087
+
1088
+ // for Functions projects, we auto-generate a `_worker.js` and `_routes.json`
1089
+ // file, based on the contents of `/functions`
1090
+ const generatedWorkerJS = body.get("_worker.js") as Blob;
1091
+ const generatedRoutesJSON = await (
1092
+ body.get("_routes.json") as Blob
1093
+ ).text();
1094
+
1095
+ // make sure this is all we uploaded
1096
+ expect([...body.keys()]).toEqual([
1097
+ "manifest",
1098
+ "_worker.js",
1099
+ "_routes.json",
1100
+ ]);
1101
+
1102
+ expect(manifest).toMatchInlineSnapshot(`
1103
+ Object {
1104
+ "/README.md": "13a03eaf24ae98378acd36ea00f77f2f",
1105
+ }
1106
+ `);
1107
+
1108
+ // the contents of the generated `_worker.js` file is pretty massive, so I don't
1109
+ // think snapshot testing makes much sense here. Plus, calling
1110
+ // `.toMatchInlineSnapshot()` without any arguments, in order to generate that
1111
+ // snapshot value, doesn't generate anything in this case (probably because the
1112
+ // file contents is too big). So for now, let's test that _worker.js was indeed
1113
+ // generated and that the file size is greater than zero
1114
+ expect(generatedWorkerJS).not.toBeNull();
1115
+ expect(generatedWorkerJS.size).toBeGreaterThan(0);
1116
+ const maybeRoutesJSONSpec = JSON.parse(generatedRoutesJSON);
1117
+ expect(isRoutesJSONSpec(maybeRoutesJSONSpec)).toBe(true);
1118
+ expect(maybeRoutesJSONSpec).toMatchObject({
1119
+ version: 1,
1120
+ description: `Generated by wrangler@${version}`,
1121
+ include: ["/hello"],
1122
+ exclude: [],
1123
+ });
1124
+ });
1125
+
1126
+ return {
1127
+ url: "https://abcxyz.foo.pages.dev/",
1128
+ };
1129
+ }
1130
+ );
1131
+
1132
+ await runWrangler("pages publish public --project-name=foo");
1133
+
1134
+ expect(std.out).toMatchInlineSnapshot(`
1135
+ "Compiled Worker successfully.
1136
+ ✨ Success! Uploaded 1 files (TIMINGS)
1137
+
1138
+ ✨ Uploading Functions
1139
+ ✨ Deployment complete! Take a peek over at https://abcxyz.foo.pages.dev/"
1140
+ `);
1141
+
1142
+ expect(std.err).toMatchInlineSnapshot('""');
1143
+ });
1144
+
1145
+ it("should upload an Advanced Mode project", async () => {
1146
+ // set up the directory of static files to upload.
1147
+ mkdirSync("public");
1148
+ writeFileSync("public/README.md", "This is a readme");
1149
+
1150
+ // set up _worker.js
1151
+ writeFileSync(
1152
+ "public/_worker.js",
1153
+ `
1154
+ export default {
1155
+ async fetch(request, env) {
1156
+ const url = new URL(request.url);
1157
+ return url.pathname.startsWith('/api/') ? new Response('Ok') : env.ASSETS.fetch(request);
1158
+ };
1159
+ `
1160
+ );
1161
+
1162
+ mockGetToken("<<funfetti-auth-jwt>>");
1163
+
1164
+ setMockResponse(
1165
+ "/pages/assets/check-missing",
1166
+ "POST",
1167
+ async (_, init) => {
1168
+ const body = JSON.parse(init.body as string) as { hashes: string[] };
1169
+ assertLater(() => {
1170
+ expect(init.headers).toMatchObject({
1171
+ Authorization: "Bearer <<funfetti-auth-jwt>>",
1172
+ });
1173
+ expect(body).toMatchObject({
1174
+ hashes: ["13a03eaf24ae98378acd36ea00f77f2f"],
1175
+ });
1176
+ });
1177
+ return body.hashes;
1178
+ }
1179
+ );
1180
+
1181
+ setMockResponse("/pages/assets/upload", "POST", async (_, init) => {
1182
+ assertLater(() => {
1183
+ expect(init.headers).toMatchObject({
1184
+ Authorization: "Bearer <<funfetti-auth-jwt>>",
1185
+ });
1186
+ const body = JSON.parse(init.body as string) as UploadPayloadFile[];
1187
+ expect(body).toMatchObject([
1188
+ {
1189
+ key: "13a03eaf24ae98378acd36ea00f77f2f",
1190
+ value: Buffer.from("This is a readme").toString("base64"),
1191
+ metadata: {
1192
+ contentType: "text/markdown",
1193
+ },
1194
+ base64: true,
1195
+ },
1196
+ ]);
1197
+ });
1198
+ });
1199
+
1200
+ setMockResponse(
1201
+ "/accounts/:accountId/pages/projects/foo/deployments",
1202
+ async ([_url, accountId], init) => {
1203
+ assertLater(async () => {
1204
+ expect(accountId).toEqual("some-account-id");
1205
+ expect(init.method).toEqual("POST");
1206
+ const body = init.body as FormData;
1207
+ const manifest = JSON.parse(body.get("manifest") as string);
1208
+ const customWorkerJS = await (
1209
+ body.get("_worker.js") as Blob
1210
+ ).text();
1211
+
1212
+ // make sure this is all we uploaded
1213
+ expect([...body.keys()]).toEqual(["manifest", "_worker.js"]);
1214
+
1215
+ expect(manifest).toMatchInlineSnapshot(`
1216
+ Object {
1217
+ "/README.md": "13a03eaf24ae98378acd36ea00f77f2f",
1218
+ }
1219
+ `);
1220
+
1221
+ expect(customWorkerJS).toMatchInlineSnapshot(`
1222
+ "
1223
+ export default {
1224
+ async fetch(request, env) {
1225
+ const url = new URL(request.url);
1226
+ return url.pathname.startsWith('/api/') ? new Response('Ok') : env.ASSETS.fetch(request);
1227
+ };
1228
+ "
1229
+ `);
1230
+ });
1231
+
1232
+ return {
1233
+ url: "https://abcxyz.foo.pages.dev/",
1234
+ };
1235
+ }
1236
+ );
1237
+
1238
+ await runWrangler("pages publish public --project-name=foo");
1239
+
1240
+ expect(std.out).toMatchInlineSnapshot(`
1241
+ "✨ Success! Uploaded 1 files (TIMINGS)
1242
+
1243
+ ✨ Uploading _worker.js
1244
+ ✨ Deployment complete! Take a peek over at https://abcxyz.foo.pages.dev/"
1245
+ `);
1246
+
1247
+ expect(std.err).toMatchInlineSnapshot('""');
1248
+ });
1249
+
1250
+ it("should upload _routes.json for Functions projects, if provided", async () => {
1251
+ // set up the directory of static files to upload.
1252
+ mkdirSync("public");
1253
+ writeFileSync("public/README.md", "This is a readme");
1254
+
1255
+ // set up /functions
1256
+ mkdirSync("functions");
1257
+ writeFileSync(
1258
+ "functions/hello.js",
1259
+ `
1260
+ export async function onRequest() {
1261
+ return new Response("Hello, world!");
1262
+ }
1263
+ `
1264
+ );
1265
+
1266
+ writeFileSync(
1267
+ "functions/goodbye.ts",
1268
+ `
1269
+ export async function onRequest() {
1270
+ return new Response("Bye bye!");
1271
+ }
1272
+ `
1273
+ );
1274
+
1275
+ // set up _routes.json
1276
+ writeFileSync(
1277
+ "public/_routes.json",
1278
+ `
1279
+ {
1280
+ "version": ${ROUTES_SPEC_VERSION},
1281
+ "description": "Custom _routes.json file",
1282
+ "include": ["/hello"],
1283
+ "exclude": []
1284
+ }
1285
+ `
1286
+ );
1287
+
1288
+ mockGetToken("<<funfetti-auth-jwt>>");
1289
+
1290
+ setMockResponse(
1291
+ "/pages/assets/check-missing",
1292
+ "POST",
1293
+ async (_, init) => {
1294
+ const body = JSON.parse(init.body as string) as { hashes: string[] };
1295
+ assertLater(() => {
1296
+ expect(init.headers).toMatchObject({
1297
+ Authorization: "Bearer <<funfetti-auth-jwt>>",
1298
+ });
1299
+ expect(body).toMatchObject({
1300
+ hashes: ["13a03eaf24ae98378acd36ea00f77f2f"],
1301
+ });
1302
+ });
1303
+ return body.hashes;
1304
+ }
1305
+ );
1306
+
1307
+ setMockResponse("/pages/assets/upload", "POST", async (_, init) => {
1308
+ assertLater(() => {
1309
+ expect(init.headers).toMatchObject({
1310
+ Authorization: "Bearer <<funfetti-auth-jwt>>",
1311
+ });
1312
+ const body = JSON.parse(init.body as string) as UploadPayloadFile[];
1313
+ expect(body).toMatchObject([
1314
+ {
1315
+ key: "13a03eaf24ae98378acd36ea00f77f2f",
1316
+ value: Buffer.from("This is a readme").toString("base64"),
1317
+ metadata: {
1318
+ contentType: "text/markdown",
1319
+ },
1320
+ base64: true,
1321
+ },
1322
+ ]);
1323
+ });
1324
+ });
1325
+
1326
+ setMockResponse(
1327
+ `/pages/assets/upsert-hashes`,
1328
+ "POST",
1329
+ async (_, init) => {
1330
+ assertLater(() => {
1331
+ expect(init.headers).toMatchObject({
1332
+ Authorization: "Bearer <<funfetti-auth-jwt>>",
1333
+ });
1334
+ const body = JSON.parse(init.body as string) as UploadPayloadFile[];
1335
+ expect(body).toMatchObject({
1336
+ hashes: ["13a03eaf24ae98378acd36ea00f77f2f"],
1337
+ });
1338
+ });
1339
+
1340
+ return Promise.resolve(true);
1341
+ }
1342
+ );
1343
+
1344
+ setMockResponse(
1345
+ "/accounts/:accountId/pages/projects/foo/deployments",
1346
+ async ([_url, accountId], init) => {
1347
+ assertLater(async () => {
1348
+ expect(accountId).toEqual("some-account-id");
1349
+ expect(init.method).toEqual("POST");
1350
+ const body = init.body as FormData;
1351
+ const manifest = JSON.parse(body.get("manifest") as string);
1352
+ const generatedWorkerJS = body.get("_worker.js") as Blob;
1353
+ const customRoutesJSON = await (
1354
+ body.get("_routes.json") as Blob
1355
+ ).text();
1356
+
1357
+ // make sure this is all we uploaded
1358
+ expect([...body.keys()]).toEqual([
1359
+ "manifest",
1360
+ "_worker.js",
1361
+ "_routes.json",
1362
+ ]);
1363
+
1364
+ expect(manifest).toMatchInlineSnapshot(`
1365
+ Object {
1366
+ "/README.md": "13a03eaf24ae98378acd36ea00f77f2f",
1367
+ }
1368
+ `);
1369
+
1370
+ // file content of generated `_worker.js` is too massive to snapshot test
1371
+ expect(generatedWorkerJS).not.toBeNull();
1372
+ expect(generatedWorkerJS.size).toBeGreaterThan(0);
1373
+
1374
+ expect(customRoutesJSON).toMatchInlineSnapshot(`
1375
+ "
1376
+ {
1377
+ \\"version\\": 1,
1378
+ \\"description\\": \\"Custom _routes.json file\\",
1379
+ \\"include\\": [\\"/hello\\"],
1380
+ \\"exclude\\": []
1381
+ }
1382
+ "
1383
+ `);
1384
+ });
1385
+
1386
+ return {
1387
+ url: "https://abcxyz.foo.pages.dev/",
1388
+ };
1389
+ }
1390
+ );
1391
+
1392
+ await runWrangler("pages publish public --project-name=foo");
1393
+
1394
+ expect(std.out).toMatchInlineSnapshot(`
1395
+ "Compiled Worker successfully.
1396
+ ✨ Success! Uploaded 1 files (TIMINGS)
1397
+
1398
+ ✨ Uploading Functions
1399
+ ✨ Uploading _routes.json
1400
+ ✨ Deployment complete! Take a peek over at https://abcxyz.foo.pages.dev/"
1401
+ `);
1402
+
1403
+ expect(std.warn).toMatchInlineSnapshot(`
1404
+ "▲ [WARNING] _routes.json is an experimental feature and is subject to change. Please use with care.
1405
+
1406
+ "
1407
+ `);
1408
+
1409
+ expect(std.err).toMatchInlineSnapshot('""');
1410
+ });
1411
+
1412
+ it("should not deploy Functions projects that provide an invalid custom _routes.json file", async () => {
1413
+ // set up the directory of static files to upload.
1414
+ mkdirSync("public");
1415
+ writeFileSync("public/README.md", "This is a readme");
1416
+
1417
+ // set up _routes.json
1418
+ writeFileSync(
1419
+ "public/_routes.json",
1420
+ `
1421
+ {
1422
+ "description": "Custom _routes.json file",
1423
+ "include": [],
1424
+ "exclude": []
1425
+ }
1426
+ `
1427
+ );
1428
+
1429
+ // set up /functions
1430
+ mkdirSync("functions");
1431
+ writeFileSync(
1432
+ "functions/hello.js",
1433
+ `
1434
+ export async function onRequest() {
1435
+ return new Response("Hello, world!");
1436
+ }
1437
+ `
1438
+ );
1439
+
1440
+ mockGetToken("<<funfetti-auth-jwt>>");
1441
+
1442
+ setMockResponse(
1443
+ "/pages/assets/check-missing",
1444
+ "POST",
1445
+ async (_, init) => {
1446
+ const body = JSON.parse(init.body as string) as { hashes: string[] };
1447
+ assertLater(() => {
1448
+ expect(init.headers).toMatchObject({
1449
+ Authorization: "Bearer <<funfetti-auth-jwt>>",
1450
+ });
1451
+ expect(body).toMatchObject({
1452
+ hashes: ["13a03eaf24ae98378acd36ea00f77f2f"],
1453
+ });
1454
+ });
1455
+ return body.hashes;
1456
+ }
1457
+ );
1458
+
1459
+ setMockResponse("/pages/assets/upload", "POST", async (_, init) => {
1460
+ assertLater(() => {
1461
+ expect(init.headers).toMatchObject({
1462
+ Authorization: "Bearer <<funfetti-auth-jwt>>",
1463
+ });
1464
+ const body = JSON.parse(init.body as string) as UploadPayloadFile[];
1465
+ expect(body).toMatchObject([
1466
+ {
1467
+ key: "13a03eaf24ae98378acd36ea00f77f2f",
1468
+ value: Buffer.from("This is a readme").toString("base64"),
1469
+ metadata: {
1470
+ contentType: "text/markdown",
1471
+ },
1472
+ base64: true,
1473
+ },
1474
+ ]);
1475
+ });
1476
+ });
1477
+
1478
+ await expect(runWrangler("pages publish public --project-name=foo"))
1479
+ .rejects.toThrowErrorMatchingInlineSnapshot(`
1480
+ "Invalid _routes.json file found at: public/_routes.json. Please make sure the JSON object has the following format:
1481
+ {
1482
+ version: ${ROUTES_SPEC_VERSION};
1483
+ include: string[];
1484
+ exclude: string[];
1485
+ }
1486
+ and that at least one include rule is provided.
1487
+ "
1488
+ `);
1489
+ });
1490
+
1491
+ it("should upload _routes.json for Advanced Mode projects, if provided", async () => {
1492
+ // set up the directory of static files to upload.
1493
+ mkdirSync("public");
1494
+ writeFileSync("public/README.md", "This is a readme");
1495
+
1496
+ // set up _routes.json
1497
+ writeFileSync(
1498
+ "public/_routes.json",
1499
+ `
1500
+ {
1501
+ "version": ${ROUTES_SPEC_VERSION},
1502
+ "description": "Custom _routes.json file",
1503
+ "include": ["/api/*"],
1504
+ "exclude": []
1505
+ }
1506
+ `
1507
+ );
1508
+
1509
+ // set up _worker.js
1510
+ writeFileSync(
1511
+ "public/_worker.js",
1512
+ `
1513
+ export default {
1514
+ async fetch(request, env) {
1515
+ const url = new URL(request.url);
1516
+ return url.pathname.startsWith('/api/') ? new Response('Ok') : env.ASSETS.fetch(request);
1517
+ };
1518
+ `
1519
+ );
1520
+
1521
+ mockGetToken("<<funfetti-auth-jwt>>");
1522
+
1523
+ setMockResponse(
1524
+ "/pages/assets/check-missing",
1525
+ "POST",
1526
+ async (_, init) => {
1527
+ const body = JSON.parse(init.body as string) as { hashes: string[] };
1528
+ assertLater(() => {
1529
+ expect(init.headers).toMatchObject({
1530
+ Authorization: "Bearer <<funfetti-auth-jwt>>",
1531
+ });
1532
+ expect(body).toMatchObject({
1533
+ hashes: ["13a03eaf24ae98378acd36ea00f77f2f"],
1534
+ });
1535
+ });
1536
+ return body.hashes;
1537
+ }
1538
+ );
1539
+
1540
+ setMockResponse("/pages/assets/upload", "POST", async (_, init) => {
1541
+ assertLater(() => {
1542
+ expect(init.headers).toMatchObject({
1543
+ Authorization: "Bearer <<funfetti-auth-jwt>>",
1544
+ });
1545
+ const body = JSON.parse(init.body as string) as UploadPayloadFile[];
1546
+ expect(body).toMatchObject([
1547
+ {
1548
+ key: "13a03eaf24ae98378acd36ea00f77f2f",
1549
+ value: Buffer.from("This is a readme").toString("base64"),
1550
+ metadata: {
1551
+ contentType: "text/markdown",
1552
+ },
1553
+ base64: true,
1554
+ },
1555
+ ]);
1556
+ });
1557
+ });
1558
+
1559
+ setMockResponse(
1560
+ `/pages/assets/upsert-hashes`,
1561
+ "POST",
1562
+ async (_, init) => {
1563
+ assertLater(() => {
1564
+ expect(init.headers).toMatchObject({
1565
+ Authorization: "Bearer <<funfetti-auth-jwt>>",
1566
+ });
1567
+ const body = JSON.parse(init.body as string) as UploadPayloadFile[];
1568
+ expect(body).toMatchObject({
1569
+ hashes: ["13a03eaf24ae98378acd36ea00f77f2f"],
1570
+ });
1571
+ });
1572
+
1573
+ return Promise.resolve(true);
1574
+ }
1575
+ );
1576
+
1577
+ setMockResponse(
1578
+ "/accounts/:accountId/pages/projects/foo/deployments",
1579
+ async ([_url, accountId], init) => {
1580
+ assertLater(async () => {
1581
+ expect(accountId).toEqual("some-account-id");
1582
+ expect(init.method).toEqual("POST");
1583
+ const body = init.body as FormData;
1584
+ const manifest = JSON.parse(body.get("manifest") as string);
1585
+ const customWorkerJS = await (
1586
+ body.get("_worker.js") as Blob
1587
+ ).text();
1588
+ const customRoutesJSON = await (
1589
+ body.get("_routes.json") as Blob
1590
+ ).text();
1591
+
1592
+ // make sure this is all we uploaded
1593
+ expect([...body.keys()]).toEqual([
1594
+ "manifest",
1595
+ "_worker.js",
1596
+ "_routes.json",
1597
+ ]);
1598
+
1599
+ expect(manifest).toMatchInlineSnapshot(`
1600
+ Object {
1601
+ "/README.md": "13a03eaf24ae98378acd36ea00f77f2f",
1602
+ }
1603
+ `);
1604
+
1605
+ expect(customWorkerJS).toMatchInlineSnapshot(`
1606
+ "
1607
+ export default {
1608
+ async fetch(request, env) {
1609
+ const url = new URL(request.url);
1610
+ return url.pathname.startsWith('/api/') ? new Response('Ok') : env.ASSETS.fetch(request);
1611
+ };
1612
+ "
1613
+ `);
1614
+
1615
+ expect(customRoutesJSON).toMatchInlineSnapshot(`
1616
+ "
1617
+ {
1618
+ \\"version\\": 1,
1619
+ \\"description\\": \\"Custom _routes.json file\\",
1620
+ \\"include\\": [\\"/api/*\\"],
1621
+ \\"exclude\\": []
1622
+ }
1623
+ "
1624
+ `);
1625
+ });
1626
+
1627
+ return {
1628
+ url: "https://abcxyz.foo.pages.dev/",
1629
+ };
1630
+ }
1631
+ );
1632
+
1633
+ await runWrangler("pages publish public --project-name=foo");
1634
+
1635
+ expect(std.out).toMatchInlineSnapshot(`
1636
+ "✨ Success! Uploaded 1 files (TIMINGS)
1637
+
1638
+ ✨ Uploading _worker.js
1639
+ ✨ Uploading _routes.json
1640
+ ✨ Deployment complete! Take a peek over at https://abcxyz.foo.pages.dev/"
1641
+ `);
1642
+
1643
+ expect(std.warn).toMatchInlineSnapshot(`
1644
+ "▲ [WARNING] _routes.json is an experimental feature and is subject to change. Please use with care.
1645
+
1646
+ "
1647
+ `);
1648
+ expect(std.err).toMatchInlineSnapshot(`""`);
1649
+ });
1650
+
1651
+ it("should not deploy Advanced Mode projects that provide an invalid _routes.json file", async () => {
1652
+ // set up the directory of static files to upload.
1653
+ mkdirSync("public");
1654
+ writeFileSync("public/README.md", "This is a readme");
1655
+
1656
+ // set up _routes.json
1657
+ writeFileSync(
1658
+ "public/_routes.json",
1659
+ `
1660
+ {
1661
+ "description": "Custom _routes.json file",
1662
+ "include": [],
1663
+ "exclude": []
1664
+ }
1665
+ `
1666
+ );
1667
+
1668
+ // set up _worker.js
1669
+ writeFileSync(
1670
+ "public/_worker.js",
1671
+ `
1672
+ export default {
1673
+ async fetch(request, env) {
1674
+ const url = new URL(request.url);
1675
+ return url.pathname.startsWith('/api/') ? new Response('Ok') : env.ASSETS.fetch(request);
1676
+ };
1677
+ `
1678
+ );
1679
+
1680
+ mockGetToken("<<funfetti-auth-jwt>>");
1681
+
1682
+ setMockResponse(
1683
+ "/pages/assets/check-missing",
1684
+ "POST",
1685
+ async (_, init) => {
1686
+ const body = JSON.parse(init.body as string) as { hashes: string[] };
1687
+ assertLater(() => {
1688
+ expect(init.headers).toMatchObject({
1689
+ Authorization: "Bearer <<funfetti-auth-jwt>>",
1690
+ });
1691
+ expect(body).toMatchObject({
1692
+ hashes: ["13a03eaf24ae98378acd36ea00f77f2f"],
1693
+ });
1694
+ });
1695
+ return body.hashes;
1696
+ }
1697
+ );
1698
+
1699
+ setMockResponse("/pages/assets/upload", "POST", async (_, init) => {
1700
+ assertLater(() => {
1701
+ expect(init.headers).toMatchObject({
1702
+ Authorization: "Bearer <<funfetti-auth-jwt>>",
1703
+ });
1704
+ const body = JSON.parse(init.body as string) as UploadPayloadFile[];
1705
+ expect(body).toMatchObject([
1706
+ {
1707
+ key: "13a03eaf24ae98378acd36ea00f77f2f",
1708
+ value: Buffer.from("This is a readme").toString("base64"),
1709
+ metadata: {
1710
+ contentType: "text/markdown",
1711
+ },
1712
+ base64: true,
1713
+ },
1714
+ ]);
1715
+ });
1716
+ });
1717
+
1718
+ await expect(runWrangler("pages publish public --project-name=foo"))
1719
+ .rejects.toThrowErrorMatchingInlineSnapshot(`
1720
+ "Invalid _routes.json file found at: public/_routes.json. Please make sure the JSON object has the following format:
1721
+ {
1722
+ version: ${ROUTES_SPEC_VERSION};
1723
+ include: string[];
1724
+ exclude: string[];
1725
+ }
1726
+ and that at least one include rule is provided.
1727
+ "
1728
+ `);
1729
+ });
1005
1730
  });
1006
1731
 
1007
1732
  describe("project upload", () => {
@@ -1063,10 +1788,10 @@ describe("pages", () => {
1063
1788
  await runWrangler("pages project upload .");
1064
1789
 
1065
1790
  expect(std.out).toMatchInlineSnapshot(`
1066
- "✨ Success! Uploaded 1 files (TIMINGS)
1791
+ "✨ Success! Uploaded 1 files (TIMINGS)
1067
1792
 
1068
- ✨ Upload complete!"
1069
- `);
1793
+ ✨ Upload complete!"
1794
+ `);
1070
1795
  });
1071
1796
 
1072
1797
  it("should retry uploads", async () => {
@@ -1131,10 +1856,10 @@ describe("pages", () => {
1131
1856
  }
1132
1857
 
1133
1858
  expect(std.out).toMatchInlineSnapshot(`
1134
- "✨ Success! Uploaded 1 files (TIMINGS)
1859
+ "✨ Success! Uploaded 1 files (TIMINGS)
1135
1860
 
1136
- ✨ Upload complete!"
1137
- `);
1861
+ ✨ Upload complete!"
1862
+ `);
1138
1863
  });
1139
1864
 
1140
1865
  it("should try to use multiple buckets (up to the max concurrency)", async () => {
@@ -1217,10 +1942,10 @@ describe("pages", () => {
1217
1942
  );
1218
1943
 
1219
1944
  expect(std.out).toMatchInlineSnapshot(`
1220
- "✨ Success! Uploaded 4 files (TIMINGS)
1945
+ "✨ Success! Uploaded 4 files (TIMINGS)
1221
1946
 
1222
- ✨ Upload complete!"
1223
- `);
1947
+ ✨ Upload complete!"
1948
+ `);
1224
1949
  });
1225
1950
 
1226
1951
  it("should not error when directory names contain periods and houses a extensionless file", async () => {