wrangler 2.0.25 → 2.0.26

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/pages/dev.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import { execSync, spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
- import { tmpdir } from "node:os";
3
+ import { homedir, tmpdir } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
5
  import { watch } from "chokidar";
6
6
  import { build as workerJsBuild } from "esbuild";
@@ -10,6 +10,7 @@ import { logger } from "../logger";
10
10
  import * as metrics from "../metrics";
11
11
  import { buildFunctions } from "./build";
12
12
  import { SECONDS_TO_WAIT_FOR_PROXY } from "./constants";
13
+ import { FunctionsNoRoutesError, getFunctionsNoRoutesWarning } from "./errors";
13
14
  import { CLEANUP, CLEANUP_CALLBACKS, pagesBetaWarning } from "./utils";
14
15
  import type { AdditionalDevProps } from "../dev";
15
16
  import type { YargsOptionsToInterface } from "./types";
@@ -40,6 +41,27 @@ export function Options(yargs: Argv) {
40
41
  default: true,
41
42
  description: "Run on my machine",
42
43
  },
44
+ "compatibility-date": {
45
+ describe: "Date to use for compatibility checks",
46
+ type: "string",
47
+ },
48
+ "compatibility-flags": {
49
+ describe: "Flags to use for compatibility checks",
50
+ alias: "compatibility-flag",
51
+ type: "string",
52
+ array: true,
53
+ },
54
+
55
+ // TODO
56
+ // For now, all Pages projects are set to 2021-11-02. We're adding compat date soon, and we can then adopt `wrangler dev`'s `default: true`.
57
+ // However, it looks like it isn't actually connected up properly in `wrangler dev` at the moment, hence commenting this out for now.
58
+
59
+ // latest: {
60
+ // describe: "Use the latest version of the worker runtime",
61
+ // type: "boolean",
62
+ // default: false,
63
+ // },
64
+
43
65
  ip: {
44
66
  type: "string",
45
67
  default: "0.0.0.0",
@@ -115,6 +137,8 @@ export function Options(yargs: Argv) {
115
137
  export const Handler = async ({
116
138
  local,
117
139
  directory,
140
+ "compatibility-date": compatibilityDate = "2021-11-02",
141
+ "compatibility-flags": compatibilityFlags,
118
142
  ip,
119
143
  port,
120
144
  "inspector-port": inspectorPort,
@@ -143,7 +167,7 @@ export const Handler = async ({
143
167
  }
144
168
 
145
169
  const functionsDirectory = "./functions";
146
- const usingFunctions = existsSync(functionsDirectory);
170
+ let usingFunctions = existsSync(functionsDirectory);
147
171
 
148
172
  const command = remaining;
149
173
 
@@ -169,8 +193,9 @@ export const Handler = async ({
169
193
  (promiseResolve) => (scriptReadyResolve = promiseResolve)
170
194
  );
171
195
 
172
- let scriptPath: string;
196
+ let scriptPath = "";
173
197
 
198
+ // Try to use Functions
174
199
  if (usingFunctions) {
175
200
  const outfile = join(tmpdir(), `./functionsWorker-${Math.random()}.js`);
176
201
  scriptPath = outfile;
@@ -182,36 +207,63 @@ export const Handler = async ({
182
207
  }
183
208
 
184
209
  logger.log(`Compiling worker to "${outfile}"...`);
185
-
210
+ const onEnd = () => scriptReadyResolve();
186
211
  try {
187
212
  await buildFunctions({
188
213
  outfile,
189
214
  functionsDirectory,
190
215
  sourcemap: true,
191
216
  watch: true,
192
- onEnd: () => scriptReadyResolve(),
217
+ onEnd,
193
218
  buildOutputDirectory: directory,
194
219
  nodeCompat,
195
220
  });
196
221
  await metrics.sendMetricsEvent("build pages functions");
197
- } catch {}
198
222
 
199
- watch([functionsDirectory], {
200
- persistent: true,
201
- ignoreInitial: true,
202
- }).on("all", async () => {
203
- await buildFunctions({
204
- outfile,
205
- functionsDirectory,
206
- sourcemap: true,
207
- watch: true,
208
- onEnd: () => scriptReadyResolve(),
209
- buildOutputDirectory: directory,
210
- nodeCompat,
223
+ // If Functions found routes, continue using Functions
224
+ watch([functionsDirectory], {
225
+ persistent: true,
226
+ ignoreInitial: true,
227
+ }).on("all", async () => {
228
+ try {
229
+ await buildFunctions({
230
+ outfile,
231
+ functionsDirectory,
232
+ sourcemap: true,
233
+ watch: true,
234
+ onEnd,
235
+ buildOutputDirectory: directory,
236
+ nodeCompat,
237
+ });
238
+ await metrics.sendMetricsEvent("build pages functions");
239
+ } catch (e) {
240
+ if (e instanceof FunctionsNoRoutesError) {
241
+ logger.warn(
242
+ getFunctionsNoRoutesWarning(functionsDirectory, "skipping")
243
+ );
244
+ } else {
245
+ throw e;
246
+ }
247
+ }
211
248
  });
212
- await metrics.sendMetricsEvent("build pages functions");
213
- });
214
- } else {
249
+ } catch (e) {
250
+ // If there are no Functions, then Pages will only serve assets.
251
+ if (e instanceof FunctionsNoRoutesError) {
252
+ logger.warn(
253
+ getFunctionsNoRoutesWarning(functionsDirectory, "skipping")
254
+ );
255
+ // Resolve anyway and run without Functions
256
+ onEnd();
257
+ // Turn off Functions
258
+ usingFunctions = false;
259
+ } else {
260
+ throw e;
261
+ }
262
+ }
263
+ }
264
+ // Depending on the result of building Functions, we may not actually be using
265
+ // Functions even if the directory exists.
266
+ if (!usingFunctions) {
215
267
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
216
268
  scriptReadyResolve!();
217
269
 
@@ -245,6 +297,15 @@ export const Handler = async ({
245
297
 
246
298
  await scriptReadyPromise;
247
299
 
300
+ if (scriptPath === "") {
301
+ // Failed to get a script with or without Functions,
302
+ // something really bad must have happend.
303
+ throw new FatalError(
304
+ "Failed to start wrangler pages dev due to an unknown error",
305
+ 1
306
+ );
307
+ }
308
+
248
309
  const { stop, waitUntilExit } = await unstable_dev(
249
310
  scriptPath,
250
311
  {
@@ -254,8 +315,8 @@ export const Handler = async ({
254
315
  watch: true,
255
316
  localProtocol,
256
317
  liveReload,
257
-
258
- compatibilityDate: "2021-11-02",
318
+ compatibilityDate,
319
+ compatibilityFlags,
259
320
  nodeCompat,
260
321
  vars: Object.fromEntries(
261
322
  bindings
@@ -365,7 +426,8 @@ function getPort(pid: number) {
365
426
  let command: string, regExp: RegExp;
366
427
 
367
428
  if (isWindows()) {
368
- command = "\\windows\\system32\\netstat.exe -nao";
429
+ const drive = homedir().split(":\\")[0];
430
+ command = drive + ":\\windows\\system32\\netstat.exe -nao";
369
431
  regExp = new RegExp(`TCP\\s+.*:(\\d+)\\s+.*:\\d+\\s+LISTENING\\s+${pid}`);
370
432
  } else {
371
433
  command = "lsof -nPi";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Pages error when no routes are found in the functions directory
3
+ */
4
+ export class FunctionsNoRoutesError extends Error {
5
+ constructor(message: string) {
6
+ super(message);
7
+ }
8
+ }
9
+ /**
10
+ * Exit code for `pages functions build` when no routes are found.
11
+ */
12
+ export const EXIT_CODE_FUNCTIONS_NO_ROUTES_ERROR = 156;
13
+
14
+ /** Warning message for when buildFunctions throws FunctionsNoRoutesError */
15
+ export function getFunctionsNoRoutesWarning(
16
+ functionsDirectory: string,
17
+ suffix?: string
18
+ ) {
19
+ return `No routes found when building Functions directory: ${functionsDirectory}${
20
+ suffix ? " - " + suffix : ""
21
+ }`;
22
+ }
@@ -1,6 +1,7 @@
1
- import { consolidateRoutes } from "./routes-consolidation";
1
+ import { consolidateRoutes, shortenRoute } from "./routes-consolidation";
2
2
 
3
3
  describe("route-consolidation", () => {
4
+ const maxRuleLength = 100; // from constants.MAX_FUNCTIONS_ROUTES_RULE_LENGTH
4
5
  describe("consolidateRoutes()", () => {
5
6
  it("should consolidate redundant routes", () => {
6
7
  expect(consolidateRoutes(["/api/foo", "/api/*"])).toEqual(["/api/*"]);
@@ -62,5 +63,188 @@ describe("route-consolidation", () => {
62
63
  consolidated.every((route) => route.match(/\/[a-z0-9-]+\/\*/) !== null)
63
64
  ).toEqual(true);
64
65
  });
66
+
67
+ it("should truncate long single-level path into catch-all path, removing other paths", () => {
68
+ expect(
69
+ consolidateRoutes([
70
+ // [/aaaaaaa, /foo] -> [/*]
71
+ "/" + "a".repeat(maxRuleLength * 2),
72
+ "/foo",
73
+ "/bar/*",
74
+ "/baz/bagel/coffee",
75
+ ])
76
+ ).toEqual(["/*"]);
77
+ });
78
+
79
+ it("should truncate long nested path, removing other paths", () => {
80
+ expect(
81
+ consolidateRoutes([
82
+ // [/aaaaaaa, /foo] -> [/*]
83
+ "/foo/" + "a".repeat(maxRuleLength * 2),
84
+ "/foo/bar",
85
+ ])
86
+ ).toEqual(["/foo/*"]);
87
+ });
88
+ });
89
+
90
+ describe(`shortenRoute()`, () => {
91
+ it("should allow max length path", () => {
92
+ const route = "/" + "a".repeat(maxRuleLength - 1);
93
+ // Make sure we don't have an off-by-one error, that'd be embarrassing...
94
+ expect(route.length).toEqual(maxRuleLength);
95
+ expect(
96
+ // Should stay the same
97
+ shortenRoute(route)
98
+ ).toEqual(route);
99
+ });
100
+
101
+ it("should allow max length path (with slash)", () => {
102
+ const route = "/" + "a".repeat(maxRuleLength - 2) + "/";
103
+ expect(route.length).toEqual(maxRuleLength);
104
+ expect(
105
+ // Should stay the same
106
+ shortenRoute(route)
107
+ ).toEqual(route);
108
+ });
109
+
110
+ it("should allow max length wildcard path", () => {
111
+ const route = "/" + "a".repeat(maxRuleLength - 3) + "/*";
112
+ expect(route.length).toEqual(maxRuleLength);
113
+ expect(
114
+ // Should stay the same
115
+ shortenRoute(route)
116
+ ).toEqual(route);
117
+ });
118
+
119
+ it("should truncate long specific path to shorter wildcard path", () => {
120
+ const short = shortenRoute(
121
+ // /aaa/bbb -> /aaa/*
122
+ "/" +
123
+ "a".repeat(maxRuleLength * 0.6) +
124
+ "/" +
125
+ "b".repeat(maxRuleLength * 0.6)
126
+ );
127
+ expect(short).toEqual("/" + "a".repeat(maxRuleLength * 0.6) + "/*");
128
+ expect(short.length).toBeLessThanOrEqual(maxRuleLength);
129
+ });
130
+
131
+ it("should truncate long specific path (with slash) to shorter wildcard path", () => {
132
+ const short = shortenRoute(
133
+ // /aaa/bbb/ -> /aaa/*
134
+ "/" +
135
+ "a".repeat(maxRuleLength * 0.6) +
136
+ "/" +
137
+ "b".repeat(maxRuleLength * 0.6) +
138
+ "/"
139
+ );
140
+ expect(short).toEqual("/" + "a".repeat(maxRuleLength * 0.6) + "/*");
141
+ expect(short.length).toBeLessThanOrEqual(maxRuleLength);
142
+ });
143
+
144
+ it("should truncate long wildcard path to shorter wildcard path", () => {
145
+ const short = shortenRoute(
146
+ // /aaa/bbb/* -> /aaa/*
147
+ "/" +
148
+ "a".repeat(maxRuleLength * 0.6) +
149
+ "/" +
150
+ "b".repeat(maxRuleLength * 0.6) +
151
+ "/*"
152
+ );
153
+ expect(short).toEqual("/" + "a".repeat(maxRuleLength * 0.6) + "/*");
154
+ expect(short.length).toBeLessThanOrEqual(maxRuleLength);
155
+ });
156
+
157
+ it("should truncate long single-level specific path to catch-all path", () => {
158
+ expect(
159
+ shortenRoute(
160
+ // /aaa -> /*
161
+ "/" + "a".repeat(maxRuleLength * 2)
162
+ )
163
+ ).toEqual("/*");
164
+ });
165
+
166
+ it("should truncate long single-level specific path (with slash) to catch-all path", () => {
167
+ expect(
168
+ shortenRoute(
169
+ // /aaa/ -> /*
170
+ "/" + "a".repeat(maxRuleLength * 2) + "/"
171
+ )
172
+ ).toEqual("/*");
173
+ });
174
+
175
+ it("should truncate long single-level wildcard path to catch-all path", () => {
176
+ expect(
177
+ shortenRoute(
178
+ // /aaa/* -> /*
179
+ "/" + "a".repeat(maxRuleLength * 2) + "/*"
180
+ )
181
+ ).toEqual("/*");
182
+ });
183
+
184
+ it("should truncate many single-character segements", () => {
185
+ const short = shortenRoute(
186
+ // /a/a/a -> /a/a/*
187
+ "/a".repeat(maxRuleLength) // 2x limit
188
+ );
189
+ expect(short).toEqual("/a".repeat(maxRuleLength / 2 - 1) + "/*");
190
+ // Should be the exact max length
191
+ expect(short.length).toEqual(maxRuleLength);
192
+ });
193
+
194
+ it("should truncate many double-character segements", () => {
195
+ // === odd ===
196
+ const short = shortenRoute(
197
+ // /aa/aa/aa -> /aa/aa/*
198
+ "/aa".repeat(maxRuleLength) // 3x limit
199
+ );
200
+ expect(short).toEqual("/aa".repeat(maxRuleLength / 3 - 1) + "/*");
201
+ // Should be the exact max length
202
+ expect(short.length).toEqual(maxRuleLength - 2); // -2 because of the odd number
203
+ });
204
+
205
+ it("should truncate many single-character segements with wildcard", () => {
206
+ const short = shortenRoute(
207
+ // /a/a/a -> /a/a/*
208
+ "/a".repeat(maxRuleLength) + "/*" // 2x limit
209
+ );
210
+ expect(short).toEqual("/a".repeat(maxRuleLength / 2 - 1) + "/*");
211
+ // Should be the exact max length
212
+ expect(short.length).toEqual(maxRuleLength);
213
+ });
214
+
215
+ it("should truncate many double-character segements with wildcard", () => {
216
+ const short = shortenRoute(
217
+ // /aa/aa/aa -> /aa/*
218
+ "/aa".repeat(maxRuleLength) + "/*" // 2x limit
219
+ );
220
+ expect(short).toEqual("/aa".repeat(maxRuleLength / 3 - 1) + "/*");
221
+ // Should be the exact max length
222
+ expect(short.length).toEqual(maxRuleLength - 2); // -2 because of the odd number
223
+ });
224
+
225
+ // This is probably the best test here - tests variable-length segments, up until the max.
226
+ // This ensures that it's always able to shorten rules, without failing and returning "/*"
227
+ // The other tests are great for ensuring exact sequences instead of only asserting length, though.
228
+ for (const suffix of ["", "/", "/*"]) {
229
+ // Test each type of path: /a, /a/a, /a/*
230
+ it(`should truncate many variable-character segements (suffix="${suffix}") without truncating to /*`, () => {
231
+ // "/" + 97 chars + "/*" === 100
232
+ for (let i = 1; i < maxRuleLength - 2; i++) {
233
+ const segment = "/" + "a".repeat(i);
234
+ // make sure the segment isn't too long since we are testing not resulting to /*
235
+ expect(segment.length).toBeLessThanOrEqual(maxRuleLength);
236
+ const route =
237
+ segment.repeat((maxRuleLength / segment.length) * 2) + suffix;
238
+ // Make sure we made the rule too long
239
+ expect(route.length).toBeGreaterThan(maxRuleLength);
240
+ const short = shortenRoute(route);
241
+
242
+ // Make sure it's not over the limit
243
+ expect(short.length).toBeLessThanOrEqual(maxRuleLength);
244
+ // It should never have to fall back to /*
245
+ expect(short).not.toEqual("/*");
246
+ }
247
+ });
248
+ }
65
249
  });
66
250
  });
@@ -1,3 +1,5 @@
1
+ import { MAX_FUNCTIONS_ROUTES_RULE_LENGTH } from "../constants";
2
+
1
3
  /**
2
4
  * consolidateRoutes consolidates redundant routes - eg. ["/api/*"", "/api/foo"] -> ["/api/*""]
3
5
  * @param routes If this is the same order as Functions routes (with most-specific first),
@@ -5,13 +7,18 @@
5
7
  * @returns Non-redundant list of routes
6
8
  */
7
9
  export function consolidateRoutes(routes: string[]): string[] {
10
+ // First we need to trim any rules that are too long and deduplicate the result
11
+ const routesShortened = Array.from(
12
+ new Set(routes.map((route) => shortenRoute(route)))
13
+ );
14
+
8
15
  // create a map of the routes
9
16
  const routesMap = new Map<string, boolean>();
10
- for (const route of routes) {
17
+ for (const route of routesShortened) {
11
18
  routesMap.set(route, true);
12
19
  }
13
20
  // Find routes that might render other routes redundant
14
- for (const route of routes.filter((r) => r.endsWith("/*"))) {
21
+ for (const route of routesShortened.filter((r) => r.endsWith("/*"))) {
15
22
  // Make sure the route still exists in the map
16
23
  if (routesMap.has(route)) {
17
24
  // Remove splat at the end, leaving the /
@@ -27,3 +34,40 @@ export function consolidateRoutes(routes: string[]): string[] {
27
34
  }
28
35
  return Array.from(routesMap.keys());
29
36
  }
37
+
38
+ /**
39
+ * Shortens a route until it's within the rule length limit defined in
40
+ * constants.MAX_FUNCTIONS_ROUTES_RULE_LENGTH
41
+ * Eg. /aaa/bbb -> /aaa/*
42
+ * @param routeToShorten Route to shorten if needed
43
+ * @param maxLength Max length of route to try to shorten to
44
+ */
45
+ export function shortenRoute(
46
+ routeToShorten: string,
47
+ maxLength: number = MAX_FUNCTIONS_ROUTES_RULE_LENGTH
48
+ ): string {
49
+ if (routeToShorten.length <= maxLength) {
50
+ return routeToShorten;
51
+ }
52
+
53
+ let route = routeToShorten;
54
+ // May have to try multiple times for longer segments
55
+ for (let i = 0; i < routeToShorten.length; i++) {
56
+ // Shorten to the first slash within the limit
57
+ for (let j = maxLength - 1 - i; j > 0; j--) {
58
+ if (route[j] === "/") {
59
+ route = route.slice(0, j) + "/*";
60
+ break;
61
+ }
62
+ }
63
+ if (route.length <= maxLength) {
64
+ break;
65
+ }
66
+ }
67
+
68
+ // If we failed to shorten it, fall back to include-all rather than breaking
69
+ if (route.length > maxLength) {
70
+ route = "/*";
71
+ }
72
+ return route;
73
+ }
@@ -13,9 +13,6 @@ interface RoutesJSONSpec {
13
13
 
14
14
  type RoutesJSONRouteInput = Pick<RouteConfig, "routePath" | "middleware">[];
15
15
 
16
- /**
17
- * TODO can we do better naming?
18
- */
19
16
  export function convertRoutesToGlobPatterns(
20
17
  routes: RoutesJSONRouteInput
21
18
  ): string[] {
@@ -0,0 +1,96 @@
1
+ import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { FatalError } from "../errors";
4
+ import { logger } from "../logger";
5
+ import { isInPagesCI, ROUTES_SPEC_VERSION } from "./constants";
6
+ import {
7
+ isRoutesJSONSpec,
8
+ optimizeRoutesJSONSpec,
9
+ } from "./functions/routes-transformation";
10
+ import { pagesBetaWarning } from "./utils";
11
+ import type { YargsOptionsToInterface } from "./types";
12
+ import type { Argv } from "yargs";
13
+
14
+ type OptimizeRoutesArgs = YargsOptionsToInterface<typeof OptimizeRoutesOptions>;
15
+
16
+ export function OptimizeRoutesOptions(yargs: Argv) {
17
+ return yargs
18
+ .options({
19
+ "routes-path": {
20
+ type: "string",
21
+ demandOption: true,
22
+ description: "The location of the _routes.json file",
23
+ },
24
+ })
25
+ .options({
26
+ "output-routes-path": {
27
+ type: "string",
28
+ demandOption: true,
29
+ description: "The location of the optimized output routes file",
30
+ },
31
+ });
32
+ }
33
+
34
+ export async function OptimizeRoutesHandler({
35
+ routesPath,
36
+ outputRoutesPath,
37
+ }: OptimizeRoutesArgs) {
38
+ if (!isInPagesCI) {
39
+ // Beta message for `wrangler pages <commands>` usage
40
+ logger.log(pagesBetaWarning);
41
+ }
42
+
43
+ let routesFileContents: string;
44
+ const routesOutputDirectory = path.dirname(outputRoutesPath);
45
+
46
+ if (!existsSync(routesPath)) {
47
+ throw new FatalError(
48
+ `Oops! File ${routesPath} does not exist. Please make sure --routes-path is a valid file path (for example "/public/_routes.json").`,
49
+ 1
50
+ );
51
+ }
52
+
53
+ if (
54
+ !existsSync(routesOutputDirectory) ||
55
+ !lstatSync(routesOutputDirectory).isDirectory()
56
+ ) {
57
+ throw new FatalError(
58
+ `Oops! Folder ${routesOutputDirectory} does not exist. Please make sure --output-routes-path is a valid file path (for example "/public/_routes.json").`,
59
+ 1
60
+ );
61
+ }
62
+
63
+ try {
64
+ routesFileContents = readFileSync(routesPath, "utf-8");
65
+ } catch (err) {
66
+ throw new FatalError(`Error while reading ${routesPath} file: ${err}`);
67
+ }
68
+
69
+ const routes = JSON.parse(routesFileContents);
70
+
71
+ if (!isRoutesJSONSpec(routes)) {
72
+ throw new FatalError(
73
+ `
74
+ Invalid _routes.json file found at: ${routesPath}. Please make sure the JSON object has the following format:
75
+ {
76
+ version: ${ROUTES_SPEC_VERSION};
77
+ include: string[];
78
+ exclude: string[];
79
+ }
80
+ `,
81
+ 1
82
+ );
83
+ }
84
+
85
+ const optimizedRoutes = optimizeRoutesJSONSpec(routes);
86
+ const optimizedRoutesContents = JSON.stringify(optimizedRoutes);
87
+
88
+ try {
89
+ writeFileSync(outputRoutesPath, optimizedRoutesContents);
90
+ } catch (err) {
91
+ throw new FatalError(
92
+ `Error writing to ${outputRoutesPath} file: ${err}`,
93
+ 1
94
+ );
95
+ }
96
+ }