wrangler 0.0.13 → 0.0.17
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/bin/wrangler.js +2 -2
- package/package.json +20 -11
- package/pages/functions/buildWorker.ts +1 -1
- package/pages/functions/filepath-routing.test.ts +112 -28
- package/pages/functions/filepath-routing.ts +44 -51
- package/pages/functions/routes.ts +11 -18
- package/pages/functions/template-worker.ts +3 -9
- package/src/__tests__/dev.test.tsx +42 -5
- package/src/__tests__/guess-worker-format.test.ts +66 -0
- package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
- package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
- package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
- package/src/__tests__/helpers/mock-account-id.ts +30 -0
- package/src/__tests__/helpers/mock-bin.ts +36 -0
- package/src/__tests__/{mock-cfetch.ts → helpers/mock-cfetch.ts} +43 -9
- package/src/__tests__/helpers/mock-console.ts +62 -0
- package/src/__tests__/{mock-dialogs.ts → helpers/mock-dialogs.ts} +1 -1
- package/src/__tests__/helpers/mock-kv.ts +40 -0
- package/src/__tests__/helpers/mock-user.ts +27 -0
- package/src/__tests__/helpers/mock-web-socket.ts +37 -0
- package/src/__tests__/{run-in-tmp.ts → helpers/run-in-tmp.ts} +1 -1
- package/src/__tests__/helpers/run-wrangler.ts +16 -0
- package/src/__tests__/helpers/write-wrangler-toml.ts +20 -0
- package/src/__tests__/index.test.ts +418 -71
- package/src/__tests__/jest.setup.ts +30 -2
- package/src/__tests__/kv.test.ts +147 -252
- package/src/__tests__/logout.test.ts +50 -0
- package/src/__tests__/package-manager.test.ts +206 -0
- package/src/__tests__/publish.test.ts +1136 -291
- package/src/__tests__/r2.test.ts +206 -0
- package/src/__tests__/secret.test.ts +210 -0
- package/src/__tests__/sentry.test.ts +146 -0
- package/src/__tests__/tail.test.ts +246 -0
- package/src/__tests__/whoami.test.tsx +6 -47
- package/src/api/form_data.ts +75 -25
- package/src/api/preview.ts +2 -2
- package/src/api/worker.ts +34 -15
- package/src/bundle.ts +127 -0
- package/src/cfetch/index.ts +7 -15
- package/src/cfetch/internal.ts +41 -6
- package/src/cli.ts +10 -0
- package/src/config.ts +125 -95
- package/src/dev.tsx +300 -193
- package/src/dialogs.tsx +2 -2
- package/src/guess-worker-format.ts +68 -0
- package/src/index.tsx +578 -192
- package/src/inspect.ts +29 -10
- package/src/kv.tsx +23 -17
- package/src/module-collection.ts +32 -12
- package/src/open-in-browser.ts +13 -0
- package/src/package-manager.ts +120 -0
- package/src/pages.tsx +28 -23
- package/src/paths.ts +26 -0
- package/src/proxy.ts +88 -14
- package/src/publish.ts +260 -297
- package/src/r2.ts +50 -0
- package/src/reporting.ts +115 -0
- package/src/sites.tsx +28 -27
- package/src/tail.tsx +178 -9
- package/src/user.tsx +58 -44
- package/templates/new-worker.js +15 -0
- package/templates/new-worker.ts +15 -0
- package/{static-asset-facade.js → templates/static-asset-facade.js} +0 -0
- package/wrangler-dist/cli.js +124315 -104677
- package/wrangler-dist/cli.js.map +3 -3
- package/src/__tests__/mock-console.ts +0 -34
- package/src/__tests__/run-wrangler.ts +0 -8
package/src/inspect.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import assert from "assert";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
import { createServer } from "http";
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
|
|
6
5
|
import { useEffect, useRef, useState } from "react";
|
|
6
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
7
7
|
import { version } from "../package.json";
|
|
8
|
-
|
|
9
8
|
import type Protocol from "devtools-protocol";
|
|
9
|
+
import type { IncomingMessage, Server, ServerResponse } from "node:http";
|
|
10
|
+
import type { MessageEvent } from "ws";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* `useInspector` is a hook for debugging Workers applications
|
|
@@ -315,9 +316,14 @@ export default function useInspector(props: InspectorProps) {
|
|
|
315
316
|
};
|
|
316
317
|
}, [
|
|
317
318
|
props.inspectorUrl,
|
|
318
|
-
retryRemoteWebSocketConnectionSigil,
|
|
319
319
|
props.logToTerminal,
|
|
320
320
|
wsServer,
|
|
321
|
+
// We use a state value as a sigil to trigger a retry of the
|
|
322
|
+
// remote websocket connection. It's not used inside the effect,
|
|
323
|
+
// so react-hooks/exhaustive-deps doesn't complain if it's not
|
|
324
|
+
// included in the dependency array. But its presence is critical,
|
|
325
|
+
// so do NOT remove it from the dependency list.
|
|
326
|
+
retryRemoteWebSocketConnectionSigil,
|
|
321
327
|
]);
|
|
322
328
|
|
|
323
329
|
/**
|
|
@@ -361,7 +367,10 @@ export default function useInspector(props: InspectorProps) {
|
|
|
361
367
|
);
|
|
362
368
|
remoteWebSocket.send(event.data);
|
|
363
369
|
} catch (e) {
|
|
364
|
-
if (
|
|
370
|
+
if (
|
|
371
|
+
(e as Error).message !==
|
|
372
|
+
"WebSocket is not open: readyState 0 (CONNECTING)"
|
|
373
|
+
) {
|
|
365
374
|
/**
|
|
366
375
|
* ^ this just means we haven't opened a websocket yet
|
|
367
376
|
* usually happens until there's at least one request
|
|
@@ -581,8 +590,18 @@ function logConsoleMessage(evt: Protocol.Runtime.ConsoleAPICalledEvent): void {
|
|
|
581
590
|
const method = mapConsoleAPIMessageTypeToConsoleMethod[evt.type];
|
|
582
591
|
|
|
583
592
|
if (method in console) {
|
|
584
|
-
|
|
585
|
-
|
|
593
|
+
switch (method) {
|
|
594
|
+
case "dir":
|
|
595
|
+
console.dir(args);
|
|
596
|
+
break;
|
|
597
|
+
case "table":
|
|
598
|
+
console.table(args);
|
|
599
|
+
break;
|
|
600
|
+
default:
|
|
601
|
+
// eslint-disable-next-line prefer-spread
|
|
602
|
+
console[method].apply(console, args);
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
586
605
|
} else {
|
|
587
606
|
console.warn(`Unsupported console method: ${method}`);
|
|
588
607
|
console.log("console event:", evt);
|
package/src/kv.tsx
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { URLSearchParams } from "node:url";
|
|
2
|
+
import { fetchListResult, fetchResult, fetchKVGetValue } from "./cfetch";
|
|
2
3
|
import type { Config } from "./config";
|
|
3
|
-
import { fetchListResult, fetchResult } from "./cfetch";
|
|
4
4
|
|
|
5
5
|
type KvArgs = {
|
|
6
6
|
binding?: string;
|
|
7
7
|
"namespace-id"?: string;
|
|
8
8
|
env?: string;
|
|
9
9
|
preview?: boolean;
|
|
10
|
-
config?: Config;
|
|
11
10
|
};
|
|
12
11
|
|
|
13
12
|
/**
|
|
@@ -115,6 +114,14 @@ export async function putKeyValue(
|
|
|
115
114
|
);
|
|
116
115
|
}
|
|
117
116
|
|
|
117
|
+
export async function getKeyValue(
|
|
118
|
+
accountId: string,
|
|
119
|
+
namespaceId: string,
|
|
120
|
+
key: string
|
|
121
|
+
): Promise<string> {
|
|
122
|
+
return await fetchKVGetValue(accountId, namespaceId, key);
|
|
123
|
+
}
|
|
124
|
+
|
|
118
125
|
export async function putBulkKeyValue(
|
|
119
126
|
accountId: string,
|
|
120
127
|
namespaceId: string,
|
|
@@ -145,13 +152,10 @@ export async function deleteBulkKeyValue(
|
|
|
145
152
|
);
|
|
146
153
|
}
|
|
147
154
|
|
|
148
|
-
export function getNamespaceId(
|
|
149
|
-
preview,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
"namespace-id": namespaceId,
|
|
153
|
-
env,
|
|
154
|
-
}: KvArgs): string {
|
|
155
|
+
export function getNamespaceId(
|
|
156
|
+
{ preview, binding, "namespace-id": namespaceId, env }: KvArgs,
|
|
157
|
+
config: Config
|
|
158
|
+
): string {
|
|
155
159
|
// nice
|
|
156
160
|
if (namespaceId) {
|
|
157
161
|
return namespaceId;
|
|
@@ -181,19 +185,21 @@ export function getNamespaceId({
|
|
|
181
185
|
}
|
|
182
186
|
|
|
183
187
|
// TODO: either a bespoke arg type for this function to avoid `undefined`s or an `EnvOrConfig` type
|
|
184
|
-
return getNamespaceId(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
return getNamespaceId(
|
|
189
|
+
{
|
|
190
|
+
binding,
|
|
191
|
+
"namespace-id": namespaceId,
|
|
192
|
+
env: undefined,
|
|
193
|
+
preview,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
190
196
|
env: undefined,
|
|
191
197
|
build: undefined,
|
|
192
198
|
name: undefined,
|
|
193
199
|
account_id: undefined,
|
|
194
200
|
...config.env[env],
|
|
195
|
-
}
|
|
196
|
-
|
|
201
|
+
}
|
|
202
|
+
);
|
|
197
203
|
}
|
|
198
204
|
|
|
199
205
|
// there's no KV namespaces
|
package/src/module-collection.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { CfModule } from "./api/worker";
|
|
2
|
-
import type esbuild from "esbuild";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { readFile } from "node:fs/promises";
|
|
5
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import type { CfModule, CfScriptFormat } from "./api/worker";
|
|
5
|
+
import type esbuild from "esbuild";
|
|
6
6
|
|
|
7
7
|
// This is a combination of an esbuild plugin and a mutable array
|
|
8
8
|
// that we use to collect module references from source code.
|
|
@@ -12,7 +12,9 @@ import crypto from "node:crypto";
|
|
|
12
12
|
// plugin+array is used to collect references to these modules, reference
|
|
13
13
|
// them correctly in the bundle, and add them to the form upload.
|
|
14
14
|
|
|
15
|
-
export default function makeModuleCollector(
|
|
15
|
+
export default function makeModuleCollector(props: {
|
|
16
|
+
format: CfScriptFormat;
|
|
17
|
+
}): {
|
|
16
18
|
modules: CfModule[];
|
|
17
19
|
plugin: esbuild.Plugin;
|
|
18
20
|
} {
|
|
@@ -23,18 +25,17 @@ export default function makeModuleCollector(): {
|
|
|
23
25
|
name: "wrangler-module-collector",
|
|
24
26
|
setup(build) {
|
|
25
27
|
build.onStart(() => {
|
|
26
|
-
// reset the
|
|
28
|
+
// reset the module collection array
|
|
27
29
|
modules.splice(0);
|
|
28
30
|
});
|
|
29
31
|
|
|
30
32
|
build.onResolve(
|
|
31
33
|
// filter on "known" file types,
|
|
32
34
|
// we can expand this list later
|
|
33
|
-
{ filter: /.*\.(
|
|
35
|
+
{ filter: /.*\.(wasm)$/ },
|
|
34
36
|
async (args: esbuild.OnResolveArgs) => {
|
|
35
37
|
// take the file and massage it to a
|
|
36
38
|
// transportable/manageable format
|
|
37
|
-
const fileExt = path.extname(args.path);
|
|
38
39
|
const filePath = path.join(args.resolveDir, args.path);
|
|
39
40
|
const fileContent = await readFile(filePath);
|
|
40
41
|
const fileHash = crypto
|
|
@@ -45,19 +46,38 @@ export default function makeModuleCollector(): {
|
|
|
45
46
|
|
|
46
47
|
// add the module to the array
|
|
47
48
|
modules.push({
|
|
48
|
-
name: fileName,
|
|
49
|
+
name: "./" + fileName,
|
|
49
50
|
content: fileContent,
|
|
50
|
-
type:
|
|
51
|
+
type: "compiled-wasm",
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
return {
|
|
54
|
-
path: fileName, // change the reference to the changed module
|
|
55
|
-
external:
|
|
55
|
+
path: "./" + fileName, // change the reference to the changed module
|
|
56
|
+
external: props.format === "modules", // mark it as external in the bundle
|
|
56
57
|
namespace: "wrangler-module-collector-ns", // just a tag, this isn't strictly necessary
|
|
57
58
|
watchFiles: [filePath], // we also add the file to esbuild's watch list
|
|
58
59
|
};
|
|
59
60
|
}
|
|
60
61
|
);
|
|
62
|
+
|
|
63
|
+
if (props.format !== "modules") {
|
|
64
|
+
build.onLoad(
|
|
65
|
+
{ filter: /.*\.(wasm)$/ },
|
|
66
|
+
async (args: esbuild.OnLoadArgs) => {
|
|
67
|
+
return {
|
|
68
|
+
// We replace the the wasm module with an identifier
|
|
69
|
+
// that we'll separately add to the form upload
|
|
70
|
+
// as part of [wasm_modules]. This identifier has to be a valid
|
|
71
|
+
// JS identifier, so we replace all non alphanumeric characters
|
|
72
|
+
// with an underscore.
|
|
73
|
+
contents: `export default ${args.path.replace(
|
|
74
|
+
/[^a-zA-Z0-9_$]/g,
|
|
75
|
+
"_"
|
|
76
|
+
)};`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
61
81
|
},
|
|
62
82
|
},
|
|
63
83
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import open from "open";
|
|
2
|
+
/**
|
|
3
|
+
* An extremely simple wrapper around the open command.
|
|
4
|
+
* Specifically, it adds an 'error' event handler so that when this function
|
|
5
|
+
* is called in environments where we can't open the browser (e.g. github codespaces,
|
|
6
|
+
* stackblitz, remote servers), it doesn't just crash the process.
|
|
7
|
+
*/
|
|
8
|
+
export default async function openInBrowser(url: string): Promise<void> {
|
|
9
|
+
const childProcess = await open(url);
|
|
10
|
+
childProcess.on("error", () => {
|
|
11
|
+
console.warn(`Failed to open ${url} in a browser`);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { execa, execaCommandSync } from "execa";
|
|
4
|
+
|
|
5
|
+
export interface PackageManager {
|
|
6
|
+
addDevDeps(...packages: string[]): Promise<void>;
|
|
7
|
+
install(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function getPackageManager(root: string): Promise<PackageManager> {
|
|
11
|
+
const [hasYarn, hasNpm] = await Promise.all([supportsYarn(), supportsNpm()]);
|
|
12
|
+
const hasYarnLock = existsSync(join(root, "yarn.lock"));
|
|
13
|
+
const hasNpmLock = existsSync(join(root, "package-lock.json"));
|
|
14
|
+
|
|
15
|
+
if (hasNpmLock) {
|
|
16
|
+
if (hasNpm) {
|
|
17
|
+
console.log(
|
|
18
|
+
"Using npm as package manager, as there is already a package-lock.json file."
|
|
19
|
+
);
|
|
20
|
+
return NpmPackageManager;
|
|
21
|
+
} else if (hasYarn) {
|
|
22
|
+
console.log("Using yarn as package manager.");
|
|
23
|
+
console.warn(
|
|
24
|
+
"There is already a package-lock.json file but could not find npm on the PATH."
|
|
25
|
+
);
|
|
26
|
+
return YarnPackageManager;
|
|
27
|
+
}
|
|
28
|
+
} else if (hasYarnLock) {
|
|
29
|
+
if (hasYarn) {
|
|
30
|
+
console.log(
|
|
31
|
+
"Using yarn as package manager, as there is already a yarn.lock file."
|
|
32
|
+
);
|
|
33
|
+
return YarnPackageManager;
|
|
34
|
+
} else if (hasNpm) {
|
|
35
|
+
console.log("Using npm as package manager.");
|
|
36
|
+
console.warn(
|
|
37
|
+
"There is already a yarn.lock file but could not find yarn on the PATH."
|
|
38
|
+
);
|
|
39
|
+
return NpmPackageManager;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (hasNpm) {
|
|
44
|
+
console.log("Using npm as package manager.");
|
|
45
|
+
return NpmPackageManager;
|
|
46
|
+
} else if (hasYarn) {
|
|
47
|
+
console.log("Using yarn as package manager.");
|
|
48
|
+
return YarnPackageManager;
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error(
|
|
51
|
+
"Unable to find a package manager. Supported managers are: npm and yarn."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get the name of the given `packageManager`.
|
|
58
|
+
*/
|
|
59
|
+
export function getPackageManagerName(packageManager: unknown): string {
|
|
60
|
+
return packageManager === NpmPackageManager
|
|
61
|
+
? "npm"
|
|
62
|
+
: packageManager === YarnPackageManager
|
|
63
|
+
? "yarn"
|
|
64
|
+
: "unknown";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Manage packages using npm
|
|
69
|
+
*/
|
|
70
|
+
const NpmPackageManager: PackageManager = {
|
|
71
|
+
/** Add and install a new devDependency into the local package.json. */
|
|
72
|
+
async addDevDeps(...packages: string[]): Promise<void> {
|
|
73
|
+
await execa("npm", ["install", ...packages, "--save-dev"], {
|
|
74
|
+
stdio: "inherit",
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/** Install all the dependencies in the local package.json. */
|
|
79
|
+
async install(): Promise<void> {
|
|
80
|
+
await execa("npm", ["install"], {
|
|
81
|
+
stdio: "inherit",
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Manage packages using yarn
|
|
88
|
+
*/
|
|
89
|
+
const YarnPackageManager: PackageManager = {
|
|
90
|
+
/** Add and install a new devDependency into the local package.json. */
|
|
91
|
+
async addDevDeps(...packages: string[]): Promise<void> {
|
|
92
|
+
await execa("yarn", ["add", ...packages, "--dev"], {
|
|
93
|
+
stdio: "inherit",
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/** Install all the dependencies in the local package.json. */
|
|
98
|
+
async install(): Promise<void> {
|
|
99
|
+
await execa("yarn", ["install"], {
|
|
100
|
+
stdio: "inherit",
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
async function supports(name: string): Promise<boolean> {
|
|
106
|
+
try {
|
|
107
|
+
execaCommandSync(`${name} --version`, { stdio: "ignore" });
|
|
108
|
+
return true;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function supportsYarn(): Promise<boolean> {
|
|
115
|
+
return supports("yarn");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function supportsNpm(): Promise<boolean> {
|
|
119
|
+
return supports("npm");
|
|
120
|
+
}
|
package/src/pages.tsx
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
/* eslint-disable no-shadow */
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import { tmpdir } from "os";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { URL } from "url";
|
|
9
|
-
import { getType } from "mime";
|
|
10
|
-
import open from "open";
|
|
3
|
+
import { execSync, spawn } from "node:child_process";
|
|
4
|
+
import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { URL } from "node:url";
|
|
11
8
|
import { watch } from "chokidar";
|
|
12
|
-
import
|
|
9
|
+
import { getType } from "mime";
|
|
13
10
|
import { buildWorker } from "../pages/functions/buildWorker";
|
|
14
|
-
import type { Config } from "../pages/functions/routes";
|
|
15
|
-
import { writeRoutesModule } from "../pages/functions/routes";
|
|
16
11
|
import { generateConfigFromFileTree } from "../pages/functions/filepath-routing";
|
|
12
|
+
import { writeRoutesModule } from "../pages/functions/routes";
|
|
13
|
+
import openInBrowser from "./open-in-browser";
|
|
14
|
+
import { toUrlPath } from "./paths";
|
|
15
|
+
import type { Config } from "../pages/functions/routes";
|
|
16
|
+
import type { Headers, Request, fetch } from "@miniflare/core";
|
|
17
|
+
import type { BuildResult } from "esbuild";
|
|
18
|
+
import type { MiniflareOptions } from "miniflare";
|
|
19
|
+
import type { BuilderCallback } from "yargs";
|
|
17
20
|
|
|
18
21
|
// Defer importing miniflare until we really need it. This takes ~0.5s
|
|
19
22
|
// and also modifies some `stream/web` and `undici` prototypes, so we
|
|
20
23
|
// don't want to do this if pages commands aren't being called.
|
|
21
|
-
import type { Headers, Request, fetch } from "@miniflare/core";
|
|
22
|
-
import type { MiniflareOptions } from "miniflare";
|
|
23
24
|
|
|
24
25
|
const EXIT_CALLBACKS: (() => void)[] = [];
|
|
25
26
|
const EXIT = (message?: string, code?: number) => {
|
|
@@ -665,16 +666,17 @@ async function buildFunctions({
|
|
|
665
666
|
);
|
|
666
667
|
|
|
667
668
|
const routesModule = join(tmpdir(), "./functionsRoutes.mjs");
|
|
669
|
+
const baseURL = toUrlPath("/");
|
|
668
670
|
|
|
669
671
|
const config: Config = await generateConfigFromFileTree({
|
|
670
672
|
baseDir: functionsDirectory,
|
|
671
|
-
baseURL
|
|
673
|
+
baseURL,
|
|
672
674
|
});
|
|
673
675
|
|
|
674
676
|
if (outputConfigPath) {
|
|
675
677
|
writeFileSync(
|
|
676
678
|
outputConfigPath,
|
|
677
|
-
JSON.stringify({ ...config, baseURL
|
|
679
|
+
JSON.stringify({ ...config, baseURL }, null, 2)
|
|
678
680
|
);
|
|
679
681
|
}
|
|
680
682
|
|
|
@@ -793,8 +795,8 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
793
795
|
|
|
794
796
|
let miniflareArgs: MiniflareOptions = {};
|
|
795
797
|
|
|
796
|
-
let scriptReadyResolve;
|
|
797
|
-
const scriptReadyPromise = new Promise(
|
|
798
|
+
let scriptReadyResolve: () => void;
|
|
799
|
+
const scriptReadyPromise = new Promise<void>(
|
|
798
800
|
(resolve) => (scriptReadyResolve = resolve)
|
|
799
801
|
);
|
|
800
802
|
|
|
@@ -828,6 +830,9 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
828
830
|
scriptPath,
|
|
829
831
|
};
|
|
830
832
|
} else {
|
|
833
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
834
|
+
scriptReadyResolve!();
|
|
835
|
+
|
|
831
836
|
const scriptPath =
|
|
832
837
|
directory !== undefined
|
|
833
838
|
? join(directory, singleWorkerScriptPath)
|
|
@@ -896,7 +901,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
896
901
|
|
|
897
902
|
// env.ASSETS.fetch
|
|
898
903
|
serviceBindings: {
|
|
899
|
-
async ASSETS(request) {
|
|
904
|
+
async ASSETS(request: Request) {
|
|
900
905
|
if (proxyPort) {
|
|
901
906
|
try {
|
|
902
907
|
const url = new URL(request.url);
|
|
@@ -941,9 +946,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
941
946
|
console.log(`Serving at http://localhost:${port}/`);
|
|
942
947
|
|
|
943
948
|
if (process.env.BROWSER !== "none") {
|
|
944
|
-
|
|
945
|
-
// fail silently if the open command doesn't work (e.g. in GitHub Codespaces)
|
|
946
|
-
childProcess.on("error", (_err) => {});
|
|
949
|
+
await openInBrowser(`http://localhost:${port}/`);
|
|
947
950
|
}
|
|
948
951
|
|
|
949
952
|
if (directory !== undefined && liveReload) {
|
|
@@ -960,12 +963,14 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
960
963
|
miniflare.dispose().catch((err) => miniflare.log.error(err));
|
|
961
964
|
});
|
|
962
965
|
} catch (e) {
|
|
963
|
-
miniflare.log.error(e);
|
|
966
|
+
miniflare.log.error(e as Error);
|
|
964
967
|
EXIT("Could not start Miniflare.", 1);
|
|
965
968
|
}
|
|
966
969
|
}
|
|
967
970
|
)
|
|
968
|
-
.command("functions",
|
|
971
|
+
.command("functions", false, (yargs) =>
|
|
972
|
+
// we hide this command from help output because
|
|
973
|
+
// it's not meant to be used directly right now
|
|
969
974
|
yargs.command(
|
|
970
975
|
"build [directory]",
|
|
971
976
|
"Compile a folder of Cloudflare Pages Functions into a single Worker",
|
package/src/paths.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { assert } from "console";
|
|
2
|
+
|
|
3
|
+
type DiscriminatedPath<Discriminator extends string> = string & {
|
|
4
|
+
_discriminator: Discriminator;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A branded string that expects to be URL compatible.
|
|
9
|
+
*
|
|
10
|
+
* Require this type when you want callers to ensure that they have converted file-path strings into URL-safe paths.
|
|
11
|
+
*/
|
|
12
|
+
export type UrlPath = DiscriminatedPath<"UrlPath">;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a file-path string to a URL-path string.
|
|
16
|
+
*
|
|
17
|
+
* Use this helper to convert a `string` to a `UrlPath` when it is not clear whether the string needs normalizing.
|
|
18
|
+
* Replaces all back-slashes with forward-slashes, and throws an error if the path contains a drive letter (e.g. `C:`).
|
|
19
|
+
*/
|
|
20
|
+
export function toUrlPath(path: string): UrlPath {
|
|
21
|
+
assert(
|
|
22
|
+
!/^[a-z]:/i.test(path),
|
|
23
|
+
"Tried to convert a Windows file path with a drive to a URL path."
|
|
24
|
+
);
|
|
25
|
+
return path.replace(/\\/g, "/") as UrlPath;
|
|
26
|
+
}
|
package/src/proxy.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { connect } from "node:http2";
|
|
2
|
-
import type { ClientHttp2Session, ServerHttp2Stream } from "node:http2";
|
|
3
1
|
import { createServer } from "node:http";
|
|
2
|
+
import { connect } from "node:http2";
|
|
3
|
+
import WebSocket from "faye-websocket";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import serveStatic from "serve-static";
|
|
6
|
+
import type { CfPreviewToken } from "./api/preview";
|
|
4
7
|
import type {
|
|
5
8
|
IncomingHttpHeaders,
|
|
6
9
|
RequestListener,
|
|
@@ -8,10 +11,13 @@ import type {
|
|
|
8
11
|
ServerResponse,
|
|
9
12
|
Server,
|
|
10
13
|
} from "node:http";
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
import type { ClientHttp2Session, ServerHttp2Stream } from "node:http2";
|
|
15
|
+
import type ws from "ws";
|
|
16
|
+
|
|
17
|
+
interface IWebsocket extends ws {
|
|
18
|
+
// Pipe implements .on("message", ...)
|
|
19
|
+
pipe<T>(fn: T): IWebsocket;
|
|
20
|
+
}
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
23
|
* `usePreviewServer` is a React hook that creates a local development
|
|
@@ -80,6 +86,17 @@ export function usePreviewServer({
|
|
|
80
86
|
{ request: IncomingMessage; response: ServerResponse }[]
|
|
81
87
|
>([]);
|
|
82
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The session doesn't last forever, and will evetually drop
|
|
91
|
+
* (usually within 5-15 minutes). When that happens, we simply
|
|
92
|
+
* restart the effect, effectively restarting the server. We use
|
|
93
|
+
* a state sigil as an effect dependency to do so.
|
|
94
|
+
*/
|
|
95
|
+
const [retryServerSetupSigil, setRetryServerSetupSigil] = useState<number>(0);
|
|
96
|
+
function retryServerSetup() {
|
|
97
|
+
setRetryServerSetupSigil((x) => x + 1);
|
|
98
|
+
}
|
|
99
|
+
|
|
83
100
|
useEffect(() => {
|
|
84
101
|
// If we don't have a token, that means either we're just starting up,
|
|
85
102
|
// or we're refreshing the token.
|
|
@@ -119,6 +136,11 @@ export function usePreviewServer({
|
|
|
119
136
|
const remote = connect(`https://${previewToken.host}`);
|
|
120
137
|
cleanupListeners.push(() => remote.destroy());
|
|
121
138
|
|
|
139
|
+
// As mentioned above, the session may die at any point,
|
|
140
|
+
// so we need to restart the effect.
|
|
141
|
+
remote.on("close", retryServerSetup);
|
|
142
|
+
cleanupListeners.push(() => remote.off("close", retryServerSetup));
|
|
143
|
+
|
|
122
144
|
/** HTTP/2 -> HTTP/2 */
|
|
123
145
|
const handleStream = createStreamHandler(previewToken, remote, port);
|
|
124
146
|
proxy.on("stream", handleStream);
|
|
@@ -191,33 +213,49 @@ export function usePreviewServer({
|
|
|
191
213
|
const { headers, url } = message;
|
|
192
214
|
addCfPreviewTokenHeader(headers, previewToken.value);
|
|
193
215
|
headers["host"] = previewToken.host;
|
|
194
|
-
const localWebsocket = new WebSocket(message, socket, body);
|
|
216
|
+
const localWebsocket = new WebSocket(message, socket, body) as IWebsocket;
|
|
195
217
|
// TODO(soon): Custom WebSocket protocol is not working?
|
|
196
218
|
const remoteWebsocketClient = new WebSocket.Client(
|
|
197
219
|
`wss://${previewToken.host}${url}`,
|
|
198
220
|
[],
|
|
199
221
|
{ headers }
|
|
200
|
-
);
|
|
222
|
+
) as IWebsocket;
|
|
201
223
|
localWebsocket.pipe(remoteWebsocketClient).pipe(localWebsocket);
|
|
202
224
|
// We close down websockets whenever we refresh the token.
|
|
203
225
|
cleanupListeners.push(() => {
|
|
204
|
-
localWebsocket.
|
|
205
|
-
remoteWebsocketClient.
|
|
226
|
+
localWebsocket.close();
|
|
227
|
+
remoteWebsocketClient.close();
|
|
206
228
|
});
|
|
207
229
|
};
|
|
208
230
|
proxy.on("upgrade", handleUpgrade);
|
|
209
231
|
cleanupListeners.push(() => proxy.off("upgrade", handleUpgrade));
|
|
210
232
|
|
|
211
233
|
return () => {
|
|
212
|
-
cleanupListeners.forEach((
|
|
234
|
+
cleanupListeners.forEach((cleanup) => cleanup());
|
|
213
235
|
};
|
|
214
|
-
}, [
|
|
236
|
+
}, [
|
|
237
|
+
previewToken,
|
|
238
|
+
publicRoot,
|
|
239
|
+
port,
|
|
240
|
+
proxy,
|
|
241
|
+
// We use a state value as a sigil to trigger reconnecting the server.
|
|
242
|
+
// It's not used inside the effect, so react-hooks/exhaustive-deps
|
|
243
|
+
// doesn't complain if it's not included in the dependency array.
|
|
244
|
+
// But its presence is critical, so Do NOT remove it from the dependency list.
|
|
245
|
+
retryServerSetupSigil,
|
|
246
|
+
]);
|
|
215
247
|
|
|
216
248
|
// Start/stop the server whenever the
|
|
217
249
|
// containing component is mounted/unmounted.
|
|
218
250
|
useEffect(() => {
|
|
219
|
-
|
|
220
|
-
|
|
251
|
+
waitForPortToBeAvailable(port, { retryPeriod: 200, timeout: 2000 })
|
|
252
|
+
.then(() => {
|
|
253
|
+
proxy.listen(port);
|
|
254
|
+
console.log(`⬣ Listening at http://localhost:${port}`);
|
|
255
|
+
})
|
|
256
|
+
.catch((err) => {
|
|
257
|
+
console.error(`⬣ Failed to start server: ${err}`);
|
|
258
|
+
});
|
|
221
259
|
|
|
222
260
|
return () => {
|
|
223
261
|
proxy.close();
|
|
@@ -300,3 +338,39 @@ function createStreamHandler(
|
|
|
300
338
|
});
|
|
301
339
|
};
|
|
302
340
|
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* A helper function that waits for a port to be available.
|
|
344
|
+
*/
|
|
345
|
+
export async function waitForPortToBeAvailable(
|
|
346
|
+
port: number,
|
|
347
|
+
options: { retryPeriod: number; timeout: number }
|
|
348
|
+
): Promise<void> {
|
|
349
|
+
return new Promise((resolve, reject) => {
|
|
350
|
+
const timeout = setTimeout(() => {
|
|
351
|
+
reject(new Error(`Timed out waiting for port ${port}`));
|
|
352
|
+
}, options.timeout);
|
|
353
|
+
|
|
354
|
+
function checkPort() {
|
|
355
|
+
// Testing whether a port is 'available' involves simply
|
|
356
|
+
// trying to make a server listen on that port, and retrying
|
|
357
|
+
// until it succeeds.
|
|
358
|
+
const server = createServer();
|
|
359
|
+
server.on("error", (err) => {
|
|
360
|
+
// @ts-expect-error non standard property on Error
|
|
361
|
+
if (err.code === "EADDRINUSE") {
|
|
362
|
+
setTimeout(checkPort, options.retryPeriod);
|
|
363
|
+
} else {
|
|
364
|
+
reject(err);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
server.listen(port, () => {
|
|
368
|
+
server.close();
|
|
369
|
+
clearTimeout(timeout);
|
|
370
|
+
resolve();
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
checkPort();
|
|
375
|
+
});
|
|
376
|
+
}
|