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/miniflare-dist/index.mjs +15 -3
- package/package.json +3 -3
- package/src/__tests__/helpers/mock-cfetch.ts +33 -0
- package/src/__tests__/init.test.ts +537 -359
- package/src/__tests__/jest.setup.ts +3 -0
- package/src/__tests__/pages.test.ts +14 -0
- package/src/__tests__/tail.test.ts +19 -3
- package/src/api/dev.ts +1 -0
- package/src/cfetch/internal.ts +39 -0
- package/src/dev/dev.tsx +4 -0
- package/src/dev.tsx +2 -2
- package/src/init.ts +111 -38
- package/src/miniflare-cli/assets.ts +8 -0
- package/src/miniflare-cli/index.ts +6 -3
- package/src/pages/build.tsx +41 -15
- package/src/pages/constants.ts +1 -0
- package/src/pages/dev.tsx +86 -24
- package/src/pages/errors.ts +22 -0
- package/src/pages/functions/routes-consolidation.test.ts +185 -1
- package/src/pages/functions/routes-consolidation.ts +46 -2
- package/src/pages/functions/routes-transformation.ts +0 -3
- package/src/pages/functions.tsx +96 -0
- package/src/pages/index.tsx +65 -55
- package/src/pages/publish.tsx +27 -16
- package/src/tail/filters.ts +3 -1
- package/src/tail/printing.ts +2 -0
- package/templates/pages-template-plugin.ts +16 -4
- package/templates/pages-template-worker.ts +16 -5
- package/templates/service-bindings-module-facade.js +10 -7
- package/templates/service-bindings-sw-facade.js +10 -7
- package/wrangler-dist/cli.d.ts +1 -0
- package/wrangler-dist/cli.js +1034 -738
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
|
-
|
|
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
|
|
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
|
|
217
|
+
onEnd,
|
|
193
218
|
buildOutputDirectory: directory,
|
|
194
219
|
nodeCompat,
|
|
195
220
|
});
|
|
196
221
|
await metrics.sendMetricsEvent("build pages functions");
|
|
197
|
-
} catch {}
|
|
198
222
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|