wrangler 2.0.29 → 2.1.2

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 (45) hide show
  1. package/miniflare-dist/index.mjs +1136 -372
  2. package/package.json +3 -2
  3. package/src/__tests__/helpers/mock-cfetch.ts +39 -19
  4. package/src/__tests__/helpers/mock-console.ts +11 -2
  5. package/src/__tests__/helpers/msw/handlers/index.ts +13 -0
  6. package/src/__tests__/helpers/msw/handlers/namespaces.ts +104 -0
  7. package/src/__tests__/helpers/msw/handlers/oauth.ts +36 -0
  8. package/src/__tests__/helpers/msw/handlers/r2.ts +80 -0
  9. package/src/__tests__/helpers/msw/handlers/user.ts +63 -0
  10. package/src/__tests__/helpers/msw/index.ts +4 -0
  11. package/src/__tests__/index.test.ts +9 -7
  12. package/src/__tests__/init.test.ts +356 -5
  13. package/src/__tests__/jest.setup.ts +16 -0
  14. package/src/__tests__/middleware.test.ts +768 -0
  15. package/src/__tests__/pages.test.ts +11 -12
  16. package/src/__tests__/publish.test.ts +516 -438
  17. package/src/__tests__/r2.test.ts +128 -93
  18. package/src/__tests__/secret.test.ts +78 -0
  19. package/src/__tests__/tail.test.ts +47 -74
  20. package/src/__tests__/whoami.test.tsx +49 -64
  21. package/src/api/dev.ts +23 -4
  22. package/src/bundle.ts +225 -1
  23. package/src/dev/dev.tsx +3 -1
  24. package/src/dev/local.tsx +2 -2
  25. package/src/dev/remote.tsx +6 -3
  26. package/src/dev/start-server.ts +11 -7
  27. package/src/dev/use-esbuild.ts +4 -0
  28. package/src/dev.tsx +6 -16
  29. package/src/dialogs.tsx +12 -0
  30. package/src/index.tsx +95 -4
  31. package/src/init.ts +286 -11
  32. package/src/miniflare-cli/assets.ts +130 -415
  33. package/src/miniflare-cli/index.ts +3 -1
  34. package/src/pages/dev.tsx +5 -1
  35. package/src/pages/hash.tsx +13 -0
  36. package/src/pages/upload.tsx +3 -18
  37. package/src/publish.ts +38 -4
  38. package/src/tail/filters.ts +1 -5
  39. package/src/tail/index.ts +6 -3
  40. package/templates/middleware/common.ts +62 -0
  41. package/templates/middleware/loader-modules.ts +84 -0
  42. package/templates/middleware/loader-sw.ts +213 -0
  43. package/templates/middleware/middleware-pretty-error.ts +40 -0
  44. package/templates/middleware/middleware-scheduled.ts +14 -0
  45. package/wrangler-dist/cli.js +65900 -65432
@@ -1,20 +1,27 @@
1
1
  import { existsSync, lstatSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import {
4
- generateRulesMatcher,
5
- replacer,
6
- } from "@cloudflare/pages-shared/src/asset-server/rulesEngine";
3
+ import { createMetadataObject } from "@cloudflare/pages-shared/src/metadata-generator/createMetadataObject";
4
+ import { parseHeaders } from "@cloudflare/pages-shared/src/metadata-generator/parseHeaders";
5
+ import { parseRedirects } from "@cloudflare/pages-shared/src/metadata-generator/parseRedirects";
7
6
  import { fetch as miniflareFetch } from "@miniflare/core";
7
+ import {
8
+ Response as MiniflareResponse,
9
+ Request as MiniflareRequest,
10
+ } from "@miniflare/core";
8
11
  import { watch } from "chokidar";
9
12
  import { getType } from "mime";
10
- import { Response } from "miniflare";
11
- import type { Headers as MiniflareHeaders } from "@miniflare/core";
12
- import type { Log } from "miniflare";
13
+ import { hashFile } from "../pages/hash";
14
+ import type { Metadata } from "@cloudflare/pages-shared/src/asset-server/metadata";
13
15
  import type {
14
- Request as MiniflareRequest,
15
- RequestInfo,
16
- RequestInit,
17
- } from "miniflare";
16
+ fetch,
17
+ Request,
18
+ } from "@cloudflare/pages-shared/src/environment-polyfills/types";
19
+ import type {
20
+ ParsedRedirects,
21
+ ParsedHeaders,
22
+ } from "@cloudflare/pages-shared/src/metadata-generator/types";
23
+ import type { RequestInfo, RequestInit, FetcherFetch } from "@miniflare/core";
24
+ import type { Log } from "miniflare";
18
25
 
