wrangler 0.0.0-e14e589 → 0.0.0-e2a26a0
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/README.md +51 -0
- package/bin/wrangler.js +2 -2
- package/package.json +49 -30
- package/pages/functions/buildWorker.ts +62 -0
- package/pages/functions/filepath-routing.test.ts +47 -0
- package/pages/functions/filepath-routing.ts +231 -0
- package/pages/functions/identifiers.ts +78 -0
- package/pages/functions/routes.ts +160 -0
- package/pages/functions/template-worker.ts +136 -0
- package/src/__tests__/dev.test.tsx +71 -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/mock-account-id.ts +30 -0
- package/src/__tests__/helpers/mock-bin.ts +33 -0
- package/src/__tests__/helpers/mock-cfetch.ts +207 -0
- package/src/__tests__/helpers/mock-console.ts +56 -0
- package/src/__tests__/helpers/mock-dialogs.ts +65 -0
- package/src/__tests__/helpers/mock-kv.ts +40 -0
- package/src/__tests__/helpers/mock-user.ts +27 -0
- package/src/__tests__/helpers/run-in-tmp.ts +29 -0
- package/src/__tests__/helpers/run-wrangler.ts +14 -0
- package/src/__tests__/index.test.ts +509 -119
- package/src/__tests__/jest.setup.ts +37 -0
- package/src/__tests__/kv.test.ts +1139 -0
- package/src/__tests__/logout.test.ts +50 -0
- package/src/__tests__/package-manager.test.ts +206 -0
- package/src/__tests__/publish.test.ts +1222 -0
- package/src/__tests__/secret.test.ts +210 -0
- package/src/__tests__/whoami.test.tsx +86 -0
- package/src/api/form_data.ts +85 -76
- package/src/api/preview.ts +29 -24
- package/src/api/worker.ts +50 -35
- package/src/cfetch/index.ts +102 -0
- package/src/cfetch/internal.ts +101 -0
- package/src/cli.ts +6 -7
- package/src/config.ts +630 -85
- package/src/dev.tsx +446 -282
- package/src/dialogs.tsx +6 -14
- package/src/index.tsx +773 -426
- package/src/inspect.ts +596 -0
- package/src/kv.tsx +118 -45
- package/src/module-collection.ts +63 -0
- package/src/package-manager.ts +120 -0
- package/src/pages.tsx +917 -211
- package/src/paths.ts +26 -0
- package/src/proxy.ts +370 -0
- package/src/publish.ts +220 -88
- package/src/sites.tsx +180 -55
- package/src/tail.tsx +6 -4
- package/src/user.tsx +77 -87
- package/src/whoami.tsx +69 -0
- 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 +125330 -557
- package/wrangler-dist/cli.js.map +3 -3
- package/src/__tests__/fixtures/init/.gitkeep +0 -0
- package/src/__tests__/mock-cfetch.js +0 -46
- package/src/api/inspect.ts +0 -441
- package/src/cfetch.ts +0 -72
- package/src/util/fetch.ts +0 -74
package/src/dev.tsx
CHANGED
|
@@ -1,62 +1,87 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { watch } from "chokidar";
|
|
7
|
+
import clipboardy from "clipboardy";
|
|
8
|
+
import commandExists from "command-exists";
|
|
9
|
+
import * as esbuild from "esbuild";
|
|
10
|
+
import { execaCommand } from "execa";
|
|
11
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
10
12
|
import open from "open";
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
13
|
-
import { createWorker } from "./api/worker";
|
|
14
|
-
import type { CfWorkerInit } from "./api/worker";
|
|
15
|
-
import { spawn } from "child_process";
|
|
13
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
14
|
+
import { withErrorBoundary, useErrorHandler } from "react-error-boundary";
|
|
16
15
|
import onExit from "signal-exit";
|
|
16
|
+
import tmp from "tmp-promise";
|
|
17
|
+
import { fetch } from "undici";
|
|
18
|
+
|
|
19
|
+
import { createWorker } from "./api/worker";
|
|
20
|
+
|
|
21
|
+
import useInspector from "./inspect";
|
|
22
|
+
import makeModuleCollector from "./module-collection";
|
|
23
|
+
import { usePreviewServer, waitForPortToBeAvailable } from "./proxy";
|
|
17
24
|
import { syncAssets } from "./sites";
|
|
18
|
-
import clipboardy from "clipboardy";
|
|
19
|
-
import http from "node:http";
|
|
20
|
-
import serveStatic from "serve-static";
|
|
21
|
-
import commandExists from "command-exists";
|
|
22
|
-
import assert from "assert";
|
|
23
25
|
import { getAPIToken } from "./user";
|
|
24
|
-
import
|
|
26
|
+
import type { CfPreviewToken } from "./api/preview";
|
|
27
|
+
import type { CfModule, CfWorkerInit } from "./api/worker";
|
|
28
|
+
import type { AssetPaths } from "./sites";
|
|
29
|
+
import type { DirectoryResult } from "tmp-promise";
|
|
25
30
|
|
|
26
|
-
type CfScriptFormat =
|
|
31
|
+
type CfScriptFormat = undefined | "modules" | "service-worker";
|
|
27
32
|
|
|
28
|
-
type
|
|
33
|
+
export type DevProps = {
|
|
29
34
|
name?: string;
|
|
30
35
|
entry: string;
|
|
31
36
|
port?: number;
|
|
32
37
|
format: CfScriptFormat;
|
|
33
|
-
accountId:
|
|
38
|
+
accountId: undefined | string;
|
|
34
39
|
initialMode: "local" | "remote";
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
jsxFactory: undefined | string;
|
|
41
|
+
jsxFragment: undefined | string;
|
|
42
|
+
enableLocalPersistence: boolean;
|
|
43
|
+
bindings: CfWorkerInit["bindings"];
|
|
44
|
+
public: undefined | string;
|
|
45
|
+
assetPaths: undefined | AssetPaths;
|
|
46
|
+
compatibilityDate: undefined | string;
|
|
47
|
+
compatibilityFlags: undefined | string[];
|
|
48
|
+
usageModel: undefined | "bundled" | "unbound";
|
|
49
|
+
buildCommand: {
|
|
50
|
+
command?: undefined | string;
|
|
51
|
+
cwd?: undefined | string;
|
|
52
|
+
watch_dir?: undefined | string;
|
|
53
|
+
};
|
|
54
|
+
env: string | undefined;
|
|
38
55
|
};
|
|
39
56
|
|
|
40
|
-
|
|
57
|
+
function Dev(props: DevProps): JSX.Element {
|
|
41
58
|
if (props.public && props.format === "service-worker") {
|
|
42
59
|
throw new Error(
|
|
43
60
|
"You cannot use the service worker format with a `public` directory."
|
|
44
61
|
);
|
|
45
62
|
}
|
|
46
|
-
const port = props.port
|
|
63
|
+
const port = props.port ?? 8787;
|
|
47
64
|
const apiToken = getAPIToken();
|
|
48
65
|
const directory = useTmpDir();
|
|
49
66
|
|
|
50
|
-
|
|
67
|
+
// if there isn't a build command, we just return the entry immediately
|
|
68
|
+
// ideally there would be a conditional here, but the rules of hooks
|
|
69
|
+
// kinda forbid that, so we thread the entry through useCustomBuild
|
|
70
|
+
const entry = useCustomBuild(props.entry, props.buildCommand);
|
|
71
|
+
|
|
72
|
+
const bundle = useEsbuild({
|
|
73
|
+
entry,
|
|
74
|
+
destination: directory,
|
|
75
|
+
staticRoot: props.public,
|
|
76
|
+
jsxFactory: props.jsxFactory,
|
|
77
|
+
jsxFragment: props.jsxFragment,
|
|
78
|
+
});
|
|
51
79
|
if (bundle && bundle.type === "commonjs" && !props.format && props.public) {
|
|
52
80
|
throw new Error(
|
|
53
81
|
"You cannot use the service worker format with a `public` directory."
|
|
54
82
|
);
|
|
55
83
|
}
|
|
56
84
|
|
|
57
|
-
// @ts-expect-error whack
|
|
58
|
-
useDevtoolsRefresh(bundle?.id ?? 0);
|
|
59
|
-
|
|
60
85
|
const toggles = useHotkeys(
|
|
61
86
|
{
|
|
62
87
|
local: props.initialMode === "local",
|
|
@@ -74,10 +99,11 @@ export function Dev(props: Props): JSX.Element {
|
|
|
74
99
|
name={props.name}
|
|
75
100
|
bundle={bundle}
|
|
76
101
|
format={props.format}
|
|
77
|
-
|
|
78
|
-
|
|
102
|
+
bindings={props.bindings}
|
|
103
|
+
assetPaths={props.assetPaths}
|
|
79
104
|
public={props.public}
|
|
80
|
-
port={
|
|
105
|
+
port={port}
|
|
106
|
+
enableLocalPersistence={props.enableLocalPersistence}
|
|
81
107
|
/>
|
|
82
108
|
) : (
|
|
83
109
|
<Remote
|
|
@@ -86,10 +112,14 @@ export function Dev(props: Props): JSX.Element {
|
|
|
86
112
|
format={props.format}
|
|
87
113
|
accountId={props.accountId}
|
|
88
114
|
apiToken={apiToken}
|
|
89
|
-
|
|
90
|
-
|
|
115
|
+
bindings={props.bindings}
|
|
116
|
+
assetPaths={props.assetPaths}
|
|
91
117
|
public={props.public}
|
|
92
|
-
port={
|
|
118
|
+
port={port}
|
|
119
|
+
compatibilityDate={props.compatibilityDate}
|
|
120
|
+
compatibilityFlags={props.compatibilityFlags}
|
|
121
|
+
usageModel={props.usageModel}
|
|
122
|
+
env={props.env}
|
|
93
123
|
/>
|
|
94
124
|
)}
|
|
95
125
|
<Box borderStyle="round" paddingLeft={1} paddingRight={1}>
|
|
@@ -105,98 +135,91 @@ export function Dev(props: Props): JSX.Element {
|
|
|
105
135
|
);
|
|
106
136
|
}
|
|
107
137
|
|
|
108
|
-
function useDevtoolsRefresh(bundleId: number) {
|
|
109
|
-
// TODO: this is a hack while we figure out
|
|
110
|
-
// a better cleaner solution to get devtools to reconnect
|
|
111
|
-
// without having to do a full refresh
|
|
112
|
-
const ref = useRef();
|
|
113
|
-
// @ts-expect-error whack
|
|
114
|
-
ref.current = bundleId;
|
|
115
|
-
|
|
116
|
-
useEffect(() => {
|
|
117
|
-
const server = http.createServer((req, res) => {
|
|
118
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
119
|
-
res.setHeader("Access-Control-Request-Method", "*");
|
|
120
|
-
res.setHeader("Access-Control-Allow-Methods", "OPTIONS, GET");
|
|
121
|
-
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
122
|
-
if (req.method === "OPTIONS") {
|
|
123
|
-
res.writeHead(200);
|
|
124
|
-
res.end();
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
128
|
-
res.end(JSON.stringify({ value: ref.current }));
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
server.listen(3142);
|
|
132
|
-
return () => {
|
|
133
|
-
server.close();
|
|
134
|
-
};
|
|
135
|
-
}, []);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
138
|
function Remote(props: {
|
|
139
|
-
name:
|
|
140
|
-
bundle: EsbuildBundle |
|
|
139
|
+
name: undefined | string;
|
|
140
|
+
bundle: EsbuildBundle | undefined;
|
|
141
141
|
format: CfScriptFormat;
|
|
142
|
-
public:
|
|
143
|
-
|
|
142
|
+
public: undefined | string;
|
|
143
|
+
assetPaths: undefined | AssetPaths;
|
|
144
144
|
port: number;
|
|
145
|
-
accountId:
|
|
146
|
-
apiToken:
|
|
147
|
-
|
|
145
|
+
accountId: undefined | string;
|
|
146
|
+
apiToken: undefined | string;
|
|
147
|
+
bindings: CfWorkerInit["bindings"];
|
|
148
|
+
compatibilityDate: string | undefined;
|
|
149
|
+
compatibilityFlags: undefined | string[];
|
|
150
|
+
usageModel: undefined | "bundled" | "unbound";
|
|
151
|
+
env: string | undefined;
|
|
148
152
|
}) {
|
|
149
153
|
assert(props.accountId, "accountId is required");
|
|
150
154
|
assert(props.apiToken, "apiToken is required");
|
|
151
|
-
const
|
|
155
|
+
const previewToken = useWorker({
|
|
152
156
|
name: props.name,
|
|
153
157
|
bundle: props.bundle,
|
|
154
158
|
format: props.format,
|
|
155
|
-
modules: [],
|
|
159
|
+
modules: props.bundle ? props.bundle.modules : [],
|
|
156
160
|
accountId: props.accountId,
|
|
157
161
|
apiToken: props.apiToken,
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
bindings: props.bindings,
|
|
163
|
+
assetPaths: props.assetPaths,
|
|
160
164
|
port: props.port,
|
|
165
|
+
compatibilityDate: props.compatibilityDate,
|
|
166
|
+
compatibilityFlags: props.compatibilityFlags,
|
|
167
|
+
usageModel: props.usageModel,
|
|
168
|
+
env: props.env,
|
|
161
169
|
});
|
|
162
170
|
|
|
163
|
-
|
|
171
|
+
usePreviewServer({
|
|
172
|
+
previewToken,
|
|
173
|
+
publicRoot: props.public,
|
|
174
|
+
port: props.port,
|
|
175
|
+
});
|
|
164
176
|
|
|
165
|
-
useInspector(
|
|
177
|
+
useInspector({
|
|
178
|
+
inspectorUrl: previewToken ? previewToken.inspectorUrl.href : undefined,
|
|
179
|
+
port: 9229,
|
|
180
|
+
logToTerminal: true,
|
|
181
|
+
});
|
|
166
182
|
return null;
|
|
167
183
|
}
|
|
168
184
|
function Local(props: {
|
|
169
|
-
name:
|
|
170
|
-
bundle: EsbuildBundle |
|
|
185
|
+
name: undefined | string;
|
|
186
|
+
bundle: EsbuildBundle | undefined;
|
|
171
187
|
format: CfScriptFormat;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
188
|
+
bindings: CfWorkerInit["bindings"];
|
|
189
|
+
assetPaths: undefined | AssetPaths;
|
|
190
|
+
public: undefined | string;
|
|
175
191
|
port: number;
|
|
192
|
+
enableLocalPersistence: boolean;
|
|
176
193
|
}) {
|
|
177
194
|
const { inspectorUrl } = useLocalWorker({
|
|
178
195
|
name: props.name,
|
|
179
196
|
bundle: props.bundle,
|
|
180
197
|
format: props.format,
|
|
181
|
-
|
|
198
|
+
bindings: props.bindings,
|
|
199
|
+
assetPaths: props.assetPaths,
|
|
200
|
+
public: props.public,
|
|
182
201
|
port: props.port,
|
|
202
|
+
enableLocalPersistence: props.enableLocalPersistence,
|
|
183
203
|
});
|
|
184
|
-
useInspector(inspectorUrl);
|
|
204
|
+
useInspector({ inspectorUrl, port: 9229, logToTerminal: false });
|
|
185
205
|
return null;
|
|
186
206
|
}
|
|
187
207
|
|
|
188
208
|
function useLocalWorker(props: {
|
|
189
|
-
name:
|
|
190
|
-
bundle: EsbuildBundle |
|
|
209
|
+
name: undefined | string;
|
|
210
|
+
bundle: EsbuildBundle | undefined;
|
|
191
211
|
format: CfScriptFormat;
|
|
192
|
-
|
|
212
|
+
bindings: CfWorkerInit["bindings"];
|
|
213
|
+
assetPaths: undefined | AssetPaths;
|
|
214
|
+
public: undefined | string;
|
|
193
215
|
port: number;
|
|
216
|
+
enableLocalPersistence: boolean;
|
|
194
217
|
}) {
|
|
195
218
|
// TODO: pass vars via command line
|
|
196
|
-
const { bundle, format,
|
|
219
|
+
const { bundle, format, bindings, port, assetPaths } = props;
|
|
197
220
|
const local = useRef<ReturnType<typeof spawn>>();
|
|
198
221
|
const removeSignalExitListener = useRef<() => void>();
|
|
199
|
-
const [inspectorUrl, setInspectorUrl] = useState<string |
|
|
222
|
+
const [inspectorUrl, setInspectorUrl] = useState<string | undefined>();
|
|
200
223
|
useEffect(() => {
|
|
201
224
|
async function startLocalWorker() {
|
|
202
225
|
if (!bundle) return;
|
|
@@ -211,7 +234,15 @@ function useLocalWorker(props: {
|
|
|
211
234
|
return;
|
|
212
235
|
}
|
|
213
236
|
|
|
237
|
+
await waitForPortToBeAvailable(port, { retryPeriod: 200, timeout: 2000 });
|
|
238
|
+
if (props.public) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
'⎔ A "public" folder is not yet supported in local mode.'
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
214
244
|
console.log("⎔ Starting a local server...");
|
|
245
|
+
// TODO: just use execa for this
|
|
215
246
|
local.current = spawn("node", [
|
|
216
247
|
"--experimental-vm-modules",
|
|
217
248
|
"--inspect",
|
|
@@ -226,20 +257,34 @@ function useLocalWorker(props: {
|
|
|
226
257
|
path.join(__dirname, "../miniflare-config-stubs/package.empty.json"),
|
|
227
258
|
"--port",
|
|
228
259
|
port.toString(),
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
260
|
+
...(assetPaths
|
|
261
|
+
? [
|
|
262
|
+
"--site",
|
|
263
|
+
assetPaths.baseDirectory,
|
|
264
|
+
...assetPaths.includePatterns.map((pattern) => [
|
|
265
|
+
"--site-include",
|
|
266
|
+
pattern,
|
|
267
|
+
]),
|
|
268
|
+
...assetPaths.excludePatterns.map((pattern) => [
|
|
269
|
+
"--site-exclude",
|
|
270
|
+
pattern,
|
|
271
|
+
]),
|
|
272
|
+
].flatMap((x) => x)
|
|
273
|
+
: []),
|
|
274
|
+
...(props.enableLocalPersistence
|
|
275
|
+
? ["--kv-persist", "--cache-persist", "--do-persist"]
|
|
276
|
+
: []),
|
|
277
|
+
...Object.entries(bindings.vars || {}).flatMap(([key, value]) => {
|
|
278
|
+
return ["--binding", `${key}=${value}`];
|
|
279
|
+
}),
|
|
280
|
+
...(bindings.kv_namespaces || []).flatMap(({ binding }) => {
|
|
281
|
+
return ["--kv", binding];
|
|
282
|
+
}),
|
|
283
|
+
...(bindings.durable_objects?.bindings || []).flatMap(
|
|
284
|
+
({ name, class_name }) => {
|
|
285
|
+
return ["--do", `${name}=${class_name}`];
|
|
286
|
+
}
|
|
287
|
+
),
|
|
243
288
|
"--modules",
|
|
244
289
|
format ||
|
|
245
290
|
(bundle.type === "esm" ? "modules" : "service-worker") === "modules"
|
|
@@ -254,29 +299,42 @@ function useLocalWorker(props: {
|
|
|
254
299
|
}
|
|
255
300
|
});
|
|
256
301
|
|
|
257
|
-
local.current.stdout
|
|
258
|
-
|
|
302
|
+
local.current.stdout?.on("data", (data: Buffer) => {
|
|
303
|
+
console.log(`${data.toString()}`);
|
|
259
304
|
});
|
|
260
305
|
|
|
261
|
-
local.current.stderr
|
|
262
|
-
|
|
306
|
+
local.current.stderr?.on("data", (data: Buffer) => {
|
|
307
|
+
console.error(`${data.toString()}`);
|
|
263
308
|
const matches =
|
|
264
309
|
/Debugger listening on (ws:\/\/127\.0\.0\.1:9229\/[A-Za-z0-9-]+)/.exec(
|
|
265
|
-
data
|
|
310
|
+
data.toString()
|
|
266
311
|
);
|
|
267
312
|
if (matches) {
|
|
268
313
|
setInspectorUrl(matches[1]);
|
|
269
314
|
}
|
|
270
315
|
});
|
|
271
316
|
|
|
272
|
-
|
|
317
|
+
local.current.on("exit", (code) => {
|
|
318
|
+
if (code !== 0) {
|
|
319
|
+
console.error(`miniflare process exited with code ${code}`);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
local.current.on("error", (error: Error) => {
|
|
324
|
+
console.error(`miniflare process failed to spawn`);
|
|
325
|
+
console.error(error);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
removeSignalExitListener.current = onExit((_code, _signal) => {
|
|
273
329
|
console.log("⎔ Shutting down local server.");
|
|
274
330
|
local.current?.kill();
|
|
275
331
|
local.current = undefined;
|
|
276
332
|
});
|
|
277
333
|
}
|
|
278
334
|
|
|
279
|
-
startLocalWorker()
|
|
335
|
+
startLocalWorker().catch((err) => {
|
|
336
|
+
console.error("local worker:", err);
|
|
337
|
+
});
|
|
280
338
|
|
|
281
339
|
return () => {
|
|
282
340
|
if (local.current) {
|
|
@@ -287,12 +345,23 @@ function useLocalWorker(props: {
|
|
|
287
345
|
removeSignalExitListener.current = undefined;
|
|
288
346
|
}
|
|
289
347
|
};
|
|
290
|
-
}, [
|
|
348
|
+
}, [
|
|
349
|
+
bundle,
|
|
350
|
+
format,
|
|
351
|
+
port,
|
|
352
|
+
bindings.durable_objects?.bindings,
|
|
353
|
+
bindings.kv_namespaces,
|
|
354
|
+
bindings.vars,
|
|
355
|
+
props.enableLocalPersistence,
|
|
356
|
+
assetPaths,
|
|
357
|
+
props.public,
|
|
358
|
+
]);
|
|
291
359
|
return { inspectorUrl };
|
|
292
360
|
}
|
|
293
361
|
|
|
294
|
-
function useTmpDir(): string |
|
|
362
|
+
function useTmpDir(): string | undefined {
|
|
295
363
|
const [directory, setDirectory] = useState<DirectoryResult>();
|
|
364
|
+
const handleError = useErrorHandler();
|
|
296
365
|
useEffect(() => {
|
|
297
366
|
let dir: DirectoryResult;
|
|
298
367
|
async function create() {
|
|
@@ -302,36 +371,118 @@ function useTmpDir(): string | void {
|
|
|
302
371
|
return;
|
|
303
372
|
} catch (err) {
|
|
304
373
|
console.error("failed to create tmp dir");
|
|
305
|
-
console.error(err);
|
|
306
374
|
throw err;
|
|
307
375
|
}
|
|
308
376
|
}
|
|
309
|
-
create()
|
|
377
|
+
create().catch((err) => {
|
|
378
|
+
// we want to break here
|
|
379
|
+
// we can't do much without a temp dir anyway
|
|
380
|
+
handleError(err);
|
|
381
|
+
});
|
|
310
382
|
return () => {
|
|
311
|
-
dir.cleanup()
|
|
383
|
+
dir.cleanup().catch(() => {
|
|
384
|
+
// extremely unlikely,
|
|
385
|
+
// but it's 2022 after all
|
|
386
|
+
console.error("failed to cleanup tmp dir");
|
|
387
|
+
});
|
|
312
388
|
};
|
|
313
|
-
}, []);
|
|
389
|
+
}, [handleError]);
|
|
314
390
|
return directory?.path;
|
|
315
391
|
}
|
|
316
392
|
|
|
393
|
+
function useCustomBuild(
|
|
394
|
+
expectedEntry: string,
|
|
395
|
+
props: {
|
|
396
|
+
command?: undefined | string;
|
|
397
|
+
cwd?: undefined | string;
|
|
398
|
+
watch_dir?: undefined | string;
|
|
399
|
+
}
|
|
400
|
+
): undefined | string {
|
|
401
|
+
const [entry, setEntry] = useState<string | undefined>(
|
|
402
|
+
// if there's no build command, just return the expected entry
|
|
403
|
+
props.command || expectedEntry
|
|
404
|
+
);
|
|
405
|
+
const { command, cwd, watch_dir } = props;
|
|
406
|
+
useEffect(() => {
|
|
407
|
+
if (!command) return;
|
|
408
|
+
let cmd, interval;
|
|
409
|
+
console.log("running:", command);
|
|
410
|
+
cmd = execaCommand(command, {
|
|
411
|
+
...(cwd && { cwd }),
|
|
412
|
+
shell: true,
|
|
413
|
+
stderr: "inherit",
|
|
414
|
+
stdout: "inherit",
|
|
415
|
+
});
|
|
416
|
+
if (watch_dir) {
|
|
417
|
+
watch(watch_dir, { persistent: true, ignoreInitial: true }).on(
|
|
418
|
+
"all",
|
|
419
|
+
(_event, filePath) => {
|
|
420
|
+
console.log(`The file ${filePath} changed, restarting build...`);
|
|
421
|
+
cmd.kill();
|
|
422
|
+
cmd = execaCommand(command, {
|
|
423
|
+
...(cwd && { cwd }),
|
|
424
|
+
shell: true,
|
|
425
|
+
stderr: "inherit",
|
|
426
|
+
stdout: "inherit",
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// check every so often whether `expectedEntry` exists
|
|
433
|
+
// if it does, we're done
|
|
434
|
+
const startedAt = Date.now();
|
|
435
|
+
interval = setInterval(() => {
|
|
436
|
+
if (existsSync(expectedEntry)) {
|
|
437
|
+
clearInterval(interval);
|
|
438
|
+
setEntry(expectedEntry);
|
|
439
|
+
} else {
|
|
440
|
+
const elapsed = Date.now() - startedAt;
|
|
441
|
+
// timeout after 30 seconds of waiting
|
|
442
|
+
if (elapsed > 1000 * 60 * 30) {
|
|
443
|
+
console.error("⎔ Build timed out.");
|
|
444
|
+
clearInterval(interval);
|
|
445
|
+
cmd.kill();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}, 200);
|
|
449
|
+
// TODO: we could probably timeout here after a while
|
|
450
|
+
|
|
451
|
+
return () => {
|
|
452
|
+
if (cmd) {
|
|
453
|
+
cmd.kill();
|
|
454
|
+
cmd = undefined;
|
|
455
|
+
}
|
|
456
|
+
clearInterval(interval);
|
|
457
|
+
interval = undefined;
|
|
458
|
+
};
|
|
459
|
+
}, [command, cwd, expectedEntry, watch_dir]);
|
|
460
|
+
return entry;
|
|
461
|
+
}
|
|
462
|
+
|
|
317
463
|
type EsbuildBundle = {
|
|
318
464
|
id: number;
|
|
319
465
|
path: string;
|
|
320
466
|
entry: string;
|
|
321
467
|
type: "esm" | "commonjs";
|
|
322
468
|
exports: string[];
|
|
469
|
+
modules: CfModule[];
|
|
323
470
|
};
|
|
324
471
|
|
|
325
|
-
function useEsbuild(
|
|
326
|
-
entry: string
|
|
327
|
-
destination: string |
|
|
328
|
-
staticRoot:
|
|
329
|
-
|
|
472
|
+
function useEsbuild(props: {
|
|
473
|
+
entry: undefined | string;
|
|
474
|
+
destination: string | undefined;
|
|
475
|
+
staticRoot: undefined | string;
|
|
476
|
+
jsxFactory: string | undefined;
|
|
477
|
+
jsxFragment: string | undefined;
|
|
478
|
+
}): EsbuildBundle | undefined {
|
|
479
|
+
const { entry, destination, staticRoot, jsxFactory, jsxFragment } = props;
|
|
330
480
|
const [bundle, setBundle] = useState<EsbuildBundle>();
|
|
331
481
|
useEffect(() => {
|
|
332
|
-
let result: esbuild.BuildResult;
|
|
482
|
+
let result: esbuild.BuildResult | undefined;
|
|
333
483
|
async function build() {
|
|
334
|
-
if (!destination) return;
|
|
484
|
+
if (!destination || !entry) return;
|
|
485
|
+
const moduleCollector = makeModuleCollector();
|
|
335
486
|
result = await esbuild.build({
|
|
336
487
|
entryPoints: [entry],
|
|
337
488
|
bundle: true,
|
|
@@ -339,7 +490,17 @@ function useEsbuild(
|
|
|
339
490
|
metafile: true,
|
|
340
491
|
format: "esm",
|
|
341
492
|
sourcemap: true,
|
|
493
|
+
loader: {
|
|
494
|
+
".js": "jsx",
|
|
495
|
+
".html": "text",
|
|
496
|
+
".pem": "text",
|
|
497
|
+
".txt": "text",
|
|
498
|
+
},
|
|
499
|
+
...(jsxFactory && { jsxFactory }),
|
|
500
|
+
...(jsxFragment && { jsxFragment }),
|
|
342
501
|
external: ["__STATIC_CONTENT_MANIFEST"],
|
|
502
|
+
conditions: ["worker", "browser"],
|
|
503
|
+
plugins: [moduleCollector.plugin],
|
|
343
504
|
// TODO: import.meta.url
|
|
344
505
|
watch: {
|
|
345
506
|
async onRebuild(error) {
|
|
@@ -347,43 +508,68 @@ function useEsbuild(
|
|
|
347
508
|
else {
|
|
348
509
|
// nothing really changes here, so let's increment the id
|
|
349
510
|
// to change the return object's identity
|
|
350
|
-
setBundle((
|
|
511
|
+
setBundle((previousBundle) => {
|
|
512
|
+
if (previousBundle === undefined) {
|
|
513
|
+
assert.fail(
|
|
514
|
+
"Rebuild triggered with no previous build available"
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
return { ...previousBundle, id: previousBundle.id + 1 };
|
|
518
|
+
});
|
|
351
519
|
}
|
|
352
520
|
},
|
|
353
521
|
},
|
|
354
522
|
});
|
|
355
523
|
|
|
356
|
-
|
|
524
|
+
// result.metafile is defined because of the `metafile: true` option above.
|
|
525
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
526
|
+
const metafile = result.metafile!;
|
|
527
|
+
const outputEntry = Object.entries(metafile.outputs).find(
|
|
357
528
|
([_path, { entryPoint }]) => entryPoint === entry
|
|
358
529
|
); // assumedly only one entry point
|
|
359
530
|
|
|
531
|
+
if (outputEntry === undefined) {
|
|
532
|
+
throw new Error(
|
|
533
|
+
`Cannot find entry-point "${entry}" in generated bundle.`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
360
536
|
setBundle({
|
|
361
537
|
id: 0,
|
|
362
538
|
entry,
|
|
363
|
-
path:
|
|
364
|
-
type:
|
|
365
|
-
exports:
|
|
539
|
+
path: outputEntry[0],
|
|
540
|
+
type: outputEntry[1].exports.length > 0 ? "esm" : "commonjs",
|
|
541
|
+
exports: outputEntry[1].exports,
|
|
542
|
+
modules: moduleCollector.modules,
|
|
366
543
|
});
|
|
367
544
|
}
|
|
368
|
-
build()
|
|
545
|
+
build().catch((_err) => {
|
|
546
|
+
// esbuild already logs errors to stderr
|
|
547
|
+
// and we don't want to end the process
|
|
548
|
+
// on build errors anyway
|
|
549
|
+
// so this is a no-op error handler
|
|
550
|
+
});
|
|
369
551
|
return () => {
|
|
370
|
-
result?.stop();
|
|
552
|
+
result?.stop?.();
|
|
371
553
|
};
|
|
372
|
-
}, [entry, destination, staticRoot]);
|
|
554
|
+
}, [entry, destination, staticRoot, jsxFactory, jsxFragment]);
|
|
373
555
|
return bundle;
|
|
374
556
|
}
|
|
375
557
|
|
|
376
558
|
function useWorker(props: {
|
|
377
|
-
name:
|
|
378
|
-
bundle: EsbuildBundle |
|
|
559
|
+
name: undefined | string;
|
|
560
|
+
bundle: EsbuildBundle | undefined;
|
|
379
561
|
format: CfScriptFormat;
|
|
380
562
|
modules: CfModule[];
|
|
381
563
|
accountId: string;
|
|
382
564
|
apiToken: string;
|
|
383
|
-
|
|
384
|
-
|
|
565
|
+
bindings: CfWorkerInit["bindings"];
|
|
566
|
+
assetPaths: undefined | AssetPaths;
|
|
385
567
|
port: number;
|
|
386
|
-
|
|
568
|
+
compatibilityDate: string | undefined;
|
|
569
|
+
compatibilityFlags: string[] | undefined;
|
|
570
|
+
usageModel: undefined | "bundled" | "unbound";
|
|
571
|
+
env: string | undefined;
|
|
572
|
+
}): CfPreviewToken | undefined {
|
|
387
573
|
const {
|
|
388
574
|
name,
|
|
389
575
|
bundle,
|
|
@@ -391,13 +577,24 @@ function useWorker(props: {
|
|
|
391
577
|
modules,
|
|
392
578
|
accountId,
|
|
393
579
|
apiToken,
|
|
394
|
-
|
|
395
|
-
|
|
580
|
+
bindings,
|
|
581
|
+
assetPaths,
|
|
582
|
+
compatibilityDate,
|
|
583
|
+
compatibilityFlags,
|
|
584
|
+
usageModel,
|
|
396
585
|
port,
|
|
397
586
|
} = props;
|
|
398
|
-
const [token, setToken] = useState<CfPreviewToken>();
|
|
587
|
+
const [token, setToken] = useState<CfPreviewToken | undefined>();
|
|
588
|
+
|
|
589
|
+
// This is the most reliable way to detect whether
|
|
590
|
+
// something's "happened" in our system; We make a ref and
|
|
591
|
+
// mark it once we log our initial message. Refs are vars!
|
|
592
|
+
const startedRef = useRef(false);
|
|
593
|
+
|
|
399
594
|
useEffect(() => {
|
|
400
595
|
async function start() {
|
|
596
|
+
setToken(undefined); // reset token in case we're re-running
|
|
597
|
+
|
|
401
598
|
if (!bundle) return;
|
|
402
599
|
if (format === "modules" && bundle.type === "commonjs") {
|
|
403
600
|
console.error("⎔ Cannot use modules with a commonjs bundle.");
|
|
@@ -410,50 +607,56 @@ function useWorker(props: {
|
|
|
410
607
|
return;
|
|
411
608
|
}
|
|
412
609
|
|
|
413
|
-
if (
|
|
414
|
-
|
|
610
|
+
if (!startedRef.current) {
|
|
611
|
+
startedRef.current = true;
|
|
415
612
|
} else {
|
|
416
|
-
console.log("⎔
|
|
613
|
+
console.log("⎔ Detected changes, restarted server.");
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const assets = await syncAssets(
|
|
617
|
+
accountId,
|
|
618
|
+
name || path.basename(bundle.path),
|
|
619
|
+
assetPaths,
|
|
620
|
+
true,
|
|
621
|
+
props.env
|
|
622
|
+
); // TODO: cancellable?
|
|
623
|
+
|
|
624
|
+
const workerType = format || bundle.type === "esm" ? "esm" : "commonjs";
|
|
625
|
+
let content = await readFile(bundle.path, "utf-8");
|
|
626
|
+
if (workerType !== "esm" && assets.manifest) {
|
|
627
|
+
content = `const __STATIC_CONTENT_MANIFEST = ${JSON.stringify(
|
|
628
|
+
assets.manifest
|
|
629
|
+
)};\n${content}`;
|
|
417
630
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const assets = sitesFolder
|
|
421
|
-
? await syncAssets(
|
|
422
|
-
accountId,
|
|
423
|
-
scriptName,
|
|
424
|
-
sitesFolder,
|
|
425
|
-
true,
|
|
426
|
-
undefined // TODO: env
|
|
427
|
-
)
|
|
428
|
-
: {
|
|
429
|
-
manifest: undefined,
|
|
430
|
-
namespace: undefined,
|
|
431
|
-
}; // TODO: cancellable?
|
|
432
|
-
|
|
433
|
-
const content = await readFile(bundle.path, "utf-8");
|
|
631
|
+
|
|
434
632
|
const init: CfWorkerInit = {
|
|
633
|
+
name,
|
|
435
634
|
main: {
|
|
436
|
-
name:
|
|
437
|
-
type:
|
|
438
|
-
format ||
|
|
439
|
-
(bundle.type === "esm" ? "modules" : "service-worker") === "modules"
|
|
440
|
-
? "esm"
|
|
441
|
-
: "commonjs",
|
|
635
|
+
name: path.basename(bundle.path),
|
|
636
|
+
type: workerType,
|
|
442
637
|
content,
|
|
443
638
|
},
|
|
444
|
-
modules:
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
639
|
+
modules: modules.concat(
|
|
640
|
+
assets.manifest && workerType === "esm"
|
|
641
|
+
? {
|
|
642
|
+
name: "__STATIC_CONTENT_MANIFEST",
|
|
643
|
+
content: JSON.stringify(assets.manifest),
|
|
644
|
+
type: "text",
|
|
645
|
+
}
|
|
646
|
+
: []
|
|
647
|
+
),
|
|
648
|
+
bindings: {
|
|
649
|
+
...bindings,
|
|
650
|
+
kv_namespaces: (bindings.kv_namespaces || []).concat(
|
|
651
|
+
assets.namespace
|
|
652
|
+
? { binding: "__STATIC_CONTENT", id: assets.namespace }
|
|
653
|
+
: []
|
|
654
|
+
),
|
|
655
|
+
},
|
|
656
|
+
migrations: undefined, // no migrations in dev
|
|
657
|
+
compatibility_date: compatibilityDate,
|
|
658
|
+
compatibility_flags: compatibilityFlags,
|
|
659
|
+
usage_model: usageModel,
|
|
457
660
|
};
|
|
458
661
|
setToken(
|
|
459
662
|
await createWorker(init, {
|
|
@@ -461,80 +664,28 @@ function useWorker(props: {
|
|
|
461
664
|
apiToken,
|
|
462
665
|
})
|
|
463
666
|
);
|
|
464
|
-
console.log(`⬣ Listening at http://localhost:${port}`);
|
|
465
667
|
}
|
|
466
|
-
start()
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
function useProxy({
|
|
472
|
-
token,
|
|
473
|
-
publicRoot,
|
|
474
|
-
port,
|
|
475
|
-
}: {
|
|
476
|
-
token: CfPreviewToken | void;
|
|
477
|
-
publicRoot: void | string;
|
|
478
|
-
port: number;
|
|
479
|
-
}) {
|
|
480
|
-
useEffect(() => {
|
|
481
|
-
if (!token) return;
|
|
482
|
-
const proxy = httpProxy.createProxyServer({
|
|
483
|
-
secure: false,
|
|
484
|
-
changeOrigin: true,
|
|
485
|
-
headers: {
|
|
486
|
-
"cf-workers-preview-token": token.value,
|
|
487
|
-
},
|
|
488
|
-
target: `https://${token.host}`,
|
|
489
|
-
// TODO: log websockets too? validate durables, etc
|
|
668
|
+
start().catch((err) => {
|
|
669
|
+
// we want to log the error, but not end the process
|
|
670
|
+
// since it could recover after the developer fixes whatever's wrong
|
|
671
|
+
console.error("remote worker:", err);
|
|
490
672
|
});
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
.listen(port); // TODO: custom port
|
|
508
|
-
|
|
509
|
-
proxy.on("proxyRes", function (proxyRes, req, res) {
|
|
510
|
-
// log all requests
|
|
511
|
-
console.log(
|
|
512
|
-
new Date().toLocaleTimeString(),
|
|
513
|
-
req.method,
|
|
514
|
-
req.url,
|
|
515
|
-
res.statusCode // TODO add a status message like Ok etc?
|
|
516
|
-
);
|
|
517
|
-
});
|
|
518
|
-
// TODO: log errors?
|
|
519
|
-
|
|
520
|
-
return () => {
|
|
521
|
-
proxy.close();
|
|
522
|
-
server.close();
|
|
523
|
-
};
|
|
524
|
-
}, [token, publicRoot, port]);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function useInspector(inspectorUrl: string | void) {
|
|
528
|
-
useEffect(() => {
|
|
529
|
-
if (!inspectorUrl) return;
|
|
530
|
-
|
|
531
|
-
const inspector = new DtInspector(inspectorUrl);
|
|
532
|
-
const abortController = inspector.proxyTo(9229);
|
|
533
|
-
return () => {
|
|
534
|
-
inspector.close();
|
|
535
|
-
abortController.abort();
|
|
536
|
-
};
|
|
537
|
-
}, [inspectorUrl]);
|
|
673
|
+
}, [
|
|
674
|
+
name,
|
|
675
|
+
bundle,
|
|
676
|
+
format,
|
|
677
|
+
accountId,
|
|
678
|
+
apiToken,
|
|
679
|
+
port,
|
|
680
|
+
assetPaths,
|
|
681
|
+
compatibilityDate,
|
|
682
|
+
compatibilityFlags,
|
|
683
|
+
usageModel,
|
|
684
|
+
bindings,
|
|
685
|
+
modules,
|
|
686
|
+
props.env,
|
|
687
|
+
]);
|
|
688
|
+
return token;
|
|
538
689
|
}
|
|
539
690
|
|
|
540
691
|
function sleep(period: number) {
|
|
@@ -544,7 +695,7 @@ const SLEEP_DURATION = 2000;
|
|
|
544
695
|
// really need a first class api for this
|
|
545
696
|
const hostNameRegex = /userHostname="(.*)"/g;
|
|
546
697
|
async function findTunnelHostname() {
|
|
547
|
-
let hostName: string;
|
|
698
|
+
let hostName: string | undefined;
|
|
548
699
|
while (!hostName) {
|
|
549
700
|
try {
|
|
550
701
|
const resp = await fetch("http://localhost:8789/metrics");
|
|
@@ -589,19 +740,21 @@ function useTunnel(toggle: boolean) {
|
|
|
589
740
|
}
|
|
590
741
|
});
|
|
591
742
|
|
|
592
|
-
removeSignalExitListener.current = onExit((
|
|
743
|
+
removeSignalExitListener.current = onExit((_code, _signal) => {
|
|
593
744
|
console.log("⎔ Shutting down local tunnel.");
|
|
594
745
|
tunnel.current?.kill();
|
|
595
746
|
tunnel.current = undefined;
|
|
596
747
|
});
|
|
597
748
|
|
|
598
749
|
const hostName = await findTunnelHostname();
|
|
599
|
-
clipboardy.write(hostName);
|
|
750
|
+
await clipboardy.write(hostName);
|
|
600
751
|
console.log(`⬣ Sharing at ${hostName}, copied to clipboard.`);
|
|
601
752
|
}
|
|
602
753
|
}
|
|
603
754
|
|
|
604
|
-
startTunnel()
|
|
755
|
+
startTunnel().catch((err) => {
|
|
756
|
+
console.error("tunnel:", err);
|
|
757
|
+
});
|
|
605
758
|
|
|
606
759
|
return () => {
|
|
607
760
|
if (tunnel.current) {
|
|
@@ -623,41 +776,39 @@ function useHotkeys(initial: useHotkeysInitialState, port: number) {
|
|
|
623
776
|
// UGH, we should put port in context instead
|
|
624
777
|
const [toggles, setToggles] = useState(initial);
|
|
625
778
|
useInput(
|
|
626
|
-
(
|
|
779
|
+
async (
|
|
627
780
|
input,
|
|
628
781
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
629
782
|
key
|
|
630
783
|
) => {
|
|
631
|
-
switch (input) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
// {
|
|
636
|
-
// app: {
|
|
637
|
-
// name: open.apps.chrome, // TODO: fallback on other browsers
|
|
638
|
-
// },
|
|
639
|
-
// }
|
|
640
|
-
);
|
|
784
|
+
switch (input.toLowerCase()) {
|
|
785
|
+
// open browser
|
|
786
|
+
case "b":
|
|
787
|
+
await open(`http://localhost:${port}/`);
|
|
641
788
|
break;
|
|
642
|
-
|
|
643
|
-
|
|
789
|
+
// toggle inspector
|
|
790
|
+
case "d":
|
|
791
|
+
await open(
|
|
644
792
|
`https://built-devtools.pages.dev/js_app?experiments=true&v8only=true&ws=localhost:9229/ws`
|
|
645
|
-
// {
|
|
646
|
-
// app: {
|
|
647
|
-
// name: open.apps.chrome,
|
|
648
|
-
// // todo - add firefox and edge fallbacks
|
|
649
|
-
// },
|
|
650
|
-
// }
|
|
651
793
|
);
|
|
652
794
|
break;
|
|
653
|
-
|
|
654
|
-
|
|
795
|
+
// toggle tunnel
|
|
796
|
+
case "s":
|
|
797
|
+
setToggles((previousToggles) => ({
|
|
798
|
+
...previousToggles,
|
|
799
|
+
tunnel: !previousToggles.tunnel,
|
|
800
|
+
}));
|
|
655
801
|
break;
|
|
656
|
-
|
|
657
|
-
|
|
802
|
+
// toggle local
|
|
803
|
+
case "l":
|
|
804
|
+
setToggles((previousToggles) => ({
|
|
805
|
+
...previousToggles,
|
|
806
|
+
local: !previousToggles.local,
|
|
807
|
+
}));
|
|
658
808
|
break;
|
|
659
|
-
|
|
660
|
-
case "
|
|
809
|
+
// shut down
|
|
810
|
+
case "q":
|
|
811
|
+
case "x":
|
|
661
812
|
process.exit(0);
|
|
662
813
|
break;
|
|
663
814
|
default:
|
|
@@ -668,3 +819,16 @@ function useHotkeys(initial: useHotkeysInitialState, port: number) {
|
|
|
668
819
|
);
|
|
669
820
|
return toggles;
|
|
670
821
|
}
|
|
822
|
+
|
|
823
|
+
function ErrorFallback(props: { error: Error }) {
|
|
824
|
+
const { exit } = useApp();
|
|
825
|
+
useEffect(() => exit(new Error()));
|
|
826
|
+
return (
|
|
827
|
+
<>
|
|
828
|
+
<Text>Something went wrong:</Text>
|
|
829
|
+
<Text>{props.error.stack}</Text>
|
|
830
|
+
</>
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
export default withErrorBoundary(Dev, { FallbackComponent: ErrorFallback });
|