wrangler 0.0.0-e043479 → 0.0.0-e0b9366
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 +10 -4
- package/pages/functions/filepath-routing.test.ts +125 -25
- package/pages/functions/filepath-routing.ts +6 -3
- package/src/__tests__/configuration.test.ts +479 -114
- package/src/__tests__/dev.test.tsx +162 -8
- package/src/__tests__/dev2.test.tsx +152 -0
- package/src/__tests__/guess-worker-format.test.ts +9 -17
- package/src/__tests__/helpers/mock-account-id.ts +4 -0
- package/src/__tests__/helpers/mock-cfetch.ts +3 -3
- package/src/__tests__/helpers/mock-kv.ts +9 -16
- package/src/__tests__/helpers/run-in-tmp.ts +3 -0
- package/src/__tests__/https-options.test.ts +127 -0
- package/src/__tests__/index.test.ts +125 -0
- package/src/__tests__/jest.setup.ts +1 -1
- package/src/__tests__/kv.test.ts +27 -53
- package/src/__tests__/logout.test.ts +15 -13
- package/src/__tests__/pages.test.ts +44 -0
- package/src/__tests__/publish.test.ts +504 -32
- package/src/__tests__/route.test.ts +45 -0
- package/src/__tests__/whoami.test.tsx +21 -7
- package/src/bundle.ts +20 -11
- package/src/cfetch/internal.ts +11 -5
- package/src/config/config.ts +53 -44
- package/src/config/validation-helpers.ts +110 -12
- package/src/config/validation.ts +59 -41
- package/src/{api/preview.ts → create-worker-preview.ts} +23 -4
- package/src/{api/form_data.ts → create-worker-upload-form.ts} +1 -1
- package/src/dev/dev.tsx +357 -0
- package/src/dev/local.tsx +241 -0
- package/src/dev/remote.tsx +201 -0
- package/src/dev/use-esbuild.ts +100 -0
- package/src/entry.ts +111 -9
- package/src/https-options.ts +122 -0
- package/src/index.tsx +171 -109
- package/src/inspect.ts +9 -1
- package/src/{kv.tsx → kv.ts} +0 -0
- package/src/module-collection.ts +85 -2
- package/src/open-in-browser.ts +14 -2
- package/src/pages.tsx +1 -1
- package/src/proxy.ts +102 -40
- package/src/publish.ts +41 -50
- package/src/reporting.ts +25 -9
- package/src/selfsigned.d.ts +29 -0
- package/src/sites.tsx +51 -16
- package/src/user.tsx +132 -77
- package/src/{api/worker.ts → worker.ts} +0 -23
- package/wrangler-dist/cli.js +5012 -3107
- package/wrangler-dist/cli.js.map +3 -3
- package/src/__tests__/helpers/mock-user.ts +0 -27
- package/src/dev.tsx +0 -960
- package/src/guess-worker-format.ts +0 -68
package/src/config/validation.ts
CHANGED
|
@@ -19,8 +19,17 @@ import {
|
|
|
19
19
|
validateOptionalTypedArray,
|
|
20
20
|
validateRequiredProperty,
|
|
21
21
|
validateTypedArray,
|
|
22
|
+
all,
|
|
23
|
+
isMutuallyExclusiveWith,
|
|
24
|
+
inheritableInLegacyEnvironments,
|
|
22
25
|
} from "./validation-helpers";
|
|
23
|
-
import type {
|
|
26
|
+
import type {
|
|
27
|
+
Config,
|
|
28
|
+
DeprecatedUpload,
|
|
29
|
+
DevConfig,
|
|
30
|
+
RawConfig,
|
|
31
|
+
RawDevConfig,
|
|
32
|
+
} from "./config";
|
|
24
33
|
import type { RawEnvironment, Environment, Rule } from "./environment";
|
|
25
34
|
import type { ValidatorFn } from "./validation-helpers";
|
|
26
35
|
|
|
@@ -61,6 +70,15 @@ export function normalizeAndValidateConfig(
|
|
|
61
70
|
true
|
|
62
71
|
);
|
|
63
72
|
|
|
73
|
+
deprecated(
|
|
74
|
+
diagnostics,
|
|
75
|
+
rawConfig,
|
|
76
|
+
`site.entry-point`,
|
|
77
|
+
`The \`site.entry-point\` config field is no longer used.\nThe entry-point should be specified via the command line or the \`main\` config field.`,
|
|
78
|
+
false,
|
|
79
|
+
true
|
|
80
|
+
);
|
|
81
|
+
|
|
64
82
|
const { deprecatedUpload, ...build } = normalizeAndValidateBuild(
|
|
65
83
|
diagnostics,
|
|
66
84
|
rawConfig,
|
|
@@ -136,7 +154,7 @@ export function normalizeAndValidateConfig(
|
|
|
136
154
|
diagnostics,
|
|
137
155
|
"top-level",
|
|
138
156
|
Object.keys(rawConfig),
|
|
139
|
-
Object.keys(config)
|
|
157
|
+
[...Object.keys(config), "miniflare"]
|
|
140
158
|
);
|
|
141
159
|
|
|
142
160
|
return { config, diagnostics };
|
|
@@ -222,7 +240,7 @@ function normalizeAndValidateMainField(
|
|
|
222
240
|
return path.resolve(directory, rawMain);
|
|
223
241
|
} else {
|
|
224
242
|
diagnostics.errors.push(
|
|
225
|
-
`
|
|
243
|
+
`Expected "main" to be a string but got ${JSON.stringify(rawMain)}`
|
|
226
244
|
);
|
|
227
245
|
return;
|
|
228
246
|
}
|
|
@@ -239,8 +257,8 @@ function normalizeAndValidateMainField(
|
|
|
239
257
|
*/
|
|
240
258
|
function normalizeAndValidateDev(
|
|
241
259
|
diagnostics: Diagnostics,
|
|
242
|
-
rawDev:
|
|
243
|
-
):
|
|
260
|
+
rawDev: RawDevConfig
|
|
261
|
+
): DevConfig {
|
|
244
262
|
const {
|
|
245
263
|
ip = "127.0.0.1",
|
|
246
264
|
port = 8787,
|
|
@@ -258,14 +276,16 @@ function normalizeAndValidateDev(
|
|
|
258
276
|
"dev",
|
|
259
277
|
"local_protocol",
|
|
260
278
|
local_protocol,
|
|
261
|
-
"string"
|
|
279
|
+
"string",
|
|
280
|
+
["http", "https"]
|
|
262
281
|
);
|
|
263
282
|
validateOptionalProperty(
|
|
264
283
|
diagnostics,
|
|
265
284
|
"dev",
|
|
266
285
|
"upstream_protocol",
|
|
267
286
|
upstream_protocol,
|
|
268
|
-
"string"
|
|
287
|
+
"string",
|
|
288
|
+
["http", "https"]
|
|
269
289
|
);
|
|
270
290
|
validateOptionalProperty(diagnostics, "dev", "host", host, "string");
|
|
271
291
|
return { ip, port, local_protocol, upstream_protocol, host };
|
|
@@ -304,7 +324,7 @@ function normalizeAndValidateMigrations(
|
|
|
304
324
|
if (migration.renamed_classes !== undefined) {
|
|
305
325
|
if (!Array.isArray(migration.renamed_classes)) {
|
|
306
326
|
diagnostics.errors.push(
|
|
307
|
-
`"migrations[${i}].renamed_classes"
|
|
327
|
+
`Expected "migrations[${i}].renamed_classes" to be an array of "{from: string, to: string}" objects but got ${JSON.stringify(
|
|
308
328
|
migration.renamed_classes
|
|
309
329
|
)}.`
|
|
310
330
|
);
|
|
@@ -317,7 +337,7 @@ function normalizeAndValidateMigrations(
|
|
|
317
337
|
)
|
|
318
338
|
) {
|
|
319
339
|
diagnostics.errors.push(
|
|
320
|
-
`"migrations[${i}].renamed_classes"
|
|
340
|
+
`Expected "migrations[${i}].renamed_classes" to be an array of "{from: string, to: string}" objects but got ${JSON.stringify(
|
|
321
341
|
migration.renamed_classes
|
|
322
342
|
)}.`
|
|
323
343
|
);
|
|
@@ -342,25 +362,12 @@ function normalizeAndValidateSite(
|
|
|
342
362
|
rawSite: Config["site"]
|
|
343
363
|
): Config["site"] {
|
|
344
364
|
if (rawSite !== undefined) {
|
|
345
|
-
const {
|
|
346
|
-
bucket,
|
|
347
|
-
"entry-point": entryPoint,
|
|
348
|
-
include = [],
|
|
349
|
-
exclude = [],
|
|
350
|
-
...rest
|
|
351
|
-
} = rawSite;
|
|
365
|
+
const { bucket, include = [], exclude = [], ...rest } = rawSite;
|
|
352
366
|
validateAdditionalProperties(diagnostics, "site", Object.keys(rest), []);
|
|
353
367
|
validateRequiredProperty(diagnostics, "site", "bucket", bucket, "string");
|
|
354
|
-
validateOptionalProperty(
|
|
355
|
-
diagnostics,
|
|
356
|
-
"site",
|
|
357
|
-
"entry_point",
|
|
358
|
-
entryPoint,
|
|
359
|
-
"string"
|
|
360
|
-
);
|
|
361
368
|
validateTypedArray(diagnostics, "sites.include", include, "string");
|
|
362
369
|
validateTypedArray(diagnostics, "sites.exclude", exclude, "string");
|
|
363
|
-
return { bucket,
|
|
370
|
+
return { bucket, include, exclude };
|
|
364
371
|
}
|
|
365
372
|
return undefined;
|
|
366
373
|
}
|
|
@@ -470,7 +477,7 @@ function normalizeAndValidateEnvironment(
|
|
|
470
477
|
config,
|
|
471
478
|
rawEnv,
|
|
472
479
|
"routes",
|
|
473
|
-
isStringArray,
|
|
480
|
+
all(isStringArray, isMutuallyExclusiveWith(rawEnv, "route")),
|
|
474
481
|
undefined
|
|
475
482
|
);
|
|
476
483
|
const workers_dev = inheritable(
|
|
@@ -531,7 +538,14 @@ function normalizeAndValidateEnvironment(
|
|
|
531
538
|
deprecatedRules,
|
|
532
539
|
envName
|
|
533
540
|
),
|
|
534
|
-
name:
|
|
541
|
+
name: inheritableInLegacyEnvironments(
|
|
542
|
+
diagnostics,
|
|
543
|
+
config,
|
|
544
|
+
rawEnv,
|
|
545
|
+
"name",
|
|
546
|
+
isString,
|
|
547
|
+
undefined
|
|
548
|
+
),
|
|
535
549
|
route,
|
|
536
550
|
routes,
|
|
537
551
|
triggers: inheritable(
|
|
@@ -623,7 +637,7 @@ const validateAndNormalizeRules = (
|
|
|
623
637
|
// Only create errors/warnings for the top-level environment
|
|
624
638
|
if (rawEnv.rules && deprecatedRules) {
|
|
625
639
|
diagnostics.errors.push(
|
|
626
|
-
`
|
|
640
|
+
`You cannot configure both [rules] and [build.upload.rules] in your wrangler.toml. Delete the \`build.upload\` section.`
|
|
627
641
|
);
|
|
628
642
|
} else if (deprecatedRules) {
|
|
629
643
|
diagnostics.warnings.push(
|
|
@@ -673,7 +687,9 @@ const validateRules =
|
|
|
673
687
|
|
|
674
688
|
const validateRule: ValidatorFn = (diagnostics, field, value) => {
|
|
675
689
|
if (typeof value !== "object" || value === null) {
|
|
676
|
-
diagnostics.errors.push(
|
|
690
|
+
diagnostics.errors.push(
|
|
691
|
+
`"${field}" should be an object but got ${JSON.stringify(value)}.`
|
|
692
|
+
);
|
|
677
693
|
return false;
|
|
678
694
|
}
|
|
679
695
|
// Rules must have a type string and glob string array, and optionally a fallthrough boolean.
|
|
@@ -690,7 +706,7 @@ const validateRule: ValidatorFn = (diagnostics, field, value) => {
|
|
|
690
706
|
])
|
|
691
707
|
) {
|
|
692
708
|
diagnostics.errors.push(
|
|
693
|
-
`
|
|
709
|
+
`bindings should have a string "type" field, which contains one of "ESModule", "CommonJS", "CompiledWasm", "Text", or "Data".`
|
|
694
710
|
);
|
|
695
711
|
isValid = false;
|
|
696
712
|
}
|
|
@@ -701,7 +717,7 @@ const validateRule: ValidatorFn = (diagnostics, field, value) => {
|
|
|
701
717
|
|
|
702
718
|
if (!isOptionalProperty(rule, "fallthrough", "boolean")) {
|
|
703
719
|
diagnostics.errors.push(
|
|
704
|
-
`
|
|
720
|
+
`binding should, optionally, have a boolean "fallthrough" field.`
|
|
705
721
|
);
|
|
706
722
|
isValid = false;
|
|
707
723
|
}
|
|
@@ -820,29 +836,29 @@ const getBindingNames = (value: unknown): string[] =>
|
|
|
820
836
|
*/
|
|
821
837
|
const validateDurableObjectBinding: ValidatorFn = (
|
|
822
838
|
diagnostics,
|
|
823
|
-
|
|
839
|
+
field,
|
|
824
840
|
value
|
|
825
841
|
) => {
|
|
826
842
|
if (typeof value !== "object" || value === null) {
|
|
827
|
-
diagnostics.errors.push(
|
|
843
|
+
diagnostics.errors.push(
|
|
844
|
+
`Expected "${field}" to be an object but got ${JSON.stringify(value)}`
|
|
845
|
+
);
|
|
828
846
|
return false;
|
|
829
847
|
}
|
|
830
848
|
|
|
831
849
|
// Durable Object bindings must have a name and class_name, and optionally a script_name.
|
|
832
850
|
let isValid = true;
|
|
833
851
|
if (!isRequiredProperty(value, "name", "string")) {
|
|
834
|
-
diagnostics.errors.push(
|
|
835
|
-
`ERROR: bindings should have a string "name" field.`
|
|
836
|
-
);
|
|
852
|
+
diagnostics.errors.push(`binding should have a string "name" field.`);
|
|
837
853
|
isValid = false;
|
|
838
854
|
}
|
|
839
855
|
if (!isRequiredProperty(value, "class_name", "string")) {
|
|
840
|
-
diagnostics.errors.push(`
|
|
856
|
+
diagnostics.errors.push(`binding should have a string "class_name" field.`);
|
|
841
857
|
isValid = false;
|
|
842
858
|
}
|
|
843
859
|
if (!isOptionalProperty(value, "script_name", "string")) {
|
|
844
860
|
diagnostics.errors.push(
|
|
845
|
-
`
|
|
861
|
+
`binding should, optionally, have a string "script_name" field.`
|
|
846
862
|
);
|
|
847
863
|
isValid = false;
|
|
848
864
|
}
|
|
@@ -855,16 +871,18 @@ const validateDurableObjectBinding: ValidatorFn = (
|
|
|
855
871
|
*
|
|
856
872
|
* TODO: further validation of known unsafe bindings.
|
|
857
873
|
*/
|
|
858
|
-
const validateUnsafeBinding: ValidatorFn = (diagnostics,
|
|
874
|
+
const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => {
|
|
859
875
|
if (typeof value !== "object" || value === null) {
|
|
860
|
-
diagnostics.errors.push(
|
|
876
|
+
diagnostics.errors.push(
|
|
877
|
+
`Expected ${field} to be an object but got ${JSON.stringify(value)}.`
|
|
878
|
+
);
|
|
861
879
|
return false;
|
|
862
880
|
}
|
|
863
881
|
|
|
864
882
|
let isValid = true;
|
|
865
883
|
// Unsafe bindings must have a name and type.
|
|
866
884
|
if (!isRequiredProperty(value, "name", "string")) {
|
|
867
|
-
diagnostics.errors.push(`
|
|
885
|
+
diagnostics.errors.push(`binding should have a string "name" field.`);
|
|
868
886
|
isValid = false;
|
|
869
887
|
}
|
|
870
888
|
if (isRequiredProperty(value, "type", "string")) {
|
|
@@ -883,7 +901,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, _field, value) => {
|
|
|
883
901
|
);
|
|
884
902
|
}
|
|
885
903
|
} else {
|
|
886
|
-
diagnostics.errors.push(`
|
|
904
|
+
diagnostics.errors.push(`binding should have a string "type" field.`);
|
|
887
905
|
isValid = false;
|
|
888
906
|
}
|
|
889
907
|
return isValid;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { URL } from "node:url";
|
|
2
2
|
import { fetch } from "undici";
|
|
3
|
-
import { fetchResult } from "
|
|
4
|
-
import {
|
|
3
|
+
import { fetchResult } from "./cfetch";
|
|
4
|
+
import { createWorkerUploadForm } from "./create-worker-upload-form";
|
|
5
5
|
import type { CfAccount, CfWorkerContext, CfWorkerInit } from "./worker";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -93,7 +93,7 @@ function randomId(): string {
|
|
|
93
93
|
/**
|
|
94
94
|
* Creates a preview token.
|
|
95
95
|
*/
|
|
96
|
-
|
|
96
|
+
async function createPreviewToken(
|
|
97
97
|
account: CfAccount,
|
|
98
98
|
worker: CfWorkerInit,
|
|
99
99
|
ctx: CfWorkerContext
|
|
@@ -114,7 +114,7 @@ export async function previewToken(
|
|
|
114
114
|
? { routes: ["*/*"] } // TODO: should we support routes here? how?
|
|
115
115
|
: { workers_dev: true };
|
|
116
116
|
|
|
117
|
-
const formData =
|
|
117
|
+
const formData = createWorkerUploadForm(worker);
|
|
118
118
|
formData.set("wrangler-session-config", JSON.stringify(mode));
|
|
119
119
|
|
|
120
120
|
const { preview_token } = await fetchResult<{ preview_token: string }>(url, {
|
|
@@ -145,3 +145,22 @@ export async function previewToken(
|
|
|
145
145
|
prewarmUrl,
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* A stub to create a Cloudflare Worker preview.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* const {value, host} = await createWorker(init, acct);
|
|
154
|
+
*/
|
|
155
|
+
export async function createWorkerPreview(
|
|
156
|
+
init: CfWorkerInit,
|
|
157
|
+
account: CfAccount,
|
|
158
|
+
ctx: CfWorkerContext
|
|
159
|
+
): Promise<CfPreviewToken> {
|
|
160
|
+
const token = await createPreviewToken(account, init, ctx);
|
|
161
|
+
const response = await fetch(token.prewarmUrl.href, { method: "POST" });
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
// console.error("worker failed to prewarm: ", response.statusText);
|
|
164
|
+
}
|
|
165
|
+
return token;
|
|
166
|
+
}
|
|
@@ -51,7 +51,7 @@ export interface WorkerMetadata {
|
|
|
51
51
|
/**
|
|
52
52
|
* Creates a `FormData` upload from a `CfWorkerInit`.
|
|
53
53
|
*/
|
|
54
|
-
export function
|
|
54
|
+
export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
|
|
55
55
|
const formData = new FormData();
|
|
56
56
|
const {
|
|
57
57
|
main,
|
package/src/dev/dev.tsx
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { watch } from "chokidar";
|
|
3
|
+
import clipboardy from "clipboardy";
|
|
4
|
+
import commandExists from "command-exists";
|
|
5
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
6
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
7
|
+
import { withErrorBoundary, useErrorHandler } from "react-error-boundary";
|
|
8
|
+
import onExit from "signal-exit";
|
|
9
|
+
import tmp from "tmp-promise";
|
|
10
|
+
import { fetch } from "undici";
|
|
11
|
+
import { runCustomBuild } from "../entry";
|
|
12
|
+
import openInBrowser from "../open-in-browser";
|
|
13
|
+
import { reportError } from "../reporting";
|
|
14
|
+
import { getAPIToken } from "../user";
|
|
15
|
+
import { Local } from "./local";
|
|
16
|
+
import { Remote } from "./remote";
|
|
17
|
+
import { useEsbuild } from "./use-esbuild";
|
|
18
|
+
import type { Config } from "../config";
|
|
19
|
+
import type { Entry } from "../entry";
|
|
20
|
+
import type { AssetPaths } from "../sites";
|
|
21
|
+
import type { CfWorkerInit } from "../worker";
|
|
22
|
+
|
|
23
|
+
export type DevProps = {
|
|
24
|
+
name?: string;
|
|
25
|
+
entry: Entry;
|
|
26
|
+
port?: number;
|
|
27
|
+
inspectorPort: number;
|
|
28
|
+
rules: Config["rules"];
|
|
29
|
+
accountId: undefined | string;
|
|
30
|
+
initialMode: "local" | "remote";
|
|
31
|
+
jsxFactory: undefined | string;
|
|
32
|
+
jsxFragment: undefined | string;
|
|
33
|
+
upstreamProtocol: "https" | "http";
|
|
34
|
+
localProtocol: "https" | "http";
|
|
35
|
+
enableLocalPersistence: boolean;
|
|
36
|
+
bindings: CfWorkerInit["bindings"];
|
|
37
|
+
public: undefined | string;
|
|
38
|
+
assetPaths: undefined | AssetPaths;
|
|
39
|
+
compatibilityDate: undefined | string;
|
|
40
|
+
compatibilityFlags: undefined | string[];
|
|
41
|
+
usageModel: undefined | "bundled" | "unbound";
|
|
42
|
+
buildCommand: {
|
|
43
|
+
command?: undefined | string;
|
|
44
|
+
cwd?: undefined | string;
|
|
45
|
+
watch_dir?: undefined | string;
|
|
46
|
+
};
|
|
47
|
+
env: string | undefined;
|
|
48
|
+
legacyEnv: boolean;
|
|
49
|
+
zone:
|
|
50
|
+
| {
|
|
51
|
+
id: string;
|
|
52
|
+
host: string;
|
|
53
|
+
}
|
|
54
|
+
| undefined;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function DevImplementation(props: DevProps): JSX.Element {
|
|
58
|
+
const port = props.port ?? 8787;
|
|
59
|
+
const apiToken = props.initialMode === "remote" ? getAPIToken() : undefined;
|
|
60
|
+
const directory = useTmpDir();
|
|
61
|
+
|
|
62
|
+
useCustomBuild(props.entry, props.buildCommand);
|
|
63
|
+
|
|
64
|
+
if (props.public && props.entry.format === "service-worker") {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"You cannot use the service worker format with a `public` directory."
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (props.bindings.wasm_modules && props.entry.format === "modules") {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"You cannot configure [wasm_modules] with an ES module worker. Instead, import the .wasm module directly in your code"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (props.bindings.text_blobs && props.entry.format === "modules") {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"You cannot configure [text_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure `[build.upload.rules]` in your wrangler.toml"
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const bundle = useEsbuild({
|
|
83
|
+
entry: props.entry,
|
|
84
|
+
destination: directory,
|
|
85
|
+
staticRoot: props.public,
|
|
86
|
+
jsxFactory: props.jsxFactory,
|
|
87
|
+
rules: props.rules,
|
|
88
|
+
jsxFragment: props.jsxFragment,
|
|
89
|
+
serveAssetsFromWorker: !!props.public,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const toggles = useHotkeys(
|
|
93
|
+
{
|
|
94
|
+
local: props.initialMode === "local",
|
|
95
|
+
tunnel: false,
|
|
96
|
+
},
|
|
97
|
+
port,
|
|
98
|
+
props.inspectorPort,
|
|
99
|
+
props.localProtocol
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
useTunnel(toggles.tunnel);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<>
|
|
106
|
+
{toggles.local ? (
|
|
107
|
+
<Local
|
|
108
|
+
name={props.name}
|
|
109
|
+
bundle={bundle}
|
|
110
|
+
format={props.entry.format}
|
|
111
|
+
bindings={props.bindings}
|
|
112
|
+
assetPaths={props.assetPaths}
|
|
113
|
+
public={props.public}
|
|
114
|
+
port={port}
|
|
115
|
+
rules={props.rules}
|
|
116
|
+
inspectorPort={props.inspectorPort}
|
|
117
|
+
enableLocalPersistence={props.enableLocalPersistence}
|
|
118
|
+
/>
|
|
119
|
+
) : (
|
|
120
|
+
<Remote
|
|
121
|
+
name={props.name}
|
|
122
|
+
bundle={bundle}
|
|
123
|
+
format={props.entry.format}
|
|
124
|
+
accountId={props.accountId}
|
|
125
|
+
apiToken={apiToken}
|
|
126
|
+
bindings={props.bindings}
|
|
127
|
+
assetPaths={props.assetPaths}
|
|
128
|
+
public={props.public}
|
|
129
|
+
port={port}
|
|
130
|
+
localProtocol={props.localProtocol}
|
|
131
|
+
inspectorPort={props.inspectorPort}
|
|
132
|
+
compatibilityDate={props.compatibilityDate}
|
|
133
|
+
compatibilityFlags={props.compatibilityFlags}
|
|
134
|
+
usageModel={props.usageModel}
|
|
135
|
+
env={props.env}
|
|
136
|
+
legacyEnv={props.legacyEnv}
|
|
137
|
+
zone={props.zone}
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
<Box borderStyle="round" paddingLeft={1} paddingRight={1}>
|
|
141
|
+
<Text>
|
|
142
|
+
{`B to open a browser, D to open Devtools, L to ${
|
|
143
|
+
toggles.local ? "turn off" : "turn on"
|
|
144
|
+
} local mode, X to exit`}
|
|
145
|
+
</Text>
|
|
146
|
+
</Box>
|
|
147
|
+
</>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface DirectorySyncResult {
|
|
152
|
+
name: string;
|
|
153
|
+
removeCallback: () => void;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function useTmpDir(): string | undefined {
|
|
157
|
+
const [directory, setDirectory] = useState<DirectorySyncResult>();
|
|
158
|
+
const handleError = useErrorHandler();
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
let dir: DirectorySyncResult | undefined;
|
|
161
|
+
try {
|
|
162
|
+
dir = tmp.dirSync({ unsafeCleanup: true });
|
|
163
|
+
setDirectory(dir);
|
|
164
|
+
return;
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.error("failed to create tmp dir");
|
|
167
|
+
handleError(err);
|
|
168
|
+
}
|
|
169
|
+
return () => {
|
|
170
|
+
dir?.removeCallback();
|
|
171
|
+
};
|
|
172
|
+
}, [handleError]);
|
|
173
|
+
return directory?.name;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function useCustomBuild(
|
|
177
|
+
expectedEntry: Entry,
|
|
178
|
+
build: {
|
|
179
|
+
command?: undefined | string;
|
|
180
|
+
cwd?: undefined | string;
|
|
181
|
+
watch_dir?: undefined | string;
|
|
182
|
+
}
|
|
183
|
+
): void {
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (!build.command) return;
|
|
186
|
+
let watcher: ReturnType<typeof watch> | undefined;
|
|
187
|
+
if (build.watch_dir) {
|
|
188
|
+
watcher = watch(build.watch_dir, {
|
|
189
|
+
persistent: true,
|
|
190
|
+
ignoreInitial: true,
|
|
191
|
+
}).on("all", (_event, filePath) => {
|
|
192
|
+
//TODO: we should buffer requests to the proxy until this completes
|
|
193
|
+
console.log(`The file ${filePath} changed, restarting build...`);
|
|
194
|
+
runCustomBuild(expectedEntry.file, build).catch((err) => {
|
|
195
|
+
console.error("Custom build failed:", err);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return () => {
|
|
201
|
+
watcher?.close();
|
|
202
|
+
};
|
|
203
|
+
}, [build, expectedEntry.file]);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function sleep(period: number) {
|
|
207
|
+
return new Promise((resolve) => setTimeout(resolve, period));
|
|
208
|
+
}
|
|
209
|
+
const SLEEP_DURATION = 2000;
|
|
210
|
+
// really need a first class api for this
|
|
211
|
+
const hostNameRegex = /userHostname="(.*)"/g;
|
|
212
|
+
async function findTunnelHostname() {
|
|
213
|
+
let hostName: string | undefined;
|
|
214
|
+
while (!hostName) {
|
|
215
|
+
try {
|
|
216
|
+
const resp = await fetch("http://localhost:8789/metrics");
|
|
217
|
+
const data = await resp.text();
|
|
218
|
+
const matches = Array.from(data.matchAll(hostNameRegex));
|
|
219
|
+
hostName = matches[0][1];
|
|
220
|
+
} catch (err) {
|
|
221
|
+
await sleep(SLEEP_DURATION);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return hostName;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Create a tunnel to the remote worker.
|
|
229
|
+
* We've disabled this for now until we figure out a better user experience.
|
|
230
|
+
*/
|
|
231
|
+
function useTunnel(toggle: boolean) {
|
|
232
|
+
const tunnel = useRef<ReturnType<typeof spawn>>();
|
|
233
|
+
const removeSignalExitListener = useRef<() => void>();
|
|
234
|
+
// TODO: test if cloudflared is available, if not
|
|
235
|
+
// point them to a url where they can get docs to install it
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
async function startTunnel() {
|
|
238
|
+
if (toggle) {
|
|
239
|
+
try {
|
|
240
|
+
await commandExists("cloudflared");
|
|
241
|
+
} catch (e) {
|
|
242
|
+
console.error(
|
|
243
|
+
"To share your worker on the Internet, please install `cloudflared` from https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation"
|
|
244
|
+
);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
console.log("⎔ Starting a tunnel...");
|
|
248
|
+
tunnel.current = spawn("cloudflared", [
|
|
249
|
+
"tunnel",
|
|
250
|
+
"--url",
|
|
251
|
+
"http://localhost:8787",
|
|
252
|
+
"--metrics",
|
|
253
|
+
"localhost:8789",
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
tunnel.current.on("close", (code) => {
|
|
257
|
+
if (code) {
|
|
258
|
+
console.log(`Tunnel process exited with code ${code}`);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
removeSignalExitListener.current = onExit((_code, _signal) => {
|
|
263
|
+
console.log("⎔ Shutting down local tunnel.");
|
|
264
|
+
tunnel.current?.kill();
|
|
265
|
+
tunnel.current = undefined;
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const hostName = await findTunnelHostname();
|
|
269
|
+
await clipboardy.write(hostName);
|
|
270
|
+
console.log(`⬣ Sharing at ${hostName}, copied to clipboard.`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
startTunnel().catch(async (err) => {
|
|
275
|
+
console.error("tunnel:", err);
|
|
276
|
+
await reportError(err);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return () => {
|
|
280
|
+
if (tunnel.current) {
|
|
281
|
+
console.log("⎔ Shutting down tunnel.");
|
|
282
|
+
tunnel.current?.kill();
|
|
283
|
+
tunnel.current = undefined;
|
|
284
|
+
removeSignalExitListener.current && removeSignalExitListener.current();
|
|
285
|
+
removeSignalExitListener.current = undefined;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}, [toggle]);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
type useHotkeysInitialState = {
|
|
292
|
+
local: boolean;
|
|
293
|
+
tunnel: boolean;
|
|
294
|
+
};
|
|
295
|
+
function useHotkeys(
|
|
296
|
+
initial: useHotkeysInitialState,
|
|
297
|
+
port: number,
|
|
298
|
+
inspectorPort: number,
|
|
299
|
+
localProtocol: "http" | "https"
|
|
300
|
+
) {
|
|
301
|
+
// UGH, we should put port in context instead
|
|
302
|
+
const [toggles, setToggles] = useState(initial);
|
|
303
|
+
useInput(
|
|
304
|
+
async (
|
|
305
|
+
input,
|
|
306
|
+
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
307
|
+
key
|
|
308
|
+
) => {
|
|
309
|
+
switch (input.toLowerCase()) {
|
|
310
|
+
// open browser
|
|
311
|
+
case "b": {
|
|
312
|
+
await openInBrowser(`${localProtocol}://localhost:${port}`);
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
// toggle inspector
|
|
316
|
+
case "d": {
|
|
317
|
+
await openInBrowser(
|
|
318
|
+
`https://built-devtools.pages.dev/js_app?experiments=true&v8only=true&ws=localhost:${inspectorPort}/ws`,
|
|
319
|
+
{ forceChromium: true }
|
|
320
|
+
);
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
// toggle local
|
|
324
|
+
case "l":
|
|
325
|
+
setToggles((previousToggles) => ({
|
|
326
|
+
...previousToggles,
|
|
327
|
+
local: !previousToggles.local,
|
|
328
|
+
}));
|
|
329
|
+
break;
|
|
330
|
+
// shut down
|
|
331
|
+
case "q":
|
|
332
|
+
case "x":
|
|
333
|
+
process.exit(0);
|
|
334
|
+
break;
|
|
335
|
+
default:
|
|
336
|
+
// nothing?
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
);
|
|
341
|
+
return toggles;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function ErrorFallback(props: { error: Error }) {
|
|
345
|
+
const { exit } = useApp();
|
|
346
|
+
useEffect(() => exit(props.error));
|
|
347
|
+
return (
|
|
348
|
+
<>
|
|
349
|
+
<Text>Something went wrong:</Text>
|
|
350
|
+
<Text>{props.error.stack}</Text>
|
|
351
|
+
</>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export default withErrorBoundary(DevImplementation, {
|
|
356
|
+
FallbackComponent: ErrorFallback,
|
|
357
|
+
});
|