wrangler 0.0.0-ece06ea → 0.0.0-eebdb6c

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 (39) hide show
  1. package/package.json +22 -15
  2. package/pages/functions/buildWorker.ts +57 -0
  3. package/pages/functions/filepath-routing.test.ts +32 -0
  4. package/pages/functions/filepath-routing.ts +225 -0
  5. package/pages/functions/identifiers.ts +78 -0
  6. package/pages/functions/routes.ts +158 -0
  7. package/pages/functions/template-worker.ts +135 -0
  8. package/src/__tests__/index.test.ts +254 -120
  9. package/src/__tests__/jest.setup.ts +22 -0
  10. package/src/__tests__/kv.test.ts +1098 -0
  11. package/src/__tests__/mock-cfetch.ts +171 -0
  12. package/src/__tests__/mock-dialogs.ts +65 -0
  13. package/src/__tests__/run-in-tmp.ts +19 -0
  14. package/src/__tests__/run-wrangler.ts +32 -0
  15. package/src/api/form_data.ts +70 -97
  16. package/src/api/inspect.ts +0 -11
  17. package/src/api/preview.ts +29 -24
  18. package/src/api/worker.ts +34 -40
  19. package/src/cfetch/index.ts +103 -0
  20. package/src/cfetch/internal.ts +69 -0
  21. package/src/cli.ts +6 -7
  22. package/src/config.ts +14 -7
  23. package/src/dev.tsx +274 -131
  24. package/src/dialogs.tsx +4 -12
  25. package/src/index.tsx +304 -245
  26. package/src/kv.tsx +101 -45
  27. package/src/module-collection.ts +64 -0
  28. package/src/pages.tsx +681 -104
  29. package/src/proxy.ts +103 -0
  30. package/src/publish.ts +54 -58
  31. package/src/sites.tsx +5 -6
  32. package/src/tail.tsx +5 -3
  33. package/src/user.tsx +9 -16
  34. package/wrangler-dist/cli.js +36633 -49770
  35. package/wrangler-dist/cli.js.map +3 -3
  36. package/src/__tests__/fixtures/init/.gitkeep +0 -0
  37. package/src/__tests__/mock-cfetch.js +0 -46
  38. package/src/cfetch.ts +0 -72
  39. package/src/util/fetch.ts +0 -74
