wrangler 0.0.0-e2a26a0 → 0.0.0-e3278fa

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 (150) hide show
  1. package/Cloudflare_CA.pem +18 -0
  2. package/README.md +27 -14
  3. package/bin/wrangler.js +49 -5
  4. package/miniflare-dist/index.mjs +4886 -0
  5. package/package.json +51 -27
  6. package/pages/functions/buildPlugin.ts +105 -0
  7. package/pages/functions/buildWorker.ts +93 -4
  8. package/pages/functions/filepath-routing.test.ts +223 -36
  9. package/pages/functions/filepath-routing.ts +78 -120
  10. package/pages/functions/routes.ts +13 -17
  11. package/pages/functions/template-plugin.ts +147 -0
  12. package/pages/functions/template-worker.ts +24 -17
  13. package/src/__tests__/config-cache.test.ts +36 -0
  14. package/src/__tests__/configuration.test.ts +3408 -0
  15. package/src/__tests__/dev.test.tsx +977 -65
  16. package/src/__tests__/guess-worker-format.test.ts +85 -0
  17. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  18. package/src/__tests__/helpers/mock-account-id.ts +26 -8
  19. package/src/__tests__/helpers/mock-bin.ts +4 -1
  20. package/src/__tests__/helpers/mock-cfetch.ts +10 -6
  21. package/src/__tests__/helpers/mock-console.ts +39 -8
  22. package/src/__tests__/helpers/mock-dialogs.ts +67 -2
  23. package/src/__tests__/helpers/mock-http-server.ts +46 -0
  24. package/src/__tests__/helpers/mock-istty.ts +27 -0
  25. package/src/__tests__/helpers/mock-kv.ts +9 -16
  26. package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
  27. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  28. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  29. package/src/__tests__/helpers/run-in-tmp.ts +10 -0
  30. package/src/__tests__/helpers/run-wrangler.ts +5 -3
  31. package/src/__tests__/helpers/write-worker-source.ts +31 -0
  32. package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
  33. package/src/__tests__/https-options.test.ts +130 -0
  34. package/src/__tests__/index.test.ts +148 -435
  35. package/src/__tests__/init.test.ts +1637 -0
  36. package/src/__tests__/jest.setup.ts +45 -5
  37. package/src/__tests__/kv.test.ts +590 -248
  38. package/src/__tests__/logger.test.ts +115 -0
  39. package/src/__tests__/package-manager.test.ts +156 -9
  40. package/src/__tests__/pages.test.ts +754 -0
  41. package/src/__tests__/parse.test.ts +330 -0
  42. package/src/__tests__/publish.test.ts +5471 -939
  43. package/src/__tests__/r2.test.ts +233 -0
  44. package/src/__tests__/route.test.ts +45 -0
  45. package/src/__tests__/secret.test.ts +362 -91
  46. package/src/__tests__/tail.test.ts +690 -0
  47. package/src/__tests__/user.test.ts +131 -0
  48. package/src/__tests__/whoami.test.tsx +60 -7
  49. package/src/abort.d.ts +3 -0
  50. package/src/bundle.ts +197 -0
  51. package/src/cfetch/index.ts +27 -8
  52. package/src/cfetch/internal.ts +73 -32
  53. package/src/cli.ts +6 -2
  54. package/src/config/README.md +107 -0
  55. package/src/config/config.ts +234 -0
  56. package/src/config/diagnostics.ts +80 -0
  57. package/src/config/environment.ts +420 -0
  58. package/src/config/index.ts +225 -0
  59. package/src/config/validation-helpers.ts +584 -0
  60. package/src/config/validation.ts +1539 -0
  61. package/src/config-cache.ts +45 -0
  62. package/src/create-worker-preview.ts +185 -0
  63. package/src/create-worker-upload-form.ts +254 -0
  64. package/src/dev/dev-vars.ts +36 -0
  65. package/src/dev/dev.tsx +412 -0
  66. package/src/dev/local.tsx +322 -0
  67. package/src/dev/remote.tsx +237 -0
  68. package/src/dev/use-esbuild.ts +118 -0
  69. package/src/dialogs.tsx +64 -6
  70. package/src/durable.ts +102 -0
  71. package/src/entry.ts +322 -0
  72. package/src/environment-variables.ts +35 -0
  73. package/src/errors.ts +11 -0
  74. package/src/https-options.ts +123 -0
  75. package/src/index.tsx +1747 -1002
  76. package/src/inspect.ts +80 -14
  77. package/src/intl-polyfill.d.ts +139 -0
  78. package/src/{kv.tsx → kv.ts} +192 -68
  79. package/src/logger.ts +66 -0
  80. package/src/miniflare-cli/README.md +30 -0
  81. package/src/miniflare-cli/enum-keys.ts +17 -0
  82. package/src/miniflare-cli/index.ts +41 -0
  83. package/src/miniflare-cli/request-context.ts +40 -0
  84. package/src/module-collection.ts +226 -31
  85. package/src/open-in-browser.ts +19 -0
  86. package/src/package-manager.ts +79 -25
  87. package/src/pages.tsx +1131 -84
  88. package/src/parse.ts +202 -0
  89. package/src/proxy.ts +170 -62
  90. package/src/publish.ts +612 -306
  91. package/src/r2.ts +50 -0
  92. package/src/selfsigned.d.ts +29 -0
  93. package/src/sites.tsx +143 -57
  94. package/src/tail/filters.ts +279 -0
  95. package/src/tail/index.ts +299 -0
  96. package/src/tail/printing.ts +67 -0
  97. package/src/update-check.ts +19 -0
  98. package/src/user.tsx +341 -146
  99. package/src/whoami.tsx +5 -3
  100. package/src/{api/worker.ts → worker.ts} +65 -39
  101. package/src/zones.ts +73 -0
  102. package/templates/gitignore +171 -0
  103. package/templates/new-worker-scheduled.js +17 -0
  104. package/templates/new-worker-scheduled.ts +32 -0
  105. package/templates/new-worker.js +1 -1
  106. package/templates/new-worker.ts +17 -2
  107. package/templates/static-asset-facade.js +1 -5
  108. package/templates/tsconfig.json +105 -0
  109. package/wrangler-dist/cli.js +72295 -71006
  110. package/src/__tests__/helpers/mock-user.ts +0 -27
  111. package/src/__tests__/logout.test.ts +0 -50
  112. package/src/api/form_data.ts +0 -134
  113. package/src/api/preview.ts +0 -128
  114. package/src/config.ts +0 -641
  115. package/src/dev.tsx +0 -834
  116. package/src/tail.tsx +0 -73
  117. package/vendor/@cloudflare/kv-asset-handler/CHANGELOG.md +0 -332
  118. package/vendor/@cloudflare/kv-asset-handler/LICENSE_APACHE +0 -176
  119. package/vendor/@cloudflare/kv-asset-handler/LICENSE_MIT +0 -25
  120. package/vendor/@cloudflare/kv-asset-handler/README.md +0 -245
  121. package/vendor/@cloudflare/kv-asset-handler/dist/index.d.ts +0 -32
  122. package/vendor/@cloudflare/kv-asset-handler/dist/index.js +0 -354
  123. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.d.ts +0 -13
  124. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.js +0 -148
  125. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.d.ts +0 -1
  126. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.js +0 -436
  127. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.d.ts +0 -1
  128. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.js +0 -40
  129. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.d.ts +0 -1
  130. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.js +0 -42
  131. package/vendor/@cloudflare/kv-asset-handler/dist/types.d.ts +0 -26
  132. package/vendor/@cloudflare/kv-asset-handler/dist/types.js +0 -31
  133. package/vendor/@cloudflare/kv-asset-handler/package.json +0 -52
  134. package/vendor/@cloudflare/kv-asset-handler/src/index.ts +0 -296
  135. package/vendor/@cloudflare/kv-asset-handler/src/mocks.ts +0 -136
  136. package/vendor/@cloudflare/kv-asset-handler/src/test/getAssetFromKV.ts +0 -464
  137. package/vendor/@cloudflare/kv-asset-handler/src/test/mapRequestToAsset.ts +0 -33
  138. package/vendor/@cloudflare/kv-asset-handler/src/test/serveSinglePageApp.ts +0 -42
  139. package/vendor/@cloudflare/kv-asset-handler/src/types.ts +0 -39
  140. package/vendor/wrangler-mime/CHANGELOG.md +0 -289
  141. package/vendor/wrangler-mime/LICENSE +0 -21
  142. package/vendor/wrangler-mime/Mime.js +0 -97
  143. package/vendor/wrangler-mime/README.md +0 -187
  144. package/vendor/wrangler-mime/cli.js +0 -46
  145. package/vendor/wrangler-mime/index.js +0 -4
  146. package/vendor/wrangler-mime/lite.js +0 -4
  147. package/vendor/wrangler-mime/package.json +0 -52
  148. package/vendor/wrangler-mime/types/other.js +0 -1
  149. package/vendor/wrangler-mime/types/standard.js +0 -1
  150. package/wrangler-dist/cli.js.map +0 -7
