wrangler 0.0.0-e22f9d7 → 0.0.0-e273e09

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/Cloudflare_CA.pem +18 -0
  2. package/README.md +27 -14
  3. package/bin/wrangler.js +48 -4
  4. package/miniflare-dist/index.mjs +4886 -0
  5. package/package.json +60 -31
  6. package/pages/functions/buildPlugin.ts +105 -0
  7. package/pages/functions/buildWorker.ts +93 -4
  8. package/pages/functions/filepath-routing.test.ts +223 -28
  9. package/pages/functions/filepath-routing.ts +100 -132
  10. package/pages/functions/routes.ts +16 -18
  11. package/pages/functions/template-plugin.ts +147 -0
  12. package/pages/functions/template-worker.ts +37 -38
  13. package/src/__tests__/config-cache.test.ts +36 -0
  14. package/src/__tests__/configuration.test.ts +3487 -0
  15. package/src/__tests__/dev.test.tsx +977 -61
  16. package/src/__tests__/guess-worker-format.test.ts +85 -0
  17. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  18. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  19. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  20. package/src/__tests__/helpers/mock-account-id.ts +48 -0
  21. package/src/__tests__/helpers/mock-bin.ts +36 -0
  22. package/src/__tests__/{mock-cfetch.ts → helpers/mock-cfetch.ts} +56 -16
  23. package/src/__tests__/helpers/mock-console.ts +87 -0
  24. package/src/__tests__/helpers/mock-dialogs.ts +130 -0
  25. package/src/__tests__/helpers/mock-http-server.ts +46 -0
  26. package/src/__tests__/helpers/mock-istty.ts +27 -0
  27. package/src/__tests__/helpers/mock-kv.ts +33 -0
  28. package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
  29. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  30. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  31. package/src/__tests__/helpers/run-in-tmp.ts +39 -0
  32. package/src/__tests__/helpers/run-wrangler.ts +16 -0
  33. package/src/__tests__/helpers/write-worker-source.ts +31 -0
  34. package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
  35. package/src/__tests__/https-options.test.ts +130 -0
  36. package/src/__tests__/index.test.ts +179 -209
  37. package/src/__tests__/init.test.ts +1637 -0
  38. package/src/__tests__/jest.setup.ts +61 -6
  39. package/src/__tests__/kv.test.ts +842 -472
  40. package/src/__tests__/logger.test.ts +115 -0
  41. package/src/__tests__/package-manager.test.ts +353 -0
  42. package/src/__tests__/pages.test.ts +754 -0
  43. package/src/__tests__/parse.test.ts +330 -0
  44. package/src/__tests__/publish.test.ts +5755 -0
  45. package/src/__tests__/r2.test.ts +233 -0
  46. package/src/__tests__/route.test.ts +45 -0
  47. package/src/__tests__/secret.test.ts +481 -0
  48. package/src/__tests__/tail.test.ts +690 -0
  49. package/src/__tests__/user.test.ts +131 -0
  50. package/src/__tests__/whoami.test.tsx +139 -0
  51. package/src/abort.d.ts +3 -0
  52. package/src/bundle.ts +197 -0
  53. package/src/cfetch/index.ts +40 -21
  54. package/src/cfetch/internal.ts +102 -29
  55. package/src/cli.ts +6 -2
  56. package/src/config/README.md +107 -0
  57. package/src/config/config.ts +234 -0
  58. package/src/config/diagnostics.ts +80 -0
  59. package/src/config/environment.ts +420 -0
  60. package/src/config/index.ts +225 -0
  61. package/src/config/validation-helpers.ts +584 -0
  62. package/src/config/validation.ts +1548 -0
  63. package/src/config-cache.ts +45 -0
  64. package/src/create-worker-preview.ts +185 -0
  65. package/src/create-worker-upload-form.ts +254 -0
  66. package/src/dev/dev-vars.ts +36 -0
  67. package/src/dev/dev.tsx +401 -0
  68. package/src/dev/local.tsx +326 -0
  69. package/src/dev/remote.tsx +237 -0
  70. package/src/dev/use-esbuild.ts +118 -0
  71. package/src/dialogs.tsx +66 -8
  72. package/src/durable.ts +102 -0
  73. package/src/entry.ts +322 -0
  74. package/src/environment-variables.ts +35 -0
  75. package/src/errors.ts +11 -0
  76. package/src/https-options.ts +123 -0
  77. package/src/index.tsx +1848 -976
  78. package/src/inspect.ts +156 -33
  79. package/src/intl-polyfill.d.ts +139 -0
  80. package/src/{kv.tsx → kv.ts} +199 -67
  81. package/src/logger.ts +66 -0
  82. package/src/miniflare-cli/README.md +30 -0
  83. package/src/miniflare-cli/enum-keys.ts +17 -0
  84. package/src/miniflare-cli/index.ts +41 -0
  85. package/src/miniflare-cli/request-context.ts +40 -0
  86. package/src/module-collection.ts +229 -35
  87. package/src/open-in-browser.ts +19 -0
  88. package/src/package-manager.ts +174 -0
  89. package/src/pages.tsx +1162 -100
  90. package/src/parse.ts +203 -0
  91. package/src/paths.ts +26 -0
  92. package/src/proxy.ts +227 -51
  93. package/src/publish.ts +633 -276
  94. package/src/r2.ts +50 -0
  95. package/src/selfsigned.d.ts +29 -0
  96. package/src/sites.tsx +270 -62
  97. package/src/tail/filters.ts +279 -0
  98. package/src/tail/index.ts +299 -0
  99. package/src/tail/printing.ts +67 -0
  100. package/src/update-check.ts +19 -0
  101. package/src/user.tsx +382 -195
  102. package/src/whoami.tsx +71 -0
  103. package/src/{api/worker.ts → worker.ts} +66 -40
  104. package/src/zones.ts +73 -0
  105. package/templates/gitignore +171 -0
  106. package/templates/new-worker-scheduled.js +17 -0
  107. package/templates/new-worker-scheduled.ts +32 -0
  108. package/templates/new-worker.js +15 -0
  109. package/templates/new-worker.ts +30 -0
  110. package/{static-asset-facade.js → templates/static-asset-facade.js} +1 -5
  111. package/templates/tsconfig.json +105 -0
  112. package/wrangler-dist/cli.js +106332 -103741
  113. package/src/__tests__/mock-dialogs.ts +0 -65
  114. package/src/__tests__/run-in-tmp.ts +0 -19
  115. package/src/__tests__/run-wrangler.ts +0 -32
  116. package/src/api/form_data.ts +0 -131
  117. package/src/api/preview.ts +0 -128
  118. package/src/config.ts +0 -487
  119. package/src/dev.tsx +0 -784
  120. package/src/tail.tsx +0 -73
  121. package/vendor/@cloudflare/kv-asset-handler/CHANGELOG.md +0 -332
  122. package/vendor/@cloudflare/kv-asset-handler/LICENSE_APACHE +0 -176
  123. package/vendor/@cloudflare/kv-asset-handler/LICENSE_MIT +0 -25
  124. package/vendor/@cloudflare/kv-asset-handler/README.md +0 -245
  125. package/vendor/@cloudflare/kv-asset-handler/dist/index.d.ts +0 -32
  126. package/vendor/@cloudflare/kv-asset-handler/dist/index.js +0 -354
  127. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.d.ts +0 -13
  128. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.js +0 -148
  129. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.d.ts +0 -1
  130. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.js +0 -436
  131. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.d.ts +0 -1
  132. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.js +0 -40
  133. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.d.ts +0 -1
  134. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.js +0 -42
  135. package/vendor/@cloudflare/kv-asset-handler/dist/types.d.ts +0 -26
  136. package/vendor/@cloudflare/kv-asset-handler/dist/types.js +0 -31
  137. package/vendor/@cloudflare/kv-asset-handler/package.json +0 -52
  138. package/vendor/@cloudflare/kv-asset-handler/src/index.ts +0 -296
  139. package/vendor/@cloudflare/kv-asset-handler/src/mocks.ts +0 -136
  140. package/vendor/@cloudflare/kv-asset-handler/src/test/getAssetFromKV.ts +0 -464
  141. package/vendor/@cloudflare/kv-asset-handler/src/test/mapRequestToAsset.ts +0 -33
  142. package/vendor/@cloudflare/kv-asset-handler/src/test/serveSinglePageApp.ts +0 -42
  143. package/vendor/@cloudflare/kv-asset-handler/src/types.ts +0 -39
  144. package/vendor/wrangler-mime/CHANGELOG.md +0 -289
  145. package/vendor/wrangler-mime/LICENSE +0 -21
  146. package/vendor/wrangler-mime/Mime.js +0 -97
  147. package/vendor/wrangler-mime/README.md +0 -187
  148. package/vendor/wrangler-mime/cli.js +0 -46
  149. package/vendor/wrangler-mime/index.js +0 -4
  150. package/vendor/wrangler-mime/lite.js +0 -4
  151. package/vendor/wrangler-mime/package.json +0 -52
  152. package/vendor/wrangler-mime/types/other.js +0 -1
  153. package/vendor/wrangler-mime/types/standard.js +0 -1
  154. package/wrangler-dist/cli.js.map +0 -7
