wrangler 2.0.22 → 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.
- package/README.md +20 -2
- package/bin/wrangler.js +1 -1
- package/miniflare-dist/index.mjs +643 -7
- package/package.json +17 -5
- package/src/__tests__/configuration.test.ts +89 -17
- package/src/__tests__/dev.test.tsx +121 -8
- package/src/__tests__/generate.test.ts +93 -0
- package/src/__tests__/helpers/mock-cfetch.ts +54 -2
- package/src/__tests__/index.test.ts +10 -27
- package/src/__tests__/jest.setup.ts +31 -1
- package/src/__tests__/kv.test.ts +82 -61
- package/src/__tests__/metrics.test.ts +5 -0
- package/src/__tests__/publish.test.ts +573 -254
- package/src/__tests__/r2.test.ts +173 -71
- package/src/__tests__/tail.test.ts +93 -39
- package/src/__tests__/user.test.ts +1 -0
- package/src/__tests__/validate-dev-props.test.ts +56 -0
- package/src/__tests__/version.test.ts +35 -0
- package/src/__tests__/whoami.test.tsx +60 -1
- package/src/api/dev.ts +49 -9
- package/src/bundle.ts +298 -37
- package/src/cfetch/internal.ts +34 -2
- package/src/config/config.ts +15 -3
- package/src/config/environment.ts +40 -8
- package/src/config/index.ts +13 -0
- package/src/config/validation.ts +111 -9
- package/src/create-worker-preview.ts +3 -1
- package/src/create-worker-upload-form.ts +25 -0
- package/src/dev/dev.tsx +145 -31
- package/src/dev/local.tsx +116 -24
- package/src/dev/remote.tsx +39 -12
- package/src/dev/use-esbuild.ts +28 -0
- package/src/dev/validate-dev-props.ts +31 -0
- package/src/dev-registry.tsx +160 -0
- package/src/dev.tsx +148 -67
- package/src/generate.ts +112 -14
- package/src/index.tsx +252 -7
- package/src/inspect.ts +90 -5
- package/src/metrics/index.ts +1 -0
- package/src/metrics/metrics-dispatcher.ts +1 -0
- package/src/metrics/metrics-usage-headers.ts +24 -0
- package/src/metrics/send-event.ts +2 -2
- package/src/miniflare-cli/assets.ts +546 -0
- package/src/miniflare-cli/index.ts +157 -6
- package/src/module-collection.ts +3 -3
- package/src/pages/build.tsx +36 -28
- package/src/pages/constants.ts +4 -0
- package/src/pages/deployments.tsx +10 -10
- package/src/pages/dev.tsx +155 -651
- package/src/pages/functions/buildPlugin.ts +4 -0
- package/src/pages/functions/buildWorker.ts +4 -0
- package/src/pages/functions/routes-consolidation.test.ts +66 -0
- package/src/pages/functions/routes-consolidation.ts +29 -0
- package/src/pages/functions/routes-transformation.test.ts +271 -0
- package/src/pages/functions/routes-transformation.ts +125 -0
- package/src/pages/projects.tsx +9 -3
- package/src/pages/publish.tsx +57 -15
- package/src/pages/types.ts +9 -0
- package/src/pages/upload.tsx +38 -21
- package/src/publish.ts +139 -112
- package/src/r2.ts +81 -0
- package/src/tail/index.ts +15 -2
- package/src/tail/printing.ts +41 -3
- package/src/user/choose-account.tsx +20 -11
- package/src/user/user.tsx +20 -2
- package/src/whoami.tsx +79 -1
- package/src/worker.ts +12 -0
- package/templates/first-party-worker-module-facade.ts +18 -0
- package/templates/format-dev-errors.ts +32 -0
- package/templates/pages-shim.ts +9 -0
- package/templates/{static-asset-facade.js → serve-static-assets.ts} +21 -7
- package/templates/service-bindings-module-facade.js +51 -0
- package/templates/service-bindings-sw-facade.js +39 -0
- package/wrangler-dist/cli.d.ts +38 -3
- package/wrangler-dist/cli.js +45244 -25199
package/src/pages/dev.tsx
CHANGED
|
@@ -1,42 +1,28 @@
|
|
|
1
1
|
import { execSync, spawn } from "node:child_process";
|
|
2
|
-
import { existsSync
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join, resolve
|
|
5
|
-
import { URL } from "node:url";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
6
5
|
import { watch } from "chokidar";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { build as workerJsBuild } from "esbuild";
|
|
7
|
+
import { unstable_dev } from "../api";
|
|
9
8
|
import { FatalError } from "../errors";
|
|
10
9
|
import { logger } from "../logger";
|
|
11
10
|
import * as metrics from "../metrics";
|
|
12
|
-
import { getRequestContextCheckOptions } from "../miniflare-cli/request-context";
|
|
13
|
-
import openInBrowser from "../open-in-browser";
|
|
14
11
|
import { buildFunctions } from "./build";
|
|
15
12
|
import { SECONDS_TO_WAIT_FOR_PROXY } from "./constants";
|
|
16
13
|
import { CLEANUP, CLEANUP_CALLBACKS, pagesBetaWarning } from "./utils";
|
|
17
|
-
import type {
|
|
18
|
-
import type {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
command?: string;
|
|
28
|
-
local: boolean;
|
|
29
|
-
port: number;
|
|
30
|
-
proxy?: number;
|
|
31
|
-
"script-path": string;
|
|
32
|
-
binding?: (string | number)[];
|
|
33
|
-
kv?: (string | number)[];
|
|
34
|
-
do?: (string | number)[];
|
|
35
|
-
"live-reload": boolean;
|
|
36
|
-
"node-compat": boolean;
|
|
37
|
-
};
|
|
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
|
+
);
|
|
22
|
+
|
|
23
|
+
type PagesDevArgs = YargsOptionsToInterface<typeof Options>;
|
|
38
24
|
|
|
39
|
-
export function Options(yargs: Argv)
|
|
25
|
+
export function Options(yargs: Argv) {
|
|
40
26
|
return yargs
|
|
41
27
|
.positional("directory", {
|
|
42
28
|
type: "string",
|
|
@@ -54,11 +40,20 @@ export function Options(yargs: Argv): Argv<PagesDevArgs> {
|
|
|
54
40
|
default: true,
|
|
55
41
|
description: "Run on my machine",
|
|
56
42
|
},
|
|
43
|
+
ip: {
|
|
44
|
+
type: "string",
|
|
45
|
+
default: "0.0.0.0",
|
|
46
|
+
description: "The IP address to listen on",
|
|
47
|
+
},
|
|
57
48
|
port: {
|
|
58
49
|
type: "number",
|
|
59
50
|
default: 8788,
|
|
60
51
|
description: "The port to listen on (serve from)",
|
|
61
52
|
},
|
|
53
|
+
"inspector-port": {
|
|
54
|
+
type: "number",
|
|
55
|
+
describe: "Port for devtools to connect to",
|
|
56
|
+
},
|
|
62
57
|
proxy: {
|
|
63
58
|
type: "number",
|
|
64
59
|
description: "The port to proxy (where the static assets are served)",
|
|
@@ -76,19 +71,32 @@ export function Options(yargs: Argv): Argv<PagesDevArgs> {
|
|
|
76
71
|
},
|
|
77
72
|
kv: {
|
|
78
73
|
type: "array",
|
|
79
|
-
description: "KV namespace to bind",
|
|
74
|
+
description: "KV namespace to bind (--kv KV_BINDING)",
|
|
80
75
|
alias: "k",
|
|
81
76
|
},
|
|
82
77
|
do: {
|
|
83
78
|
type: "array",
|
|
84
|
-
description: "Durable Object to bind (NAME=CLASS)",
|
|
79
|
+
description: "Durable Object to bind (--do NAME=CLASS)",
|
|
85
80
|
alias: "o",
|
|
86
81
|
},
|
|
82
|
+
r2: {
|
|
83
|
+
type: "array",
|
|
84
|
+
description: "R2 bucket to bind (--r2 R2_BINDING)",
|
|
85
|
+
},
|
|
87
86
|
"live-reload": {
|
|
88
87
|
type: "boolean",
|
|
89
88
|
default: false,
|
|
90
89
|
description: "Auto reload HTML pages when change is detected",
|
|
91
90
|
},
|
|
91
|
+
"local-protocol": {
|
|
92
|
+
describe: "Protocol to listen to requests on, defaults to http.",
|
|
93
|
+
choices: ["http", "https"] as const,
|
|
94
|
+
},
|
|
95
|
+
"experimental-enable-local-persistence": {
|
|
96
|
+
type: "boolean",
|
|
97
|
+
default: false,
|
|
98
|
+
describe: "Enable persistence for this session (only for local mode)",
|
|
99
|
+
},
|
|
92
100
|
"node-compat": {
|
|
93
101
|
describe: "Enable node.js compatibility",
|
|
94
102
|
default: false,
|
|
@@ -100,7 +108,6 @@ export function Options(yargs: Argv): Argv<PagesDevArgs> {
|
|
|
100
108
|
type: "string",
|
|
101
109
|
hidden: true,
|
|
102
110
|
},
|
|
103
|
-
// // TODO: Miniflare user options
|
|
104
111
|
})
|
|
105
112
|
.epilogue(pagesBetaWarning);
|
|
106
113
|
}
|
|
@@ -108,17 +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,
|
|
128
|
+
"local-protocol": localProtocol,
|
|
129
|
+
"experimental-enable-local-persistence": experimentalEnableLocalPersistence,
|
|
118
130
|
"node-compat": nodeCompat,
|
|
119
131
|
config: config,
|
|
120
132
|
_: [_pages, _dev, ...remaining],
|
|
121
|
-
}:
|
|
133
|
+
}: PagesDevArgs) => {
|
|
122
134
|
// Beta message for `wrangler pages <commands>` usage
|
|
123
135
|
logger.log(pagesBetaWarning);
|
|
124
136
|
|
|
@@ -133,27 +145,35 @@ export const Handler = async ({
|
|
|
133
145
|
const functionsDirectory = "./functions";
|
|
134
146
|
const usingFunctions = existsSync(functionsDirectory);
|
|
135
147
|
|
|
136
|
-
const command = remaining
|
|
148
|
+
const command = remaining;
|
|
137
149
|
|
|
138
|
-
let proxyPort: number |
|
|
150
|
+
let proxyPort: number | undefined;
|
|
139
151
|
|
|
140
|
-
if (directory
|
|
152
|
+
if (directory !== undefined && command.length > 0) {
|
|
153
|
+
throw new FatalError(
|
|
154
|
+
"Specify either a directory OR a proxy command, not both.",
|
|
155
|
+
1
|
|
156
|
+
);
|
|
157
|
+
} else if (directory === undefined) {
|
|
141
158
|
proxyPort = await spawnProxyProcess({
|
|
142
159
|
port: requestedProxyPort,
|
|
143
160
|
command,
|
|
144
161
|
});
|
|
145
162
|
if (proxyPort === undefined) return undefined;
|
|
163
|
+
} else {
|
|
164
|
+
directory = resolve(directory);
|
|
146
165
|
}
|
|
147
166
|
|
|
148
|
-
let miniflareArgs: MiniflareOptions = {};
|
|
149
|
-
|
|
150
167
|
let scriptReadyResolve: () => void;
|
|
151
168
|
const scriptReadyPromise = new Promise<void>(
|
|
152
|
-
(
|
|
169
|
+
(promiseResolve) => (scriptReadyResolve = promiseResolve)
|
|
153
170
|
);
|
|
154
171
|
|
|
172
|
+
let scriptPath: string;
|
|
173
|
+
|
|
155
174
|
if (usingFunctions) {
|
|
156
175
|
const outfile = join(tmpdir(), `./functionsWorker-${Math.random()}.js`);
|
|
176
|
+
scriptPath = outfile;
|
|
157
177
|
|
|
158
178
|
if (nodeCompat) {
|
|
159
179
|
console.warn(
|
|
@@ -191,160 +211,119 @@ export const Handler = async ({
|
|
|
191
211
|
});
|
|
192
212
|
await metrics.sendMetricsEvent("build pages functions");
|
|
193
213
|
});
|
|
194
|
-
|
|
195
|
-
miniflareArgs = {
|
|
196
|
-
scriptPath: outfile,
|
|
197
|
-
};
|
|
198
214
|
} else {
|
|
199
215
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
200
216
|
scriptReadyResolve!();
|
|
201
217
|
|
|
202
|
-
|
|
218
|
+
scriptPath =
|
|
203
219
|
directory !== undefined
|
|
204
220
|
? join(directory, singleWorkerScriptPath)
|
|
205
221
|
: singleWorkerScriptPath;
|
|
206
222
|
|
|
207
|
-
if (existsSync(scriptPath)) {
|
|
208
|
-
miniflareArgs = {
|
|
209
|
-
scriptPath,
|
|
210
|
-
};
|
|
211
|
-
} else {
|
|
223
|
+
if (!existsSync(scriptPath)) {
|
|
212
224
|
logger.log("No functions. Shimming...");
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return new Response(response.body, response)
|
|
224
|
-
}
|
|
225
|
-
}`,
|
|
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 {}
|
|
226
235
|
};
|
|
236
|
+
await runBuild();
|
|
237
|
+
watch([scriptPath], {
|
|
238
|
+
persistent: true,
|
|
239
|
+
ignoreInitial: true,
|
|
240
|
+
}).on("all", async () => {
|
|
241
|
+
await runBuild();
|
|
242
|
+
});
|
|
227
243
|
}
|
|
228
244
|
}
|
|
229
245
|
|
|
230
|
-
// Defer importing miniflare until we really need it
|
|
231
|
-
const { Miniflare, Log, LogLevel } = await import("miniflare");
|
|
232
|
-
const { Response, fetch } = await import("@miniflare/core");
|
|
233
|
-
|
|
234
|
-
// Wait for esbuild to finish building before starting Miniflare.
|
|
235
|
-
// This must be before the call to `new Miniflare`, as that will
|
|
236
|
-
// asynchronously start loading the script. `await startServer()`
|
|
237
|
-
// internally just waits for that promise to resolve.
|
|
238
246
|
await scriptReadyPromise;
|
|
239
247
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const miniflare = new Miniflare({
|
|
254
|
-
port,
|
|
255
|
-
watch: true,
|
|
256
|
-
modules: true,
|
|
257
|
-
|
|
258
|
-
log: new Log(LogLevel.ERROR, { prefix: "pages" }),
|
|
259
|
-
logUnhandledRejections: true,
|
|
260
|
-
sourceMap: true,
|
|
261
|
-
|
|
262
|
-
kvNamespaces: kvs.map((kv) => kv.toString()),
|
|
263
|
-
|
|
264
|
-
durableObjects: Object.fromEntries(
|
|
265
|
-
durableObjects.map((durableObject) => durableObject.toString().split("="))
|
|
266
|
-
),
|
|
267
|
-
|
|
268
|
-
// User bindings
|
|
269
|
-
bindings: {
|
|
270
|
-
...vars,
|
|
271
|
-
...Object.fromEntries(
|
|
248
|
+
const { stop, waitUntilExit } = await unstable_dev(
|
|
249
|
+
scriptPath,
|
|
250
|
+
{
|
|
251
|
+
ip,
|
|
252
|
+
port,
|
|
253
|
+
inspectorPort,
|
|
254
|
+
watch: true,
|
|
255
|
+
localProtocol,
|
|
256
|
+
liveReload,
|
|
257
|
+
|
|
258
|
+
compatibilityDate: "2021-11-02",
|
|
259
|
+
nodeCompat,
|
|
260
|
+
vars: Object.fromEntries(
|
|
272
261
|
bindings
|
|
273
262
|
.map((binding) => binding.toString().split("="))
|
|
274
263
|
.map(([key, ...values]) => [key, values.join("=")])
|
|
275
264
|
),
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
logger.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return new Response(
|
|
291
|
-
`[wrangler] Could not proxy request: ${thrown}`,
|
|
292
|
-
{ status: 502 }
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
} else {
|
|
296
|
-
try {
|
|
297
|
-
return await assetsFetch(request);
|
|
298
|
-
} catch (thrown) {
|
|
299
|
-
logger.error(`Could not serve static asset: ${thrown}`);
|
|
300
|
-
|
|
301
|
-
// TODO: Pretty error page
|
|
302
|
-
return new Response(
|
|
303
|
-
`[wrangler] Could not serve static asset: ${thrown}`,
|
|
304
|
-
{ status: 502 }
|
|
265
|
+
kv: kvs.map((val) => ({
|
|
266
|
+
binding: val.toString(),
|
|
267
|
+
id: "",
|
|
268
|
+
})),
|
|
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()
|
|
305
279
|
);
|
|
280
|
+
return;
|
|
306
281
|
}
|
|
307
|
-
|
|
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: "" };
|
|
292
|
+
}),
|
|
293
|
+
|
|
294
|
+
enablePagesAssetsServiceBinding: {
|
|
295
|
+
proxyPort,
|
|
296
|
+
directory,
|
|
308
297
|
},
|
|
298
|
+
forceLocal: true,
|
|
299
|
+
experimentalEnableLocalPersistence,
|
|
300
|
+
showInteractiveDevSession: undefined,
|
|
301
|
+
inspect: true,
|
|
302
|
+
logLevel: "error",
|
|
303
|
+
logPrefix: "pages",
|
|
309
304
|
},
|
|
305
|
+
true
|
|
306
|
+
);
|
|
307
|
+
await metrics.sendMetricsEvent("run pages dev");
|
|
310
308
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
...requestContextCheckOptions,
|
|
317
|
-
...miniflareArgs,
|
|
309
|
+
waitUntilExit().then(() => {
|
|
310
|
+
CLEANUP();
|
|
311
|
+
stop();
|
|
312
|
+
process.exit(0);
|
|
318
313
|
});
|
|
319
314
|
|
|
320
|
-
|
|
321
|
-
// `startServer` might throw if user code contains errors
|
|
322
|
-
const server = await miniflare.startServer();
|
|
323
|
-
logger.log(`Serving at http://localhost:${port}/`);
|
|
324
|
-
await metrics.sendMetricsEvent("run pages dev");
|
|
325
|
-
|
|
326
|
-
if (process.env.BROWSER !== "none") {
|
|
327
|
-
await openInBrowser(`http://localhost:${port}/`);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (directory !== undefined && liveReload) {
|
|
331
|
-
watch([directory], {
|
|
332
|
-
persistent: true,
|
|
333
|
-
ignoreInitial: true,
|
|
334
|
-
}).on("all", async () => {
|
|
335
|
-
await miniflare.reload();
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
CLEANUP_CALLBACKS.push(() => {
|
|
340
|
-
server.close();
|
|
341
|
-
miniflare.dispose().catch((err) => miniflare.log.error(err));
|
|
342
|
-
});
|
|
343
|
-
} catch (e) {
|
|
344
|
-
miniflare.log.error(e as Error);
|
|
315
|
+
process.on("exit", () => {
|
|
345
316
|
CLEANUP();
|
|
346
|
-
|
|
347
|
-
}
|
|
317
|
+
stop();
|
|
318
|
+
});
|
|
319
|
+
process.on("SIGINT", () => {
|
|
320
|
+
CLEANUP();
|
|
321
|
+
stop();
|
|
322
|
+
});
|
|
323
|
+
process.on("SIGTERM", () => {
|
|
324
|
+
CLEANUP();
|
|
325
|
+
stop();
|
|
326
|
+
});
|
|
348
327
|
};
|
|
349
328
|
|
|
350
329
|
function isWindows() {
|
|
@@ -352,7 +331,7 @@ function isWindows() {
|
|
|
352
331
|
}
|
|
353
332
|
|
|
354
333
|
async function sleep(ms: number) {
|
|
355
|
-
await new Promise((
|
|
334
|
+
await new Promise((promiseResolve) => setTimeout(promiseResolve, ms));
|
|
356
335
|
}
|
|
357
336
|
|
|
358
337
|
function getPids(pid: number) {
|
|
@@ -415,7 +394,7 @@ async function spawnProxyProcess({
|
|
|
415
394
|
}: {
|
|
416
395
|
port?: number;
|
|
417
396
|
command: (string | number)[];
|
|
418
|
-
}): Promise<
|
|
397
|
+
}): Promise<undefined | number> {
|
|
419
398
|
if (command.length === 0) {
|
|
420
399
|
CLEANUP();
|
|
421
400
|
throw new FatalError(
|
|
@@ -480,489 +459,14 @@ async function spawnProxyProcess({
|
|
|
480
459
|
return port;
|
|
481
460
|
}
|
|
482
461
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
function replacer(str: string, replacements: Replacements) {
|
|
490
|
-
for (const [replacement, value] of Object.entries(replacements)) {
|
|
491
|
-
str = str.replace(`:${replacement}`, value);
|
|
492
|
-
}
|
|
493
|
-
return str;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function generateRulesMatcher<T>(
|
|
497
|
-
rules?: Record<string, T>,
|
|
498
|
-
replacerFn: (match: T, replacements: Replacements) => T = (match) => match
|
|
499
|
-
) {
|
|
500
|
-
// TODO: How can you test cross-host rules?
|
|
501
|
-
if (!rules) return () => [];
|
|
502
|
-
|
|
503
|
-
const compiledRules = Object.entries(rules)
|
|
504
|
-
.map(([rule, match]) => {
|
|
505
|
-
const crossHost = rule.startsWith("https://");
|
|
506
|
-
|
|
507
|
-
rule = rule.split("*").map(escapeRegex).join("(?<splat>.*)");
|
|
508
|
-
|
|
509
|
-
const host_matches = rule.matchAll(
|
|
510
|
-
/(?<=^https:\\\/\\\/[^/]*?):([^\\]+)(?=\\)/g
|
|
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.`
|
|
511
468
|
);
|
|
512
|
-
|
|
513
|
-
rule = rule.split(hostMatch[0]).join(`(?<${hostMatch[1]}>[^/.]+)`);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const path_matches = rule.matchAll(/:(\w+)/g);
|
|
517
|
-
for (const pathMatch of path_matches) {
|
|
518
|
-
rule = rule.split(pathMatch[0]).join(`(?<${pathMatch[1]}>[^/]+)`);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
rule = "^" + rule + "$";
|
|
522
|
-
|
|
523
|
-
try {
|
|
524
|
-
const regExp = new RegExp(rule);
|
|
525
|
-
return [{ crossHost, regExp }, match];
|
|
526
|
-
} catch {}
|
|
527
|
-
})
|
|
528
|
-
.filter((value) => value !== undefined) as [
|
|
529
|
-
{ crossHost: boolean; regExp: RegExp },
|
|
530
|
-
T
|
|
531
|
-
][];
|
|
532
|
-
|
|
533
|
-
return ({ request }: { request: MiniflareRequest }) => {
|
|
534
|
-
const { pathname, host } = new URL(request.url);
|
|
535
|
-
|
|
536
|
-
return compiledRules
|
|
537
|
-
.map(([{ crossHost, regExp }, match]) => {
|
|
538
|
-
const test = crossHost ? `https://${host}${pathname}` : pathname;
|
|
539
|
-
const result = regExp.exec(test);
|
|
540
|
-
if (result) {
|
|
541
|
-
return replacerFn(match, result.groups || {});
|
|
542
|
-
}
|
|
543
|
-
})
|
|
544
|
-
.filter((value) => value !== undefined) as T[];
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
function generateHeadersMatcher(headersFile: string) {
|
|
549
|
-
if (existsSync(headersFile)) {
|
|
550
|
-
const contents = readFileSync(headersFile).toString();
|
|
551
|
-
|
|
552
|
-
// TODO: Log errors
|
|
553
|
-
const lines = contents
|
|
554
|
-
.split("\n")
|
|
555
|
-
.map((line) => line.trim())
|
|
556
|
-
.filter((line) => !line.startsWith("#") && line !== "");
|
|
557
|
-
|
|
558
|
-
const rules: Record<string, Record<string, string>> = {};
|
|
559
|
-
let rule: { path: string; headers: Record<string, string> } | undefined =
|
|
560
|
-
undefined;
|
|
561
|
-
|
|
562
|
-
for (const line of lines) {
|
|
563
|
-
if (/^([^\s]+:\/\/|^\/)/.test(line)) {
|
|
564
|
-
if (rule && Object.keys(rule.headers).length > 0) {
|
|
565
|
-
rules[rule.path] = rule.headers;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
const path = validateURL(line);
|
|
569
|
-
if (path) {
|
|
570
|
-
rule = {
|
|
571
|
-
path,
|
|
572
|
-
headers: {},
|
|
573
|
-
};
|
|
574
|
-
continue;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
if (!line.includes(":")) continue;
|
|
579
|
-
|
|
580
|
-
const [rawName, ...rawValue] = line.split(":");
|
|
581
|
-
const name = rawName.trim().toLowerCase();
|
|
582
|
-
const value = rawValue.join(":").trim();
|
|
583
|
-
|
|
584
|
-
if (name === "") continue;
|
|
585
|
-
if (!rule) continue;
|
|
586
|
-
|
|
587
|
-
const existingValues = rule.headers[name];
|
|
588
|
-
rule.headers[name] = existingValues
|
|
589
|
-
? `${existingValues}, ${value}`
|
|
590
|
-
: value;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
if (rule && Object.keys(rule.headers).length > 0) {
|
|
594
|
-
rules[rule.path] = rule.headers;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const rulesMatcher = generateRulesMatcher(rules, (match, replacements) =>
|
|
598
|
-
Object.fromEntries(
|
|
599
|
-
Object.entries(match).map(([name, value]) => [
|
|
600
|
-
name,
|
|
601
|
-
replacer(value, replacements),
|
|
602
|
-
])
|
|
603
|
-
)
|
|
604
|
-
);
|
|
605
|
-
|
|
606
|
-
return (request: MiniflareRequest) => {
|
|
607
|
-
const matches = rulesMatcher({
|
|
608
|
-
request,
|
|
609
|
-
});
|
|
610
|
-
if (matches) return matches;
|
|
611
|
-
};
|
|
612
|
-
} else {
|
|
613
|
-
return () => undefined;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function generateRedirectsMatcher(redirectsFile: string) {
|
|
618
|
-
if (existsSync(redirectsFile)) {
|
|
619
|
-
const contents = readFileSync(redirectsFile).toString();
|
|
620
|
-
|
|
621
|
-
// TODO: Log errors
|
|
622
|
-
const lines = contents
|
|
623
|
-
.split("\n")
|
|
624
|
-
.map((line) => line.trim())
|
|
625
|
-
.filter((line) => !line.startsWith("#") && line !== "");
|
|
626
|
-
|
|
627
|
-
const rules = Object.fromEntries(
|
|
628
|
-
lines
|
|
629
|
-
.map((line) => line.split(" "))
|
|
630
|
-
.filter((tokens) => tokens.length === 2 || tokens.length === 3)
|
|
631
|
-
.map((tokens) => {
|
|
632
|
-
const from = validateURL(tokens[0], true, false, false);
|
|
633
|
-
const to = validateURL(tokens[1], false, true, true);
|
|
634
|
-
let status: number | undefined = parseInt(tokens[2]) || 302;
|
|
635
|
-
status = [301, 302, 303, 307, 308].includes(status)
|
|
636
|
-
? status
|
|
637
|
-
: undefined;
|
|
638
|
-
|
|
639
|
-
return from && to && status ? [from, { to, status }] : undefined;
|
|
640
|
-
})
|
|
641
|
-
.filter((rule) => rule !== undefined) as [
|
|
642
|
-
string,
|
|
643
|
-
{ to: string; status?: number }
|
|
644
|
-
][]
|
|
645
|
-
);
|
|
646
|
-
|
|
647
|
-
const rulesMatcher = generateRulesMatcher(
|
|
648
|
-
rules,
|
|
649
|
-
({ status, to }, replacements) => ({
|
|
650
|
-
status,
|
|
651
|
-
to: replacer(to, replacements),
|
|
652
|
-
})
|
|
653
|
-
);
|
|
654
|
-
|
|
655
|
-
return (request: MiniflareRequest) => {
|
|
656
|
-
const match = rulesMatcher({
|
|
657
|
-
request,
|
|
658
|
-
})[0];
|
|
659
|
-
if (match) return match;
|
|
660
|
-
};
|
|
661
|
-
} else {
|
|
662
|
-
return () => undefined;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function extractPathname(
|
|
667
|
-
path = "/",
|
|
668
|
-
includeSearch: boolean,
|
|
669
|
-
includeHash: boolean
|
|
670
|
-
) {
|
|
671
|
-
if (!path.startsWith("/")) path = `/${path}`;
|
|
672
|
-
const url = new URL(`//${path}`, "relative://");
|
|
673
|
-
return `${url.pathname}${includeSearch ? url.search : ""}${
|
|
674
|
-
includeHash ? url.hash : ""
|
|
675
|
-
}`;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function validateURL(
|
|
679
|
-
token: string,
|
|
680
|
-
onlyRelative = false,
|
|
681
|
-
includeSearch = false,
|
|
682
|
-
includeHash = false
|
|
683
|
-
) {
|
|
684
|
-
const host = /^https:\/\/+(?<host>[^/]+)\/?(?<path>.*)/.exec(token);
|
|
685
|
-
if (host && host.groups && host.groups.host) {
|
|
686
|
-
if (onlyRelative) return;
|
|
687
|
-
|
|
688
|
-
return `https://${host.groups.host}${extractPathname(
|
|
689
|
-
host.groups.path,
|
|
690
|
-
includeSearch,
|
|
691
|
-
includeHash
|
|
692
|
-
)}`;
|
|
693
|
-
} else {
|
|
694
|
-
if (!token.startsWith("/") && onlyRelative) token = `/${token}`;
|
|
695
|
-
|
|
696
|
-
const path = /^\//.exec(token);
|
|
697
|
-
if (path) {
|
|
698
|
-
try {
|
|
699
|
-
return extractPathname(token, includeSearch, includeHash);
|
|
700
|
-
} catch {}
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
return "";
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function hasFileExtension(pathname: string) {
|
|
707
|
-
return /\/.+\.[a-z0-9]+$/i.test(pathname);
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
async function generateAssetsFetch(
|
|
711
|
-
directory: string
|
|
712
|
-
): Promise<typeof miniflareFetch> {
|
|
713
|
-
// Defer importing miniflare until we really need it
|
|
714
|
-
const { Headers, Request, Response } = await import("@miniflare/core");
|
|
715
|
-
|
|
716
|
-
const headersFile = join(directory, "_headers");
|
|
717
|
-
const redirectsFile = join(directory, "_redirects");
|
|
718
|
-
const workerFile = join(directory, "_worker.js");
|
|
719
|
-
|
|
720
|
-
const ignoredFiles = [headersFile, redirectsFile, workerFile];
|
|
721
|
-
|
|
722
|
-
const assetExists = (path: string) => {
|
|
723
|
-
path = join(directory, path);
|
|
724
|
-
return (
|
|
725
|
-
existsSync(path) &&
|
|
726
|
-
lstatSync(path).isFile() &&
|
|
727
|
-
!ignoredFiles.includes(path)
|
|
728
|
-
);
|
|
729
|
-
};
|
|
730
|
-
|
|
731
|
-
const getAsset = (path: string) => {
|
|
732
|
-
if (assetExists(path)) {
|
|
733
|
-
return join(directory, path);
|
|
734
|
-
}
|
|
735
|
-
};
|
|
736
|
-
|
|
737
|
-
let redirectsMatcher = generateRedirectsMatcher(redirectsFile);
|
|
738
|
-
let headersMatcher = generateHeadersMatcher(headersFile);
|
|
739
|
-
|
|
740
|
-
watch([headersFile, redirectsFile], {
|
|
741
|
-
persistent: true,
|
|
742
|
-
}).on("change", (path) => {
|
|
743
|
-
switch (path) {
|
|
744
|
-
case headersFile: {
|
|
745
|
-
logger.log("_headers modified. Re-evaluating...");
|
|
746
|
-
headersMatcher = generateHeadersMatcher(headersFile);
|
|
747
|
-
break;
|
|
748
|
-
}
|
|
749
|
-
case redirectsFile: {
|
|
750
|
-
logger.log("_redirects modified. Re-evaluating...");
|
|
751
|
-
redirectsMatcher = generateRedirectsMatcher(redirectsFile);
|
|
752
|
-
break;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
const serveAsset = (file: string) => {
|
|
758
|
-
return readFileSync(file);
|
|
759
|
-
};
|
|
760
|
-
|
|
761
|
-
const generateResponse = (request: MiniflareRequest) => {
|
|
762
|
-
const url = new URL(request.url);
|
|
763
|
-
|
|
764
|
-
const deconstructedResponse: {
|
|
765
|
-
status: number;
|
|
766
|
-
headers: MiniflareHeaders;
|
|
767
|
-
body?: Buffer;
|
|
768
|
-
} = {
|
|
769
|
-
status: 200,
|
|
770
|
-
headers: new Headers(),
|
|
771
|
-
body: undefined,
|
|
772
|
-
};
|
|
773
|
-
|
|
774
|
-
const match = redirectsMatcher(request);
|
|
775
|
-
if (match) {
|
|
776
|
-
const { status, to } = match;
|
|
777
|
-
|
|
778
|
-
let location = to;
|
|
779
|
-
let search;
|
|
780
|
-
|
|
781
|
-
if (to.startsWith("/")) {
|
|
782
|
-
search = new URL(location, "http://fakehost").search;
|
|
783
|
-
} else {
|
|
784
|
-
search = new URL(location).search;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
location = `${location}${search ? "" : url.search}`;
|
|
788
|
-
|
|
789
|
-
if (status && [301, 302, 303, 307, 308].includes(status)) {
|
|
790
|
-
deconstructedResponse.status = status;
|
|
791
|
-
} else {
|
|
792
|
-
deconstructedResponse.status = 302;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
deconstructedResponse.headers.set("Location", location);
|
|
796
|
-
return deconstructedResponse;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
if (!request.method?.match(/^(get|head)$/i)) {
|
|
800
|
-
deconstructedResponse.status = 405;
|
|
801
|
-
return deconstructedResponse;
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
const notFound = () => {
|
|
805
|
-
let cwd = url.pathname;
|
|
806
|
-
while (cwd) {
|
|
807
|
-
cwd = cwd.slice(0, cwd.lastIndexOf("/"));
|
|
808
|
-
|
|
809
|
-
if ((asset = getAsset(`${cwd}/404.html`))) {
|
|
810
|
-
deconstructedResponse.status = 404;
|
|
811
|
-
deconstructedResponse.body = serveAsset(asset);
|
|
812
|
-
deconstructedResponse.headers.set(
|
|
813
|
-
"Content-Type",
|
|
814
|
-
getType(asset) || "application/octet-stream"
|
|
815
|
-
);
|
|
816
|
-
return deconstructedResponse;
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
if ((asset = getAsset(`/index.html`))) {
|
|
821
|
-
deconstructedResponse.body = serveAsset(asset);
|
|
822
|
-
deconstructedResponse.headers.set(
|
|
823
|
-
"Content-Type",
|
|
824
|
-
getType(asset) || "application/octet-stream"
|
|
825
|
-
);
|
|
826
|
-
return deconstructedResponse;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
deconstructedResponse.status = 404;
|
|
830
|
-
return deconstructedResponse;
|
|
831
|
-
};
|
|
832
|
-
|
|
833
|
-
let asset;
|
|
834
|
-
|
|
835
|
-
if (url.pathname.endsWith("/")) {
|
|
836
|
-
if ((asset = getAsset(`${url.pathname}/index.html`))) {
|
|
837
|
-
deconstructedResponse.body = serveAsset(asset);
|
|
838
|
-
deconstructedResponse.headers.set(
|
|
839
|
-
"Content-Type",
|
|
840
|
-
getType(asset) || "application/octet-stream"
|
|
841
|
-
);
|
|
842
|
-
return deconstructedResponse;
|
|
843
|
-
} else if (
|
|
844
|
-
(asset = getAsset(`${url.pathname.replace(/\/$/, ".html")}`))
|
|
845
|
-
) {
|
|
846
|
-
deconstructedResponse.status = 301;
|
|
847
|
-
deconstructedResponse.headers.set(
|
|
848
|
-
"Location",
|
|
849
|
-
`${url.pathname.slice(0, -1)}${url.search}`
|
|
850
|
-
);
|
|
851
|
-
return deconstructedResponse;
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
if (url.pathname.endsWith("/index")) {
|
|
856
|
-
deconstructedResponse.status = 301;
|
|
857
|
-
deconstructedResponse.headers.set(
|
|
858
|
-
"Location",
|
|
859
|
-
`${url.pathname.slice(0, -"index".length)}${url.search}`
|
|
860
|
-
);
|
|
861
|
-
return deconstructedResponse;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
if ((asset = getAsset(url.pathname))) {
|
|
865
|
-
if (url.pathname.endsWith(".html")) {
|
|
866
|
-
const extensionlessPath = url.pathname.slice(0, -".html".length);
|
|
867
|
-
if (getAsset(extensionlessPath) || extensionlessPath === "/") {
|
|
868
|
-
deconstructedResponse.body = serveAsset(asset);
|
|
869
|
-
deconstructedResponse.headers.set(
|
|
870
|
-
"Content-Type",
|
|
871
|
-
getType(asset) || "application/octet-stream"
|
|
872
|
-
);
|
|
873
|
-
return deconstructedResponse;
|
|
874
|
-
} else {
|
|
875
|
-
deconstructedResponse.status = 301;
|
|
876
|
-
deconstructedResponse.headers.set(
|
|
877
|
-
"Location",
|
|
878
|
-
`${extensionlessPath}${url.search}`
|
|
879
|
-
);
|
|
880
|
-
return deconstructedResponse;
|
|
881
|
-
}
|
|
882
|
-
} else {
|
|
883
|
-
deconstructedResponse.body = serveAsset(asset);
|
|
884
|
-
deconstructedResponse.headers.set(
|
|
885
|
-
"Content-Type",
|
|
886
|
-
getType(asset) || "application/octet-stream"
|
|
887
|
-
);
|
|
888
|
-
return deconstructedResponse;
|
|
889
|
-
}
|
|
890
|
-
} else if (hasFileExtension(url.pathname)) {
|
|
891
|
-
notFound();
|
|
892
|
-
return deconstructedResponse;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
if ((asset = getAsset(`${url.pathname}.html`))) {
|
|
896
|
-
deconstructedResponse.body = serveAsset(asset);
|
|
897
|
-
deconstructedResponse.headers.set(
|
|
898
|
-
"Content-Type",
|
|
899
|
-
getType(asset) || "application/octet-stream"
|
|
900
|
-
);
|
|
901
|
-
return deconstructedResponse;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
if ((asset = getAsset(`${url.pathname}/index.html`))) {
|
|
905
|
-
deconstructedResponse.status = 301;
|
|
906
|
-
deconstructedResponse.headers.set(
|
|
907
|
-
"Location",
|
|
908
|
-
`${url.pathname}/${url.search}`
|
|
909
|
-
);
|
|
910
|
-
return deconstructedResponse;
|
|
911
|
-
} else {
|
|
912
|
-
notFound();
|
|
913
|
-
return deconstructedResponse;
|
|
914
|
-
}
|
|
915
|
-
};
|
|
916
|
-
|
|
917
|
-
const attachHeaders = (
|
|
918
|
-
request: MiniflareRequest,
|
|
919
|
-
deconstructedResponse: {
|
|
920
|
-
status: number;
|
|
921
|
-
headers: MiniflareHeaders;
|
|
922
|
-
body?: Buffer;
|
|
923
|
-
}
|
|
924
|
-
) => {
|
|
925
|
-
const headers = deconstructedResponse.headers;
|
|
926
|
-
const newHeaders = new Headers({});
|
|
927
|
-
const matches = headersMatcher(request) || [];
|
|
928
|
-
|
|
929
|
-
matches.forEach((match) => {
|
|
930
|
-
Object.entries(match).forEach(([name, value]) => {
|
|
931
|
-
newHeaders.append(name, `${value}`);
|
|
932
|
-
});
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
const combinedHeaders = {
|
|
936
|
-
...Object.fromEntries(headers.entries()),
|
|
937
|
-
...Object.fromEntries(newHeaders.entries()),
|
|
938
|
-
};
|
|
939
|
-
|
|
940
|
-
deconstructedResponse.headers = new Headers({});
|
|
941
|
-
Object.entries(combinedHeaders).forEach(([name, value]) => {
|
|
942
|
-
if (value) deconstructedResponse.headers.set(name, value);
|
|
469
|
+
return null;
|
|
943
470
|
});
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
return async (input, init) => {
|
|
947
|
-
const request = new Request(input, init);
|
|
948
|
-
const deconstructedResponse = generateResponse(request);
|
|
949
|
-
attachHeaders(request, deconstructedResponse);
|
|
950
|
-
|
|
951
|
-
const headers = new Headers();
|
|
952
|
-
|
|
953
|
-
[...deconstructedResponse.headers.entries()].forEach(([name, value]) => {
|
|
954
|
-
if (value) headers.set(name, value);
|
|
955
|
-
});
|
|
956
|
-
|
|
957
|
-
return new Response(deconstructedResponse.body, {
|
|
958
|
-
headers,
|
|
959
|
-
status: deconstructedResponse.status,
|
|
960
|
-
});
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
const invalidAssetsFetch: typeof miniflareFetch = () => {
|
|
965
|
-
throw new Error(
|
|
966
|
-
"Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
|
|
967
|
-
);
|
|
471
|
+
},
|
|
968
472
|
};
|