wrangler 0.0.13 → 0.0.17

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 (67) hide show
  1. package/bin/wrangler.js +2 -2
  2. package/package.json +20 -11
  3. package/pages/functions/buildWorker.ts +1 -1
  4. package/pages/functions/filepath-routing.test.ts +112 -28
  5. package/pages/functions/filepath-routing.ts +44 -51
  6. package/pages/functions/routes.ts +11 -18
  7. package/pages/functions/template-worker.ts +3 -9
  8. package/src/__tests__/dev.test.tsx +42 -5
  9. package/src/__tests__/guess-worker-format.test.ts +66 -0
  10. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  11. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  12. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  13. package/src/__tests__/helpers/mock-account-id.ts +30 -0
  14. package/src/__tests__/helpers/mock-bin.ts +36 -0
  15. package/src/__tests__/{mock-cfetch.ts → helpers/mock-cfetch.ts} +43 -9
  16. package/src/__tests__/helpers/mock-console.ts +62 -0
  17. package/src/__tests__/{mock-dialogs.ts → helpers/mock-dialogs.ts} +1 -1
  18. package/src/__tests__/helpers/mock-kv.ts +40 -0
  19. package/src/__tests__/helpers/mock-user.ts +27 -0
  20. package/src/__tests__/helpers/mock-web-socket.ts +37 -0
  21. package/src/__tests__/{run-in-tmp.ts → helpers/run-in-tmp.ts} +1 -1
  22. package/src/__tests__/helpers/run-wrangler.ts +16 -0
  23. package/src/__tests__/helpers/write-wrangler-toml.ts +20 -0
  24. package/src/__tests__/index.test.ts +418 -71
  25. package/src/__tests__/jest.setup.ts +30 -2
  26. package/src/__tests__/kv.test.ts +147 -252
  27. package/src/__tests__/logout.test.ts +50 -0
  28. package/src/__tests__/package-manager.test.ts +206 -0
  29. package/src/__tests__/publish.test.ts +1136 -291
  30. package/src/__tests__/r2.test.ts +206 -0
  31. package/src/__tests__/secret.test.ts +210 -0
  32. package/src/__tests__/sentry.test.ts +146 -0
  33. package/src/__tests__/tail.test.ts +246 -0
  34. package/src/__tests__/whoami.test.tsx +6 -47
  35. package/src/api/form_data.ts +75 -25
  36. package/src/api/preview.ts +2 -2
  37. package/src/api/worker.ts +34 -15
  38. package/src/bundle.ts +127 -0
  39. package/src/cfetch/index.ts +7 -15
  40. package/src/cfetch/internal.ts +41 -6
  41. package/src/cli.ts +10 -0
  42. package/src/config.ts +125 -95
  43. package/src/dev.tsx +300 -193
  44. package/src/dialogs.tsx +2 -2
  45. package/src/guess-worker-format.ts +68 -0
  46. package/src/index.tsx +578 -192
  47. package/src/inspect.ts +29 -10
  48. package/src/kv.tsx +23 -17
  49. package/src/module-collection.ts +32 -12
  50. package/src/open-in-browser.ts +13 -0
  51. package/src/package-manager.ts +120 -0
  52. package/src/pages.tsx +28 -23
  53. package/src/paths.ts +26 -0
  54. package/src/proxy.ts +88 -14
  55. package/src/publish.ts +260 -297
  56. package/src/r2.ts +50 -0
  57. package/src/reporting.ts +115 -0
  58. package/src/sites.tsx +28 -27
  59. package/src/tail.tsx +178 -9
  60. package/src/user.tsx +58 -44
  61. package/templates/new-worker.js +15 -0
  62. package/templates/new-worker.ts +15 -0
  63. package/{static-asset-facade.js → templates/static-asset-facade.js} +0 -0
  64. package/wrangler-dist/cli.js +124315 -104677
  65. package/wrangler-dist/cli.js.map +3 -3
  66. package/src/__tests__/mock-console.ts +0 -34
  67. package/src/__tests__/run-wrangler.ts +0 -8