package/src/publish.ts CHANGED
@@ -1,364 +1,721 @@
1
1
  import assert from "node:assert";
2
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
3
  import path from "node:path";
3
- import { readFile } from "node:fs/promises";
4
- import esbuild from "esbuild";
5
- import { execa } from "execa";
4
+ import { URLSearchParams } from "node:url";
6
5
  import tmp from "tmp-promise";
7
- import type { CfWorkerInit } from "./api/worker";
8
- import { toFormData } from "./api/form_data";
9
- import { fetchResult } from "./cfetch";
10
- import type { Config } from "./config";
11
- import makeModuleCollector from "./module-collection";
6
+ import { bundleWorker } from "./bundle";
7
+ import { fetchListResult, fetchResult } from "./cfetch";
8
+ import { printBindings } from "./config";
9
+ import { createWorkerUploadForm } from "./create-worker-upload-form";
10
+ import { confirm } from "./dialogs";
11
+ import { getMigrationsToUpload } from "./durable";
12
+ import { logger } from "./logger";
13
+ import { ParseError } from "./parse";
12
14
  import { syncAssets } from "./sites";
13
-
14
- type CfScriptFormat = undefined | "modules" | "service-worker";
15
+ import { getZoneForRoute } from "./zones";
16
+ import type { Config } from "./config";
17
+ import type {
18
+ Route,
19
+ ZoneIdRoute,
20
+ ZoneNameRoute,
21
+ CustomDomainRoute,
22
+ } from "./config/environment";
23
+ import type { Entry } from "./entry";
24
+ import type { AssetPaths } from "./sites";
25
+ import type { CfWorkerInit } from "./worker";
15
26
 
16
27
  type Props = {
17
28
  config: Config;
18
- format?: CfScriptFormat;
19
- script?: string;
20
- name?: string;
21
- env?: string;
22
- compatibilityDate?: string;
23
- compatibilityFlags?: string[];
24
- public?: string;
25
- site?: string;
26
- triggers?: (string | number)[];
27
- routes?: (string | number)[];
28
- legacyEnv?: boolean;
29
- jsxFactory: undefined | string;
30
- jsxFragment: undefined | string;
29
+ accountId: string | undefined;
30
+ entry: Entry;
31
+ rules: Config["rules"];
32
+ name: string | undefined;
33
+ env: string | undefined;
34
+ compatibilityDate: string | undefined;
35
+ compatibilityFlags: string[] | undefined;
36
+ assetPaths: AssetPaths | undefined;
37
+ triggers: string[] | undefined;
38
+ routes: string[] | undefined;
39
+ legacyEnv: boolean | undefined;
40
+ jsxFactory: string | undefined;
41
+ jsxFragment: string | undefined;
42
+ tsconfig: string | undefined;
43
+ experimentalPublic: boolean;
44
+ minify: boolean | undefined;
45
+ nodeCompat: boolean | undefined;
46
+ outDir: string | undefined;
47
+ dryRun: boolean | undefined;
31
48
  };
32
49
 
50
+ type RouteObject = ZoneIdRoute | ZoneNameRoute | CustomDomainRoute;
51
+
33
52
  function sleep(ms: number) {
34
53
  return new Promise((resolve) => setTimeout(resolve, ms));
35
54
  }
