wrangler 2.0.7 → 2.0.11
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 +2 -2
- package/src/__tests__/configuration.test.ts +165 -70
- package/src/__tests__/dev.test.tsx +158 -66
- package/src/__tests__/helpers/mock-dialogs.ts +41 -1
- package/src/__tests__/init.test.ts +191 -111
- package/src/__tests__/kv.test.ts +8 -8
- package/src/__tests__/package-manager.test.ts +154 -7
- package/src/__tests__/pages.test.ts +115 -18
- package/src/__tests__/publish.test.ts +431 -140
- package/src/__tests__/secret.test.ts +4 -4
- package/src/__tests__/whoami.test.tsx +34 -0
- package/src/cfetch/index.ts +17 -2
- package/src/cfetch/internal.ts +12 -9
- package/src/config/config.ts +1 -1
- package/src/config/validation-helpers.ts +10 -1
- package/src/config/validation.ts +59 -33
- package/src/create-worker-preview.ts +15 -15
- package/src/dev/dev.tsx +4 -15
- package/src/dev/remote.tsx +26 -16
- package/src/dialogs.tsx +48 -0
- package/src/index.tsx +181 -167
- package/src/package-manager.ts +50 -3
- package/src/pages.tsx +298 -228
- package/src/publish.ts +148 -15
- package/src/sites.tsx +52 -14
- 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/wrangler-dist/cli.js +707 -407
package/src/publish.ts
CHANGED
|
@@ -4,13 +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
8
|
import { printBindings } from "./config";
|
|
9
9
|
import { createWorkerUploadForm } from "./create-worker-upload-form";
|
|
10
10
|
import { confirm } from "./dialogs";
|
|
11
11
|
import { getMigrationsToUpload } from "./durable";
|
|
12
12
|
import { logger } from "./logger";
|
|
13
|
+
import { ParseError } from "./parse";
|
|
13
14
|
import { syncAssets } from "./sites";
|
|
15
|
+
import { getZoneForRoute } from "./zones";
|
|
14
16
|
import type { Config } from "./config";
|
|
15
17
|
import type {
|
|
16
18
|
Route,
|
|
@@ -260,7 +262,7 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
260
262
|
const nodeCompat = props.nodeCompat ?? config.node_compat;
|
|
261
263
|
if (nodeCompat) {
|
|
262
264
|
logger.warn(
|
|
263
|
-
"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."
|
|
264
266
|
);
|
|
265
267
|
}
|
|
266
268
|
|
|
@@ -291,7 +293,7 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
291
293
|
const envName = props.env ?? "production";
|
|
292
294
|
|
|
293
295
|
const start = Date.now();
|
|
294
|
-
const notProd = !props.legacyEnv && props.env;
|
|
296
|
+
const notProd = Boolean(!props.legacyEnv && props.env);
|
|
295
297
|
const workerName = notProd ? `${scriptName} (${envName})` : scriptName;
|
|
296
298
|
const workerUrl = notProd
|
|
297
299
|
? `/accounts/${accountId}/workers/services/${scriptName}/environments/${envName}`
|
|
@@ -497,18 +499,7 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
497
499
|
// Update routing table for the script.
|
|
498
500
|
if (routesOnly.length > 0) {
|
|
499
501
|
deployments.push(
|
|
500
|
-
|
|
501
|
-
// Note: PUT will delete previous routes on this script.
|
|
502
|
-
method: "PUT",
|
|
503
|
-
body: JSON.stringify(
|
|
504
|
-
routesOnly.map((route) =>
|
|
505
|
-
typeof route !== "object" ? { pattern: route } : route
|
|
506
|
-
)
|
|
507
|
-
),
|
|
508
|
-
headers: {
|
|
509
|
-
"Content-Type": "application/json",
|
|
510
|
-
},
|
|
511
|
-
}).then(() => {
|
|
502
|
+
publishRoutes(routesOnly, { workerUrl, scriptName, notProd }).then(() => {
|
|
512
503
|
if (routesOnly.length > 10) {
|
|
513
504
|
return routesOnly
|
|
514
505
|
.slice(0, 9)
|
|
@@ -581,3 +572,145 @@ async function getSubdomain(accountId: string): Promise<string> {
|
|
|
581
572
|
}
|
|
582
573
|
}
|
|
583
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
|
@@ -137,7 +137,13 @@ export async function syncAssets(
|
|
|
137
137
|
const namespaceKeys = new Set(namespaceKeysResponse.map((x) => x.name));
|
|
138
138
|
|
|
139
139
|
const manifest: Record<string, string> = {};
|
|
140
|
-
|
|
140
|
+
|
|
141
|
+
// A batch of uploads where each bucket has to be less than 100mb
|
|
142
|
+
const uploadBuckets: KeyValue[][] = [];
|
|
143
|
+
// The "live" bucket that we'll keep filling until it's just below 100mb
|
|
144
|
+
let uploadBucket: KeyValue[] = [];
|
|
145
|
+
// A size counter for the live bucket
|
|
146
|
+
let uploadBucketSize = 0;
|
|
141
147
|
|
|
142
148
|
const include = createPatternMatcher(siteAssets.includePatterns, false);
|
|
143
149
|
const exclude = createPatternMatcher(siteAssets.excludePatterns, true);
|
|
@@ -148,7 +154,7 @@ export async function syncAssets(
|
|
|
148
154
|
siteAssets.assetDirectory
|
|
149
155
|
);
|
|
150
156
|
for await (const absAssetFile of getFilesInFolder(assetDirectory)) {
|
|
151
|
-
const assetFile = path.relative(
|
|
157
|
+
const assetFile = path.relative(assetDirectory, absAssetFile);
|
|
152
158
|
if (!include(assetFile)) {
|
|
153
159
|
continue;
|
|
154
160
|
}
|
|
@@ -156,17 +162,32 @@ export async function syncAssets(
|
|
|
156
162
|
continue;
|
|
157
163
|
}
|
|
158
164
|
|
|
159
|
-
await validateAssetSize(absAssetFile, assetFile);
|
|
160
165
|
logger.log(`Reading ${assetFile}...`);
|
|
161
166
|
const content = await readFile(absAssetFile, "base64");
|
|
162
|
-
|
|
167
|
+
await validateAssetSize(absAssetFile, assetFile);
|
|
168
|
+
// while KV accepts files that are 25 MiB **before** b64 encoding
|
|
169
|
+
// the overall bucket size must be below 100 MiB **after** b64 encoding
|
|
170
|
+
const assetSize = Buffer.from(content).length;
|
|
163
171
|
const assetKey = hashAsset(hasher, assetFile, content);
|
|
164
172
|
validateAssetKey(assetKey);
|
|
165
173
|
|
|
166
174
|
// now put each of the files into kv
|
|
167
175
|
if (!namespaceKeys.has(assetKey)) {
|
|
168
176
|
logger.log(`Uploading as ${assetKey}...`);
|
|
169
|
-
|
|
177
|
+
|
|
178
|
+
// Check if adding this asset to the bucket would
|
|
179
|
+
// push it over the 100 MiB limit KV bulk API limit
|
|
180
|
+
if (uploadBucketSize + assetSize > 100 * 1024 * 1024) {
|
|
181
|
+
// If so, move the current bucket into the batch,
|
|
182
|
+
// and reset the counter/bucket
|
|
183
|
+
uploadBuckets.push(uploadBucket);
|
|
184
|
+
uploadBucketSize = 0;
|
|
185
|
+
uploadBucket = [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Update the bucket and the size counter
|
|
189
|
+
uploadBucketSize += assetSize;
|
|
190
|
+
uploadBucket.push({
|
|
170
191
|
key: assetKey,
|
|
171
192
|
value: content,
|
|
172
193
|
base64: true,
|
|
@@ -175,22 +196,33 @@ export async function syncAssets(
|
|
|
175
196
|
logger.log(`Skipping - already uploaded.`);
|
|
176
197
|
}
|
|
177
198
|
|
|
178
|
-
//
|
|
199
|
+
// Remove the key from the set so we know what we've already uploaded
|
|
179
200
|
namespaceKeys.delete(assetKey);
|
|
180
|
-
|
|
201
|
+
|
|
202
|
+
// Prevent different manifest keys on windows
|
|
203
|
+
const manifestKey = urlSafe(
|
|
204
|
+
path.relative(siteAssets.assetDirectory, absAssetFile)
|
|
205
|
+
);
|
|
206
|
+
manifest[manifestKey] = assetKey;
|
|
181
207
|
}
|
|
182
208
|
|
|
209
|
+
// Add the last (potentially only) bucket to the batch
|
|
210
|
+
uploadBuckets.push(uploadBucket);
|
|
211
|
+
|
|
183
212
|
// keys now contains all the files we're deleting
|
|
184
213
|
for (const key of namespaceKeys) {
|
|
185
214
|
logger.log(`Deleting ${key} from the asset store...`);
|
|
186
215
|
}
|
|
187
216
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
217
|
+
// upload each bucket in parallel
|
|
218
|
+
const bucketsToPut = [];
|
|
219
|
+
for (const bucket of uploadBuckets) {
|
|
220
|
+
bucketsToPut.push(putKVBulkKeyValue(accountId, namespace, bucket));
|
|
221
|
+
}
|
|
222
|
+
await Promise.all(bucketsToPut);
|
|
223
|
+
|
|
224
|
+
// then delete all the assets that aren't used anymore
|
|
225
|
+
await deleteKVBulkKeyValue(accountId, namespace, Array.from(namespaceKeys));
|
|
194
226
|
|
|
195
227
|
logger.log("↗️ Done syncing assets");
|
|
196
228
|
|
|
@@ -209,10 +241,16 @@ function createPatternMatcher(
|
|
|
209
241
|
}
|
|
210
242
|
}
|
|
211
243
|
|
|
244
|
+
/**
|
|
245
|
+
* validate that the passed-in file is below 25 MiB
|
|
246
|
+
* **PRIOR** to base64 encoding. 25 MiB is a KV limit
|
|
247
|
+
* @param absFilePath
|
|
248
|
+
* @param relativeFilePath
|
|
249
|
+
*/
|
|
212
250
|
async function validateAssetSize(
|
|
213
251
|
absFilePath: string,
|
|
214
252
|
relativeFilePath: string
|
|
215
|
-
) {
|
|
253
|
+
): Promise<void> {
|
|
216
254
|
const { size } = await stat(absFilePath);
|
|
217
255
|
if (size > 25 * 1024 * 1024) {
|
|
218
256
|
throw new Error(
|
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
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
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 interface Env {
|
|
14
|
+
// Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/
|
|
15
|
+
// MY_KV_NAMESPACE: KVNamespace;
|
|
16
|
+
//
|
|
17
|
+
// Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/
|
|
18
|
+
// MY_DURABLE_OBJECT: DurableObjectNamespace;
|
|
19
|
+
//
|
|
20
|
+
// Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/
|
|
21
|
+
// MY_BUCKET: R2Bucket;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
async scheduled(
|
|
26
|
+
controller: ScheduledController,
|
|
27
|
+
env: Env,
|
|
28
|
+
ctx: ExecutionContext
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
console.log(`Hello World!`);
|
|
31
|
+
},
|
|
32
|
+
};
|