wrangler 2.8.0 → 2.8.1

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 (44) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/d1/d1.test.ts +12 -8
  3. package/src/__tests__/deployments.test.ts +4 -4
  4. package/src/__tests__/helpers/msw/handlers/deployments.ts +10 -18
  5. package/src/__tests__/helpers/msw/handlers/namespaces.ts +18 -41
  6. package/src/__tests__/helpers/msw/handlers/r2.ts +14 -34
  7. package/src/__tests__/helpers/msw/handlers/script.ts +9 -28
  8. package/src/__tests__/helpers/msw/handlers/user.ts +13 -24
  9. package/src/__tests__/helpers/msw/handlers/zones.ts +6 -8
  10. package/src/__tests__/pages.test.ts +34 -37
  11. package/src/__tests__/publish.test.ts +126 -0
  12. package/src/__tests__/r2.test.ts +11 -35
  13. package/src/__tests__/tail.test.ts +6 -18
  14. package/src/__tests__/tsconfig.tsbuildinfo +1 -1
  15. package/src/__tests__/user.test.ts +0 -1
  16. package/src/__tests__/whoami.test.tsx +6 -17
  17. package/src/__tests__/worker-namespace.test.ts +56 -48
  18. package/src/api/index.ts +1 -0
  19. package/src/api/pages/index.ts +5 -0
  20. package/src/api/pages/publish.tsx +321 -0
  21. package/src/bundle.ts +62 -10
  22. package/src/cli.ts +2 -2
  23. package/src/config/environment.ts +12 -10
  24. package/src/d1/utils.ts +1 -1
  25. package/src/deployments.ts +16 -6
  26. package/src/dev/local.tsx +1 -10
  27. package/src/dev/start-server.ts +5 -10
  28. package/src/dev/use-esbuild.ts +1 -0
  29. package/src/entry.ts +1 -2
  30. package/src/index.ts +1 -1
  31. package/src/metrics/send-event.ts +2 -1
  32. package/src/pages/build.ts +4 -124
  33. package/src/pages/buildFunctions.ts +129 -0
  34. package/src/pages/dev.ts +12 -2
  35. package/src/pages/functions/buildPlugin.ts +1 -0
  36. package/src/pages/functions/buildWorker.ts +8 -2
  37. package/src/pages/functions/tsconfig.tsbuildinfo +1 -1
  38. package/src/pages/publish.tsx +9 -235
  39. package/src/publish/publish.ts +1 -0
  40. package/templates/d1-beta-facade.js +1 -1
  41. package/templates/middleware/loader-modules.ts +2 -0
  42. package/templates/tsconfig.tsbuildinfo +1 -1
  43. package/wrangler-dist/cli.d.ts +132 -10
  44. package/wrangler-dist/cli.js +486 -388
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wrangler",
3
- "version": "2.8.0",
3
+ "version": "2.8.1",
4
4
  "description": "Command-line interface for all things Cloudflare Workers",
5
5
  "keywords": [
6
6
  "wrangler",
@@ -37,10 +37,12 @@ describe("d1", () => {
37
37
  -h, --help Show help [boolean]
38
38
  -v, --version Show version number [boolean]
39
39
 
40
- 🚧 D1 is currently in open alpha and is not recommended for production data and traffic.
41
- Please report any bugs to https://github.com/cloudflare/wrangler2/issues/new/choose.
42
- To request features, visit https://community.cloudflare.com/c/developers/d1.
43
- To give feedback, visit https://discord.gg/cloudflaredev"
40
+ --------------------
41
+ 🚧 D1 is currently in open alpha and is not recommended for production data and traffic
42
+ 🚧 Please report any bugs to https://github.com/cloudflare/wrangler2/issues/new/choose
43
+ 🚧 To request features, visit https://community.cloudflare.com/c/developers/d1
44
+ 🚧 To give feedback, visit https://discord.gg/cloudflaredev
45
+ --------------------"
44
46
  `);
45
47
  });
46
48
 
@@ -74,10 +76,12 @@ describe("d1", () => {
74
76
  -h, --help Show help [boolean]
75
77
  -v, --version Show version number [boolean]
76
78
 
77
- 🚧 D1 is currently in open alpha and is not recommended for production data and traffic.
78
- Please report any bugs to https://github.com/cloudflare/wrangler2/issues/new/choose.
79
- To request features, visit https://community.cloudflare.com/c/developers/d1.
80
- To give feedback, visit https://discord.gg/cloudflaredev"
79
+ --------------------
80
+ 🚧 D1 is currently in open alpha and is not recommended for production data and traffic
81
+ 🚧 Please report any bugs to https://github.com/cloudflare/wrangler2/issues/new/choose
82
+ 🚧 To request features, visit https://community.cloudflare.com/c/developers/d1
83
+ 🚧 To give feedback, visit https://discord.gg/cloudflaredev
84
+ --------------------"
81
85
  `);
82
86
  });
