wrangler 0.0.0-e2f9bb2 → 0.0.0-e3278fa

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 (90) hide show
  1. package/Cloudflare_CA.pem +18 -0
  2. package/README.md +3 -1
  3. package/bin/wrangler.js +36 -4
  4. package/package.json +12 -7
  5. package/pages/functions/buildPlugin.ts +13 -0
  6. package/pages/functions/buildWorker.ts +14 -1
  7. package/pages/functions/template-plugin.ts +3 -2
  8. package/src/__tests__/configuration.test.ts +370 -87
  9. package/src/__tests__/dev.test.tsx +298 -77
  10. package/src/__tests__/helpers/mock-dialogs.ts +43 -2
  11. package/src/__tests__/index.test.ts +79 -72
  12. package/src/__tests__/init.test.ts +718 -123
  13. package/src/__tests__/jest.setup.ts +8 -0
  14. package/src/__tests__/kv.test.ts +173 -146
  15. package/src/__tests__/package-manager.test.ts +154 -7
  16. package/src/__tests__/pages.test.ts +478 -53
  17. package/src/__tests__/parse.test.ts +8 -0
  18. package/src/__tests__/publish.test.ts +1094 -215
  19. package/src/__tests__/r2.test.ts +55 -36
  20. package/src/__tests__/secret.test.ts +39 -4
  21. package/src/__tests__/whoami.test.tsx +34 -0
  22. package/src/abort.d.ts +3 -0
  23. package/src/bundle.ts +34 -6
  24. package/src/cfetch/index.ts +21 -4
  25. package/src/cfetch/internal.ts +23 -18
  26. package/src/config/config.ts +2 -2
  27. package/src/config/environment.ts +40 -14
  28. package/src/config/index.ts +162 -0
  29. package/src/config/validation-helpers.ts +10 -1
  30. package/src/config/validation.ts +180 -65
  31. package/src/create-worker-preview.ts +32 -22
  32. package/src/create-worker-upload-form.ts +22 -8
  33. package/src/dev/dev.tsx +12 -18
  34. package/src/dev/local.tsx +10 -0
  35. package/src/dev/remote.tsx +40 -16
  36. package/src/dialogs.tsx +48 -0
  37. package/src/durable.ts +102 -0
  38. package/src/entry.ts +64 -9
  39. package/src/index.tsx +548 -317
  40. package/src/inspect.ts +39 -0
  41. package/src/kv.ts +114 -25
  42. package/src/open-in-browser.ts +5 -12
  43. package/src/package-manager.ts +50 -3
  44. package/src/pages.tsx +434 -207
  45. package/src/parse.ts +23 -4
  46. package/src/proxy.ts +59 -35
  47. package/src/publish.ts +374 -127
  48. package/src/sites.tsx +61 -28
  49. package/src/user.tsx +23 -2
  50. package/src/whoami.tsx +3 -2
  51. package/src/worker.ts +10 -1
  52. package/src/zones.ts +73 -0
  53. package/templates/new-worker-scheduled.js +17 -0
  54. package/templates/new-worker-scheduled.ts +32 -0
  55. package/templates/new-worker.ts +16 -1
  56. package/templates/static-asset-facade.js +0 -4
  57. package/wrangler-dist/cli.js +96965 -83414
  58. package/vendor/@cloudflare/kv-asset-handler/CHANGELOG.md +0 -332
  59. package/vendor/@cloudflare/kv-asset-handler/LICENSE_APACHE +0 -176
  60. package/vendor/@cloudflare/kv-asset-handler/LICENSE_MIT +0 -25
  61. package/vendor/@cloudflare/kv-asset-handler/README.md +0 -245
  62. package/vendor/@cloudflare/kv-asset-handler/dist/index.d.ts +0 -32
  63. package/vendor/@cloudflare/kv-asset-handler/dist/index.js +0 -354
  64. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.d.ts +0 -13
  65. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.js +0 -148
  66. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.d.ts +0 -1
  67. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.js +0 -436
  68. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.d.ts +0 -1
  69. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.js +0 -40
  70. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.d.ts +0 -1
  71. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.js +0 -42
  72. package/vendor/@cloudflare/kv-asset-handler/dist/types.d.ts +0 -26
  73. package/vendor/@cloudflare/kv-asset-handler/dist/types.js +0 -31
  74. package/vendor/@cloudflare/kv-asset-handler/package.json +0 -52
  75. package/vendor/@cloudflare/kv-asset-handler/src/index.ts +0 -296
  76. package/vendor/@cloudflare/kv-asset-handler/src/mocks.ts +0 -136
  77. package/vendor/@cloudflare/kv-asset-handler/src/test/getAssetFromKV.ts +0 -464
  78. package/vendor/@cloudflare/kv-asset-handler/src/test/mapRequestToAsset.ts +0 -33
  79. package/vendor/@cloudflare/kv-asset-handler/src/test/serveSinglePageApp.ts +0 -42
  80. package/vendor/@cloudflare/kv-asset-handler/src/types.ts +0 -39
  81. package/vendor/wrangler-mime/CHANGELOG.md +0 -289
  82. package/vendor/wrangler-mime/LICENSE +0 -21
  83. package/vendor/wrangler-mime/Mime.js +0 -97
  84. package/vendor/wrangler-mime/README.md +0 -187
  85. package/vendor/wrangler-mime/cli.js +0 -46
  86. package/vendor/wrangler-mime/index.js +0 -4
  87. package/vendor/wrangler-mime/lite.js +0 -4
  88. package/vendor/wrangler-mime/package.json +0 -52
  89. package/vendor/wrangler-mime/types/other.js +0 -1
  90. package/vendor/wrangler-mime/types/standard.js +0 -1