19
26
  export interface Options {
20
27
  log: Log;
@@ -28,204 +35,49 @@ export default async function generateASSETSBinding(options: Options) {
28
35
  ? await generateAssetsFetch(options.directory, options.log)
29
36
  : invalidAssetsFetch;
30
37
 
31
- return async function (request: MiniflareRequest) {
38
+ return async function (miniflareRequest: MiniflareRequest) {
32
39
  if (options.proxyPort) {
33
40
  try {
34
- const url = new URL(request.url);
41
+ const url = new URL(miniflareRequest.url);
35
42
  url.host = `localhost:${options.proxyPort}`;
36
- return await miniflareFetch(url, request);
43
+ return await miniflareFetch(url, miniflareRequest);
37
44
  } catch (thrown) {
38
45
  options.log.error(new Error(`Could not proxy request: ${thrown}`));
39
46
 
40
47
  // TODO: Pretty error page
41
- return new Response(`[wrangler] Could not proxy request: ${thrown}`, {
42
- status: 502,
43
- });
48
+ return new MiniflareResponse(
49
+ `[wrangler] Could not proxy request: ${thrown}`,
50
+ {
51
+ status: 502,
52
+ }
53
+ );
44
54
  }
45
55
  } else {
46
56
  try {
47
- return await assetsFetch(request);
57
+ return await assetsFetch(miniflareRequest);
48
58
  } catch (thrown) {
49
59
  options.log.error(new Error(`Could not serve static asset: ${thrown}`));
50
60
 
51
61
  // TODO: Pretty error page
52
- return new Response(
62
+ return new MiniflareResponse(
53
63
  `[wrangler] Could not serve static asset: ${thrown}`,
54
64
  { status: 502 }
55
65
  );
56
66
  }
57
67
  }
58
- };
59
- }
60
-
61
- function generateHeadersMatcher(headersFile: string) {
62
- if (existsSync(headersFile)) {
63
- const contents = readFileSync(headersFile).toString();
64
-
65
- // TODO: Log errors
66
- const lines = contents
67
- .split("\n")
68
- .map((line) => line.trim())
69
- .filter((line) => !line.startsWith("#") && line !== "");
70
-
71
- const rules: Record<string, Record<string, string>> = {};
72
- let rule: { path: string; headers: Record<string, string> } | undefined =
73
- undefined;
74
-
75
- for (const line of lines) {
76
- if (/^([^\s]+:\/\/|^\/)/.test(line)) {
77
- if (rule && Object.keys(rule.headers).length > 0) {
78
- rules[rule.path] = rule.headers;
79
- }
80
-
81
- const path = validateURL(line);
82
- if (path) {
83
- rule = {
84
- path,
85
- headers: {},
86
- };
87
- continue;
88
- }
89
- }
90
-
91
- if (!line.includes(":")) continue;
92
-
93
- const [rawName, ...rawValue] = line.split(":");
94
- const name = rawName.trim().toLowerCase();
95
- const value = rawValue.join(":").trim();
96
-
97
- if (name === "") continue;
98
- if (!rule) continue;
99
-
100
- const existingValues = rule.headers[name];
101
- rule.headers[name] = existingValues
102
- ? `${existingValues}, ${value}`
103
- : value;
104
- }
105
-
106
- if (rule && Object.keys(rule.headers).length > 0) {
107
- rules[rule.path] = rule.headers;
108
- }
109
-
110
- const rulesMatcher = generateRulesMatcher(rules, (match, replacements) =>
111
- Object.fromEntries(
112
- Object.entries(match).map(([name, value]) => [
113
- name,
114
- replacer(value, replacements),
115
- ])
116
- )
117
- );
118
-
119
- return (request: MiniflareRequest) => {
120
- const matches = rulesMatcher({
121
- request,
122
- });
123
- if (matches) return matches;
124
- };
125
- } else {
126
- return () => undefined;
127
- }
128
- }
129
-
130
- function generateRedirectsMatcher(redirectsFile: string) {
131
- if (existsSync(redirectsFile)) {
132
- const contents = readFileSync(redirectsFile).toString();
133
-
134
- // TODO: Log errors
135
- const lines = contents
136
- .split("\n")
137
- .map((line) => line.trim())
138
- .filter((line) => !line.startsWith("#") && line !== "");
139
-
140
- const rules = Object.fromEntries(
141
- lines
142
- .map((line) => line.split(" "))
143
- .filter((tokens) => tokens.length === 2 || tokens.length === 3)
144
- .map((tokens) => {
145
- const from = validateURL(tokens[0], true, false, false);
146
- const to = validateURL(tokens[1], false, true, true);
147
- let status: number | undefined = parseInt(tokens[2]) || 302;
148
- status = [301, 302, 303, 307, 308].includes(status)
149
- ? status
150
- : undefined;
151
-
152
- return from && to && status ? [from, { to, status }] : undefined;
153
- })
154
- .filter((rule) => rule !== undefined) as [
155
- string,
156
- { to: string; status?: number }
157
- ][]
158
- );
159
-
160
- const rulesMatcher = generateRulesMatcher(
161
- rules,
162
- ({ status, to }, replacements) => ({
163
- status,
164
- to: replacer(to, replacements),
165
- })
166
- );
167
-
168
- return (request: MiniflareRequest) => {
169
- const match = rulesMatcher({
170
- request,
171
- })[0];
172
- if (match) return match;
173
- };
174
- } else {
175
- return () => undefined;
176
- }
177
- }
178
-
179
- function extractPathname(
180
- path = "/",
181
- includeSearch: boolean,
182
- includeHash: boolean
183
- ) {
184
- if (!path.startsWith("/")) path = `/${path}`;
185
- const url = new URL(`//${path}`, "relative://");
186
- return `${url.pathname}${includeSearch ? url.search : ""}${
187
- includeHash ? url.hash : ""
188
- }`;
189
- }
190
-
191
- function validateURL(
192
- token: string,
193
- onlyRelative = false,
194
- includeSearch = false,
195
- includeHash = false
196
- ) {
197
- const host = /^https:\/\/+(?<host>[^/]+)\/?(?<path>.*)/.exec(token);
198
- if (host && host.groups && host.groups.host) {
199
- if (onlyRelative) return;
200
-
201
- return `https://${host.groups.host}${extractPathname(
202
- host.groups.path,
203
- includeSearch,
204
- includeHash
205
- )}`;
206
- } else {
207
- if (!token.startsWith("/") && onlyRelative) token = `/${token}`;
208
-
209
- const path = /^\//.exec(token);
210
- if (path) {
211
- try {
212
- return extractPathname(token, includeSearch, includeHash);
213
- } catch {}
214
- }
215
- }
216
- return "";
217
- }
218
-
219
- function hasFileExtension(pathname: string) {
220
- return /\/.+\.[a-z0-9]+$/i.test(pathname);
68
+ } as FetcherFetch;
221
69
  }
222
70
 
223
71
  async function generateAssetsFetch(
224
72
  directory: string,
225
73
  log: Log
226
- ): Promise<typeof miniflareFetch> {
74
+ ): Promise<typeof fetch> {
227
75
  // Defer importing miniflare until we really need it
228
- const { Headers, Request } = await import("@miniflare/core");
76
+ await import("@cloudflare/pages-shared/src/environment-polyfills/miniflare");
77
+
78
+ const { generateHandler, parseQualityWeightedList } = await import(
79
+ "@cloudflare/pages-shared/src/asset-server/handler"
80
+ );
229
81
 
230
82
  const headersFile = join(directory, "_headers");
231
83
  const redirectsFile = join(directory, "_redirects");
@@ -233,260 +85,123 @@ async function generateAssetsFetch(
233
85
 
234
86
  const ignoredFiles = [headersFile, redirectsFile, workerFile];
235
87
 
236
- const assetExists = (path: string) => {
237
- path = join(directory, path);
238
- return (
239
- existsSync(path) &&
240
- lstatSync(path).isFile() &&
241
- !ignoredFiles.includes(path)
242
- );
243
- };
244
-
245
- const getAsset = (path: string) => {
246
- if (assetExists(path)) {
247
- return join(directory, path);
248
- }
249
- };
88
+ let redirects: ParsedRedirects | undefined;
89
+ if (existsSync(redirectsFile)) {
90
+ const contents = readFileSync(redirectsFile, "utf-8");
91
+ redirects = parseRedirects(contents);
92
+ }
250
93
 
251
- let redirectsMatcher = generateRedirectsMatcher(redirectsFile);
252
- let headersMatcher = generateHeadersMatcher(headersFile);
94
+ let headers: ParsedHeaders | undefined;
95
+ if (existsSync(headersFile)) {
96
+ const contents = readFileSync(headersFile, "utf-8");
97
+ headers = parseHeaders(contents);
98
+ }
253
99
 
254
- watch([headersFile, redirectsFile], {
255
- persistent: true,
256
- }).on("change", (path) => {
257
- switch (path) {
258
- case headersFile: {
259
- log.log("_headers modified. Re-evaluating...");
260
- headersMatcher = generateHeadersMatcher(headersFile);
261
- break;
262
- }
263
- case redirectsFile: {
264
- log.log("_redirects modified. Re-evaluating...");
265
- redirectsMatcher = generateRedirectsMatcher(redirectsFile);
266
- break;
267
- }
268
- }
100
+ let metadata = createMetadataObject({
101
+ redirects,
102
+ headers,
103
+ logger: log.warn.bind(log),
269
104
  });
270
105
 
271
- const serveAsset = (file: string) => {
272
- return readFileSync(file);
273
- };
274
-
275
- const generateResponse = (request: MiniflareRequest) => {
276
- const url = new URL(request.url);
277
- let assetName = url.pathname;
278
- try {
279
- //it's possible for someone to send a URL like http://fakehost/abc%2 which would fail to decode
280
- assetName = decodeURIComponent(url.pathname);
281
- } catch {}
282
-
283
- const deconstructedResponse: {
284
- status: number;
285
- headers: MiniflareHeaders;
286
- body?: Buffer;
287
- } = {
288
- status: 200,
289
- headers: new Headers(),
290
- body: undefined,
291
- };
292
-
293
- const match = redirectsMatcher(request);
294
- if (match) {
295
- const { status, to } = match;
296
-
297
- let location = to;
298
- let search;
299
-
300
- if (to.startsWith("/")) {
301
- search = new URL(location, "http://fakehost").search;
302
- } else {
303
- search = new URL(location).search;
304
- }
305
-
306
- location = `${location}${search ? "" : url.search}`;
307
-
308
- if (status && [301, 302, 303, 307, 308].includes(status)) {
309
- deconstructedResponse.status = status;
310
- } else {
311
- deconstructedResponse.status = 302;
312
- }
313
-
314
- deconstructedResponse.headers.set("Location", location);
315
- return deconstructedResponse;
316
- }
317
-
318
- if (!request.method?.match(/^(get|head)$/i)) {
319
- deconstructedResponse.status = 405;
320
- return deconstructedResponse;
321
- }
322
-
323
- const notFound = () => {
324
- let cwd = assetName;
325
- while (cwd) {
326
- cwd = cwd.slice(0, cwd.lastIndexOf("/"));
327
-
328
- if ((asset = getAsset(`${cwd}/404.html`))) {
329
- deconstructedResponse.status = 404;
330
- deconstructedResponse.body = serveAsset(asset);
331
- deconstructedResponse.headers.set(
332
- "Content-Type",
333
- getType(asset) || "application/octet-stream"
334
- );
335
- return deconstructedResponse;
106
+ watch([headersFile, redirectsFile], { persistent: true }).on(
107
+ "change",
108
+ (path) => {
109
+ switch (path) {
110
+ case headersFile: {
111
+ log.log("_headers modified. Re-evaluating...");
112
+ const contents = readFileSync(headersFile).toString();
113
+ headers = parseHeaders(contents);
114
+ break;
115
+ }
116
+ case redirectsFile: {
117
+ log.log("_redirects modified. Re-evaluating...");
118
+ const contents = readFileSync(redirectsFile).toString();
119
+ redirects = parseRedirects(contents);
120
+ break;
336
121
  }
337
122
  }
338
123
 
339
- if ((asset = getAsset(`/index.html`))) {
340
- deconstructedResponse.body = serveAsset(asset);
341
- deconstructedResponse.headers.set(
342
- "Content-Type",
343
- getType(asset) || "application/octet-stream"
344
- );
345
- return deconstructedResponse;
346
- }
347
-
348
- deconstructedResponse.status = 404;
349
- return deconstructedResponse;
350
- };
351
-
352
- let asset;
353
-
354
- if (assetName.endsWith("/")) {
355
- if ((asset = getAsset(`${assetName}/index.html`))) {
356
- deconstructedResponse.body = serveAsset(asset);
357
- deconstructedResponse.headers.set(
358
- "Content-Type",
359
- getType(asset) || "application/octet-stream"
360
- );
361
- return deconstructedResponse;
362
- } else if ((asset = getAsset(`${assetName.replace(/\/$/, ".html")}`))) {
363
- deconstructedResponse.status = 301;
364
- deconstructedResponse.headers.set(
365
- "Location",
366
- `${assetName.slice(0, -1)}${url.search}`
367
- );
368
- return deconstructedResponse;
369
- }
124
+ metadata = createMetadataObject({
125
+ redirects,
126
+ headers,
127
+ logger: log.warn,
128
+ });
370
129
  }
130
+ );
371
131
 
372
- if (assetName.endsWith("/index")) {
373
- deconstructedResponse.status = 301;
374
- deconstructedResponse.headers.set(
375
- "Location",
376
- `${assetName.slice(0, -"index".length)}${url.search}`
377
- );
378
- return deconstructedResponse;
379
- }
132
+ const generateResponse = async (request: Request) => {
133
+ const assetKeyEntryMap = new Map<string, string>();
134
+
135
+ return await generateHandler<string>({
136
+ request,
137
+ metadata: metadata as Metadata,
138
+ xServerEnvHeader: "dev",
139
+ logError: console.error,
140
+ findAssetEntryForPath: async (path) => {
141
+ const filepath = join(directory, path);
142
+
143
+ if (
144
+ existsSync(filepath) &&
145
+ lstatSync(filepath).isFile() &&
146
+ !ignoredFiles.includes(filepath)
147
+ ) {
148
+ const hash = hashFile(filepath);
149
+ assetKeyEntryMap.set(hash, filepath);
150
+ return hash;
151
+ }
380
152
 
381
- if ((asset = getAsset(assetName))) {
382
- if (assetName.endsWith(".html")) {
383
- const extensionlessPath = assetName.slice(0, -".html".length);
384
- if (getAsset(extensionlessPath) || extensionlessPath === "/") {
385
- deconstructedResponse.body = serveAsset(asset);
386
- deconstructedResponse.headers.set(
387
- "Content-Type",
388
- getType(asset) || "application/octet-stream"
389
- );
390
- return deconstructedResponse;
153
+ return null;
154
+ },
155
+ getAssetKey: (assetEntry) => {
156
+ return assetEntry;
157
+ },
158
+ negotiateContent: (contentRequest) => {
159
+ let rawAcceptEncoding: string | undefined;
160
+ if (
161
+ contentRequest.cf &&
162
+ "clientAcceptEncoding" in contentRequest.cf &&
163
+ contentRequest.cf.clientAcceptEncoding
164
+ ) {
165
+ rawAcceptEncoding = contentRequest.cf.clientAcceptEncoding as string;
391
166
  } else {
392
- deconstructedResponse.status = 301;
393
- deconstructedResponse.headers.set(
394
- "Location",
395
- `${extensionlessPath}${url.search}`
396
- );
397
- return deconstructedResponse;
167
+ rawAcceptEncoding =
168
+ contentRequest.headers.get("Accept-Encoding") || undefined;
398
169
  }
399
- } else {
400
- deconstructedResponse.body = serveAsset(asset);
401
- deconstructedResponse.headers.set(
402
- "Content-Type",
403
- getType(asset) || "application/octet-stream"
404
- );
405
- return deconstructedResponse;
406
- }
407
- } else if (hasFileExtension(assetName)) {
408
- if ((asset = getAsset(assetName + ".html"))) {
409
- deconstructedResponse.body = serveAsset(asset);
410
- deconstructedResponse.headers.set(
411
- "Content-Type",
412
- getType(asset) || "application/octet-stream"
413
- );
414
- return deconstructedResponse;
415
- }
416
- notFound();
417
- return deconstructedResponse;
418
- }
419
170
 
420
- if ((asset = getAsset(`${assetName}.html`))) {
421
- deconstructedResponse.body = serveAsset(asset);
422
- deconstructedResponse.headers.set(
423
- "Content-Type",
424
- getType(asset) || "application/octet-stream"
425
- );
426
- return deconstructedResponse;
427
- }
171
+ const acceptEncoding = parseQualityWeightedList(rawAcceptEncoding);
428
172
 
429
- if ((asset = getAsset(`${assetName}/index.html`))) {
430
- deconstructedResponse.status = 301;
431
- deconstructedResponse.headers.set(
432
- "Location",
433
- `${assetName}/${url.search}`
434
- );
435
- return deconstructedResponse;
436
- } else {
437
- notFound();
438
- return deconstructedResponse;
439
- }
440
- };
441
-
442
- const attachHeaders = (
443
- request: MiniflareRequest,
444
- deconstructedResponse: {
445
- status: number;
446
- headers: MiniflareHeaders;
447
- body?: Buffer;
448
- }
449
- ) => {
450
- const headers = deconstructedResponse.headers;
451
- const newHeaders = new Headers({});
452
- const matches = headersMatcher(request) || [];
453
-
454
- matches.forEach((match) => {
455
- Object.entries(match).forEach(([name, value]) => {
456
- newHeaders.append(name, `${value}`);
457
- });
458
- });
173
+ if (
174
+ acceptEncoding["identity"] === 0 ||
175
+ (acceptEncoding["*"] === 0 &&
176
+ acceptEncoding["identity"] === undefined)
177
+ ) {
178
+ throw new Error("No acceptable encodings available");
179
+ }
459
180
 
460
- const combinedHeaders = {
461
- ...Object.fromEntries(headers.entries()),
462
- ...Object.fromEntries(newHeaders.entries()),
463
- };
181
+ return { encoding: null };
182
+ },
183
+ fetchAsset: async (assetKey) => {
184
+ const filepath = assetKeyEntryMap.get(assetKey);
185
+ if (!filepath) {
186
+ throw new Error(
187
+ "Could not fetch asset. Please file an issue on GitHub (https://github.com/cloudflare/wrangler2/issues/new/choose) with reproduction steps."
188
+ );
189
+ }
190
+ const body = readFileSync(filepath) as unknown as ReadableStream;
464
191
 
465
- deconstructedResponse.headers = new Headers({});
466
- Object.entries(combinedHeaders).forEach(([name, value]) => {
467
- if (value) deconstructedResponse.headers.set(name, value);
192
+ const contentType = getType(filepath) || "application/octet-stream";
193
+ return { body, contentType };
194
+ },
468
195
  });
469
196
  };
470
197
 
471
198
  return async (input: RequestInfo, init?: RequestInit) => {
472
- const request = new Request(input, init);
473
- const deconstructedResponse = generateResponse(request);
474
- attachHeaders(request, deconstructedResponse);
475
-
476
- const headers = new Headers();
477
-
478
- [...deconstructedResponse.headers.entries()].forEach(([name, value]) => {
479
- if (value) headers.set(name, value);
480
- });
481
-
482
- return new Response(deconstructedResponse.body, {
483
- headers,
484
- status: deconstructedResponse.status,
485
- });
199
+ const request = new MiniflareRequest(input, init);
200
+ return await generateResponse(request as unknown as Request);
486
201
  };
487
202
  }
488
203
 
489
- const invalidAssetsFetch: typeof miniflareFetch = () => {
204
+ const invalidAssetsFetch: typeof fetch = () => {
490
205
  throw new Error(
491
206
  "Trying to fetch assets directly when there is no `directory` option specified."
492
207
  );
@@ -129,7 +129,8 @@ async function main() {
129
129
  }
130
130
  mf = new Miniflare(config);
131
131
  // Start Miniflare development server
132
- await mf.startServer();
132
+ const mfServer = await mf.startServer();
133
+ const mfPort = (mfServer.address() as AddressInfo).port;
133
134
  await mf.startScheduler();
134
135
 
135
136
  const internalDurableObjectClassNames = Object.values(
@@ -195,6 +196,7 @@ async function main() {
195
196
  process.send &&
196
197
  process.send(
197
198
  JSON.stringify({
199
+ mfPort: mfPort,
198
200
  ready: true,
199
201
  durableObjectsPort: durableObjectsMfPort,
200
202
  })
package/src/pages/dev.tsx CHANGED
@@ -451,9 +451,13 @@ async function spawnProxyProcess({
451
451
  command: (string | number)[];
452
452
  }): Promise<undefined | number> {
453
453
  if (command.length === 0) {
454
+ if (port !== undefined) {
455
+ return port;
456
+ }
457
+
454
458
  CLEANUP();
455
459
  throw new FatalError(
456
- "Must specify a directory of static assets to serve or a command to run.",
460
+ "Must specify a directory of static assets to serve or a command to run or a proxy port.",
457
461
  1
458
462
  );
459
463
  }
@@ -0,0 +1,13 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { extname } from "node:path";
3
+ import { hash as blake3hash } from "blake3-wasm";
4
+
5
+ export const hashFile = (filepath: string) => {
6
+ const contents = readFileSync(filepath);
7
+ const base64Contents = contents.toString("base64");
8
+ const extension = extname(filepath).substring(1);
9
+
10
+ return blake3hash(base64Contents + extension)
11
+ .toString("hex")
12
+ .slice(0, 32);
13
+ };