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