@@ -0,0 +1,246 @@
1
+ import { mockAccountId, mockApiToken } from "./helpers/mock-account-id";
2
+ import { setMockResponse } from "./helpers/mock-cfetch";
3
+ import { mockConsoleMethods } from "./helpers/mock-console";
4
+ import { WS } from "./helpers/mock-web-socket";
5
+ import { runInTempDir } from "./helpers/run-in-tmp";
6
+ import { runWrangler } from "./helpers/run-wrangler";
7
+ import type Websocket from "ws";
8
+
9
+ // change this if you're testing using debug mode
10
+ // (sends all logs regardless of filters)
11
+ const DEBUG = false;
12
+
13
+ describe("tail", () => {
14
+ runInTempDir();
15
+ mockAccountId();
16
+ mockApiToken();
17
+
18
+ const std = mockConsoleMethods();
19
+ const api = mockWebsocketApis();
20
+
21
+ /* API related functionality */
22
+
23
+ it("creates and then delete tails", async () => {
24
+ expect(api.requests.creation.count).toStrictEqual(0);
25
+
26
+ await runWrangler("tail test-worker");
27
+
28
+ await expect(api.ws.connected).resolves.toBeTruthy();
29
+ expect(api.requests.creation.count).toStrictEqual(1);
30
+ expect(api.requests.deletion.count).toStrictEqual(0);
31
+
32
+ api.ws.close();
33
+ expect(api.requests.deletion.count).toStrictEqual(1);
34
+ });
35
+
36
+ /* filtering */
37
+
38
+ it("sends sampling rate filters", async () => {
39
+ const tooHigh = runWrangler("tail test-worker --sampling-rate 10");
40
+ await expect(tooHigh).rejects.toThrow();
41
+
42
+ const tooLow = runWrangler("tail test-worker --sampling-rate -5");
43
+ await expect(tooLow).rejects.toThrow();
44
+
45
+ await runWrangler("tail test-worker --sampling-rate 0.25");
46
+ await expect(api.ws.nextMessageJson()).resolves.toHaveProperty("filters", [
47
+ { sampling_rate: 0.25 },
48
+ ]);
49
+ });
50
+
51
+ it("sends single status filters", async () => {
52
+ await runWrangler("tail test-worker --status error");
53
+ await expect(api.ws.nextMessageJson()).resolves.toHaveProperty("filters", [
54
+ { outcome: ["exception", "exceededCpu", "unknown"] },
55
+ ]);
56
+ });
57
+
58
+ it("sends multiple status filters", async () => {
59
+ await runWrangler("tail test-worker --status error --status canceled");
60
+ await expect(api.ws.nextMessageJson()).resolves.toHaveProperty("filters", [
61
+ { outcome: ["exception", "exceededCpu", "unknown", "canceled"] },
62
+ ]);
63
+ });
64
+
65
+ it("sends single HTTP method filters", async () => {
66
+ await runWrangler("tail test-worker --method POST");
67
+ await expect(api.ws.nextMessageJson()).resolves.toHaveProperty("filters", [
68
+ { method: ["POST"] },
69
+ ]);
70
+ });
71
+
72
+ it("sends multiple HTTP method filters", async () => {
73
+ await runWrangler("tail test-worker --method POST --method GET");
74
+ await expect(api.ws.nextMessageJson()).resolves.toHaveProperty("filters", [
75
+ { method: ["POST", "GET"] },
76
+ ]);
77
+ });
78
+
79
+ it("sends header filters without a query", async () => {
80
+ await runWrangler("tail test-worker --header X-CUSTOM-HEADER ");
81
+ await expect(api.ws.nextMessageJson()).resolves.toHaveProperty("filters", [
82
+ { header: { key: "X-CUSTOM-HEADER" } },
83
+ ]);
84
+ });
85
+
86
+ it("sends header filters with a query", async () => {
87
+ await runWrangler("tail test-worker --header X-CUSTOM-HEADER:some-value ");
88
+ await expect(api.ws.nextMessageJson()).resolves.toHaveProperty("filters", [
89
+ { header: { key: "X-CUSTOM-HEADER", query: "some-value" } },
90
+ ]);
91
+ });
92
+
93
+ it("sends single IP filters", async () => {
94
+ const fakeIp = "192.0.2.1";
95
+
96
+ await runWrangler(`tail test-worker --ip ${fakeIp}`);
97
+ await expect(api.ws.nextMessageJson()).resolves.toHaveProperty("filters", [
98
+ { client_ip: [fakeIp] },
99
+ ]);
100
+ });
101
+
102
+ it("sends multiple IP filters", async () => {
103
+ const fakeIp = "192.0.2.1";
104
+
105
+ await runWrangler(`tail test-worker --ip ${fakeIp} --ip self`);
106
+ await expect(api.ws.nextMessageJson()).resolves.toHaveProperty("filters", [
107
+ { client_ip: [fakeIp, "self"] },
108
+ ]);
109
+ });
110
+
111
+ it("sends search filters", async () => {
112
+ const search = "filterMe";
113
+
114
+ await runWrangler(`tail test-worker --search ${search}`);
115
+ await expect(api.ws.nextMessageJson()).resolves.toHaveProperty("filters", [
116
+ { query: search },
117
+ ]);
118
+ });
119
+
120
+ it("sends everything but the kitchen sink", async () => {
121
+ const sampling_rate = 0.69;
122
+ const status = ["ok", "error"];
123
+ const method = ["GET", "POST", "PUT"];
124
+ const header = "X-HELLO:world";
125
+ const client_ip = ["192.0.2.1", "self"];
126
+ const query = "onlyTheseMessagesPlease";
127
+
128
+ const cliFilters =
129
+ `--sampling-rate ${sampling_rate} ` +
130
+ status.map((s) => `--status ${s} `).join("") +
131
+ method.map((m) => `--method ${m} `).join("") +
132
+ `--header ${header} ` +
133
+ client_ip.map((c) => `--ip ${c} `).join("") +
134
+ `--search ${query}`;
135
+
136
+ const expectedWebsocketMessage = {
137
+ filters: [
138
+ { sampling_rate },
139
+ { outcome: ["ok", "exception", "exceededCpu", "unknown"] },
140
+ { method },
141
+ { header: { key: "X-HELLO", query: "world" } },
142
+ { client_ip },
143
+ { query },
144
+ ],
145
+ debug: DEBUG,
146
+ };
147
+
148
+ await runWrangler(`tail test-worker ${cliFilters}`);
149
+ await expect(api.ws.nextMessageJson()).resolves.toEqual(
150
+ expectedWebsocketMessage
151
+ );
152
+ });
153
+
154
+ /* Basic logging */
155
+
156
+ it("logs incoming messages", async () => {
157
+ await runWrangler("tail test-worker");
158
+ const greeting = serialize({ hello: "world!" });
159
+ api.ws.send(greeting);
160
+ expect(std.out).toMatch(deserializeToJson(greeting));
161
+ });
162
+ });
163
+
164
+ /* helpers */
165
+
166
+ function serialize(message: unknown): Websocket.RawData {
167
+ return Buffer.from(JSON.stringify(message), "utf-8");
168
+ }
169
+
170
+ function deserializeToJson(message: Websocket.RawData): string {
171
+ return JSON.stringify(JSON.parse(message.toString()), null, 2);
172
+ }
173
+
174
+ type RequestCounter = {
175
+ count: number;
176
+ };
177
+
178
+ function mockCreateTailRequest(websocketURL: string): RequestCounter {
179
+ const requests = { count: 0 };
180
+ setMockResponse(
181
+ "/accounts/:accountId/workers/scripts/:worker/tails",
182
+ "POST",
183
+ () => {
184
+ requests.count++;
185
+ return {
186
+ id: "tail-id",
187
+ url: websocketURL,
188
+ expires_at: new Date(3005, 0, 0),
189
+ };
190
+ }
191
+ );
192
+
193
+ return requests;
194
+ }
195
+
196
+ function mockDeleteTailRequest(): RequestCounter {
197
+ const requests = { count: 0 };
198
+ setMockResponse(
199
+ "/accounts/:accountId/workers/scripts/:worker/tails/:tailId",
200
+ "DELETE",
201
+ () => {
202
+ requests.count++;
203
+ return null;
204
+ }
205
+ );
206
+
207
+ return requests;
208
+ }
209
+
210
+ type MockAPI = {
211
+ requests: {
212
+ creation: RequestCounter;
213
+ deletion: RequestCounter;
214
+ };
215
+ ws: WS;
216
+ };
217
+
218
+ function mockWebsocketApis(websocketURL = "ws://localhost:1234"): MockAPI {
219
+ const api: MockAPI = {
220
+ requests: {
221
+ deletion: { count: 0 },
222
+ creation: { count: 0 },
223
+ },
224
+ ws: new WS(websocketURL),
225
+ };
226
+
227
+ // don't delete this line or else it breaks.
228
+ // we need to have api.ws be an instasnce of WS for
229
+ // the type checker to be happy, but if we have
230
+ // an actual open websocket then it causes problems
231
+ // since we create a new websocket beforeEach test.
232
+ // so we need to close it before we ever use it.
233
+ api.ws.close();
234
+
235
+ beforeEach(() => {
236
+ api.requests.creation = mockCreateTailRequest(websocketURL);
237
+ api.requests.deletion = mockDeleteTailRequest();
238
+ api.ws = new WS(websocketURL);
239
+ });
240
+
241
+ afterEach(() => {
242
+ api.ws.close();
243
+ });
244
+
245
+ return api;
246
+ }
@@ -1,32 +1,15 @@
1
- import React from "react";
2
- import os from "node:os";
3
- import path from "node:path";
4
1
  import { render } from "ink-testing-library";