@@ -0,0 +1,131 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import fetchMock from "jest-fetch-mock";
5
+ import {
6
+ readAuthConfigFile,
7
+ requireAuth,
8
+ USER_AUTH_CONFIG_FILE,
9
+ writeAuthConfigFile,
10
+ } from "../user";
11
+ import { mockConsoleMethods } from "./helpers/mock-console";
12
+ import { useMockIsTTY } from "./helpers/mock-istty";
13
+ import { mockOAuthFlow } from "./helpers/mock-oauth-flow";
14
+ import { runInTempDir } from "./helpers/run-in-tmp";
15
+ import { runWrangler } from "./helpers/run-wrangler";
16
+ import type { Config } from "../config";
17
+ import type { UserAuthConfig } from "../user";
18
+
19
+ describe("User", () => {
20
+ runInTempDir({ homedir: "./home" });
21
+ const std = mockConsoleMethods();
22
+ const {
23
+ mockOAuthServerCallback,
24
+ mockGrantAccessToken,
25
+ mockGrantAuthorization,
26
+ mockRevokeAuthorization,
27
+ mockExchangeRefreshTokenForAccessToken,
28
+ } = mockOAuthFlow();
29
+
30
+ const { setIsTTY } = useMockIsTTY();
31
+
32
+ describe("login", () => {
33
+ it("should login a user when `wrangler login` is run", async () => {
34
+ mockOAuthServerCallback();
35
+ const accessTokenRequest = mockGrantAccessToken({ respondWith: "ok" });
36
+ mockGrantAuthorization({ respondWith: "success" });
37
+
38
+ await runWrangler("login");
39
+
40
+ expect(accessTokenRequest.actual.url).toEqual(
41
+ accessTokenRequest.expected.url
42
+ );
43
+ expect(accessTokenRequest.actual.method).toEqual(
44
+ accessTokenRequest.expected.method
45
+ );
46
+
47
+ expect(std.out).toMatchInlineSnapshot(`
48
+ "Attempting to login via OAuth...
49
+ Successfully logged in."
50
+ `);
51
+
52
+ expect(readAuthConfigFile()).toEqual<UserAuthConfig>({
53
+ api_token: undefined,
54
+ oauth_token: "test-access-token",
55
+ refresh_token: "test-refresh-token",
56
+ expiration_time: expect.any(String),
57
+ });
58
+ });
59
+ });
60
+
61
+ describe("logout", () => {
62
+ it("should exit with a message stating the user is not logged in", async () => {
63
+ await runWrangler("logout");
64
+ expect(std.out).toMatchInlineSnapshot(`"Not logged in, exiting..."`);
65
+ });
66
+
67
+ it("should logout user that has been properly logged in", async () => {
68
+ writeAuthConfigFile({
69
+ oauth_token: "some-oauth-tok",
70
+ refresh_token: "some-refresh-tok",
71
+ });
72
+ const outcome = mockRevokeAuthorization();
73
+
74
+ await runWrangler("logout");
75
+
76
+ expect(outcome.actual.url).toEqual(
77
+ "https://dash.cloudflare.com/oauth2/revoke"
78
+ );
79
+ expect(outcome.actual.method).toEqual("POST");
80
+
81
+ expect(std.out).toMatchInlineSnapshot(`"Successfully logged out."`);
82
+
83
+ // Make sure that we made the request to logout.
84
+ expect(fetchMock).toHaveBeenCalledTimes(1);
85
+
86
+ // Make sure that logout removed the config file containing the auth tokens.
87
+ const config = path.join(os.homedir(), USER_AUTH_CONFIG_FILE);
88
+ expect(fs.existsSync(config)).toBeFalsy();
89
+ });
90
+ });
91
+
92
+ // TODO: Improve OAuth mocking to handle `/token` endpoints from different calls
93
+ it("should handle errors for failed token refresh", async () => {
94
+ setIsTTY(false);
95
+ mockOAuthServerCallback();
96
+ writeAuthConfigFile({
97
+ oauth_token: "hunter2",
98
+ refresh_token: "Order 66",
99
+ });
100
+ mockGrantAuthorization({ respondWith: "success" });
101
+
102
+ mockExchangeRefreshTokenForAccessToken({
103
+ respondWith: "badResponse",
104
+ });
105
+
106
+ // Handles the requireAuth error throw from failed login that is unhandled due to directly calling it here
107
+ await expect(
108
+ requireAuth({} as Config)
109
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
110
+ `"Did not login, quitting..."`
111
+ );
112
+ });
113
+
114
+ it("should confirm no error message when refresh is successful", async () => {
115
+ setIsTTY(false);
116
+ mockOAuthServerCallback();
117
+ writeAuthConfigFile({
118
+ oauth_token: "hunter2",
119
+ refresh_token: "Order 66",
120
+ });
121
+ mockGrantAuthorization({ respondWith: "success" });
122
+
123
+ mockExchangeRefreshTokenForAccessToken({
124
+ respondWith: "refreshSuccess",
125
+ });
126
+
127
+ // Handles the requireAuth error throw from failed login that is unhandled due to directly calling it here
128
+ await expect(requireAuth({} as Config)).rejects.toThrowError();
129
+ expect(std.err).toContain("");
130
+ });
131
+ });
@@ -1,30 +1,70 @@
1
1
  import { render } from "ink-testing-library";
