wrangler 0.0.0-e22f9d7 → 0.0.0-e2a26a0

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 (54) hide show
  1. package/bin/wrangler.js +2 -2
  2. package/package.json +15 -10
  3. package/pages/functions/buildWorker.ts +1 -1
  4. package/pages/functions/filepath-routing.test.ts +21 -13
  5. package/pages/functions/filepath-routing.ts +38 -28
  6. package/pages/functions/routes.ts +8 -6
  7. package/pages/functions/template-worker.ts +16 -24
  8. package/src/__tests__/dev.test.tsx +7 -3
  9. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  10. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  11. package/src/__tests__/helpers/mock-account-id.ts +30 -0
  12. package/src/__tests__/helpers/mock-bin.ts +33 -0
  13. package/src/__tests__/{mock-cfetch.ts → helpers/mock-cfetch.ts} +49 -13
  14. package/src/__tests__/helpers/mock-console.ts +56 -0
  15. package/src/__tests__/{mock-dialogs.ts → helpers/mock-dialogs.ts} +1 -1
  16. package/src/__tests__/helpers/mock-kv.ts +40 -0
  17. package/src/__tests__/helpers/mock-user.ts +27 -0
  18. package/src/__tests__/helpers/run-in-tmp.ts +29 -0
  19. package/src/__tests__/helpers/run-wrangler.ts +14 -0
  20. package/src/__tests__/index.test.ts +312 -56
  21. package/src/__tests__/jest.setup.ts +17 -2
  22. package/src/__tests__/kv.test.ts +344 -316
  23. package/src/__tests__/logout.test.ts +50 -0
  24. package/src/__tests__/package-manager.test.ts +206 -0
  25. package/src/__tests__/publish.test.ts +1222 -0
  26. package/src/__tests__/secret.test.ts +210 -0
  27. package/src/__tests__/whoami.test.tsx +86 -0
  28. package/src/api/form_data.ts +10 -7
  29. package/src/api/preview.ts +2 -2
  30. package/src/api/worker.ts +3 -3
  31. package/src/cfetch/index.ts +14 -14
  32. package/src/cfetch/internal.ts +40 -8
  33. package/src/config.ts +162 -8
  34. package/src/dev.tsx +103 -53
  35. package/src/dialogs.tsx +2 -2
  36. package/src/index.tsx +438 -311
  37. package/src/inspect.ts +80 -23
  38. package/src/kv.tsx +9 -1
  39. package/src/module-collection.ts +5 -6
  40. package/src/package-manager.ts +120 -0
  41. package/src/pages.tsx +41 -26
  42. package/src/paths.ts +26 -0
  43. package/src/proxy.ts +78 -10
  44. package/src/publish.ts +98 -52
  45. package/src/sites.tsx +175 -53
  46. package/src/user.tsx +51 -59
  47. package/src/whoami.tsx +69 -0
  48. package/templates/new-worker.js +15 -0
  49. package/templates/new-worker.ts +15 -0
  50. package/{static-asset-facade.js → templates/static-asset-facade.js} +0 -0
  51. package/wrangler-dist/cli.js +111479 -110187
  52. package/wrangler-dist/cli.js.map +3 -3
  53. package/src/__tests__/run-in-tmp.ts +0 -19
  54. package/src/__tests__/run-wrangler.ts +0 -32
package/bin/wrangler.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- const { spawn } = require("child_process");
3
- const { join } = require("path");
2
+ const { spawn } = require("node:child_process");
3
+ const { join } = require("node:path");
4
4
  const semiver = require("semiver");
5
5
 
6
6
  const MIN_NODE_VERSION = "16.7.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wrangler",
3
- "version": "0.0.0-e22f9d7",
3
+ "version": "0.0.0-e2a26a0",
4
4
  "author": "wrangler@cloudflare.com",
5
5
  "description": "Command-line interface for all things Cloudflare Workers",
6
6
  "bin": {
@@ -36,10 +36,11 @@
36
36
  "cli"
37
37
  ],