83
87
 
@@ -68,12 +68,12 @@ describe("deployments", () => {
68
68
  Deployment ID: Galaxy-Class
69
69
  Created on: 2021-01-01T00:00:00.000000Z
70
70
  Author: Jean-Luc-Picard@federation.org
71
- Source: Wrangler
71
+ Source: 🤠 Wrangler
72
72
 
73
73
  Deployment ID: Intrepid-Class
74
74
  Created on: 2021-02-02T00:00:00.000000Z
75
75
  Author: Kathryn-Janeway@federation.org
76
- Source: Wrangler
76
+ Source: 🤠 Wrangler
77
77
  🟩 Active"
78
78
  `);
79
79
  });
@@ -87,12 +87,12 @@ describe("deployments", () => {
87
87
  Deployment ID: Galaxy-Class
88
88
  Created on: 2021-01-01T00:00:00.000000Z
89
89
  Author: Jean-Luc-Picard@federation.org
90
- Source: Wrangler
90
+ Source: 🤠 Wrangler
91
91
 
92
92
  Deployment ID: Intrepid-Class
93
93
  Created on: 2021-02-02T00:00:00.000000Z
94
94
  Author: Kathryn-Janeway@federation.org
95
- Source: Wrangler
95
+ Source: 🤠 Wrangler
96
96
  🟩 Active"
97
97
  `);
98
98
  });
@@ -1,18 +1,13 @@
1
1
  import { rest } from "msw";
2
-
3
- import type { DeploymentListRes } from "../../../../deployments";
2
+ import { createFetchResult } from "../index";
4
3
 
5
4
  export const mswSuccessDeployments = [
6
5
  rest.get(
7
6
  "*/accounts/:accountId/workers/deployments/by-script/:scriptTag",
8
7
  (_, response, context) =>
9
8
  response.once(
10
- context.status(200),
11
- context.json({
12
- success: true,
13
- errors: [],
14
- messages: [],
15
- result: {
9
+ context.json(
10
+ createFetchResult({
16
11
  latest: {
17
12
  id: "Galaxy-Class",
18
13
  number: "1701-E",
@@ -52,8 +47,8 @@ export const mswSuccessDeployments = [
52
47
  },
53
48
  },
54
49
  ],
55
- } as DeploymentListRes,
56
- })
50
+ })
51
+ )
57
52
  )
58
53
  ),
59
54
  ];