@@ -32,20 +32,24 @@ export interface WorkerMetadata {
32
32
  compatibility_flags?: string[];
33
33
  usage_model?: "bundled" | "unbound";
34
34
  migrations?: CfDurableObjectMigrations;
35
+ // If you add any new binding types here, also add it to safeBindings
36
+ // under validateUnsafeBinding in config/validation.ts
35
37
  bindings: (
36
- | { type: "kv_namespace"; name: string; namespace_id: string }
37
38
  | { type: "plain_text"; name: string; text: string }
38
39
  | { type: "json"; name: string; json: unknown }
39
40
  | { type: "wasm_module"; name: string; part: string }
40
41
  | { type: "text_blob"; name: string; part: string }
41
42
  | { type: "data_blob"; name: string; part: string }
43
+ | { type: "kv_namespace"; name: string; namespace_id: string }
42
44
  | {
43
45
  type: "durable_object_namespace";
44
46
  name: string;
45
47
  class_name: string;
46
48
  script_name?: string;
49
+ environment?: string;
47
50
  }
48
51
  | { type: "r2_bucket"; name: string; bucket_name: string }
52
+ | { type: "service"; name: string; service: string; environment?: string }
49
53
  )[];
50
54
  }
51
55
 
@@ -67,6 +71,14 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
67
71
 
68
72
  const metadataBindings: WorkerMetadata["bindings"] = [];
69
73
 
74
+ Object.entries(bindings.vars || {})?.forEach(([key, value]) => {
75
+ if (typeof value === "string") {
76
+ metadataBindings.push({ name: key, type: "plain_text", text: value });
77
+ } else {
78
+ metadataBindings.push({ name: key, type: "json", json: value });
79
+ }
80
+ });
81
+
70
82
  bindings.kv_namespaces?.forEach(({ id, binding }) => {
71
83
  metadataBindings.push({
72
84
  name: binding,
@@ -76,12 +88,13 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
76
88
  });
77
89
 
78
90
  bindings.durable_objects?.bindings.forEach(
79
- ({ name, class_name, script_name }) => {
91
+ ({ name, class_name, script_name, environment }) => {
80
92
  metadataBindings.push({
81
93
  name,
82
94
  type: "durable_object_namespace",
83
95
  class_name: class_name,
84
96
  ...(script_name && { script_name }),
97
+ ...(environment && { environment }),
85
98
  });
86
99
  }
87
100
  );
@@ -94,12 +107,13 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
94
107
  });
95
108
  });