38
38
  "dependencies": {
39
- "esbuild": "0.14.1",
39
+ "esbuild": "0.14.14",
40
40
  "miniflare": "2.2.0",
41
41
  "path-to-regexp": "^6.2.0",
42
- "semiver": "^1.1.0"
42
+ "semiver": "^1.1.0",
43
+ "xxhash-addon": "^1.4.0"
43
44
  },
44
45
  "optionalDependencies": {
45
46
  "fsevents": "~2.3.2"
@@ -58,27 +59,28 @@
58
59
  "acorn-walk": "^8.2.0",
59
60
  "chokidar": "^3.5.2",
60
61
  "clipboardy": "^3.0.0",
62
+ "cmd-shim": "^4.1.0",
61
63
  "command-exists": "^1.2.9",
62
64
  "devtools-protocol": "^0.0.955664",
63
65
  "execa": "^6.0.0",
64
66
  "faye-websocket": "^0.11.4",
65
67
  "finalhandler": "^1.1.2",
66
68
  "find-up": "^6.2.0",
67
- "formdata-node": "^4.3.1",
69
+ "ignore": "^5.2.0",
68
70
  "ink": "^3.2.0",
69
71
  "ink-select-input": "^4.2.1",
70
72
  "ink-table": "^3.0.0",
71
73
  "ink-testing-library": "^2.1.0",
72
74
  "ink-text-input": "^4.0.2",
75
+ "jest-fetch-mock": "^3.0.3",
73
76
  "mime": "^3.0.0",
74
- "node-fetch": "^3.1.0",
75
77
  "open": "^8.4.0",
76
78
  "react": "^17.0.2",
77
79
  "react-error-boundary": "^3.1.4",
78
80
  "serve-static": "^1.14.1",
79
81
  "signal-exit": "^3.0.6",
80
82
  "tmp-promise": "^3.0.3",
81
- "undici": "^4.11.1",
83
+ "undici": "4.13.0",
82
84
  "ws": "^8.3.0",
83
85
  "yargs": "^17.3.0"
84
86
  },
@@ -88,28 +90,31 @@
88
90
  "pages",
89
91
  "miniflare-config-stubs",
90
92
  "wrangler-dist",
91
- "static-asset-facade.js",
93
+ "templates",
92
94
  "vendor",
93
95
  "import_meta_url.js"
94
96
  ],
95
97
  "scripts": {
96
98
  "clean": "rm -rf wrangler-dist",
99
+ "check:type": "tsc",
97
100
  "bundle": "node -r esbuild-register scripts/bundle.ts",
98
101
  "build": "npm run clean && npm run bundle",
99
102
  "start": "npm run bundle && NODE_OPTIONS=--enable-source-maps ./bin/wrangler.js",
100
- "test": "CF_API_TOKEN=some-api-token CF_ACCOUNT_ID=some-account-id jest --silent=false --verbose=true"
103
+ "test": "jest --silent=false --verbose=true",
104
+ "test-watch": "npm run test -- --runInBand --testTimeout=50000 --watch"
101
105
  },
102
106
  "engines": {
103
107
  "node": ">=16.7.0"
104
108
  },
