wrangler 0.0.0-e1c615f → 0.0.0-e5d3690
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 +9 -5
- package/pages/functions/buildWorker.ts +5 -0
- package/pages/functions/filepath-routing.ts +15 -8
- package/pages/functions/routes.ts +1 -1
- package/pages/functions/template-worker.ts +18 -19
- package/src/__tests__/dev.test.tsx +5 -4
- package/src/__tests__/index.test.ts +2 -0
- package/src/__tests__/kv.test.ts +173 -72
- package/src/__tests__/mock-cfetch.ts +8 -6
- package/src/__tests__/publish.test.ts +672 -0
- package/src/__tests__/whoami.test.tsx +130 -0
- package/src/api/form_data.ts +1 -1
- package/src/api/worker.ts +6 -6
- package/src/cfetch/index.ts +10 -2
- package/src/cfetch/internal.ts +10 -8
- package/src/config.ts +633 -118
- package/src/dev.tsx +119 -214
- package/src/index.tsx +268 -222
- package/src/inspect.ts +543 -0
- package/src/kv.tsx +18 -9
- package/src/pages.tsx +117 -45
- package/src/proxy.ts +275 -76
- package/src/publish.ts +78 -46
- package/src/sites.tsx +167 -38
- package/src/user.tsx +56 -59
- package/src/whoami.tsx +69 -0
- package/wrangler-dist/cli.js +31405 -30705
- package/wrangler-dist/cli.js.map +3 -3
- package/src/api/inspect.ts +0 -430
package/src/pages.tsx
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/* eslint-disable no-shadow */
|
|
2
2
|
|
|
3
|
-
import assert from "assert";
|
|
4
3
|
import type { BuilderCallback } from "yargs";
|
|
5
4
|
import { join } from "path";
|
|
6
5
|
import { tmpdir } from "os";
|
|
7
|
-
import { existsSync, lstatSync, readFileSync } from "fs";
|
|
6
|
+
import { existsSync, lstatSync, readFileSync, writeFileSync } from "fs";
|
|
8
7
|
import { execSync, spawn } from "child_process";
|
|
9
8
|
import { URL } from "url";
|
|
10
9
|
import { getType } from "mime";
|
|
@@ -22,7 +21,7 @@ import { generateConfigFromFileTree } from "../pages/functions/filepath-routing"
|
|
|
22
21
|
import type { Headers, Request, fetch } from "@miniflare/core";
|
|
23
22
|
import type { MiniflareOptions } from "miniflare";
|
|
24
23
|
|
|
25
|
-
const EXIT_CALLBACKS = [];
|
|
24
|
+
const EXIT_CALLBACKS: (() => void)[] = [];
|
|
26
25
|
const EXIT = (message?: string, code?: number) => {
|
|
27
26
|
if (message) console.log(message);
|
|
28
27
|
if (code) process.exitCode = code;
|
|
@@ -122,7 +121,9 @@ async function spawnProxyProcess({
|
|
|
122
121
|
},
|
|
123
122
|
}
|
|
124
123
|
);
|
|
125
|
-
EXIT_CALLBACKS.push(() =>
|
|
124
|
+
EXIT_CALLBACKS.push(() => {
|
|
125
|
+
proxy.kill();
|
|
126
|
+
});
|
|
126
127
|
|
|
127
128
|
proxy.stdout.on("data", (data) => {
|
|
128
129
|
console.log(`[proxy]: ${data}`);
|
|
@@ -642,16 +643,20 @@ const RUNNING_BUILDERS: BuildResult[] = [];
|
|
|
642
643
|
|
|
643
644
|
async function buildFunctions({
|
|
644
645
|
scriptPath,
|
|
646
|
+
outputConfigPath,
|
|
645
647
|
functionsDirectory,
|
|
646
648
|
minify = false,
|
|
647
649
|
sourcemap = false,
|
|
650
|
+
fallbackService = "ASSETS",
|
|
648
651
|
watch = false,
|
|
649
652
|
onEnd,
|
|
650
653
|
}: {
|
|
651
654
|
scriptPath: string;
|
|
655
|
+
outputConfigPath?: string;
|
|
652
656
|
functionsDirectory: string;
|
|
653
657
|
minify?: boolean;
|
|
654
658
|
sourcemap?: boolean;
|
|
659
|
+
fallbackService?: string;
|
|
655
660
|
watch?: boolean;
|
|
656
661
|
onEnd?: () => void;
|
|
657
662
|
}) {
|
|
@@ -666,6 +671,13 @@ async function buildFunctions({
|
|
|
666
671
|
baseURL: "/",
|
|
667
672
|
});
|
|
668
673
|
|
|
674
|
+
if (outputConfigPath) {
|
|
675
|
+
writeFileSync(
|
|
676
|
+
outputConfigPath,
|
|
677
|
+
JSON.stringify({ ...config, baseURL: "/" }, null, 2)
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
669
681
|
await writeRoutesModule({
|
|
670
682
|
config,
|
|
671
683
|
srcDir: functionsDirectory,
|
|
@@ -678,6 +690,7 @@ async function buildFunctions({
|
|
|
678
690
|
outfile: scriptPath,
|
|
679
691
|
minify,
|
|
680
692
|
sourcemap,
|
|
693
|
+
fallbackService,
|
|
681
694
|
watch,
|
|
682
695
|
onEnd,
|
|
683
696
|
})
|
|
@@ -738,6 +751,11 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
738
751
|
description: "Durable Object to bind (NAME=CLASS)",
|
|
739
752
|
alias: "o",
|
|
740
753
|
},
|
|
754
|
+
"live-reload": {
|
|
755
|
+
type: "boolean",
|
|
756
|
+
default: false,
|
|
757
|
+
description: "Auto reload HTML pages when change is detected",
|
|
758
|
+
},
|
|
741
759
|
// TODO: Miniflare user options
|
|
742
760
|
});
|
|
743
761
|
},
|
|
@@ -750,6 +768,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
750
768
|
binding: bindings = [],
|
|
751
769
|
kv: kvs = [],
|
|
752
770
|
do: durableObjects = [],
|
|
771
|
+
"live-reload": liveReload,
|
|
753
772
|
"--": remaining = [],
|
|
754
773
|
}) => {
|
|
755
774
|
if (!local) {
|
|
@@ -774,7 +793,10 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
774
793
|
|
|
775
794
|
let miniflareArgs: MiniflareOptions = {};
|
|
776
795
|
|
|
777
|
-
let
|
|
796
|
+
let scriptReadyResolve;
|
|
797
|
+
const scriptReadyPromise = new Promise(
|
|
798
|
+
(resolve) => (scriptReadyResolve = resolve)
|
|
799
|
+
);
|
|
778
800
|
|
|
779
801
|
if (usingFunctions) {
|
|
780
802
|
const scriptPath = join(tmpdir(), "./functionsWorker.js");
|
|
@@ -786,22 +808,19 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
786
808
|
functionsDirectory,
|
|
787
809
|
sourcemap: true,
|
|
788
810
|
watch: true,
|
|
789
|
-
onEnd: () =>
|
|
790
|
-
scriptReady = true;
|
|
791
|
-
},
|
|
811
|
+
onEnd: () => scriptReadyResolve(),
|
|
792
812
|
});
|
|
793
813
|
|
|
794
814
|
watch([functionsDirectory], {
|
|
795
815
|
persistent: true,
|
|
816
|
+
ignoreInitial: true,
|
|
796
817
|
}).on("all", async () => {
|
|
797
818
|
await buildFunctions({
|
|
798
819
|
scriptPath,
|
|
799
820
|
functionsDirectory,
|
|
800
821
|
sourcemap: true,
|
|
801
822
|
watch: true,
|
|
802
|
-
onEnd: () =>
|
|
803
|
-
scriptReady = true;
|
|
804
|
-
},
|
|
823
|
+
onEnd: () => scriptReadyResolve(),
|
|
805
824
|
});
|
|
806
825
|
});
|
|
807
826
|
|
|
@@ -838,11 +857,19 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
838
857
|
const { Miniflare, Log, LogLevel } = await import("miniflare");
|
|
839
858
|
const { Response, fetch } = await import("@miniflare/core");
|
|
840
859
|
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
860
|
+
// Wait for esbuild to finish building before starting Miniflare.
|
|
861
|
+
// This must be before the call to `new Miniflare`, as that will
|
|
862
|
+
// asynchronously start loading the script. `await startServer()`
|
|
863
|
+
// internally just waits for that promise to resolve.
|
|
864
|
+
await scriptReadyPromise;
|
|
865
|
+
|
|
866
|
+
// `assetsFetch()` will only be called if there is `proxyPort` defined.
|
|
867
|
+
// We only define `proxyPort`, above, when there is no `directory` defined.
|
|
868
|
+
const assetsFetch =
|
|
869
|
+
directory !== undefined
|
|
870
|
+
? await generateAssetsFetch(directory)
|
|
871
|
+
: invalidAssetsFetch;
|
|
872
|
+
|
|
846
873
|
const miniflare = new Miniflare({
|
|
847
874
|
port,
|
|
848
875
|
watch: true,
|
|
@@ -873,7 +900,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
873
900
|
if (proxyPort) {
|
|
874
901
|
try {
|
|
875
902
|
const url = new URL(request.url);
|
|
876
|
-
url.host = `
|
|
903
|
+
url.host = `localhost:${proxyPort}`;
|
|
877
904
|
return await fetch(url, request);
|
|
878
905
|
} catch (thrown) {
|
|
879
906
|
console.error(`Could not proxy request: ${thrown}`);
|
|
@@ -903,22 +930,29 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
903
930
|
kvPersist: true,
|
|
904
931
|
durableObjectsPersist: true,
|
|
905
932
|
cachePersist: true,
|
|
933
|
+
liveReload,
|
|
906
934
|
|
|
907
935
|
...miniflareArgs,
|
|
908
936
|
});
|
|
909
937
|
|
|
910
|
-
// Wait for esbuild to finish building the script
|
|
911
|
-
while (!scriptReady) {}
|
|
912
|
-
|
|
913
938
|
try {
|
|
914
939
|
// `startServer` might throw if user code contains errors
|
|
915
940
|
const server = await miniflare.startServer();
|
|
916
|
-
console.log(`Serving at http://
|
|
941
|
+
console.log(`Serving at http://localhost:${port}/`);
|
|
917
942
|
|
|
918
943
|
if (process.env.BROWSER !== "none") {
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
944
|
+
const childProcess = await open(`http://localhost:${port}/`);
|
|
945
|
+
// fail silently if the open command doesn't work (e.g. in GitHub Codespaces)
|
|
946
|
+
childProcess.on("error", (_err) => {});
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (directory !== undefined && liveReload) {
|
|
950
|
+
watch([directory], {
|
|
951
|
+
persistent: true,
|
|
952
|
+
ignoreInitial: true,
|
|
953
|
+
}).on("all", async () => {
|
|
954
|
+
await miniflare.reload();
|
|
955
|
+
});
|
|
922
956
|
}
|
|
923
957
|
|
|
924
958
|
EXIT_CALLBACKS.push(() => {
|
|
@@ -933,36 +967,74 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
933
967
|
)
|
|
934
968
|
.command("functions", "Cloudflare Pages Functions", (yargs) =>
|
|
935
969
|
yargs.command(
|
|
936
|
-
"build",
|
|
970
|
+
"build [directory]",
|
|
937
971
|
"Compile a folder of Cloudflare Pages Functions into a single Worker",
|
|
938
972
|
(yargs) =>
|
|
939
|
-
yargs
|
|
940
|
-
"
|
|
973
|
+
yargs
|
|
974
|
+
.positional("directory", {
|
|
941
975
|
type: "string",
|
|
942
|
-
default: "
|
|
943
|
-
description: "The
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
976
|
+
default: "functions",
|
|
977
|
+
description: "The directory of Pages Functions",
|
|
978
|
+
})
|
|
979
|
+
.options({
|
|
980
|
+
"script-path": {
|
|
981
|
+
type: "string",
|
|
982
|
+
default: "_worker.js",
|
|
983
|
+
description: "The location of the output Worker script",
|
|
984
|
+
},
|
|
985
|
+
"output-config-path": {
|
|
986
|
+
type: "string",
|
|
987
|
+
description: "The location for the output config file",
|
|
988
|
+
},
|
|
989
|
+
minify: {
|
|
990
|
+
type: "boolean",
|
|
991
|
+
default: false,
|
|
992
|
+
description: "Minify the output Worker script",
|
|
993
|
+
},
|
|
994
|
+
sourcemap: {
|
|
995
|
+
type: "boolean",
|
|
996
|
+
default: false,
|
|
997
|
+
description:
|
|
998
|
+
"Generate a sourcemap for the output Worker script",
|
|
999
|
+
},
|
|
1000
|
+
"fallback-service": {
|
|
1001
|
+
type: "string",
|
|
1002
|
+
default: "ASSETS",
|
|
1003
|
+
description:
|
|
1004
|
+
"The service to fallback to at the end of the `next` chain. Setting to '' will fallback to the global `fetch`.",
|
|
1005
|
+
},
|
|
1006
|
+
watch: {
|
|
1007
|
+
type: "boolean",
|
|
1008
|
+
default: false,
|
|
1009
|
+
description:
|
|
1010
|
+
"Watch for changes to the functions and automatically rebuild the Worker script",
|
|
1011
|
+
},
|
|
1012
|
+
}),
|
|
1013
|
+
async ({
|
|
1014
|
+
directory,
|
|
1015
|
+
"script-path": scriptPath,
|
|
1016
|
+
"output-config-path": outputConfigPath,
|
|
1017
|
+
minify,
|
|
1018
|
+
sourcemap,
|
|
1019
|
+
fallbackService,
|
|
1020
|
+
watch,
|
|
1021
|
+
}) => {
|
|
959
1022
|
await buildFunctions({
|
|
960
1023
|
scriptPath,
|
|
961
|
-
|
|
1024
|
+
outputConfigPath,
|
|
1025
|
+
functionsDirectory: directory,
|
|
962
1026
|
minify,
|
|
963
1027
|
sourcemap,
|
|
1028
|
+
fallbackService,
|
|
1029
|
+
watch,
|
|
964
1030
|
});
|
|
965
1031
|
}
|
|
966
1032
|
)
|
|
967
1033
|
);
|
|
968
1034
|
};
|
|
1035
|
+
|
|
1036
|
+
const invalidAssetsFetch: typeof fetch = () => {
|
|
1037
|
+
throw new Error(
|
|
1038
|
+
"Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
|
|
1039
|
+
);
|
|
1040
|
+
};
|
package/src/proxy.ts
CHANGED
|
@@ -1,97 +1,245 @@
|
|
|
1
1
|
import { connect } from "node:http2";
|
|
2
|
+
import type { ClientHttp2Session, ServerHttp2Stream } from "node:http2";
|
|
2
3
|
import { createServer } from "node:http";
|
|
3
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
IncomingHttpHeaders,
|
|
6
|
+
RequestListener,
|
|
7
|
+
IncomingMessage,
|
|
8
|
+
ServerResponse,
|
|
9
|
+
Server,
|
|
10
|
+
} from "node:http";
|
|
4
11
|
import WebSocket from "faye-websocket";
|
|
5
12
|
import serveStatic from "serve-static";
|
|
13
|
+
import type { CfPreviewToken } from "./api/preview";
|
|
14
|
+
import { useEffect, useRef } from "react";
|
|
6
15
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
/**
|
|
17
|
+
* `usePreviewServer` is a React hook that creates a local development
|
|
18
|
+
* server that can be used to develop a Worker.
|
|
19
|
+
*
|
|
20
|
+
* When we run `wrangler dev`, we start by uploading the compiled worker
|
|
21
|
+
* to the preview service, which responds with a preview token.
|
|
22
|
+
* (see `useWorker`/`createWorker` for details.)
|
|
23
|
+
* We can then use that token to connect to the preview server for a
|
|
24
|
+
* great local development experience. Further, as we change the worker,
|
|
25
|
+
* we can update the preview token transparently without having to restart
|
|
26
|
+
* the development server.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Rewrite request headers to add the preview token. */
|
|
30
|
+
function addCfPreviewTokenHeader(
|
|
31
|
+
headers: IncomingHttpHeaders,
|
|
32
|
+
previewTokenValue: string
|
|
33
|
+
) {
|
|
34
|
+
headers["cf-workers-preview-token"] = previewTokenValue;
|
|
12
35
|
}
|
|
13
36
|
|
|
14
37
|
/**
|
|
15
|
-
*
|
|
38
|
+
* Rewrite references in request headers
|
|
39
|
+
* from the preview host to the local host.
|
|
16
40
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
stream.respond(responseHeaders);
|
|
29
|
-
request.pipe(stream, { end: true });
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
// HTTP/1 -> HTTP/2
|
|
33
|
-
const handleRequest: RequestListener = (message, response) => {
|
|
34
|
-
const { httpVersionMajor, headers, method, url } = message;
|
|
35
|
-
if (httpVersionMajor >= 2) {
|
|
36
|
-
return; // Already handled by the "stream" event.
|
|
41
|
+
function rewriteRemoteHostToLocalHostInHeaders(
|
|
42
|
+
headers: IncomingHttpHeaders,
|
|
43
|
+
remoteHost: string,
|
|
44
|
+
localPort: number
|
|
45
|
+
) {
|
|
46
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
47
|
+
// Rewrite the remote host to the local host.
|
|
48
|
+
if (typeof value === "string" && value.includes(remoteHost)) {
|
|
49
|
+
headers[name] = value
|
|
50
|
+
.replaceAll(`https://${remoteHost}`, `http://localhost:${localPort}`)
|
|
51
|
+
.replaceAll(remoteHost, `localhost:${localPort}`);
|
|
37
52
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function usePreviewServer({
|
|
57
|
+
previewToken,
|
|
58
|
+
publicRoot,
|
|
59
|
+
port,
|
|
60
|
+
}: {
|
|
61
|
+
previewToken: CfPreviewToken | undefined;
|
|
62
|
+
publicRoot: undefined | string;
|
|
63
|
+
port: number;
|
|
64
|
+
}) {
|
|
65
|
+
/** Creates an HTTP/1 proxy that sends requests over HTTP/2. */
|
|
66
|
+
const proxyServer = useRef<Server>();
|
|
67
|
+
const proxy = (proxyServer.current ??= createProxyServer());
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* When we're not connected / getting a fresh token on changes,
|
|
71
|
+
* we'd like to buffer streams/requests until we're connected.
|
|
72
|
+
* Once connected, we can flush the buffered streams/requests.
|
|
73
|
+
* streamBufferRef is used to buffer http/2 streams, while
|
|
74
|
+
* requestResponseBufferRef is used to buffer http/1 requests.
|
|
75
|
+
*/
|
|
76
|
+
const streamBufferRef = useRef<
|
|
77
|
+
{ stream: ServerHttp2Stream; headers: IncomingHttpHeaders }[]
|
|
78
|
+
>([]);
|
|
79
|
+
const requestResponseBufferRef = useRef<
|
|
80
|
+
{ request: IncomingMessage; response: ServerResponse }[]
|
|
81
|
+
>([]);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
// If we don't have a token, that means either we're just starting up,
|
|
85
|
+
// or we're refreshing the token.
|
|
86
|
+
if (!previewToken) {
|
|
87
|
+
const cleanupListeners: (() => void)[] = [];
|
|
88
|
+
const bufferStream = (
|
|
89
|
+
stream: ServerHttp2Stream,
|
|
90
|
+
headers: IncomingHttpHeaders
|
|
91
|
+
) => {
|
|
92
|
+
// store the stream in a buffer so we can replay it later
|
|
93
|
+
streamBufferRef.current.push({ stream, headers });
|
|
94
|
+
};
|
|
95
|
+
proxy.on("stream", bufferStream);
|
|
96
|
+
cleanupListeners.push(() => proxy.off("stream", bufferStream));
|
|
97
|
+
|
|
98
|
+
const bufferRequestResponse = (
|
|
99
|
+
request: IncomingMessage,
|
|
100
|
+
response: ServerResponse
|
|
101
|
+
) => {
|
|
102
|
+
// store the request and response in a buffer so we can replay it later
|
|
103
|
+
requestResponseBufferRef.current.push({ request, response });
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
proxy.on("request", bufferRequestResponse);
|
|
107
|
+
cleanupListeners.push(() => proxy.off("request", bufferRequestResponse));
|
|
108
|
+
return () => {
|
|
109
|
+
cleanupListeners.forEach((cleanup) => cleanup());
|
|
110
|
+
};
|
|
47
111
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
112
|
+
|
|
113
|
+
// We have a token. Let's proxy requests to the preview end point.
|
|
114
|
+
const cleanupListeners: (() => void)[] = [];
|
|
115
|
+
|
|
116
|
+
const assetPath = typeof publicRoot === "string" ? publicRoot : null;
|
|
117
|
+
|
|
118
|
+
// create a ClientHttp2Session
|
|
119
|
+
const remote = connect(`https://${previewToken.host}`);
|
|
120
|
+
cleanupListeners.push(() => remote.destroy());
|
|
121
|
+
|
|
122
|
+
/** HTTP/2 -> HTTP/2 */
|
|
123
|
+
const handleStream = createStreamHandler(previewToken, remote, port);
|
|
124
|
+
proxy.on("stream", handleStream);
|
|
125
|
+
cleanupListeners.push(() => proxy.off("stream", handleStream));
|
|
126
|
+
|
|
127
|
+
// flush and replay buffered streams
|
|
128
|
+
streamBufferRef.current.forEach((buffer) =>
|
|
129
|
+
handleStream(buffer.stream, buffer.headers)
|
|
130
|
+
);
|
|
131
|
+
streamBufferRef.current = [];
|
|
132
|
+
|
|
133
|
+
/** HTTP/1 -> HTTP/2 */
|
|
134
|
+
const handleRequest: RequestListener = (
|
|
135
|
+
message: IncomingMessage,
|
|
136
|
+
response: ServerResponse
|
|
137
|
+
) => {
|
|
138
|
+
const { httpVersionMajor, headers, method, url } = message;
|
|
139
|
+
if (httpVersionMajor >= 2) {
|
|
140
|
+
return; // Already handled by the "stream" event.
|
|
141
|
+
}
|
|
142
|
+
addCfPreviewTokenHeader(headers, previewToken.value);
|
|
143
|
+
headers[":method"] = method;
|
|
144
|
+
headers[":path"] = url;
|
|
145
|
+
headers[":authority"] = previewToken.host;
|
|
146
|
+
headers[":scheme"] = "https";
|
|
147
|
+
for (const name of Object.keys(headers)) {
|
|
148
|
+
if (HTTP1_HEADERS.has(name.toLowerCase())) {
|
|
149
|
+
delete headers[name];
|
|
55
150
|
}
|
|
56
151
|
}
|
|
57
|
-
|
|
58
|
-
request.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
152
|
+
const request = message.pipe(remote.request(headers));
|
|
153
|
+
request.on("response", (responseHeaders) => {
|
|
154
|
+
const status = responseHeaders[":status"] ?? 500;
|
|
155
|
+
rewriteRemoteHostToLocalHostInHeaders(
|
|
156
|
+
responseHeaders,
|
|
157
|
+
previewToken.host,
|
|
158
|
+
port
|
|
159
|
+
);
|
|
160
|
+
for (const name of Object.keys(responseHeaders)) {
|
|
161
|
+
if (name.startsWith(":")) {
|
|
162
|
+
delete responseHeaders[name];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
response.writeHead(status, responseHeaders);
|
|
166
|
+
request.pipe(response, { end: true });
|
|
70
167
|
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
{ headers }
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// If an asset path is defined, check the file system
|
|
171
|
+
// for a file first and serve if it exists.
|
|
172
|
+
const actualHandleRequest = assetPath
|
|
173
|
+
? createHandleAssetsRequest(assetPath, handleRequest)
|
|
174
|
+
: handleRequest;
|
|
175
|
+
|
|
176
|
+
proxy.on("request", actualHandleRequest);
|
|
177
|
+
cleanupListeners.push(() => proxy.off("request", actualHandleRequest));
|
|
178
|
+
|
|
179
|
+
// flush and replay buffered requests
|
|
180
|
+
requestResponseBufferRef.current.forEach(({ request, response }) =>
|
|
181
|
+
actualHandleRequest(request, response)
|
|
86
182
|
);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
183
|
+
requestResponseBufferRef.current = [];
|
|
184
|
+
|
|
185
|
+
/** HTTP/1 -> WebSocket (over HTTP/1) */
|
|
186
|
+
const handleUpgrade = (
|
|
187
|
+
message: IncomingMessage,
|
|
188
|
+
socket: WebSocket,
|
|
189
|
+
body: Buffer
|
|
190
|
+
) => {
|
|
191
|
+
const { headers, url } = message;
|
|
192
|
+
addCfPreviewTokenHeader(headers, previewToken.value);
|
|
193
|
+
headers["host"] = previewToken.host;
|
|
194
|
+
const localWebsocket = new WebSocket(message, socket, body);
|
|
195
|
+
// TODO(soon): Custom WebSocket protocol is not working?
|
|
196
|
+
const remoteWebsocketClient = new WebSocket.Client(
|
|
197
|
+
`wss://${previewToken.host}${url}`,
|
|
198
|
+
[],
|
|
199
|
+
{ headers }
|
|
200
|
+
);
|
|
201
|
+
localWebsocket.pipe(remoteWebsocketClient).pipe(localWebsocket);
|
|
202
|
+
// We close down websockets whenever we refresh the token.
|
|
203
|
+
cleanupListeners.push(() => {
|
|
204
|
+
localWebsocket.destroy();
|
|
205
|
+
remoteWebsocketClient.destroy();
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
proxy.on("upgrade", handleUpgrade);
|
|
209
|
+
cleanupListeners.push(() => proxy.off("upgrade", handleUpgrade));
|
|
210
|
+
|
|
211
|
+
return () => {
|
|
212
|
+
cleanupListeners.forEach((d) => d());
|
|
213
|
+
};
|
|
214
|
+
}, [previewToken, publicRoot, port, proxy]);
|
|
215
|
+
|
|
216
|
+
// Start/stop the server whenever the
|
|
217
|
+
// containing component is mounted/unmounted.
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
proxy.listen(port);
|
|
220
|
+
console.log(`⬣ Listening at http://localhost:${port}`);
|
|
221
|
+
|
|
222
|
+
return () => {
|
|
223
|
+
proxy.close();
|
|
224
|
+
};
|
|
225
|
+
}, [port, proxy]);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function createHandleAssetsRequest(
|
|
229
|
+
assetPath: string,
|
|
230
|
+
handleRequest: RequestListener
|
|
231
|
+
) {
|
|
232
|
+
const handleAsset = serveStatic(assetPath, {
|
|
233
|
+
cacheControl: false,
|
|
91
234
|
});
|
|
92
|
-
return
|
|
235
|
+
return (request: IncomingMessage, response: ServerResponse) => {
|
|
236
|
+
handleAsset(request, response, () => {
|
|
237
|
+
handleRequest(request, response);
|
|
238
|
+
});
|
|
239
|
+
};
|
|
93
240
|
}
|
|
94
241
|
|
|
242
|
+
/** A Set of headers we want to remove from HTTP/1 requests. */
|
|
95
243
|
const HTTP1_HEADERS = new Set([
|
|
96
244
|
"host",
|
|
97
245
|
"connection",
|
|
@@ -101,3 +249,54 @@ const HTTP1_HEADERS = new Set([
|
|
|
101
249
|
"transfer-encoding",
|
|
102
250
|
"http2-settings",
|
|
103
251
|
]);
|
|
252
|
+
|
|
253
|
+
function createProxyServer() {
|
|
254
|
+
return createServer()
|
|
255
|
+
.on("request", function (req, res) {
|
|
256
|
+
// log all requests
|
|
257
|
+
console.log(
|
|
258
|
+
new Date().toLocaleTimeString(),
|
|
259
|
+
req.method,
|
|
260
|
+
req.url,
|
|
261
|
+
res.statusCode
|
|
262
|
+
);
|
|
263
|
+
})
|
|
264
|
+
.on("upgrade", (req) => {
|
|
265
|
+
// log all websocket connections
|
|
266
|
+
console.log(
|
|
267
|
+
new Date().toLocaleTimeString(),
|
|
268
|
+
req.method,
|
|
269
|
+
req.url,
|
|
270
|
+
101,
|
|
271
|
+
"(WebSocket)"
|
|
272
|
+
);
|
|
273
|
+
})
|
|
274
|
+
.on("error", (err) => {
|
|
275
|
+
// log all connection errors
|
|
276
|
+
console.error(new Date().toLocaleTimeString(), err);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function createStreamHandler(
|
|
281
|
+
previewToken: CfPreviewToken,
|
|
282
|
+
remote: ClientHttp2Session,
|
|
283
|
+
port: number
|
|
284
|
+
) {
|
|
285
|
+
return function handleStream(
|
|
286
|
+
stream: ServerHttp2Stream,
|
|
287
|
+
headers: IncomingHttpHeaders
|
|
288
|
+
) {
|
|
289
|
+
addCfPreviewTokenHeader(headers, previewToken.value);
|
|
290
|
+
headers[":authority"] = previewToken.host;
|
|
291
|
+
const request = stream.pipe(remote.request(headers));
|
|
292
|
+
request.on("response", (responseHeaders: IncomingHttpHeaders) => {
|
|
293
|
+
rewriteRemoteHostToLocalHostInHeaders(
|
|
294
|
+
responseHeaders,
|
|
295
|
+
previewToken.host,
|
|
296
|
+
port
|
|
297
|
+
);
|
|
298
|
+
stream.respond(responseHeaders);
|
|
299
|
+
request.pipe(stream, { end: true });
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
}
|