96
109
 
97
- Object.entries(bindings.vars || {})?.forEach(([key, value]) => {
98
- if (typeof value === "string") {
99
- metadataBindings.push({ name: key, type: "plain_text", text: value });
100
- } else {
101
- metadataBindings.push({ name: key, type: "json", json: value });
102
- }
110
+ bindings.services?.forEach(({ binding, service, environment }) => {
111
+ metadataBindings.push({
112
+ name: binding,
113
+ type: "service",
114
+ service,
115
+ ...(environment && { environment }),
116
+ });
103
117
  });
104
118
 
105
119
  for (const [name, filePath] of Object.entries(bindings.wasm_modules || {})) {
package/src/dev/dev.tsx CHANGED
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
+ import * as path from "node:path";
2
3
  import { watch } from "chokidar";
3
4
  import clipboardy from "clipboardy";
4
5
  import commandExists from "command-exists";
@@ -9,9 +10,9 @@ import onExit from "signal-exit";
9
10
  import tmp from "tmp-promise";
10
11
  import { fetch } from "undici";
11
12
  import { runCustomBuild } from "../entry";
13
+ import { openInspector } from "../inspect";
12
14
  import { logger } from "../logger";
13
15
  import openInBrowser from "../open-in-browser";
14
- import { getAPIToken } from "../user";
15
16
  import { Local } from "./local";
16
17
  import { Remote } from "./remote";
17
18
  import { useEsbuild } from "./use-esbuild";
@@ -52,16 +53,11 @@ export type DevProps = {
52
53
  };
53
54
  env: string | undefined;
54
55
  legacyEnv: boolean;
55
- zone:
56
- | {
57
- id: string;
58
- host: string;
59
- }
60
- | undefined;
56
+ zone: string | undefined;
57
+ host: string | undefined;
61
58
  };
62
59
 
63
60
  export function DevImplementation(props: DevProps): JSX.Element {
64
- const apiToken = props.initialMode === "remote" ? getAPIToken() : undefined;
65
61
  const directory = useTmpDir();
66
62
 
67
63
  useCustomBuild(props.entry, props.build);
@@ -106,19 +102,17 @@ export function DevImplementation(props: DevProps): JSX.Element {
106
102
  // only load the UI if we're running in a supported environment
107
103
  const { isRawModeSupported } = useStdin();
108
104
  return isRawModeSupported ? (
109
- <InteractiveDevSession {...props} bundle={bundle} apiToken={apiToken} />
105
+ <InteractiveDevSession {...props} bundle={bundle} />
110
106
  ) : (
111
107
  <DevSession
112
108
  {...props}
113
109
  bundle={bundle}
114
- apiToken={apiToken}
115
110
  local={props.initialMode === "local"}
116
111
  />
117
112
  );
118
113
  }
119
114
 
120
115
  type InteractiveDevSessionProps = DevProps & {
121
- apiToken: string | undefined;
122
116
  bundle: EsbuildBundle | undefined;
123
117
  };
124
118
 
@@ -164,6 +158,7 @@ function DevSession(props: DevSessionProps) {
164
158
  bundle={props.bundle}
165
159
  format={props.entry.format}
166
160
  compatibilityDate={props.compatibilityDate}
161
+ localProtocol={props.localProtocol}
167
162
  compatibilityFlags={props.compatibilityFlags}
168
163
  bindings={props.bindings}
169
164
  assetPaths={props.assetPaths}
@@ -181,7 +176,6 @@ function DevSession(props: DevSessionProps) {
181
176
  bundle={props.bundle}
182
177
  format={props.entry.format}
183
178
  accountId={props.accountId}
184
- apiToken={props.apiToken}
185
179
  bindings={props.bindings}
186
180
  assetPaths={props.assetPaths}
187
181
  public={props.public}
@@ -195,6 +189,7 @@ function DevSession(props: DevSessionProps) {
195
189
  env={props.env}
196
190
  legacyEnv={props.legacyEnv}
197
191
  zone={props.zone}
192
+ host={props.host}
198
193
  />
199
194
  );
200
195
  }
@@ -242,9 +237,11 @@ function useCustomBuild(
242
237
  persistent: true,
243
238
  ignoreInitial: true,
244
239
  }).on("all", (_event, filePath) => {
240
+ const relativeFile =
241
+ path.relative(expectedEntry.directory, expectedEntry.file) || ".";
245
242
  //TODO: we should buffer requests to the proxy until this completes
246
243
  logger.log(`The file ${filePath} changed, restarting build...`);
247
- runCustomBuild(expectedEntry.file, build).catch((err) => {
244
+ runCustomBuild(expectedEntry.file, relativeFile, build).catch((err) => {
248
245
  logger.error("Custom build failed:", err);
249
246
  });
250
247
  });
@@ -253,7 +250,7 @@ function useCustomBuild(
253
250
  return () => {
254
251
  watcher?.close();
255
252
  };
256
- }, [build, expectedEntry.file]);
253
+ }, [build, expectedEntry]);
257
254
  }
258
255
 
259
256
  function sleep(period: number) {
@@ -375,10 +372,7 @@ function useHotkeys(
375
372
  }
376
373
  // toggle inspector
377
374
  case "d": {
378
- await openInBrowser(
379
- `https://built-devtools.pages.dev/js_app?experiments=true&v8only=true&ws=localhost:${inspectorPort}/ws`,
380
- { forceChromium: true }
381
- );
375
+ await openInspector(inspectorPort);
382
376
  break;
383
377
  }
384
378
  // toggle local
package/src/dev/local.tsx CHANGED
@@ -21,6 +21,7 @@ interface LocalProps {
21
21
  compatibilityDate: string;
22
22
  compatibilityFlags: string[] | undefined;
23
23
  bindings: CfWorkerInit["bindings"];
24
+ localProtocol: "http" | "https";
24
25
  assetPaths: AssetPaths | undefined;
25
26
  public: string | undefined;
26
27
  port: number;
@@ -54,6 +55,7 @@ function useLocalWorker({
54
55
  rules,
55
56
  enableLocalPersistence,
56
57
  ip,
58
+ localProtocol,
57
59
  crons,
58
60
  }: LocalProps) {
59
61
  // TODO: pass vars via command line
@@ -87,6 +89,11 @@ function useLocalWorker({
87
89
  '⎔ A "public" folder is not yet supported in local mode.'
88
90
  );
89
91
  }
92
+ if (bindings.services && bindings.services.length > 0) {
93
+ throw new Error(
94
+ "⎔ Service bindings are not yet supported in local mode."
95
+ );
96
+ }
90
97
 
91
98
  // In local mode, we want to copy all referenced modules into
92
99
  // the output bundle directory before starting up
@@ -159,6 +166,7 @@ function useLocalWorker({
159
166
  name: workerName,
160
167
  port,
161
168
  scriptPath,
169
+ https: localProtocol === "https",
162
170
  host: ip,
163
171
  modules: format === "modules",
164
172
  modulesRules: (rules || [])
@@ -293,10 +301,12 @@ function useLocalWorker({
293
301
  workerName,
294
302
  format,
295
303
  port,
304
+ localProtocol,
296
305
  ip,
297
306
  bindings.durable_objects?.bindings,
298
307
  bindings.kv_namespaces,
299
308
  bindings.vars,
309
+ bindings.services,
300
310
  compatibilityDate,
301
311
  compatibilityFlags,
302
312
  localPersistencePath,
@@ -1,4 +1,3 @@
1
- import assert from "node:assert";
2
1
  import { readFile } from "node:fs/promises";
3
2
  import path from "node:path";
4
3
  import { useState, useEffect, useRef } from "react";
@@ -7,6 +6,7 @@ import useInspector from "../inspect";
7
6
  import { logger } from "../logger";
8
7
  import { usePreviewServer } from "../proxy";
9
8
  import { syncAssets } from "../sites";
9
+ import { requireApiToken, requireAuth } from "../user";
10
10
  import type { CfPreviewToken } from "../create-worker-preview";
11
11
  import type { AssetPaths } from "../sites";
12
12
  import type { CfModule, CfWorkerInit, CfScriptFormat } from "../worker";
@@ -23,24 +23,21 @@ export function Remote(props: {
23
23
  localProtocol: "https" | "http";
24
24
  inspectorPort: number;
25
25
  accountId: string | undefined;
26
- apiToken: string | undefined;
27
26
  bindings: CfWorkerInit["bindings"];
28
27
  compatibilityDate: string;
29
28
  compatibilityFlags: string[] | undefined;
30
29
  usageModel: "bundled" | "unbound" | undefined;
31
30
  env: string | undefined;
32
31
  legacyEnv: boolean | undefined;
33
- zone: { id: string; host: string } | undefined;
32
+ zone: string | undefined;
33
+ host: string | undefined;
34
34
  }) {
35
- assert(props.accountId, "accountId is required");
36
- assert(props.apiToken, "apiToken is required");
37
35
  const previewToken = useWorker({
38
36
  name: props.name,
39
37
  bundle: props.bundle,
40
38
  format: props.format,
41
39
  modules: props.bundle ? props.bundle.modules : [],
42
40
  accountId: props.accountId,
43
- apiToken: props.apiToken,
44
41
  bindings: props.bindings,
45
42
  assetPaths: props.assetPaths,
46
43
  port: props.port,
@@ -50,6 +47,7 @@ export function Remote(props: {
50
47
  env: props.env,
51
48
  legacyEnv: props.legacyEnv,
52
49
  zone: props.zone,
50
+ host: props.host,
53
51
  });
54
52
 
55
53
  usePreviewServer({
@@ -73,8 +71,7 @@ export function useWorker(props: {
73
71
  bundle: EsbuildBundle | undefined;
74
72
  format: CfScriptFormat | undefined;
75
73
  modules: CfModule[];
76
- accountId: string;
77
- apiToken: string;
74
+ accountId: string | undefined;
78
75
  bindings: CfWorkerInit["bindings"];
79
76
  assetPaths: AssetPaths | undefined;
80
77
  port: number;
@@ -83,7 +80,8 @@ export function useWorker(props: {
83
80
  usageModel: "bundled" | "unbound" | undefined;
84
81
  env: string | undefined;
85
82
  legacyEnv: boolean | undefined;
86
- zone: { id: string; host: string } | undefined;
83
+ zone: string | undefined;
84
+ host: string | undefined;
87
85
  }): CfPreviewToken | undefined {
88
86
  const {
89
87
  name,
@@ -91,7 +89,6 @@ export function useWorker(props: {
91
89
  format,
92
90
  modules,
93
91
  accountId,
94
- apiToken,
95
92
  bindings,
96
93
  assetPaths,
97
94
  compatibilityDate,
@@ -105,6 +102,9 @@ export function useWorker(props: {
105
102
  // something's "happened" in our system; We make a ref and
106
103
  // mark it once we log our initial message. Refs are vars!
107
104
  const startedRef = useRef(false);
105
+ // This ref holds the actual accountId being used, the `accountId` prop could be undefined
106
+ // as it is only what is retrieved from the wrangler.toml config.
107
+ const accountIdRef = useRef(accountId);
108
108
 
109
109
  useEffect(() => {
110
110
  const abortController = new AbortController();
@@ -119,8 +119,13 @@ export function useWorker(props: {
119
119
  logger.log("⎔ Detected changes, restarted server.");
120
120
  }
121
121
 
122
+ // Ensure we have an account id, even if it means logging in here.
123
+ accountIdRef.current = await requireAuth({
124
+ account_id: accountIdRef.current,
125
+ });
126
+
122
127
  const assets = await syncAssets(
123
- accountId,
128
+ accountIdRef.current,
124
129
  // When we're using the newer service environments, we wouldn't
125
130
  // have added the env name on to the script name. However, we must
126
131
  // include it in the kv namespace name regardless (since there's no
@@ -173,10 +178,15 @@ export function useWorker(props: {
173
178
  await createWorkerPreview(
174
179
  init,
175
180
  {
176
- accountId,
177
- apiToken,
181
+ accountId: accountIdRef.current,
182
+ apiToken: requireApiToken(),
183
+ },
184
+ {
185
+ env: props.env,
186
+ legacyEnv: props.legacyEnv,
187
+ zone: props.zone,
188
+ host: props.host,
178
189
  },
179
- { env: props.env, legacyEnv: props.legacyEnv, zone: props.zone },
180
190
  abortController.signal
181
191
  )
182
192
  );
@@ -185,7 +195,21 @@ export function useWorker(props: {
185
195
  // we want to log the error, but not end the process
186
196
  // since it could recover after the developer fixes whatever's wrong
187
197
  if ((err as { code: string }).code !== "ABORT_ERR") {
188
- logger.error("Error on remote worker:", err);
198
+ // instead of logging the raw API error to the user,
199
+ // give them friendly instructions
200
+ // for error 10063 (workers.dev subdomain required)
201
+ if (err.code === 10063) {
202
+ const errorMessage =
203
+ "Error: You need to register a workers.dev subdomain before running the dev command in remote mode";
204
+ const solutionMessage =
205
+ "You can either enable local mode by pressing l, or register a workers.dev subdomain here:";
206
+ const onboardingLink = `https://dash.cloudflare.com/${accountIdRef.current}/workers/onboarding`;
207
+ logger.error(
208
+ `${errorMessage}\n${solutionMessage}\n${onboardingLink}`
209
+ );
210
+ } else {
211
+ logger.error("Error on remote worker:", err);
212
+ }
189
213
  }
190
214
  });
191
215
 
@@ -197,7 +221,6 @@ export function useWorker(props: {
197
221
  bundle,
198
222
  format,
199
223
  accountId,
200
- apiToken,
201
224
  port,
202
225
  assetPaths,
203
226
  compatibilityDate,
@@ -208,6 +231,7 @@ export function useWorker(props: {
208
231
  props.env,
209
232
  props.legacyEnv,
210
233
  props.zone,
234
+ props.host,
211
235
  ]);
212
236
  return token;
213
237
  }
package/src/dialogs.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import { Box, Text, useInput, render } from "ink";
3
+ import SelectInput from "ink-select-input";
3
4
  import TextInput from "ink-text-input";
4
5
  import * as React from "react";
5
6
  import { useState } from "react";
@@ -85,3 +86,50 @@ export async function prompt(
85
86
  );
86
87
  });
87
88
  }
89
+
90
+ type SelectOption = {
91
+ value: string;
92
+ label: string;
93
+ };
94
+
95
+ type SelectProps = {
96
+ text: string;
97
+ options: SelectOption[];
98
+ initialIndex: number;
99
+ onSelect: (value: string) => void;
100
+ };
101
+
102
+ function Select(props: SelectProps) {
103
+ return (
104
+ <Box flexDirection="column">
105
+ <Text>{props.text}</Text>
106
+ <SelectInput
107
+ initialIndex={props.initialIndex}
108
+ items={props.options}
109
+ onSelect={async (selected) => {
110
+ props.onSelect(selected.value);
111
+ }}
112
+ />
113
+ </Box>
114
+ );
115
+ }
116
+
117
+ export function select(
118
+ text: string,
119
+ options: SelectOption[],
120
+ initialIndex: number
121
+ ): Promise<string> {
122
+ return new Promise((resolve) => {
123
+ const { unmount } = render(
124
+ <Select
125
+ text={text}
126
+ options={options}
127
+ initialIndex={initialIndex}
128
+ onSelect={(option: string) => {
129
+ unmount();
130
+ resolve(option);
131
+ }}
132
+ />
133
+ );
134
+ });
135
+ }
package/src/durable.ts ADDED
@@ -0,0 +1,102 @@
1
+ import assert from "node:assert";
2
+ import { fetchResult } from "./cfetch";
3
+ import { logger } from "./logger";
4
+ import type { Config } from "./config";
5
+ import type { CfWorkerInit } from "./worker";
6
+
7
+ /**
8
+ * For a given Worker + migrations config, figure out which migrations
9
+ * to upload based on the current migration tag of the deployed Worker.
10
+ */
11
+ export async function getMigrationsToUpload(
12
+ scriptName: string,
13
+ props: {
14
+ accountId: string | undefined;
15
+ config: Config;
16
+ legacyEnv: boolean | undefined;
17
+ env: string | undefined;
18
+ }
19
+ ): Promise<CfWorkerInit["migrations"]> {
20
+ const { config, accountId } = props;
21
+
22
+ assert(accountId, "Missing accountId");
23
+ // if config.migrations
24
+ let migrations;
25
+ if (config.migrations.length > 0) {
26
+ // get current migration tag
27
+ type ScriptData = { id: string; migration_tag?: string };
28
+ let script: ScriptData | undefined;
29
+ if (!props.legacyEnv) {
30
+ try {
31
+ if (props.env) {
32
+ const scriptData = await fetchResult<{
33
+ script: ScriptData;
34
+ }>(
35
+ `/accounts/${accountId}/workers/services/${scriptName}/environments/${props.env}`
36
+ );
37
+ script = scriptData.script;
38
+ } else {
39
+ const scriptData = await fetchResult<{
40
+ default_environment: {
41
+ script: ScriptData;
42
+ };
43
+ }>(`/accounts/${accountId}/workers/services/${scriptName}`);
44
+ script = scriptData.default_environment.script;
45
+ }
46
+ } catch (err) {
47
+ if (
48
+ ![
49
+ 10090, // corresponds to workers.api.error.service_not_found, so the script wasn't previously published at all
50
+ 10092, // workers.api.error.environment_not_found, so the script wasn't published to this environment yet
51
+ ].includes((err as { code: number }).code)
52
+ ) {
53
+ throw err;
54
+ }
55
+ // else it's a 404, no script found, and we can proceed
56
+ }
57
+ } else {
58
+ const scripts = await fetchResult<ScriptData[]>(
59
+ `/accounts/${accountId}/workers/scripts`
60
+ );
61
+ script = scripts.find(({ id }) => id === scriptName);
62
+ }
63
+
64
+ if (script?.migration_tag) {
65
+ // was already published once
66
+ const scriptMigrationTag = script.migration_tag;
67
+ const foundIndex = config.migrations.findIndex(
68
+ (migration) => migration.tag === scriptMigrationTag
69
+ );
70
+ if (foundIndex === -1) {
71
+ logger.warn(
72
+ `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...`
73
+ );
74
+ migrations = {
75
+ old_tag: script.migration_tag,
76
+ new_tag: config.migrations[config.migrations.length - 1].tag,
77
+ steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
78
+ };
79
+ } else {
80
+ if (foundIndex !== config.migrations.length - 1) {
81
+ // there are new migrations to send up
82
+ migrations = {
83
+ old_tag: script.migration_tag,
84
+ new_tag: config.migrations[config.migrations.length - 1].tag,
85
+ steps: config.migrations
86
+ .slice(foundIndex + 1)
87
+ .map(({ tag: _tag, ...rest }) => rest),
88
+ };
89
+ }
90
+ // else, we're up to date, no migrations to send
91
+ }
92
+ } else {
93
+ // first time publishing durable objects to this script,
94
+ // so we send all the migrations
95
+ migrations = {
96
+ new_tag: config.migrations[config.migrations.length - 1].tag,
97
+ steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
98
+ };
99
+ }
100
+ }
101
+ return migrations;
102
+ }
package/src/entry.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert";
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import * as esbuild from "esbuild";
5
5
  import { execaCommand } from "execa";
@@ -45,11 +45,16 @@ export async function getEntry(
45
45
  file = path.resolve(directory, config.main);
46
46
  }
47
47
 
48
- await runCustomBuild(file, config.build);
48
+ const relativeFile = path.relative(directory, file) || ".";
49
+ await runCustomBuild(file, relativeFile, config.build);
49
50
 
50
51
  if (fileExists(file) === false) {
51
52
  throw new Error(
52
- `Could not resolve "${path.relative(process.cwd(), file)}".`
53
+ getMissingEntryPointMessage(
54
+ `The entry-point file at "${relativeFile}" was not found.`,
55
+ file,
56
+ relativeFile
57
+ )
53
58
  );
54
59
  }
55
60
  const format = await guessWorkerFormat(
@@ -90,7 +95,8 @@ export async function getEntry(
90
95
  }
91
96
 
92
97
  export async function runCustomBuild(
93
- expectedEntry: string,
98
+ expectedEntryAbsolute: string,
99
+ expectedEntryRelative: string,
94
100
  build: Config["build"]
95
101
  ) {
96
102
  if (build?.command) {
@@ -105,12 +111,14 @@ export async function runCustomBuild(
105
111
  ...(build.cwd && { cwd: build.cwd }),
106
112
  });
107
113
 
108
- if (fileExists(expectedEntry) === false) {
114
+ if (fileExists(expectedEntryAbsolute) === false) {
109
115
  throw new Error(
110
- `Could not resolve "${path.relative(
111
- process.cwd(),
112
- expectedEntry
113
- )}" after running custom build: ${build.command}`
116
+ getMissingEntryPointMessage(
117
+ `The expected output file at "${expectedEntryRelative}" was not found after running custom build: ${build.command}.\n` +
118
+ "The `main` property in wrangler.toml should point to the file generated by the custom build.",
119
+ expectedEntryAbsolute,
120
+ expectedEntryRelative
121
+ )
114
122
  );
115
123
  }
116
124
  }
@@ -265,3 +273,50 @@ function generateAddScriptNameExamples(
265
273
  })
266
274
  .join("\n");
267
275
  }
276
+
277
+ /**
278
+ * Generate an appropriate message for when the entry-point is missing.
279
+ *
280
+ * To be more helpful to developers, we check whether there is a suitable file
281
+ * nearby to the expected file path.
282
+ */
283
+ function getMissingEntryPointMessage(
284
+ message: string,
285
+ absoluteEntryPointPath: string,
286
+ relativeEntryPointPath: string
287
+ ): string {
288
+ if (
289
+ existsSync(absoluteEntryPointPath) &&
290
+ statSync(absoluteEntryPointPath).isDirectory()
291
+ ) {
292
+ // The expected entry-point is a directory, so offer further guidance.
293
+ message += `\nThe provided entry-point path, "${relativeEntryPointPath}", points to a directory, rather than a file.\n`;
294
+
295
+ // Perhaps we can even guess what the correct path should be...
296
+ const possiblePaths: string[] = [];
297
+ for (const basenamePath of [
298
+ "worker",
299
+ "dist/worker",
300
+ "index",
301
+ "dist/index",
302
+ ]) {
303
+ for (const extension of [".ts", ".tsx", ".js", ".jsx"]) {
304
+ const filePath = basenamePath + extension;
305
+ if (fileExists(path.resolve(absoluteEntryPointPath, filePath))) {
306
+ possiblePaths.push(path.join(relativeEntryPointPath, filePath));
307
+ }
308
+ }
309
+ }
310
+
311
+ if (possiblePaths.length > 0) {
312
+ message +=
313
+ `\nDid you mean to set the main field to${
314
+ possiblePaths.length > 1 ? " one of" : ""
315
+ }:\n` +
316
+ "```\n" +
317
+ possiblePaths.map((filePath) => `main = "./${filePath}"\n`).join("") +
318
+ "```";
319
+ }
320
+ }
321
+ return message;
322
+ }