wrangler 2.0.8 → 2.0.12

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.
Files changed (67) hide show
  1. package/kv-asset-handler.js +1 -0
  2. package/package.json +3 -1
  3. package/src/__tests__/configuration.test.ts +255 -142
  4. package/src/__tests__/dev.test.tsx +88 -58
  5. package/src/__tests__/index.test.ts +2 -1
  6. package/src/__tests__/init.test.ts +3 -0
  7. package/src/__tests__/kv.test.ts +23 -2
  8. package/src/__tests__/pages.test.ts +98 -1
  9. package/src/__tests__/publish.test.ts +514 -162
  10. package/src/__tests__/whoami.test.tsx +34 -0
  11. package/src/bundle.ts +9 -5
  12. package/src/cfetch/internal.ts +6 -9
  13. package/src/config/config.ts +1 -1
  14. package/src/config/environment.ts +1 -1
  15. package/src/config/validation-helpers.ts +10 -1
  16. package/src/config/validation.ts +22 -13
  17. package/src/create-worker-preview.ts +15 -15
  18. package/src/dev/dev.tsx +32 -56
  19. package/src/dev/local.tsx +10 -7
  20. package/src/dev/remote.tsx +30 -17
  21. package/src/dev/use-esbuild.ts +1 -4
  22. package/src/index.tsx +239 -244
  23. package/src/kv.ts +1 -1
  24. package/src/pages.tsx +295 -229
  25. package/src/parse.ts +21 -1
  26. package/src/proxy.ts +19 -6
  27. package/src/publish.ts +154 -16
  28. package/src/sites.tsx +49 -18
  29. package/src/user.tsx +12 -1
  30. package/src/whoami.tsx +3 -2
  31. package/src/worker.ts +2 -1
  32. package/src/zones.ts +73 -0
  33. package/templates/static-asset-facade.js +1 -5
  34. package/wrangler-dist/cli.js +73693 -73458
  35. package/vendor/@cloudflare/kv-asset-handler/CHANGELOG.md +0 -332
  36. package/vendor/@cloudflare/kv-asset-handler/LICENSE_APACHE +0 -176
  37. package/vendor/@cloudflare/kv-asset-handler/LICENSE_MIT +0 -25
  38. package/vendor/@cloudflare/kv-asset-handler/README.md +0 -245
  39. package/vendor/@cloudflare/kv-asset-handler/dist/index.d.ts +0 -32
  40. package/vendor/@cloudflare/kv-asset-handler/dist/index.js +0 -354
  41. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.d.ts +0 -13
  42. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.js +0 -148
  43. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.d.ts +0 -1
  44. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.js +0 -436
  45. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.d.ts +0 -1
  46. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.js +0 -40
  47. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.d.ts +0 -1
  48. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.js +0 -42
  49. package/vendor/@cloudflare/kv-asset-handler/dist/types.d.ts +0 -26
  50. package/vendor/@cloudflare/kv-asset-handler/dist/types.js +0 -31
  51. package/vendor/@cloudflare/kv-asset-handler/package.json +0 -52
  52. package/vendor/@cloudflare/kv-asset-handler/src/index.ts +0 -296
  53. package/vendor/@cloudflare/kv-asset-handler/src/mocks.ts +0 -136
  54. package/vendor/@cloudflare/kv-asset-handler/src/test/getAssetFromKV.ts +0 -464
  55. package/vendor/@cloudflare/kv-asset-handler/src/test/mapRequestToAsset.ts +0 -33
  56. package/vendor/@cloudflare/kv-asset-handler/src/test/serveSinglePageApp.ts +0 -42
  57. package/vendor/@cloudflare/kv-asset-handler/src/types.ts +0 -39
  58. package/vendor/wrangler-mime/CHANGELOG.md +0 -289
  59. package/vendor/wrangler-mime/LICENSE +0 -21
  60. package/vendor/wrangler-mime/Mime.js +0 -97
  61. package/vendor/wrangler-mime/README.md +0 -187
  62. package/vendor/wrangler-mime/cli.js +0 -46
  63. package/vendor/wrangler-mime/index.js +0 -4
  64. package/vendor/wrangler-mime/lite.js +0 -4
  65. package/vendor/wrangler-mime/package.json +0 -52
  66. package/vendor/wrangler-mime/types/other.js +0 -1
  67. package/vendor/wrangler-mime/types/standard.js +0 -1
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 builtins and globals. This is experimental and has serious tradeoffs. Please see https://github.com/ionic-team/rollup-plugin-node-polyfills/ for more details."
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}`
@@ -428,7 +430,12 @@ export default async function publish(props: Props): Promise<void> {
428
430
  method: "PUT",
429
431
  body: createWorkerUploadForm(worker),
430
432
  },
431
- new URLSearchParams({ include_subdomain_availability: "true" })
433
+ new URLSearchParams({
434
+ include_subdomain_availability: "true",
435
+ // pass excludeScript so the whole body of the
436
+ // script doesn't get included in the response
437
+ excludeScript: "true",
438
+ })
432
439
  )
433
440
  ).available_on_subdomain;
434
441
  }
@@ -497,18 +504,7 @@ export default async function publish(props: Props): Promise<void> {
497
504
  // Update routing table for the script.
498
505
  if (routesOnly.length > 0) {
499
506
  deployments.push(
500
- fetchResult(`${workerUrl}/routes`, {
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(() => {
507
+ publishRoutes(routesOnly, { workerUrl, scriptName, notProd }).then(() => {
512
508
  if (routesOnly.length > 10) {
513
509
  return routesOnly
514
510
  .slice(0, 9)
@@ -581,3 +577,145 @@ async function getSubdomain(accountId: string): Promise<string> {
581
577
  }
582
578
  }
583
579
  }
580
+
581
+ /**
582
+ * Associate the newly deployed Worker with the given routes.
583
+ */
584
+ async function publishRoutes(
585
+ routes: Route[],
586
+ {
587
+ workerUrl,
588
+ scriptName,
589
+ notProd,
590
+ }: { workerUrl: string; scriptName: string; notProd: boolean }
591
+ ): Promise<string[]> {
592
+ try {
593
+ return await fetchResult(`${workerUrl}/routes`, {
594
+ // Note: PUT will delete previous routes on this script.
595
+ method: "PUT",
596
+ body: JSON.stringify(
597
+ routes.map((route) =>
598
+ typeof route !== "object" ? { pattern: route } : route
599
+ )
600
+ ),
601
+ headers: {
602
+ "Content-Type": "application/json",
603
+ },
604
+ });
605
+ } catch (e) {
606
+ if (isAuthenticationError(e)) {
607
+ // An authentication error is probably due to a known issue,
608
+ // where the user is logged in via an API token that does not have "All Zones".
609
+ return await publishRoutesFallback(routes, { scriptName, notProd });
610
+ } else {
611
+ throw e;
612
+ }
613
+ }
614
+ }
615
+
616
+ /**
617
+ * Try updating routes for the Worker using a less optimal zone-based API.
618
+ *
619
+ * Compute match zones to the routes, then for each route attempt to connect it to the Worker via the zone.
620
+ */
621
+ async function publishRoutesFallback(
622
+ routes: Route[],
623
+ { scriptName, notProd }: { scriptName: string; notProd: boolean }
624
+ ) {
625
+ if (notProd) {
626
+ throw new Error(
627
+ "Service environments combined with an API token that doesn't have 'All Zones' permissions is not supported.\n" +
628
+ "Either turn off service environments by setting `legacy_env = true`, creating an API token with 'All Zones' permissions, or logging in via OAuth"
629
+ );
630
+ }
631
+ logger.warn(
632
+ "The current authentication token does not have 'All Zones' permissions.\n" +
633
+ "Falling back to using the zone-based API endpoint to update each route individually.\n" +
634
+ "Note that there is no access to routes associated with zones that the API token does not have permission for.\n" +
635
+ "Existing routes for this Worker in such zones will not be deleted."
636
+ );
637
+
638
+ const deployedRoutes: string[] = [];
639
+
640
+ // Collect the routes (and their zones) that will be deployed.
641
+ const activeZones = new Map<string, string>();
642
+ const routesToDeploy = new Map<string, string>();
643
+ for (const route of routes) {
644
+ const zone = await getZoneForRoute(route);
645
+ if (zone) {
646
+ activeZones.set(zone.id, zone.host);
647
+ routesToDeploy.set(
648
+ typeof route === "string" ? route : route.pattern,
649
+ zone.id
650
+ );
651
+ }
652
+ }
653
+
654
+ // Collect the routes that are already deployed.
655
+ const allRoutes = new Map<string, string>();
656
+ const alreadyDeployedRoutes = new Set<string>();
657
+ for (const [zone, host] of activeZones) {
658
+ try {
659
+ for (const { pattern, script } of await fetchListResult<{
660
+ pattern: string;
661
+ script: string;
662
+ }>(`/zones/${zone}/workers/routes`)) {
663
+ allRoutes.set(pattern, script);
664
+ if (script === scriptName) {
665
+ alreadyDeployedRoutes.add(pattern);
666
+ }
667
+ }
668
+ } catch (e) {
669
+ if (isAuthenticationError(e)) {
670
+ e.notes.push({
671
+ text: `This could be because the API token being used does not have permission to access the zone "${host}" (${zone}).`,
672
+ });
673
+ }
674
+ throw e;
675
+ }
676
+ }
677
+
678
+ // Deploy each route that is not already deployed.
679
+ for (const [routePattern, zoneId] of routesToDeploy.entries()) {
680
+ if (allRoutes.has(routePattern)) {
681
+ const knownScript = allRoutes.get(routePattern);
682
+ if (knownScript === scriptName) {
683
+ // This route is already associated with this worker, so no need to hit the API.
684
+ alreadyDeployedRoutes.delete(routePattern);
685
+ continue;
686
+ } else {
687
+ throw new Error(
688
+ `The route with pattern "${routePattern}" is already associated with another worker called "${knownScript}".`
689
+ );
690
+ }
691
+ }
692
+
693
+ const { pattern } = await fetchResult(`/zones/${zoneId}/workers/routes`, {
694
+ method: "POST",
695
+ body: JSON.stringify({
696
+ pattern: routePattern,
697
+ script: scriptName,
698
+ }),
699
+ headers: {
700
+ "Content-Type": "application/json",
701
+ },
702
+ });
703
+
704
+ deployedRoutes.push(pattern);
705
+ }
706
+
707
+ if (alreadyDeployedRoutes.size) {
708
+ logger.warn(
709
+ "Previously deployed routes:\n" +
710
+ "The following routes were already associated with this worker, and have not been deleted:\n" +
711
+ [...alreadyDeployedRoutes.values()].map((route) => ` - "${route}"\n`) +
712
+ "If these routes are not wanted then you can remove them in the dashboard."
713
+ );
714
+ }
715
+
716
+ return deployedRoutes;
717
+ }
718
+
719
+ function isAuthenticationError(e: unknown): e is ParseError {
720
+ return e instanceof ParseError && (e as { code?: number }).code === 10000;
721
+ }
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
- const toUpload: KeyValue[] = [];
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(siteAssets.baseDirectory, absAssetFile);
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
- toUpload.push({
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,27 +196,31 @@ export async function syncAssets(
175
196
  logger.log(`Skipping - already uploaded.`);
176
197
  }
177
198
 
178
- // remove the key from the set so we know what we've already uploaded
199
+ // Remove the key from the set so we know what we've already uploaded
179
200
  namespaceKeys.delete(assetKey);
180
201
 
181
- // prevent causing different manifest keys on windows
182
- const maifestKey = urlSafe(
183
- path.relative(siteAssets.assetDirectory, absAssetFile)
184
- );
185
- manifest[maifestKey] = assetKey;
202
+ // Prevent different manifest keys on windows
203
+ const manifestKey = urlSafe(path.relative(assetDirectory, absAssetFile));
204
+ manifest[manifestKey] = assetKey;
186
205
  }
187
206
 
207
+ // Add the last (potentially only) bucket to the batch
208
+ uploadBuckets.push(uploadBucket);
209
+
188
210
  // keys now contains all the files we're deleting
189
211
  for (const key of namespaceKeys) {
190
212
  logger.log(`Deleting ${key} from the asset store...`);
191
213
  }
192
214
 
193
- await Promise.all([
194
- // upload all the new assets
195
- putKVBulkKeyValue(accountId, namespace, toUpload),
196
- // delete all the unused assets
197
- deleteKVBulkKeyValue(accountId, namespace, Array.from(namespaceKeys)),
198
- ]);
215
+ // upload each bucket in parallel
216
+ const bucketsToPut = [];
217
+ for (const bucket of uploadBuckets) {
218
+ bucketsToPut.push(putKVBulkKeyValue(accountId, namespace, bucket));
219
+ }
220
+ await Promise.all(bucketsToPut);
221
+
222
+ // then delete all the assets that aren't used anymore
223
+ await deleteKVBulkKeyValue(accountId, namespace, Array.from(namespaceKeys));
199
224
 
200
225
  logger.log("↗️ Done syncing assets");
201
226
 
@@ -214,10 +239,16 @@ function createPatternMatcher(
214
239
  }
215
240
  }
216
241
 
242
+ /**
243
+ * validate that the passed-in file is below 25 MiB
244
+ * **PRIOR** to base64 encoding. 25 MiB is a KV limit
245
+ * @param absFilePath
246
+ * @param relativeFilePath
247
+ */
217
248
  async function validateAssetSize(
218
249
  absFilePath: string,
219
250
  relativeFilePath: string
220
- ) {
251
+ ): Promise<void> {
221
252
  const { size } = await stat(absFilePath);
222
253
  if (size > 25 * 1024 * 1024) {
223
254
  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
@@ -176,5 +176,6 @@ export interface CfWorkerInit {
176
176
  export interface CfWorkerContext {
177
177
  env: string | undefined;
178
178
  legacyEnv: boolean | undefined;
179
- zone: { id: string; host: string } | undefined;
179
+ zone: string | undefined;
180
+ host: string | undefined;
180
181
  }
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
+ }
@@ -1,13 +1,9 @@
1
1
  // DO NOT IMPORT THIS DIRECTLY
2
2
  import worker from "__ENTRY_POINT__";
3
- import { getAssetFromKV } from "@cloudflare/kv-asset-handler";
3
+ import { getAssetFromKV } from "__KV_ASSET_HANDLER__";
4
4
  import manifest from "__STATIC_CONTENT_MANIFEST";
5
5
  const ASSET_MANIFEST = JSON.parse(manifest);
6
6
 
7
- // TODO: remove this
8
- globalThis.__STATIC_CONTENT = undefined;
9
- globalThis.__STATIC_CONTENT_MANIFEST = undefined;
10
-
11
7
  export default {
12
8
  async fetch(request, env, ctx) {
13
9
  let options = {