wrangler 0.0.0-e2a26a0 → 0.0.0-e3278fa
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/Cloudflare_CA.pem +18 -0
- package/README.md +27 -14
- package/bin/wrangler.js +49 -5
- package/miniflare-dist/index.mjs +4886 -0
- package/package.json +51 -27
- package/pages/functions/buildPlugin.ts +105 -0
- package/pages/functions/buildWorker.ts +93 -4
- package/pages/functions/filepath-routing.test.ts +223 -36
- package/pages/functions/filepath-routing.ts +78 -120
- package/pages/functions/routes.ts +13 -17
- package/pages/functions/template-plugin.ts +147 -0
- package/pages/functions/template-worker.ts +24 -17
- package/src/__tests__/config-cache.test.ts +36 -0
- package/src/__tests__/configuration.test.ts +3408 -0
- package/src/__tests__/dev.test.tsx +977 -65
- package/src/__tests__/guess-worker-format.test.ts +85 -0
- package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
- package/src/__tests__/helpers/mock-account-id.ts +26 -8
- package/src/__tests__/helpers/mock-bin.ts +4 -1
- package/src/__tests__/helpers/mock-cfetch.ts +10 -6
- package/src/__tests__/helpers/mock-console.ts +39 -8
- package/src/__tests__/helpers/mock-dialogs.ts +67 -2
- package/src/__tests__/helpers/mock-http-server.ts +46 -0
- package/src/__tests__/helpers/mock-istty.ts +27 -0
- package/src/__tests__/helpers/mock-kv.ts +9 -16
- package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
- package/src/__tests__/helpers/mock-stdin.ts +103 -0
- package/src/__tests__/helpers/mock-web-socket.ts +29 -0
- package/src/__tests__/helpers/run-in-tmp.ts +10 -0
- package/src/__tests__/helpers/run-wrangler.ts +5 -3
- package/src/__tests__/helpers/write-worker-source.ts +31 -0
- package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
- package/src/__tests__/https-options.test.ts +130 -0
- package/src/__tests__/index.test.ts +148 -435
- package/src/__tests__/init.test.ts +1637 -0
- package/src/__tests__/jest.setup.ts +45 -5
- package/src/__tests__/kv.test.ts +590 -248
- package/src/__tests__/logger.test.ts +115 -0
- package/src/__tests__/package-manager.test.ts +156 -9
- package/src/__tests__/pages.test.ts +754 -0
- package/src/__tests__/parse.test.ts +330 -0
- package/src/__tests__/publish.test.ts +5471 -939
- package/src/__tests__/r2.test.ts +233 -0
- package/src/__tests__/route.test.ts +45 -0
- package/src/__tests__/secret.test.ts +362 -91
- package/src/__tests__/tail.test.ts +690 -0
- package/src/__tests__/user.test.ts +131 -0
- package/src/__tests__/whoami.test.tsx +60 -7
- package/src/abort.d.ts +3 -0
- package/src/bundle.ts +197 -0
- package/src/cfetch/index.ts +27 -8
- package/src/cfetch/internal.ts +73 -32
- package/src/cli.ts +6 -2
- package/src/config/README.md +107 -0
- package/src/config/config.ts +234 -0
- package/src/config/diagnostics.ts +80 -0
- package/src/config/environment.ts +420 -0
- package/src/config/index.ts +225 -0
- package/src/config/validation-helpers.ts +584 -0
- package/src/config/validation.ts +1539 -0
- package/src/config-cache.ts +45 -0
- package/src/create-worker-preview.ts +185 -0
- package/src/create-worker-upload-form.ts +254 -0
- package/src/dev/dev-vars.ts +36 -0
- package/src/dev/dev.tsx +412 -0
- package/src/dev/local.tsx +322 -0
- package/src/dev/remote.tsx +237 -0
- package/src/dev/use-esbuild.ts +118 -0
- package/src/dialogs.tsx +64 -6
- package/src/durable.ts +102 -0
- package/src/entry.ts +322 -0
- package/src/environment-variables.ts +35 -0
- package/src/errors.ts +11 -0
- package/src/https-options.ts +123 -0
- package/src/index.tsx +1747 -1002
- package/src/inspect.ts +80 -14
- package/src/intl-polyfill.d.ts +139 -0
- package/src/{kv.tsx → kv.ts} +192 -68
- package/src/logger.ts +66 -0
- package/src/miniflare-cli/README.md +30 -0
- package/src/miniflare-cli/enum-keys.ts +17 -0
- package/src/miniflare-cli/index.ts +41 -0
- package/src/miniflare-cli/request-context.ts +40 -0
- package/src/module-collection.ts +226 -31
- package/src/open-in-browser.ts +19 -0
- package/src/package-manager.ts +79 -25
- package/src/pages.tsx +1131 -84
- package/src/parse.ts +202 -0
- package/src/proxy.ts +170 -62
- package/src/publish.ts +612 -306
- package/src/r2.ts +50 -0
- package/src/selfsigned.d.ts +29 -0
- package/src/sites.tsx +143 -57
- package/src/tail/filters.ts +279 -0
- package/src/tail/index.ts +299 -0
- package/src/tail/printing.ts +67 -0
- package/src/update-check.ts +19 -0
- package/src/user.tsx +341 -146
- package/src/whoami.tsx +5 -3
- package/src/{api/worker.ts → worker.ts} +65 -39
- package/src/zones.ts +73 -0
- package/templates/gitignore +171 -0
- package/templates/new-worker-scheduled.js +17 -0
- package/templates/new-worker-scheduled.ts +32 -0
- package/templates/new-worker.js +1 -1
- package/templates/new-worker.ts +17 -2
- package/templates/static-asset-facade.js +1 -5
- package/templates/tsconfig.json +105 -0
- package/wrangler-dist/cli.js +72295 -71006
- package/src/__tests__/helpers/mock-user.ts +0 -27
- package/src/__tests__/logout.test.ts +0 -50
- package/src/api/form_data.ts +0 -134
- package/src/api/preview.ts +0 -128
- package/src/config.ts +0 -641
- package/src/dev.tsx +0 -834
- package/src/tail.tsx +0 -73
- package/vendor/@cloudflare/kv-asset-handler/CHANGELOG.md +0 -332
- package/vendor/@cloudflare/kv-asset-handler/LICENSE_APACHE +0 -176
- package/vendor/@cloudflare/kv-asset-handler/LICENSE_MIT +0 -25
- package/vendor/@cloudflare/kv-asset-handler/README.md +0 -245
- package/vendor/@cloudflare/kv-asset-handler/dist/index.d.ts +0 -32
- package/vendor/@cloudflare/kv-asset-handler/dist/index.js +0 -354
- package/vendor/@cloudflare/kv-asset-handler/dist/mocks.d.ts +0 -13
- package/vendor/@cloudflare/kv-asset-handler/dist/mocks.js +0 -148
- package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.d.ts +0 -1
- package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.js +0 -436
- package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.d.ts +0 -1
- package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.js +0 -40
- package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.d.ts +0 -1
- package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.js +0 -42
- package/vendor/@cloudflare/kv-asset-handler/dist/types.d.ts +0 -26
- package/vendor/@cloudflare/kv-asset-handler/dist/types.js +0 -31
- package/vendor/@cloudflare/kv-asset-handler/package.json +0 -52
- package/vendor/@cloudflare/kv-asset-handler/src/index.ts +0 -296
- package/vendor/@cloudflare/kv-asset-handler/src/mocks.ts +0 -136
- package/vendor/@cloudflare/kv-asset-handler/src/test/getAssetFromKV.ts +0 -464
- package/vendor/@cloudflare/kv-asset-handler/src/test/mapRequestToAsset.ts +0 -33
- package/vendor/@cloudflare/kv-asset-handler/src/test/serveSinglePageApp.ts +0 -42
- package/vendor/@cloudflare/kv-asset-handler/src/types.ts +0 -39
- package/vendor/wrangler-mime/CHANGELOG.md +0 -289
- package/vendor/wrangler-mime/LICENSE +0 -21
- package/vendor/wrangler-mime/Mime.js +0 -97
- package/vendor/wrangler-mime/README.md +0 -187
- package/vendor/wrangler-mime/cli.js +0 -46
- package/vendor/wrangler-mime/index.js +0 -4
- package/vendor/wrangler-mime/lite.js +0 -4
- package/vendor/wrangler-mime/package.json +0 -52
- package/vendor/wrangler-mime/types/other.js +0 -1
- package/vendor/wrangler-mime/types/standard.js +0 -1
- package/wrangler-dist/cli.js.map +0 -7
package/src/parse.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import TOML from "@iarna/toml";
|
|
4
|
+
import { formatMessagesSync } from "esbuild";
|
|
5
|
+
|
|
6
|
+
export type Message = {
|
|
7
|
+
text: string;
|
|
8
|
+
location?: Location;
|
|
9
|
+
notes?: Message[];
|
|
10
|
+
kind?: "warning" | "error";
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type Location = File & {
|
|
14
|
+
line: number;
|
|
15
|
+
column: number;
|
|
16
|
+
length?: number;
|
|
17
|
+
lineText?: string;
|
|
18
|
+
suggestion?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type File = {
|
|
22
|
+
file?: string;
|
|
23
|
+
fileText?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Formats a `Message` using esbuild's pretty-printing algorithm.
|
|
28
|
+
*/
|
|
29
|
+
export function formatMessage(
|
|
30
|
+
{ text, notes, location, kind = "error" }: Message,
|
|
31
|
+
color = true
|
|
32
|
+
): string {
|
|
33
|
+
const input = { text, notes, location };
|
|
34
|
+
delete input.location?.fileText;
|
|
35
|
+
for (const note of notes ?? []) {
|
|
36
|
+
delete note.location?.fileText;
|
|
37
|
+
}
|
|
38
|
+
const lines = formatMessagesSync([input], {
|
|
39
|
+
color,
|
|
40
|
+
kind: kind,
|
|
41
|
+
terminalWidth: process.stderr.columns,
|
|
42
|
+
});
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* An error that's thrown when something fails to parse.
|
|
48
|
+
*/
|
|
49
|
+
export class ParseError extends Error implements Message {
|
|
50
|
+
readonly text: string;
|
|
51
|
+
readonly notes: Message[];
|
|
52
|
+
readonly location?: Location;
|
|
53
|
+
readonly kind: "warning" | "error";
|
|
54
|
+
|
|
55
|
+
constructor({ text, notes, location, kind }: Message) {
|
|
56
|
+
super(text);
|
|
57
|
+
this.name = this.constructor.name;
|
|
58
|
+
this.text = text;
|
|
59
|
+
this.notes = notes ?? [];
|
|
60
|
+
this.location = location;
|
|
61
|
+
this.kind = kind ?? "error";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const TOML_ERROR_NAME = "TomlError";
|
|
66
|
+
const TOML_ERROR_SUFFIX = " at row ";
|
|
67
|
+
|
|
68
|
+
type TomlError = Error & {
|
|
69
|
+
line: number;
|
|
70
|
+
col: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A wrapper around `TOML.parse` that throws a `ParseError`.
|
|
75
|
+
*/
|
|
76
|
+
export function parseTOML(input: string, file?: string): TOML.JsonMap | never {
|
|
77
|
+
try {
|
|
78
|
+
// Normalize CRLF to LF to avoid hitting https://github.com/iarna/iarna-toml/issues/33.
|
|
79
|
+
const normalizedInput = input.replace(/\r\n/g, "\n");
|
|
80
|
+
return TOML.parse(normalizedInput);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const { name, message, line, col } = err as TomlError;
|
|
83
|
+
if (name !== TOML_ERROR_NAME) {
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
const text = message.substring(0, message.lastIndexOf(TOML_ERROR_SUFFIX));
|
|
87
|
+
const lineText = input.split("\n")[line];
|
|
88
|
+
const location = {
|
|
89
|
+
lineText,
|
|
90
|
+
line: line + 1,
|
|
91
|
+
column: col - 1,
|
|
92
|
+
file,
|
|
93
|
+
fileText: input,
|
|
94
|
+
};
|
|
95
|
+
throw new ParseError({ text, location });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const JSON_ERROR_SUFFIX = " in JSON at position ";
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A minimal type describing a package.json file.
|
|
103
|
+
*/
|
|
104
|
+
export type PackageJSON = {
|
|
105
|
+
devDependencies?: Record<string, unknown>;
|
|
106
|
+
dependencies?: Record<string, unknown>;
|
|
107
|
+
scripts?: Record<string, unknown>;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A typed version of `parseJSON()`.
|
|
112
|
+
*/
|
|
113
|
+
export function parsePackageJSON<T extends PackageJSON = PackageJSON>(
|
|
114
|
+
input: string,
|
|
115
|
+
file?: string
|
|
116
|
+
): T {
|
|
117
|
+
return parseJSON<T>(input, file);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A wrapper around `JSON.parse` that throws a `ParseError`.
|
|
122
|
+
*/
|
|
123
|
+
export function parseJSON<T>(input: string, file?: string): T {
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(input);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
const { message } = err as Error;
|
|
128
|
+
const index = message.lastIndexOf(JSON_ERROR_SUFFIX);
|
|
129
|
+
if (index < 0) {
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
const text = message.substring(0, index);
|
|
133
|
+
const position = parseInt(
|
|
134
|
+
message.substring(index + JSON_ERROR_SUFFIX.length)
|
|
135
|
+
);
|
|
136
|
+
const location = indexLocation({ file, fileText: input }, position);
|
|
137
|
+
throw new ParseError({ text, location });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Reads a file and parses it based on its type.
|
|
143
|
+
*/
|
|
144
|
+
export function readFileSync(file: string): string {
|
|
145
|
+
try {
|
|
146
|
+
return fs.readFileSync(file, { encoding: "utf-8" });
|
|
147
|
+
} catch (err) {
|
|
148
|
+
const { message } = err as Error;
|
|
149
|
+
throw new ParseError({
|
|
150
|
+
text: `Could not read file: ${file}`,
|
|
151
|
+
notes: [
|
|
152
|
+
{
|
|
153
|
+
text: message.replace(file, resolve(file)),
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Calculates the line and column location from an index.
|
|
162
|
+
*/
|
|
163
|
+
export function indexLocation(file: File, index: number): Location {
|
|
164
|
+
let lineText,
|
|
165
|
+
line = 0,
|
|
166
|
+
column = 0,
|
|
167
|
+
cursor = 0;
|
|
168
|
+
const { fileText = "" } = file;
|
|
169
|
+
for (const row of fileText.split("\n")) {
|
|
170
|
+
line++;
|
|
171
|
+
cursor += row.length + 1;
|
|
172
|
+
if (cursor >= index) {
|
|
173
|
+
lineText = row;
|
|
174
|
+
column = row.length - (cursor - index);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { lineText, line, column, ...file };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Guesses the line and column location of a search query.
|
|
183
|
+
*/
|
|
184
|
+
export function searchLocation(file: File, query: unknown): Location {
|
|
185
|
+
let lineText,
|
|
186
|
+
length,
|
|
187
|
+
line = 0,
|
|
188
|
+
column = 0;
|
|
189
|
+
const queryText = String(query);
|
|
190
|
+
const { fileText = "" } = file;
|
|
191
|
+
for (const content of fileText.split("\n")) {
|
|
192
|
+
line++;
|
|
193
|
+
const index = content.indexOf(queryText);
|
|
194
|
+
if (index >= 0) {
|
|
195
|
+
lineText = content;
|
|
196
|
+
column = index;
|
|
197
|
+
length = queryText.length;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { lineText, line, column, length, ...file };
|
|
202
|
+
}
|
package/src/proxy.ts
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
|
-
import { createServer } from "node:http";
|
|
1
|
+
import { createServer as createHttpServer } from "node:http";
|
|
2
2
|
import { connect } from "node:http2";
|
|
3
|
+
import { createServer as createHttpsServer } from "node:https";
|
|
3
4
|
import WebSocket from "faye-websocket";
|
|
5
|
+
import { createHttpTerminator } from "http-terminator";
|
|
4
6
|
import { useEffect, useRef, useState } from "react";
|
|
5
7
|
import serveStatic from "serve-static";
|
|
6
|
-
import
|
|
8
|
+
import { getHttpsOptions } from "./https-options";
|
|
9
|
+
import { logger } from "./logger";
|
|
10
|
+
import type { CfPreviewToken } from "./create-worker-preview";
|
|
11
|
+
import type { HttpTerminator } from "http-terminator";
|
|
7
12
|
import type {
|
|
8
13
|
IncomingHttpHeaders,
|
|
9
14
|
RequestListener,
|
|
10
15
|
IncomingMessage,
|
|
11
16
|
ServerResponse,
|
|
12
|
-
Server,
|
|
17
|
+
Server as HttpServer,
|
|
13
18
|
} from "node:http";
|
|
14
19
|
import type { ClientHttp2Session, ServerHttp2Stream } from "node:http2";
|
|
20
|
+
import type { Server as HttpsServer } from "node:https";
|
|
21
|
+
import type ws from "ws";
|
|
22
|
+
|
|
23
|
+
interface IWebsocket extends ws {
|
|
24
|
+
// Pipe implements .on("message", ...)
|
|
25
|
+
pipe<T>(fn: T): IWebsocket;
|
|
26
|
+
}
|
|
15
27
|
|
|
16
28
|
/**
|
|
17
29
|
* `usePreviewServer` is a React hook that creates a local development
|
|
@@ -41,30 +53,64 @@ function addCfPreviewTokenHeader(
|
|
|
41
53
|
function rewriteRemoteHostToLocalHostInHeaders(
|
|
42
54
|
headers: IncomingHttpHeaders,
|
|
43
55
|
remoteHost: string,
|
|
44
|
-
localPort: number
|
|
56
|
+
localPort: number,
|
|
57
|
+
localProtocol: "https" | "http"
|
|
45
58
|
) {
|
|
46
59
|
for (const [name, value] of Object.entries(headers)) {
|
|
47
60
|
// Rewrite the remote host to the local host.
|
|
48
61
|
if (typeof value === "string" && value.includes(remoteHost)) {
|
|
49
62
|
headers[name] = value
|
|
50
|
-
.replaceAll(
|
|
63
|
+
.replaceAll(
|
|
64
|
+
`https://${remoteHost}`,
|
|
65
|
+
`${localProtocol}://localhost:${localPort}`
|
|
66
|
+
)
|
|
51
67
|
.replaceAll(remoteHost, `localhost:${localPort}`);
|
|
52
68
|
}
|
|
53
69
|
}
|
|
54
70
|
}
|
|
55
71
|
|
|
72
|
+
type PreviewProxy = {
|
|
73
|
+
server: HttpServer | HttpsServer;
|
|
74
|
+
terminator: HttpTerminator;
|
|
75
|
+
};
|
|
76
|
+
|
|
56
77
|
export function usePreviewServer({
|
|
57
78
|
previewToken,
|
|
58
79
|
publicRoot,
|
|
59
|
-
|
|
80
|
+
localProtocol,
|
|
81
|
+
localPort: port,
|
|
82
|
+
ip,
|
|
60
83
|
}: {
|
|
61
84
|
previewToken: CfPreviewToken | undefined;
|
|
62
|
-
publicRoot:
|
|
63
|
-
|
|
85
|
+
publicRoot: string | undefined;
|
|
86
|
+
localProtocol: "https" | "http";
|
|
87
|
+
localPort: number;
|
|
88
|
+
ip: string;
|
|
64
89
|
}) {
|
|
65
90
|
/** Creates an HTTP/1 proxy that sends requests over HTTP/2. */
|
|
66
|
-
const
|
|
67
|
-
|
|
91
|
+
const [proxy, setProxy] = useState<PreviewProxy>();
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create the instance of the local proxy server that will pass on
|
|
95
|
+
* requests to the preview worker.
|
|
96
|
+
*/
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (proxy === undefined) {
|
|
99
|
+
createProxyServer(localProtocol)
|
|
100
|
+
.then((server) => {
|
|
101
|
+
setProxy({
|
|
102
|
+
server,
|
|
103
|
+
terminator: createHttpTerminator({
|
|
104
|
+
server,
|
|
105
|
+
gracefulTerminationTimeout: 0,
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
})
|
|
109
|
+
.catch(async (err) => {
|
|
110
|
+
logger.error("Failed to create proxy server:", err);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}, [proxy, localProtocol]);
|
|
68
114
|
|
|
69
115
|
/**
|
|
70
116
|
* When we're not connected / getting a fresh token on changes,
|
|
@@ -81,7 +127,7 @@ export function usePreviewServer({
|
|
|
81
127
|
>([]);
|
|
82
128
|
|
|
83
129
|
/**
|
|
84
|
-
* The session doesn't last forever, and will
|
|
130
|
+
* The session doesn't last forever, and will eventually drop
|
|
85
131
|
* (usually within 5-15 minutes). When that happens, we simply
|
|
86
132
|
* restart the effect, effectively restarting the server. We use
|
|
87
133
|
* a state sigil as an effect dependency to do so.
|
|
@@ -92,6 +138,10 @@ export function usePreviewServer({
|
|
|
92
138
|
}
|
|
93
139
|
|
|
94
140
|
useEffect(() => {
|
|
141
|
+
if (proxy === undefined) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
95
145
|
// If we don't have a token, that means either we're just starting up,
|
|
96
146
|
// or we're refreshing the token.
|
|
97
147
|
if (!previewToken) {
|
|
@@ -103,8 +153,8 @@ export function usePreviewServer({
|
|
|
103
153
|
// store the stream in a buffer so we can replay it later
|
|
104
154
|
streamBufferRef.current.push({ stream, headers });
|
|
105
155
|
};
|
|
106
|
-
proxy.on("stream", bufferStream);
|
|
107
|
-
cleanupListeners.push(() => proxy.off("stream", bufferStream));
|
|
156
|
+
proxy.server.on("stream", bufferStream);
|
|
157
|
+
cleanupListeners.push(() => proxy.server.off("stream", bufferStream));
|
|
108
158
|
|
|
109
159
|
const bufferRequestResponse = (
|
|
110
160
|
request: IncomingMessage,
|
|
@@ -114,8 +164,10 @@ export function usePreviewServer({
|
|
|
114
164
|
requestResponseBufferRef.current.push({ request, response });
|
|
115
165
|
};
|
|
116
166
|
|
|
117
|
-
proxy.on("request", bufferRequestResponse);
|
|
118
|
-
cleanupListeners.push(() =>
|
|
167
|
+
proxy.server.on("request", bufferRequestResponse);
|
|
168
|
+
cleanupListeners.push(() =>
|
|
169
|
+
proxy.server.off("request", bufferRequestResponse)
|
|
170
|
+
);
|
|
119
171
|
return () => {
|
|
120
172
|
cleanupListeners.forEach((cleanup) => cleanup());
|
|
121
173
|
};
|
|
@@ -136,9 +188,14 @@ export function usePreviewServer({
|
|
|
136
188
|
cleanupListeners.push(() => remote.off("close", retryServerSetup));
|
|
137
189
|
|
|
138
190
|
/** HTTP/2 -> HTTP/2 */
|
|
139
|
-
const handleStream = createStreamHandler(
|
|
140
|
-
|
|
141
|
-
|
|
191
|
+
const handleStream = createStreamHandler(
|
|
192
|
+
previewToken,
|
|
193
|
+
remote,
|
|
194
|
+
port,
|
|
195
|
+
localProtocol
|
|
196
|
+
);
|
|
197
|
+
proxy.server.on("stream", handleStream);
|
|
198
|
+
cleanupListeners.push(() => proxy.server.off("stream", handleStream));
|
|
142
199
|
|
|
143
200
|
// flush and replay buffered streams
|
|
144
201
|
streamBufferRef.current.forEach((buffer) =>
|
|
@@ -168,10 +225,15 @@ export function usePreviewServer({
|
|
|
168
225
|
const request = message.pipe(remote.request(headers));
|
|
169
226
|
request.on("response", (responseHeaders) => {
|
|
170
227
|
const status = responseHeaders[":status"] ?? 500;
|
|
228
|
+
|
|
229
|
+
// log all requests to terminal
|
|
230
|
+
logger.log(new Date().toLocaleTimeString(), method, url, status);
|
|
231
|
+
|
|
171
232
|
rewriteRemoteHostToLocalHostInHeaders(
|
|
172
233
|
responseHeaders,
|
|
173
234
|
previewToken.host,
|
|
174
|
-
port
|
|
235
|
+
port,
|
|
236
|
+
localProtocol
|
|
175
237
|
);
|
|
176
238
|
for (const name of Object.keys(responseHeaders)) {
|
|
177
239
|
if (name.startsWith(":")) {
|
|
@@ -189,8 +251,10 @@ export function usePreviewServer({
|
|
|
189
251
|
? createHandleAssetsRequest(assetPath, handleRequest)
|
|
190
252
|
: handleRequest;
|
|
191
253
|
|
|
192
|
-
proxy.on("request", actualHandleRequest);
|
|
193
|
-
cleanupListeners.push(() =>
|
|
254
|
+
proxy.server.on("request", actualHandleRequest);
|
|
255
|
+
cleanupListeners.push(() =>
|
|
256
|
+
proxy.server.off("request", actualHandleRequest)
|
|
257
|
+
);
|
|
194
258
|
|
|
195
259
|
// flush and replay buffered requests
|
|
196
260
|
requestResponseBufferRef.current.forEach(({ request, response }) =>
|
|
@@ -207,22 +271,22 @@ export function usePreviewServer({
|
|
|
207
271
|
const { headers, url } = message;
|
|
208
272
|
addCfPreviewTokenHeader(headers, previewToken.value);
|
|
209
273
|
headers["host"] = previewToken.host;
|
|
210
|
-
const localWebsocket = new WebSocket(message, socket, body);
|
|
274
|
+
const localWebsocket = new WebSocket(message, socket, body) as IWebsocket;
|
|
211
275
|
// TODO(soon): Custom WebSocket protocol is not working?
|
|
212
276
|
const remoteWebsocketClient = new WebSocket.Client(
|
|
213
277
|
`wss://${previewToken.host}${url}`,
|
|
214
278
|
[],
|
|
215
279
|
{ headers }
|
|
216
|
-
);
|
|
280
|
+
) as IWebsocket;
|
|
217
281
|
localWebsocket.pipe(remoteWebsocketClient).pipe(localWebsocket);
|
|
218
282
|
// We close down websockets whenever we refresh the token.
|
|
219
283
|
cleanupListeners.push(() => {
|
|
220
|
-
localWebsocket.
|
|
221
|
-
remoteWebsocketClient.
|
|
284
|
+
localWebsocket.close();
|
|
285
|
+
remoteWebsocketClient.close();
|
|
222
286
|
});
|
|
223
287
|
};
|
|
224
|
-
proxy.on("upgrade", handleUpgrade);
|
|
225
|
-
cleanupListeners.push(() => proxy.off("upgrade", handleUpgrade));
|
|
288
|
+
proxy.server.on("upgrade", handleUpgrade);
|
|
289
|
+
cleanupListeners.push(() => proxy.server.off("upgrade", handleUpgrade));
|
|
226
290
|
|
|
227
291
|
return () => {
|
|
228
292
|
cleanupListeners.forEach((cleanup) => cleanup());
|
|
@@ -231,6 +295,7 @@ export function usePreviewServer({
|
|
|
231
295
|
previewToken,
|
|
232
296
|
publicRoot,
|
|
233
297
|
port,
|
|
298
|
+
localProtocol,
|
|
234
299
|
proxy,
|
|
235
300
|
// We use a state value as a sigil to trigger reconnecting the server.
|
|
236
301
|
// It's not used inside the effect, so react-hooks/exhaustive-deps
|
|
@@ -242,19 +307,37 @@ export function usePreviewServer({
|
|
|
242
307
|
// Start/stop the server whenever the
|
|
243
308
|
// containing component is mounted/unmounted.
|
|
244
309
|
useEffect(() => {
|
|
245
|
-
|
|
310
|
+
const abortController = new AbortController();
|
|
311
|
+
if (proxy === undefined) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
waitForPortToBeAvailable(port, {
|
|
316
|
+
retryPeriod: 200,
|
|
317
|
+
timeout: 2000,
|
|
318
|
+
abortSignal: abortController.signal,
|
|
319
|
+
})
|
|
246
320
|
.then(() => {
|
|
247
|
-
proxy.
|
|
248
|
-
|
|
321
|
+
proxy.server.on("listening", () => {
|
|
322
|
+
logger.log(`⬣ Listening at ${localProtocol}://${ip}:${port}`);
|
|
323
|
+
});
|
|
324
|
+
proxy.server.listen(port, ip);
|
|
249
325
|
})
|
|
250
326
|
.catch((err) => {
|
|
251
|
-
|
|
327
|
+
if ((err as { code: string }).code !== "ABORT_ERR") {
|
|
328
|
+
logger.error(`Failed to start server: ${err}`);
|
|
329
|
+
}
|
|
252
330
|
});
|
|
253
331
|
|
|
254
332
|
return () => {
|
|
255
|
-
|
|
333
|
+
abortController.abort();
|
|
334
|
+
// Running `proxy.server.close()` does not close open connections, preventing the process from exiting.
|
|
335
|
+
// So we use this `terminator` to close all the connections and force the server to shutdown.
|
|
336
|
+
proxy.terminator
|
|
337
|
+
.terminate()
|
|
338
|
+
.catch(() => logger.error("Failed to terminate the proxy server."));
|
|
256
339
|
};
|
|
257
|
-
}, [port, proxy]);
|
|
340
|
+
}, [port, ip, proxy, localProtocol]);
|
|
258
341
|
}
|
|
259
342
|
|
|
260
343
|
function createHandleAssetsRequest(
|
|
@@ -282,20 +365,18 @@ const HTTP1_HEADERS = new Set([
|
|
|
282
365
|
"http2-settings",
|
|
283
366
|
]);
|
|
284
367
|
|
|
285
|
-
function createProxyServer(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
);
|
|
295
|
-
})
|
|
368
|
+
async function createProxyServer(
|
|
369
|
+
localProtocol: "https" | "http"
|
|
370
|
+
): Promise<HttpServer | HttpsServer> {
|
|
371
|
+
const server: HttpServer | HttpsServer =
|
|
372
|
+
localProtocol === "https"
|
|
373
|
+
? createHttpsServer(await getHttpsOptions())
|
|
374
|
+
: createHttpServer();
|
|
375
|
+
|
|
376
|
+
return server
|
|
296
377
|
.on("upgrade", (req) => {
|
|
297
378
|
// log all websocket connections
|
|
298
|
-
|
|
379
|
+
logger.log(
|
|
299
380
|
new Date().toLocaleTimeString(),
|
|
300
381
|
req.method,
|
|
301
382
|
req.url,
|
|
@@ -305,14 +386,15 @@ function createProxyServer() {
|
|
|
305
386
|
})
|
|
306
387
|
.on("error", (err) => {
|
|
307
388
|
// log all connection errors
|
|
308
|
-
|
|
389
|
+
logger.error(new Date().toLocaleTimeString(), err);
|
|
309
390
|
});
|
|
310
391
|
}
|
|
311
392
|
|
|
312
393
|
function createStreamHandler(
|
|
313
394
|
previewToken: CfPreviewToken,
|
|
314
395
|
remote: ClientHttp2Session,
|
|
315
|
-
|
|
396
|
+
localPort: number,
|
|
397
|
+
localProtocol: "https" | "http"
|
|
316
398
|
) {
|
|
317
399
|
return function handleStream(
|
|
318
400
|
stream: ServerHttp2Stream,
|
|
@@ -325,7 +407,8 @@ function createStreamHandler(
|
|
|
325
407
|
rewriteRemoteHostToLocalHostInHeaders(
|
|
326
408
|
responseHeaders,
|
|
327
409
|
previewToken.host,
|
|
328
|
-
|
|
410
|
+
localPort,
|
|
411
|
+
localProtocol
|
|
329
412
|
);
|
|
330
413
|
stream.respond(responseHeaders);
|
|
331
414
|
request.pipe(stream, { end: true });
|
|
@@ -338,33 +421,58 @@ function createStreamHandler(
|
|
|
338
421
|
*/
|
|
339
422
|
export async function waitForPortToBeAvailable(
|
|
340
423
|
port: number,
|
|
341
|
-
options: { retryPeriod: number; timeout: number }
|
|
424
|
+
options: { retryPeriod: number; timeout: number; abortSignal: AbortSignal }
|
|
342
425
|
): Promise<void> {
|
|
343
426
|
return new Promise((resolve, reject) => {
|
|
427
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
428
|
+
options.abortSignal.addEventListener("abort", () => {
|
|
429
|
+
const abortError = new Error("waitForPortToBeAvailable() aborted");
|
|
430
|
+
(abortError as Error & { code: string }).code = "ABORT_ERR";
|
|
431
|
+
doReject(abortError);
|
|
432
|
+
});
|
|
433
|
+
|
|
344
434
|
const timeout = setTimeout(() => {
|
|
345
|
-
|
|
435
|
+
doReject(new Error(`Timed out waiting for port ${port}`));
|
|
346
436
|
}, options.timeout);
|
|
347
437
|
|
|
438
|
+
const interval = setInterval(checkPort, options.retryPeriod);
|
|
439
|
+
checkPort();
|
|
440
|
+
|
|
441
|
+
function doResolve() {
|
|
442
|
+
clearTimeout(timeout);
|
|
443
|
+
clearInterval(interval);
|
|
444
|
+
resolve();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function doReject(err: unknown) {
|
|
448
|
+
clearInterval(interval);
|
|
449
|
+
clearTimeout(timeout);
|
|
450
|
+
reject(err);
|
|
451
|
+
}
|
|
452
|
+
|
|
348
453
|
function checkPort() {
|
|
349
454
|
// Testing whether a port is 'available' involves simply
|
|
350
455
|
// trying to make a server listen on that port, and retrying
|
|
351
456
|
// until it succeeds.
|
|
352
|
-
const server =
|
|
457
|
+
const server = createHttpServer();
|
|
458
|
+
const terminator = createHttpTerminator({
|
|
459
|
+
server,
|
|
460
|
+
gracefulTerminationTimeout: 0, // default 1000
|
|
461
|
+
});
|
|
462
|
+
|
|
353
463
|
server.on("error", (err) => {
|
|
354
464
|
// @ts-expect-error non standard property on Error
|
|
355
|
-
if (err.code
|
|
356
|
-
|
|
357
|
-
} else {
|
|
358
|
-
reject(err);
|
|
465
|
+
if (err.code !== "EADDRINUSE") {
|
|
466
|
+
doReject(err);
|
|
359
467
|
}
|
|
360
468
|
});
|
|
361
|
-
server.listen(port, () =>
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
469
|
+
server.listen(port, () =>
|
|
470
|
+
terminator
|
|
471
|
+
.terminate()
|
|
472
|
+
.then(doResolve, () =>
|
|
473
|
+
logger.error("Failed to terminate the port checker.")
|
|
474
|
+
)
|
|
475
|
+
);
|
|
366
476
|
}
|
|
367
|
-
|
|
368
|
-
checkPort();
|
|
369
477
|
});
|
|
370
478
|
}
|