wrangler 2.0.24 → 2.0.25

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 (39) hide show
  1. package/miniflare-dist/index.mjs +130 -16
  2. package/package.json +1 -1
  3. package/src/__tests__/configuration.test.ts +1 -1
  4. package/src/__tests__/dev.test.tsx +26 -4
  5. package/src/__tests__/helpers/mock-cfetch.ts +2 -2
  6. package/src/__tests__/r2.test.ts +18 -0
  7. package/src/__tests__/tail.test.ts +93 -39
  8. package/src/api/dev.ts +6 -0
  9. package/src/bundle.ts +3 -2
  10. package/src/config/config.ts +1 -1
  11. package/src/config/validation.ts +1 -1
  12. package/src/dev/dev.tsx +12 -2
  13. package/src/dev/local.tsx +69 -5
  14. package/src/dev/use-esbuild.ts +3 -0
  15. package/src/dev-registry.tsx +3 -0
  16. package/src/dev.tsx +26 -17
  17. package/src/index.tsx +51 -21
  18. package/src/inspect.ts +1 -4
  19. package/src/miniflare-cli/assets.ts +19 -16
  20. package/src/miniflare-cli/index.ts +121 -2
  21. package/src/pages/build.tsx +36 -28
  22. package/src/pages/constants.ts +3 -0
  23. package/src/pages/deployments.tsx +9 -9
  24. package/src/pages/dev.tsx +85 -27
  25. package/src/pages/functions/buildPlugin.ts +4 -0
  26. package/src/pages/functions/buildWorker.ts +4 -0
  27. package/src/pages/functions/routes-consolidation.test.ts +66 -0
  28. package/src/pages/functions/routes-consolidation.ts +29 -0
  29. package/src/pages/functions/routes-transformation.test.ts +271 -0
  30. package/src/pages/functions/routes-transformation.ts +125 -0
  31. package/src/pages/projects.tsx +9 -3
  32. package/src/pages/publish.tsx +56 -14
  33. package/src/pages/types.ts +9 -0
  34. package/src/pages/upload.tsx +6 -8
  35. package/src/r2.ts +13 -0
  36. package/src/tail/index.ts +15 -2
  37. package/src/tail/printing.ts +41 -3
  38. package/wrangler-dist/cli.d.ts +6 -0
  39. package/wrangler-dist/cli.js +385 -89
@@ -1,10 +1,23 @@
1
- import { Log, LogLevel, Miniflare } from "miniflare";
1
+ import { fetch } from "@miniflare/core";
2
+ import {
3
+ DurableObjectNamespace,
4
+ DurableObjectStub,
5
+ } from "@miniflare/durable-objects";
6
+ import {
7
+ Log,
8
+ LogLevel,
9
+ Miniflare,
10
+ Response as MiniflareResponse,
11
+ Request as MiniflareRequest,
12
+ } from "miniflare";
2
13
  import yargs from "yargs";
3
14
  import { hideBin } from "yargs/helpers";
15
+ import { FatalError } from "../errors";
4
16
  import generateASSETSBinding from "./assets";
5
17
  import { enumKeys } from "./enum-keys";
6
18
  import { getRequestContextCheckOptions } from "./request-context";
7
19
  import type { Options } from "./assets";
20
+ import type { AddressInfo } from "net";
8
21
 