36
55
 
37
- export default async function publish(props: Props): Promise<void> {
38
- if (props.public && props.format === "service-worker") {
39
- // TODO: check config too
40
- throw new Error(
41
- "You cannot use the service worker format with a public directory."
56
+ function renderRoute(route: Route): string {
57
+ let result = "";
58
+ if (typeof route === "string") {
59
+ result = route;
60
+ } else {
61
+ result = route.pattern;
62
+ const isCustomDomain = Boolean(
63
+ "custom_domain" in route && route.custom_domain
42
64
  );
65
+ if (isCustomDomain && "zone_id" in route) {
66
+ result += ` (custom domain - zone id: ${route.zone_id})`;
67
+ } else if (isCustomDomain && "zone_name" in route) {
68
+ result += ` (custom domain - zone name: ${route.zone_name})`;
69
+ } else if (isCustomDomain) {
70
+ result += ` (custom domain)`;
71
+ } else if ("zone_id" in route) {
72
+ result += ` (zone id: ${route.zone_id})`;
73
+ } else if ("zone_name" in route) {
74
+ result += ` (zone name: ${route.zone_name})`;
75
+ }
43
76
  }
44
- // TODO: warn if git/hg has uncommitted changes
45
- const { config } = props;
46
- const {
47
- account_id: accountId,
48
- build,
49
- // @ts-expect-error hidden
50
- __path__,
51
- } = config;
77
+ return result;
78
+ }
52
79
 
53
- const envRootObj =
54
- props.env && config.env ? config.env[props.env] || {} : config;
80
+ // this function takes a string with quotes in it
81
+ // (i.e. `hello "world", if that really is your name`)
82
+ // and peels out the first instance of a substring
83
+ // bounded by quotes (so, in the example above, `world`)
84
+ //
85
+ // this is useful because the /domains api will return
86
+ // which domains conflicted in an error message, bounded
87
+ // by a string, which we can use to provide helpful
88
+ // messages to a user
89
+ function getQuoteBoundedSubstring(content: string) {
90
+ const matches = content.split('"');
91
+ return matches[1] ?? "";
92
+ }
55
93
 
56
- assert(
57
- envRootObj.compatibility_date || props.compatibilityDate,
58
- "A compatibility_date is required when publishing. Add one to your wrangler.toml file, or pass it in your terminal as --compatibility_date. See https://developers.cloudflare.com/workers/platform/compatibility-dates for more information."
94
+ function isOriginConflictError(
95
+ e: unknown
96
+ ): e is { code: 100116; message: string; notes: Array<{ text: string }> } {
97
+ return (
98
+ typeof e === "object" &&
99
+ e !== null &&
100
+ (e as { code: number }).code === 100116
59
101
  );
102
+ }
103
+
104
+ function isDNSConflictError(
105
+ e: unknown
106
+ ): e is { code: 100117; message: string; notes: Array<{ text: string }> } {
107
+ return (
108
+ typeof e === "object" &&
109
+ e !== null &&
110
+ (e as { code: number }).code === 100117
111
+ );
112
+ }
113
+
114
+ // empty error class to throw and then explicitly catch via `instanceof`
115
+ class CustomDomainOverrideRejected extends Error {}
116
+
117
+ // publishing to custom domains involves a few more steps than just updating
118
+ // the routing table, and thus the api implementing it is fairly defensive -
119
+ // it will error eagerly on conflicts against existing domains or existing
120
+ // managed DNS records
121
+ //
122
+ // however, you can pass params to override the errors. we start on the
123
+ // defensive path, and if one of these errors occur, we prompt the user
124
+ // for confirmation that they do indeed want to override the conflicts, and
125
+ // then retry the request with the right override added
126
+ //
127
+ // if a user does not confirm that they want to override, we skip publishing
128
+ // to these custom domains, but continue on through the rest of the
129
+ // publish stage
130
+ function publishCustomDomains(
131
+ workerUrl: string,
132
+ domains: Array<RouteObject>
133
+ ): Promise<string[]> {
134
+ const config = {
135
+ override_scope: true,
136
+ override_existing_origin: false,
137
+ override_existing_dns_record: false,
138
+ };
139
+ const origins = domains.map((domainRoute) => {
140
+ return {
141
+ hostname: domainRoute.pattern,
142
+ zone_id: "zone_id" in domainRoute ? domainRoute.zone_id : undefined,
143
+ zone_name: "zone_name" in domainRoute ? domainRoute.zone_name : undefined,
144
+ };
145
+ });
60
146
 
61
- if (accountId === undefined) {
62
- throw new Error("No account_id provided.");
147
+ if (!process.stdout.isTTY) {
148
+ // running in non-interactive mode.
149
+ // existing origins / dns records are not indicative of errors,
150
+ // so we aggressively update rather than aggressively fail
151
+ config.override_existing_origin = true;
152
+ config.override_existing_dns_record = true;
63
153
  }
64
154
 
155
+ // Mixing promise chains with async/await is funky, but it allows us to keep related
156
+ // logic synchronous-looking (i.e. prompting for confirmation and then re-requesting)
157
+ // while retaining the flexibility of promise chain fall-throughs. We can group error
158
+ // handling logic in dedicated catch calls, and all we have to do is re-throw an
159
+ // error and it will pass down to the next catch call
160
+ return fetchResult(`${workerUrl}/domains`, {
161
+ method: "PUT",
162
+ body: JSON.stringify({ ...config, origins }),
163
+ headers: {
164
+ "Content-Type": "application/json",
165
+ },
166
+ })
167
+ .catch(async (err) => {
168
+ if (isOriginConflictError(err)) {
169
+ const conflictingOrigins = getQuoteBoundedSubstring(err.notes[0].text);
170
+ const shouldContinue = await confirm(
171
+ `Custom Domains already exist for these domains: "${conflictingOrigins}"\nUpdate them to point to this script instead?`
172
+ );
173
+ if (!shouldContinue) {
174
+ throw new CustomDomainOverrideRejected();
175
+ }
176
+ config.override_existing_origin = true;
177
+ await fetchResult(`${workerUrl}/domains`, {
178
+ method: "PUT",
179
+ body: JSON.stringify({ ...config, origins }),
180
+ headers: {
181
+ "Content-Type": "application/json",
182
+ },
183
+ });
184
+ } else {
185
+ throw err;
186
+ }
187
+ })
188
+ .catch(async (err) => {
189
+ if (isDNSConflictError(err)) {
190
+ const conflictingOrigins = getQuoteBoundedSubstring(err.notes[0].text);
191
+ const shouldContinue = await confirm(
192
+ `You already have conflicting DNS records for these domains: "${conflictingOrigins}"\nUpdate them to point to this script instead?`
193
+ );
194
+ if (!shouldContinue) {
195
+ throw new CustomDomainOverrideRejected();
196
+ }
197
+ config.override_existing_dns_record = true;
198
+ await fetchResult(`${workerUrl}/domains`, {
199
+ method: "PUT",
200
+ body: JSON.stringify({ ...config, origins }),
201
+ headers: {
202
+ "Content-Type": "application/json",
203
+ },
204
+ });
205
+ } else {
206
+ throw err;
207
+ }
208
+ })
209
+ .then(() => domains.map((domain) => renderRoute(domain)))
210
+ .catch((err) => {
211
+ if (err instanceof CustomDomainOverrideRejected) {
212
+ return [
213
+ domains.length > 1
214
+ ? `Publishing to ${domains.length} Custom Domains was skipped, fix conflicts and try again`
215
+ : `Publishing to Custom Domain "${domains[0].pattern}" was skipped, fix conflict and try again`,
216
+ ];
217
+ }
218
+ throw err;
219
+ });
220
+ }
221
+
222
+ export default async function publish(props: Props): Promise<void> {
223
+ // TODO: warn if git/hg has uncommitted changes
224
+ const { config, accountId } = props;
225
+
226
+ assert(
227
+ props.compatibilityDate || config.compatibility_date,
228
+ "A compatibility_date is required when publishing. Add one to your wrangler.toml file, or pass it in your terminal as --compatibility-date. See https://developers.cloudflare.com/workers/platform/compatibility-dates for more information."
229
+ );
230
+
65
231
  const triggers = props.triggers || config.triggers?.crons;
66
- const routes = props.routes || config.routes;
232
+ const routes =
233
+ props.routes ?? config.routes ?? (config.route ? [config.route] : []) ?? [];
234
+ const routesOnly: Array<Route> = [];
235
+ const customDomainsOnly: Array<RouteObject> = [];
236
+ for (const route of routes) {
237
+ if (typeof route !== "string" && route.custom_domain) {
238
+ if (route.pattern.includes("*")) {
239
+ throw new Error(
240
+ `Cannot use "${route.pattern}" as a Custom Domain; wildcard operators (*) are not allowed`
241
+ );
242
+ }
243
+ if (route.pattern.includes("/")) {
244
+ throw new Error(
245
+ `Cannot use "${route.pattern}" as a Custom Domain; paths are not allowed`
246
+ );
247
+ }
248
+ customDomainsOnly.push(route);
249
+ } else {
250
+ routesOnly.push(route);
251
+ }
252
+ }
253
+
254
+ // deployToWorkersDev defaults to true only if there aren't any routes defined
255
+ const deployToWorkersDev = config.workers_dev ?? routes.length === 0;
67
256
 
68
257
  const jsxFactory = props.jsxFactory || config.jsx_factory;
69
258
  const jsxFragment = props.jsxFragment || config.jsx_fragment;
70
259
 
71
- assert(config.account_id, "missing account id");
260
+ const minify = props.minify ?? config.minify;
261
+
262
+ const nodeCompat = props.nodeCompat ?? config.node_compat;
263
+ if (nodeCompat) {
264
+ logger.warn(
265
+ "Enabling node.js compatibility mode for built-ins and globals. This is experimental and has serious tradeoffs. Please see https://github.com/ionic-team/rollup-plugin-node-polyfills/ for more details."
266
+ );
267
+ }
72
268
 
73
- let scriptName = props.name || config.name;
269
+ const scriptName = props.name;
74
270
  assert(
75
271
  scriptName,
76
272
  'You need to provide a name when publishing a worker. Either pass it as a cli arg with `--name <name>` or in your config file as `name = "<name>"`'
77
273
  );
78
274
 
79
- let file: string;
80
- if (props.script) {
81
- file = props.script;
82
- } else {
83
- assert(build?.upload?.main, "missing main file");
84
- file = path.join(path.dirname(__path__), build.upload.main);
85
- }
275
+ assert(
276
+ !config.site || config.site.bucket,
277
+ "A [site] definition requires a `bucket` field with a path to the site's public directory."
278
+ );
86
279
 
87
- if (props.legacyEnv) {
88
- scriptName += props.env ? `-${props.env}` : "";
280
+ if (props.outDir) {
281
+ // we're using a custom output directory,
282
+ // so let's first ensure it exists
283
+ mkdirSync(props.outDir, { recursive: true });
284
+ // add a README
285
+ const readmePath = path.join(props.outDir, "README.md");
286
+ writeFileSync(
287
+ readmePath,
288
+ `This folder contains the built output assets for the worker "${scriptName}" generated at ${new Date().toISOString()}.`
289
+ );
89
290
  }
291
+
292
+ const destination = props.outDir ?? (await tmp.dir({ unsafeCleanup: true }));
90
293
  const envName = props.env ?? "production";
91
294
 
92
- const destination = await tmp.dir({ unsafeCleanup: true });
295
+ const start = Date.now();
296
+ const notProd = Boolean(!props.legacyEnv && props.env);
297
+ const workerName = notProd ? `${scriptName} (${envName})` : scriptName;
298
+ const workerUrl = notProd
299
+ ? `/accounts/${accountId}/workers/services/${scriptName}/environments/${envName}`
300
+ : `/accounts/${accountId}/workers/scripts/${scriptName}`;
93
301
 
94
- if (props.config.build?.command) {
95
- // TODO: add a deprecation message here?
96
- console.log("running:", props.config.build.command);
97
- const buildCommandPieces = props.config.build.command.split(" ");
98
- await execa(buildCommandPieces[0], buildCommandPieces.slice(1), {
99
- stdout: "inherit",
100
- stderr: "inherit",
101
- ...(props.config.build?.cwd && { cwd: props.config.build.cwd }),
102
- });
103
- }
302
+ let available_on_subdomain; // we'll set this later
104
303
 
105
- const moduleCollector = makeModuleCollector();
106
- const result = await esbuild.build({
107
- ...(props.public
108
- ? {
109
- stdin: {
110
- contents: (
111
- await readFile(
112
- path.join(__dirname, "../static-asset-facade.js"),
113
- "utf8"
114
- )
115
- ).replace("__ENTRY_POINT__", path.join(process.cwd(), file)),
116
- sourcefile: "static-asset-facade.js",
117
- resolveDir: path.dirname(file),
118
- },
119
- }
120
- : { entryPoints: [file] }),
121
- bundle: true,
122
- nodePaths: props.public ? [path.join(__dirname, "../vendor")] : undefined,
123
- outdir: destination.path,
124
- external: ["__STATIC_CONTENT_MANIFEST"],
125
- format: "esm",
126
- sourcemap: true,
127
- metafile: true,
128
- conditions: ["worker", "browser"],
129
- loader: {
130
- ".js": "jsx",
131
- },
132
- plugins: [moduleCollector.plugin],
133
- ...(jsxFactory && { jsxFactory }),
134
- ...(jsxFragment && { jsxFragment }),
135
- });
304
+ const { format } = props.entry;
136
305
 
137
- // result.metafile is defined because of the `metafile: true` option above.
138
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
139
- const metafile = result.metafile!;
140
- const expectedEntryPoint = props.public
141
- ? path.join(path.dirname(file), "static-asset-facade.js")
142
- : file;
143
- const outputEntry = Object.entries(metafile.outputs).find(
144
- ([, { entryPoint }]) => entryPoint === expectedEntryPoint
145
- );
146
- if (outputEntry === undefined) {
306
+ if (props.experimentalPublic && format === "service-worker") {
147
307
  throw new Error(
148
- `Cannot find entry-point "${expectedEntryPoint}" in generated bundle.`
308
+ "You cannot publish in the service-worker format with a public directory."
149
309
  );
150
310
  }
151
- const { format } = props;
152
- const bundle = {
153
- type: outputEntry[1].exports.length > 0 ? "esm" : "commonjs",
154
- exports: outputEntry[1].exports,
155
- };
156
311
 
157
- // TODO: instead of bundling the facade with the worker, we should just bundle the worker and expose it as a module.
158
- // That way we'll be able to accurately tell if this is a service worker or not.
159
-
160
- if (format === "modules" && bundle.type === "commonjs") {
161
- console.error("⎔ Cannot use modules with a commonjs bundle.");
162
- // TODO: a much better error message here, with what to do next
163
- return;
164
- }
165
- if (format === "service-worker" && bundle.type !== "esm") {
166
- console.error("⎔ Cannot use service-worker with a esm bundle.");
167
- // TODO: a much better error message here, with what to do next
168
- return;
312
+ if (config.wasm_modules && format === "modules") {
313
+ throw new Error(
314
+ "You cannot configure [wasm_modules] with an ES module worker. Instead, import the .wasm module directly in your code"
315
+ );
169
316
  }
170
317
 
171
- const content = await readFile(outputEntry[0], { encoding: "utf-8" });
172
- await destination.cleanup();
173
-
174
- // if config.migrations
175
- // get current migration tag
176
- let migrations;
177
- if (config.migrations !== undefined) {
178
- const scripts = await fetchResult<{ id: string; migration_tag: string }[]>(
179
- `/accounts/${accountId}/workers/scripts`
318
+ if (config.text_blobs && format === "modules") {
319
+ throw new Error(
320
+ "You cannot configure [text_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure `[rules]` in your wrangler.toml"
180
321
  );
181
- const script = scripts.find(({ id }) => id === scriptName);
182
- if (script?.migration_tag) {
183
- // was already published once
184
- const foundIndex = config.migrations.findIndex(
185
- (migration) => migration.tag === script.migration_tag
186
- );
187
- if (foundIndex === -1) {
188
- console.warn(
189
- `The published script ${scriptName} has a migration tag "${script.migration_tag}, which was not found in wrangler.toml. You may have already deleted it. Applying all available migrations to the script...`
190
- );
191
- migrations = {
192
- old_tag: script.migration_tag,
193
- new_tag: config.migrations[config.migrations.length - 1].tag,
194
- steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
195
- };
196
- } else {
197
- migrations = {
198
- old_tag: script.migration_tag,
199
- new_tag: config.migrations[config.migrations.length - 1].tag,
200
- steps: config.migrations
201
- .slice(foundIndex + 1)
202
- .map(({ tag: _tag, ...rest }) => rest),
203
- };
204
- }
205
- } else {
206
- migrations = {
207
- new_tag: config.migrations[config.migrations.length - 1].tag,
208
- steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
209
- };
210
- }
211
322
  }
212
323
 
213
- const assetPath = props.public || props.site || props.config.site?.bucket; // TODO: allow both
214
- const assets = assetPath
215
- ? await syncAssets(accountId, scriptName, assetPath, false)
216
- : { manifest: undefined, namespace: undefined };
217
-
218
- const bindings: CfWorkerInit["bindings"] = {
219
- kv_namespaces: envRootObj.kv_namespaces?.concat(
220
- assets.namespace
221
- ? { binding: "__STATIC_CONTENT", id: assets.namespace }
222
- : []
223
- ),
224
- vars: envRootObj.vars,
225
- durable_objects: envRootObj.durable_objects,
226
- services: envRootObj.experimental_services,
227
- };
324
+ if (config.data_blobs && format === "modules") {
325
+ throw new Error(
326
+ "You cannot configure [data_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure `[rules]` in your wrangler.toml"
327
+ );
328
+ }
329
+ try {
330
+ const { modules, resolvedEntryPointPath, bundleType } = await bundleWorker(
331
+ props.entry,
332
+ typeof destination === "string" ? destination : destination.path,
333
+ {
334
+ serveAssetsFromWorker: props.experimentalPublic,
335
+ jsxFactory,
336
+ jsxFragment,
337
+ rules: props.rules,
338
+ tsconfig: props.tsconfig ?? config.tsconfig,
339
+ minify,
340
+ nodeCompat,
341
+ }
342
+ );
228
343
 
229
- const worker: CfWorkerInit = {
230
- name: scriptName,
231
- main: {
232
- name: path.basename(outputEntry[0]),
233
- content: content,
234
- type: bundle.type === "esm" ? "esm" : "commonjs",
235
- },
236
- bindings,
237
- ...(migrations && { migrations }),
238
- modules: moduleCollector.modules.concat(
239
- assets.manifest
240
- ? {
241
- name: "__STATIC_CONTENT_MANIFEST",
242
- content: JSON.stringify(assets.manifest),
243
- type: "text",
244
- }
245
- : []
246
- ),
247
- compatibility_date: config.compatibility_date,
248
- compatibility_flags: config.compatibility_flags,
249
- usage_model: config.usage_model,
250
- };
344
+ const content = readFileSync(resolvedEntryPointPath, {
345
+ encoding: "utf-8",
346
+ });
251
347
 
252
- const start = Date.now();
253
- function formatTime(duration: number) {
254
- return `(${(duration / 1000).toFixed(2)} sec)`;
255
- }
348
+ // durable object migrations
349
+ const migrations = !props.dryRun
350
+ ? await getMigrationsToUpload(scriptName, {
351
+ accountId,
352
+ config,
353
+ legacyEnv: props.legacyEnv,
354
+ env: props.env,
355
+ })
356
+ : undefined;
357
+
358
+ const assets = await syncAssets(
359
+ accountId,
360
+ // When we're using the newer service environments, we wouldn't
361
+ // have added the env name on to the script name. However, we must
362
+ // include it in the kv namespace name regardless (since there's no
363
+ // concept of service environments for kv namespaces yet).
364
+ scriptName + (!props.legacyEnv && props.env ? `-${props.env}` : ""),
365
+ props.assetPaths,
366
+ false,
367
+ props.dryRun
368
+ );
256
369
 
257
- const notProd = !props.legacyEnv && props.env;
258
- const workerName = notProd ? `${scriptName} (${envName})` : scriptName;
259
- const workerUrl = notProd
260
- ? `/accounts/${accountId}/workers/services/${scriptName}/environments/${envName}`
261
- : `/accounts/${accountId}/workers/scripts/${scriptName}`;
370
+ const bindings: CfWorkerInit["bindings"] = {
371
+ kv_namespaces: (config.kv_namespaces || []).concat(
372
+ assets.namespace
373
+ ? { binding: "__STATIC_CONTENT", id: assets.namespace }
374
+ : []
375
+ ),
376
+ vars: config.vars,
377
+ wasm_modules: config.wasm_modules,
378
+ text_blobs: {
379
+ ...config.text_blobs,
380
+ ...(assets.manifest &&
381
+ format === "service-worker" && {
382
+ __STATIC_CONTENT_MANIFEST: "__STATIC_CONTENT_MANIFEST",
383
+ }),
384
+ },
385
+ data_blobs: config.data_blobs,
386
+ durable_objects: config.durable_objects,
387
+ r2_buckets: config.r2_buckets,
388
+ services: config.services,
389
+ unsafe: config.unsafe?.bindings,
390
+ };
391
+
392
+ if (assets.manifest) {
393
+ modules.push({
394
+ name: "__STATIC_CONTENT_MANIFEST",
395
+ content: JSON.stringify(assets.manifest),
396
+ type: "text",
397
+ });
398
+ }
262
399
 
263
- // Upload the script so it has time to propagate.
264
- const { available_on_subdomain } = await fetchResult(
265
- `${workerUrl}?available_on_subdomain=true`,
266
- {
267
- method: "PUT",
268
- // @ts-expect-error: TODO: fix this type error!
269
- body: toFormData(worker),
400
+ const worker: CfWorkerInit = {
401
+ name: scriptName,
402
+ main: {
403
+ name: path.basename(resolvedEntryPointPath),
404
+ content: content,
405
+ type: bundleType,
406
+ },
407
+ bindings,
408
+ migrations,
409
+ modules,
410
+ compatibility_date: props.compatibilityDate ?? config.compatibility_date,
411
+ compatibility_flags:
412
+ props.compatibilityFlags ?? config.compatibility_flags,
413
+ usage_model: config.usage_model,
414
+ };
415
+
416
+ const withoutStaticAssets = {
417
+ ...bindings,
418
+ kv_namespaces: config.kv_namespaces,
419
+ text_blobs: config.text_blobs,
420
+ };
421
+ printBindings(withoutStaticAssets);
422
+
423
+ if (!props.dryRun) {
424
+ // Upload the script so it has time to propagate.
425
+ // We can also now tell whether available_on_subdomain is set
426
+ available_on_subdomain = (
427
+ await fetchResult<{ available_on_subdomain: boolean }>(
428
+ workerUrl,
429
+ {
430
+ method: "PUT",
431
+ body: createWorkerUploadForm(worker),
432
+ },
433
+ new URLSearchParams({
434
+ include_subdomain_availability: "true",
435
+ // pass excludeScript so the whole body of the
436
+ // script doesn't get included in the response
437
+ excludeScript: "true",
438
+ })
439
+ )
440
+ ).available_on_subdomain;
270
441
  }
271
- );
442
+ } finally {
443
+ if (typeof destination !== "string") {
444
+ // this means we're using a temp dir,
445
+ // so let's clean up before we proceed
446
+ await destination.cleanup();
447
+ }
448
+ }
449
+
450
+ if (props.dryRun) {
451
+ logger.log(`--dry-run: exiting now.`);
452
+ return;
453
+ }
454
+ assert(accountId, "Missing accountId");
272
455
 
273
456
  const uploadMs = Date.now() - start;
274
- console.log("Uploaded", workerName, formatTime(uploadMs));
275
457
  const deployments: Promise<string[]>[] = [];
276
458
 
277
- const userSubdomain = (
278
- await fetchResult<{ subdomain: string }>(
279
- `/accounts/${accountId}/workers/subdomain`
280
- )
281
- ).subdomain;
282
-
283
- const scriptURL =
284
- props.legacyEnv || !props.env
285
- ? `${scriptName}.${userSubdomain}.workers.dev`
286
- : `${envName}.${scriptName}.${userSubdomain}.workers.dev`;
287
-
288
- // Enable the `workers.dev` subdomain.
289
- // TODO: Make this configurable.
290
- if (!available_on_subdomain) {
291
- deployments.push(
292
- fetchResult(`${workerUrl}/subdomain`, {
459
+ if (deployToWorkersDev) {
460
+ // Deploy to a subdomain of `workers.dev`
461
+ const userSubdomain = await getSubdomain(accountId);
462
+ const scriptURL =
463
+ props.legacyEnv || !props.env
464
+ ? `${scriptName}.${userSubdomain}.workers.dev`
465
+ : `${envName}.${scriptName}.${userSubdomain}.workers.dev`;
466
+ if (!available_on_subdomain) {
467
+ // Enable the `workers.dev` subdomain.
468
+ deployments.push(
469
+ fetchResult(`${workerUrl}/subdomain`, {
470
+ method: "POST",
471
+ body: JSON.stringify({ enabled: true }),
472
+ headers: {
473
+ "Content-Type": "application/json",
474
+ },
475
+ })
476
+ .then(() => [scriptURL])
477
+ // Add a delay when the subdomain is first created.
478
+ // This is to prevent an issue where a negative cache-hit
479
+ // causes the subdomain to be unavailable for 30 seconds.
480
+ // This is a temporary measure until we fix this on the edge.
481
+ .then(async (url) => {
482
+ await sleep(3000);
483
+ return url;
484
+ })
485
+ );
486
+ } else {
487
+ deployments.push(Promise.resolve([scriptURL]));
488
+ }
489
+ } else {
490
+ if (available_on_subdomain) {
491
+ // Disable the workers.dev deployment
492
+ await fetchResult(`${workerUrl}/subdomain`, {
293
493
  method: "POST",
294
- body: JSON.stringify({ enabled: true }),
494
+ body: JSON.stringify({ enabled: false }),
295
495
  headers: {
296
496
  "Content-Type": "application/json",
297
497
  },
298
- })
299
- .then(() => [scriptURL])
300
- // Add a delay when the subdomain is first created.
301
- // This is to prevent an issue where a negative cache-hit
302
- // causes the subdomain to be unavailable for 30 seconds.
303
- // This is a temporary measure until we fix this on the edge.
304
- .then(async (url) => {
305
- await sleep(3000);
306
- return url;
307
- })
308
- );
309
- } else {
310
- deployments.push(Promise.resolve([scriptURL]));
498
+ });
499
+ }
311
500
  }
312
501
 
502
+ logger.log("Uploaded", workerName, formatTime(uploadMs));
503
+
313
504
  // Update routing table for the script.
314
- if (routes && routes.length) {
505
+ if (routesOnly.length > 0) {
315
506
  deployments.push(
316
- fetchResult(`${workerUrl}/routes`, {
317
- // TODO: PATCH will not delete previous routes on this script,
318
- // whereas PUT will. We need to decide on the default behaviour
319
- // and how to configure it.
320
- method: "PUT",
321
- body: JSON.stringify(routes.map((pattern) => ({ pattern }))),
322
- headers: {
323
- "Content-Type": "application/json",
324
- },
325
- }).then(() => {
326
- if (routes.length > 10) {
327
- return routes
507
+ publishRoutes(routesOnly, { workerUrl, scriptName, notProd }).then(() => {
508
+ if (routesOnly.length > 10) {
509
+ return routesOnly
328
510
  .slice(0, 9)
329
- .map(String)
330
- .concat([`...and ${routes.length - 10} more routes`]);
511
+ .map((route) => renderRoute(route))
512
+ .concat([`...and ${routesOnly.length - 10} more routes`]);
331
513
  }
332
- return routes.map(String);
514
+ return routesOnly.map((route) => renderRoute(route));
333
515
  })
334
516
  );
335
517
  }
336
518
 
519
+ // Update custom domains for the script
520
+ if (customDomainsOnly.length > 0) {
521
+ deployments.push(publishCustomDomains(workerUrl, customDomainsOnly));
522
+ }
523
+
337
524
  // Configure any schedules for the script.
338
525
  // TODO: rename this to `schedules`?
339
526
  if (triggers && triggers.length) {
340
527
  deployments.push(
341
528
  fetchResult(`${workerUrl}/schedules`, {
342
- // TODO: Unlike routes, this endpoint does not support PATCH.
343
- // So technically, this will override any previous schedules.
344
- // We should change the endpoint to support PATCH.
529
+ // Note: PUT will override previous schedules on this script.
345
530
  method: "PUT",
346
531
  body: JSON.stringify(triggers.map((cron) => ({ cron }))),
347
532
  headers: {
348
533
  "Content-Type": "application/json",
349
534
  },
350
- }).then(() => triggers.map(String))
535
+ }).then(() => triggers.map((trigger) => `schedule: ${trigger}`))
351
536
  );
352
537
  }
353
538
 
354
- if (!deployments.length) {
355
- return;
356
- }
357
-
358
539
  const targets = await Promise.all(deployments);
359
540
  const deployMs = Date.now() - start - uploadMs;
360
- console.log("Deployed", workerName, formatTime(deployMs));
361
- for (const target of targets.flat()) {
362
- console.log(" ", target);
541
+
542
+ if (deployments.length > 0) {
543
+ logger.log("Published", workerName, formatTime(deployMs));
544
+ for (const target of targets.flat()) {
545
+ logger.log(" ", target);
546
+ }
547
+ } else {
548
+ logger.log("No publish targets for", workerName, formatTime(deployMs));
363
549
  }
364
550
  }
551
+
552
+ function formatTime(duration: number) {
553
+ return `(${(duration / 1000).toFixed(2)} sec)`;
554
+ }
555
+
556
+ async function getSubdomain(accountId: string): Promise<string> {
557
+ try {
558
+ const { subdomain } = await fetchResult(
559
+ `/accounts/${accountId}/workers/subdomain`
560
+ );
561
+ return subdomain;
562
+ } catch (e) {
563
+ const error = e as { code?: number };
564
+ if (typeof error === "object" && !!error && error.code === 10007) {
565
+ // 10007 error code: not found
566
+ // https://api.cloudflare.com/#worker-subdomain-get-subdomain
567
+
568
+ const errorMessage =
569
+ "Error: You need to register a workers.dev subdomain before publishing to workers.dev";
570
+ const solutionMessage =
571
+ "You can either publish your worker to one or more routes by specifying them in wrangler.toml, or register a workers.dev subdomain here:";
572
+ const onboardingLink = `https://dash.cloudflare.com/${accountId}/workers/onboarding`;
573
+
574
+ throw new Error(`${errorMessage}\n${solutionMessage}\n${onboardingLink}`);
575
+ } else {
576
+ throw e;
577
+ }
578
+ }
579
+ }
580
+
581
+ /**
582
+ * Associate the newly deployed Worker with the given routes.
583
+ */
584
+ async function publishRoutes(
585
+ routes: Route[],
586
+ {
587
+ workerUrl,
588
+ scriptName,
589
+ notProd,
590
+ }: { workerUrl: string; scriptName: string; notProd: boolean }
591
+ ): Promise<string[]> {
592
+ try {
593
+ return await fetchResult(`${workerUrl}/routes`, {
594
+ // Note: PUT will delete previous routes on this script.
595
+ method: "PUT",
596
+ body: JSON.stringify(
597
+ routes.map((route) =>
598
+ typeof route !== "object" ? { pattern: route } : route
599
+ )
600
+ ),
601
+ headers: {
602
+ "Content-Type": "application/json",
603
+ },
604
+ });
605
+ } catch (e) {
606
+ if (isAuthenticationError(e)) {
607
+ // An authentication error is probably due to a known issue,
608
+ // where the user is logged in via an API token that does not have "All Zones".
609
+ return await publishRoutesFallback(routes, { scriptName, notProd });
610
+ } else {
611
+ throw e;
612
+ }
613
+ }
614
+ }
615
+
616
+ /**
617
+ * Try updating routes for the Worker using a less optimal zone-based API.
618
+ *
619
+ * Compute match zones to the routes, then for each route attempt to connect it to the Worker via the zone.
620
+ */
621
+ async function publishRoutesFallback(
622
+ routes: Route[],
623
+ { scriptName, notProd }: { scriptName: string; notProd: boolean }
624
+ ) {
625
+ if (notProd) {
626
+ throw new Error(
627
+ "Service environments combined with an API token that doesn't have 'All Zones' permissions is not supported.\n" +
628
+ "Either turn off service environments by setting `legacy_env = true`, creating an API token with 'All Zones' permissions, or logging in via OAuth"
629
+ );
630
+ }
631
+ logger.warn(
632
+ "The current authentication token does not have 'All Zones' permissions.\n" +
633
+ "Falling back to using the zone-based API endpoint to update each route individually.\n" +
634
+ "Note that there is no access to routes associated with zones that the API token does not have permission for.\n" +
635
+ "Existing routes for this Worker in such zones will not be deleted."
636
+ );
637
+
638
+ const deployedRoutes: string[] = [];
639
+
640
+ // Collect the routes (and their zones) that will be deployed.
641
+ const activeZones = new Map<string, string>();
642
+ const routesToDeploy = new Map<string, string>();
643
+ for (const route of routes) {
644
+ const zone = await getZoneForRoute(route);
645
+ if (zone) {
646
+ activeZones.set(zone.id, zone.host);
647
+ routesToDeploy.set(
648
+ typeof route === "string" ? route : route.pattern,
649
+ zone.id
650
+ );
651
+ }
652
+ }
653
+
654
+ // Collect the routes that are already deployed.
655
+ const allRoutes = new Map<string, string>();
656
+ const alreadyDeployedRoutes = new Set<string>();
657
+ for (const [zone, host] of activeZones) {
658
+ try {
659
+ for (const { pattern, script } of await fetchListResult<{
660
+ pattern: string;
661
+ script: string;
662
+ }>(`/zones/${zone}/workers/routes`)) {
663
+ allRoutes.set(pattern, script);
664
+ if (script === scriptName) {
665
+ alreadyDeployedRoutes.add(pattern);
666
+ }
667
+ }
668
+ } catch (e) {
669
+ if (isAuthenticationError(e)) {
670
+ e.notes.push({
671
+ text: `This could be because the API token being used does not have permission to access the zone "${host}" (${zone}).`,
672
+ });
673
+ }
674
+ throw e;
675
+ }
676
+ }
677
+
678
+ // Deploy each route that is not already deployed.
679
+ for (const [routePattern, zoneId] of routesToDeploy.entries()) {
680
+ if (allRoutes.has(routePattern)) {
681
+ const knownScript = allRoutes.get(routePattern);
682
+ if (knownScript === scriptName) {
683
+ // This route is already associated with this worker, so no need to hit the API.
684
+ alreadyDeployedRoutes.delete(routePattern);
685
+ continue;
686
+ } else {
687
+ throw new Error(
688
+ `The route with pattern "${routePattern}" is already associated with another worker called "${knownScript}".`
689
+ );
690
+ }
691
+ }
692
+
693
+ const { pattern } = await fetchResult(`/zones/${zoneId}/workers/routes`, {
694
+ method: "POST",
695
+ body: JSON.stringify({
696
+ pattern: routePattern,
697
+ script: scriptName,
698
+ }),
699
+ headers: {
700
+ "Content-Type": "application/json",
701
+ },
702
+ });
703
+
704
+ deployedRoutes.push(pattern);
705
+ }
706
+
707
+ if (alreadyDeployedRoutes.size) {
708
+ logger.warn(
709
+ "Previously deployed routes:\n" +
710
+ "The following routes were already associated with this worker, and have not been deleted:\n" +
711
+ [...alreadyDeployedRoutes.values()].map((route) => ` - "${route}"\n`) +
712
+ "If these routes are not wanted then you can remove them in the dashboard."
713
+ );
714
+ }
715
+
716
+ return deployedRoutes;
717
+ }
718
+
719
+ function isAuthenticationError(e: unknown): e is ParseError {
720
+ return e instanceof ParseError && (e as { code?: number }).code === 10000;
721
+ }