@@ -61,16 +56,13 @@ export const mswSuccessDeployments = [
61
56
  export const mswSuccessLastDeployment = [
62
57
  rest.get(
63
58
  "*/accounts/:accountId/workers/services/:scriptName",
64
- (req, res, ctx) => {
59
+ (_, res, ctx) => {
65
60
  return res.once(
66
- ctx.json({
67
- success: true,
68
- messages: [],
69
- errors: [],
70
- result: {
61
+ ctx.json(
62
+ createFetchResult({
71
63
  default_environment: { script: { last_deployed_from: "wrangler" } },
72
- },
73
- })
64
+ })
65
+ )
74
66
  );
75
67
  }
76
68
  ),
@@ -1,59 +1,44 @@
1
1
  import { rest } from "msw";
2
+ import { createFetchResult } from "../index";
2
3
 
3
4
  export const mswSuccessNamespacesHandlers = [
4
5
  rest.post(
5
6
  "*/accounts/:accountId/workers/dispatch/namespaces",
6
7
  (_, response, context) => {
7
8
  return response.once(
8
- context.status(200),
9
- context.json({
10
- success: true,
11
- errors: [],
12
- messages: [],
13
- result: {
9
+ context.json(
10
+ createFetchResult({
14
11
  namespace_id: "some-namespace-id",
15
12
  namespace_name: "namespace-name",
16
13
  created_on: "2022-06-29T14:30:08.16152Z",
17
14
  created_by: "1fc1df98cc4420fe00367c3ab68c1639",
18
15
  modified_on: "2022-06-29T14:30:08.16152Z",
19
16
  modified_by: "1fc1df98cc4420fe00367c3ab68c1639",
20
- },
21
- })
17
+ })
18
+ )
22
19
  );
23
20
  }
24
21
  ),
25
22
  rest.delete(
26
23
  "*/accounts/:accountId/workers/dispatch/namespaces/:namespaceName",
27
24
  (_, response, context) => {
28
- return response.once(
29
- context.status(200),
30
- context.json({
31
- success: true,
32
- errors: [],
33
- messages: [],
34
- result: null,
35
- })
36
- );
25
+ return response.once(context.json(createFetchResult(null)));
37
26
  }
38
27
  ),
39
28
  rest.put(
40
29
  "*/accounts/:accountId/workers/dispatch/namespaces/:namespaceName",
41
30
  (_, response, context) => {
42
31
  return response.once(
43
- context.status(200),
44
- context.json({
45
- success: true,
46
- errors: [],
47
- messages: [],
48
- result: {
32
+ context.json(
33
+ createFetchResult({
49
34
  namespace_id: "some-namespace-id",
50
35
  namespace_name: "namespace-name",
51
36
  created_on: "2022-06-29T14:30:08.16152Z",
52
37
  created_by: "1fc1df98cc4420fe00367c3ab68c1639",
53
38
  modified_on: "2022-06-29T14:30:08.16152Z",
54
39
  modified_by: "1fc1df98cc4420fe00367c3ab68c1639",
55
- },
56
- })
40
+ })
41
+ )
57
42
  );
58
43
  }
59
44
  ),
@@ -61,20 +46,16 @@ export const mswSuccessNamespacesHandlers = [
61
46
  "*/accounts/:accountId/workers/dispatch/namespaces/:namespaceName",
62
47
  (_, response, context) => {
63
48
  return response.once(
64
- context.status(200),
65
- context.json({
66
- success: true,
67
- errors: [],
68
- messages: [],
69
- result: {
49
+ context.json(
50
+ createFetchResult({
70
51
  namespace_id: "some-namespace-id",
71
52
  namespace_name: "namespace-name",
72
53
  created_on: "2022-06-29T14:30:08.16152Z",
73
54
  created_by: "1fc1df98cc4420fe00367c3ab68c1639",
74
55
  modified_on: "2022-06-29T14:30:08.16152Z",
75
56
  modified_by: "1fc1df98cc4420fe00367c3ab68c1639",
76
- },
77
- })
57
+ })
58
+ )
78
59
  );
79
60
  }
80
61
  ),
@@ -82,12 +63,8 @@ export const mswSuccessNamespacesHandlers = [
82
63
  "*/accounts/:accountId/workers/dispatch/namespaces",
83
64
  (_, response, context) => {
84
65
  return response.once(
85
- context.status(200),
86
- context.json({
87
- success: true,
88
- errors: [],
89
- messages: [],
90
- result: [
66
+ context.json(
67
+ createFetchResult([
91
68
  {
92
69
  namespace_id: "some-namespace-id",
93
70
  namespace_name: "namespace-name",
@@ -96,8 +73,8 @@ export const mswSuccessNamespacesHandlers = [
96
73
  modified_on: "2022-06-29T14:30:08.16152Z",
97
74
  modified_by: "1fc1df98cc4420fe00367c3ab68c1639",
98
75
  },
99
- ],
100
- })
76
+ ])
77
+ )
101
78
  );
102
79
  }
103
80
  ),
@@ -1,44 +1,31 @@
1
1
  import { rest } from "msw";
2
+ import { createFetchResult } from "../index";
2
3
 
3
4
  export const mswSuccessR2handlers = [
4
5
  // List endpoint r2Buckets
5
6
  rest.get("*/accounts/:accountId/r2/buckets", (_, response, context) =>
6
7
  response.once(
7
- context.status(200),
8
- context.json({
9
- success: true,
10
- errors: [],
11
- messages: [],
12
- result: {
8
+ context.json(
9
+ createFetchResult({
13
10
  buckets: [
14
11
  { name: "bucket-1", creation_date: "01-01-2001" },
15
12
  { name: "bucket-2", creation_date: "01-01-2001" },
16
13
  ],
17
- },
18
- })
14
+ })
15
+ )
19
16
  )
20
17
  ),
21
18
  rest.post("*/accounts/:accountId/r2/buckets", (_, response, context) =>
22
- response.once(
23
- context.status(200),
24
- context.json({ success: true, errors: [], messages: [], result: {} })
25
- )
19
+ response.once(context.json(createFetchResult({})))
26
20
  ),
27
21
  rest.put(
28
22
  "*/accounts/:accountId/r2/buckets/:bucketName",
29
- (_, response, context) =>
30
- response.once(
31
- context.status(200),
32
- context.json({ success: true, errors: [], messages: [], result: {} })
33
- )
23
+ (_, response, context) => response.once(context.json(createFetchResult({})))
34
24
  ),
35
25
  rest.delete(
36
26
  "*/accounts/:accountId/r2/buckets/:bucketName",
37
27
  (_, response, context) =>
38
- response.once(
39
- context.status(200),
40
- context.json({ success: true, errors: [], messages: [], result: null })
41
- )
28
+ response.once(context.json(createFetchResult(null)))
42
29
  ),
43
30
  rest.get(
44
31
  "*/accounts/:accountId/r2/buckets/:bucketName/objects/:objectName",
@@ -47,7 +34,7 @@ export const mswSuccessR2handlers = [
47
34
  return response.once(
48
35
  context.set("Content-Length", imageBuffer.byteLength.toString()),
49
36
  context.set("Content-Type", "image/png"),
50
- context.status(200),
37
+
51
38
  context.body(imageBuffer)
52
39
  );
53
40
  }
@@ -56,25 +43,18 @@ export const mswSuccessR2handlers = [
56
43
  "*/accounts/:accountId/r2/buckets/:bucketName/objects/:objectName",
57
44
  (_, response, context) =>
58
45
  response.once(
59
- context.status(200),
60
- context.json({
61
- success: true,
62
- errors: [],
63
- messages: [],
64
- result: {
46
+ context.json(
47
+ createFetchResult({
65
48
  accountId: "some-account-id",
66
49
  bucketName: "bucketName-object-test",
67
50
  objectName: "wormhole-img.png",
68
- },
69
- })
51
+ })
52
+ )
70
53
  )
71
54
  ),
72
55
  rest.delete(
73
56
  "*/accounts/:accountId/r2/buckets/:bucketName/objects/:objectName",
74
57
  (_, response, context) =>
75
- response.once(
76
- context.status(200),
77
- context.json({ success: true, errors: [], messages: [], result: null })
78
- )
58
+ response.once(context.json(createFetchResult(null)))
79
59
  ),
80
60
  ];
@@ -1,4 +1,5 @@
1
1
  import { rest } from "msw";
2
+ import { createFetchResult } from "../index";
2
3
  import type { WorkerMetadata } from "../../../../create-worker-upload-form";
3
4
 
4
5
  const bindings: Record<string, WorkerMetadata["bindings"]> = {
@@ -14,10 +15,12 @@ const scripts: Record<string, string> = {
14
15
  websocket: `new WebSocket("ws://dummy")`,
15
16
  response: `return new Response("ok")`,
16
17
  };
17
- function getBindings(scriptName: string) {
18
+ function getBindings(scriptName: string | readonly string[]) {
19
+ if (typeof scriptName !== "string") return "";
18
20
  return scriptName.split("--").flatMap((part) => bindings[part] ?? []);
19
21
  }
20
- function getScript(scriptName: string) {
22
+ function getScript(scriptName: string | readonly string[]): string {
23
+ if (typeof scriptName !== "string") return "";
21
24
  return `export default {fetch(request){
22
25
  ${scriptName
23
26
  .split("--")
@@ -29,47 +32,25 @@ export default [
29
32
  rest.get(
30
33
  "*/accounts/:accountId/workers/services/:scriptName/environments/:env/content",
31
34
  ({ params: { scriptName } }, res, context) => {
32
- return res(
33
- context.status(200),
34
- context.text(getScript(scriptName as string))
35
- );
35
+ return res(context.text(getScript(scriptName)));
36
36
  }
37
37
  ),
38
38
  rest.get(
39
39
  "*/accounts/:accountId/workers/scripts/:scriptName",
40
40
  ({ params: { scriptName } }, res, context) => {
41
- return res(
42
- context.status(200),
43
- context.text(getScript(scriptName as string))
44
- );
41
+ return res(context.text(getScript(scriptName)));
45
42
  }
46
43
  ),
47
44
  rest.get(
48
45
  "*/accounts/:accountId/workers/services/:scriptName/environments/:env/bindings",
49
46
  ({ params: { scriptName } }, res, context) => {
50
- return res(
51
- context.status(200),
52
- context.json({
53
- success: true,
54
- errors: [],
55
- messages: [],
56
- result: getBindings(scriptName as string),
57
- })
58
- );
47
+ return res(context.json(createFetchResult(getBindings(scriptName))));
59
48
  }
60
49
  ),
61
50
  rest.get(
62
51
  "*/accounts/:accountId/workers/scripts/:scriptName/bindings",
63
52
  ({ params: { scriptName } }, res, context) => {
64
- return res(
65
- context.status(200),
66
- context.json({
67
- success: true,
68
- errors: [],
69
- messages: [],
70
- result: getBindings(scriptName as string),
71
- })
72
- );
53
+ return res(context.json(createFetchResult(getBindings(scriptName))));
73
54
  }
74
55
  ),
75
56
  ];
@@ -1,14 +1,11 @@
1
1
  import { rest } from "msw";
2
+ import { createFetchResult } from "../index";
2
3
 
3
4
  export const mswSuccessUserHandlers = [
4
5
  rest.get("*/user", (_, response, cxt) => {
5
6
  return response.once(
6
- cxt.status(200),
7
- cxt.json({
8
- success: true,
9
- errors: [],
10
- messages: [],
11
- result: {
7
+ cxt.json(
8
+ createFetchResult({
12
9
  id: "7c5dae5552338874e5053f2534d2767a",
13
10
  email: "user@example.com",
14
11
  first_name: "John",
@@ -21,33 +18,25 @@ export const mswSuccessUserHandlers = [
21
18
  modified_on: "2014-01-01T05:20:00Z",
22
19
  two_factor_authentication_enabled: false,
23
20
  suspended: false,
24
- },
25
- })
21
+ })
22
+ )
26
23
  );
27
24
  }),
28
25
  rest.get("*/accounts", (_, response, cxt) => {
29
26
  return response.once(
30
- cxt.status(200),
31
- cxt.json({
32
- success: true,
33
- errors: [],
34
- messages: [],
35
- result: [
27
+ cxt.json(
28
+ createFetchResult([
36
29
  { name: "Account One", id: "account-1" },
37
30
  { name: "Account Two", id: "account-2" },
38
31
  { name: "Account Three", id: "account-3" },
39
- ],
40
- })
32
+ ])
33
+ )
41
34
  );
42
35
  }),
43
36
  rest.get("*/memberships", (_, response, context) => {
44
37
  return response.once(
45
- context.status(200),
46
- context.json({
47
- success: true,
48
- errors: [],
49
- messages: [],
50
- result: [
38
+ context.json(
39
+ createFetchResult([
51
40
  {
52
41
  id: "membership-id-1",
53
42
  account: { id: "account-id-1", name: "My Personal Account" },
@@ -56,8 +45,8 @@ export const mswSuccessUserHandlers = [
56
45
  id: "membership-id-2",
57
46
  account: { id: "account-id-2", name: "Enterprise Account" },
58
47
  },
59
- ],
60
- })
48
+ ])
49
+ )
61
50
  );
62
51
  }),
63
52
  ];
@@ -1,22 +1,20 @@
1
1
  import { rest } from "msw";
2
+ import { createFetchResult } from "../index";
2
3
 
3
4
  export default [
4
5
  rest.get("*/zones", ({ url: { searchParams } }, res, context) => {
5
6
  return res(
6
- context.status(200),
7
- context.json({
8
- success: true,
9
- errors: [],
10
- messages: [],
11
- result:
7
+ context.json(
8
+ createFetchResult(
12
9
  searchParams.get("name") === "exists.com"
13
10
  ? [
14
11
  {
15
12
  id: "exists-com",
16
13
  },
17
14
  ]
18
- : [],
19
- })
15
+ : []
16
+ )
17
+ )
20
18
  );
21
19
  }),
22
20
  ];
@@ -2543,19 +2543,20 @@ and that at least one include rule is provided.
2543
2543
  );
2544
2544
 
2545
2545
  await runWrangler("pages project upload .");
2546
-
2547
2546
  expect(requests.length).toBe(3);
2548
2547
 
2549
- const resolvedRequests = await Promise.all(
2550
- requests.map(async (req) => await req.json<UploadPayloadFile>())
2551
- );
2552
-
2553
- const sortedRequests = resolvedRequests.sort((a, b) => {
2554
- const aKey = a.key as string;
2555
- const bKey = b.key as string;
2548
+ const resolvedRequests = (
2549
+ await Promise.all(
2550
+ requests.map(async (req) => await req.json<UploadPayloadFile[]>())
2551
+ )
2552
+ ).flat();
2556
2553
 
2557
- return aKey?.localeCompare(bKey);
2558
- });
2554
+ const requestMap = resolvedRequests.reduce<{
2555
+ [key: string]: UploadPayloadFile;
2556
+ }>(
2557
+ (requestMap, req) => Object.assign(requestMap, { [req.key]: req }),
2558
+ {}
2559
+ );
2559
2560
 
2560
2561
  for (const req of requests) {
2561
2562
  expect(req.headers.get("Authorization")).toBe(
@@ -2563,38 +2564,34 @@ and that at least one include rule is provided.
2563
2564
  );
2564
2565
  }
2565
2566
 
2566
- expect(sortedRequests[0]).toMatchObject([
2567
- {
2568
- base64: true,
2569
- key: "95dedb64e6d4940fc2e0f11f711cc2f4",
2570
- metadata: {
2571
- contentType: "application/octet-stream",
2572
- },
2573
- value: "aGVhZGVyc2ZpbGU=",
2567
+ expect(Object.keys(requestMap).length).toBe(3);
2568
+
2569
+ expect(requestMap["95dedb64e6d4940fc2e0f11f711cc2f4"]).toMatchObject({
2570
+ base64: true,
2571
+ key: "95dedb64e6d4940fc2e0f11f711cc2f4",
2572
+ metadata: {
2573
+ contentType: "application/octet-stream",
2574
2574
  },
2575
- ]);
2575
+ value: "aGVhZGVyc2ZpbGU=",
2576
+ });
2576
2577
 
2577
- expect(sortedRequests[1]).toMatchObject([
2578
- {
2579
- base64: true,
2580
- key: "2082190357cfd3617ccfe04f340c6247",
2581
- metadata: {
2582
- contentType: "image/png",
2583
- },
2584
- value: "Zm9vYmFy",
2578
+ expect(requestMap["2082190357cfd3617ccfe04f340c6247"]).toMatchObject({
2579
+ base64: true,
2580
+ key: "2082190357cfd3617ccfe04f340c6247",
2581
+ metadata: {
2582
+ contentType: "image/png",
2585
2583
  },
2586
- ]);
2584
+ value: "Zm9vYmFy",
2585
+ });
2587
2586
 
2588
- expect(sortedRequests[2]).toMatchObject([
2589
- {
2590
- base64: true,
2591
- key: "09a79777abda8ccc8bdd51dd3ff8e9e9",
2592
- metadata: {
2593
- contentType: "application/javascript",
2594
- },
2595
- value: "ZnVuYw==",
2587
+ expect(requestMap["09a79777abda8ccc8bdd51dd3ff8e9e9"]).toMatchObject({
2588
+ base64: true,
2589
+ key: "09a79777abda8ccc8bdd51dd3ff8e9e9",
2590
+ metadata: {
2591
+ contentType: "application/javascript",
2596
2592
  },
2597
- ]);
2593
+ value: "ZnVuYw==",
2594
+ });
2598
2595
 
2599
2596
  expect(std.out).toMatchInlineSnapshot(`
2600
2597
  "✨ Success! Uploaded 3 files (TIMINGS)