9
22
  export interface EnablePagesAssetsServiceBindingOptions {
10
23
  proxyPort?: number;
@@ -44,7 +57,45 @@ async function main() {
44
57
  console.log("OPTIONS:\n", JSON.stringify(config, null, 2));
45
58
  }
46
59
 
60
+ config.bindings = {
61
+ ...config.bindings,
62
+ ...Object.fromEntries(
63
+ Object.entries(
64
+ config.externalDurableObjects as Record<
65
+ string,
66
+ { name: string; host: string; port: number }
67
+ >
68
+ ).map(([binding, { name, host, port }]) => {
69
+ const factory = () => {
70
+ throw new FatalError(
71
+ "An external Durable Object instance's state has somehow been attempted to be accessed.",
72
+ 1
73
+ );
74
+ };
75
+ const namespace = new DurableObjectNamespace(name as string, factory);
76
+ namespace.get = (id) => {
77
+ const stub = new DurableObjectStub(factory, id);
78
+ stub.fetch = (...reqArgs) => {
79
+ const url = `http://${host}${port ? `:${port}` : ""}`;
80
+ const request = new MiniflareRequest(
81
+ url,
82
+ new MiniflareRequest(...reqArgs)
83
+ );
84
+ request.headers.set("x-miniflare-durable-object-name", name);
85
+ request.headers.set("x-miniflare-durable-object-id", id.toString());
86
+
87
+ return fetch(request);
88
+ };
89
+ return stub;
90
+ };
91
+ return [binding, namespace];
92
+ })
93
+ ),
94
+ };
95
+
47
96
  let mf: Miniflare | undefined;
97
+ let durableObjectsMf: Miniflare | undefined = undefined;
98
+ let durableObjectsMfPort: number | undefined = undefined;
48
99
 
49
100
  try {
50
101
  if (args._[1]) {
@@ -73,12 +124,80 @@ async function main() {
73
124
  // Start Miniflare development server
74
125
  await mf.startServer();
75
126
  await mf.startScheduler();
76
- process.send && process.send("ready");
127
+
128
+ const internalDurableObjectClassNames = Object.values(
129
+ config.durableObjects as Record<string, string>
130
+ );
131
+
132
+ if (internalDurableObjectClassNames.length > 0) {
133
+ durableObjectsMf = new Miniflare({
134
+ host: config.host,
135
+ port: 0,
136
+ script: `
137
+ export default {
138
+ fetch(request, env) {
139
+ return env.DO.fetch(request)
140
+ }
141
+ }`,
142
+ serviceBindings: {
143
+ DO: async (request: MiniflareRequest) => {
144
+ request = new MiniflareRequest(request);
145
+
146
+ const name = request.headers.get("x-miniflare-durable-object-name");
147
+ const idString = request.headers.get(
148
+ "x-miniflare-durable-object-id"
149
+ );
150
+ request.headers.delete("x-miniflare-durable-object-name");
151
+ request.headers.delete("x-miniflare-durable-object-id");
152
+
153
+ if (!name || !idString) {
154
+ return new MiniflareResponse(
155
+ "[durable-object-proxy-err] Missing `x-miniflare-durable-object-name` or `x-miniflare-durable-object-id` headers.",
156
+ { status: 400 }
157
+ );
158
+ }
159
+
160
+ const namespace = await mf?.getDurableObjectNamespace(name);
161
+ const id = namespace?.idFromString(idString);
162
+
163
+ if (!id) {
164
+ return new MiniflareResponse(
165
+ "[durable-object-proxy-err] Could not generate an ID. Possibly due to a mismatched DO name and ID?",
166
+ { status: 500 }
167
+ );
168
+ }
169
+
170
+ const stub = namespace?.get(id);
171
+
172
+ if (!stub) {
173
+ return new MiniflareResponse(
174
+ "[durable-object-proxy-err] Could not generate a stub. Possibly due to a mismatched DO name and ID?",
175
+ { status: 500 }
176
+ );
177
+ }
178
+
179
+ return stub.fetch(request);
180
+ },
181
+ },
182
+ modules: true,
183
+ });
184
+ const server = await durableObjectsMf.startServer();
185
+ durableObjectsMfPort = (server.address() as AddressInfo).port;
186
+ }
187
+
188
+ process.send &&
189
+ process.send(
190
+ JSON.stringify({
191
+ ready: true,
192
+ durableObjectsPort: durableObjectsMfPort,
193
+ })
194
+ );
77
195
  } catch (e) {
78
196
  mf?.log.error(e as Error);
79
197
  process.exitCode = 1;
80
198
  // Unmount any mounted workers
81
199
  await mf?.dispose();
200
+ await durableObjectsMf?.dispose();
82
201
  }
83
202
  }
84
203
 
@@ -9,24 +9,15 @@ import { buildPlugin } from "./functions/buildPlugin";
9
9
  import { buildWorker } from "./functions/buildWorker";
10
10
  import { generateConfigFromFileTree } from "./functions/filepath-routing";
11
11
  import { writeRoutesModule } from "./functions/routes";
12
+ import { convertRoutesToRoutesJSONSpec } from "./functions/routes-transformation";
12
13
  import { pagesBetaWarning, RUNNING_BUILDERS } from "./utils";
13
14
  import type { Config } from "./functions/routes";
14
- import type { ArgumentsCamelCase, Argv } from "yargs";
15
+ import type { YargsOptionsToInterface } from "./types";
16
+ import type { Argv } from "yargs";
15
17
 
16
- type PagesBuildArgs = {
17
- directory: string;
18
- outfile: string;
19
- "output-config-path"?: string;
20
- minify: boolean;
21
- sourcemap: boolean;
22
- "fallback-service": string;
23
- watch: boolean;
24
- plugin: boolean;
25
- "build-output-directory"?: string;
26
- "node-compat": boolean;
27
- };
18
+ type PagesBuildArgs = YargsOptionsToInterface<typeof Options>;
28
19
 
29
- export function Options(yargs: Argv): Argv<PagesBuildArgs> {
20
+ export function Options(yargs: Argv) {
30
21
  return yargs
31
22
  .positional("directory", {
32
23
  type: "string",
@@ -43,6 +34,10 @@ export function Options(yargs: Argv): Argv<PagesBuildArgs> {
43
34
  type: "string",
44
35
  description: "The location for the output config file",
45
36
  },
37
+ "output-routes-path": {
38
+ type: "string",
39
+ description: "The location for the output _routes.json file",
40
+ },
46
41
  minify: {
47
42
  type: "boolean",
48
43
  default: false,
@@ -87,15 +82,16 @@ export function Options(yargs: Argv): Argv<PagesBuildArgs> {
87
82
  export const Handler = async ({
88
83
  directory,
89
84
  outfile,
90
- "output-config-path": outputConfigPath,
85
+ outputConfigPath,
86
+ outputRoutesPath: routesOutputPath,
91
87
  minify,
92
88
  sourcemap,
93
89
  fallbackService,
94
90
  watch,
95
91
  plugin,
96
- "build-output-directory": buildOutputDirectory,
97
- "node-compat": nodeCompat,
98
- }: ArgumentsCamelCase<PagesBuildArgs>) => {
92
+ buildOutputDirectory,
93
+ nodeCompat,
94
+ }: PagesBuildArgs) => {
99
95
  if (!isInPagesCI) {
100
96
  // Beta message for `wrangler pages <commands>` usage
101
97
  logger.log(pagesBetaWarning);
@@ -120,6 +116,7 @@ export const Handler = async ({
120
116
  plugin,
121
117
  buildOutputDirectory,
122
118
  nodeCompat,
119
+ routesOutputPath,
123
120
  });
124
121
  await metrics.sendMetricsEvent("build pages functions");
125
122
  };
@@ -135,19 +132,25 @@ export async function buildFunctions({
135
132
  onEnd,
136
133
  plugin = false,
137
134
  buildOutputDirectory,
135
+ routesOutputPath,
138
136
  nodeCompat,
139
- }: {
140
- outfile: string;
141
- outputConfigPath?: string;
137
+ }: Partial<
138
+ Pick<
139
+ PagesBuildArgs,
140
+ | "outputConfigPath"
141
+ | "minify"
142
+ | "sourcemap"
143
+ | "fallbackService"
144
+ | "watch"
145
+ | "plugin"
146
+ | "buildOutputDirectory"
147
+ | "nodeCompat"
148
+ >
149
+ > & {
142
150
  functionsDirectory: string;
143
- minify?: boolean;
144
- sourcemap?: boolean;
145
- fallbackService?: string;
146
- watch?: boolean;
147
151
  onEnd?: () => void;
148
- plugin?: boolean;
149
- buildOutputDirectory?: string;
150
- nodeCompat?: boolean;
152
+ outfile: Required<PagesBuildArgs>["outfile"];
153
+ routesOutputPath?: PagesBuildArgs["outputRoutesPath"];
151
154
  }) {
152
155
  RUNNING_BUILDERS.forEach(
153
156
  (runningBuilder) => runningBuilder.stop && runningBuilder.stop()
@@ -161,6 +164,11 @@ export async function buildFunctions({
161
164
  baseURL,
162
165
  });
163
166
 
167
+ if (config.routes && routesOutputPath) {
168
+ const routesJSON = convertRoutesToRoutesJSONSpec(config.routes);
169
+ writeFileSync(routesOutputPath, JSON.stringify(routesJSON, null, 2));
170
+ }
171
+
164
172
  if (outputConfigPath) {
165
173
  writeFileSync(
166
174
  outputConfigPath,
@@ -6,3 +6,6 @@ export const MAX_UPLOAD_ATTEMPTS = 5;
6
6
  export const MAX_CHECK_MISSING_ATTEMPTS = 5;
7
7
  export const SECONDS_TO_WAIT_FOR_PROXY = 5;
8
8
  export const isInPagesCI = !!process.env.CF_PAGES;
9
+ /** The max number of rules in _routes.json */
10
+ export const MAX_FUNCTIONS_ROUTES_RULES = 100;
11
+ export const ROUTES_SPEC_VERSION = 1;
@@ -11,14 +11,16 @@ import { requireAuth } from "../user";
11
11
  import { PAGES_CONFIG_CACHE_FILENAME } from "./constants";
12
12
  import { listProjects } from "./projects";
13
13
  import { pagesBetaWarning } from "./utils";
14
- import type { Deployment, PagesConfigCache } from "./types";
15
- import type { ArgumentsCamelCase, Argv } from "yargs";
14
+ import type {
15
+ Deployment,
16
+ PagesConfigCache,
17
+ YargsOptionsToInterface,
18
+ } from "./types";
19
+ import type { Argv } from "yargs";
16
20
 
17
- type ListArgs = {
18
- "project-name"?: string;
19
- };
21
+ type ListArgs = YargsOptionsToInterface<typeof ListOptions>;
20
22
 
21
- export function ListOptions(yargs: Argv): Argv<ListArgs> {
23
+ export function ListOptions(yargs: Argv) {
22
24
  return yargs
23
25
  .options({
24
26
  "project-name": {
@@ -30,9 +32,7 @@ export function ListOptions(yargs: Argv): Argv<ListArgs> {
30
32
  .epilogue(pagesBetaWarning);
31
33
  }
32
34
 
33
- export async function ListHandler({
34
- projectName,
35
- }: ArgumentsCamelCase<ListArgs>) {
35
+ export async function ListHandler({ projectName }: ListArgs) {
36
36
  const config = getConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME);
37
37
  const accountId = await requireAuth(config);
38
38
 
package/src/pages/dev.tsx CHANGED
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
5
  import { watch } from "chokidar";
6
+ import { build as workerJsBuild } from "esbuild";
6
7
  import { unstable_dev } from "../api";
7
8
  import { FatalError } from "../errors";
8
9
  import { logger } from "../logger";
@@ -10,25 +11,18 @@ import * as metrics from "../metrics";
10
11
  import { buildFunctions } from "./build";
11
12
  import { SECONDS_TO_WAIT_FOR_PROXY } from "./constants";
12
13
  import { CLEANUP, CLEANUP_CALLBACKS, pagesBetaWarning } from "./utils";
13
- import type { ArgumentsCamelCase, Argv } from "yargs";
14
-
15
- type PagesDevArgs = {
16
- directory?: string;
17
- command?: string;
18
- local: boolean;
19
- port: number;
20
- proxy?: number;
21
- "script-path": string;
22
- binding?: (string | number)[];
23
- kv?: (string | number)[];
24
- do?: (string | number)[];
25
- "live-reload": boolean;
26
- "local-protocol"?: "https" | "http";
27
- "experimental-enable-local-persistence": boolean;
28
- "node-compat": boolean;
29
- };
14
+ import type { AdditionalDevProps } from "../dev";
15
+ import type { YargsOptionsToInterface } from "./types";
16
+ import type { Plugin } from "esbuild";
17
+ import type { Argv } from "yargs";
18
+
19
+ const DURABLE_OBJECTS_BINDING_REGEXP = new RegExp(
20
+ /^(?<binding>[^=]+)=(?<className>[^@\s]+)(@(?<scriptName>.*)$)?$/
21
+ );
30
22
 
31
- export function Options(yargs: Argv): Argv<PagesDevArgs> {
23
+ type PagesDevArgs = YargsOptionsToInterface<typeof Options>;
24
+
25
+ export function Options(yargs: Argv) {
32
26
  return yargs
33
27
  .positional("directory", {
34
28
  type: "string",
@@ -46,11 +40,20 @@ export function Options(yargs: Argv): Argv<PagesDevArgs> {
46
40
  default: true,
47
41
  description: "Run on my machine",
48
42
  },
43
+ ip: {
44
+ type: "string",
45
+ default: "0.0.0.0",
46
+ description: "The IP address to listen on",
47
+ },
49
48
  port: {
50
49
  type: "number",
51
50
  default: 8788,
52
51
  description: "The port to listen on (serve from)",
53
52
  },
53
+ "inspector-port": {
54
+ type: "number",
55
+ describe: "Port for devtools to connect to",
56
+ },
54
57
  proxy: {
55
58
  type: "number",
56
59
  description: "The port to proxy (where the static assets are served)",
@@ -68,14 +71,18 @@ export function Options(yargs: Argv): Argv<PagesDevArgs> {
68
71
  },
69
72
  kv: {
70
73
  type: "array",
71
- description: "KV namespace to bind",
74
+ description: "KV namespace to bind (--kv KV_BINDING)",
72
75
  alias: "k",
73
76
  },
74
77
  do: {
75
78
  type: "array",
76
- description: "Durable Object to bind (NAME=CLASS)",
79
+ description: "Durable Object to bind (--do NAME=CLASS)",
77
80
  alias: "o",
78
81
  },
82
+ r2: {
83
+ type: "array",
84
+ description: "R2 bucket to bind (--r2 R2_BINDING)",
85
+ },
79
86
  "live-reload": {
80
87
  type: "boolean",
81
88
  default: false,
@@ -108,19 +115,22 @@ export function Options(yargs: Argv): Argv<PagesDevArgs> {
108
115
  export const Handler = async ({
109
116
  local,
110
117
  directory,
118
+ ip,
111
119
  port,
120
+ "inspector-port": inspectorPort,
112
121
  proxy: requestedProxyPort,
113
122
  "script-path": singleWorkerScriptPath,
114
123
  binding: bindings = [],
115
124
  kv: kvs = [],
116
125
  do: durableObjects = [],
126
+ r2: r2s = [],
117
127
  "live-reload": liveReload,
118
128
  "local-protocol": localProtocol,
119
129
  "experimental-enable-local-persistence": experimentalEnableLocalPersistence,
120
130
  "node-compat": nodeCompat,
121
131
  config: config,
122
132
  _: [_pages, _dev, ...remaining],
123
- }: ArgumentsCamelCase<PagesDevArgs>) => {
133
+ }: PagesDevArgs) => {
124
134
  // Beta message for `wrangler pages <commands>` usage
125
135
  logger.log(pagesBetaWarning);
126
136
 
@@ -213,6 +223,23 @@ export const Handler = async ({
213
223
  if (!existsSync(scriptPath)) {
214
224
  logger.log("No functions. Shimming...");
215
225
  scriptPath = resolve(__dirname, "../templates/pages-shim.ts");
226
+ } else {
227
+ const runBuild = async () => {
228
+ try {
229
+ await workerJsBuild({
230
+ entryPoints: [scriptPath],
231
+ write: false,
232
+ plugins: [blockWorkerJsImports],
233
+ });
234
+ } catch {}
235
+ };
236
+ await runBuild();
237
+ watch([scriptPath], {
238
+ persistent: true,
239
+ ignoreInitial: true,
240
+ }).on("all", async () => {
241
+ await runBuild();
242
+ });
216
243
  }
217
244
  }
218
245
 
@@ -221,7 +248,9 @@ export const Handler = async ({
221
248
  const { stop, waitUntilExit } = await unstable_dev(
222
249
  scriptPath,
223
250
  {
251
+ ip,
224
252
  port,
253
+ inspectorPort,
225
254
  watch: true,
226
255
  localProtocol,
227
256
  liveReload,
@@ -237,12 +266,29 @@ export const Handler = async ({
237
266
  binding: val.toString(),
238
267
  id: "",
239
268
  })),
240
- durableObjects: durableObjects.map((durableObject) => {
241
- const [name, class_name] = durableObject.toString().split("=");
242
- return {
243
- name,
244
- class_name,
245
- };
269
+ durableObjects: durableObjects
270
+ .map((durableObject) => {
271
+ const { binding, className, scriptName } =
272
+ DURABLE_OBJECTS_BINDING_REGEXP.exec(durableObject.toString())
273
+ ?.groups || {};
274
+
275
+ if (!binding || !className) {
276
+ logger.warn(
277
+ "Could not parse Durable Object binding:",
278
+ durableObject.toString()
279
+ );
280
+ return;
281
+ }
282
+
283
+ return {
284
+ name: binding,
285
+ class_name: className,
286
+ script_name: scriptName,
287
+ };
288
+ })
289
+ .filter(Boolean) as AdditionalDevProps["durableObjects"],
290
+ r2: r2s.map((binding) => {
291
+ return { binding: binding.toString(), bucket_name: "" };
246
292
  }),
247
293
 
248
294
  enablePagesAssetsServiceBinding: {
@@ -412,3 +458,15 @@ async function spawnProxyProcess({
412
458
 
413
459
  return port;
414
460
  }
461
+
462
+ const blockWorkerJsImports: Plugin = {
463
+ name: "block-worker-js-imports",
464
+ setup(build) {
465
+ build.onResolve({ filter: /.*/g }, (_args) => {
466
+ logger.error(
467
+ `_worker.js is importing from another file. This will throw an error if deployed.\nYou should bundle your Worker or remove the import if it is unused.`
468
+ );
469
+ return null;
470
+ });
471
+ },
472
+ };
@@ -29,6 +29,10 @@ export function buildPlugin({
29
29
  bundle: true,
30
30
  format: "esm",
31
31
  target: "esnext",
32
+ loader: {
33
+ ".html": "text",
34
+ ".txt": "text",
35
+ },
32
36
  outfile,
33
37
  minify,
34
38
  sourcemap,
@@ -34,6 +34,10 @@ export function buildWorker({
34
34
  bundle: true,
35
35
  format: "esm",
36
36
  target: "esnext",
37
+ loader: {
38
+ ".html": "text",
39
+ ".txt": "text",
40
+ },
37
41
  outfile,
38
42
  minify,
39
43
  sourcemap,
@@ -0,0 +1,66 @@
1
+ import { consolidateRoutes } from "./routes-consolidation";
2
+
3
+ describe("route-consolidation", () => {
4
+ describe("consolidateRoutes()", () => {
5
+ it("should consolidate redundant routes", () => {
6
+ expect(consolidateRoutes(["/api/foo", "/api/*"])).toEqual(["/api/*"]);
7
+ expect(
8
+ consolidateRoutes([
9
+ "/api/foo",
10
+ "/api/foo/*",
11
+ "/api/bar/*",
12
+ "/api/*",
13
+ "/foo",
14
+ "/foo/bar",
15
+ "/bar/*",
16
+ "/bar/baz/*",
17
+ "/bar/baz/hello",
18
+ ])
19
+ ).toEqual(["/api/*", "/foo", "/foo/bar", "/bar/*"]);
20
+ });
21
+ it("should consolidate thousands of redundant routes", () => {
22
+ // Test to make sure the consolidator isn't horribly slow
23
+ const routes: string[] = [];
24
+ const limit = 1000;
25
+ for (let i = 0; i < limit; i++) {
26
+ // Add 3 routes per id
27
+ const id = `some-id-${i}`;
28
+ routes.push(`/${id}/*`, `/${id}/foo`, `/${id}/bar/*`);
29
+ }
30
+ const consolidated = consolidateRoutes(routes);
31
+ expect(consolidated.length).toEqual(limit);
32
+ // Should be all unique
33
+ expect(Array.from(new Set(consolidated)).length).toEqual(limit);
34
+ // Should all have pattern `/$id/*`
35
+ expect(
36
+ consolidated.every((route) => route.match(/\/[a-z0-9-]+\/\*/) !== null)
37
+ ).toEqual(true);
38
+ });
39
+
40
+ it("should consolidate many redundant sub-routes", () => {
41
+ const routes: string[] = [];
42
+ const limit = 15;
43
+
44
+ // Create $limit of top-level catch-all routes, with a lot of sub-routes
45
+ for (let i = 0; i < limit; i++) {
46
+ routes.push(`/foo-${i}/*`);
47
+ for (let j = 0; j < limit; j++) {
48
+ routes.push(`/foo-${i}/bar-${j}/hello`);
49
+ for (let k = 0; k < limit; k++) {
50
+ routes.push(`/foo-${i}/bar-${j}/baz-${k}/*`);
51
+ routes.push(`/foo-${i}/bar-${j}/baz-${k}/profile`);
52
+ }
53
+ }
54
+ }
55
+
56
+ const consolidated = consolidateRoutes(routes);
57
+ expect(consolidated.length).toEqual(limit);
58
+ // Should be all unique
59
+ expect(Array.from(new Set(consolidated)).length).toEqual(limit);
60
+ // Should all have pattern `/$id/*`
61
+ expect(
62
+ consolidated.every((route) => route.match(/\/[a-z0-9-]+\/\*/) !== null)
63
+ ).toEqual(true);
64
+ });
65
+ });
66
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * consolidateRoutes consolidates redundant routes - eg. ["/api/*"", "/api/foo"] -> ["/api/*""]
3
+ * @param routes If this is the same order as Functions routes (with most-specific first),
4
+ * it will be more efficient to reverse it first. Should be in the format: /api/foo, /api/*
5
+ * @returns Non-redundant list of routes
6
+ */
7
+ export function consolidateRoutes(routes: string[]): string[] {
8
+ // create a map of the routes
9
+ const routesMap = new Map<string, boolean>();
10
+ for (const route of routes) {
11
+ routesMap.set(route, true);
12
+ }
13
+ // Find routes that might render other routes redundant
14
+ for (const route of routes.filter((r) => r.endsWith("/*"))) {
15
+ // Make sure the route still exists in the map
16
+ if (routesMap.has(route)) {
17
+ // Remove splat at the end, leaving the /
18
+ // eg. /api/* -> /api/
19
+ const routeTrimmed = route.substring(0, route.length - 1);
20
+ for (const nextRoute of routesMap.keys()) {
21
+ // Delete any route that has the wildcard route as a prefix
22
+ if (nextRoute !== route && nextRoute.startsWith(routeTrimmed)) {
23
+ routesMap.delete(nextRoute);
24
+ }
25
+ }
26
+ }
27
+ }
28
+ return Array.from(routesMap.keys());
29
+ }