wrangler 2.1.13 → 2.1.14

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/src/delete.ts CHANGED
@@ -1,9 +1,11 @@
1
+ import assert from "assert";
1
2
  import path from "path";
2
3
  import { fetchResult } from "./cfetch";
3
4
  import { findWranglerToml, readConfig } from "./config";
4
5
  import { confirm } from "./dialogs";
5
6
  import { CI } from "./is-ci";
6
7
  import isInteractive from "./is-interactive";
8
+ import { deleteKVNamespace, listKVNamespaces } from "./kv/helpers";
7
9
  import { logger } from "./logger";
8
10
  import * as metrics from "./metrics";
9
11
  import { requireAuth } from "./user";
@@ -60,11 +62,18 @@ export async function deleteHandler(args: ArgumentsCamelCase<DeleteArgs>) {
60
62
 
61
63
  const scriptName = getScriptName(args, config);
62
64
 
65
+ assert(
66
+ scriptName,
67
+ "A worker name must be defined, either via --name, or in wrangler.toml"
68
+ );
69
+
63
70
  if (args.dryRun) {
64
71
  logger.log(`--dry-run: exiting now.`);
65
72
  return;
66
73
  }
67
74
 
75
+ assert(accountId, "Missing accountId");
76
+
68
77
  let confirmed = true;
69
78
  if (isInteractive() || !CI.isCI()) {
70
79
  confirmed = await confirm(
@@ -79,8 +88,24 @@ export async function deleteHandler(args: ArgumentsCamelCase<DeleteArgs>) {
79
88
  new URLSearchParams({ force: "true" })
80
89
  );
81
90
 
91
+ await deleteSiteNamespaceIfExisting(scriptName, accountId);
92
+
82
93
  logger.log("Successfully deleted", scriptName);
83
94
  }
95
+ }
84
96
 
85
- // TODO: maybe delete sites/assets kv namespace as well?
97
+ async function deleteSiteNamespaceIfExisting(
98
+ scriptName: string,
99
+ accountId: string
100
+ ): Promise<void> {
101
+ const title = `__${scriptName}-workers_sites_assets`;
102
+ const previewTitle = `__${scriptName}-workers_sites_assets_preview`;
103
+ const allNamespaces = await listKVNamespaces(accountId);
104
+ const namespacesToDelete = allNamespaces.filter(
105
+ (ns) => ns.title === title || ns.title === previewTitle
106
+ );
107
+ for (const ns of namespacesToDelete) {
108
+ await deleteKVNamespace(accountId, ns.id);
109
+ logger.log(`🌀 Deleted asset namespace for Workers Site "${ns.title}"`);
110
+ }
86
111
  }
package/src/dev/local.tsx CHANGED
@@ -3,6 +3,7 @@ import { fork } from "node:child_process";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { readFile, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
+ import chalk from "chalk";
6
7
  import { npxImport } from "npx-import";
7
8
  import { useState, useEffect, useRef } from "react";
8
9
  import onExit from "signal-exit";
@@ -221,6 +222,10 @@ function useLocalWorker({
221
222
  cwd: path.dirname(scriptPath),
222
223
  execArgv: nodeOptions,
223
224
  stdio: "pipe",
225
+ env: {
226
+ ...process.env,
227
+ FORCE_COLOR: chalk.supportsColor.hasBasic ? "1" : undefined,
228
+ },
224
229
  }));
225
230
 
226
231
  child.on("message", async (messageString) => {
package/src/entry.ts CHANGED
@@ -26,7 +26,7 @@ export async function getEntry(
26
26
  assets?: string | undefined;
27
27
  },
28
28
  config: Config,
29
- command: "dev" | "publish"
29
+ command: "dev" | "publish" | "types"
30
30
  ): Promise<Entry> {
31
31
  let file: string;
32
32
  let directory = process.cwd();
package/src/index.tsx CHANGED
@@ -5,6 +5,7 @@ import supportsColor from "supports-color";
5
5
  import { ProxyAgent, setGlobalDispatcher } from "undici";
6
6
  import makeCLI from "yargs";
7
7
  import { version as wranglerVersion } from "../package.json";
8
+ import { fetchResult } from "./cfetch";
8
9
  import { readConfig } from "./config";
9
10
  import { d1 } from "./d1";
10
11
  import { deleteHandler, deleteOptions } from "./delete";
@@ -35,11 +36,22 @@ import { pubSubCommands } from "./pubsub/pubsub-commands";
35
36
  import { r2 } from "./r2";
36
37
  import { secret, secretBulkHandler, secretBulkOptions } from "./secret";
37
38
  import { tailOptions, tailHandler } from "./tail";
39
+ import { generateTypes } from "./type-generation";
38
40
  import { updateCheck } from "./update-check";
39
- import { listScopes, login, logout, validateScopeKeys } from "./user";
41
+ import {
42
+ listScopes,
43
+ login,
44
+ logout,
45
+ requireAuth,
46
+ validateScopeKeys,
47
+ } from "./user";
40
48
  import { whoami } from "./whoami";
41
49
 
50
+ import type { DeploymentListRes } from "./__tests__/helpers/msw/handlers/deployments";
42
51
  import type { Config } from "./config";
52
+ import type { ServiceMetadataRes } from "./init";
53
+ import type { PartialConfigToDTS } from "./type-generation";
54
+ import type { ArgumentsCamelCase } from "yargs";
43
55
  import type Yargs from "yargs";
44
56
 
45
57
  export type ConfigPath = string | undefined;
@@ -262,7 +274,7 @@ export function createCLIParser(argv: string[]) {
262
274
  // delete
263
275
  wrangler.command(
264
276
  "delete [script]",
265
- "🗑 Delete your Worker from Cloudflare.",
277
+ "🗑 Delete your Worker from Cloudflare.",
266
278
  deleteOptions,
267
279
  deleteHandler
268
280
  );
@@ -467,6 +479,80 @@ export function createCLIParser(argv: string[]) {
467
479
  }
468
480
  );
469
481
 
482
+ // type generation
483
+ wrangler.command(
484
+ "types",
485
+ "📝 Generate types from bindings & module rules in config",
486
+ () => {},
487
+ async () => {
488
+ await printWranglerBanner();
489
+ const config = readConfig(undefined, {});
490
+
491
+ const configBindings: PartialConfigToDTS = {
492
+ kv_namespaces: config.kv_namespaces ?? [],
493
+ vars: { ...config.vars },
494
+ wasm_modules: config.wasm_modules,
495
+ text_blobs: {
496
+ ...config.text_blobs,
497
+ },
498
+ data_blobs: config.data_blobs,
499
+ durable_objects: config.durable_objects,
500
+ r2_buckets: config.r2_buckets,
501
+ // @ts-expect-error - We don't want the type generated to inlcude the Beta prefix
502
+ d1_databases: config.d1_databases,
503
+ services: config.services,
504
+ dispatch_namespaces: config.dispatch_namespaces,
505
+ logfwdr: config.logfwdr,
506
+ unsafe: config.unsafe?.bindings,
507
+ rules: config.rules,
508
+ };
509
+
510
+ await generateTypes(configBindings, config);
511
+ }
512
+ );
513
+
514
+ wrangler.command(
515
+ "deployments",
516
+ false,
517
+ // "🚢 Logs the 10 most recent deployments with 'Version ID', 'Version number','Author email', 'Created on' and 'Latest deploy'",
518
+ (yargs) => {
519
+ yargs.option("name", {
520
+ describe: "The name of your worker",
521
+ type: "string",
522
+ });
523
+ },
524
+ async (deploymentsYargs: ArgumentsCamelCase<{ name: string }>) => {
525
+ await printWranglerBanner();
526
+ const config = readConfig(
527
+ deploymentsYargs.config as ConfigPath,
528
+ deploymentsYargs
529
+ );
530
+ const accountId = await requireAuth(config);
531
+ const scriptName = getScriptName(
532
+ { name: deploymentsYargs.name, env: undefined },
533
+ config
534
+ );
535
+ const scriptMetadata = await fetchResult<ServiceMetadataRes>(
536
+ `/accounts/${accountId}/workers/services/${scriptName}`
537
+ );
538
+
539
+ const scriptTag = scriptMetadata.default_environment.script.tag;
540
+ const deployments = await fetchResult<DeploymentListRes>(
541
+ `/accounts/${accountId}/workers/versions/by-script/${scriptTag}`
542
+ );
543
+
544
+ const versionMessages = deployments.versions.map(
545
+ (versions, index) =>
546
+ `\nVersion ID: ${versions.version_id}\nVersion number: ${
547
+ versions.version_number
548
+ }\nCreated on: ${versions.metadata.created_on}\nAuthor email: ${
549
+ versions.metadata.author_email
550
+ }\nLatest deploy: ${index === 0}\n`
551
+ );
552
+ logger.log(...versionMessages);
553
+ }
554
+ );
555
+
470
556
  // This set to false to allow overwrite of default behaviour
471
557
  wrangler.version(false);
472
558
 
package/src/init.ts CHANGED
@@ -16,8 +16,8 @@ import { parsePackageJSON, parseTOML, readFileSync } from "./parse";
16
16
  import { getBasePath } from "./paths";
17
17
  import { requireAuth } from "./user";
18
18
  import { CommandLineArgsError, printWranglerBanner } from "./index";
19
- import type { RawConfig } from "./config";
20
19
 
20
+ import type { RawConfig } from "./config";
21
21
  import type { Route, SimpleRoute } from "./config/environment";
22
22
  import type { WorkerMetadata } from "./create-worker-upload-form";
23
23
  import type { ConfigPath } from "./index";
package/src/logger.ts CHANGED
@@ -2,7 +2,7 @@ import { format } from "node:util";
2
2
  import { formatMessagesSync } from "esbuild";
3
3
  import { getEnvironmentVariableFactory } from "./environment-variables";
4
4
 
5
- const LOGGER_LEVELS = {
5
+ export const LOGGER_LEVELS = {
6
6
  none: -1,
7
7
  error: 0,
8
8
  warn: 1,
@@ -11,7 +11,7 @@ const LOGGER_LEVELS = {
11
11
  debug: 4,
12
12
  } as const;
13
13
 
14
- type LoggerLevel = keyof typeof LOGGER_LEVELS;
14
+ export type LoggerLevel = keyof typeof LOGGER_LEVELS;
15
15
 
16
16
  /** A map from LOGGER_LEVEL to the error `kind` needed by `formatMessagesSync()`. */
17
17
  const LOGGER_LEVEL_FORMAT_TYPE_MAP = {
@@ -4,16 +4,18 @@ import {
4
4
  DurableObjectStub,
5
5
  } from "@miniflare/durable-objects";
6
6
  import {
7
- Log,
7
+ Log as MiniflareLog,
8
+ LogLevel as MiniflareLogLevel,
8
9
  Miniflare,
9
- Response as MiniflareResponse,
10
10
  Request as MiniflareRequest,
11
+ Response as MiniflareResponse,
11
12
  } from "miniflare";
12
13
  import yargs from "yargs";
13
14
  import { hideBin } from "yargs/helpers";
14
15
  import { FatalError } from "../errors";
15
16
  import generateASSETSBinding from "./assets";
16
17
  import { getRequestContextCheckOptions } from "./request-context";
18
+ import type { LoggerLevel } from "../logger";
17
19
  import type { Options } from "./assets";
18
20
  import type { AddressInfo } from "net";
19
21
 
@@ -24,7 +26,7 @@ export interface EnablePagesAssetsServiceBindingOptions {
24
26
 
25
27
  // miniflare defines this but importing it throws:
26
28
  // Dynamic require of "path" is not supported
27
- class NoOpLog extends Log {
29
+ class MiniflareNoOpLog extends MiniflareLog {
28
30
  log(): void {}
29
31
 
30
32
  error(message: Error): void {
@@ -41,14 +43,17 @@ async function main() {
41
43
  ...JSON.parse((args._[0] as string) ?? "{}"),
42
44
  ...requestContextCheckOptions,
43
45
  };
44
- const logLevel = config.logLevel.toUpperCase();
46
+
47
+ let logLevelString: Uppercase<LoggerLevel> = config.logLevel.toUpperCase();
48
+ if (logLevelString === "LOG") logLevelString = "INFO";
49
+ const logLevel = MiniflareLogLevel[logLevelString];
45
50
 
46
51
  config.log =
47
- config.logLevel === "none"
48
- ? new NoOpLog()
49
- : new Log(logLevel, config.logOptions);
52
+ logLevel === MiniflareLogLevel.NONE
53
+ ? new MiniflareNoOpLog()
54
+ : new MiniflareLog(logLevel, config.logOptions);
50
55
 
51
- if (logLevel === "DEBUG" || logLevel === "VERBOSE") {
56
+ if (logLevel === MiniflareLogLevel.DEBUG) {
52
57
  console.log("MINIFLARE OPTIONS:\n", JSON.stringify(config, null, 2));
53
58
  }
54
59
 
package/src/pages/dev.tsx CHANGED
@@ -379,6 +379,10 @@ export const Handler = async ({
379
379
  plugins: [
380
380
  esbuildAliasExternalPlugin({
381
381
  __ENTRY_POINT__: entrypointFile,
382
+ "./pages-dev-util": resolve(
383
+ getBasePath(),
384
+ "templates/pages-dev-util.ts"
385
+ ),
382
386
  }),
383
387
  ],
384
388
  outfile,
package/src/sites.tsx CHANGED
@@ -77,7 +77,7 @@ async function createKVNamespaceIfNotAlreadyExisting(
77
77
  // check if it already exists
78
78
  // TODO: this is super inefficient, should be made better
79
79
  const namespaces = await listKVNamespaces(accountId);
80
- const found = namespaces.find((x) => x.title === title);
80
+ const found = namespaces.find((ns) => ns.title === title);
81
81
  if (found) {
82
82
  return { created: false, id: found.id };
83
83
  }
@@ -0,0 +1,159 @@
1
+ import * as fs from "fs";
2
+ import { findUpSync } from "find-up";
3
+ import { getEntry } from "./entry";
4
+ import { logger } from "./logger";
5
+ import type { Config } from "./config";
6
+ import type { CfWorkerInit } from "./worker";
7
+
8
+ // Currently includes bindings & rules for declaring modules
9
+ export type PartialConfigToDTS = CfWorkerInit["bindings"] & {
10
+ rules: Config["rules"];
11
+ };
12
+ export async function generateTypes(
13
+ configToDTS: PartialConfigToDTS,
14
+ config: Config
15
+ ) {
16
+ const entry = await getEntry({}, config, "types");
17
+ const envTypeStructure: string[] = [];
18
+
19
+ if (configToDTS.kv_namespaces) {
20
+ for (const kvNamespace of configToDTS.kv_namespaces) {
21
+ envTypeStructure.push(` ${kvNamespace.binding}: KVNamespace;`);
22
+ }
23
+ }
24
+
25
+ if (configToDTS.vars) {
26
+ for (const varName in configToDTS.vars) {
27
+ const varValue = configToDTS.vars[varName];
28
+ if (
29
+ typeof varValue === "string" ||
30
+ typeof varValue === "number" ||
31
+ typeof varValue === "boolean"
32
+ ) {
33
+ envTypeStructure.push(` ${varName}: ${varValue};`);
34
+ }
35
+ if (typeof varValue === "object" && varValue !== null) {
36
+ envTypeStructure.push(` ${varName}: ${JSON.stringify(varValue)};`);
37
+ }
38
+ }
39
+ }
40
+
41
+ if (configToDTS.durable_objects?.bindings) {
42
+ for (const durableObject of configToDTS.durable_objects.bindings) {
43
+ envTypeStructure.push(` ${durableObject.name}: DurableObjectNamespace;`);
44
+ }
45
+ }
46
+
47
+ if (configToDTS.r2_buckets) {
48
+ for (const R2Bucket of configToDTS.r2_buckets) {
49
+ envTypeStructure.push(` ${R2Bucket.binding}: R2Bucket;`);
50
+ }
51
+ }
52
+
53
+ if (configToDTS.d1_databases) {
54
+ for (const d1 of configToDTS.d1_databases) {
55
+ envTypeStructure.push(` ${d1.binding}: D1Database;`);
56
+ }
57
+ }
58
+
59
+ if (configToDTS.services) {
60
+ for (const service of configToDTS.services) {
61
+ envTypeStructure.push(` ${service.binding}: Fetcher;`);
62
+ }
63
+ }
64
+
65
+ if (configToDTS.dispatch_namespaces) {
66
+ for (const namespace of configToDTS.dispatch_namespaces) {
67
+ envTypeStructure.push(` ${namespace.binding}: any;`);
68
+ }
69
+ }
70
+
71
+ if (configToDTS.logfwdr?.schema) {
72
+ envTypeStructure.push(` LOGFWDR_SCHEMA: any;`);
73
+ }
74
+
75
+ if (configToDTS.data_blobs) {
76
+ for (const dataBlobs in configToDTS.data_blobs) {
77
+ envTypeStructure.push(` ${dataBlobs}: ArrayBuffer;`);
78
+ }
79
+ }
80
+
81
+ if (configToDTS.text_blobs) {
82
+ for (const textBlobs in configToDTS.text_blobs) {
83
+ envTypeStructure.push(` ${textBlobs}: string;`);
84
+ }
85
+ }
86
+
87
+ if (configToDTS.unsafe) {
88
+ for (const unsafe of configToDTS.unsafe) {
89
+ envTypeStructure.push(` ${unsafe.name}: any;`);
90
+ }
91
+ }
92
+
93
+ const modulesTypeStructure: string[] = [];
94
+ if (configToDTS.rules) {
95
+ const moduleTypeMap = {
96
+ Text: "string",
97
+ Data: "ArrayBuffer",
98
+ CompiledWasm: "WebAssembly.Module",
99
+ };
100
+ for (const ruleObject of configToDTS.rules) {
101
+ const typeScriptType =
102
+ moduleTypeMap[ruleObject.type as keyof typeof moduleTypeMap];
103
+ if (typeScriptType !== undefined) {
104
+ ruleObject.globs.forEach((glob) => {
105
+ modulesTypeStructure.push(`declare module "*.${glob
106
+ .split(".")
107
+ .at(-1)}" {
108
+ const value: ${typeScriptType};
109
+ export default value;
110
+ }`);
111
+ });
112
+ }
113
+ }
114
+ }
115
+
116
+ function writeDTSFile(
117
+ typesString: string[],
118
+ formatType: "modules" | "service-worker"
119
+ ) {
120
+ const wranglerOverrideDTSPath = findUpSync("worker-configuration.d.ts");
121
+ try {
122
+ if (
123
+ wranglerOverrideDTSPath !== undefined &&
124
+ !fs
125
+ .readFileSync(wranglerOverrideDTSPath, "utf8")
126
+ .includes("***AUTO GENERATED BY WORKERS CLI WRANGLER***")
127
+ ) {
128
+ throw new Error(
129
+ "A non-wrangler worker-configuration.d.ts already exists, please rename and try again."
130
+ );
131
+ }
132
+ } catch (error) {
133
+ if (error instanceof Error && !error.message.includes("not found")) {
134
+ throw error;
135
+ }
136
+ }
137
+
138
+ let combinedTypeStrings = "";
139
+ if (formatType === "modules") {
140
+ combinedTypeStrings = `interface Env {\n${typesString.join(
141
+ "\n"
142
+ )} \n}\n${modulesTypeStructure.join("\n")}`;
143
+ } else {
144
+ combinedTypeStrings = `declare global {\n${typesString.join(
145
+ "\n"
146
+ )} \n}\n${modulesTypeStructure.join("\n")}`;
147
+ }
148
+
149
+ if (envTypeStructure.length || modulesTypeStructure.length) {
150
+ fs.writeFileSync(
151
+ "worker-configuration.d.ts",
152
+ `// Generated by Wrangler on ${new Date()}` + "\n" + combinedTypeStrings
153
+ );
154
+ logger.log(combinedTypeStrings);
155
+ }
156
+ }
157
+
158
+ writeDTSFile(envTypeStructure, entry.format);
159
+ }
@@ -0,0 +1,128 @@
1
+ import { isRoutingRuleMatch } from "../pages-dev-util";
2
+
3
+ describe("isRoutingRuleMatch", () => {
4
+ it("should match rules referencing root level correctly", () => {
5
+ const routingRule = "/";
6
+
7
+ expect(isRoutingRuleMatch("/", routingRule)).toBeTruthy();
8
+ expect(isRoutingRuleMatch("/foo", routingRule)).toBeFalsy();
9
+ expect(isRoutingRuleMatch("/foo/", routingRule)).toBeFalsy();
10
+ expect(isRoutingRuleMatch("/foo/bar", routingRule)).toBeFalsy();
11
+ });
12
+
13
+ it("should match include-all rules correctly", () => {
14
+ const routingRule = "/*";
15
+
16
+ expect(isRoutingRuleMatch("/", routingRule)).toBeTruthy();
17
+ expect(isRoutingRuleMatch("/foo", routingRule)).toBeTruthy();
18
+ expect(isRoutingRuleMatch("/foo/", routingRule)).toBeTruthy();
19
+ expect(isRoutingRuleMatch("/foo/bar", routingRule)).toBeTruthy();
20
+ expect(isRoutingRuleMatch("/foo/bar/", routingRule)).toBeTruthy();
21
+ expect(isRoutingRuleMatch("/foo/bar/baz", routingRule)).toBeTruthy();
22
+ expect(isRoutingRuleMatch("/foo/bar/baz/", routingRule)).toBeTruthy();
23
+ });
24
+
25
+ it("should match `/*` suffix-ed rules correctly", () => {
26
+ let routingRule = "/foo/*";
27
+
28
+ expect(isRoutingRuleMatch("/foo", routingRule)).toBeTruthy();
29
+ expect(isRoutingRuleMatch("/foo/", routingRule)).toBeTruthy();
30
+ expect(isRoutingRuleMatch("/foobar", routingRule)).toBeFalsy();
31
+ expect(isRoutingRuleMatch("/foo/bar", routingRule)).toBeTruthy();
32
+ expect(isRoutingRuleMatch("/foo/bar/baz", routingRule)).toBeTruthy();
33
+ expect(isRoutingRuleMatch("/bar/foo", routingRule)).toBeFalsy();
34
+ expect(isRoutingRuleMatch("/bar/foo/baz", routingRule)).toBeFalsy();
35
+
36
+ routingRule = "/foo/bar/*";
37
+
38
+ expect(isRoutingRuleMatch("/foo", routingRule)).toBeFalsy();
39
+ expect(isRoutingRuleMatch("/foo/", routingRule)).toBeFalsy();
40
+ expect(isRoutingRuleMatch("/foo/bar", routingRule)).toBeTruthy();
41
+ expect(isRoutingRuleMatch("/foo/bar/baz", routingRule)).toBeTruthy();
42
+ expect(isRoutingRuleMatch("/foo/barfoo", routingRule)).toBeFalsy();
43
+ expect(isRoutingRuleMatch("baz/foo/bar", routingRule)).toBeFalsy();
44
+ expect(isRoutingRuleMatch("baz/foo/bar/", routingRule)).toBeFalsy();
45
+ });
46
+
47
+ it("should match `/` suffix-ed rules correctly", () => {
48
+ let routingRule = "/foo/";
49
+ expect(isRoutingRuleMatch("/foo/", routingRule)).toBeTruthy();
50
+ expect(isRoutingRuleMatch("/foo", routingRule)).toBeTruthy();
51
+
52
+ routingRule = "/foo/bar/";
53
+ expect(isRoutingRuleMatch("/foo/bar/", routingRule)).toBeTruthy();
54
+ expect(isRoutingRuleMatch("/foo/bar", routingRule)).toBeTruthy();
55
+ });
56
+
57
+ it("should match `*` suffix-ed rules correctly", () => {
58
+ let routingRule = "/foo*";
59
+ expect(isRoutingRuleMatch("/foo", routingRule)).toBeTruthy();
60
+ expect(isRoutingRuleMatch("/foo/", routingRule)).toBeTruthy();
61
+ expect(isRoutingRuleMatch("/foobar", routingRule)).toBeTruthy();
62
+ expect(isRoutingRuleMatch("/barfoo", routingRule)).toBeFalsy();
63
+ expect(isRoutingRuleMatch("/foo/bar", routingRule)).toBeTruthy();
64
+ expect(isRoutingRuleMatch("/bar/foo", routingRule)).toBeFalsy();
65
+ expect(isRoutingRuleMatch("/bar/foobar", routingRule)).toBeFalsy();
66
+ expect(isRoutingRuleMatch("/foo/bar/baz", routingRule)).toBeTruthy();
67
+ expect(isRoutingRuleMatch("/bar/foo/baz", routingRule)).toBeFalsy();
68
+
69
+ routingRule = "/foo/bar*";
70
+ expect(isRoutingRuleMatch("/foo/bar", routingRule)).toBeTruthy();
71
+ expect(isRoutingRuleMatch("/foo/bar/", routingRule)).toBeTruthy();
72
+ expect(isRoutingRuleMatch("/foo/barfoo", routingRule)).toBeTruthy();
73
+ expect(isRoutingRuleMatch("/bar/foo/barfoo", routingRule)).toBeFalsy();
74
+ expect(isRoutingRuleMatch("/foo/bar/baz", routingRule)).toBeTruthy();
75
+ expect(isRoutingRuleMatch("/bar/foo/bar/baz", routingRule)).toBeFalsy();
76
+ });
77
+
78
+ it("should match rules without wildcards correctly", () => {
79
+ let routingRule = "/foo";
80
+
81
+ expect(isRoutingRuleMatch("/foo", routingRule)).toBeTruthy();
82
+ expect(isRoutingRuleMatch("/foo/", routingRule)).toBeTruthy();
83
+ expect(isRoutingRuleMatch("/foo/bar", routingRule)).toBeFalsy();
84
+ expect(isRoutingRuleMatch("/bar/foo", routingRule)).toBeFalsy();
85
+
86
+ routingRule = "/foo/bar";
87
+ expect(isRoutingRuleMatch("/foo/bar", routingRule)).toBeTruthy();
88
+ expect(isRoutingRuleMatch("/foo/bar/", routingRule)).toBeTruthy();
89
+ expect(isRoutingRuleMatch("/foo/bar/baz", routingRule)).toBeFalsy();
90
+ expect(isRoutingRuleMatch("/baz/foo/bar", routingRule)).toBeFalsy();
91
+ });
92
+
93
+ it("should throw an error if pathname or routing rule params are missing", () => {
94
+ // MISSING PATHNAME
95
+ expect(() =>
96
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
97
+ // @ts-ignore: sanity check
98
+ isRoutingRuleMatch(undefined, "/*")
99
+ ).toThrow("Pathname is undefined.");
100
+
101
+ expect(() =>
102
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
103
+ // @ts-ignore: sanity check
104
+ isRoutingRuleMatch(null, "/*")
105
+ ).toThrow("Pathname is undefined.");
106
+
107
+ expect(() => isRoutingRuleMatch("", "/*")).toThrow(
108
+ "Pathname is undefined."
109
+ );
110
+
111
+ // MISSING ROUTING RULE
112
+ expect(() =>
113
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
114
+ // @ts-ignore: sanity check
115
+ isRoutingRuleMatch("/foo", undefined)
116
+ ).toThrow("Routing rule is undefined.");
117
+
118
+ expect(() =>
119
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
120
+ // @ts-ignore: sanity check
121
+ isRoutingRuleMatch("/foo", null)
122
+ ).toThrow("Routing rule is undefined.");
123
+
124
+ expect(() => isRoutingRuleMatch("/foo", "")).toThrow(
125
+ "Routing rule is undefined."
126
+ );
127
+ });
128
+ });
@@ -2,30 +2,23 @@
2
2
  import worker from "__ENTRY_POINT__";
3
3
  // @ts-ignore entry point will get replaced
4
4
  export * from "__ENTRY_POINT__";
5
+ import { isRoutingRuleMatch } from "./pages-dev-util";
5
6
 
6
- const transformToRegex = (filter: string) => {
7
- return filter.replace("*", ".*");
8
- };
9
-
10
- const routes = {
11
- // @ts-ignore routes are injected
12
- include: __ROUTES__.include.map(transformToRegex),
13
- // @ts-ignore routes are injected
14
- exclude: __ROUTES__.exclude.map(transformToRegex) || [],
15
- };
7
+ // @ts-ignore routes are injected
8
+ const routes = __ROUTES__;
16
9
 
17
10
  export default {
18
11
  fetch(request, env, context) {
19
12
  const { pathname } = new URL(request.url);
20
13
 
21
14
  for (const exclude of routes.exclude) {
22
- if (pathname.match(exclude)) {
15
+ if (isRoutingRuleMatch(pathname, exclude)) {
23
16
  return env.ASSETS.fetch(request);
24
17
  }
25
18
  }
26
19
 
27
20
  for (const include of routes.include) {
28
- if (pathname.match(include)) {
21
+ if (isRoutingRuleMatch(pathname, include)) {
29
22
  return worker.fetch(request, env, context);
30
23
  }
31
24
  }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @param pathname A pathname string, such as `/foo` or `/foo/bar`
3
+ * @param routingRule The routing rule, such as `/foo/*`
4
+ * @returns True if pathname matches the routing rule
5
+ *
6
+ * / -> /
7
+ * /* -> /*
8
+ * /foo -> /foo
9
+ * /foo* -> /foo, /foo-bar, /foo/*
10
+ * /foo/* -> /foo, /foo/bar
11
+ */
12
+ export function isRoutingRuleMatch(
13
+ pathname: string,
14
+ routingRule: string
15
+ ): boolean {
16
+ // sanity checks
17
+ if (!pathname) {
18
+ throw new Error("Pathname is undefined.");
19
+ }
20
+ if (!routingRule) {
21
+ throw new Error("Routing rule is undefined.");
22
+ }
23
+
24
+ const ruleRegExp = transformRoutingRuleToRegExp(routingRule);
25
+ return pathname.match(ruleRegExp) !== null;
26
+ }
27
+
28
+ function transformRoutingRuleToRegExp(rule: string): RegExp {
29
+ let transformedRule;
30
+
31
+ if (rule === "/" || rule === "/*") {
32
+ transformedRule = rule;
33
+ } else if (rule.endsWith("/*")) {
34
+ // make `/*` an optional group so we can match both /foo/* and /foo
35
+ // /foo/* => /foo(/*)?
36
+ transformedRule = `${rule.substring(0, rule.length - 2)}(/*)?`;
37
+ } else if (rule.endsWith("/")) {
38
+ // make `/` an optional group so we can match both /foo/ and /foo
39
+ // /foo/ => /foo(/)?
40
+ transformedRule = `${rule.substring(0, rule.length - 1)}(/)?`;
41
+ } else if (rule.endsWith("*")) {
42
+ transformedRule = rule;
43
+ } else {
44
+ transformedRule = `${rule}(/)?`;
45
+ }
46
+
47
+ // /foo* => /foo.* => ^/foo.*$
48
+ transformedRule = `^${transformedRule.replace("*", ".*")}$`;
49
+
50
+ // ^/foo.*$ => /^\/foo.*$/
51
+ return new RegExp(transformedRule);
52
+ }