@@ -0,0 +1,171 @@
1
+ import type { RequestInit } from "node-fetch";
2
+ import type { URLSearchParams } from "node:url";
3
+ import { pathToRegexp } from "path-to-regexp";
4
+ import { CF_API_BASE_URL } from "../cfetch";
5
+ import type { FetchResult } from "../cfetch";
6
+
7
+ /**
8
+ * The signature of the function that will handle a mock request.
9
+ */
10
+ export type MockHandler<ResponseType> = (
11
+ uri: RegExpExecArray,
12
+ init?: RequestInit,
13
+ queryParams?: URLSearchParams
14
+ ) => ResponseType;
15
+
16
+ type RemoveMockFn = () => void;
17
+
18
+ interface MockFetch<ResponseType> {
19
+ regexp: RegExp;
20
+ method: string | undefined;
21
+ handler: MockHandler<ResponseType>;
22
+ }
23
+ const mocks: MockFetch<unknown>[] = [];
24
+
25
+ /**
26
+ * The mock implementation of `cfApi.fetch()`.
27
+ *
28
+ * This function will attempt to match the given request to one of the mock handlers configured by calls to `setMock`.
29
+ *
30
+ * Once found the handler will be used to generate a mock response.
31
+ */
32
+ export async function mockFetchInternal(
33
+ resource: string,
34
+ init: RequestInit = {},
35
+ queryParams?: URLSearchParams
36
+ ) {
37
+ for (const { regexp, method, handler } of mocks) {
38
+ const resourcePath = new URL(resource, CF_API_BASE_URL).pathname;
39
+ const uri = regexp.exec(resourcePath);
40
+ // Do the resource path and (if specified) the HTTP method match?
41
+ if (uri !== null && (!method || method === init.method)) {
42
+ // The `resource` regular expression will extract the labelled groups from the URL.
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.
45
+ }
46
+ }
47
+ throw new Error(`no mocks found for ${init.method}: ${resource}`);
48
+ }
49
+
50
+ /**
51
+ * Specify an expected resource path that is to be handled, resulting in a raw JSON response.
52
+ *
53
+ * @param resource The path of the resource to be matched.
54
+ * This can include wildcards whose value will be passed to the `handler`.
55
+ * @param handler The function that will generate the mock response for this request.
56
+ */
57
+ export function setMockRawResponse<ResponseType>(
58
+ resource: string,
59
+ handler: MockHandler<ResponseType>
60
+ ): RemoveMockFn;
61
+ /**
62
+ * Specify an expected resource path that is to be handled, resulting in a raw JSON response.
63
+ *
64
+ * @param resource The path of the resource to be matched.
65
+ * This can include wildcards whose value will be passed to the `handler`.
66
+ * @param method The HTTP method (e.g. GET, POST, etc) that the request must have to match this mock handler.
67
+ * @param handler The function that will generate the mock response for this request.
68
+ */
69
+ export function setMockRawResponse<ResponseType>(
70
+ resource: string,
71
+ method: string,
72
+ handler: MockHandler<ResponseType>
73
+ ): RemoveMockFn;
74
+ /**
75
+ * Specify an expected resource path that is to be handled, resulting in a raw JSON response.
76
+ */
77
+ export function setMockRawResponse<ResponseType>(
78
+ resource: string,
79
+ ...args: [string, MockHandler<ResponseType>] | [MockHandler<ResponseType>]
80
+ ): RemoveMockFn {
81
+ const handler = args.pop() as MockHandler<ResponseType>;
82
+ const method = args.pop() as string;
83
+ const mock = {
84
+ resource,
85
+ method,
86
+ handler,
87
+ regexp: pathToRegexp(resource),
88
+ };
89
+ mocks.push(mock);
90
+ return () => {
91
+ const mockIndex = mocks.indexOf(mock);
92
+ if (mockIndex !== -1) {
93
+ mocks.splice(mockIndex, 1);
94
+ }
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Specify an expected resource path that is to be handled, resulting in a `FetchRequest`.
100
+ *
101
+ * The mock `handler` should return the `result`, which will then be wrapped in a `FetchRequest` object.
102
+ *
103
+ * @param resource The path of the resource to be matched.
104
+ * This can include wildcards whose value will be passed to the `handler`.
105
+ * @param handler The function that will generate the mock response for this request.
106
+ */
107
+ export function setMockResponse<ResponseType>(
108
+ resource: string,
109
+ handler: MockHandler<ResponseType>
110
+ ): RemoveMockFn;
111
+ /**
112
+ * Specify an expected resource path that is to be handled, resulting in a FetchRequest..
113
+ *
114
+ * @param resource The path of the resource to be matched.
115
+ * This can include wildcards whose value will be passed to the `handler`.
116
+ * @param method The HTTP method (e.g. GET, POST, etc) that the request must have to match this mock handler.
117
+ * @param handler The function that will generate the mock response for this request.
118
+ */
119
+ export function setMockResponse<ResponseType>(
120
+ resource: string,
121
+ method: string,
122
+ handler: MockHandler<ResponseType>
123
+ ): RemoveMockFn;
124
+ /**
125
+ * Specify an expected resource path that is to be handled, resulting in a FetchRequest.
126
+ */
127
+ export function setMockResponse<ResponseType>(
128
+ resource: string,
129
+ ...args: [string, MockHandler<ResponseType>] | [MockHandler<ResponseType>]
130
+ ): RemoveMockFn {
131
+ const handler = args.pop() as MockHandler<ResponseType>;
132
+ const method = args.pop() as string;
133
+ return setMockRawResponse(resource, method, (...handlerArgs) =>
134
+ createFetchResult(handler(...handlerArgs))
135
+ );
136
+ }
137
+
138
+ /**
139
+ * A helper to make it easier to create `FetchResult` objects in tests.
140
+ */
141
+ export function createFetchResult<ResponseType>(
142
+ result: ResponseType,
143
+ success = true,
144
+ errors = [],
145
+ messages = [],
146
+ result_info?: unknown
147
+ ): FetchResult<ResponseType> {
148
+ return result_info
149
+ ? {
150
+ result,
151
+ success,
152
+ errors,
153
+ messages,
154
+ result_info,
155
+ }
156
+ : {
157
+ result,
158
+ success,
159
+ errors,
160
+ messages,
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Remove all the configured mock handlers.
166
+ *
167
+ * This should be called in an `afterEach()` block to ensure that mock handlers do not leak between tests.
168
+ */
169
+ export function unsetAllMocks() {
170
+ mocks.length = 0;
171
+ }
@@ -0,0 +1,65 @@
1
+ import { confirm, prompt } from "../dialogs";
2
+
3
+ /**
4
+ * The expected values for a confirmation request.
5
+ */
6
+ export interface ConfirmExpectation {
7
+ /** The text expected to be seen in the confirmation dialog. */
8
+ text: string;
9
+ /** The mock response send back from the confirmation dialog. */
10
+ result: boolean;
11
+ }
12
+
13
+ /**
14
+ * Mock the implementation of `confirm()` that will respond with configured results
15
+ * for configured confirmation text messages.
16
+ *
17
+ * If there is a call to `confirm()` that does not match any of the expectations
18
+ * then an error is thrown.
19
+ */
20
+ export function mockConfirm(...expectations: ConfirmExpectation[]) {
21
+ (confirm as jest.Mock).mockImplementation((text: string) => {
22
+ for (const { text: expectedText, result } of expectations) {
23
+ if (text === expectedText) {
24
+ return Promise.resolve(result);
25
+ }
26
+ }
27
+ throw new Error(`Unexpected confirmation message: ${text}`);
28
+ });
29
+ }
30
+
31
+ /**
32
+ * The expected values for a prompt request.
33
+ */
34
+ export interface PromptExpectation {
35
+ /** The text expected to be seen in the prompt dialog. */
36
+ text: string;
37
+ /** The type of the prompt. */
38
+ type: "text" | "password";
39
+ /** The mock response send back from the prompt dialog. */
40
+ result: string;
41
+ }
42
+
43
+ /**
44
+ * Mock the implementation of `prompt()` that will respond with configured results
45
+ * for configured prompt text messages.
46
+ *
47
+ * If there is a call to `prompt()` that does not match any of the expectations
48
+ * then an error is thrown.
49
+ */
50
+ export function mockPrompt(...expectations: PromptExpectation[]) {
51
+ (prompt as jest.Mock).mockImplementation(
52
+ (text: string, type: "text" | "password") => {
53
+ for (const {
54
+ text: expectedText,
55
+ type: expectedType,
56
+ result,
57
+ } of expectations) {
58
+ if (text === expectedText && type == expectedType) {
59
+ return Promise.resolve(result);
60
+ }
61
+ }
62
+ throw new Error(`Unexpected confirmation message: ${text}`);
63
+ }
64
+ );
65
+ }
@@ -0,0 +1,19 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
3
+ import * as fs from "fs";
4
+
5
+ const originalCwd = process.cwd();
6
+
7
+ export function runInTempDir() {
8
+ let tmpDir: string;
9
+
10
+ beforeEach(() => {
11
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wrangler-tests"));
12
+ process.chdir(tmpDir);
13
+ });
14
+
15
+ afterEach(() => {
16
+ process.chdir(originalCwd);
17
+ fs.rmSync(tmpDir, { recursive: true });
18
+ });
19
+ }
@@ -0,0 +1,32 @@
1
+ import { main } from "../index";
2
+
3
+ /**
4
+ * Run the wrangler CLI tool in tests, capturing the logging output.
5
+ */
6
+ export async function runWrangler(cmd?: string) {
7
+ const logSpy = jest.spyOn(console, "log").mockImplementation();
8
+ const errorSpy = jest.spyOn(console, "error").mockImplementation();
9
+ const warnSpy = jest.spyOn(console, "warn").mockImplementation();
10
+ try {
11
+ let error: unknown = undefined;
12
+ try {
13
+ await main(cmd?.split(" ") ?? []);
14
+ } catch (e) {
15
+ error = e;
16
+ }
17
+ return {
18
+ error,
19
+ stdout: captureCalls(logSpy),
20
+ stderr: captureCalls(errorSpy),
21
+ warnings: captureCalls(warnSpy),
22
+ };
23
+ } finally {
24
+ logSpy.mockRestore();
25
+ errorSpy.mockRestore();
26
+ warnSpy.mockRestore();
27
+ }
28
+ }
29
+
30
+ function captureCalls(spy: jest.SpyInstance): string {
31
+ return spy.mock.calls.flat(2).join("\n");
32
+ }
@@ -1,72 +1,11 @@
1
1
  import type {
2
2
  CfWorkerInit,
3
3
  CfModuleType,
4
- CfVariable,
5
4
  CfModule,
5
+ CfDurableObjectMigrations,
6
6
  } from "./worker.js";
7
7
  import { FormData, Blob } from "formdata-node";
8
8
 
9
- // Credit: https://stackoverflow.com/a/9458996
10
- function toBase64(source: BufferSource): string {
11
- let result = "";
12
- const buffer = source instanceof ArrayBuffer ? source : source.buffer;
13
- const bytes = new Uint8Array(buffer);
14
- for (let i = 0; i < bytes.byteLength; i++) {
15
- result += String.fromCharCode(bytes[i]);
16
- }
17
- return btoa(result);
18
- }
19
-
20
- function toBinding(
21
- name: string,
22
- variable: CfVariable
23
- ): Record<string, unknown> {
24
- if (typeof variable === "string") {
25
- return { name, type: "plain_text", text: variable };
26
- }
27
-
28
- if ("namespaceId" in variable) {
29
- return {
30
- name,
31
- type: "kv_namespace",
32
- namespace_id: variable.namespaceId,
33
- };
34
- }
35
-
36
- if ("class_name" in variable) {
37
- return {
38
- name,
39
- type: "durable_object_namespace",
40
- class_name: variable.class_name,
41
- ...(variable.script_name && {
42
- script_name: variable.script_name,
43
- }),
44
- };
45
- }
46
-
47
- const { format, algorithm, usages, data } = variable;
48
- if (format) {
49
- let key_base64;
50
- let key_jwk;
51
- if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
52
- key_base64 = toBase64(data);
53
- } else {
54
- key_jwk = data;
55
- }
56
- return {
57
- name,
58
- type: "secret_key",
59
- format,
60
- algorithm,
61
- usages,
62
- key_base64,
63
- key_jwk,
64
- };
65
- }
66
-
67
- throw new TypeError("Unsupported variable: " + variable);
68
- }
69
-
70
9
  export function toMimeType(type: CfModuleType): string {
71
10
  switch (type) {
72
11
  case "esm":
@@ -91,6 +30,29 @@ function toModule(module: CfModule, entryType?: CfModuleType): Blob {
91
30
  return new Blob([content], { type });
92
31
  }
93
32
 
33
+ interface WorkerMetadata {
34
+ compatibility_date?: string;
35
+ compatibility_flags?: string[];
36
+ usage_model?: "bundled" | "unbound";
37
+ migrations?: CfDurableObjectMigrations;
38
+ bindings: (
39
+ | { type: "kv_namespace"; name: string; namespace_id: string }
40
+ | { type: "plain_text"; name: string; text: string }
41
+ | {
42
+ type: "durable_object_namespace";
43
+ name: string;
44
+ class_name: string;
45
+ script_name?: string;
46
+ }
47
+ | {
48
+ type: "service";
49
+ name: string;
50
+ service: string;
51
+ environment: string;
52
+ }
53
+ )[];
54
+ }
55
+
94
56
  /**
95
57
  * Creates a `FormData` upload from a `CfWorkerInit`.
96
58
  */
@@ -99,50 +61,61 @@ export function toFormData(worker: CfWorkerInit): FormData {
99
61
  const {
100
62
  main,
101
63
  modules,
102
- variables,
64
+ bindings,
103
65
  migrations,
104
66
  usage_model,
105
67
  compatibility_date,
106
68
  compatibility_flags,
107
69
  } = worker;
108
- const { name, type: mainType } = main;
109
70
 
110
- const bindings = [];
111
- for (const [name, variable] of Object.entries(variables ?? {})) {
112
- const binding = toBinding(name, variable);
113
- bindings.push(binding);
114
- }
71
+ const metadataBindings: WorkerMetadata["bindings"] = [];
115
72
 
116
- const metadata =
117
- mainType !== "commonjs"
118
- ? {
119
- main_module: name,
120
- bindings,
121
- }
122
- : {
123
- body_part: name,
124
- bindings,
125
- };
126
- if (compatibility_date) {
127
- // @ts-expect-error - we should type metadata
128
- metadata.compatibility_date = compatibility_date;
129
- }
130
- if (compatibility_flags) {
131
- // @ts-expect-error - we should type metadata
132
- metadata.compatibility_flags = compatibility_flags;
133
- }
134
- if (usage_model) {
135
- // @ts-expect-error - we should type metadata
136
- metadata.usage_model = usage_model;
137
- }
138
- if (migrations) {
139
- // @ts-expect-error - we should type metadata
140
- metadata.migrations = migrations;
141
- }
73
+ bindings.kv_namespaces?.forEach(({ id, binding }) => {
74
+ metadataBindings.push({
75
+ name: binding,
76
+ type: "kv_namespace",
77
+ namespace_id: id,
78
+ });
79
+ });
80
+
81
+ bindings.durable_objects?.bindings.forEach(
82
+ ({ name, class_name, script_name }) => {
83
+ metadataBindings.push({
84
+ name,
85
+ type: "durable_object_namespace",
86
+ class_name: class_name,
87
+ ...(script_name && { script_name }),
88
+ });
89
+ }
90
+ );
91
+
92
+ Object.entries(bindings.vars || {})?.forEach(([key, value]) => {
93
+ metadataBindings.push({ name: key, type: "plain_text", text: value });
94
+ });
95
+
96
+ bindings.services?.forEach(({ name, service, environment }) => {
97
+ metadataBindings.push({
98
+ name,
99
+ type: "service",
100
+ service,
101
+ environment,
102
+ });
103
+ });
104
+
105
+ const metadata: WorkerMetadata = {
106
+ ...(main.type !== "commonjs"
107
+ ? { main_module: main.name }
108
+ : { body_part: main.name }),
109
+ bindings: metadataBindings,
110
+ ...(compatibility_date && { compatibility_date }),
111
+ ...(compatibility_flags && { compatibility_flags }),
112
+ ...(usage_model && { usage_model }),
113
+ ...(migrations && { migrations }),
114
+ };
142
115
 
143
116
  formData.set("metadata", JSON.stringify(metadata));
144
117
 
145
- if (mainType === "commonjs" && modules && modules.length > 0) {
118
+ if (main.type === "commonjs" && modules && modules.length > 0) {
146
119
  throw new TypeError(
147
120
  "More than one module can only be specified when type = 'esm'"
148
121
  );
@@ -150,7 +123,7 @@ export function toFormData(worker: CfWorkerInit): FormData {
150
123
 
151
124
  for (const module of [main].concat(modules || [])) {
152
125
  const { name } = module;
153
- const blob = toModule(module, mainType ?? "esm");
126
+ const blob = toModule(module, main.type ?? "esm");
154
127
  formData.set(name, blob, name);
155
128
  }
156
129
 
@@ -140,17 +140,6 @@ export type DtEvent = DtLogEvent | DtExceptionEvent;
140
140
  */
141
141
  export type DtListener = (event: DtEvent) => void;
142
142
 
143
- interface DtProtocolRequest<T> {
144
- id: number;
145
- method: string;
146
- params?: T;
147
- }
148
-
149
- interface DtProtocolResponse<T> {
150
- id: number;
151
- result: T;
152
- }
153
-
154
143
  /**
155
144
  * A DevTools inspector that listens to logs and debug events from a Worker.
156
145
  *
@@ -1,5 +1,5 @@
1
- import cfetch from "../cfetch";
2
- import { fetchJson } from "../util/fetch";
1
+ import fetch from "node-fetch";
2
+ import { fetchResult } from "../cfetch";
3
3
  import { toFormData } from "./form_data";
4
4
  import type { CfAccount, CfWorkerInit } from "./worker";
5
5
 
@@ -9,7 +9,7 @@ import type { CfAccount, CfWorkerInit } from "./worker";
9
9
  * * If true, then using a `workers.dev` subdomain.
10
10
  * * Otherwise, a list of routes under a single zone.
11
11
  */
12
- export type CfPreviewMode = { workers_dev: boolean } | { routes: string[] };
12
+ type CfPreviewMode = { workers_dev: boolean } | { routes: string[] };
13
13
 
14
14
  /**
15
15
  * A preview token.
@@ -60,17 +60,17 @@ async function sessionToken(account: CfAccount): Promise<CfPreviewToken> {
60
60
  ? `/zones/${zoneId}/workers/edge-preview`
61
61
  : `/accounts/${accountId}/workers/subdomain/edge-preview`;
62
62
 
63
- const { exchange_url: tokenUrl } = await cfetch<{ exchange_url: string }>(
64
- initUrl
65
- );
66
- const { inspector_websocket: url, token } = await fetchJson()(tokenUrl);
67
- const { host } = new URL(url);
63
+ const { exchange_url } = await fetchResult<{ exchange_url: string }>(initUrl);
64
+ const { inspector_websocket, token } = (await (
65
+ await fetch(exchange_url)
66
+ ).json()) as { inspector_websocket: string; token: string };
67
+ const { host } = new URL(inspector_websocket);
68
68
  const query = `cf_workers_preview_token=${token}`;
69
69
 
70
70
  return {
71
71
  value: token,
72
72
  host,
73
- inspectorUrl: new URL(`${url}?${query}`),
73
+ inspectorUrl: new URL(`${inspector_websocket}?${query}`),
74
74
  prewarmUrl: new URL(
75
75
  `https://${host}/cdn-cgi/workers/preview/prewarm?${query}`
76
76
  ),
@@ -96,27 +96,32 @@ export async function previewToken(
96
96
  const { value, host, inspectorUrl, prewarmUrl } = await sessionToken(account);
97
97
 
98
98
  const { accountId, zoneId } = account;
99
- const scriptId = zoneId ? randomId() : host.split(".")[0];
99
+ const scriptId = zoneId ? randomId() : worker.name || host.split(".")[0];
100
100
  const url = `/accounts/${accountId}/workers/scripts/${scriptId}/edge-preview`;
101
101
 
102
- const mode = zoneId ? { routes: ["*/*"] } : { workers_dev: true };
102
+ const mode: CfPreviewMode = zoneId
103
+ ? { routes: ["*/*"] }
104
+ : { workers_dev: true };
105
+
103
106
  const formData = toFormData(worker);
104
107
  formData.set("wrangler-session-config", JSON.stringify(mode));
105
108
 
106
- const { preview_token: token } = await cfetch<{ preview_token: string }>(
107
- url,
108
- {
109
- method: "POST",
110
- // @ts-expect-error TODO: fix this
111
- body: formData,
112
- headers: {
113
- "cf-preview-upload-config-token": value,
114
- },
115
- }
116
- );
109
+ const { preview_token } = await fetchResult<{ preview_token: string }>(url, {
110
+ method: "POST",
111
+ // @ts-expect-error TODO: fix this
112
+ body: formData,
113
+ headers: {
114
+ "cf-preview-upload-config-token": value,
115
+ },
116
+ });
117
+
117
118
  return {
118
- value: token,
119
- host,
119
+ value: preview_token,
120
+ // TODO: verify this works with zoned workers
121
+ host:
122
+ worker.name && !zoneId
123
+ ? `${worker.name}.${host.split(".").slice(1).join(".")}`
124
+ : host,
120
125
  inspectorUrl,
121
126
  prewarmUrl,
122
127
  };