wrangler 2.13.0 → 2.14.0

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.
@@ -1,6 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import chalk from "chalk";
4
5
  import globToRegExp from "glob-to-regexp";
5
6
  import { logger } from "./logger";
6
7
  import type { Config, ConfigModuleRuleType } from "./config";
@@ -38,28 +39,8 @@ export const DEFAULT_MODULE_RULES: Config["rules"] = [
38
39
  { type: "CompiledWasm", globs: ["**/*.wasm"] },
39
40
  ];
40
41
 
41
- export default function createModuleCollector(props: {
42
- format: CfScriptFormat;
43
- rules?: Config["rules"];
44
- // a collection of "legacy" style module references, which are just file names
45
- // we will eventually deprecate this functionality, hence the verbose greppable name
46
- wrangler1xlegacyModuleReferences: {
47
- rootDirectory: string;
48
- fileNames: Set<string>;
49
- };
50
- }): {
51
- modules: CfModule[];
52
- plugin: esbuild.Plugin;
53
- } {
54
- const rules: Config["rules"] = [
55
- ...(props.rules || []),
56
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
57
- ...DEFAULT_MODULE_RULES!,
58
- ];
59
-
60
- // First, we want to add some validations to the module rules
61
- // We want to warn if rules are accidentally configured in such a way that
62
- // subsequent rules will never match because `fallthrough` hasn't been set
42
+ export function parseRules(userRules: Config["rules"] = []) {
43
+ const rules: Config["rules"] = [...userRules, ...DEFAULT_MODULE_RULES];
63
44
 
64
45
  const completedRuleLocations: Record<string, number> = {};
65
46
  let index = 0;
@@ -67,7 +48,7 @@ export default function createModuleCollector(props: {
67
48
  for (const rule of rules) {
68
49
  if (rule.type in completedRuleLocations) {
69
50
  if (rules[completedRuleLocations[rule.type]].fallthrough !== false) {
70
- if (index < (props.rules || []).length) {
51
+ if (index < userRules.length) {
71
52
  logger.warn(
72
53
  `The module rule at position ${index} (${JSON.stringify(
73
54
  rule
@@ -101,6 +82,89 @@ export default function createModuleCollector(props: {
101
82
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
102
83
  rulesToRemove.forEach((rule) => rules!.splice(rules!.indexOf(rule), 1));
103
84
 
85
+ return { rules, removedRules: rulesToRemove };
86
+ }
87
+
88
+ export async function matchFiles(
89
+ files: string[],
90
+ relativeTo: string,
91
+ {
92
+ rules,
93
+ removedRules,
94
+ }: { rules: Config["rules"]; removedRules: Config["rules"] }
95
+ ) {
96
+ const modules: CfModule[] = [];
97
+
98
+ // Deduplicate modules. This is usually a poorly specified `wrangler.toml` configuration, but duplicate modules will cause a crash at runtime
99
+ const moduleNames = new Set<string>();
100
+ for (const rule of rules) {
101
+ for (const glob of rule.globs) {
102
+ const regexp = globToRegExp(glob, {
103
+ globstar: true,
104
+ });
105
+ const newModules = await Promise.all(
106
+ files
107
+ .filter((f) => regexp.test(f))
108
+ .map(async (name) => {
109
+ const filePath = name;
110
+ const fileContent = await readFile(path.join(relativeTo, filePath));
111
+
112
+ return {
113
+ name: filePath,
114
+ content: fileContent,
115
+ type: RuleTypeToModuleType[rule.type],
116
+ };
117
+ })
118
+ );
119
+ for (const module of newModules) {
120
+ if (!moduleNames.has(module.name)) {
121
+ moduleNames.add(module.name);
122
+ modules.push(module);
123
+ } else {
124
+ logger.warn(
125
+ `Ignoring duplicate module: ${chalk.blue(
126
+ module.name
127
+ )} (${chalk.green(module.type ?? "")})`
128
+ );
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ // This is just a sanity check verifying that no files match rules that were removed
135
+ for (const rule of removedRules) {
136
+ for (const glob of rule.globs) {
137
+ const regexp = globToRegExp(glob);
138
+ for (const file of files) {
139
+ if (regexp.test(file)) {
140
+ throw new Error(
141
+ `The file ${file} matched a module rule in your configuration (${JSON.stringify(
142
+ rule
143
+ )}), but was ignored because a previous rule with the same type was not marked as \`fallthrough = true\`.`
144
+ );
145
+ }
146
+ }
147
+ }
148
+ }
149
+ return modules;
150
+ }
151
+
152
+ export default function createModuleCollector(props: {
153
+ format: CfScriptFormat;
154
+ rules?: Config["rules"];
155
+ // a collection of "legacy" style module references, which are just file names
156
+ // we will eventually deprecate this functionality, hence the verbose greppable name
157
+ wrangler1xlegacyModuleReferences: {
158
+ rootDirectory: string;
159
+ fileNames: Set<string>;
160
+ };
161
+ preserveFileNames?: boolean;
162
+ }): {
163
+ modules: CfModule[];
164
+ plugin: esbuild.Plugin;
165
+ } {
166
+ const { rules, removedRules } = parseRules(props.rules);
167
+
104
168
  const modules: CfModule[] = [];
105
169
  return {
106
170
  modules,
@@ -206,7 +270,9 @@ export default function createModuleCollector(props: {
206
270
  .createHash("sha1")
207
271
  .update(fileContent)
208
272
  .digest("hex");
209
- const fileName = `./${fileHash}-${path.basename(args.path)}`;
273
+ const fileName = props.preserveFileNames
274
+ ? filePath
275
+ : `./${fileHash}-${path.basename(args.path)}`;
210
276
 
211
277
  // add the module to the array
212
278
  modules.push({
@@ -245,7 +311,7 @@ export default function createModuleCollector(props: {
245
311
  });
246
312
  });
247
313
 
248
- rulesToRemove.forEach((rule) => {
314
+ removedRules.forEach((rule) => {
249
315
  rule.globs.forEach((glob) => {
250
316
  build.onResolve(
251
317
  { filter: globToRegExp(glob) },
@@ -28,6 +28,7 @@ export function buildPlugin({
28
28
  file: resolve(getBasePath(), "templates/pages-template-plugin.ts"),
29
29
  directory: functionsDirectory,
30
30
  format: "modules",
31
+ moduleRoot: functionsDirectory,
31
32
  },
32
33
  resolve(outdir),
33
34
  {
@@ -48,6 +48,7 @@ export function buildWorker({
48
48
  file: resolve(getBasePath(), "templates/pages-template-worker.ts"),
49
49
  directory: functionsDirectory,
50
50
  format: "modules",
51
+ moduleRoot: functionsDirectory,
51
52
  },
52
53
  outdir ? resolve(outdir) : resolve(outfile),
53
54
  {
@@ -58,10 +59,6 @@ export function buildWorker({
58
59
  watch,
59
60
  legacyNodeCompat,
60
61
  nodejsCompat,
61
- loader: {
62
- ".txt": "text",
63
- ".html": "text",
64
- },
65
62
  define: {
66
63
  __FALLBACK_SERVICE__: JSON.stringify(fallbackService),
67
64
  },
@@ -202,6 +199,7 @@ export function buildRawWorker({
202
199
  file: workerScriptPath,
203
200
  directory: resolve(directory),
204
201
  format: "modules",
202
+ moduleRoot: resolve(directory),
205
203
  },
206
204
  outdir ? resolve(outdir) : resolve(outfile),
207
205
  {
@@ -210,10 +208,6 @@ export function buildRawWorker({
210
208
  watch,
211
209
  legacyNodeCompat,
212
210
  nodejsCompat,
213
- loader: {
214
- ".txt": "text",
215
- ".html": "text",
216
- },
217
211
  define: {},
218
212
  betaD1Shims: (betaD1Shims || []).map(
219
213
  (binding) => `${D1_BETA_PREFIX}${binding}`
@@ -21,6 +21,7 @@ import { ParseError } from "../parse";
21
21
  import { getQueue, putConsumer } from "../queues/client";
22
22
  import { getWorkersDevSubdomain } from "../routes";
23
23
  import { syncAssets } from "../sites";
24
+ import traverseModuleGraph from "../traverse-module-graph";
24
25
  import { identifyD1BindingsAsBeta } from "../worker";
25
26
  import { getZoneForRoute } from "../zones";
26
27
  import type { FetchError } from "../cfetch";
@@ -463,14 +464,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
463
464
  resolvedEntryPointPath,
464
465
  bundleType,
465
466
  }: Awaited<ReturnType<typeof bundleWorker>> = props.noBundle
466
- ? // we can skip the whole bundling step and mock a bundle here
467
- {
468
- modules: [],
469
- dependencies: {},
470
- resolvedEntryPointPath: props.entry.file,
471
- bundleType: props.entry.format === "modules" ? "esm" : "commonjs",
472
- stop: undefined,
473
- }
467
+ ? await traverseModuleGraph(props.entry, props.rules)
474
468
  : await bundleWorker(
475
469
  props.entry,
476
470
  typeof destination === "string" ? destination : destination.path,
@@ -541,6 +535,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
541
535
  ? { binding: "__STATIC_CONTENT", id: assets.namespace }
542
536
  : []
543
537
  ),
538
+ send_email: config.send_email,
544
539
  vars: { ...config.vars, ...props.vars },
545
540
  wasm_modules: config.wasm_modules,
546
541
  text_blobs: {
@@ -100,6 +100,7 @@ export const secret = (secretYargs: CommonYargsArgv) => {
100
100
  },
101
101
  bindings: {
102
102
  kv_namespaces: [],
103
+ send_email: [],
103
104
  vars: {},
104
105
  durable_objects: { bindings: [] },
105
106
  queues: [],
@@ -0,0 +1,53 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import chalk from "chalk";
4
+ import { logger } from "./logger";
5
+ import { matchFiles, parseRules } from "./module-collection";
6
+ import type { BundleResult } from "./bundle";
7
+ import type { Config } from "./config";
8
+ import type { Entry } from "./entry";
9
+
10
+ async function getFiles(root: string, relativeTo: string): Promise<string[]> {
11
+ const files = [];
12
+ for (const file of await readdir(root, { withFileTypes: true })) {
13
+ if (file.isDirectory()) {
14
+ files.push(...(await getFiles(path.join(root, file.name), relativeTo)));
15
+ } else {
16
+ files.push(path.relative(relativeTo, path.join(root, file.name)));
17
+ }
18
+ }
19
+ return files;
20
+ }
21
+
22
+ export default async function traverseModuleGraph(
23
+ entry: Entry,
24
+ rules: Config["rules"]
25
+ ): Promise<BundleResult> {
26
+ const files = await getFiles(entry.moduleRoot, entry.moduleRoot);
27
+ const relativeEntryPoint = path.relative(entry.moduleRoot, entry.file);
28
+
29
+ const modules = (await matchFiles(files, entry.moduleRoot, parseRules(rules)))
30
+ .filter((m) => m.name !== relativeEntryPoint)
31
+ .map((m) => ({
32
+ ...m,
33
+ name: m.name,
34
+ }));
35
+
36
+ const bundleType = entry.format === "modules" ? "esm" : "commonjs";
37
+
38
+ if (modules.length > 0) {
39
+ logger.info(`Uploading additional modules:`);
40
+ modules.forEach(({ name, type }) => {
41
+ logger.info(`- ${chalk.blue(name)} (${chalk.green(type ?? "")})`);
42
+ });
43
+ }
44
+
45
+ return {
46
+ modules,
47
+ dependencies: {},
48
+ resolvedEntryPointPath: entry.file,
49
+ bundleType,
50
+ stop: undefined,
51
+ sourceMapPath: undefined,
52
+ };
53
+ }
package/src/worker.ts CHANGED
@@ -79,6 +79,15 @@ export interface CfKvNamespace {
79
79
  id: string;
80
80
  }
81
81
 
82
+ /**
83
+ * A binding to send email.
84
+ */
85
+ export interface CfSendEmailBindings {
86
+ name: string;
87
+ destination_address?: string;
88
+ allowed_destination_addresses?: string[];
89
+ }
90
+
82
91
  /**
83
92
  * A binding to a wasm module (in service-worker format)
84
93
  */
@@ -216,6 +225,7 @@ export interface CfWorkerInit {
216
225
  bindings: {
217
226
  vars: CfVars | undefined;
218
227
  kv_namespaces: CfKvNamespace[] | undefined;
228
+ send_email: CfSendEmailBindings[] | undefined;
219
229
  wasm_modules: CfWasmModuleBindings | undefined;
220
230
  text_blobs: CfTextBlobBindings | undefined;
221
231
  data_blobs: CfDataBlobBindings | undefined;
@@ -532,6 +532,11 @@ declare interface EnvironmentInheritable {
532
532
  * The entrypoint/path to the JavaScript file that will be executed.
533
533
  */
534
534
  main: string | undefined;
535
+ /**
536
+ * The directory in which module rules should be evaluated in a `--no-bundle` worker
537
+ * This defaults to dirname(main) when left undefined
538
+ */
539
+ base_dir: string | undefined;
535
540
  /**
536
541
  * Whether we use <name>.<subdomain>.workers.dev to
537
542
  * test and deploy your worker.
@@ -770,6 +775,23 @@ declare interface EnvironmentNonInheritable {
770
775
  /** The ID of the KV namespace used during `wrangler dev` */
771
776
  preview_id?: string;
772
777
  }[];
778
+ /**
779
+ * These specify bindings to send email from inside your Worker.
780
+ *
781
+ * NOTE: This field is not automatically inherited from the top level environment,
782
+ * and so must be specified in every named environment.
783
+ *
784
+ * @default `[]`
785
+ * @nonInheritable
786
+ */
787
+ send_email: {
788
+ /** The binding name used to refer to the this binding */
789
+ name: string;
790
+ /** If this binding should be restricted to a specific verified address */
791
+ destination_address?: string;
792
+ /** If this binding should be restricted to a set of verified addresses */
793
+ allowed_destination_addresses?: string[];
794
+ }[];
773
795
  /**
774
796
  * Specifies Queues that are bound to this Worker environment.
775
797
  *