wrangler 2.0.6 → 2.0.9
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 +1 -1
- package/bin/wrangler.js +16 -4
- package/package.json +6 -4
- package/pages/functions/buildPlugin.ts +13 -0
- package/pages/functions/buildWorker.ts +13 -0
- package/src/__tests__/configuration.test.ts +132 -60
- package/src/__tests__/dev.test.tsx +168 -67
- package/src/__tests__/helpers/mock-dialogs.ts +41 -1
- package/src/__tests__/index.test.ts +25 -10
- package/src/__tests__/init.test.ts +252 -131
- package/src/__tests__/kv.test.ts +16 -16
- package/src/__tests__/package-manager.test.ts +154 -7
- package/src/__tests__/pages.test.ts +442 -38
- package/src/__tests__/parse.test.ts +5 -1
- package/src/__tests__/publish.test.ts +377 -84
- package/src/__tests__/secret.test.ts +4 -4
- package/src/__tests__/whoami.test.tsx +34 -0
- package/src/abort.d.ts +3 -0
- package/src/cfetch/index.ts +21 -4
- package/src/cfetch/internal.ts +20 -18
- package/src/config/config.ts +1 -1
- package/src/config/index.ts +162 -0
- package/src/config/validation.ts +77 -29
- package/src/create-worker-preview.ts +32 -22
- package/src/dev/dev.tsx +6 -16
- package/src/dev/remote.tsx +40 -16
- package/src/dialogs.tsx +48 -0
- package/src/durable.ts +102 -0
- package/src/index.tsx +291 -207
- package/src/inspect.ts +39 -0
- package/src/kv.ts +74 -25
- package/src/open-in-browser.ts +5 -12
- package/src/package-manager.ts +50 -3
- package/src/pages.tsx +218 -61
- package/src/parse.ts +21 -4
- package/src/proxy.ts +38 -22
- package/src/publish.ts +166 -108
- package/src/sites.tsx +8 -8
- package/src/user.tsx +12 -1
- package/src/whoami.tsx +3 -2
- package/src/worker.ts +2 -1
- package/src/zones.ts +73 -0
- package/templates/new-worker-scheduled.js +17 -0
- package/templates/new-worker-scheduled.ts +32 -0
- package/templates/new-worker.ts +16 -1
- package/wrangler-dist/cli.js +33066 -20052
package/src/proxy.ts
CHANGED
|
@@ -2,11 +2,13 @@ import { createServer as createHttpServer } from "node:http";
|
|
|
2
2
|
import { connect } from "node:http2";
|
|
3
3
|
import { createServer as createHttpsServer } from "node:https";
|
|
4
4
|
import WebSocket from "faye-websocket";
|
|
5
|
+
import { createHttpTerminator } from "http-terminator";
|
|
5
6
|
import { useEffect, useRef, useState } from "react";
|
|
6
7
|
import serveStatic from "serve-static";
|
|
7
8
|
import { getHttpsOptions } from "./https-options";
|
|
8
9
|
import { logger } from "./logger";
|
|
9
10
|
import type { CfPreviewToken } from "./create-worker-preview";
|
|
11
|
+
import type { HttpTerminator } from "http-terminator";
|
|
10
12
|
import type {
|
|
11
13
|
IncomingHttpHeaders,
|
|
12
14
|
RequestListener,
|
|
@@ -67,6 +69,11 @@ function rewriteRemoteHostToLocalHostInHeaders(
|
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
type PreviewProxy = {
|
|
73
|
+
server: HttpServer | HttpsServer;
|
|
74
|
+
terminator: HttpTerminator;
|
|
75
|
+
};
|
|
76
|
+
|
|
70
77
|
export function usePreviewServer({
|
|
71
78
|
previewToken,
|
|
72
79
|
publicRoot,
|
|
@@ -81,21 +88,26 @@ export function usePreviewServer({
|
|
|
81
88
|
ip: string;
|
|
82
89
|
}) {
|
|
83
90
|
/** Creates an HTTP/1 proxy that sends requests over HTTP/2. */
|
|
84
|
-
const [
|
|
91
|
+
const [proxy, setProxy] = useState<PreviewProxy>();
|
|
85
92
|
|
|
86
93
|
/**
|
|
87
94
|
* Create the instance of the local proxy server that will pass on
|
|
88
95
|
* requests to the preview worker.
|
|
89
96
|
*/
|
|
90
97
|
useEffect(() => {
|
|
91
|
-
if (
|
|
98
|
+
if (proxy === undefined) {
|
|
92
99
|
createProxyServer(localProtocol)
|
|
93
|
-
.then((
|
|
100
|
+
.then((server) => {
|
|
101
|
+
setProxy({
|
|
102
|
+
server,
|
|
103
|
+
terminator: createHttpTerminator({ server }),
|
|
104
|
+
});
|
|
105
|
+
})
|
|
94
106
|
.catch(async (err) => {
|
|
95
107
|
logger.error("Failed to create proxy server:", err);
|
|
96
108
|
});
|
|
97
109
|
}
|
|
98
|
-
}, [
|
|
110
|
+
}, [proxy, localProtocol]);
|
|
99
111
|
|
|
100
112
|
/**
|
|
101
113
|
* When we're not connected / getting a fresh token on changes,
|
|
@@ -123,7 +135,7 @@ export function usePreviewServer({
|
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
useEffect(() => {
|
|
126
|
-
if (
|
|
138
|
+
if (proxy === undefined) {
|
|
127
139
|
return;
|
|
128
140
|
}
|
|
129
141
|
|
|
@@ -138,8 +150,8 @@ export function usePreviewServer({
|
|
|
138
150
|
// store the stream in a buffer so we can replay it later
|
|
139
151
|
streamBufferRef.current.push({ stream, headers });
|
|
140
152
|
};
|
|
141
|
-
|
|
142
|
-
cleanupListeners.push(() =>
|
|
153
|
+
proxy.server.on("stream", bufferStream);
|
|
154
|
+
cleanupListeners.push(() => proxy.server.off("stream", bufferStream));
|
|
143
155
|
|
|
144
156
|
const bufferRequestResponse = (
|
|
145
157
|
request: IncomingMessage,
|
|
@@ -149,9 +161,9 @@ export function usePreviewServer({
|
|
|
149
161
|
requestResponseBufferRef.current.push({ request, response });
|
|
150
162
|
};
|
|
151
163
|
|
|
152
|
-
|
|
164
|
+
proxy.server.on("request", bufferRequestResponse);
|
|
153
165
|
cleanupListeners.push(() =>
|
|
154
|
-
|
|
166
|
+
proxy.server.off("request", bufferRequestResponse)
|
|
155
167
|
);
|
|
156
168
|
return () => {
|
|
157
169
|
cleanupListeners.forEach((cleanup) => cleanup());
|
|
@@ -179,8 +191,8 @@ export function usePreviewServer({
|
|
|
179
191
|
port,
|
|
180
192
|
localProtocol
|
|
181
193
|
);
|
|
182
|
-
|
|
183
|
-
cleanupListeners.push(() =>
|
|
194
|
+
proxy.server.on("stream", handleStream);
|
|
195
|
+
cleanupListeners.push(() => proxy.server.off("stream", handleStream));
|
|
184
196
|
|
|
185
197
|
// flush and replay buffered streams
|
|
186
198
|
streamBufferRef.current.forEach((buffer) =>
|
|
@@ -236,9 +248,9 @@ export function usePreviewServer({
|
|
|
236
248
|
? createHandleAssetsRequest(assetPath, handleRequest)
|
|
237
249
|
: handleRequest;
|
|
238
250
|
|
|
239
|
-
|
|
251
|
+
proxy.server.on("request", actualHandleRequest);
|
|
240
252
|
cleanupListeners.push(() =>
|
|
241
|
-
|
|
253
|
+
proxy.server.off("request", actualHandleRequest)
|
|
242
254
|
);
|
|
243
255
|
|
|
244
256
|
// flush and replay buffered requests
|
|
@@ -270,8 +282,8 @@ export function usePreviewServer({
|
|
|
270
282
|
remoteWebsocketClient.close();
|
|
271
283
|
});
|
|
272
284
|
};
|
|
273
|
-
|
|
274
|
-
cleanupListeners.push(() =>
|
|
285
|
+
proxy.server.on("upgrade", handleUpgrade);
|
|
286
|
+
cleanupListeners.push(() => proxy.server.off("upgrade", handleUpgrade));
|
|
275
287
|
|
|
276
288
|
return () => {
|
|
277
289
|
cleanupListeners.forEach((cleanup) => cleanup());
|
|
@@ -281,7 +293,7 @@ export function usePreviewServer({
|
|
|
281
293
|
publicRoot,
|
|
282
294
|
port,
|
|
283
295
|
localProtocol,
|
|
284
|
-
|
|
296
|
+
proxy,
|
|
285
297
|
// We use a state value as a sigil to trigger reconnecting the server.
|
|
286
298
|
// It's not used inside the effect, so react-hooks/exhaustive-deps
|
|
287
299
|
// doesn't complain if it's not included in the dependency array.
|
|
@@ -293,7 +305,7 @@ export function usePreviewServer({
|
|
|
293
305
|
// containing component is mounted/unmounted.
|
|
294
306
|
useEffect(() => {
|
|
295
307
|
const abortController = new AbortController();
|
|
296
|
-
if (
|
|
308
|
+
if (proxy === undefined) {
|
|
297
309
|
return;
|
|
298
310
|
}
|
|
299
311
|
|
|
@@ -303,8 +315,10 @@ export function usePreviewServer({
|
|
|
303
315
|
abortSignal: abortController.signal,
|
|
304
316
|
})
|
|
305
317
|
.then(() => {
|
|
306
|
-
|
|
307
|
-
|
|
318
|
+
proxy.server.on("listening", () => {
|
|
319
|
+
logger.log(`⬣ Listening at ${localProtocol}://${ip}:${port}`);
|
|
320
|
+
});
|
|
321
|
+
proxy.server.listen(port, ip);
|
|
308
322
|
})
|
|
309
323
|
.catch((err) => {
|
|
310
324
|
if ((err as { code: string }).code !== "ABORT_ERR") {
|
|
@@ -313,10 +327,12 @@ export function usePreviewServer({
|
|
|
313
327
|
});
|
|
314
328
|
|
|
315
329
|
return () => {
|
|
316
|
-
proxyServer.close();
|
|
317
330
|
abortController.abort();
|
|
331
|
+
// Running `proxy.server.close()` does not close open connections, preventing the process from exiting.
|
|
332
|
+
// So we use this `terminator` to close all the connections and force the server to shutdown.
|
|
333
|
+
proxy.terminator.terminate();
|
|
318
334
|
};
|
|
319
|
-
}, [port, ip,
|
|
335
|
+
}, [port, ip, proxy, localProtocol]);
|
|
320
336
|
}
|
|
321
337
|
|
|
322
338
|
function createHandleAssetsRequest(
|
|
@@ -404,7 +420,7 @@ export async function waitForPortToBeAvailable(
|
|
|
404
420
|
): Promise<void> {
|
|
405
421
|
return new Promise((resolve, reject) => {
|
|
406
422
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
407
|
-
|
|
423
|
+
options.abortSignal.addEventListener("abort", () => {
|
|
408
424
|
const abortError = new Error("waitForPortToBeAvailable() aborted");
|
|
409
425
|
(abortError as Error & { code: string }).code = "ABORT_ERR";
|
|
410
426
|
doReject(abortError);
|
package/src/publish.ts
CHANGED
|
@@ -4,11 +4,15 @@ import path from "node:path";
|
|
|
4
4
|
import { URLSearchParams } from "node:url";
|
|
5
5
|
import tmp from "tmp-promise";
|
|
6
6
|
import { bundleWorker } from "./bundle";
|
|
7
|
-
import { fetchResult } from "./cfetch";
|
|
7
|
+
import { fetchListResult, fetchResult } from "./cfetch";
|
|
8
|
+
import { printBindings } from "./config";
|
|
8
9
|
import { createWorkerUploadForm } from "./create-worker-upload-form";
|
|
9
10
|
import { confirm } from "./dialogs";
|
|
11
|
+
import { getMigrationsToUpload } from "./durable";
|
|
10
12
|
import { logger } from "./logger";
|
|
13
|
+
import { ParseError } from "./parse";
|
|
11
14
|
import { syncAssets } from "./sites";
|
|
15
|
+
import { getZoneForRoute } from "./zones";
|
|
12
16
|
import type { Config } from "./config";
|
|
13
17
|
import type {
|
|
14
18
|
Route,
|
|
@@ -258,7 +262,7 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
258
262
|
const nodeCompat = props.nodeCompat ?? config.node_compat;
|
|
259
263
|
if (nodeCompat) {
|
|
260
264
|
logger.warn(
|
|
261
|
-
"Enabling node.js compatibility mode for
|
|
265
|
+
"Enabling node.js compatibility mode for built-ins and globals. This is experimental and has serious tradeoffs. Please see https://github.com/ionic-team/rollup-plugin-node-polyfills/ for more details."
|
|
262
266
|
);
|
|
263
267
|
}
|
|
264
268
|
|
|
@@ -289,7 +293,7 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
289
293
|
const envName = props.env ?? "production";
|
|
290
294
|
|
|
291
295
|
const start = Date.now();
|
|
292
|
-
const notProd = !props.legacyEnv && props.env;
|
|
296
|
+
const notProd = Boolean(!props.legacyEnv && props.env);
|
|
293
297
|
const workerName = notProd ? `${scriptName} (${envName})` : scriptName;
|
|
294
298
|
const workerUrl = notProd
|
|
295
299
|
? `/accounts/${accountId}/workers/services/${scriptName}/environments/${envName}`
|
|
@@ -337,103 +341,19 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
337
341
|
}
|
|
338
342
|
);
|
|
339
343
|
|
|
340
|
-
// Some validation of durable objects + migrations
|
|
341
|
-
if (config.durable_objects.bindings.length > 0) {
|
|
342
|
-
// intrinsic [durable_objects] implies [migrations]
|
|
343
|
-
const exportedDurableObjects = config.durable_objects.bindings.filter(
|
|
344
|
-
(binding) => !binding.script_name
|
|
345
|
-
);
|
|
346
|
-
if (exportedDurableObjects.length > 0 && config.migrations.length === 0) {
|
|
347
|
-
logger.warn(
|
|
348
|
-
`In wrangler.toml, you have configured [durable_objects] exported by this Worker (${exportedDurableObjects.map(
|
|
349
|
-
(durable) => durable.class_name
|
|
350
|
-
)}), but no [migrations] for them. This may not work as expected until you add a [migrations] section to your wrangler.toml. Refer to https://developers.cloudflare.com/workers/learning/using-durable-objects/#durable-object-migrations-in-wranglertoml for more details.`
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
344
|
const content = readFileSync(resolvedEntryPointPath, {
|
|
356
345
|
encoding: "utf-8",
|
|
357
346
|
});
|
|
358
347
|
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const scriptData = await fetchResult<{
|
|
369
|
-
script: ScriptData;
|
|
370
|
-
}>(
|
|
371
|
-
`/accounts/${accountId}/workers/services/${scriptName}/environments/${props.env}`
|
|
372
|
-
);
|
|
373
|
-
script = scriptData.script;
|
|
374
|
-
} else {
|
|
375
|
-
const scriptData = await fetchResult<{
|
|
376
|
-
default_environment: {
|
|
377
|
-
script: ScriptData;
|
|
378
|
-
};
|
|
379
|
-
}>(`/accounts/${accountId}/workers/services/${scriptName}`);
|
|
380
|
-
script = scriptData.default_environment.script;
|
|
381
|
-
}
|
|
382
|
-
} catch (err) {
|
|
383
|
-
if (
|
|
384
|
-
![
|
|
385
|
-
10090, // corresponds to workers.api.error.service_not_found, so the script wasn't previously published at all
|
|
386
|
-
10092, // workers.api.error.environment_not_found, so the script wasn't published to this environment yet
|
|
387
|
-
].includes((err as { code: number }).code)
|
|
388
|
-
) {
|
|
389
|
-
throw err;
|
|
390
|
-
}
|
|
391
|
-
// else it's a 404, no script found, and we can proceed
|
|
392
|
-
}
|
|
393
|
-
} else {
|
|
394
|
-
const scripts = await fetchResult<ScriptData[]>(
|
|
395
|
-
`/accounts/${accountId}/workers/scripts`
|
|
396
|
-
);
|
|
397
|
-
script = scripts.find(({ id }) => id === scriptName);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (script?.migration_tag) {
|
|
401
|
-
// was already published once
|
|
402
|
-
const scriptMigrationTag = script.migration_tag;
|
|
403
|
-
const foundIndex = config.migrations.findIndex(
|
|
404
|
-
(migration) => migration.tag === scriptMigrationTag
|
|
405
|
-
);
|
|
406
|
-
if (foundIndex === -1) {
|
|
407
|
-
logger.warn(
|
|
408
|
-
`The published script ${scriptName} has a migration tag "${script.migration_tag}, which was not found in wrangler.toml. You may have already deleted it. Applying all available migrations to the script...`
|
|
409
|
-
);
|
|
410
|
-
migrations = {
|
|
411
|
-
old_tag: script.migration_tag,
|
|
412
|
-
new_tag: config.migrations[config.migrations.length - 1].tag,
|
|
413
|
-
steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
|
|
414
|
-
};
|
|
415
|
-
} else {
|
|
416
|
-
if (foundIndex !== config.migrations.length - 1) {
|
|
417
|
-
// there are new migrations to send up
|
|
418
|
-
migrations = {
|
|
419
|
-
old_tag: script.migration_tag,
|
|
420
|
-
new_tag: config.migrations[config.migrations.length - 1].tag,
|
|
421
|
-
steps: config.migrations
|
|
422
|
-
.slice(foundIndex + 1)
|
|
423
|
-
.map(({ tag: _tag, ...rest }) => rest),
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
// else, we're up to date, no migrations to send
|
|
427
|
-
}
|
|
428
|
-
} else {
|
|
429
|
-
// first time publishing durable objects to this script,
|
|
430
|
-
// so we send all the migrations
|
|
431
|
-
migrations = {
|
|
432
|
-
new_tag: config.migrations[config.migrations.length - 1].tag,
|
|
433
|
-
steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
}
|
|
348
|
+
// durable object migrations
|
|
349
|
+
const migrations = !props.dryRun
|
|
350
|
+
? await getMigrationsToUpload(scriptName, {
|
|
351
|
+
accountId,
|
|
352
|
+
config,
|
|
353
|
+
legacyEnv: props.legacyEnv,
|
|
354
|
+
env: props.env,
|
|
355
|
+
})
|
|
356
|
+
: undefined;
|
|
437
357
|
|
|
438
358
|
const assets = await syncAssets(
|
|
439
359
|
accountId,
|
|
@@ -493,6 +413,13 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
493
413
|
usage_model: config.usage_model,
|
|
494
414
|
};
|
|
495
415
|
|
|
416
|
+
const withoutStaticAssets = {
|
|
417
|
+
...bindings,
|
|
418
|
+
kv_namespaces: config.kv_namespaces,
|
|
419
|
+
text_blobs: config.text_blobs,
|
|
420
|
+
};
|
|
421
|
+
printBindings(withoutStaticAssets);
|
|
422
|
+
|
|
496
423
|
if (!props.dryRun) {
|
|
497
424
|
// Upload the script so it has time to propagate.
|
|
498
425
|
// We can also now tell whether available_on_subdomain is set
|
|
@@ -572,18 +499,7 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
572
499
|
// Update routing table for the script.
|
|
573
500
|
if (routesOnly.length > 0) {
|
|
574
501
|
deployments.push(
|
|
575
|
-
|
|
576
|
-
// Note: PUT will delete previous routes on this script.
|
|
577
|
-
method: "PUT",
|
|
578
|
-
body: JSON.stringify(
|
|
579
|
-
routesOnly.map((route) =>
|
|
580
|
-
typeof route !== "object" ? { pattern: route } : route
|
|
581
|
-
)
|
|
582
|
-
),
|
|
583
|
-
headers: {
|
|
584
|
-
"Content-Type": "application/json",
|
|
585
|
-
},
|
|
586
|
-
}).then(() => {
|
|
502
|
+
publishRoutes(routesOnly, { workerUrl, scriptName, notProd }).then(() => {
|
|
587
503
|
if (routesOnly.length > 10) {
|
|
588
504
|
return routesOnly
|
|
589
505
|
.slice(0, 9)
|
|
@@ -656,3 +572,145 @@ async function getSubdomain(accountId: string): Promise<string> {
|
|
|
656
572
|
}
|
|
657
573
|
}
|
|
658
574
|
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Associate the newly deployed Worker with the given routes.
|
|
578
|
+
*/
|
|
579
|
+
async function publishRoutes(
|
|
580
|
+
routes: Route[],
|
|
581
|
+
{
|
|
582
|
+
workerUrl,
|
|
583
|
+
scriptName,
|
|
584
|
+
notProd,
|
|
585
|
+
}: { workerUrl: string; scriptName: string; notProd: boolean }
|
|
586
|
+
): Promise<string[]> {
|
|
587
|
+
try {
|
|
588
|
+
return await fetchResult(`${workerUrl}/routes`, {
|
|
589
|
+
// Note: PUT will delete previous routes on this script.
|
|
590
|
+
method: "PUT",
|
|
591
|
+
body: JSON.stringify(
|
|
592
|
+
routes.map((route) =>
|
|
593
|
+
typeof route !== "object" ? { pattern: route } : route
|
|
594
|
+
)
|
|
595
|
+
),
|
|
596
|
+
headers: {
|
|
597
|
+
"Content-Type": "application/json",
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
} catch (e) {
|
|
601
|
+
if (isAuthenticationError(e)) {
|
|
602
|
+
// An authentication error is probably due to a known issue,
|
|
603
|
+
// where the user is logged in via an API token that does not have "All Zones".
|
|
604
|
+
return await publishRoutesFallback(routes, { scriptName, notProd });
|
|
605
|
+
} else {
|
|
606
|
+
throw e;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Try updating routes for the Worker using a less optimal zone-based API.
|
|
613
|
+
*
|
|
614
|
+
* Compute match zones to the routes, then for each route attempt to connect it to the Worker via the zone.
|
|
615
|
+
*/
|
|
616
|
+
async function publishRoutesFallback(
|
|
617
|
+
routes: Route[],
|
|
618
|
+
{ scriptName, notProd }: { scriptName: string; notProd: boolean }
|
|
619
|
+
) {
|
|
620
|
+
if (notProd) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
"Service environments combined with an API token that doesn't have 'All Zones' permissions is not supported.\n" +
|
|
623
|
+
"Either turn off service environments by setting `legacy_env = true`, creating an API token with 'All Zones' permissions, or logging in via OAuth"
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
logger.warn(
|
|
627
|
+
"The current authentication token does not have 'All Zones' permissions.\n" +
|
|
628
|
+
"Falling back to using the zone-based API endpoint to update each route individually.\n" +
|
|
629
|
+
"Note that there is no access to routes associated with zones that the API token does not have permission for.\n" +
|
|
630
|
+
"Existing routes for this Worker in such zones will not be deleted."
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
const deployedRoutes: string[] = [];
|
|
634
|
+
|
|
635
|
+
// Collect the routes (and their zones) that will be deployed.
|
|
636
|
+
const activeZones = new Map<string, string>();
|
|
637
|
+
const routesToDeploy = new Map<string, string>();
|
|
638
|
+
for (const route of routes) {
|
|
639
|
+
const zone = await getZoneForRoute(route);
|
|
640
|
+
if (zone) {
|
|
641
|
+
activeZones.set(zone.id, zone.host);
|
|
642
|
+
routesToDeploy.set(
|
|
643
|
+
typeof route === "string" ? route : route.pattern,
|
|
644
|
+
zone.id
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Collect the routes that are already deployed.
|
|
650
|
+
const allRoutes = new Map<string, string>();
|
|
651
|
+
const alreadyDeployedRoutes = new Set<string>();
|
|
652
|
+
for (const [zone, host] of activeZones) {
|
|
653
|
+
try {
|
|
654
|
+
for (const { pattern, script } of await fetchListResult<{
|
|
655
|
+
pattern: string;
|
|
656
|
+
script: string;
|
|
657
|
+
}>(`/zones/${zone}/workers/routes`)) {
|
|
658
|
+
allRoutes.set(pattern, script);
|
|
659
|
+
if (script === scriptName) {
|
|
660
|
+
alreadyDeployedRoutes.add(pattern);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
} catch (e) {
|
|
664
|
+
if (isAuthenticationError(e)) {
|
|
665
|
+
e.notes.push({
|
|
666
|
+
text: `This could be because the API token being used does not have permission to access the zone "${host}" (${zone}).`,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
throw e;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Deploy each route that is not already deployed.
|
|
674
|
+
for (const [routePattern, zoneId] of routesToDeploy.entries()) {
|
|
675
|
+
if (allRoutes.has(routePattern)) {
|
|
676
|
+
const knownScript = allRoutes.get(routePattern);
|
|
677
|
+
if (knownScript === scriptName) {
|
|
678
|
+
// This route is already associated with this worker, so no need to hit the API.
|
|
679
|
+
alreadyDeployedRoutes.delete(routePattern);
|
|
680
|
+
continue;
|
|
681
|
+
} else {
|
|
682
|
+
throw new Error(
|
|
683
|
+
`The route with pattern "${routePattern}" is already associated with another worker called "${knownScript}".`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const { pattern } = await fetchResult(`/zones/${zoneId}/workers/routes`, {
|
|
689
|
+
method: "POST",
|
|
690
|
+
body: JSON.stringify({
|
|
691
|
+
pattern: routePattern,
|
|
692
|
+
script: scriptName,
|
|
693
|
+
}),
|
|
694
|
+
headers: {
|
|
695
|
+
"Content-Type": "application/json",
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
deployedRoutes.push(pattern);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (alreadyDeployedRoutes.size) {
|
|
703
|
+
logger.warn(
|
|
704
|
+
"Previously deployed routes:\n" +
|
|
705
|
+
"The following routes were already associated with this worker, and have not been deleted:\n" +
|
|
706
|
+
[...alreadyDeployedRoutes.values()].map((route) => ` - "${route}"\n`) +
|
|
707
|
+
"If these routes are not wanted then you can remove them in the dashboard."
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return deployedRoutes;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function isAuthenticationError(e: unknown): e is ParseError {
|
|
715
|
+
return e instanceof ParseError && (e as { code?: number }).code === 10000;
|
|
716
|
+
}
|
package/src/sites.tsx
CHANGED
|
@@ -177,7 +177,12 @@ export async function syncAssets(
|
|
|
177
177
|
|
|
178
178
|
// remove the key from the set so we know what we've already uploaded
|
|
179
179
|
namespaceKeys.delete(assetKey);
|
|
180
|
-
|
|
180
|
+
|
|
181
|
+
// prevent causing different manifest keys on windows
|
|
182
|
+
const maifestKey = urlSafe(
|
|
183
|
+
path.relative(siteAssets.assetDirectory, absAssetFile)
|
|
184
|
+
);
|
|
185
|
+
manifest[maifestKey] = assetKey;
|
|
181
186
|
}
|
|
182
187
|
|
|
183
188
|
// keys now contains all the files we're deleting
|
|
@@ -187,14 +192,9 @@ export async function syncAssets(
|
|
|
187
192
|
|
|
188
193
|
await Promise.all([
|
|
189
194
|
// upload all the new assets
|
|
190
|
-
putKVBulkKeyValue(accountId, namespace, toUpload
|
|
195
|
+
putKVBulkKeyValue(accountId, namespace, toUpload),
|
|
191
196
|
// delete all the unused assets
|
|
192
|
-
deleteKVBulkKeyValue(
|
|
193
|
-
accountId,
|
|
194
|
-
namespace,
|
|
195
|
-
Array.from(namespaceKeys),
|
|
196
|
-
() => {}
|
|
197
|
-
),
|
|
197
|
+
deleteKVBulkKeyValue(accountId, namespace, Array.from(namespaceKeys)),
|
|
198
198
|
]);
|
|
199
199
|
|
|
200
200
|
logger.log("↗️ Done syncing assets");
|
package/src/user.tsx
CHANGED
|
@@ -233,7 +233,7 @@ import type { Response } from "undici";
|
|
|
233
233
|
/**
|
|
234
234
|
* Try to read the API token from the environment.
|
|
235
235
|
*/
|
|
236
|
-
const getCloudflareAPITokenFromEnv = getEnvironmentVariableFactory({
|
|
236
|
+
export const getCloudflareAPITokenFromEnv = getEnvironmentVariableFactory({
|
|
237
237
|
variableName: "CLOUDFLARE_API_TOKEN",
|
|
238
238
|
deprecatedName: "CF_API_TOKEN",
|
|
239
239
|
});
|
|
@@ -1201,3 +1201,14 @@ export async function requireAuth(config: {
|
|
|
1201
1201
|
|
|
1202
1202
|
return accountId;
|
|
1203
1203
|
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Throw an error if there is no API token available.
|
|
1207
|
+
*/
|
|
1208
|
+
export function requireApiToken(): string {
|
|
1209
|
+
const authToken = getAPIToken();
|
|
1210
|
+
if (!authToken) {
|
|
1211
|
+
throw new Error("No API token found.");
|
|
1212
|
+
}
|
|
1213
|
+
return authToken;
|
|
1214
|
+
}
|
package/src/whoami.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import Table from "ink-table";
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { fetchListResult, fetchResult } from "./cfetch";
|
|
5
5
|
import { logger } from "./logger";
|
|
6
|
-
import { getAPIToken } from "./user";
|
|
6
|
+
import { getAPIToken, getCloudflareAPITokenFromEnv } from "./user";
|
|
7
7
|
|
|
8
8
|
export async function whoami() {
|
|
9
9
|
logger.log("Getting User settings...");
|
|
@@ -48,10 +48,11 @@ export interface UserInfo {
|
|
|
48
48
|
|
|
49
49
|
export async function getUserInfo(): Promise<UserInfo | undefined> {
|
|
50
50
|
const apiToken = getAPIToken();
|
|
51
|
+
const apiTokenFromEnv = getCloudflareAPITokenFromEnv();
|
|
51
52
|
return apiToken
|
|
52
53
|
? {
|
|
53
54
|
apiToken,
|
|
54
|
-
authType: "OAuth",
|
|
55
|
+
authType: apiTokenFromEnv ? "API" : "OAuth",
|
|
55
56
|
email: await getEmail(),
|
|
56
57
|
accounts: await getAccounts(),
|
|
57
58
|
}
|
package/src/worker.ts
CHANGED
package/src/zones.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { fetchListResult } from "./cfetch";
|
|
2
|
+
import type { Route } from "./config/environment";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An object holding information about a zone for publishing.
|
|
6
|
+
*/
|
|
7
|
+
export interface Zone {
|
|
8
|
+
id: string;
|
|
9
|
+
host: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Try to compute the a zone ID and host name for one or more routes.
|
|
14
|
+
*
|
|
15
|
+
* When we're given a route, we do 2 things:
|
|
16
|
+
* - We try to extract a host from it
|
|
17
|
+
* - We try to get a zone id from the host
|
|
18
|
+
*/
|
|
19
|
+
export async function getZoneForRoute(route: Route): Promise<Zone | undefined> {
|
|
20
|
+
const host =
|
|
21
|
+
typeof route === "string"
|
|
22
|
+
? getHostFromUrl(route)
|
|
23
|
+
: typeof route === "object"
|
|
24
|
+
? "zone_name" in route
|
|
25
|
+
? getHostFromUrl(route.zone_name)
|
|
26
|
+
: getHostFromUrl(route.pattern)
|
|
27
|
+
: undefined;
|
|
28
|
+
const id =
|
|
29
|
+
typeof route === "object" && "zone_id" in route
|
|
30
|
+
? route.zone_id
|
|
31
|
+
: host
|
|
32
|
+
? await getZoneIdFromHost(host)
|
|
33
|
+
: undefined;
|
|
34
|
+
return id && host ? { id, host } : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Given something that resembles a URL, try to extract a host from it.
|
|
39
|
+
*/
|
|
40
|
+
function getHostFromUrl(urlLike: string): string | undefined {
|
|
41
|
+
// strip leading * / *.
|
|
42
|
+
urlLike = urlLike.replace(/^\*(\.)?/g, "");
|
|
43
|
+
|
|
44
|
+
if (!(urlLike.startsWith("http://") || urlLike.startsWith("https://"))) {
|
|
45
|
+
urlLike = "http://" + urlLike;
|
|
46
|
+
}
|
|
47
|
+
return new URL(urlLike).host;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Given something that resembles a host, try to infer a zone id from it.
|
|
52
|
+
*
|
|
53
|
+
* It's hard to get a 'valid' domain from a string, so we don't even try to validate TLDs, etc.
|
|
54
|
+
* For each domain-like part of the host (e.g. w.x.y.z) try to get a zone id for it by
|
|
55
|
+
* lopping off subdomains until we get a hit from the API.
|
|
56
|
+
*/
|
|
57
|
+
export async function getZoneIdFromHost(host: string): Promise<string> {
|
|
58
|
+
const hostPieces = host.split(".");
|
|
59
|
+
|
|
60
|
+
while (hostPieces.length > 1) {
|
|
61
|
+
const zones = await fetchListResult<{ id: string }>(
|
|
62
|
+
`/zones`,
|
|
63
|
+
{},
|
|
64
|
+
new URLSearchParams({ name: hostPieces.join(".") })
|
|
65
|
+
);
|
|
66
|
+
if (zones.length > 0) {
|
|
67
|
+
return zones[0].id;
|
|
68
|
+
}
|
|
69
|
+
hostPieces.shift();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw new Error(`Could not find zone for ${host}`);
|
|
73
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Welcome to Cloudflare Workers! This is your first scheduled worker.
|
|
3
|
+
*
|
|
4
|
+
* - Run `wrangler dev --local` in your terminal to start a development server
|
|
5
|
+
* - Run `curl "http://localhost:8787/cdn-cgi/mf/scheduled"` to trigger the scheduled event
|
|
6
|
+
* - Go back to the console to see what your worker has logged
|
|
7
|
+
* - Update the Cron trigger in wrangler.toml (see https://developers.cloudflare.com/workers/wrangler/configuration/#triggers)
|
|
8
|
+
* - Run `wrangler publish --name my-worker` to publish your worker
|
|
9
|
+
*
|
|
10
|
+
* Learn more at https://developers.cloudflare.com/workers/runtime-apis/scheduled-event/
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
async scheduled(controller, env, ctx) {
|
|
15
|
+
console.log(`Hello World!`);
|
|
16
|
+
},
|
|
17
|
+
};
|