2
2
  import React from "react";
3
- import { initialise } from "../user";
3
+ import { writeAuthConfigFile } from "../user";
4
4
  import { getUserInfo, WhoAmI } from "../whoami";
5
5
  import { setMockResponse } from "./helpers/mock-cfetch";
6
- import { writeUserConfig } from "./helpers/mock-user";
6
+ import { mockConsoleMethods } from "./helpers/mock-console";
7
+ import { useMockIsTTY } from "./helpers/mock-istty";
7
8
  import { runInTempDir } from "./helpers/run-in-tmp";
8
9
  import type { UserInfo } from "../whoami";
9
10
 
10
11
  describe("getUserInfo()", () => {
12
+ const ENV_COPY = process.env;
13
+
11
14
  runInTempDir({ homedir: "./home" });
15
+ const std = mockConsoleMethods();
16
+ const { setIsTTY } = useMockIsTTY();
17
+
18
+ beforeEach(() => {
19
+ setIsTTY(true);
20
+ });
21
+
22
+ afterEach(() => {
23
+ process.env = ENV_COPY;
24
+ });
12
25
 
13
26
  it("should return undefined if there is no config file", async () => {
14
- await initialise();
15
27
  const userInfo = await getUserInfo();
16
28
  expect(userInfo).toBeUndefined();
17
29
  });
18
30
 
19
31
  it("should return undefined if there is an empty config file", async () => {
20
- writeUserConfig();
21
- await initialise();
32
+ writeAuthConfigFile({});
22
33
  const userInfo = await getUserInfo();
23
34
  expect(userInfo).toBeUndefined();
24
35
  });
25
36
 
37
+ it("should say it's using an API token when one is set", async () => {
38
+ process.env = {
39
+ CLOUDFLARE_API_TOKEN: "123456789",
40
+ };
41
+ setMockResponse("/user", () => {
42
+ return { email: "user@example.com" };
43
+ });
44
+ setMockResponse("/accounts", () => {
45
+ return [
46
+ { name: "Account One", id: "account-1" },
47
+ { name: "Account Two", id: "account-2" },
48
+ { name: "Account Three", id: "account-3" },
49
+ ];
50
+ });
51
+
52
+ const userInfo = await getUserInfo();
53
+ expect(userInfo).toEqual({
54
+ authType: "API",
55
+ apiToken: "123456789",
56
+ email: "user@example.com",
57
+ accounts: [
58
+ { name: "Account One", id: "account-1" },
59
+ { name: "Account Two", id: "account-2" },
60
+ { name: "Account Three", id: "account-3" },
61
+ ],
62
+ });
63
+ });
64
+
26
65
  it("should return the user's email and accounts if authenticated via config token", async () => {
27
- writeUserConfig("some-oauth-token");
66
+ writeAuthConfigFile({ oauth_token: "some-oauth-token" });
67
+
28
68
  setMockResponse("/user", () => {
29
69
  return { email: "user@example.com" };
30
70
  });
@@ -36,7 +76,6 @@ describe("getUserInfo()", () => {
36
76
  ];
37
77
  });
38
78
 
39
- await initialise();
40
79
  const userInfo = await getUserInfo();
41
80
 
42
81
  expect(userInfo).toEqual({
@@ -50,6 +89,20 @@ describe("getUserInfo()", () => {
50
89
  ],
51
90
  });
52
91
  });
92
+
93
+ it("should display a warning message if the config file contains a legacy api_token field", async () => {
94
+ writeAuthConfigFile({ api_token: "API_TOKEN" });
95
+ await getUserInfo();
96
+ expect(std.warn).toMatchInlineSnapshot(`
97
+ "▲ [WARNING] It looks like you have used Wrangler 1's \`config\` command to login with an API token.
98
+
99
+ This is no longer supported in the current version of Wrangler.
100
+ If you wish to authenticate via an API token then please set the \`CLOUDFLARE_API_TOKEN\`
101
+ environment variable.
102
+
103
+ "
104
+ `);
105
+ });
53
106
  });