105
109
  "jest": {
106
110
  "restoreMocks": true,
111
+ "testTimeout": 30000,
107
112
  "testRegex": ".*.(test|spec)\\.[jt]sx?$",
108
113
  "transformIgnorePatterns": [
109
- "node_modules/(?!node-fetch|fetch-blob|find-up|locate-path|p-locate|p-limit|yocto-queue|path-exists|data-uri-to-buffer|formdata-polyfill|execa|strip-final-newline|npm-run-path|path-key|onetime|mimic-fn|human-signals|is-stream)"
114
+ "node_modules/(?!find-up|locate-path|p-locate|p-limit|yocto-queue|path-exists|execa|strip-final-newline|npm-run-path|path-key|onetime|mimic-fn|human-signals|is-stream)"
110
115
  ],
111
116
  "moduleNameMapper": {
112
- "clipboardy": "<rootDir>/src/__tests__/clipboardy-mock.js"
117
+ "clipboardy": "<rootDir>/src/__tests__/helpers/clipboardy-mock.js"
113
118
  },
114
119
  "transform": {
115
120
  "^.+\\.c?(t|j)sx?$": [
@@ -1,4 +1,4 @@
1
- import path from "path";
1
+ import path from "node:path";
2
2
  import { build } from "esbuild";
3
3
 
4
4
  type Options = {
@@ -1,39 +1,47 @@
1
+ import { toUrlPath } from "../../src/paths";
1
2
  import { compareRoutes } from "./filepath-routing";
2
3
 
3
4
  describe("compareRoutes()", () => {
5
+ const url = toUrlPath;
4
6
  test("routes / last", () => {
5
- expect(compareRoutes("/", "/foo")).toBeGreaterThanOrEqual(1);
6
- expect(compareRoutes("/", "/:foo")).toBeGreaterThanOrEqual(1);
7
- expect(compareRoutes("/", "/:foo*")).toBeGreaterThanOrEqual(1);
7
+ expect(compareRoutes([url("/")], [url("/foo")])).toBeGreaterThanOrEqual(1);
8
+ expect(compareRoutes([url("/")], [url("/:foo")])).toBeGreaterThanOrEqual(1);
9
+ expect(compareRoutes([url("/")], [url("/:foo*")])).toBeGreaterThanOrEqual(
10
+ 1
11
+ );
8
12
  });
9
13
 
10
14
  test("routes with fewer segments come after those with more segments", () => {
11
- expect(compareRoutes("/foo", "/foo/bar")).toBeGreaterThanOrEqual(1);
12
- expect(compareRoutes("/foo", "/foo/bar/cat")).toBeGreaterThanOrEqual(1);
15
+ expect(
16
+ compareRoutes([url("/foo")], [url("/foo/bar")])
17
+ ).toBeGreaterThanOrEqual(1);
18
+ expect(
19
+ compareRoutes([url("/foo")], [url("/foo/bar/cat")])
20
+ ).toBeGreaterThanOrEqual(1);
13
21
  });
14
22
 
15
23
  test("routes with wildcard segments come after those without", () => {
16
- expect(compareRoutes("/:foo*", "/foo")).toBe(1);
17
- expect(compareRoutes("/:foo*", "/:foo")).toBe(1);
24
+ expect(compareRoutes([url("/:foo*")], [url("/foo")])).toBe(1);
25
+ expect(compareRoutes([url("/:foo*")], [url("/:foo")])).toBe(1);
18
26
  });
19
27
 
20
28
  test("routes with dynamic segments come after those without", () => {
21
- expect(compareRoutes("/:foo", "/foo")).toBe(1);
29
+ expect(compareRoutes([url("/:foo")], [url("/foo")])).toBe(1);
22
30
  });
23
31
 
24
- test("routes with dynamic segments occuring earlier come after those with dynamic segments in later positions", () => {
25
- expect(compareRoutes("/foo/:id/bar", "/foo/bar/:id")).toBe(1);
32
+ test("routes with dynamic segments occurring earlier come after those with dynamic segments in later positions", () => {
33
+ expect(compareRoutes([url("/foo/:id/bar")], [url("/foo/bar/:id")])).toBe(1);
26
34
  });
27
35
 
28
36
  test("routes with no HTTP method come after those specifying a method", () => {
29
- expect(compareRoutes("/foo", "GET /foo")).toBe(1);
37
+ expect(compareRoutes([url("/foo")], [url("/foo"), "GET"])).toBe(1);
30
38
  });
31
39
 
32
40
  test("two equal routes are sorted according to their original position in the list", () => {
33
- expect(compareRoutes("GET /foo", "GET /foo")).toBe(0);
41
+ expect(compareRoutes([url("/foo"), "GET"], [url("/foo"), "GET"])).toBe(0);
34
42
  });
35
43
 
36
44
  test("it returns -1 if the first argument should appear first in the list", () => {
37
- expect(compareRoutes("GET /foo", "/foo")).toBe(-1);
45
+ expect(compareRoutes([url("/foo"), "GET"], [url("/foo")])).toBe(-1);
38
46
  });
39
47
  });
@@ -1,28 +1,33 @@
1
- import path from "path";
2
- import fs from "fs/promises";
3
- import { transform } from "esbuild";
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
4
3
  import * as acorn from "acorn";
5
4
  import * as acornWalk from "acorn-walk";
5
+ import { transform } from "esbuild";
6
+ import { toUrlPath } from "../../src/paths";
7
+ import type { UrlPath } from "../../src/paths";
6
8
  import type { Config, RouteConfig } from "./routes";
7
9
  import type { ExportNamedDeclaration, Identifier } from "estree";
8
10
 
9
11
  type Arguments = {
10
12
  baseDir: string;
11
- baseURL: string;
13
+ baseURL: UrlPath;
12
14
  };
13
15
 
16
+ type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "HEAD";
17
+ type RouteKey = [UrlPath, Method] | [UrlPath, undefined] | [UrlPath];
18
+
14
19
  export async function generateConfigFromFileTree({
15
20
  baseDir,
16
21
  baseURL,
17
22
  }: Arguments) {
18
- let routeEntries: [string, RouteConfig][] = [];
23
+ let routeEntries: [RouteKey, RouteConfig][] = [];
19
24
 
20
25
  if (!baseURL.startsWith("/")) {
21
- baseURL = `/${baseURL}`;
26
+ baseURL = `/${baseURL}` as UrlPath;
22
27
  }
23
28
 
24
29
  if (baseURL.endsWith("/")) {
25
- baseURL = baseURL.slice(0, -1);
30
+ baseURL = baseURL.slice(0, -1) as UrlPath;
26
31
  }
27
32
 
28
33
  await forEachFile(baseDir, async (filepath) => {
@@ -52,7 +57,7 @@ export async function generateConfigFromFileTree({
52
57
  const declaration = node.declaration;
53
58
 
54
59
  // `export async function onRequest() {...}`
55
- if (declaration.type === "FunctionDeclaration") {
60
+ if (declaration.type === "FunctionDeclaration" && declaration.id) {
56
61
  exportNames.push(declaration.id.name);
57
62
  }
58
63
 
@@ -108,12 +113,14 @@ export async function generateConfigFromFileTree({
108
113
  routePath = routePath.replace(/\[\[(.+)]]/g, ":$1*"); // transform [[id]] => :id*
109
114
  routePath = routePath.replace(/\[(.+)]/g, ":$1"); // transform [id] => :id
110
115
 
111
- if (method) {
112
- routePath = `${method.toUpperCase()} ${routePath}`;
113
- }
116
+ const routeUrlPath = toUrlPath(routePath);
117
+ const routeKey: RouteKey = [
118
+ routeUrlPath,
119
+ method ? (method.toUpperCase() as Method) : undefined,
120
+ ];
114
121
 
115
122
  routeEntries.push([
116
- routePath,
123
+ routeKey,
117
124
  {
118
125
  [isMiddlewareFile ? "middleware" : "module"]: [
119
126
  `${path.relative(baseDir, filepath)}:${exportName}`,
@@ -146,7 +153,7 @@ export async function generateConfigFromFileTree({
146
153
  []
147
154
  );
148
155
 
149
- routeEntries.sort(([pathA], [pathB]) => compareRoutes(pathA, pathB));
156
+ routeEntries.sort(([a], [b]) => compareRoutes(a, b));
150
157
 
151
158
  return { routes: Object.fromEntries(routeEntries) } as Config;
152
159
  }
@@ -154,20 +161,16 @@ export async function generateConfigFromFileTree({
154
161
  // Ensure routes are produced in order of precedence so that
155
162
  // more specific routes aren't occluded from matching due to
156
163
  // less specific routes appearing first in the route list.
157
- export function compareRoutes(a: string, b: string) {
158
- function parseRoutePath(routePath: string) {
159
- let [method, segmentedPath] = routePath.split(" ");
160
- if (!segmentedPath) {
161
- segmentedPath = method;
162
- method = null;
163
- }
164
-
165
- const segments = segmentedPath.slice(1).split("/").filter(Boolean);
166
- return [method, segments];
164
+ export function compareRoutes(
165
+ [routePathA, methodA]: RouteKey,
166
+ [routePathB, methodB]: RouteKey
167
+ ) {
168
+ function parseRoutePath(routePath: UrlPath): string[] {
169
+ return routePath.slice(1).split("/").filter(Boolean);
167
170
  }
168
171
 
169
- const [methodA, segmentsA] = parseRoutePath(a);
170
- const [methodB, segmentsB] = parseRoutePath(b);
172
+ const segmentsA = parseRoutePath(routePathA);
173
+ const segmentsB = parseRoutePath(routePathB);
171
174
 
172
175
  // sort routes with fewer segments after those with more segments
173
176
  if (segmentsA.length !== segmentsB.length) {
@@ -193,8 +196,8 @@ export function compareRoutes(a: string, b: string) {
193
196
  if (methodA && !methodB) return -1;
194
197
  if (!methodA && methodB) return 1;
195
198
 
196
- // all else equal, just sort them lexicographically
197
- return a.localeCompare(b);
199
+ // all else equal, just sort the paths lexicographically
200
+ return routePathA.localeCompare(routePathB);
198
201
  }
199
202
 
200
203
  async function forEachFile<T>(
@@ -204,7 +207,7 @@ async function forEachFile<T>(
204
207
  const searchPaths = [baseDir];
205
208
  const returnValues: T[] = [];
206
209
 
207
- while (searchPaths.length) {
210
+ while (isNotEmpty(searchPaths)) {
208
211
  const cwd = searchPaths.shift();
209
212
  const dir = await fs.readdir(cwd, { withFileTypes: true });
210
213
  for (const entry of dir) {
@@ -219,3 +222,10 @@ async function forEachFile<T>(
219
222
 
220
223
  return returnValues;
221
224
  }
225
+
226
+ interface NonEmptyArray<T> extends Array<T> {
227
+ shift(): T;
228
+ }
229
+ function isNotEmpty<T>(array: T[]): array is NonEmptyArray<T> {
230
+ return array.length > 0;
231
+ }
@@ -1,6 +1,8 @@
1
- import path from "path";
2
- import fs from "fs/promises";
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { toUrlPath } from "../../src/paths";
3
4
  import { isValidIdentifier, normalizeIdentifier } from "./identifiers";
5
+ import type { UrlPath } from "../../src/paths";
4
6
 
5
7
  export const HTTP_METHODS = [
6
8
  "HEAD",
@@ -19,7 +21,7 @@ export function isHTTPMethod(
19
21
  }
20
22
 
21
23
  export type RoutesCollection = Array<{
22
- routePath: string;
24
+ routePath: UrlPath;
23
25
  methods: HTTPMethod[];
24
26
  modules: string[];
25
27
  middlewares: string[];
@@ -31,7 +33,7 @@ export type Config = {
31
33
  };
32
34
 
33
35
  export type RoutesConfig = {
34
- [route: string]: RouteConfig;
36
+ [route: UrlPath]: RouteConfig;
35
37
  };
36
38
 
37
39
  export type RouteConfig = {
@@ -114,7 +116,7 @@ export function parseConfig(config: Config, baseDir: string) {
114
116
  });
115
117
  }
116
118
 
117
- for (const [route, props] of Object.entries(config.routes)) {
119
+ for (const [route, props] of Object.entries(config.routes ?? {})) {
118
120
  let [_methods, routePath] = route.split(" ");
119
121
  if (!routePath) {
120
122
  routePath = _methods;
@@ -122,7 +124,7 @@ export function parseConfig(config: Config, baseDir: string) {
122
124
  }
123
125
 
124
126
  routes.push({
125
- routePath,
127
+ routePath: toUrlPath(routePath),
126
128
  methods: _methods.split("|").filter(isHTTPMethod),
127
129
  middlewares: parseModuleIdentifiers(props.middleware),
128
130
  modules: parseModuleIdentifiers(props.module),
@@ -33,17 +33,16 @@ declare const routes: RouteHandler[];
33
33
  declare const __FALLBACK_SERVICE__: string;
34
34
 
35
35
  // expect an ASSETS fetcher binding pointing to the asset-server stage
36
- type Env = {
37
- [name: string]: unknown;
38
- ASSETS: { fetch(url: string, init: RequestInit): Promise<Response> };
36
+ type FetchEnv = {
37
+ [name: string]: { fetch: typeof fetch };
38
+ ASSETS: { fetch: typeof fetch };
39
39
  };
40
40
 
41
41
  type WorkerContext = {
42
42
  waitUntil: (promise: Promise<unknown>) => void;
43
43
  };
44
44
 
45
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- `env` can be used by __FALLBACK_SERVICE_FETCH__
46
- function* executeRequest(request: Request, env: Env) {
45
+ function* executeRequest(request: Request, _env: FetchEnv) {
47
46
  const requestPath = new URL(request.url).pathname;
48
47
 
49
48
  // First, iterate through the routes (backwards) and execute "middlewares" on partial route matches
@@ -88,20 +87,10 @@ function* executeRequest(request: Request, env: Env) {
88
87
  break;
89
88
  }
90
89
  }
91
-
92
- // Finally, yield to the fallback service (`env.ASSETS.fetch` in Pages' case)
93
- return {
94
- handler: () =>
95
- __FALLBACK_SERVICE__
96
- ? // @ts-expect-error expecting __FALLBACK_SERVICE__ to be the name of a service binding, so fetch should be defined
97
- env[__FALLBACK_SERVICE__].fetch(request)
98
- : fetch(request),
99
- params: {} as Params,
100
- };
101
90
  }
102
91
 
103
92
  export default {
104
- async fetch(request: Request, env: Env, workerContext: WorkerContext) {
93
+ async fetch(request: Request, env: FetchEnv, workerContext: WorkerContext) {
105
94
  const handlerIterator = executeRequest(request, env);
106
95
  const data = {}; // arbitrary data the user can set between functions
107
96
  const next = async (input?: RequestInfo, init?: RequestInit) => {
@@ -109,14 +98,11 @@ export default {
109
98
  request = new Request(input, init);
110
99
  }
111
100
 
112
- const { value } = handlerIterator.next();
113
- if (value) {
114
- const { handler, params } = value;
115
- const context: EventContext<
116
- unknown,
117
- string,
118
- Record<string, unknown>
119
- > = {
101
+ const result = handlerIterator.next();
102
+ // Note we can't use `!result.done` because this doesn't narrow to the correct type
103
+ if (result.done == false) {
104
+ const { handler, params } = result.value;
105
+ const context = {
120
106
  request: new Request(request.clone()),
121
107
  next,
122
108
  params,
@@ -132,6 +118,12 @@ export default {
132
118
  [101, 204, 205, 304].includes(response.status) ? null : response.body,
133
119
  response
134
120
  );
121
+ } else if (__FALLBACK_SERVICE__) {
122
+ // There are no more handlers so finish with the fallback service (`env.ASSETS.fetch` in Pages' case)
123
+ return env[__FALLBACK_SERVICE__].fetch(request);
124
+ } else {
125
+ // There was not fallback service so actually make the request to the origin.
126
+ return fetch(request);
135
127
  }
136
128
  };
137
129
 
@@ -15,7 +15,7 @@ describe("Dev component", () => {
15
15
  accountId: "some-account-id",
16
16
  public: "some/public/path",
17
17
  });
18
- expect(lastFrame().split("\n").slice(0, 2).join("\n"))
18
+ expect(lastFrame()?.split("\n").slice(0, 2).join("\n"))
19
19
  .toMatchInlineSnapshot(`
20
20
  "Something went wrong:
21
21
  Error: You cannot use the service worker format with a \`public\` directory."
@@ -39,16 +39,19 @@ function renderDev({
39
39
  jsxFragment,
40
40
  bindings = {},
41
41
  public: publicDir,
42
- site,
42
+ assetPaths,
43
43
  compatibilityDate,
44
44
  compatibilityFlags,
45
45
  usageModel,
46
46
  buildCommand = {},
47
+ enableLocalPersistence = false,
48
+ env = undefined,
47
49
  }: Partial<DevProps>) {
48
50
  return render(
49
51
  <Dev
50
52
  name={name}
51
53
  entry={entry}
54
+ env={env}
52
55
  port={port}
53
56
  buildCommand={buildCommand}
54
57
  format={format}
@@ -56,12 +59,13 @@ function renderDev({
56
59
  jsxFactory={jsxFactory}
57
60
  jsxFragment={jsxFragment}
58
61
  accountId={accountId}
59
- site={site}
62
+ assetPaths={assetPaths}
60
63
  public={publicDir}
61
64
  compatibilityDate={compatibilityDate}
62
65
  compatibilityFlags={compatibilityFlags}
63
66
  usageModel={usageModel}
64
67
  bindings={bindings}
68
+ enableLocalPersistence={enableLocalPersistence}
65
69
  />
66
70
  );
67
71
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The typings file available at `@types/cmd-shim` are out of date.
3
+ */
4
+ module "cmd-shim" {
5
+ /**
6
+ *
7
+ * Create a cmd shim at `to` for the command line program at `from`.
8
+ *
9
+ */
10
+ export default function cmdShim(from: string, to: string): Promise<void>;
11
+ }
@@ -0,0 +1,30 @@
1
+ const ORIGINAL_CF_API_TOKEN = process.env.CF_API_TOKEN;
2
+ const ORIGINAL_CF_ACCOUNT_ID = process.env.CF_ACCOUNT_ID;
3
+
4
+ /**
5
+ * Mock the API token so that we don't need to read it from user configuration files.
6
+ */
7
+ export function mockApiToken({
8
+ apiToken = "some-api-token",
9
+ }: { apiToken?: string } = {}) {
10
+ beforeEach(() => {
11
+ process.env.CF_API_TOKEN = apiToken;
12
+ });
13
+ afterEach(() => {
14
+ process.env.CF_API_TOKEN = ORIGINAL_CF_API_TOKEN;
15
+ });
16
+ }
17
+
18
+ /**
19
+ * Mock the current account ID so that we don't need to read it from configuration files.
20
+ */
21
+ export function mockAccountId({
22
+ accountId = "some-account-id",
23
+ }: { accountId?: string } = {}) {
24
+ beforeEach(() => {
25
+ process.env.CF_ACCOUNT_ID = accountId;
26
+ });
27
+ afterEach(() => {
28
+ process.env.CF_ACCOUNT_ID = ORIGINAL_CF_ACCOUNT_ID;
29
+ });
30
+ }
@@ -0,0 +1,33 @@
1
+ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { basename, resolve } from "node:path";
3
+ import cmdShim from "cmd-shim";
4
+
5
+ const nodeShebang = "#!/usr/bin/env node";
6
+
7
+ /**
8
+ * Create a binary file in a temp directory and make it available on the PATH.
9
+ */
10
+ export async function mockBinary(binaryName: string, code: string) {
11
+ // Ensure there is a directory to put the mock binary in.
12
+ const tmpDir = resolve(mkdtempSync(".mock-binary-"));
13
+
14
+ // Use a fake extension on Windows because we will create a cmd-shim to run the binary.
15
+ const extension = process.platform === "win32" ? ".x-mock-bin" : "";
16
+ const filePath = resolve(tmpDir, `${binaryName}${extension}`);
17
+ writeFileSync(filePath, nodeShebang + "\n" + code);
18
+ chmodSync(filePath, 0o777);
19
+
20
+ if (process.platform === "win32") {
21
+ await cmdShim(filePath, basename(filePath, ".x-mock-bin"));
22
+ }
23
+
24
+ // Update PATH using the appropriate separator for the platform.
25
+ const oldPath = process.env.PATH;
26
+ const sep = process.platform === "win32" ? ";" : ":";
27
+ process.env.PATH = tmpDir + sep + oldPath;
28
+
29
+ return function unMock() {
30
+ rmSync(tmpDir, { recursive: true });
31
+ process.env.PATH = process.env.PATH?.replace(tmpDir + sep, "");
32
+ };
33
+ }
@@ -1,8 +1,8 @@
1
- import type { RequestInit } from "node-fetch";
2
- import { URLSearchParams } from "node:url";
1
+ import { URL, URLSearchParams } from "node:url";
3
2
  import { pathToRegexp } from "path-to-regexp";
4
- import { CF_API_BASE_URL } from "../cfetch";
5
- import type { FetchResult } from "../cfetch";
3
+ import { CF_API_BASE_URL } from "../../cfetch";
4
+ import type { FetchResult } from "../../cfetch";
5
+ import type { RequestInit } from "undici";
6
6
 
7
7
  /**
8
8
  * The signature of the function that will handle a mock request.
@@ -11,7 +11,7 @@ export type MockHandler<ResponseType> = (
11
11
  uri: RegExpExecArray,
12
12
  init: RequestInit,
13
13
  queryParams: URLSearchParams
14
- ) => ResponseType;
14
+ ) => ResponseType | Promise<ResponseType>;
15
15
 
16
16
  type RemoveMockFn = () => void;
17
17
 
@@ -38,13 +38,15 @@ export async function mockFetchInternal(
38
38
  const resourcePath = new URL(resource, CF_API_BASE_URL).pathname;
39
39
  const uri = regexp.exec(resourcePath);
40
40
  // Do the resource path and (if specified) the HTTP method match?
41
- if (uri !== null && (!method || method === init.method)) {
41
+ if (uri !== null && (!method || method === (init.method ?? "GET"))) {
42
42
  // The `resource` regular expression will extract the labelled groups from the URL.
43
43
  // These are passed through to the `handler` call, to allow it to do additional checks or behaviour.
44
- return handler(uri, init, queryParams); // TODO: should we have some kind of fallthrough system? we'll see.
44
+ return await handler(uri, init, queryParams); // TODO: should we have some kind of fallthrough system? we'll see.
45
45
  }
46
46
  }
47
- throw new Error(`no mocks found for ${init.method}: ${resource}`);
47
+ throw new Error(
48
+ `no mocks found for ${init.method ?? "any HTTP"} request to ${resource}`
49
+ );
48
50
  }
49
51
 
50
52
  /**
@@ -138,23 +140,23 @@ export function setMockResponse<ResponseType>(
138
140
  /**
139
141
  * A helper to make it easier to create `FetchResult` objects in tests.
140
142
  */
141
- export function createFetchResult<ResponseType>(
142
- result: ResponseType,
143
+ export async function createFetchResult<ResponseType>(
144
+ result: ResponseType | Promise<ResponseType>,
143
145
  success = true,
144
146
  errors = [],
145
147
  messages = [],
146
148
  result_info?: unknown
147
- ): FetchResult<ResponseType> {
149
+ ): Promise<FetchResult<ResponseType>> {
148
150
  return result_info
149
151
  ? {
150
- result,
152
+ result: await result,
151
153
  success,
152
154
  errors,
153
155
  messages,
154
156
  result_info,
155
157
  }
156
158
  : {
157
- result,
159
+ result: await result,
158
160
  success,
159
161
  errors,
160
162
  messages,
@@ -169,3 +171,37 @@ export function createFetchResult<ResponseType>(
169
171
  export function unsetAllMocks() {
170
172
  mocks.length = 0;
171
173
  }
174
+
175
+ /**
176
+ * We special-case fetching the request for `kv:key get`, because it's
177
+ * the only cloudflare API endpoint that returns a plain string as the
178
+ * value, and not as the "standard" FetchResult-style json. Hence, we also
179
+ * special-case mocking it here.
180
+ */
181
+
182
+ const kvGetMocks = new Map<string, string>();
183
+
184
+ export function mockFetchKVGetValue(
185
+ accountId: string,
186
+ namespaceId: string,
187
+ key: string
188
+ ) {
189
+ const mapKey = `${accountId}/${namespaceId}/${key}`;
190
+ if (kvGetMocks.has(mapKey)) {
191
+ return kvGetMocks.get(mapKey);
192
+ }
193
+ throw new Error(`no mock value found for \`kv:key get\` - ${mapKey}`);
194
+ }
195
+
196
+ export function setMockFetchKVGetValue(
197
+ accountId: string,
198
+ namespaceId: string,
199
+ key: string,
200
+ value: string
201
+ ) {
202
+ kvGetMocks.set(`${accountId}/${namespaceId}/${key}`, value);
203
+ }
204
+
205
+ export function unsetMockFetchKVGetValues() {
206
+ kvGetMocks.clear();
207
+ }