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/upload.tsx
CHANGED
|
@@ -22,18 +22,16 @@ import {
|
|
|
22
22
|
BULK_UPLOAD_CONCURRENCY,
|
|
23
23
|
MAX_BUCKET_FILE_COUNT,
|
|
24
24
|
MAX_BUCKET_SIZE,
|
|
25
|
+
MAX_CHECK_MISSING_ATTEMPTS,
|
|
25
26
|
MAX_UPLOAD_ATTEMPTS,
|
|
26
27
|
} from "./constants";
|
|
27
28
|
import { pagesBetaWarning } from "./utils";
|
|
28
|
-
import type { UploadPayloadFile } from "./types";
|
|
29
|
-
import type {
|
|
29
|
+
import type { UploadPayloadFile, YargsOptionsToInterface } from "./types";
|
|
30
|
+
import type { Argv } from "yargs";
|
|
30
31
|
|
|
31
|
-
type UploadArgs =
|
|
32
|
-
directory: string;
|
|
33
|
-
"output-manifest-path"?: string;
|
|
34
|
-
};
|
|
32
|
+
type UploadArgs = YargsOptionsToInterface<typeof Options>;
|
|
35
33
|
|
|
36
|
-
export function Options(yargs: Argv)
|
|
34
|
+
export function Options(yargs: Argv) {
|
|
37
35
|
return yargs
|
|
38
36
|
.positional("directory", {
|
|
39
37
|
type: "string",
|
|
@@ -52,7 +50,7 @@ export function Options(yargs: Argv): Argv<UploadArgs> {
|
|
|
52
50
|
export const Handler = async ({
|
|
53
51
|
directory,
|
|
54
52
|
outputManifestPath,
|
|
55
|
-
}:
|
|
53
|
+
}: UploadArgs) => {
|
|
56
54
|
if (!directory) {
|
|
57
55
|
throw new FatalError("Must specify a directory.", 1);
|
|
58
56
|
}
|
|
@@ -105,6 +103,7 @@ export const upload = async (
|
|
|
105
103
|
"_worker.js",
|
|
106
104
|
"_redirects",
|
|
107
105
|
"_headers",
|
|
106
|
+
"_routes.json",
|
|
108
107
|
".DS_Store",
|
|
109
108
|
"node_modules",
|
|
110
109
|
".git",
|
|
@@ -190,19 +189,37 @@ export const upload = async (
|
|
|
190
189
|
|
|
191
190
|
const start = Date.now();
|
|
192
191
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
{
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
192
|
+
let attempts = 0;
|
|
193
|
+
const getMissingHashes = async (): Promise<string[]> => {
|
|
194
|
+
try {
|
|
195
|
+
return await fetchResult<string[]>(`/pages/assets/check-missing`, {
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers: {
|
|
198
|
+
"Content-Type": "application/json",
|
|
199
|
+
Authorization: `Bearer ${jwt}`,
|
|
200
|
+
},
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
hashes: files.map(({ hash }) => hash),
|
|
203
|
+
}),
|
|
204
|
+
});
|
|
205
|
+
} catch (e) {
|
|
206
|
+
if (attempts < MAX_CHECK_MISSING_ATTEMPTS) {
|
|
207
|
+
// Exponential backoff, 1 second first time, then 2 second, then 4 second etc.
|
|
208
|
+
await new Promise((resolvePromise) =>
|
|
209
|
+
setTimeout(resolvePromise, Math.pow(2, attempts++) * 1000)
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
if ((e as { code: number }).code === 8000013) {
|
|
213
|
+
// Looks like the JWT expired, fetch another one
|
|
214
|
+
jwt = await fetchJwt();
|
|
215
|
+
}
|
|
216
|
+
return getMissingHashes();
|
|
217
|
+
} else {
|
|
218
|
+
throw e;
|
|
219
|
+
}
|
|
204
220
|
}
|
|
205
|
-
|
|
221
|
+
};
|
|
222
|
+
const missingHashes = await getMissingHashes();
|
|
206
223
|
|
|
207
224
|
const sortedFiles = files
|
|
208
225
|
.filter((file) => missingHashes.includes(file.hash))
|
|
@@ -256,7 +273,7 @@ export const upload = async (
|
|
|
256
273
|
// Don't upload empty buckets (can happen for tiny projects)
|
|
257
274
|
if (bucket.files.length === 0) continue;
|
|
258
275
|
|
|
259
|
-
|
|
276
|
+
attempts = 0;
|
|
260
277
|
const doUpload = async (): Promise<void> => {
|
|
261
278
|
// Populate the payload only when actually uploading (this is limited to 3 concurrent uploads at 50 MiB per bucket meaning we'd only load in a max of ~150 MiB)
|
|
262
279
|
// This is so we don't run out of memory trying to upload the files.
|
package/src/publish.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { createWorkerUploadForm } from "./create-worker-upload-form";
|
|
|
11
11
|
import { confirm } from "./dialogs";
|
|
12
12
|
import { getMigrationsToUpload } from "./durable";
|
|
13
13
|
import { logger } from "./logger";
|
|
14
|
+
import { getMetricsUsageHeaders } from "./metrics";
|
|
14
15
|
import { ParseError } from "./parse";
|
|
15
16
|
import { syncAssets } from "./sites";
|
|
16
17
|
import { getZoneForRoute } from "./zones";
|
|
@@ -51,6 +52,27 @@ type Props = {
|
|
|
51
52
|
|
|
52
53
|
type RouteObject = ZoneIdRoute | ZoneNameRoute | CustomDomainRoute;
|
|
53
54
|
|
|
55
|
+
export type CustomDomain = {
|
|
56
|
+
id: string;
|
|
57
|
+
zone_id: string;
|
|
58
|
+
zone_name: string;
|
|
59
|
+
hostname: string;
|
|
60
|
+
service: string;
|
|
61
|
+
environment: string;
|
|
62
|
+
};
|
|
63
|
+
type UpdatedCustomDomain = CustomDomain & { modified: boolean };
|
|
64
|
+
type ConflictingCustomDomain = CustomDomain & {
|
|
65
|
+
external_dns_record_id?: string;
|
|
66
|
+
external_cert_id?: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type CustomDomainChangeset = {
|
|
70
|
+
added: CustomDomain[];
|
|
71
|
+
removed: CustomDomain[];
|
|
72
|
+
updated: UpdatedCustomDomain[];
|
|
73
|
+
conflicting: ConflictingCustomDomain[];
|
|
74
|
+
};
|
|
75
|
+
|
|
54
76
|
function sleep(ms: number) {
|
|
55
77
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
56
78
|
}
|
|
@@ -79,58 +101,27 @@ function renderRoute(route: Route): string {
|
|
|
79
101
|
return result;
|
|
80
102
|
}
|
|
81
103
|
|
|
82
|
-
// this function takes a string with quotes in it
|
|
83
|
-
// (i.e. `hello "world", if that really is your name`)
|
|
84
|
-
// and peels out the first instance of a substring
|
|
85
|
-
// bounded by quotes (so, in the example above, `world`)
|
|
86
|
-
//
|
|
87
|
-
// this is useful because the /domains api will return
|
|
88
|
-
// which domains conflicted in an error message, bounded
|
|
89
|
-
// by a string, which we can use to provide helpful
|
|
90
|
-
// messages to a user
|
|
91
|
-
function getQuoteBoundedSubstring(content: string) {
|
|
92
|
-
const matches = content.split('"');
|
|
93
|
-
return matches[1] ?? "";
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function isOriginConflictError(
|
|
97
|
-
e: unknown
|
|
98
|
-
): e is { code: 100116; message: string; notes: Array<{ text: string }> } {
|
|
99
|
-
return (
|
|
100
|
-
typeof e === "object" &&
|
|
101
|
-
e !== null &&
|
|
102
|
-
(e as { code: number }).code === 100116
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function isDNSConflictError(
|
|
107
|
-
e: unknown
|
|
108
|
-
): e is { code: 100117; message: string; notes: Array<{ text: string }> } {
|
|
109
|
-
return (
|
|
110
|
-
typeof e === "object" &&
|
|
111
|
-
e !== null &&
|
|
112
|
-
(e as { code: number }).code === 100117
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// empty error class to throw and then explicitly catch via `instanceof`
|
|
117
|
-
class CustomDomainOverrideRejected extends Error {}
|
|
118
|
-
|
|
119
104
|
// publishing to custom domains involves a few more steps than just updating
|
|
120
105
|
// the routing table, and thus the api implementing it is fairly defensive -
|
|
121
106
|
// it will error eagerly on conflicts against existing domains or existing
|
|
122
107
|
// managed DNS records
|
|
123
|
-
|
|
124
|
-
// however, you can pass params to override the errors.
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
108
|
+
|
|
109
|
+
// however, you can pass params to override the errors. to know if we should
|
|
110
|
+
// override the current state, we generate a "changeset" of required actions
|
|
111
|
+
// to get to the state we want (specified by the list of custom domains). the
|
|
112
|
+
// changeset returns an "updated" collection (existing custom domains
|
|
113
|
+
// connected to other scripts) and a "conflicting" collection (the requested
|
|
114
|
+
// custom domains that have a managed, conflicting DNS record preventing the
|
|
115
|
+
// host's use as a custom domain). with this information, we can prompt to
|
|
116
|
+
// the user what will occur if we create the custom domains requested, and
|
|
117
|
+
// add the override param if they confirm the action
|
|
128
118
|
//
|
|
129
119
|
// if a user does not confirm that they want to override, we skip publishing
|
|
130
120
|
// to these custom domains, but continue on through the rest of the
|
|
131
121
|
// publish stage
|
|
132
|
-
function publishCustomDomains(
|
|
122
|
+
async function publishCustomDomains(
|
|
133
123
|
workerUrl: string,
|
|
124
|
+
accountId: string,
|
|
134
125
|
domains: Array<RouteObject>
|
|
135
126
|
): Promise<string[]> {
|
|
136
127
|
const config = {
|
|
@@ -146,79 +137,81 @@ function publishCustomDomains(
|
|
|
146
137
|
};
|
|
147
138
|
});
|
|
148
139
|
|
|
140
|
+
const fail = () => {
|
|
141
|
+
return [
|
|
142
|
+
domains.length > 1
|
|
143
|
+
? `Publishing to ${domains.length} Custom Domains was skipped, fix conflicts and try again`
|
|
144
|
+
: `Publishing to Custom Domain "${domains[0].pattern}" was skipped, fix conflict and try again`,
|
|
145
|
+
];
|
|
146
|
+
};
|
|
147
|
+
|
|
149
148
|
if (!process.stdout.isTTY) {
|
|
150
149
|
// running in non-interactive mode.
|
|
151
150
|
// existing origins / dns records are not indicative of errors,
|
|
152
151
|
// so we aggressively update rather than aggressively fail
|
|
153
152
|
config.override_existing_origin = true;
|
|
154
153
|
config.override_existing_dns_record = true;
|
|
154
|
+
} else {
|
|
155
|
+
// get a changeset for operations required to achieve a state with the requested domains
|
|
156
|
+
const changeset = await fetchResult<CustomDomainChangeset>(
|
|
157
|
+
`${workerUrl}/domains/changeset?replace_state=true`,
|
|
158
|
+
{
|
|
159
|
+
method: "POST",
|
|
160
|
+
body: JSON.stringify(origins),
|
|
161
|
+
headers: {
|
|
162
|
+
"Content-Type": "application/json",
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const updatesRequired = changeset.updated.filter(
|
|
168
|
+
(domain) => domain.modified
|
|
169
|
+
);
|
|
170
|
+
if (updatesRequired.length > 0) {
|
|
171
|
+
// find out which scripts the conflict domains are already attached to
|
|
172
|
+
// so we can provide that in the confirmation prompt
|
|
173
|
+
const existing = await Promise.all(
|
|
174
|
+
updatesRequired.map((domain) =>
|
|
175
|
+
fetchResult<CustomDomain>(
|
|
176
|
+
`/accounts/${accountId}/workers/domains/records/${domain.id}`
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
const existingRendered = existing
|
|
181
|
+
.map(
|
|
182
|
+
(domain) =>
|
|
183
|
+
`\t• ${domain.hostname} (used as a domain for "${domain.service}")`
|
|
184
|
+
)
|
|
185
|
+
.join("\n");
|
|
186
|
+
const message = `Custom Domains already exist for these domains:
|
|
187
|
+
${existingRendered}
|
|
188
|
+
Update them to point to this script instead?`;
|
|
189
|
+
if (!(await confirm(message))) return fail();
|
|
190
|
+
config.override_existing_origin = true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (changeset.conflicting.length > 0) {
|
|
194
|
+
const conflicitingRendered = changeset.conflicting
|
|
195
|
+
.map((domain) => `\t• ${domain.hostname}`)
|
|
196
|
+
.join("\n");
|
|
197
|
+
const message = `You already have DNS records that conflict for these Custom Domains:
|
|
198
|
+
${conflicitingRendered}
|
|
199
|
+
Update them to point to this script instead?`;
|
|
200
|
+
if (!(await confirm(message))) return fail();
|
|
201
|
+
config.override_existing_dns_record = true;
|
|
202
|
+
}
|
|
155
203
|
}
|
|
156
204
|
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
// while retaining the flexibility of promise chain fall-throughs. We can group error
|
|
160
|
-
// handling logic in dedicated catch calls, and all we have to do is re-throw an
|
|
161
|
-
// error and it will pass down to the next catch call
|
|
162
|
-
return fetchResult(`${workerUrl}/domains`, {
|
|
205
|
+
// publish to domains
|
|
206
|
+
await fetchResult(`${workerUrl}/domains/records`, {
|
|
163
207
|
method: "PUT",
|
|
164
208
|
body: JSON.stringify({ ...config, origins }),
|
|
165
209
|
headers: {
|
|
166
210
|
"Content-Type": "application/json",
|
|
167
211
|
},
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const conflictingOrigins = getQuoteBoundedSubstring(err.notes[0].text);
|
|
172
|
-
const shouldContinue = await confirm(
|
|
173
|
-
`Custom Domains already exist for these domains: "${conflictingOrigins}"\nUpdate them to point to this script instead?`
|
|
174
|
-
);
|
|
175
|
-
if (!shouldContinue) {
|
|
176
|
-
throw new CustomDomainOverrideRejected();
|
|
177
|
-
}
|
|
178
|
-
config.override_existing_origin = true;
|
|
179
|
-
await fetchResult(`${workerUrl}/domains`, {
|
|
180
|
-
method: "PUT",
|
|
181
|
-
body: JSON.stringify({ ...config, origins }),
|
|
182
|
-
headers: {
|
|
183
|
-
"Content-Type": "application/json",
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
} else {
|
|
187
|
-
throw err;
|
|
188
|
-
}
|
|
189
|
-
})
|
|
190
|
-
.catch(async (err) => {
|
|
191
|
-
if (isDNSConflictError(err)) {
|
|
192
|
-
const conflictingOrigins = getQuoteBoundedSubstring(err.notes[0].text);
|
|
193
|
-
const shouldContinue = await confirm(
|
|
194
|
-
`You already have conflicting DNS records for these domains: "${conflictingOrigins}"\nUpdate them to point to this script instead?`
|
|
195
|
-
);
|
|
196
|
-
if (!shouldContinue) {
|
|
197
|
-
throw new CustomDomainOverrideRejected();
|
|
198
|
-
}
|
|
199
|
-
config.override_existing_dns_record = true;
|
|
200
|
-
await fetchResult(`${workerUrl}/domains`, {
|
|
201
|
-
method: "PUT",
|
|
202
|
-
body: JSON.stringify({ ...config, origins }),
|
|
203
|
-
headers: {
|
|
204
|
-
"Content-Type": "application/json",
|
|
205
|
-
},
|
|
206
|
-
});
|
|
207
|
-
} else {
|
|
208
|
-
throw err;
|
|
209
|
-
}
|
|
210
|
-
})
|
|
211
|
-
.then(() => domains.map((domain) => renderRoute(domain)))
|
|
212
|
-
.catch((err) => {
|
|
213
|
-
if (err instanceof CustomDomainOverrideRejected) {
|
|
214
|
-
return [
|
|
215
|
-
domains.length > 1
|
|
216
|
-
? `Publishing to ${domains.length} Custom Domains was skipped, fix conflicts and try again`
|
|
217
|
-
: `Publishing to Custom Domain "${domains[0].pattern}" was skipped, fix conflict and try again`,
|
|
218
|
-
];
|
|
219
|
-
}
|
|
220
|
-
throw err;
|
|
221
|
-
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return domains.map((domain) => renderRoute(domain));
|
|
222
215
|
}
|
|
223
216
|
|
|
224
217
|
export default async function publish(props: Props): Promise<void> {
|
|
@@ -276,6 +269,19 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
|
|
|
276
269
|
);
|
|
277
270
|
}
|
|
278
271
|
|
|
272
|
+
// Warn if user tries minify or node-compat with no-bundle
|
|
273
|
+
if (props.noBundle && minify) {
|
|
274
|
+
logger.warn(
|
|
275
|
+
"`--minify` and `--no-bundle` can't be used together. If you want to minify your Worker and disable Wrangler's bundling, please minify as part of your own bundling process."
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (props.noBundle && nodeCompat) {
|
|
280
|
+
logger.warn(
|
|
281
|
+
"`--node-compat` and `--no-bundle` can't be used together. If you want to polyfill Node.js built-ins and disable Wrangler's bundling, please polyfill as part of your own bundling process."
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
279
285
|
const scriptName = props.name;
|
|
280
286
|
assert(
|
|
281
287
|
scriptName,
|
|
@@ -378,6 +384,17 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
|
|
|
378
384
|
nodeCompat,
|
|
379
385
|
define: config.define,
|
|
380
386
|
checkFetch: false,
|
|
387
|
+
assets: config.assets && {
|
|
388
|
+
...config.assets,
|
|
389
|
+
// enable the cache when publishing
|
|
390
|
+
bypassCache: false,
|
|
391
|
+
},
|
|
392
|
+
services: config.services,
|
|
393
|
+
// We don't set workerDefinitions here,
|
|
394
|
+
// because we don't want to apply the dev-time
|
|
395
|
+
// facades on top of it
|
|
396
|
+
workerDefinitions: undefined,
|
|
397
|
+
firstPartyWorkerDevFacade: false,
|
|
381
398
|
}
|
|
382
399
|
);
|
|
383
400
|
|
|
@@ -427,6 +444,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
|
|
|
427
444
|
r2_buckets: config.r2_buckets,
|
|
428
445
|
services: config.services,
|
|
429
446
|
worker_namespaces: config.worker_namespaces,
|
|
447
|
+
logfwdr: config.logfwdr,
|
|
430
448
|
unsafe: config.unsafe?.bindings,
|
|
431
449
|
};
|
|
432
450
|
|
|
@@ -479,6 +497,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
|
|
|
479
497
|
{
|
|
480
498
|
method: "PUT",
|
|
481
499
|
body: createWorkerUploadForm(worker),
|
|
500
|
+
headers: await getMetricsUsageHeaders(config.send_metrics),
|
|
482
501
|
},
|
|
483
502
|
new URLSearchParams({
|
|
484
503
|
include_subdomain_availability: "true",
|
|
@@ -490,13 +509,15 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
|
|
|
490
509
|
|
|
491
510
|
available_on_subdomain = result.available_on_subdomain;
|
|
492
511
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
512
|
+
if (config.first_party_worker) {
|
|
513
|
+
// Print some useful information returned after publishing
|
|
514
|
+
// Not all fields will be populated for every worker
|
|
515
|
+
// These fields are likely to be scraped by tools, so do not rename
|
|
516
|
+
if (result.id) logger.log("Worker ID: ", result.id);
|
|
517
|
+
if (result.etag) logger.log("Worker ETag: ", result.etag);
|
|
518
|
+
if (result.pipeline_hash)
|
|
519
|
+
logger.log("Worker PipelineHash: ", result.pipeline_hash);
|
|
520
|
+
}
|
|
500
521
|
}
|
|
501
522
|
} finally {
|
|
502
523
|
if (typeof destination !== "string") {
|
|
@@ -577,7 +598,9 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
|
|
|
577
598
|
|
|
578
599
|
// Update custom domains for the script
|
|
579
600
|
if (customDomainsOnly.length > 0) {
|
|
580
|
-
deployments.push(
|
|
601
|
+
deployments.push(
|
|
602
|
+
publishCustomDomains(workerUrl, accountId, customDomainsOnly)
|
|
603
|
+
);
|
|
581
604
|
}
|
|
582
605
|
|
|
583
606
|
// Configure any schedules for the script.
|
|
@@ -601,7 +624,11 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
|
|
|
601
624
|
if (deployments.length > 0) {
|
|
602
625
|
logger.log("Published", workerName, formatTime(deployMs));
|
|
603
626
|
for (const target of targets.flat()) {
|
|
604
|
-
|
|
627
|
+
// Append protocol only on workers.dev domains
|
|
628
|
+
logger.log(
|
|
629
|
+
" ",
|
|
630
|
+
(target.endsWith("workers.dev") ? "https://" : "") + target
|
|
631
|
+
);
|
|
605
632
|
}
|
|
606
633
|
} else {
|
|
607
634
|
logger.log("No publish targets for", workerName, formatTime(deployMs));
|
package/src/r2.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
1
2
|
import { fetchResult } from "./cfetch";
|
|
3
|
+
import { fetchR2Objects } from "./cfetch/internal";
|
|
4
|
+
import type { HeadersInit } from "undici";
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* Information about a bucket, returned from `listR2Buckets()`.
|
|
@@ -48,3 +51,81 @@ export async function deleteR2Bucket(
|
|
|
48
51
|
{ method: "DELETE" }
|
|
49
52
|
);
|
|
50
53
|
}
|
|
54
|
+
|
|
55
|
+
export function bucketAndKeyFromObjectPath(objectPath = ""): {
|
|
56
|
+
bucket: string;
|
|
57
|
+
key: string;
|
|
58
|
+
} {
|
|
59
|
+
const match = /^([^/]+)\/(.*)/.exec(objectPath);
|
|
60
|
+
if (match === null) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`The object path must be in the form of {bucket}/{key} you provided ${objectPath}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { bucket: match[1], key: match[2] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Downloads an object
|
|
71
|
+
*/
|
|
72
|
+
export async function getR2Object(
|
|
73
|
+
accountId: string,
|
|
74
|
+
bucketName: string,
|
|
75
|
+
objectName: string
|
|
76
|
+
): Promise<Readable> {
|
|
77
|
+
const response = await fetchR2Objects(
|
|
78
|
+
`/accounts/${accountId}/r2/buckets/${bucketName}/objects/${objectName}`,
|
|
79
|
+
{ method: "GET" }
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return Readable.from(response.body);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Uploads an object
|
|
87
|
+
*/
|
|
88
|
+
export async function putR2Object(
|
|
89
|
+
accountId: string,
|
|
90
|
+
bucketName: string,
|
|
91
|
+
objectName: string,
|
|
92
|
+
object: Readable | Buffer,
|
|
93
|
+
options: Record<string, unknown>
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
const headerKeys = [
|
|
96
|
+
"content-length",
|
|
97
|
+
"content-type",
|
|
98
|
+
"content-disposition",
|
|
99
|
+
"content-encoding",
|
|
100
|
+
"content-language",
|
|
101
|
+
"cache-control",
|
|
102
|
+
"expires",
|
|
103
|
+
];
|
|
104
|
+
const headers: HeadersInit = {};
|
|
105
|
+
for (const key of headerKeys) {
|
|
106
|
+
const value = options[key] || "";
|
|
107
|
+
if (value && typeof value === "string") headers[key] = value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await fetchR2Objects(
|
|
111
|
+
`/accounts/${accountId}/r2/buckets/${bucketName}/objects/${objectName}`,
|
|
112
|
+
{
|
|
113
|
+
body: object,
|
|
114
|
+
headers,
|
|
115
|
+
method: "PUT",
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Delete an Object
|
|
121
|
+
*/
|
|
122
|
+
export async function deleteR2Object(
|
|
123
|
+
accountId: string,
|
|
124
|
+
bucketName: string,
|
|
125
|
+
objectName: string
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
await fetchR2Objects(
|
|
128
|
+
`/accounts/${accountId}/r2/buckets/${bucketName}/objects/${objectName}`,
|
|
129
|
+
{ method: "DELETE" }
|
|
130
|
+
);
|
|
131
|
+
}
|
package/src/tail/index.ts
CHANGED
|
@@ -174,12 +174,12 @@ export type TailEventMessage = {
|
|
|
174
174
|
/**
|
|
175
175
|
* The event that triggered the worker. In the case of an HTTP request,
|
|
176
176
|
* this will be a RequestEvent. If it's a cron trigger, it'll be a
|
|
177
|
-
* ScheduledEvent.
|
|
177
|
+
* ScheduledEvent. If it's a durable object alarm, it's an AlarmEvent.
|
|
178
178
|
*
|
|
179
179
|
* Until workers-types exposes individual types for export, we'll have
|
|
180
180
|
* to just re-define these types ourselves.
|
|
181
181
|
*/
|
|
182
|
-
event: RequestEvent | ScheduledEvent | undefined | null;
|
|
182
|
+
event: RequestEvent | ScheduledEvent | AlarmEvent | undefined | null;
|
|
183
183
|
};
|
|
184
184
|
|
|
185
185
|
/**
|
|
@@ -297,3 +297,16 @@ export type ScheduledEvent = {
|
|
|
297
297
|
*/
|
|
298
298
|
scheduledTime: number;
|
|
299
299
|
};
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* A event that was triggered from a durable object alarm
|
|
303
|
+
*/
|
|
304
|
+
export type AlarmEvent = {
|
|
305
|
+
/**
|
|
306
|
+
* The datetime the alarm was scheduled for.
|
|
307
|
+
*
|
|
308
|
+
* This is sent as an ISO timestamp string (different than ScheduledEvent.scheduledTime),
|
|
309
|
+
* you should parse it later on on your own.
|
|
310
|
+
*/
|
|
311
|
+
scheduledTime: string;
|
|
312
|
+
};
|
package/src/tail/printing.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { logger } from "../logger";
|
|
2
|
-
import type { RequestEvent, ScheduledEvent, TailEventMessage } from ".";
|
|
3
2
|
import type { Outcome } from "./filters";
|
|
3
|
+
import type {
|
|
4
|
+
AlarmEvent,
|
|
5
|
+
RequestEvent,
|
|
6
|
+
ScheduledEvent,
|
|
7
|
+
TailEventMessage,
|
|
8
|
+
} from "./index";
|
|
4
9
|
import type WebSocket from "ws";
|
|
5
10
|
|
|
6
11
|
export function prettyPrintLogs(data: WebSocket.RawData): void {
|
|
@@ -14,7 +19,7 @@ export function prettyPrintLogs(data: WebSocket.RawData): void {
|
|
|
14
19
|
const outcome = prettifyOutcome(eventMessage.outcome);
|
|
15
20
|
|
|
16
21
|
logger.log(`"${cronPattern}" @ ${datetime} - ${outcome}`);
|
|
17
|
-
} else {
|
|
22
|
+
} else if (isRequestEvent(eventMessage.event)) {
|
|
18
23
|
const requestMethod = eventMessage.event?.request.method.toUpperCase();
|
|
19
24
|
const url = eventMessage.event?.request.url;
|
|
20
25
|
const outcome = prettifyOutcome(eventMessage.outcome);
|
|
@@ -25,6 +30,19 @@ export function prettyPrintLogs(data: WebSocket.RawData): void {
|
|
|
25
30
|
? `${requestMethod} ${url} - ${outcome} @ ${datetime}`
|
|
26
31
|
: `[missing request] - ${outcome} @ ${datetime}`
|
|
27
32
|
);
|
|
33
|
+
} else if (isAlarmEvent(eventMessage.event)) {
|
|
34
|
+
const outcome = prettifyOutcome(eventMessage.outcome);
|
|
35
|
+
const datetime = new Date(
|
|
36
|
+
eventMessage.event.scheduledTime
|
|
37
|
+
).toLocaleString();
|
|
38
|
+
|
|
39
|
+
logger.log(`Alarm @ ${datetime} - ${outcome}`);
|
|
40
|
+
} else {
|
|
41
|
+
// Unknown event type
|
|
42
|
+
const outcome = prettifyOutcome(eventMessage.outcome);
|
|
43
|
+
const datetime = new Date(eventMessage.eventTimestamp).toLocaleString();
|
|
44
|
+
|
|
45
|
+
logger.log(`Unknown Event - ${outcome} @ ${datetime}`);
|
|
28
46
|
}
|
|
29
47
|
|
|
30
48
|
if (eventMessage.logs.length > 0) {
|
|
@@ -44,12 +62,32 @@ export function jsonPrintLogs(data: WebSocket.RawData): void {
|
|
|
44
62
|
console.log(JSON.stringify(JSON.parse(data.toString()), null, 2));
|
|
45
63
|
}
|
|
46
64
|
|
|
65
|
+
function isRequestEvent(
|
|
66
|
+
event: TailEventMessage["event"]
|
|
67
|
+
): event is RequestEvent {
|
|
68
|
+
return Boolean(event && "request" in event);
|
|
69
|
+
}
|
|
70
|
+
|
|
47
71
|
function isScheduledEvent(
|
|
48
|
-
event:
|
|
72
|
+
event: TailEventMessage["event"]
|
|
49
73
|
): event is ScheduledEvent {
|
|
50
74
|
return Boolean(event && "cron" in event);
|
|
51
75
|
}
|
|
52
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Check to see if an event sent from a worker is an AlarmEvent.
|
|
79
|
+
*
|
|
80
|
+
* Because the only property on `AlarmEvent` is "scheduledTime", which it
|
|
81
|
+
* shares with `ScheduledEvent`, `isAlarmEvent` checks if there's _not_
|
|
82
|
+
* a "cron" property in `event` to confirm it's an alarm event.
|
|
83
|
+
*
|
|
84
|
+
* @param event An event
|
|
85
|
+
* @returns true if the event is an AlarmEvent
|
|
86
|
+
*/
|
|
87
|
+
function isAlarmEvent(event: TailEventMessage["event"]): event is AlarmEvent {
|
|
88
|
+
return Boolean(event && "scheduledTime" in event && !("cron" in event));
|
|
89
|
+
}
|
|
90
|
+
|
|
53
91
|
function prettifyOutcome(outcome: Outcome): string {
|
|
54
92
|
switch (outcome) {
|
|
55
93
|
case "ok":
|
|
@@ -44,17 +44,26 @@ export async function getAccountChoices(): Promise<ChooseAccountItem[]> {
|
|
|
44
44
|
if (accountIdFromEnv) {
|
|
45
45
|
return [{ id: accountIdFromEnv, name: "" }];
|
|
46
46
|
} else {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetchListResult<{
|
|
49
|
+
account: ChooseAccountItem;
|
|
50
|
+
}>(`/memberships`);
|
|
51
|
+
const accounts = response.map((r) => r.account);
|
|
52
|
+
if (accounts.length === 0) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"Failed to automatically retrieve account IDs for the logged in user.\n" +
|
|
55
|
+
"In a non-interactive environment, it is mandatory to specify an account ID, either by assigning its value to CLOUDFLARE_ACCOUNT_ID, or as `account_id` in your `wrangler.toml` file."
|
|
56
|
+
);
|
|
57
|
+
} else {
|
|
58
|
+
return accounts;
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if ((err as { code: number }).code === 9109) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Failed to automatically retrieve account IDs for the logged in user.
|
|
64
|
+
You may have incorrect permissions on your API token. You can skip this account check by adding an \`account_id\` in your \`wrangler.toml\`, or by setting the value of CLOUDFLARE_ACCOUNT_ID"`
|
|
65
|
+
);
|
|
66
|
+
} else throw err;
|
|
58
67
|
}
|
|
59
68
|
}
|
|
60
69
|
}
|