wrangler 0.0.0-e2a26a0 → 0.0.0-e3a458e
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 +24 -11
- package/pages/functions/filepath-routing.test.ts +212 -36
- package/pages/functions/filepath-routing.ts +69 -119
- package/pages/functions/routes.ts +8 -17
- package/pages/functions/template-worker.ts +3 -9
- package/src/__tests__/configuration.test.ts +2598 -0
- package/src/__tests__/dev.test.tsx +437 -63
- package/src/__tests__/dev2.test.tsx +154 -0
- package/src/__tests__/guess-worker-format.test.ts +58 -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 +5 -5
- package/src/__tests__/helpers/mock-console.ts +21 -7
- package/src/__tests__/helpers/mock-kv.ts +9 -16
- 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 +3 -0
- package/src/__tests__/helpers/run-wrangler.ts +5 -3
- package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
- package/src/__tests__/https-options.test.ts +127 -0
- package/src/__tests__/index.test.ts +468 -20
- package/src/__tests__/jest.setup.ts +37 -5
- package/src/__tests__/kv.test.ts +426 -183
- package/src/__tests__/logout.test.ts +15 -13
- package/src/__tests__/package-manager.test.ts +2 -2
- package/src/__tests__/pages.test.ts +44 -0
- package/src/__tests__/parse.test.ts +322 -0
- package/src/__tests__/publish.test.ts +1733 -344
- package/src/__tests__/r2.test.ts +198 -0
- package/src/__tests__/route.test.ts +45 -0
- package/src/__tests__/secret.test.ts +301 -86
- package/src/__tests__/sentry.test.ts +146 -0
- package/src/__tests__/tail.test.ts +564 -0
- package/src/__tests__/whoami.test.tsx +21 -7
- package/src/bundle.ts +142 -0
- package/src/cfetch/index.ts +8 -6
- package/src/cfetch/internal.ts +60 -22
- package/src/cli.ts +10 -0
- package/src/config/README.md +107 -0
- package/src/config/config.ts +279 -0
- package/src/config/diagnostics.ts +80 -0
- package/src/config/environment.ts +305 -0
- package/src/config/index.ts +52 -0
- package/src/config/validation-helpers.ts +468 -0
- package/src/config/validation.ts +1057 -0
- package/src/{api/preview.ts → create-worker-preview.ts} +59 -21
- package/src/create-worker-upload-form.ts +217 -0
- package/src/dev/dev.tsx +362 -0
- package/src/dev/local.tsx +247 -0
- package/src/dev/remote.tsx +203 -0
- package/src/dev/use-esbuild.ts +100 -0
- package/src/entry.ts +159 -0
- package/src/environment-variables.ts +33 -0
- package/src/https-options.ts +122 -0
- package/src/index.tsx +1030 -766
- package/src/inspect.ts +25 -4
- package/src/{kv.tsx → kv.ts} +110 -55
- package/src/module-collection.ts +212 -22
- package/src/open-in-browser.ts +25 -0
- package/src/package-manager.ts +21 -15
- package/src/pages.tsx +10 -11
- package/src/parse.ts +183 -0
- package/src/proxy.ts +115 -45
- package/src/publish.ts +273 -329
- package/src/r2.ts +50 -0
- package/src/reporting.ts +131 -0
- package/src/selfsigned.d.ts +29 -0
- package/src/sites.tsx +66 -33
- package/src/tail/filters.ts +279 -0
- package/src/tail/index.ts +299 -0
- package/src/tail/printing.ts +62 -0
- package/src/user.tsx +223 -124
- package/src/{api/worker.ts → worker.ts} +44 -36
- package/templates/new-worker.js +1 -1
- package/templates/new-worker.ts +1 -1
- package/templates/tsconfig.json +105 -0
- package/wrangler-dist/cli.js +129900 -113128
- package/wrangler-dist/cli.js.map +3 -3
- package/src/__tests__/helpers/mock-user.ts +0 -27
- package/src/api/form_data.ts +0 -134
- package/src/config.ts +0 -641
- package/src/dev.tsx +0 -834
- package/src/tail.tsx +0 -73
package/src/package-manager.ts
CHANGED
|
@@ -3,49 +3,51 @@ import { join } from "node:path";
|
|
|
3
3
|
import { execa, execaCommandSync } from "execa";
|
|
4
4
|
|
|
5
5
|
export interface PackageManager {
|
|
6
|
+
cwd: string;
|
|
7
|
+
type: "npm" | "yarn";
|
|
6
8
|
addDevDeps(...packages: string[]): Promise<void>;
|
|
7
9
|
install(): Promise<void>;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
export async function getPackageManager(
|
|
12
|
+
export async function getPackageManager(cwd: string): Promise<PackageManager> {
|
|
11
13
|
const [hasYarn, hasNpm] = await Promise.all([supportsYarn(), supportsNpm()]);
|
|
12
|
-
const hasYarnLock = existsSync(join(
|
|
13
|
-
const hasNpmLock = existsSync(join(
|
|
14
|
+
const hasYarnLock = existsSync(join(cwd, "yarn.lock"));
|
|
15
|
+
const hasNpmLock = existsSync(join(cwd, "package-lock.json"));
|
|
14
16
|
|
|
15
17
|
if (hasNpmLock) {
|
|
16
18
|
if (hasNpm) {
|
|
17
19
|
console.log(
|
|
18
20
|
"Using npm as package manager, as there is already a package-lock.json file."
|
|
19
21
|
);
|
|
20
|
-
return NpmPackageManager;
|
|
22
|
+
return { ...NpmPackageManager, cwd };
|
|
21
23
|
} else if (hasYarn) {
|
|
22
24
|
console.log("Using yarn as package manager.");
|
|
23
25
|
console.warn(
|
|
24
26
|
"There is already a package-lock.json file but could not find npm on the PATH."
|
|
25
27
|
);
|
|
26
|
-
return YarnPackageManager;
|
|
28
|
+
return { ...YarnPackageManager, cwd };
|
|
27
29
|
}
|
|
28
30
|
} else if (hasYarnLock) {
|
|
29
31
|
if (hasYarn) {
|
|
30
32
|
console.log(
|
|
31
33
|
"Using yarn as package manager, as there is already a yarn.lock file."
|
|
32
34
|
);
|
|
33
|
-
return YarnPackageManager;
|
|
35
|
+
return { ...YarnPackageManager, cwd };
|
|
34
36
|
} else if (hasNpm) {
|
|
35
37
|
console.log("Using npm as package manager.");
|
|
36
38
|
console.warn(
|
|
37
39
|
"There is already a yarn.lock file but could not find yarn on the PATH."
|
|
38
40
|
);
|
|
39
|
-
return NpmPackageManager;
|
|
41
|
+
return { ...NpmPackageManager, cwd };
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
if (hasNpm) {
|
|
44
46
|
console.log("Using npm as package manager.");
|
|
45
|
-
return NpmPackageManager;
|
|
47
|
+
return { ...NpmPackageManager, cwd };
|
|
46
48
|
} else if (hasYarn) {
|
|
47
49
|
console.log("Using yarn as package manager.");
|
|
48
|
-
return YarnPackageManager;
|
|
50
|
+
return { ...YarnPackageManager, cwd };
|
|
49
51
|
} else {
|
|
50
52
|
throw new Error(
|
|
51
53
|
"Unable to find a package manager. Supported managers are: npm and yarn."
|
|
@@ -56,22 +58,21 @@ export async function getPackageManager(root: string): Promise<PackageManager> {
|
|
|
56
58
|
/**
|
|
57
59
|
* Get the name of the given `packageManager`.
|
|
58
60
|
*/
|
|
59
|
-
export function getPackageManagerName(packageManager:
|
|
60
|
-
return packageManager
|
|
61
|
-
? "npm"
|
|
62
|
-
: packageManager === YarnPackageManager
|
|
63
|
-
? "yarn"
|
|
64
|
-
: "unknown";
|
|
61
|
+
export function getPackageManagerName(packageManager: PackageManager): string {
|
|
62
|
+
return packageManager.type ?? "unknown";
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
/**
|
|
68
66
|
* Manage packages using npm
|
|
69
67
|
*/
|
|
70
68
|
const NpmPackageManager: PackageManager = {
|
|
69
|
+
cwd: process.cwd(),
|
|
70
|
+
type: "npm",
|
|
71
71
|
/** Add and install a new devDependency into the local package.json. */
|
|
72
72
|
async addDevDeps(...packages: string[]): Promise<void> {
|
|
73
73
|
await execa("npm", ["install", ...packages, "--save-dev"], {
|
|
74
74
|
stdio: "inherit",
|
|
75
|
+
cwd: this.cwd,
|
|
75
76
|
});
|
|
76
77
|
},
|
|
77
78
|
|
|
@@ -79,6 +80,7 @@ const NpmPackageManager: PackageManager = {
|
|
|
79
80
|
async install(): Promise<void> {
|
|
80
81
|
await execa("npm", ["install"], {
|
|
81
82
|
stdio: "inherit",
|
|
83
|
+
cwd: this.cwd,
|
|
82
84
|
});
|
|
83
85
|
},
|
|
84
86
|
};
|
|
@@ -87,10 +89,13 @@ const NpmPackageManager: PackageManager = {
|
|
|
87
89
|
* Manage packages using yarn
|
|
88
90
|
*/
|
|
89
91
|
const YarnPackageManager: PackageManager = {
|
|
92
|
+
cwd: process.cwd(),
|
|
93
|
+
type: "yarn",
|
|
90
94
|
/** Add and install a new devDependency into the local package.json. */
|
|
91
95
|
async addDevDeps(...packages: string[]): Promise<void> {
|
|
92
96
|
await execa("yarn", ["add", ...packages, "--dev"], {
|
|
93
97
|
stdio: "inherit",
|
|
98
|
+
cwd: this.cwd,
|
|
94
99
|
});
|
|
95
100
|
},
|
|
96
101
|
|
|
@@ -98,6 +103,7 @@ const YarnPackageManager: PackageManager = {
|
|
|
98
103
|
async install(): Promise<void> {
|
|
99
104
|
await execa("yarn", ["install"], {
|
|
100
105
|
stdio: "inherit",
|
|
106
|
+
cwd: this.cwd,
|
|
101
107
|
});
|
|
102
108
|
},
|
|
103
109
|
};
|
package/src/pages.tsx
CHANGED
|
@@ -7,10 +7,10 @@ import { join } from "node:path";
|
|
|
7
7
|
import { URL } from "node:url";
|
|
8
8
|
import { watch } from "chokidar";
|
|
9
9
|
import { getType } from "mime";
|
|
10
|
-
import open from "open";
|
|
11
10
|
import { buildWorker } from "../pages/functions/buildWorker";
|
|
12
11
|
import { generateConfigFromFileTree } from "../pages/functions/filepath-routing";
|
|
13
12
|
import { writeRoutesModule } from "../pages/functions/routes";
|
|
13
|
+
import openInBrowser from "./open-in-browser";
|
|
14
14
|
import { toUrlPath } from "./paths";
|
|
15
15
|
import type { Config } from "../pages/functions/routes";
|
|
16
16
|
import type { Headers, Request, fetch } from "@miniflare/core";
|
|
@@ -771,7 +771,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
771
771
|
kv: kvs = [],
|
|
772
772
|
do: durableObjects = [],
|
|
773
773
|
"live-reload": liveReload,
|
|
774
|
-
|
|
774
|
+
_: [_pages, _dev, ...remaining],
|
|
775
775
|
}) => {
|
|
776
776
|
if (!local) {
|
|
777
777
|
console.error("Only local mode is supported at the moment.");
|
|
@@ -795,8 +795,8 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
795
795
|
|
|
796
796
|
let miniflareArgs: MiniflareOptions = {};
|
|
797
797
|
|
|
798
|
-
let scriptReadyResolve;
|
|
799
|
-
const scriptReadyPromise = new Promise(
|
|
798
|
+
let scriptReadyResolve: () => void;
|
|
799
|
+
const scriptReadyPromise = new Promise<void>(
|
|
800
800
|
(resolve) => (scriptReadyResolve = resolve)
|
|
801
801
|
);
|
|
802
802
|
|
|
@@ -830,7 +830,8 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
830
830
|
scriptPath,
|
|
831
831
|
};
|
|
832
832
|
} else {
|
|
833
|
-
|
|
833
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
834
|
+
scriptReadyResolve!();
|
|
834
835
|
|
|
835
836
|
const scriptPath =
|
|
836
837
|
directory !== undefined
|
|
@@ -894,13 +895,13 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
894
895
|
// User bindings
|
|
895
896
|
bindings: {
|
|
896
897
|
...Object.fromEntries(
|
|
897
|
-
bindings.map((binding) => binding.toString().split("="))
|
|
898
|
+
bindings.map((binding) => binding.toString().split("=", 1))
|
|
898
899
|
),
|
|
899
900
|
},
|
|
900
901
|
|
|
901
902
|
// env.ASSETS.fetch
|
|
902
903
|
serviceBindings: {
|
|
903
|
-
async ASSETS(request) {
|
|
904
|
+
async ASSETS(request: Request) {
|
|
904
905
|
if (proxyPort) {
|
|
905
906
|
try {
|
|
906
907
|
const url = new URL(request.url);
|
|
@@ -945,9 +946,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
945
946
|
console.log(`Serving at http://localhost:${port}/`);
|
|
946
947
|
|
|
947
948
|
if (process.env.BROWSER !== "none") {
|
|
948
|
-
|
|
949
|
-
// fail silently if the open command doesn't work (e.g. in GitHub Codespaces)
|
|
950
|
-
childProcess.on("error", (_err) => {});
|
|
949
|
+
await openInBrowser(`http://localhost:${port}/`);
|
|
951
950
|
}
|
|
952
951
|
|
|
953
952
|
if (directory !== undefined && liveReload) {
|
|
@@ -964,7 +963,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
964
963
|
miniflare.dispose().catch((err) => miniflare.log.error(err));
|
|
965
964
|
});
|
|
966
965
|
} catch (e) {
|
|
967
|
-
miniflare.log.error(e);
|
|
966
|
+
miniflare.log.error(e as Error);
|
|
968
967
|
EXIT("Could not start Miniflare.", 1);
|
|
969
968
|
}
|
|
970
969
|
}
|
package/src/parse.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
66
|
+
|
|
67
|
+
const TOML_ERROR_NAME = "TomlError";
|
|
68
|
+
const TOML_ERROR_SUFFIX = " at row ";
|
|
69
|
+
|
|
70
|
+
type TomlError = Error & {
|
|
71
|
+
line: number;
|
|
72
|
+
col: number;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A wrapper around `TOML.parse` that throws a `ParseError`.
|
|
77
|
+
*/
|
|
78
|
+
export function parseTOML(input: string, file?: string): object {
|
|
79
|
+
try {
|
|
80
|
+
return TOML.parse(input);
|
|
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 wrapper around `JSON.parse` that throws a `ParseError`.
|
|
103
|
+
*/
|
|
104
|
+
export function parseJSON(input: string, file?: string): any {
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(input);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
const { message } = err as Error;
|
|
109
|
+
const index = message.lastIndexOf(JSON_ERROR_SUFFIX);
|
|
110
|
+
if (index < 0) {
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
const text = message.substring(0, index);
|
|
114
|
+
const position = parseInt(
|
|
115
|
+
message.substring(index + JSON_ERROR_SUFFIX.length)
|
|
116
|
+
);
|
|
117
|
+
const location = indexLocation({ file, fileText: input }, position);
|
|
118
|
+
throw new ParseError({ text, location });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Reads a file and parses it based on its type.
|
|
124
|
+
*/
|
|
125
|
+
export function readFileSync(file: string): string {
|
|
126
|
+
try {
|
|
127
|
+
return fs.readFileSync(file, { encoding: "utf-8" });
|
|
128
|
+
} catch (err) {
|
|
129
|
+
const { message } = err as Error;
|
|
130
|
+
throw new ParseError({
|
|
131
|
+
text: `Could not read file: ${file}`,
|
|
132
|
+
notes: [
|
|
133
|
+
{
|
|
134
|
+
text: message.replace(file, resolve(file)),
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Calculates the line and column location from an index.
|
|
143
|
+
*/
|
|
144
|
+
export function indexLocation(file: File, index: number): Location {
|
|
145
|
+
let lineText,
|
|
146
|
+
line = 0,
|
|
147
|
+
column = 0,
|
|
148
|
+
cursor = 0;
|
|
149
|
+
const { fileText = "" } = file;
|
|
150
|
+
for (const row of fileText.split("\n")) {
|
|
151
|
+
line++;
|
|
152
|
+
cursor += row.length + 1;
|
|
153
|
+
if (cursor >= index) {
|
|
154
|
+
lineText = row;
|
|
155
|
+
column = row.length - (cursor - index);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { lineText, line, column, ...file };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Guesses the line and column location of a search query.
|
|
164
|
+
*/
|
|
165
|
+
export function searchLocation(file: File, query: unknown): Location {
|
|
166
|
+
let lineText,
|
|
167
|
+
length,
|
|
168
|
+
line = 0,
|
|
169
|
+
column = 0;
|
|
170
|
+
const queryText = String(query);
|
|
171
|
+
const { fileText = "" } = file;
|
|
172
|
+
for (const content of fileText.split("\n")) {
|
|
173
|
+
line++;
|
|
174
|
+
const index = content.indexOf(queryText);
|
|
175
|
+
if (index >= 0) {
|
|
176
|
+
lineText = content;
|
|
177
|
+
column = index;
|
|
178
|
+
length = queryText.length;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { lineText, line, column, length, ...file };
|
|
183
|
+
}
|
package/src/proxy.ts
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
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";
|
|
4
5
|
import { useEffect, useRef, useState } from "react";
|
|
5
6
|
import serveStatic from "serve-static";
|
|
6
|
-
import
|
|
7
|
+
import { getHttpsOptions } from "./https-options";
|
|
8
|
+
import { reportError } from "./reporting";
|
|
9
|
+
import type { CfPreviewToken } from "./create-worker-preview";
|
|
7
10
|
import type {
|
|
8
11
|
IncomingHttpHeaders,
|
|
9
12
|
RequestListener,
|
|
10
13
|
IncomingMessage,
|
|
11
14
|
ServerResponse,
|
|
12
|
-
Server,
|
|
15
|
+
Server as HttpServer,
|
|
13
16
|
} from "node:http";
|
|
14
17
|
import type { ClientHttp2Session, ServerHttp2Stream } from "node:http2";
|
|
18
|
+
import type { Server as HttpsServer } from "node:https";
|
|
19
|
+
import type ws from "ws";
|
|
20
|
+
|
|
21
|
+
interface IWebsocket extends ws {
|
|
22
|
+
// Pipe implements .on("message", ...)
|
|
23
|
+
pipe<T>(fn: T): IWebsocket;
|
|
24
|
+
}
|
|
15
25
|
|
|
16
26
|
/**
|
|
17
27
|
* `usePreviewServer` is a React hook that creates a local development
|
|
@@ -41,13 +51,17 @@ function addCfPreviewTokenHeader(
|
|
|
41
51
|
function rewriteRemoteHostToLocalHostInHeaders(
|
|
42
52
|
headers: IncomingHttpHeaders,
|
|
43
53
|
remoteHost: string,
|
|
44
|
-
localPort: number
|
|
54
|
+
localPort: number,
|
|
55
|
+
localProtocol: "https" | "http"
|
|
45
56
|
) {
|
|
46
57
|
for (const [name, value] of Object.entries(headers)) {
|
|
47
58
|
// Rewrite the remote host to the local host.
|
|
48
59
|
if (typeof value === "string" && value.includes(remoteHost)) {
|
|
49
60
|
headers[name] = value
|
|
50
|
-
.replaceAll(
|
|
61
|
+
.replaceAll(
|
|
62
|
+
`https://${remoteHost}`,
|
|
63
|
+
`${localProtocol}://localhost:${localPort}`
|
|
64
|
+
)
|
|
51
65
|
.replaceAll(remoteHost, `localhost:${localPort}`);
|
|
52
66
|
}
|
|
53
67
|
}
|
|
@@ -56,15 +70,33 @@ function rewriteRemoteHostToLocalHostInHeaders(
|
|
|
56
70
|
export function usePreviewServer({
|
|
57
71
|
previewToken,
|
|
58
72
|
publicRoot,
|
|
59
|
-
|
|
73
|
+
localProtocol,
|
|
74
|
+
localPort: port,
|
|
75
|
+
ip,
|
|
60
76
|
}: {
|
|
61
77
|
previewToken: CfPreviewToken | undefined;
|
|
62
78
|
publicRoot: undefined | string;
|
|
63
|
-
|
|
79
|
+
localProtocol: "https" | "http";
|
|
80
|
+
localPort: number;
|
|
81
|
+
ip: string;
|
|
64
82
|
}) {
|
|
65
83
|
/** Creates an HTTP/1 proxy that sends requests over HTTP/2. */
|
|
66
|
-
const proxyServer =
|
|
67
|
-
|
|
84
|
+
const [proxyServer, setProxyServer] = useState<HttpServer | HttpsServer>();
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create the instance of the local proxy server that will pass on
|
|
88
|
+
* requests to the preview worker.
|
|
89
|
+
*/
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (proxyServer === undefined) {
|
|
92
|
+
createProxyServer(localProtocol)
|
|
93
|
+
.then((proxy) => setProxyServer(proxy))
|
|
94
|
+
.catch(async (err) => {
|
|
95
|
+
console.error("Failed to create proxy server.", err);
|
|
96
|
+
await reportError(err);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}, [proxyServer, localProtocol]);
|
|
68
100
|
|
|
69
101
|
/**
|
|
70
102
|
* When we're not connected / getting a fresh token on changes,
|
|
@@ -81,7 +113,7 @@ export function usePreviewServer({
|
|
|
81
113
|
>([]);
|
|
82
114
|
|
|
83
115
|
/**
|
|
84
|
-
* The session doesn't last forever, and will
|
|
116
|
+
* The session doesn't last forever, and will eventually drop
|
|
85
117
|
* (usually within 5-15 minutes). When that happens, we simply
|
|
86
118
|
* restart the effect, effectively restarting the server. We use
|
|
87
119
|
* a state sigil as an effect dependency to do so.
|
|
@@ -92,6 +124,10 @@ export function usePreviewServer({
|
|
|
92
124
|
}
|
|
93
125
|
|
|
94
126
|
useEffect(() => {
|
|
127
|
+
if (proxyServer === undefined) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
95
131
|
// If we don't have a token, that means either we're just starting up,
|
|
96
132
|
// or we're refreshing the token.
|
|
97
133
|
if (!previewToken) {
|
|
@@ -103,8 +139,8 @@ export function usePreviewServer({
|
|
|
103
139
|
// store the stream in a buffer so we can replay it later
|
|
104
140
|
streamBufferRef.current.push({ stream, headers });
|
|
105
141
|
};
|
|
106
|
-
|
|
107
|
-
cleanupListeners.push(() =>
|
|
142
|
+
proxyServer.on("stream", bufferStream);
|
|
143
|
+
cleanupListeners.push(() => proxyServer.off("stream", bufferStream));
|
|
108
144
|
|
|
109
145
|
const bufferRequestResponse = (
|
|
110
146
|
request: IncomingMessage,
|
|
@@ -114,8 +150,10 @@ export function usePreviewServer({
|
|
|
114
150
|
requestResponseBufferRef.current.push({ request, response });
|
|
115
151
|
};
|
|
116
152
|
|
|
117
|
-
|
|
118
|
-
cleanupListeners.push(() =>
|
|
153
|
+
proxyServer.on("request", bufferRequestResponse);
|
|
154
|
+
cleanupListeners.push(() =>
|
|
155
|
+
proxyServer.off("request", bufferRequestResponse)
|
|
156
|
+
);
|
|
119
157
|
return () => {
|
|
120
158
|
cleanupListeners.forEach((cleanup) => cleanup());
|
|
121
159
|
};
|
|
@@ -136,9 +174,14 @@ export function usePreviewServer({
|
|
|
136
174
|
cleanupListeners.push(() => remote.off("close", retryServerSetup));
|
|
137
175
|
|
|
138
176
|
/** HTTP/2 -> HTTP/2 */
|
|
139
|
-
const handleStream = createStreamHandler(
|
|
140
|
-
|
|
141
|
-
|
|
177
|
+
const handleStream = createStreamHandler(
|
|
178
|
+
previewToken,
|
|
179
|
+
remote,
|
|
180
|
+
port,
|
|
181
|
+
localProtocol
|
|
182
|
+
);
|
|
183
|
+
proxyServer.on("stream", handleStream);
|
|
184
|
+
cleanupListeners.push(() => proxyServer.off("stream", handleStream));
|
|
142
185
|
|
|
143
186
|
// flush and replay buffered streams
|
|
144
187
|
streamBufferRef.current.forEach((buffer) =>
|
|
@@ -171,7 +214,8 @@ export function usePreviewServer({
|
|
|
171
214
|
rewriteRemoteHostToLocalHostInHeaders(
|
|
172
215
|
responseHeaders,
|
|
173
216
|
previewToken.host,
|
|
174
|
-
port
|
|
217
|
+
port,
|
|
218
|
+
localProtocol
|
|
175
219
|
);
|
|
176
220
|
for (const name of Object.keys(responseHeaders)) {
|
|
177
221
|
if (name.startsWith(":")) {
|
|
@@ -189,8 +233,10 @@ export function usePreviewServer({
|
|
|
189
233
|
? createHandleAssetsRequest(assetPath, handleRequest)
|
|
190
234
|
: handleRequest;
|
|
191
235
|
|
|
192
|
-
|
|
193
|
-
cleanupListeners.push(() =>
|
|
236
|
+
proxyServer.on("request", actualHandleRequest);
|
|
237
|
+
cleanupListeners.push(() =>
|
|
238
|
+
proxyServer.off("request", actualHandleRequest)
|
|
239
|
+
);
|
|
194
240
|
|
|
195
241
|
// flush and replay buffered requests
|
|
196
242
|
requestResponseBufferRef.current.forEach(({ request, response }) =>
|
|
@@ -207,22 +253,22 @@ export function usePreviewServer({
|
|
|
207
253
|
const { headers, url } = message;
|
|
208
254
|
addCfPreviewTokenHeader(headers, previewToken.value);
|
|
209
255
|
headers["host"] = previewToken.host;
|
|
210
|
-
const localWebsocket = new WebSocket(message, socket, body);
|
|
256
|
+
const localWebsocket = new WebSocket(message, socket, body) as IWebsocket;
|
|
211
257
|
// TODO(soon): Custom WebSocket protocol is not working?
|
|
212
258
|
const remoteWebsocketClient = new WebSocket.Client(
|
|
213
259
|
`wss://${previewToken.host}${url}`,
|
|
214
260
|
[],
|
|
215
261
|
{ headers }
|
|
216
|
-
);
|
|
262
|
+
) as IWebsocket;
|
|
217
263
|
localWebsocket.pipe(remoteWebsocketClient).pipe(localWebsocket);
|
|
218
264
|
// We close down websockets whenever we refresh the token.
|
|
219
265
|
cleanupListeners.push(() => {
|
|
220
|
-
localWebsocket.
|
|
221
|
-
remoteWebsocketClient.
|
|
266
|
+
localWebsocket.close();
|
|
267
|
+
remoteWebsocketClient.close();
|
|
222
268
|
});
|
|
223
269
|
};
|
|
224
|
-
|
|
225
|
-
cleanupListeners.push(() =>
|
|
270
|
+
proxyServer.on("upgrade", handleUpgrade);
|
|
271
|
+
cleanupListeners.push(() => proxyServer.off("upgrade", handleUpgrade));
|
|
226
272
|
|
|
227
273
|
return () => {
|
|
228
274
|
cleanupListeners.forEach((cleanup) => cleanup());
|
|
@@ -231,7 +277,8 @@ export function usePreviewServer({
|
|
|
231
277
|
previewToken,
|
|
232
278
|
publicRoot,
|
|
233
279
|
port,
|
|
234
|
-
|
|
280
|
+
localProtocol,
|
|
281
|
+
proxyServer,
|
|
235
282
|
// We use a state value as a sigil to trigger reconnecting the server.
|
|
236
283
|
// It's not used inside the effect, so react-hooks/exhaustive-deps
|
|
237
284
|
// doesn't complain if it's not included in the dependency array.
|
|
@@ -242,19 +289,23 @@ export function usePreviewServer({
|
|
|
242
289
|
// Start/stop the server whenever the
|
|
243
290
|
// containing component is mounted/unmounted.
|
|
244
291
|
useEffect(() => {
|
|
292
|
+
if (proxyServer === undefined) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
245
296
|
waitForPortToBeAvailable(port, { retryPeriod: 200, timeout: 2000 })
|
|
246
297
|
.then(() => {
|
|
247
|
-
|
|
248
|
-
console.log(`⬣ Listening at
|
|
298
|
+
proxyServer.listen(port, ip);
|
|
299
|
+
console.log(`⬣ Listening at ${localProtocol}://${ip}:${port}`);
|
|
249
300
|
})
|
|
250
301
|
.catch((err) => {
|
|
251
302
|
console.error(`⬣ Failed to start server: ${err}`);
|
|
252
303
|
});
|
|
253
304
|
|
|
254
305
|
return () => {
|
|
255
|
-
|
|
306
|
+
proxyServer.close();
|
|
256
307
|
};
|
|
257
|
-
}, [port,
|
|
308
|
+
}, [port, ip, proxyServer, localProtocol]);
|
|
258
309
|
}
|
|
259
310
|
|
|
260
311
|
function createHandleAssetsRequest(
|
|
@@ -282,8 +333,15 @@ const HTTP1_HEADERS = new Set([
|
|
|
282
333
|
"http2-settings",
|
|
283
334
|
]);
|
|
284
335
|
|
|
285
|
-
function createProxyServer(
|
|
286
|
-
|
|
336
|
+
async function createProxyServer(
|
|
337
|
+
localProtocol: "https" | "http"
|
|
338
|
+
): Promise<HttpServer | HttpsServer> {
|
|
339
|
+
const server: HttpServer | HttpsServer =
|
|
340
|
+
localProtocol === "https"
|
|
341
|
+
? createHttpsServer(await getHttpsOptions())
|
|
342
|
+
: createHttpServer();
|
|
343
|
+
|
|
344
|
+
return server
|
|
287
345
|
.on("request", function (req, res) {
|
|
288
346
|
// log all requests
|
|
289
347
|
console.log(
|
|
@@ -312,7 +370,8 @@ function createProxyServer() {
|
|
|
312
370
|
function createStreamHandler(
|
|
313
371
|
previewToken: CfPreviewToken,
|
|
314
372
|
remote: ClientHttp2Session,
|
|
315
|
-
|
|
373
|
+
localPort: number,
|
|
374
|
+
localProtocol: "https" | "http"
|
|
316
375
|
) {
|
|
317
376
|
return function handleStream(
|
|
318
377
|
stream: ServerHttp2Stream,
|
|
@@ -325,7 +384,8 @@ function createStreamHandler(
|
|
|
325
384
|
rewriteRemoteHostToLocalHostInHeaders(
|
|
326
385
|
responseHeaders,
|
|
327
386
|
previewToken.host,
|
|
328
|
-
|
|
387
|
+
localPort,
|
|
388
|
+
localProtocol
|
|
329
389
|
);
|
|
330
390
|
stream.respond(responseHeaders);
|
|
331
391
|
request.pipe(stream, { end: true });
|
|
@@ -342,29 +402,39 @@ export async function waitForPortToBeAvailable(
|
|
|
342
402
|
): Promise<void> {
|
|
343
403
|
return new Promise((resolve, reject) => {
|
|
344
404
|
const timeout = setTimeout(() => {
|
|
345
|
-
|
|
405
|
+
doReject(new Error(`Timed out waiting for port ${port}`));
|
|
346
406
|
}, options.timeout);
|
|
347
407
|
|
|
408
|
+
const interval = setInterval(checkPort, options.retryPeriod);
|
|
409
|
+
checkPort();
|
|
410
|
+
|
|
411
|
+
function doResolve() {
|
|
412
|
+
clearTimeout(timeout);
|
|
413
|
+
clearInterval(interval);
|
|
414
|
+
resolve();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function doReject(err: unknown) {
|
|
418
|
+
clearInterval(interval);
|
|
419
|
+
clearTimeout(timeout);
|
|
420
|
+
reject(err);
|
|
421
|
+
}
|
|
422
|
+
|
|
348
423
|
function checkPort() {
|
|
349
424
|
// Testing whether a port is 'available' involves simply
|
|
350
425
|
// trying to make a server listen on that port, and retrying
|
|
351
426
|
// until it succeeds.
|
|
352
|
-
const server =
|
|
427
|
+
const server = createHttpServer();
|
|
353
428
|
server.on("error", (err) => {
|
|
354
429
|
// @ts-expect-error non standard property on Error
|
|
355
|
-
if (err.code
|
|
356
|
-
|
|
357
|
-
} else {
|
|
358
|
-
reject(err);
|
|
430
|
+
if (err.code !== "EADDRINUSE") {
|
|
431
|
+
doReject(err);
|
|
359
432
|
}
|
|
360
433
|
});
|
|
361
434
|
server.listen(port, () => {
|
|
362
435
|
server.close();
|
|
363
|
-
|
|
364
|
-
resolve();
|
|
436
|
+
doResolve();
|
|
365
437
|
});
|
|
366
438
|
}
|
|
367
|
-
|
|
368
|
-
checkPort();
|
|
369
439
|
});
|
|
370
440
|
}
|