54
107
 
55
108
  describe("WhoAmI component", () => {
package/src/abort.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ declare interface AbortSignal {
2
+ addEventListener(event: "abort", handler: () => void);
3
+ }
package/src/bundle.ts ADDED
@@ -0,0 +1,197 @@
1
+ import assert from "node:assert";
2
+ import * as fs from "node:fs";
3
+ import { builtinModules } from "node:module";
4
+ import * as path from "node:path";
5
+ import NodeGlobalsPolyfills from "@esbuild-plugins/node-globals-polyfill";
6
+ import NodeModulesPolyfills from "@esbuild-plugins/node-modules-polyfill";
7
+ import * as esbuild from "esbuild";
8
+ import createModuleCollector from "./module-collection";
9
+ import type { Config } from "./config";
10
+ import type { Entry } from "./entry";
11
+ import type { CfModule } from "./worker";
12
+
13
+ type BundleResult = {
14
+ modules: CfModule[];
15
+ resolvedEntryPointPath: string;
16
+ bundleType: "esm" | "commonjs";
17
+ stop: (() => void) | undefined;
18
+ };
19
+
20
+ /**
21
+ * Searches for any uses of node's builtin modules, and throws an error if it
22
+ * finds anything. This plugin is only used when nodeCompat is not enabled.
23
+ * Supports both regular node builtins, and the new "node:<MODULE>" format.
24
+ */
25
+ const checkForNodeBuiltinsPlugin = {
26
+ name: "checkForNodeBuiltins",
27
+ setup(build: esbuild.PluginBuild) {
28
+ build.onResolve(
29
+ {
30
+ filter: new RegExp(
31
+ "^(" +
32
+ builtinModules.join("|") +
33
+ "|" +
34
+ builtinModules.map((module) => "node:" + module).join("|") +
35
+ ")$"
36
+ ),
37
+ },
38
+ () => {
39
+ throw new Error(
40
+ `Detected a Node builtin module import while Node compatibility is disabled.\nAdd node_compat = true to your wrangler.toml file to enable Node compatibility.`
41
+ );
42
+ }
43
+ );
44
+ },
45
+ };
46
+
47
+ /**
48
+ * Generate a bundle for the worker identified by the arguments passed in.
49
+ */
50
+ export async function bundleWorker(
51
+ entry: Entry,
52
+ destination: string,
53
+ options: {
54
+ serveAssetsFromWorker: boolean;
55
+ jsxFactory: string | undefined;
56
+ jsxFragment: string | undefined;
57
+ rules: Config["rules"];
58
+ watch?: esbuild.WatchMode;
59
+ tsconfig: string | undefined;
60
+ minify: boolean | undefined;
61
+ nodeCompat: boolean | undefined;
62
+ }
63
+ ): Promise<BundleResult> {
64
+ const {
65
+ serveAssetsFromWorker,
66
+ jsxFactory,
67
+ jsxFragment,
68
+ rules,
69
+ watch,
70
+ tsconfig,
71
+ minify,
72
+ nodeCompat,
73
+ } = options;
74
+ const entryDirectory = path.dirname(entry.file);
75
+ const moduleCollector = createModuleCollector({
76
+ wrangler1xlegacyModuleReferences: {
77
+ rootDirectory: entryDirectory,
78
+ fileNames: new Set(
79
+ fs
80
+ .readdirSync(entryDirectory, { withFileTypes: true })
81
+ .filter(
82
+ (dirEntry) =>
83
+ dirEntry.isFile() && dirEntry.name !== path.basename(entry.file)
84
+ )
85
+ .map((dirEnt) => dirEnt.name)
86
+ ),
87
+ },
88
+ format: entry.format,
89
+ rules,
90
+ });
91
+
92
+ const result = await esbuild.build({
93
+ ...getEntryPoint(entry.file, serveAssetsFromWorker),
94
+ bundle: true,
95
+ absWorkingDir: entry.directory,
96
+ outdir: destination,
97
+ external: ["__STATIC_CONTENT_MANIFEST"],
98
+ format: entry.format === "modules" ? "esm" : "iife",
99
+ target: "es2020",
100
+ sourcemap: true,
101
+ minify,
102
+ metafile: true,
103
+ conditions: ["worker", "browser"],
104
+ ...(process.env.NODE_ENV && {
105
+ define: {
106
+ "process.env.NODE_ENV": `"${process.env.NODE_ENV}"`,
107
+ ...(nodeCompat ? { global: "globalThis" } : {}),
108
+ },
109
+ }),
110
+ loader: {
111
+ ".js": "jsx",
112
+ ".mjs": "jsx",
113
+ ".cjs": "jsx",
114
+ },
115
+ plugins: [
116
+ moduleCollector.plugin,
117
+ ...(nodeCompat
118
+ ? [NodeGlobalsPolyfills({ buffer: true }), NodeModulesPolyfills()]
119
+ : // we use checkForNodeBuiltinsPlugin to throw a nicer error
120
+ // if we find node builtins when nodeCompat isn't turned on
121
+ [checkForNodeBuiltinsPlugin]),
122
+ ],
123
+ ...(jsxFactory && { jsxFactory }),
124
+ ...(jsxFragment && { jsxFragment }),
125
+ ...(tsconfig && { tsconfig }),
126
+ watch,
127
+ });
128
+
129
+ const entryPointOutputs = Object.entries(result.metafile.outputs).filter(
130
+ ([_path, output]) => output.entryPoint !== undefined
131
+ );
132
+ assert(
133
+ entryPointOutputs.length > 0,
134
+ `Cannot find entry-point "${entry.file}" in generated bundle.` +
135
+ listEntryPoints(entryPointOutputs)
136
+ );
137
+ assert(
138
+ entryPointOutputs.length < 2,
139
+ "More than one entry-point found for generated bundle." +
140
+ listEntryPoints(entryPointOutputs)
141
+ );
142
+
143
+ const entryPointExports = entryPointOutputs[0][1].exports;
144
+ const bundleType = entryPointExports.length > 0 ? "esm" : "commonjs";
145
+
146
+ return {
147
+ modules: moduleCollector.modules,
148
+ resolvedEntryPointPath: path.resolve(
149
+ entry.directory,
150
+ entryPointOutputs[0][0]
151
+ ),
152
+ bundleType,
153
+ stop: result.stop,
154
+ };
155
+ }
156
+
157
+ type EntryPoint = { stdin: esbuild.StdinOptions } | { entryPoints: string[] };
158
+
159
+ /**
160
+ * Create an object that describes the entry point for esbuild.
161
+ *
162
+ * If we are using the experimental asset handling, then the entry point is
163
+ * actually a shim worker that will either return an asset from a KV store,
164
+ * or delegate to the actual worker.
165
+ */
166
+ function getEntryPoint(
167
+ entryFile: string,
168
+ serveAssetsFromWorker: boolean
169
+ ): EntryPoint {
170
+ if (serveAssetsFromWorker) {
171
+ return {
172
+ stdin: {
173
+ contents: fs
174
+ .readFileSync(
175
+ path.join(__dirname, "../templates/static-asset-facade.js"),
176
+ "utf8"
177
+ )
178
+ .replace("__ENTRY_POINT__", entryFile),
179
+ sourcefile: "static-asset-facade.js",
180
+ resolveDir: path.dirname(entryFile),
181
+ },
182
+ };
183
+ } else {
184
+ return { entryPoints: [entryFile] };
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Generate a string that describes the entry-points that were identified by esbuild.
190
+ */
191
+ function listEntryPoints(
192
+ outputs: [string, ValueOf<esbuild.Metafile["outputs"]>][]
193
+ ): string {
194
+ return outputs.map(([_input, output]) => output.entryPoint).join("\n");
195
+ }
196
+
197
+ type ValueOf<T> = T[keyof T];
@@ -1,14 +1,16 @@
1
1
  import { URLSearchParams } from "node:url";
2
+ import { ParseError } from "../parse";
2
3
  import { fetchInternal } from "./internal";
3
4
  import type { RequestInit } from "undici";
4
5
 
5
6
  // Check out https://api.cloudflare.com/ for API docs.
6
7
 
7
- export { CF_API_BASE_URL } from "./internal";
8
+ export { getCloudflareAPIBaseURL as getCloudflareApiBaseUrl } from "./internal";
8
9
 
9
10
  export interface FetchError {
10
11
  code: number;
11
12
  message: string;
13
+ error_chain?: FetchError[];
12
14
  }
13
15
  export interface FetchResult<ResponseType = unknown> {
14
16
  success: boolean;
@@ -26,12 +28,14 @@ export { fetchKVGetValue } from "./internal";
26
28
  export async function fetchResult<ResponseType>(
27
29
  resource: string,
28
30
  init: RequestInit = {},
29
- queryParams?: URLSearchParams
31
+ queryParams?: URLSearchParams,
32
+ abortSignal?: AbortSignal
30
33
  ): Promise<ResponseType> {
31
34
  const json = await fetchInternal<FetchResult<ResponseType>>(
32
35
  resource,
33
36
  init,
34
- queryParams
37
+ queryParams,
38
+ abortSignal
35
39
  );
36
40
  if (json.success) {
37
41
  return json.result;
@@ -81,11 +85,12 @@ function throwFetchError(
81
85
  resource: string,
82
86
  response: FetchResult<unknown>
83
87
  ): never {
84
- response.messages.forEach((message) => console.warn(message));
85
- const errors = response.errors
86
- .map((error) => `${error.code}: ${error.message}`)
87
- .join("\n");
88
- const error = new Error(`Failed to fetch ${resource} - \n${errors}`);
88
+ const error = new ParseError({
89
+ text: `A request to the Cloudflare API (${resource}) failed.`,
90
+ notes: response.errors.map((err) => ({
91
+ text: renderError(err),
92
+ })),
93
+ });
89
94
  // add the first error code directly to this error
90
95
  // so consumers can use it for specific behaviour
91
96
  const code = response.errors[0]?.code;
@@ -100,3 +105,17 @@ function hasCursor(result_info: unknown): result_info is { cursor: string } {
100
105
  const cursor = (result_info as { cursor: string } | undefined)?.cursor;
101
106
  return cursor !== undefined && cursor !== null && cursor !== "";
102
107
  }
108
+
109
+ function renderError(err: FetchError, level = 0): string {
110
+ const chainedMessages =
111
+ err.error_chain
112
+ ?.map(
113
+ (chainedError) =>
114
+ `\n${" ".repeat(level)}- ${renderError(chainedError, level + 1)}`
115
+ )
116
+ .join("\n") ?? "";
117
+ return (
118
+ (err.code ? `${err.message} [code: ${err.code}]` : err.message) +
119
+ chainedMessages
120
+ );
121
+ }
@@ -1,10 +1,20 @@
1
- import { fetch } from "undici";
2
- import { getAPIToken, loginOrRefreshIfRequired } from "../user";
1
+ import assert from "node:assert";
2
+ import { fetch, Headers } from "undici";
3
+ import { version as wranglerVersion } from "../../package.json";
4
+ import { getEnvironmentVariableFactory } from "../environment-variables";
5
+ import { ParseError, parseJSON } from "../parse";
6
+ import { loginOrRefreshIfRequired, requireApiToken } from "../user";
3
7
  import type { URLSearchParams } from "node:url";
4
8
  import type { RequestInit, HeadersInit } from "undici";
5
9
 
6
- export const CF_API_BASE_URL =
7
- process.env.CF_API_BASE_URL || "https://api.cloudflare.com/client/v4";
10
+ /**
11
+ * Get the URL to use to access the Cloudflare API.
12
+ */
13
+ export const getCloudflareAPIBaseURL = getEnvironmentVariableFactory({
14
+ variableName: "CLOUDFLARE_API_BASE_URL",
15
+ deprecatedName: "CF_API_BASE_URL",
16
+ defaultValue: "https://api.cloudflare.com/client/v4",
17
+ });
8
18
 
9
19
  /**
10
20
  * Make a fetch request to the Cloudflare API.
@@ -18,32 +28,64 @@ export const CF_API_BASE_URL =
18
28
  export async function fetchInternal<ResponseType>(
19
29
  resource: string,
20
30
  init: RequestInit = {},
21
- queryParams?: URLSearchParams
31
+ queryParams?: URLSearchParams,
32
+ abortSignal?: AbortSignal
22
33
  ): Promise<ResponseType> {
34
+ assert(
35
+ resource.startsWith("/"),
36
+ `CF API fetch - resource path must start with a "/" but got "${resource}"`
37
+ );
23
38
  await requireLoggedIn();
24
39
  const apiToken = requireApiToken();
25
40
  const headers = cloneHeaders(init.headers);
26
- addAuthorizationHeader(headers, apiToken);
41
+ addAuthorizationHeaderIfUnspecified(headers, apiToken);
42
+ addUserAgent(headers);
27
43
 
28
44
  const queryString = queryParams ? `?${queryParams.toString()}` : "";
29
- const response = await fetch(`${CF_API_BASE_URL}${resource}${queryString}`, {
30
- method: "GET",
31
- ...init,
32
- headers,
33
- });
45
+ const method = init.method ?? "GET";
46
+ const response = await fetch(
47
+ `${getCloudflareAPIBaseURL()}${resource}${queryString}`,
48
+ {
49
+ method,
50
+ ...init,
51
+ headers,
52
+ signal: abortSignal,
53
+ }
54
+ );
34
55
  const jsonText = await response.text();
35
56
  try {
36
- const json = JSON.parse(jsonText);
37
- return json as ResponseType;
38
- } catch (e) {
39
- throw new Error(
40
- `Failed to fetch ${resource} - ${response.status}: ${response.statusText}\nInvalid JSON response:\n${jsonText}`
41
- );
57
+ return parseJSON<ResponseType>(jsonText);
58
+ } catch (err) {
59
+ throw new ParseError({
60
+ text: "Received a malformed response from the API",
61
+ notes: [
62
+ {
63
+ text: truncate(jsonText, 100),
64
+ },
65
+ {
66
+ text: `${method} ${resource} -> ${response.status} ${response.statusText}`,
67
+ },
68
+ ],
69
+ });
42
70
  }
43
71
  }
44
72
 
45
- function cloneHeaders(headers: HeadersInit | undefined): HeadersInit {
46
- return { ...headers };
73
+ function truncate(text: string, maxLength: number): string {
74
+ const { length } = text;
75
+ if (length <= maxLength) {
76
+ return text;
77
+ }
78
+ return `${text.substring(0, maxLength)}... (length = ${length})`;
79
+ }
80
+
81
+ function cloneHeaders(
82
+ headers: HeadersInit | undefined
83
+ ): Record<string, string> {
84
+ return headers instanceof Headers
85
+ ? Object.fromEntries(headers.entries())
86
+ : Array.isArray(headers)
87
+ ? Object.fromEntries(headers)
88
+ : { ...headers };
47
89
  }
48
90
 
49
91
  async function requireLoggedIn(): Promise<void> {
@@ -53,21 +95,17 @@ async function requireLoggedIn(): Promise<void> {
53
95
  }
54
96
  }
55
97
 
56
- function requireApiToken(): string {
57
- const apiToken = getAPIToken();
58
- if (!apiToken) {
59
- throw new Error("No API token found.");
98
+ function addAuthorizationHeaderIfUnspecified(
99
+ headers: Record<string, string>,
100
+ apiToken: string
101
+ ): void {
102
+ if (!("Authorization" in headers)) {
103
+ headers["Authorization"] = `Bearer ${apiToken}`;
60
104
  }
61
- return apiToken;
62
105
  }
63
106
 
64
- function addAuthorizationHeader(headers: HeadersInit, apiToken: string): void {
65
- if (headers["Authorization"]) {
66
- throw new Error(
67
- "The request already specifies an authorisation header - cannot add a new one."
68
- );
69
- }
70
- headers["Authorization"] = `Bearer ${apiToken}`;
107
+ function addUserAgent(headers: Record<string, string>): void {
108
+ headers["User-Agent"] = `wrangler/${wranglerVersion}`;
71
109
  }
72
110
 
73
111
  /**
@@ -76,6 +114,9 @@ function addAuthorizationHeader(headers: HeadersInit, apiToken: string): void {
76
114
  * doesn't return json. We inline the implementation and try not to share
77
115
  * any code with the other calls. We should push back on any new APIs that
78
116
  * try to introduce non-"standard" response structures.
117
+ *
118
+ * Note: any calls to fetchKVGetValue must call encodeURIComponent on key
119
+ * before passing it
79
120
  */
80
121
 
81
122
  export async function fetchKVGetValue(
@@ -86,7 +127,7 @@ export async function fetchKVGetValue(
86
127
  await requireLoggedIn();
87
128
  const apiToken = requireApiToken();
88
129
  const headers = { Authorization: `Bearer ${apiToken}` };
89
- const resource = `${CF_API_BASE_URL}/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${key}`;
130
+ const resource = `${getCloudflareAPIBaseURL()}/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${key}`;
90
131
  const response = await fetch(resource, {
91
132
  method: "GET",
92
133
  headers,
package/src/cli.ts CHANGED
@@ -1,9 +1,13 @@
1
+ import "dotenv/config"; // Grab locally specified env params from a `.env` file.
2
+ import process from "process";
1
3
  import { hideBin } from "yargs/helpers";
4
+ import { FatalError } from "./errors";
2
5
  import { main } from ".";
3
6
 
4
- main(hideBin(process.argv)).catch(() => {
7
+ main(hideBin(process.argv)).catch((e) => {
5
8
  // The logging of any error that was thrown from `main()` is handled in the `yargs.fail()` handler.
6
9
  // Here we just want to ensure that the process exits with a non-zero code.
7
10
  // We don't want to do this inside the `main()` function, since that would kill the process when running our tests.
8
- process.exit(1);
11
+ const exitCode = (e instanceof FatalError && e.code) || 1;
12
+ process.exit(exitCode);
9
13
  });