wrangler 2.0.6 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/parse.ts CHANGED
@@ -62,8 +62,6 @@ export class ParseError extends Error implements Message {
62
62
  }
63
63
  }
64
64
 
65
- /* eslint-disable @typescript-eslint/no-explicit-any */
66
-
67
65
  const TOML_ERROR_NAME = "TomlError";
68
66
  const TOML_ERROR_SUFFIX = " at row ";
69
67
 
@@ -78,7 +76,7 @@ type TomlError = Error & {
78
76
  export function parseTOML(input: string, file?: string): TOML.JsonMap | never {
79
77
  try {
80
78
  // Normalize CRLF to LF to avoid hitting https://github.com/iarna/iarna-toml/issues/33.
81
- const normalizedInput = input.replace(/\r\n$/g, "\n");
79
+ const normalizedInput = input.replace(/\r\n/g, "\n");
82
80
  return TOML.parse(normalizedInput);
83
81
  } catch (err) {
84
82
  const { name, message, line, col } = err as TomlError;
@@ -100,10 +98,29 @@ export function parseTOML(input: string, file?: string): TOML.JsonMap | never {
100
98
 
101
99
  const JSON_ERROR_SUFFIX = " in JSON at position ";
102
100
 
101
+ /**
102
+ * A minimal type describing a package.json file.
103
+ */
104
+ export type PackageJSON = {
105
+ devDependencies?: Record<string, unknown>;
106
+ dependencies?: Record<string, unknown>;
107
+ scripts?: Record<string, unknown>;
108
+ };
109
+
110
+ /**
111
+ * A typed version of `parseJSON()`.
112
+ */
113
+ export function parsePackageJSON<T extends PackageJSON = PackageJSON>(
114
+ input: string,
115
+ file?: string
116
+ ): T {
117
+ return parseJSON<T>(input, file);
118
+ }
119
+
103
120
  /**
104
121
  * A wrapper around `JSON.parse` that throws a `ParseError`.
105
122
  */
106
- export function parseJSON(input: string, file?: string): any {
123
+ export function parseJSON<T>(input: string, file?: string): T {
107
124
  try {
108
125
  return JSON.parse(input);
109
126
  } catch (err) {
package/src/proxy.ts CHANGED
@@ -2,11 +2,13 @@ import { createServer as createHttpServer } from "node:http";
2
2
  import { connect } from "node:http2";
3
3
  import { createServer as createHttpsServer } from "node:https";
4
4
  import WebSocket from "faye-websocket";
5
+ import { createHttpTerminator } from "http-terminator";
5
6
  import { useEffect, useRef, useState } from "react";
6
7
  import serveStatic from "serve-static";
7
8
  import { getHttpsOptions } from "./https-options";
8
9
  import { logger } from "./logger";
9
10
  import type { CfPreviewToken } from "./create-worker-preview";
11
+ import type { HttpTerminator } from "http-terminator";
10
12
  import type {
11
13
  IncomingHttpHeaders,
12
14
  RequestListener,
@@ -67,6 +69,11 @@ function rewriteRemoteHostToLocalHostInHeaders(
67
69
  }
68
70
  }
69
71
 
72
+ type PreviewProxy = {
73
+ server: HttpServer | HttpsServer;
74
+ terminator: HttpTerminator;
75
+ };
76
+
70
77
  export function usePreviewServer({
71
78
  previewToken,
72
79
  publicRoot,
@@ -81,21 +88,26 @@ export function usePreviewServer({
81
88
  ip: string;
82
89
  }) {
83
90
  /** Creates an HTTP/1 proxy that sends requests over HTTP/2. */
84
- const [proxyServer, setProxyServer] = useState<HttpServer | HttpsServer>();
91
+ const [proxy, setProxy] = useState<PreviewProxy>();
85
92
 
86
93
  /**
87
94
  * Create the instance of the local proxy server that will pass on
88
95
  * requests to the preview worker.
89
96
  */
90
97
  useEffect(() => {
91
- if (proxyServer === undefined) {
98
+ if (proxy === undefined) {
92
99
  createProxyServer(localProtocol)
93
- .then((proxy) => setProxyServer(proxy))
100
+ .then((server) => {
101
+ setProxy({
102
+ server,
103
+ terminator: createHttpTerminator({ server }),
104
+ });
105
+ })
94
106
  .catch(async (err) => {
95
107
  logger.error("Failed to create proxy server:", err);
96
108
  });
97
109
  }
98
- }, [proxyServer, localProtocol]);
110
+ }, [proxy, localProtocol]);
99
111
 
100
112
  /**
101
113
  * When we're not connected / getting a fresh token on changes,
@@ -123,7 +135,7 @@ export function usePreviewServer({
123
135
  }
124
136
 
125
137
  useEffect(() => {
126
- if (proxyServer === undefined) {
138
+ if (proxy === undefined) {
127
139
  return;
128
140
  }
129
141
 
@@ -138,8 +150,8 @@ export function usePreviewServer({
138
150
  // store the stream in a buffer so we can replay it later
139
151
  streamBufferRef.current.push({ stream, headers });
140
152
  };
141
- proxyServer.on("stream", bufferStream);
142
- cleanupListeners.push(() => proxyServer.off("stream", bufferStream));
153
+ proxy.server.on("stream", bufferStream);
154
+ cleanupListeners.push(() => proxy.server.off("stream", bufferStream));
143
155
 
144
156
  const bufferRequestResponse = (
145
157
  request: IncomingMessage,
@@ -149,9 +161,9 @@ export function usePreviewServer({
149
161
  requestResponseBufferRef.current.push({ request, response });
150
162
  };
151
163
 
152
- proxyServer.on("request", bufferRequestResponse);
164
+ proxy.server.on("request", bufferRequestResponse);
153
165
  cleanupListeners.push(() =>
154
- proxyServer.off("request", bufferRequestResponse)
166
+ proxy.server.off("request", bufferRequestResponse)
155
167
  );
156
168
  return () => {
157
169
  cleanupListeners.forEach((cleanup) => cleanup());
@@ -179,8 +191,8 @@ export function usePreviewServer({
179
191
  port,
180
192
  localProtocol
181
193
  );
182
- proxyServer.on("stream", handleStream);
183
- cleanupListeners.push(() => proxyServer.off("stream", handleStream));
194
+ proxy.server.on("stream", handleStream);
195
+ cleanupListeners.push(() => proxy.server.off("stream", handleStream));
184
196
 
185
197
  // flush and replay buffered streams
186
198
  streamBufferRef.current.forEach((buffer) =>
@@ -236,9 +248,9 @@ export function usePreviewServer({
236
248
  ? createHandleAssetsRequest(assetPath, handleRequest)
237
249
  : handleRequest;
238
250
 
239
- proxyServer.on("request", actualHandleRequest);
251
+ proxy.server.on("request", actualHandleRequest);
240
252
  cleanupListeners.push(() =>
241
- proxyServer.off("request", actualHandleRequest)
253
+ proxy.server.off("request", actualHandleRequest)
242
254
  );
243
255
 
244
256
  // flush and replay buffered requests
@@ -270,8 +282,8 @@ export function usePreviewServer({
270
282
  remoteWebsocketClient.close();
271
283
  });
272
284
  };
273
- proxyServer.on("upgrade", handleUpgrade);
274
- cleanupListeners.push(() => proxyServer.off("upgrade", handleUpgrade));
285
+ proxy.server.on("upgrade", handleUpgrade);
286
+ cleanupListeners.push(() => proxy.server.off("upgrade", handleUpgrade));
275
287
 
276
288
  return () => {
277
289
  cleanupListeners.forEach((cleanup) => cleanup());
@@ -281,7 +293,7 @@ export function usePreviewServer({
281
293
  publicRoot,
282
294
  port,
283
295
  localProtocol,
284
- proxyServer,
296
+ proxy,
285
297
  // We use a state value as a sigil to trigger reconnecting the server.
286
298
  // It's not used inside the effect, so react-hooks/exhaustive-deps
287
299
  // doesn't complain if it's not included in the dependency array.
@@ -293,7 +305,7 @@ export function usePreviewServer({
293
305
  // containing component is mounted/unmounted.
294
306
  useEffect(() => {
295
307
  const abortController = new AbortController();
296
- if (proxyServer === undefined) {
308
+ if (proxy === undefined) {
297
309
  return;
298
310
  }
299
311
 
@@ -303,8 +315,10 @@ export function usePreviewServer({
303
315
  abortSignal: abortController.signal,
304
316
  })
305
317
  .then(() => {
306
- proxyServer.listen(port, ip);
307
- logger.log(`⬣ Listening at ${localProtocol}://${ip}:${port}`);
318
+ proxy.server.on("listening", () => {
319
+ logger.log(`⬣ Listening at ${localProtocol}://${ip}:${port}`);
320
+ });
321
+ proxy.server.listen(port, ip);
308
322
  })
309
323
  .catch((err) => {
310
324
  if ((err as { code: string }).code !== "ABORT_ERR") {
@@ -313,10 +327,12 @@ export function usePreviewServer({
313
327
  });
314
328
 
315
329
  return () => {
316
- proxyServer.close();
317
330
  abortController.abort();
331
+ // Running `proxy.server.close()` does not close open connections, preventing the process from exiting.
332
+ // So we use this `terminator` to close all the connections and force the server to shutdown.
333
+ proxy.terminator.terminate();
318
334
  };
319
- }, [port, ip, proxyServer, localProtocol]);
335
+ }, [port, ip, proxy, localProtocol]);
320
336
  }
321
337
 
322
338
  function createHandleAssetsRequest(
@@ -404,7 +420,7 @@ export async function waitForPortToBeAvailable(
404
420
  ): Promise<void> {
405
421
  return new Promise((resolve, reject) => {
406
422
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
407
- (options.abortSignal as any).addEventListener("abort", () => {
423
+ options.abortSignal.addEventListener("abort", () => {
408
424
  const abortError = new Error("waitForPortToBeAvailable() aborted");
409
425
  (abortError as Error & { code: string }).code = "ABORT_ERR";
410
426
  doReject(abortError);
package/src/publish.ts CHANGED
@@ -5,8 +5,10 @@ import { URLSearchParams } from "node:url";
5
5
  import tmp from "tmp-promise";
6
6
  import { bundleWorker } from "./bundle";
7
7
  import { fetchResult } from "./cfetch";
8
+ import { printBindings } from "./config";
8
9
  import { createWorkerUploadForm } from "./create-worker-upload-form";
9
10
  import { confirm } from "./dialogs";
11
+ import { getMigrationsToUpload } from "./durable";
10
12
  import { logger } from "./logger";
11
13
  import { syncAssets } from "./sites";
12
14
  import type { Config } from "./config";
@@ -337,103 +339,19 @@ export default async function publish(props: Props): Promise<void> {
337
339
  }
338
340
  );
339
341
 
340
- // Some validation of durable objects + migrations
341
- if (config.durable_objects.bindings.length > 0) {
342
- // intrinsic [durable_objects] implies [migrations]
343
- const exportedDurableObjects = config.durable_objects.bindings.filter(
344
- (binding) => !binding.script_name
345
- );
346
- if (exportedDurableObjects.length > 0 && config.migrations.length === 0) {
347
- logger.warn(
348
- `In wrangler.toml, you have configured [durable_objects] exported by this Worker (${exportedDurableObjects.map(
349
- (durable) => durable.class_name
350
- )}), but no [migrations] for them. This may not work as expected until you add a [migrations] section to your wrangler.toml. Refer to https://developers.cloudflare.com/workers/learning/using-durable-objects/#durable-object-migrations-in-wranglertoml for more details.`
351
- );
352
- }
353
- }
354
-
355
342
  const content = readFileSync(resolvedEntryPointPath, {
356
343
  encoding: "utf-8",
357
344
  });
358
345
 
359
- // if config.migrations
360
- let migrations;
361
- if (!props.dryRun && config.migrations.length > 0) {
362
- // get current migration tag
363
- type ScriptData = { id: string; migration_tag?: string };
364
- let script: ScriptData | undefined;
365
- if (!props.legacyEnv) {
366
- try {
367
- if (props.env) {
368
- const scriptData = await fetchResult<{
369
- script: ScriptData;
370
- }>(
371
- `/accounts/${accountId}/workers/services/${scriptName}/environments/${props.env}`
372
- );
373
- script = scriptData.script;
374
- } else {
375
- const scriptData = await fetchResult<{
376
- default_environment: {
377
- script: ScriptData;
378
- };
379
- }>(`/accounts/${accountId}/workers/services/${scriptName}`);
380
- script = scriptData.default_environment.script;
381
- }
382
- } catch (err) {
383
- if (
384
- ![
385
- 10090, // corresponds to workers.api.error.service_not_found, so the script wasn't previously published at all
386
- 10092, // workers.api.error.environment_not_found, so the script wasn't published to this environment yet
387
- ].includes((err as { code: number }).code)
388
- ) {
389
- throw err;
390
- }
391
- // else it's a 404, no script found, and we can proceed
392
- }
393
- } else {
394
- const scripts = await fetchResult<ScriptData[]>(
395
- `/accounts/${accountId}/workers/scripts`
396
- );
397
- script = scripts.find(({ id }) => id === scriptName);
398
- }
399
-
400
- if (script?.migration_tag) {
401
- // was already published once
402
- const scriptMigrationTag = script.migration_tag;
403
- const foundIndex = config.migrations.findIndex(
404
- (migration) => migration.tag === scriptMigrationTag
405
- );
406
- if (foundIndex === -1) {
407
- logger.warn(
408
- `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...`
409
- );
410
- migrations = {
411
- old_tag: script.migration_tag,
412
- new_tag: config.migrations[config.migrations.length - 1].tag,
413
- steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
414
- };
415
- } else {
416
- if (foundIndex !== config.migrations.length - 1) {
417
- // there are new migrations to send up
418
- migrations = {
419
- old_tag: script.migration_tag,
420
- new_tag: config.migrations[config.migrations.length - 1].tag,
421
- steps: config.migrations
422
- .slice(foundIndex + 1)
423
- .map(({ tag: _tag, ...rest }) => rest),
424
- };
425
- }
426
- // else, we're up to date, no migrations to send
427
- }
428
- } else {
429
- // first time publishing durable objects to this script,
430
- // so we send all the migrations
431
- migrations = {
432
- new_tag: config.migrations[config.migrations.length - 1].tag,
433
- steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
434
- };
435
- }
436
- }
346
+ // durable object migrations
347
+ const migrations = !props.dryRun
348
+ ? await getMigrationsToUpload(scriptName, {
349
+ accountId,
350
+ config,
351
+ legacyEnv: props.legacyEnv,
352
+ env: props.env,
353
+ })
354
+ : undefined;
437
355
 
438
356
  const assets = await syncAssets(
439
357
  accountId,
@@ -493,6 +411,13 @@ export default async function publish(props: Props): Promise<void> {
493
411
  usage_model: config.usage_model,
494
412
  };
495
413
 
414
+ const withoutStaticAssets = {
415
+ ...bindings,
416
+ kv_namespaces: config.kv_namespaces,
417
+ text_blobs: config.text_blobs,
418
+ };
419
+ printBindings(withoutStaticAssets);
420
+
496
421
  if (!props.dryRun) {
497
422
  // Upload the script so it has time to propagate.
498
423
  // We can also now tell whether available_on_subdomain is set
package/src/sites.tsx CHANGED
@@ -187,14 +187,9 @@ export async function syncAssets(
187
187
 
188
188
  await Promise.all([
189
189
  // upload all the new assets
190
- putKVBulkKeyValue(accountId, namespace, toUpload, () => {}),
190
+ putKVBulkKeyValue(accountId, namespace, toUpload),
191
191
  // delete all the unused assets
192
- deleteKVBulkKeyValue(
193
- accountId,
194
- namespace,
195
- Array.from(namespaceKeys),
196
- () => {}
197
- ),
192
+ deleteKVBulkKeyValue(accountId, namespace, Array.from(namespaceKeys)),
198
193
  ]);
199
194
 
200
195
  logger.log("↗️ Done syncing assets");
@@ -8,8 +8,23 @@
8
8
  * Learn more at https://developers.cloudflare.com/workers/
9
9
  */
10
10
 
11
+ export interface Env {
12
+ // Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/
13
+ // MY_KV_NAMESPACE: KVNamespace;
14
+ //
15
+ // Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/
16
+ // MY_DURABLE_OBJECT: DurableObjectNamespace;
17
+ //
18
+ // Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/
19
+ // MY_BUCKET: R2Bucket;
20
+ }
21
+
11
22
  export default {
12
- async fetch(request: Request): Promise<Response> {
23
+ async fetch(
24
+ request: Request,
25
+ env: Env,
26
+ ctx: ExecutionContext
27
+ ): Promise<Response> {
13
28
  return new Response("Hello World!");
14
29
  },
15
30
  };