5
- import type { UserInfo } from "../whoami";
6
- import { getUserInfo, WhoAmI } from "../whoami";
7
- import { runInTempDir } from "./run-in-tmp";
8
- import { mkdirSync, writeFileSync } from "node:fs";
9
- import { setMockResponse } from "./mock-cfetch";
2
+ import React from "react";
10
3
  import { initialise } from "../user";
11
-
12
- const ORIGINAL_CF_API_TOKEN = process.env.CF_API_TOKEN;
13
- const ORIGINAL_CF_ACCOUNT_ID = process.env.CF_ACCOUNT_ID;
4
+ import { getUserInfo, WhoAmI } from "../whoami";
5
+ import { setMockResponse } from "./helpers/mock-cfetch";
6
+ import { writeUserConfig } from "./helpers/mock-user";
7
+ import { runInTempDir } from "./helpers/run-in-tmp";
8
+ import type { UserInfo } from "../whoami";
14
9
 
15
10
  describe("getUserInfo()", () => {
16
11
  runInTempDir({ homedir: "./home" });
17
12
 
18
- beforeEach(() => {
19
- // Clear the environment variables, so we can control them in the tests
20
- delete process.env.CF_API_TOKEN;
21
- delete process.env.CF_ACCOUNT_ID;
22
- });
23
-
24
- afterEach(() => {
25
- // Reset any changes to the environment variables
26
- process.env.CF_API_TOKEN = ORIGINAL_CF_API_TOKEN;
27
- process.env.CF_ACCOUNT_ID = ORIGINAL_CF_ACCOUNT_ID;
28
- });
29
-
30
13
  it("should return undefined if there is no config file", async () => {
31
14
  await initialise();
32
15
  const userInfo = await getUserInfo();
@@ -101,27 +84,3 @@ describe("WhoAmI component", () => {
101
84
  expect(lastFrame()).toMatch(/Account Three .+ account-3/);
102
85
  });
103
86
  });
104
-
105
- function writeUserConfig(
106
- oauth_token?: string,
107
- refresh_token?: string,
108
- expiration_time?: string
109
- ) {
110
- const lines: string[] = [];
111
- if (oauth_token) {
112
- lines.push(`oauth_token = "${oauth_token}"`);
113
- }
114
- if (refresh_token) {
115
- lines.push(`refresh_token = "${refresh_token}"`);
116
- }
117
- if (expiration_time) {
118
- lines.push(`expiration_time = "${expiration_time}"`);
119
- }
120
- const configPath = path.join(os.homedir(), ".wrangler/config");
121
- mkdirSync(configPath, { recursive: true });
122
- writeFileSync(
123
- path.join(configPath, "default.toml"),
124
- lines.join("\n"),
125
- "utf-8"
126
- );
127
- }
@@ -1,10 +1,10 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { FormData, File } from "undici";
1
3
  import type {
2
4
  CfWorkerInit,
3
5
  CfModuleType,
4
- CfModule,
5
6
  CfDurableObjectMigrations,
6
7
  } from "./worker.js";
7
- import { FormData, Blob } from "formdata-node";
8
8
 
9
9
  export function toMimeType(type: CfModuleType): string {
10
10
  switch (type) {
@@ -23,14 +23,11 @@ export function toMimeType(type: CfModuleType): string {
23
23
  }
24
24
  }
25
25
 
26
- function toModule(module: CfModule, entryType: CfModuleType): Blob {
27
- const { type: moduleType, content } = module;
28
- const type = toMimeType(moduleType ?? entryType);
29
-
30
- return new Blob([content], { type });
31
- }
32
-
33
- interface WorkerMetadata {
26
+ export interface WorkerMetadata {
27
+ /** The name of the entry point module. Only exists when the worker is in the ES module format */
28
+ main_module?: string;
29
+ /** The name of the entry point module. Only exists when the worker is in the Service Worker format */
30
+ body_part?: string;
34
31
  compatibility_date?: string;
35
32
  compatibility_flags?: string[];
36
33
  usage_model?: "bundled" | "unbound";
@@ -38,18 +35,15 @@ interface WorkerMetadata {
38
35
  bindings: (
39
36
  | { type: "kv_namespace"; name: string; namespace_id: string }
40
37
  | { type: "plain_text"; name: string; text: string }
38
+ | { type: "json"; name: string; json: unknown }
39
+ | { type: "wasm_module"; name: string; part: string }
41
40
  | {
42
41
  type: "durable_object_namespace";
43
42
  name: string;
44
43
  class_name: string;
45
44
  script_name?: string;
46
45
  }
47
- | {
48
- type: "service";
49
- name: string;
50
- service: string;
51
- environment: string;
52
- }
46
+ | { type: "r2_bucket"; name: string; bucket_name: string }
53
47
  )[];
54
48
  }
55
49
 
@@ -89,18 +83,71 @@ export function toFormData(worker: CfWorkerInit): FormData {
89
83
  }
90
84
  );
91
85
 
86
+ bindings.r2_buckets?.forEach(({ binding, bucket_name }) => {
87
+ metadataBindings.push({
88
+ name: binding,
89
+ type: "r2_bucket",
90
+ bucket_name,
91
+ });
92
+ });
93
+
92
94
  Object.entries(bindings.vars || {})?.forEach(([key, value]) => {
93
- metadataBindings.push({ name: key, type: "plain_text", text: value });
95
+ if (typeof value === "string") {
96
+ metadataBindings.push({ name: key, type: "plain_text", text: value });
97
+ } else {
98
+ metadataBindings.push({ name: key, type: "json", json: value });
99
+ }
94
100
  });
95
101
 
96
- bindings.services?.forEach(({ name, service, environment }) => {
102
+ for (const [name, filePath] of Object.entries(bindings.wasm_modules || {})) {
97
103
  metadataBindings.push({
98
104
  name,
99
- type: "service",
100
- service,
101
- environment,
105
+ type: "wasm_module",
106
+ part: name,
102
107
  });
103
- });
108
+
109
+ formData.set(
110
+ name,
111
+ new File([readFileSync(filePath)], filePath, {
112
+ type: "application/wasm",
113
+ })
114
+ );
115
+ }
116
+
117
+ if (main.type === "commonjs") {
118
+ // This is a service-worker format worker.
119
+ // So we convert all `.wasm` modules into `wasm_module` bindings.
120
+ for (const [index, module] of Object.entries(modules || [])) {
121
+ if (module.type === "compiled-wasm") {
122
+ // The "name" of the module is a file path. We use it
123
+ // to instead be a "part" of the body, and a reference
124
+ // that we can use inside our source. This identifier has to be a valid
125
+ // JS identifier, so we replace all non alphanumeric characters
126
+ // with an underscore.
127
+ const name = module.name.replace(/[^a-zA-Z0-9_$]/g, "_");
128
+ metadataBindings.push({
129
+ name,
130
+ type: "wasm_module",
131
+ part: name,
132
+ });
133
+
134
+ // Add the module to the form data.
135
+ formData.set(
136
+ name,
137
+ new File([module.content], module.name, {
138
+ type: "application/wasm",
139
+ })
140
+ );
141
+ // And then remove it from the modules collection
142
+ modules?.splice(parseInt(index, 10), 1);
143
+ }
144
+ }
145
+ }
146
+
147
+ if (bindings.unsafe) {
148
+ // @ts-expect-error unsafe bindings don't need to match a specific type here
149
+ metadataBindings.push(...bindings.unsafe);
150
+ }
104
151
 
105
152
  const metadata: WorkerMetadata = {
106
153
  ...(main.type !== "commonjs"
@@ -122,9 +169,12 @@ export function toFormData(worker: CfWorkerInit): FormData {
122
169
  }
123
170
 
124
171
  for (const module of [main].concat(modules || [])) {
125
- const { name } = module;
126
- const blob = toModule(module, main.type ?? "esm");
127
- formData.set(name, blob, name);
172
+ formData.set(
173
+ module.name,
174
+ new File([module.content], module.name, {
175
+ type: toMimeType(module.type ?? main.type ?? "esm"),
176
+ })
177
+ );
128
178
  }
129
179
 
130
180
  return formData;
@@ -1,4 +1,5 @@
1
- import fetch from "node-fetch";
1
+ import { URL } from "node:url";
2
+ import { fetch } from "undici";
2
3
  import { fetchResult } from "../cfetch";
3
4
  import { toFormData } from "./form_data";
4
5
  import type { CfAccount, CfWorkerInit } from "./worker";
@@ -108,7 +109,6 @@ export async function previewToken(
108
109
 
109
110
  const { preview_token } = await fetchResult<{ preview_token: string }>(url, {
110
111
  method: "POST",
111
- // @ts-expect-error TODO: fix this
112
112
  body: formData,
113
113
  headers: {
114
114
  "cf-preview-upload-config-token": value,
package/src/api/worker.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { CfPreviewToken } from "./preview";
1
+ import { fetch } from "undici";
2
2
  import { previewToken } from "./preview";
3
- import fetch from "node-fetch";
3
+ import type { CfPreviewToken } from "./preview";
4
4
 
5
5
  /**
6
6
  * A Cloudflare account.
@@ -22,6 +22,11 @@ export interface CfAccount {
22
22
  zoneId?: string;
23
23
  }
24
24
 
25
+ /**
26
+ * The type of Worker
27
+ */
28
+ export type CfScriptFormat = "modules" | "service-worker";
29
+
25
30
  /**
26
31
  * A module type.
27
32
  */
@@ -53,7 +58,7 @@ export interface CfModule {
53
58
  * }
54
59
  * }
55
60
  */
56
- content: string | BufferSource;
61
+ content: string | Buffer;
57
62
  /**
58
63
  * The module type.
59
64
  *
@@ -66,7 +71,7 @@ export interface CfModule {
66
71
  * A map of variable names to values.
67
72
  */
68
73
  interface CfVars {
69
- [key: string]: string;
74
+ [key: string]: unknown;
70
75
  }
71
76
 
72
77
  /**
@@ -77,6 +82,14 @@ interface CfKvNamespace {
77
82
  id: string;
78
83
  }
79
84
 
85
+ /**
86
+ * A binding to a wasm module (in service worker format)
87
+ */
88
+
89
+ interface CfWasmModuleBindings {
90
+ [key: string]: string;
91
+ }
92
+
80
93
  /**
81
94
  * A Durable Object.
82
95
  */
@@ -86,13 +99,14 @@ interface CfDurableObject {
86
99
  script_name?: string;
87
100
  }
88
101
 
89
- /**
90
- * A Service.
91
- */
92
- interface CfService {
102
+ interface CfR2Bucket {
103
+ binding: string;
104
+ bucket_name: string;
105
+ }
106
+
107
+ interface CfUnsafeBinding {
93
108
  name: string;
94
- service: string;
95
- environment: string;
109
+ type: string;
96
110
  }
97
111
 
98
112
  export interface CfDurableObjectMigrations {
@@ -100,7 +114,10 @@ export interface CfDurableObjectMigrations {
100
114
  new_tag: string;
101
115
  steps: {
102
116
  new_classes?: string[];
103
- renamed_classes?: string[];
117
+ renamed_classes?: {
118
+ from: string;
119
+ to: string;
120
+ }[];
104
121
  deleted_classes?: string[];
105
122
  }[];
106
123
  }
@@ -125,10 +142,12 @@ export interface CfWorkerInit {
125
142
  * All the bindings
126
143
  */
127
144
  bindings: {
128
- kv_namespaces?: CfKvNamespace[];
129
- durable_objects?: { bindings: CfDurableObject[] };
130
- vars?: CfVars;
131
- services?: CfService[];
145
+ vars: CfVars | undefined;
146
+ kv_namespaces: CfKvNamespace[] | undefined;
147
+ wasm_modules: CfWasmModuleBindings | undefined;
148
+ durable_objects: { bindings: CfDurableObject[] } | undefined;
149
+ r2_buckets: CfR2Bucket[] | undefined;
150
+ unsafe: CfUnsafeBinding[] | undefined;
132
151
  };
133
152
  migrations: undefined | CfDurableObjectMigrations;
134
153
  compatibility_date: string | undefined;
package/src/bundle.ts ADDED
@@ -0,0 +1,127 @@
1
+ import assert from "node:assert";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import * as esbuild from "esbuild";
5
+ import makeModuleCollector from "./module-collection";
6
+ import type { CfModule, CfScriptFormat } from "./api/worker";
7
+
8
+ /**
9
+ * An entry point for the worker. It consists not just of a `file`,
10
+ * but also of a `directory` that is used to resolve relative paths.
11
+ */
12
+ export type Entry = { file: string; directory: string };
13
+
14
+ type BundleResult = {
15
+ modules: CfModule[];
16
+ resolvedEntryPointPath: string;
17
+ bundleType: "esm" | "commonjs";
18
+ stop: (() => void) | undefined;
19
+ };
20
+
21
+ /**
22
+ * Generate a bundle for the worker identified by the arguments passed in.
23
+ */
24
+ export async function bundleWorker(
25
+ entry: Entry,
26
+ serveAssetsFromWorker: boolean,
27
+ destination: string,
28
+ jsxFactory: string | undefined,
29
+ jsxFragment: string | undefined,
30
+ format: CfScriptFormat,
31
+ watch?: esbuild.WatchMode
32
+ ): Promise<BundleResult> {
33
+ const moduleCollector = makeModuleCollector({ format });
34
+ const result = await esbuild.build({
35
+ ...getEntryPoint(entry.file, serveAssetsFromWorker),
36
+ bundle: true,
37
+ absWorkingDir: entry.directory,
38
+ outdir: destination,
39
+ external: ["__STATIC_CONTENT_MANIFEST"],
40
+ format: "esm",
41
+ sourcemap: true,
42
+ metafile: true,
43
+ conditions: ["worker", "browser"],
44
+ loader: {
45
+ ".js": "jsx",
46
+ ".html": "text",
47
+ ".pem": "text",
48
+ ".txt": "text",
49
+ },
50
+ plugins: [moduleCollector.plugin],
51
+ ...(jsxFactory && { jsxFactory }),
52
+ ...(jsxFragment && { jsxFragment }),
53
+ watch,
54
+ });
55
+
56
+ const entryPointOutputs = Object.entries(result.metafile.outputs).filter(
57
+ ([_path, output]) => output.entryPoint !== undefined
58
+ );
59
+ assert(
60
+ entryPointOutputs.length > 0,
61
+ `Cannot find entry-point "${entry.file}" in generated bundle.` +
62
+ listEntryPoints(entryPointOutputs)
63
+ );
64
+ assert(
65
+ entryPointOutputs.length < 2,
66
+ "More than one entry-point found for generated bundle." +
67
+ listEntryPoints(entryPointOutputs)
68
+ );
69
+
70
+ const entryPointExports = entryPointOutputs[0][1].exports;
71
+ const bundleType = entryPointExports.length > 0 ? "esm" : "commonjs";
72
+
73
+ return {
74
+ modules: moduleCollector.modules,
75
+ resolvedEntryPointPath: path.resolve(
76
+ entry.directory,
77
+ entryPointOutputs[0][0]
78
+ ),
79
+ bundleType,
80
+ stop: result.stop,
81
+ };
82
+ }
83
+
84
+ type EntryPoint =
85
+ | { stdin: esbuild.StdinOptions; nodePaths: string[] }
86
+ | { entryPoints: string[] };
87
+
88
+ /**
89
+ * Create an object that describes the entry point for esbuild.
90
+ *
91
+ * If we are using the experimental asset handling, then the entry point is
92
+ * actually a shim worker that will either return an asset from a KV store,
93
+ * or delegate to the actual worker.
94
+ */
95
+ function getEntryPoint(
96
+ entryFile: string,
97
+ serveAssetsFromWorker: boolean
98
+ ): EntryPoint {
99
+ if (serveAssetsFromWorker) {
100
+ return {
101
+ stdin: {
102
+ contents: fs
103
+ .readFileSync(
104
+ path.join(__dirname, "../templates/static-asset-facade.js"),
105
+ "utf8"
106
+ )
107
+ .replace("__ENTRY_POINT__", entryFile),
108
+ sourcefile: "static-asset-facade.js",
109
+ resolveDir: path.dirname(entryFile),
110
+ },
111
+ nodePaths: [path.join(__dirname, "../vendor")],
112
+ };
113
+ } else {
114
+ return { entryPoints: [entryFile] };
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Generate a string that describes the entry-points that were identified by esbuild.
120
+ */
121
+ function listEntryPoints(
122
+ outputs: [string, ValueOf<esbuild.Metafile["outputs"]>][]
123
+ ): string {
124
+ return outputs.map(([_input, output]) => output.entryPoint).join("\n");
125
+ }
126
+
127
+ type ValueOf<T> = T[keyof T];