wrangler 2.1.8 → 2.1.9

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wrangler",
3
- "version": "2.1.8",
3
+ "version": "2.1.9",
4
4
  "description": "Command-line interface for all things Cloudflare Workers",
5
5
  "keywords": [
6
6
  "wrangler",
@@ -112,7 +112,7 @@
112
112
  "@databases/sql": "^3.2.0",
113
113
  "@iarna/toml": "^3.0.0",
114
114
  "@microsoft/api-extractor": "^7.28.3",
115
- "@miniflare/tre": "3.0.0-next.1",
115
+ "@miniflare/tre": "3.0.0-next.2",
116
116
  "@types/better-sqlite3": "^7.6.0",
117
117
  "@types/busboy": "^1.5.0",
118
118
  "@types/command-exists": "^1.2.0",
@@ -139,7 +139,6 @@
139
139
  "dotenv": "^16.0.0",
140
140
  "execa": "^6.1.0",
141
141
  "express": "^4.18.1",
142
- "faye-websocket": "^0.11.4",
143
142
  "finalhandler": "^1.2.0",
144
143
  "find-up": "^6.3.0",
145
144
  "get-port": "^6.1.2",
@@ -1022,6 +1022,7 @@ describe("wrangler dev", () => {
1022
1022
  --jsx-fragment The function that is called for each JSX fragment [string]
1023
1023
  --tsconfig Path to a custom tsconfig.json file [string]
1024
1024
  -l, --local Run on my machine [boolean] [default: false]
1025
+ --experimental-local Run on my machine using the Cloudflare Workers runtime [boolean] [default: false]
1025
1026
  --minify Minify the script [boolean]
1026
1027
  --node-compat Enable node.js compatibility [boolean]
1027
1028
  --persist Enable persistence for local mode, using default path: .wrangler/state [boolean]
@@ -1325,18 +1326,18 @@ describe("wrangler dev", () => {
1325
1326
  fs.writeFileSync("index.js", `export default {};`);
1326
1327
  await runWrangler("dev index.js");
1327
1328
  expect(std).toMatchInlineSnapshot(`
1328
- Object {
1329
- "debug": "",
1330
- "err": "",
1331
- "out": "Using vars defined in .dev.vars
1332
- Your worker has access to the following bindings:
1333
- - Vars:
1334
- - variable: \\"123\\"
1335
- - overriden: \\"(hidden)\\"
1336
- - SECRET: \\"(hidden)\\"",
1337
- "warn": "",
1338
- }
1339
- `);
1329
+ Object {
1330
+ "debug": "",
1331
+ "err": "",
1332
+ "out": "Using vars defined in .dev.vars
1333
+ Your worker has access to the following bindings:
1334
+ - Vars:
1335
+ - variable: 123
1336
+ - overriden: \\"(hidden)\\"
1337
+ - SECRET: \\"(hidden)\\"",
1338
+ "warn": "",
1339
+ }
1340
+ `);
1340
1341
  });
1341
1342
  });
1342
1343
  });
@@ -2608,6 +2608,105 @@ describe("init", () => {
2608
2608
  ).rejects.toThrowError();
2609
2609
  });
2610
2610
 
2611
+ it("should not inlcude migrations in config file when none are necessary", async () => {
2612
+ const mockDate = "1988-08-07";
2613
+ jest
2614
+ .spyOn(Date.prototype, "toISOString")
2615
+ .mockImplementation(() => `${mockDate}T00:00:00.000Z`);
2616
+ const mockData = {
2617
+ id: "memory-crystal",
2618
+ default_environment: {
2619
+ environment: "test",
2620
+ created_on: "1988-08-07",
2621
+ modified_on: "1988-08-07",
2622
+ script: {
2623
+ id: "memory-crystal",
2624
+ tag: "test-tag",
2625
+ etag: "some-etag",
2626
+ handlers: [],
2627
+ modified_on: "1988-08-07",
2628
+ created_on: "1988-08-07",
2629
+ usage_model: "bundled",
2630
+ compatibility_date: "1988-08-07",
2631
+ },
2632
+ },
2633
+ environments: [],
2634
+ };
2635
+
2636
+ setMockResponse(
2637
+ `/accounts/:accountId/workers/services/:scriptName`,
2638
+ "GET",
2639
+ () => mockData
2640
+ );
2641
+ setMockResponse(
2642
+ `/accounts/:accountId/workers/services/:scriptName/environments/:environment/bindings`,
2643
+ "GET",
2644
+ () => []
2645
+ );
2646
+ setMockResponse(
2647
+ `/accounts/:accountId/workers/services/:scriptName/environments/:environment/routes`,
2648
+ "GET",
2649
+ () => []
2650
+ );
2651
+ setMockResponse(
2652
+ `/accounts/:accountId/workers/services/:scriptName/environments/:environment`,
2653
+ "GET",
2654
+ () => mockServiceMetadata.default_environment
2655
+ );
2656
+ setMockResponse(
2657
+ `/accounts/:accountId/workers/scripts/:scriptName/schedules`,
2658
+ "GET",
2659
+ () => {
2660
+ return {
2661
+ schedules: [],
2662
+ };
2663
+ }
2664
+ );
2665
+
2666
+ setMockFetchDashScript({
2667
+ accountId: "LCARS",
2668
+ fromDashScriptName: "isolinear-optical-chip",
2669
+ environment: mockServiceMetadata.default_environment.environment,
2670
+ mockResponse: mockDashboardScript,
2671
+ });
2672
+
2673
+ mockConfirm(
2674
+ {
2675
+ text: "Would you like to use git to manage this Worker?",
2676
+ result: false,
2677
+ },
2678
+ {
2679
+ text: "Would you like to use TypeScript?",
2680
+ result: true,
2681
+ },
2682
+ {
2683
+ text: "No package.json found. Would you like to create one?",
2684
+ result: true,
2685
+ },
2686
+ {
2687
+ text: "Would you like to install the type definitions for Workers into your package.json?",
2688
+ result: true,
2689
+ }
2690
+ );
2691
+
2692
+ await runWrangler("init --from-dash isolinear-optical-chip");
2693
+
2694
+ checkFiles({
2695
+ items: {
2696
+ "isolinear-optical-chip/wrangler.toml": wranglerToml({
2697
+ compatibility_date: "1988-08-07",
2698
+ env: {},
2699
+ main: "src/index.ts",
2700
+ triggers: {
2701
+ crons: [],
2702
+ },
2703
+ usage_model: "bundled",
2704
+ name: "isolinear-optical-chip",
2705
+ }),
2706
+ },
2707
+ });
2708
+ });
2709
+
2611
2710
  it("should not continue if no worker name is provided", async () => {
2612
2711
  await expect(
2613
2712
  runWrangler("init --from-dash")
@@ -512,7 +512,7 @@ describe("publish", () => {
512
512
  "Total Upload: xx KiB / gzip: xx KiB
513
513
  Your worker has access to the following bindings:
514
514
  - Vars:
515
- - xyz: \\"123\\"
515
+ - xyz: 123
516
516
  Uploaded test-name (TIMINGS)
517
517
  Published test-name (TIMINGS)
518
518
  https://test-name.test-sub-domain.workers.dev"
@@ -4527,7 +4527,7 @@ addEventListener('fetch', event => {});`
4527
4527
  - some unsafe thing: UNSAFE_BINDING_ONE
4528
4528
  - another unsafe thing: UNSAFE_BINDING_TWO
4529
4529
  - Vars:
4530
- - ENV_VAR_ONE: \\"123\\"
4530
+ - ENV_VAR_ONE: 123
4531
4531
  - ENV_VAR_TWO: \\"Hello, I'm an environment variable\\"
4532
4532
  - Wasm Modules:
4533
4533
  - WASM_MODULE_ONE: some_wasm.wasm
@@ -5275,8 +5275,11 @@ addEventListener('fetch', event => {});`
5275
5275
  Your worker has access to the following bindings:
5276
5276
  - Vars:
5277
5277
  - text: \\"plain ol' string\\"
5278
- - count: \\"1\\"
5279
- - complex: \\"[object Object]\\"
5278
+ - count: 1
5279
+ - complex: {
5280
+ \\"enabled\\": true,
5281
+ \\"id\\": 123
5282
+ }
5280
5283
  Uploaded test-name (TIMINGS)
5281
5284
  Published test-name (TIMINGS)
5282
5285
  https://test-name.test-sub-domain.workers.dev"
package/src/api/dev.ts CHANGED
@@ -54,6 +54,7 @@ interface DevOptions {
54
54
  _?: (string | number)[]; //yargs wants this
55
55
  $0?: string; //yargs wants this
56
56
  testScheduled?: boolean;
57
+ experimentalLocal?: boolean;
57
58
  }
58
59
 
59
60
  interface DevApiOptions {
@@ -219,10 +219,20 @@ export function printBindings(bindings: CfWorkerInit["bindings"]) {
219
219
  if (vars !== undefined && Object.keys(vars).length > 0) {
220
220
  output.push({
221
221
  type: "Vars",
222
- entries: Object.entries(vars).map(([key, value]) => ({
223
- key,
224
- value: `"${truncate(`${value}`)}"`,
225
- })),
222
+ entries: Object.entries(vars).map(([key, value]) => {
223
+ let parsedValue;
224
+ if (typeof value === "string") {
225
+ parsedValue = `"${truncate(value)}"`;
226
+ } else if (typeof value === "object") {
227
+ parsedValue = JSON.stringify(value, null, 1);
228
+ } else {
229
+ parsedValue = `${truncate(`${value}`)}`;
230
+ }
231
+ return {
232
+ key,
233
+ value: parsedValue,
234
+ };
235
+ }),
226
236
  });
227
237
  }
228
238
 
package/src/dev/local.tsx CHANGED
@@ -1,6 +1,7 @@
1
+ import assert from "node:assert";
1
2
  import { fork } from "node:child_process";
2
3
  import { realpathSync } from "node:fs";
3
- import { writeFile } from "node:fs/promises";
4
+ import { readFile, writeFile } from "node:fs/promises";
4
5
  import path from "node:path";
5
6
  import { npxImport } from "npx-import";
6
7
  import { useState, useEffect, useRef } from "react";
@@ -8,7 +9,10 @@ import onExit from "signal-exit";
8
9
  import { registerWorker } from "../dev-registry";
9
10
  import useInspector from "../inspect";
10
11
  import { logger } from "../logger";
11
- import { DEFAULT_MODULE_RULES } from "../module-collection";
12
+ import {
13
+ DEFAULT_MODULE_RULES,
14
+ ModuleTypeToRuleType,
15
+ } from "../module-collection";
12
16
  import { getBasePath } from "../paths";
13
17
  import { waitForPortToBeAvailable } from "../proxy";
14
18
  import type { Config } from "../config";
@@ -35,10 +39,6 @@ import type {
35
39
  import type { MiniflareOptions } from "miniflare";
36
40
  import type { ChildProcess } from "node:child_process";
37
41
 
38
- // caching of the miniflare package
39
- // eslint-disable-next-line @typescript-eslint/consistent-type-imports
40
- let Miniflare: typeof import("@miniflare/tre")["Miniflare"];
41
-
42
42
  export interface LocalProps {
43
43
  name: string | undefined;
44
44
  bundle: EsbuildBundle | undefined;
@@ -76,10 +76,6 @@ export function Local(props: LocalProps) {
76
76
  return null;
77
77
  }
78
78
 
79
- function arrayToObject(values: string[] = []): Record<string, string> {
80
- return Object.fromEntries(values.map((value) => [value, value]));
81
- }
82
-
83
79
  function useLocalWorker({
84
80
  name: workerName,
85
81
  bundle,
@@ -206,35 +202,15 @@ function useLocalWorker({
206
202
 
207
203
  if (experimentalLocal) {
208
204
  // TODO: refactor setupMiniflareOptions so we don't need to parse here
209
- const miniflare2Options: MiniflareOptions = JSON.parse(forkOptions[0]);
210
- const options: Miniflare3Options = {
211
- ...miniflare2Options,
212
- // Miniflare 3 distinguishes between binding name and namespace/bucket
213
- // IDs. For now, just use the same value as we did in Miniflare 2.
214
- // TODO: use defined KV preview ID if any
215
- kvNamespaces: arrayToObject(miniflare2Options.kvNamespaces),
216
- r2Buckets: arrayToObject(miniflare2Options.r2Buckets),
217
- // TODO: pass-through collected modules instead of getting Miniflare
218
- // to collect them again
219
- };
220
-
221
- logger.log("⎔ Starting an experimental local server...");
222
-
223
- if (Miniflare === undefined) {
224
- ({ Miniflare } = await npxImport<
225
- // eslint-disable-next-line @typescript-eslint/consistent-type-imports
226
- typeof import("@miniflare/tre")
227
- >("@miniflare/tre"));
228
- }
229
-
230
- const mf = new Miniflare(options);
205
+ const mf2Options: MiniflareOptions = JSON.parse(forkOptions[0]);
206
+ const mf = await setupExperimentalLocal(mf2Options, format, bundle);
207
+ await mf.ready;
231
208
  experimentalLocalRef.current = mf;
232
- removeSignalExitListener.current = onExit((_code, _signal) => {
209
+ removeSignalExitListener.current = onExit(() => {
233
210
  logger.log("⎔ Shutting down experimental local server.");
234
211
  mf.dispose();
235
212
  experimentalLocalRef.current = undefined;
236
213
  });
237
- await mf.ready;
238
214
  return;
239
215
  }
240
216
 
@@ -648,3 +624,63 @@ export function setupNodeOptions({
648
624
  }
649
625
  return nodeOptions;
650
626
  }
627
+
628
+ // Caching of the `npx-import`ed `@miniflare/tre` package
629
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
630
+ let Miniflare: typeof import("@miniflare/tre").Miniflare;
631
+
632
+ function arrayToObject(values: string[] = []): Record<string, string> {
633
+ return Object.fromEntries(values.map((value) => [value, value]));
634
+ }
635
+
636
+ export async function setupExperimentalLocal(
637
+ mf2Options: MiniflareOptions,
638
+ format: CfScriptFormat,
639
+ bundle: EsbuildBundle
640
+ ): Promise<Miniflare3Type> {
641
+ const options: Miniflare3Options = {
642
+ ...mf2Options,
643
+ // Miniflare 3 distinguishes between binding name and namespace/bucket IDs.
644
+ // For now, just use the same value as we did in Miniflare 2.
645
+ // TODO: use defined KV preview ID if any
646
+ kvNamespaces: arrayToObject(mf2Options.kvNamespaces),
647
+ r2Buckets: arrayToObject(mf2Options.r2Buckets),
648
+ };
649
+
650
+ if (format === "modules") {
651
+ // Manually specify all modules from the bundle. If we didn't do this,
652
+ // Miniflare 3 would try collect them automatically again itself.
653
+
654
+ // Resolve entrypoint relative to the temporary directory, ensuring
655
+ // path doesn't start with `..`, which causes issues in `workerd`.
656
+ // Also ensures other modules with relative names can be resolved.
657
+ const root = path.dirname(bundle.path);
658
+
659
+ assert.strictEqual(bundle.type, "esm");
660
+ options.modules = [
661
+ // Entrypoint
662
+ {
663
+ type: "ESModule",
664
+ path: path.relative(root, bundle.path),
665
+ contents: await readFile(bundle.path, "utf-8"),
666
+ },
667
+ // Misc (WebAssembly, etc, ...)
668
+ ...bundle.modules.map((module) => ({
669
+ type: ModuleTypeToRuleType[module.type ?? "esm"],
670
+ path: module.name,
671
+ contents: module.content,
672
+ })),
673
+ ];
674
+ }
675
+
676
+ logger.log("⎔ Starting an experimental local server...");
677
+
678
+ if (Miniflare === undefined) {
679
+ ({ Miniflare } = await npxImport<
680
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
681
+ typeof import("@miniflare/tre")
682
+ >("@miniflare/tre@next"));
683
+ }
684
+
685
+ return new Miniflare(options);
686
+ }
@@ -17,6 +17,7 @@ import { logger } from "../logger";
17
17
  import { waitForPortToBeAvailable } from "../proxy";
18
18
  import {
19
19
  setupBindings,
20
+ setupExperimentalLocal,
20
21
  setupMiniflareOptions,
21
22
  setupNodeOptions,
22
23
  } from "./local";
@@ -28,6 +29,8 @@ import type { Entry } from "../entry";
28
29
  import type { DevProps, DirectorySyncResult } from "./dev";
29
30
  import type { LocalProps } from "./local";
30
31
  import type { EsbuildBundle } from "./use-esbuild";
32
+ import type { Miniflare as Miniflare3Type } from "@miniflare/tre";
33
+ import type { MiniflareOptions } from "miniflare";
31
34
 
32
35
  import type { ChildProcess } from "node:child_process";
33
36
 
@@ -118,6 +121,7 @@ export async function startDevServer(
118
121
  enablePagesAssetsServiceBinding: props.enablePagesAssetsServiceBinding,
119
122
  usageModel: props.usageModel,
120
123
  workerDefinitions,
124
+ experimentalLocal: props.experimentalLocal,
121
125
  });
122
126
 
123
127
  return {
@@ -252,8 +256,10 @@ export async function startLocalServer({
252
256
  onReady,
253
257
  logPrefix,
254
258
  enablePagesAssetsServiceBinding,
259
+ experimentalLocal,
255
260
  }: LocalProps) {
256
261
  let local: ChildProcess | undefined;
262
+ let experimentalLocalRef: Miniflare3Type | undefined;
257
263
  let removeSignalExitListener: (() => void) | undefined;
258
264
  let inspectorUrl: string | undefined;
259
265
  const setInspectorUrl = (url: string) => {
@@ -341,6 +347,21 @@ export async function startLocalServer({
341
347
  enablePagesAssetsServiceBinding,
342
348
  });
343
349
 
350
+ if (experimentalLocal) {
351
+ // TODO: refactor setupMiniflareOptions so we don't need to parse here
352
+ const mf2Options: MiniflareOptions = JSON.parse(forkOptions[0]);
353
+ const mf = await setupExperimentalLocal(mf2Options, format, bundle);
354
+ const runtimeURL = await mf.ready;
355
+ experimentalLocalRef = mf;
356
+ removeSignalExitListener = onExit((_code, _signal) => {
357
+ logger.log("⎔ Shutting down experimental local server.");
358
+ mf.dispose();
359
+ experimentalLocalRef = undefined;
360
+ });
361
+ onReady?.(runtimeURL.hostname, parseInt(runtimeURL.port ?? 8787));
362
+ return;
363
+ }
364
+
344
365
  const nodeOptions = setupNodeOptions({ inspect, ip, inspectorPort });
345
366
  logger.log("⎔ Starting a local server...");
346
367
 
@@ -435,9 +456,16 @@ export async function startLocalServer({
435
456
  logger.log("⎔ Shutting down local server.");
436
457
  local.kill();
437
458
  local = undefined;
438
- removeSignalExitListener && removeSignalExitListener();
439
- removeSignalExitListener = undefined;
440
459
  }
460
+ if (experimentalLocalRef) {
461
+ logger.log("⎔ Shutting down experimental local server.");
462
+ // Initialisation errors are also thrown asynchronously by dispose().
463
+ // The catch() above should've caught them though.
464
+ experimentalLocalRef?.dispose().catch(() => {});
465
+ experimentalLocalRef = undefined;
466
+ }
467
+ removeSignalExitListener?.();
468
+ removeSignalExitListener = undefined;
441
469
  },
442
470
  };
443
471
  }
package/src/dev.tsx CHANGED
@@ -561,10 +561,13 @@ export async function startApiDev(args: StartDevOptions) {
561
561
  isWorkersSite: Boolean(args.site || configParam.site),
562
562
  compatibilityDate: getDevCompatibilityDate(
563
563
  config,
564
- args["compatibility-date"]
564
+ // Only `compatibilityDate` will be set when using `unstable_dev`
565
+ args["compatibility-date"] ?? args.compatibilityDate
565
566
  ),
566
567
  compatibilityFlags:
567
- args["compatibility-flags"] || configParam.compatibility_flags,
568
+ args["compatibility-flags"] ??
569
+ args.compatibilityFlags ??
570
+ configParam.compatibility_flags,
568
571
  usageModel: configParam.usage_model,
569
572
  bindings: bindings,
570
573
  crons: configParam.triggers.crons,
@@ -578,7 +581,7 @@ export async function startApiDev(args: StartDevOptions) {
578
581
  firstPartyWorker: configParam.first_party_worker,
579
582
  sendMetrics: configParam.send_metrics,
580
583
  testScheduled: args.testScheduled,
581
- experimentalLocal: undefined,
584
+ experimentalLocal: args.experimentalLocal,
582
585
  });
583
586
  }
584
587
 
package/src/init.ts CHANGED
@@ -946,12 +946,16 @@ async function getWorkerConfig(
946
946
  new Date().toISOString().substring(0, 10),
947
947
  ...routeOrRoutesToConfig,
948
948
  usage_model: serviceEnvMetadata.script.usage_model,
949
- migrations: [
950
- {
951
- tag: serviceEnvMetadata.script.migration_tag,
952
- new_classes: durableObjectClassNames,
953
- },
954
- ],
949
+ ...(durableObjectClassNames.length
950
+ ? {
951
+ migrations: [
952
+ {
953
+ tag: serviceEnvMetadata.script.migration_tag,
954
+ new_classes: durableObjectClassNames,
955
+ },
956
+ ],
957
+ }
958
+ : {}),
955
959
  triggers: {
956
960
  crons: cronTriggers.schedules.map((scheduled) => scheduled.cron),
957
961
  },
@@ -7,6 +7,13 @@ import type { Config, ConfigModuleRuleType } from "./config";
7
7
  import type { CfModule, CfModuleType, CfScriptFormat } from "./worker";
8
8
  import type esbuild from "esbuild";
9
9
 
10
+ function flipObject<
11
+ K extends string | number | symbol,
12
+ V extends string | number | symbol
13
+ >(obj: Record<K, V>): Record<V, K> {
14
+ return Object.fromEntries(Object.entries(obj).map(([k, v]) => [v, k]));
15
+ }
16
+
10
17
  const RuleTypeToModuleType: Record<ConfigModuleRuleType, CfModuleType> = {
11
18
  ESModule: "esm",
12
19
  CommonJS: "commonjs",
@@ -15,6 +22,8 @@ const RuleTypeToModuleType: Record<ConfigModuleRuleType, CfModuleType> = {
15
22
  Text: "text",
16
23
  };
17
24
 
25
+ export const ModuleTypeToRuleType = flipObject(RuleTypeToModuleType);
26
+
18
27
  // This is a combination of an esbuild plugin and a mutable array
19
28
  // that we use to collect module references from source code.
20
29
  // There will be modules that _shouldn't_ be inlined directly into
package/src/proxy.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { createServer as createHttpServer } from "node:http";
2
2
  import { connect } from "node:http2";
3
3
  import { createServer as createHttpsServer } from "node:https";
4
+ import https from "node:https";
4
5
  import { networkInterfaces } from "node:os";
5
- import WebSocket from "faye-websocket";
6
6
  import { createHttpTerminator } from "http-terminator";
7
7
  import { useEffect, useRef, useState } from "react";
8
8
  import serveStatic from "serve-static";
@@ -19,12 +19,7 @@ import type {
19
19
  } from "node:http";
20
20
  import type { ClientHttp2Session, ServerHttp2Stream } from "node:http2";
21
21
  import type { Server as HttpsServer } from "node:https";
22
- import type ws from "ws";
23
-
24
- interface IWebsocket extends ws {
25
- // Pipe implements .on("message", ...)
26
- pipe<T>(fn: T): IWebsocket;
27
- }
22
+ import type { Duplex, Writable } from "node:stream";
28
23
 
29
24
  /**
30
25
  * `usePreviewServer` is a React hook that creates a local development
@@ -70,6 +65,26 @@ function rewriteRemoteHostToLocalHostInHeaders(
70
65
  }
71
66
  }
72
67
 
68
+ function writeHead(
69
+ socket: Writable,
70
+ res: Pick<
71
+ IncomingMessage,
72
+ "httpVersion" | "statusCode" | "statusMessage" | "headers"
73
+ >
74
+ ) {
75
+ socket.write(
76
+ `HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}\r\n`
77
+ );
78
+ for (const [key, values] of Object.entries(res.headers)) {
79
+ if (Array.isArray(values)) {
80
+ for (const value of values) socket.write(`${key}: ${value}\r\n`);
81
+ } else {
82
+ socket.write(`${key}: ${values}\r\n`);
83
+ }
84
+ }
85
+ socket.write("\r\n");
86
+ }
87
+
73
88
  type PreviewProxy = {
74
89
  server: HttpServer | HttpsServer;
75
90
  terminator: HttpTerminator;
@@ -273,27 +288,47 @@ export function usePreviewServer({
273
288
 
274
289
  /** HTTP/1 -> WebSocket (over HTTP/1) */
275
290
  const handleUpgrade = (
276
- message: IncomingMessage,
277
- socket: WebSocket,
278
- body: Buffer
291
+ originalMessage: IncomingMessage,
292
+ originalSocket: Duplex,
293
+ originalHead: Buffer
279
294
  ) => {
280
- const { headers, url } = message;
295
+ const { headers, method, url } = originalMessage;
281
296
  addCfPreviewTokenHeader(headers, previewToken.value);
282
297
  headers["host"] = previewToken.host;
283
- const localWebsocket = new WebSocket(message, socket, body) as IWebsocket;
284
- // TODO(soon): Custom WebSocket protocol is not working?
285
- const remoteWebsocketClient = new WebSocket.Client(
286
- `wss://${previewToken.host}${url}`,
287
- [],
288
- { headers }
289
- ) as IWebsocket;
290
- localWebsocket.pipe(remoteWebsocketClient).pipe(localWebsocket);
291
- // We close down websockets whenever we refresh the token.
292
- cleanupListeners.push(() => {
293
- localWebsocket.close();
294
- remoteWebsocketClient.close();
295
- });
298
+
299
+ if (originalHead?.byteLength) originalSocket.unshift(originalHead);
300
+
301
+ const runtimeRequest = https.request(
302
+ {
303
+ hostname: previewToken.host,
304
+ path: url,
305
+ method,
306
+ headers,
307
+ },
308
+ (runtimeResponse) => {
309
+ if (!(runtimeResponse as { upgrade?: boolean }).upgrade) {
310
+ writeHead(originalSocket, runtimeResponse);
311
+ runtimeResponse.pipe(originalSocket);
312
+ }
313
+ }
314
+ );
315
+
316
+ runtimeRequest.on(
317
+ "upgrade",
318
+ (runtimeResponse, runtimeSocket, runtimeHead) => {
319
+ if (runtimeHead?.byteLength) runtimeSocket.unshift(runtimeHead);
320
+ writeHead(originalSocket, {
321
+ httpVersion: "1.1",
322
+ statusCode: 101,
323
+ statusMessage: "Switching Protocols",
324
+ headers: runtimeResponse.headers,
325
+ });
326
+ runtimeSocket.pipe(originalSocket).pipe(runtimeSocket);
327
+ }
328
+ );
329
+ originalMessage.pipe(runtimeRequest);
296
330
  };
331
+
297
332
  proxy.server.on("upgrade", handleUpgrade);
298
333
  cleanupListeners.push(() => proxy.server.off("upgrade", handleUpgrade));
299
334
 
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  export default {
12
- async fetch(request) {
12
+ async fetch(request, env, ctx) {
13
13
  return new Response("Hello World!");
14
14
  },
15
15
  };
@@ -127,6 +127,7 @@ declare interface DevOptions {
127
127
  _?: (string | number)[];
128
128
  $0?: string;
129
129
  testScheduled?: boolean;
130
+ experimentalLocal?: boolean;
130
131
  }
131
132
 
132
133
  declare interface EnablePagesAssetsServiceBindingOptions {