wrangler 0.0.0-e22f9d7 → 0.0.0-e2a26a0

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 (54) hide show
  1. package/bin/wrangler.js +2 -2
  2. package/package.json +15 -10
  3. package/pages/functions/buildWorker.ts +1 -1
  4. package/pages/functions/filepath-routing.test.ts +21 -13
  5. package/pages/functions/filepath-routing.ts +38 -28
  6. package/pages/functions/routes.ts +8 -6
  7. package/pages/functions/template-worker.ts +16 -24
  8. package/src/__tests__/dev.test.tsx +7 -3
  9. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  10. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  11. package/src/__tests__/helpers/mock-account-id.ts +30 -0
  12. package/src/__tests__/helpers/mock-bin.ts +33 -0
  13. package/src/__tests__/{mock-cfetch.ts → helpers/mock-cfetch.ts} +49 -13
  14. package/src/__tests__/helpers/mock-console.ts +56 -0
  15. package/src/__tests__/{mock-dialogs.ts → helpers/mock-dialogs.ts} +1 -1
  16. package/src/__tests__/helpers/mock-kv.ts +40 -0
  17. package/src/__tests__/helpers/mock-user.ts +27 -0
  18. package/src/__tests__/helpers/run-in-tmp.ts +29 -0
  19. package/src/__tests__/helpers/run-wrangler.ts +14 -0
  20. package/src/__tests__/index.test.ts +312 -56
  21. package/src/__tests__/jest.setup.ts +17 -2
  22. package/src/__tests__/kv.test.ts +344 -316
  23. package/src/__tests__/logout.test.ts +50 -0
  24. package/src/__tests__/package-manager.test.ts +206 -0
  25. package/src/__tests__/publish.test.ts +1222 -0
  26. package/src/__tests__/secret.test.ts +210 -0
  27. package/src/__tests__/whoami.test.tsx +86 -0
  28. package/src/api/form_data.ts +10 -7
  29. package/src/api/preview.ts +2 -2
  30. package/src/api/worker.ts +3 -3
  31. package/src/cfetch/index.ts +14 -14
  32. package/src/cfetch/internal.ts +40 -8
  33. package/src/config.ts +162 -8
  34. package/src/dev.tsx +103 -53
  35. package/src/dialogs.tsx +2 -2
  36. package/src/index.tsx +438 -311
  37. package/src/inspect.ts +80 -23
  38. package/src/kv.tsx +9 -1
  39. package/src/module-collection.ts +5 -6
  40. package/src/package-manager.ts +120 -0
  41. package/src/pages.tsx +41 -26
  42. package/src/paths.ts +26 -0
  43. package/src/proxy.ts +78 -10
  44. package/src/publish.ts +98 -52
  45. package/src/sites.tsx +175 -53
  46. package/src/user.tsx +51 -59
  47. package/src/whoami.tsx +69 -0
  48. package/templates/new-worker.js +15 -0
  49. package/templates/new-worker.ts +15 -0
  50. package/{static-asset-facade.js → templates/static-asset-facade.js} +0 -0
  51. package/wrangler-dist/cli.js +111479 -110187
  52. package/wrangler-dist/cli.js.map +3 -3
  53. package/src/__tests__/run-in-tmp.ts +0 -19
  54. package/src/__tests__/run-wrangler.ts +0 -32
package/src/publish.ts CHANGED
@@ -1,33 +1,36 @@
1
1
  import assert from "node:assert";
2
- import path from "node:path";
3
2
  import { readFile } from "node:fs/promises";
4
- import esbuild from "esbuild";
5
- import { execa } from "execa";
3
+ import path from "node:path";
4
+ import { URLSearchParams } from "node:url";
5
+ import * as esbuild from "esbuild";
6
+ import { execaCommand } from "execa";
6
7
  import tmp from "tmp-promise";
7
- import type { CfWorkerInit } from "./api/worker";
8
8
  import { toFormData } from "./api/form_data";
9
9
  import { fetchResult } from "./cfetch";
10
- import type { Config } from "./config";
11
10
  import makeModuleCollector from "./module-collection";
12
11
  import { syncAssets } from "./sites";
12
+ import type { CfWorkerInit } from "./api/worker";
13
+ import type { Config } from "./config";
14
+ import type { AssetPaths } from "./sites";
15
+ import type { Metafile } from "esbuild";
13
16
 
14
17
  type CfScriptFormat = undefined | "modules" | "service-worker";
15
18
 
16
19
  type Props = {
17
20
  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;
21
+ format: CfScriptFormat | undefined;
22
+ script: string | undefined;
23
+ name: string | undefined;
24
+ env: string | undefined;
25
+ compatibilityDate: string | undefined;
26
+ compatibilityFlags: string[] | undefined;
27
+ assetPaths: AssetPaths | undefined;
28
+ triggers: (string | number)[] | undefined;
29
+ routes: (string | number)[] | undefined;
30
+ legacyEnv: boolean | undefined;
29
31
  jsxFactory: undefined | string;
30
32
  jsxFragment: undefined | string;
33
+ experimentalPublic: boolean;
31
34
  };
32
35
 
33
36
  function sleep(ms: number) {
@@ -35,10 +38,10 @@ function sleep(ms: number) {
35
38
  }
36
39
 
37
40
  export default async function publish(props: Props): Promise<void> {
38
- if (props.public && props.format === "service-worker") {
41
+ if (props.experimentalPublic && props.format === "service-worker") {
39
42
  // TODO: check config too
40
43
  throw new Error(
41
- "You cannot use the service worker format with a public directory."
44
+ "You cannot publish in the service worker format with a public directory."
42
45
  );
43
46
  }
44
47
  // TODO: warn if git/hg has uncommitted changes
@@ -76,12 +79,31 @@ export default async function publish(props: Props): Promise<void> {
76
79
  '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
80
  );
78
81
 
82
+ if (config.site?.["entry-point"]) {
83
+ console.warn(
84
+ "Deprecation notice: The `site.entry-point` config field is no longer used.\n" +
85
+ "The entry-point should be specified via the command line (e.g. `wrangler publish path/to/script`) or the `build.upload.main` config field.\n" +
86
+ "Please remove the `site.entry-point` field from the `wrangler.toml` file."
87
+ );
88
+ }
89
+
90
+ assert(
91
+ !config.site || config.site.bucket,
92
+ "A [site] definition requires a `bucket` field with a path to the site's public directory."
93
+ );
94
+
79
95
  let file: string;
80
96
  if (props.script) {
81
- file = props.script;
97
+ // If the script name comes from the command line it is relative to the current working directory.
98
+ file = path.resolve(props.script);
82
99
  } else {
83
- assert(build?.upload?.main, "missing main file");
84
- file = path.join(path.dirname(__path__), build.upload.main);
100
+ // If the script name comes from the config, then it is relative to the wrangler.toml file.
101
+ if (build?.upload?.main === undefined) {
102
+ throw new Error(
103
+ "Missing entry-point: The entry-point should be specified via the command line (e.g. `wrangler publish path/to/script`) or the `build.upload.main` config field."
104
+ );
105
+ }
106
+ file = path.resolve(path.dirname(__path__), build.upload.main);
85
107
  }
86
108
 
87
109
  if (props.legacyEnv) {
@@ -90,12 +112,11 @@ export default async function publish(props: Props): Promise<void> {
90
112
  const envName = props.env ?? "production";
91
113
 
92
114
  const destination = await tmp.dir({ unsafeCleanup: true });
93
-
94
115
  if (props.config.build?.command) {
95
116
  // TODO: add a deprecation message here?
96
117
  console.log("running:", props.config.build.command);
97
- const buildCommandPieces = props.config.build.command.split(" ");
98
- await execa(buildCommandPieces[0], buildCommandPieces.slice(1), {
118
+ await execaCommand(props.config.build.command, {
119
+ shell: true,
99
120
  stdout: "inherit",
100
121
  stderr: "inherit",
101
122
  ...(props.config.build?.cwd && { cwd: props.config.build.cwd }),
@@ -104,22 +125,22 @@ export default async function publish(props: Props): Promise<void> {
104
125
 
105
126
  const moduleCollector = makeModuleCollector();
106
127
  const result = await esbuild.build({
107
- ...(props.public
128
+ ...(props.experimentalPublic
108
129
  ? {
109
130
  stdin: {
110
131
  contents: (
111
132
  await readFile(
112
- path.join(__dirname, "../static-asset-facade.js"),
133
+ path.join(__dirname, "../templates/static-asset-facade.js"),
113
134
  "utf8"
114
135
  )
115
- ).replace("__ENTRY_POINT__", path.join(process.cwd(), file)),
136
+ ).replace("__ENTRY_POINT__", file),
116
137
  sourcefile: "static-asset-facade.js",
117
138
  resolveDir: path.dirname(file),
118
139
  },
140
+ nodePaths: [path.join(__dirname, "../vendor")],
119
141
  }
120
142
  : { entryPoints: [file] }),
121
143
  bundle: true,
122
- nodePaths: props.public ? [path.join(__dirname, "../vendor")] : undefined,
123
144
  outdir: destination.path,
124
145
  external: ["__STATIC_CONTENT_MANIFEST"],
125
146
  format: "esm",
@@ -128,6 +149,9 @@ export default async function publish(props: Props): Promise<void> {
128
149
  conditions: ["worker", "browser"],
129
150
  loader: {
130
151
  ".js": "jsx",
152
+ ".html": "text",
153
+ ".pem": "text",
154
+ ".txt": "text",
131
155
  },
132
156
  plugins: [moduleCollector.plugin],
133
157
  ...(jsxFactory && { jsxFactory }),
@@ -137,21 +161,25 @@ export default async function publish(props: Props): Promise<void> {
137
161
  // result.metafile is defined because of the `metafile: true` option above.
138
162
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
139
163
  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
164
+ const entryPoints = Object.entries(metafile.outputs).filter(
165
+ ([_path, output]) => output.entryPoint !== undefined
145
166
  );
146
- if (outputEntry === undefined) {
147
- throw new Error(
148
- `Cannot find entry-point "${expectedEntryPoint}" in generated bundle.`
149
- );
150
- }
167
+ assert(
168
+ entryPoints.length > 0,
169
+ `Cannot find entry-point "${file}" in generated bundle.` +
170
+ listEntryPoints(entryPoints)
171
+ );
172
+ assert(
173
+ entryPoints.length < 2,
174
+ "More than one entry-point found for generated bundle." +
175
+ listEntryPoints(entryPoints)
176
+ );
177
+ const entryPointExports = entryPoints[0][1].exports;
178
+ const resolvedEntryPointPath = entryPoints[0][0];
151
179
  const { format } = props;
152
180
  const bundle = {
153
- type: outputEntry[1].exports.length > 0 ? "esm" : "commonjs",
154
- exports: outputEntry[1].exports,
181
+ type: entryPointExports.length > 0 ? "esm" : "commonjs",
182
+ exports: entryPointExports,
155
183
  };
156
184
 
157
185
  // TODO: instead of bundling the facade with the worker, we should just bundle the worker and expose it as a module.
@@ -168,7 +196,7 @@ export default async function publish(props: Props): Promise<void> {
168
196
  return;
169
197
  }
170
198
 
171
- const content = await readFile(outputEntry[0], { encoding: "utf-8" });
199
+ let content = await readFile(resolvedEntryPointPath, { encoding: "utf-8" });
172
200
  await destination.cleanup();
173
201
 
174
202
  // if config.migrations
@@ -210,13 +238,16 @@ export default async function publish(props: Props): Promise<void> {
210
238
  }
211
239
  }
212
240
 
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 };
241
+ const assets = await syncAssets(
242
+ accountId,
243
+ scriptName,
244
+ props.assetPaths,
245
+ false,
246
+ props.env
247
+ );
217
248
 
218
249
  const bindings: CfWorkerInit["bindings"] = {
219
- kv_namespaces: envRootObj.kv_namespaces?.concat(
250
+ kv_namespaces: (envRootObj.kv_namespaces || []).concat(
220
251
  assets.namespace
221
252
  ? { binding: "__STATIC_CONTENT", id: assets.namespace }
222
253
  : []
@@ -226,17 +257,24 @@ export default async function publish(props: Props): Promise<void> {
226
257
  services: envRootObj.experimental_services,
227
258
  };
228
259
 
260
+ const workerType = bundle.type === "esm" ? "esm" : "commonjs";
261
+ if (workerType !== "esm" && assets.manifest) {
262
+ content = `const __STATIC_CONTENT_MANIFEST = ${JSON.stringify(
263
+ assets.manifest
264
+ )};\n${content}`;
265
+ }
266
+
229
267
  const worker: CfWorkerInit = {
230
268
  name: scriptName,
231
269
  main: {
232
- name: path.basename(outputEntry[0]),
270
+ name: path.basename(resolvedEntryPointPath),
233
271
  content: content,
234
- type: bundle.type === "esm" ? "esm" : "commonjs",
272
+ type: workerType,
235
273
  },
236
274
  bindings,
237
275
  ...(migrations && { migrations }),
238
276
  modules: moduleCollector.modules.concat(
239
- assets.manifest
277
+ assets.manifest && workerType === "esm"
240
278
  ? {
241
279
  name: "__STATIC_CONTENT_MANIFEST",
242
280
  content: JSON.stringify(assets.manifest),
@@ -262,12 +300,12 @@ export default async function publish(props: Props): Promise<void> {
262
300
 
263
301
  // Upload the script so it has time to propagate.
264
302
  const { available_on_subdomain } = await fetchResult(
265
- `${workerUrl}?available_on_subdomain=true`,
303
+ workerUrl,
266
304
  {
267
305
  method: "PUT",
268
- // @ts-expect-error: TODO: fix this type error!
269
306
  body: toFormData(worker),
270
- }
307
+ },
308
+ new URLSearchParams({ available_on_subdomains: "true" })
271
309
  );
272
310
 
273
311
  const uploadMs = Date.now() - start;
@@ -362,3 +400,11 @@ export default async function publish(props: Props): Promise<void> {
362
400
  console.log(" ", target);
363
401
  }
364
402
  }
403
+
404
+ function listEntryPoints(
405
+ outputs: [string, ValueOf<Metafile["outputs"]>][]
406
+ ): string {
407
+ return outputs.map(([_input, output]) => output.entryPoint).join("\n");
408
+ }
409
+
410
+ type ValueOf<T> = T[keyof T];
package/src/sites.tsx CHANGED
@@ -1,13 +1,36 @@
1
- import crypto from "node:crypto";
2
- import { createReadStream } from "node:fs";
1
+ import { readdir, readFile, stat } from "node:fs/promises";
3
2
  import * as path from "node:path";
4
- import { readdir, readFile } from "node:fs/promises";
5
- import { fetchResult } from "./cfetch";
6
- import { listNamespaceKeys, listNamespaces, putBulkKeyValue } from "./kv";
3
+ import ignore from "ignore";
4
+ import { XXHash64 } from "xxhash-addon";
5
+ import {
6
+ createNamespace,
7
+ listNamespaceKeys,
8
+ listNamespaces,
9
+ putBulkKeyValue,
10
+ } from "./kv";
11
+ import type { Config } from "./config";
12
+
13
+ /** Paths to always ignore. */
14
+ const ALWAYS_IGNORE = ["node_modules"];
15
+ const HIDDEN_FILES_TO_INCLUDE = [
16
+ ".well-known", // See https://datatracker.ietf.org/doc/html/rfc8615
17
+ ];
7
18
 
8
19
  async function* getFilesInFolder(dirPath: string): AsyncIterable<string> {
9
20
  const files = await readdir(dirPath, { withFileTypes: true });
10
21
  for (const file of files) {
22
+ // Skip files that we never want to process.
23
+ if (ALWAYS_IGNORE.some((p) => file.name === p)) {
24
+ continue;
25
+ }
26
+ // Skip hidden files (starting with .) except for some special ones
27
+ if (
28
+ file.name.startsWith(".") &&
29
+ !HIDDEN_FILES_TO_INCLUDE.some((p) => file.name === p)
30
+ ) {
31
+ continue;
32
+ }
33
+ // TODO: follow symlinks??
11
34
  if (file.isDirectory()) {
12
35
  yield* await getFilesInFolder(path.join(dirPath, file.name));
13
36
  } else {
@@ -16,27 +39,32 @@ async function* getFilesInFolder(dirPath: string): AsyncIterable<string> {
16
39
  }
17
40
  }
18
41
 
19
- async function hashFileContent(filePath: string): Promise<string> {
20
- return new Promise((resolve, reject) => {
21
- const hash = crypto.createHash("sha1");
22
- const rs = createReadStream(filePath);
23
- rs.on("error", reject);
24
- rs.on("data", (chunk) => hash.update(chunk));
25
- rs.on("end", () => resolve(hash.digest("hex")));
26
- });
42
+ /**
43
+ * Create a hash key for the given content using the xxhash algorithm.
44
+ *
45
+ * Note we only return the first 10 characters, since we will also include the file name in the asset manifest key
46
+ * the most important thing here is to detect changes of a single file to invalidate the cache and
47
+ * it's impossible to serve two different files with the same name
48
+ */
49
+ function hashFileContent(content: string): string {
50
+ const hasher = new XXHash64();
51
+ hasher.update(Buffer.from(content));
52
+ const hash = hasher.digest();
53
+ return hash.toString("hex").substring(0, 10);
27
54
  }
28
55
 
29
- async function hashFile(filePath: string): Promise<{
30
- filePath: string;
31
- hash: string;
32
- }> {
33
- const extName = path.extname(filePath);
56
+ /**
57
+ * Create a hashed asset key for the given asset.
58
+ *
59
+ * The key will change if the file path or content of the asset changes.
60
+ * The algorithm used here matches that of Wrangler 1.
61
+ */
62
+ function hashAsset(filePath: string, content: string): string {
63
+ const extName = path.extname(filePath) || "";
34
64
  const baseName = path.basename(filePath, extName);
35
- const hash = await hashFileContent(filePath);
36
- return {
37
- filePath: `${baseName}.${hash}${extName || ""}`,
38
- hash,
39
- };
65
+ const directory = path.dirname(filePath);
66
+ const hash = hashFileContent(content);
67
+ return urlSafe(path.join(directory, `${baseName}.${hash}${extName}`));
40
68
  }
41
69
 
42
70
  async function createKVNamespaceIfNotAlreadyExisting(
@@ -52,41 +80,52 @@ async function createKVNamespaceIfNotAlreadyExisting(
52
80
  }
53
81
 
54
82
  // else we make the namespace
55
- // TODO: use an export from ./kv
56
- const json = await fetchResult<{ id: string }>(
57
- `/accounts/${accountId}/storage/kv/namespaces`,
58
- {
59
- method: "POST",
60
- headers: {
61
- "Content-Type": "application/json",
62
- },
63
- body: JSON.stringify({ title }),
64
- }
65
- );
83
+ const id = await createNamespace(accountId, title);
84
+ console.log(`🌀 Created namespace for Workers Site "${title}"`);
66
85
 
67
86
  return {
68
87
  created: true,
69
- id: json.id,
88
+ id,
70
89
  };
71
90
  }
72
91
 
92
+ /**
93
+ * Upload the assets found within the `dirPath` directory to the sites assets KV namespace for
94
+ * the worker given by `scriptName`.
95
+ *
96
+ * @param accountId the account to upload to.
97
+ * @param scriptName the name of the worker whose assets we are uploading.
98
+ * @param siteAssets an objects describing what assets to upload, or undefined if there are no assets to upload.
99
+ * @param preview if true then upload to a "preview" KV namespace.
100
+ * @param _env (not implemented).
101
+ * @returns a promise for an object mapping the relative paths of the assets to the key of that
102
+ * asset in the KV namespace.
103
+ */
73
104
  export async function syncAssets(
74
105
  accountId: string,
75
106
  scriptName: string,
76
- dirPath: string,
107
+ siteAssets: AssetPaths | undefined,
77
108
  preview: boolean,
78
- _env?: string
79
- ) {
80
- const title = `__${scriptName}_sites_assets${preview ? "_preview" : ""}`;
109
+ env: string | undefined
110
+ ): Promise<{
111
+ manifest: { [filePath: string]: string } | undefined;
112
+ namespace: string | undefined;
113
+ }> {
114
+ if (siteAssets === undefined) {
115
+ return { manifest: undefined, namespace: undefined };
116
+ }
117
+
118
+ const title = `__${scriptName}${env ? `-${env}` : ""}-workers_sites_assets${
119
+ preview ? "_preview" : ""
120
+ }`;
81
121
  const { id: namespace } = await createKVNamespaceIfNotAlreadyExisting(
82
122
  title,
83
123
  accountId
84
124
  );
85
125
 
86
126
  // let's get all the keys in this namespace
87
- const keys = new Set(
88
- (await listNamespaceKeys(accountId, namespace)).map((x) => x.name)
89
- );
127
+ const result = await listNamespaceKeys(accountId, namespace);
128
+ const keys = new Set(result.map((x) => x.name));
90
129
 
91
130
  const manifest = {};
92
131
  const upload: {
@@ -94,25 +133,108 @@ export async function syncAssets(
94
133
  value: string;
95
134
  base64: boolean;
96
135
  }[] = [];
136
+
137
+ const include = createPatternMatcher(siteAssets.includePatterns, false);
138
+ const exclude = createPatternMatcher(siteAssets.excludePatterns, true);
97
139
  // TODO: this can be more efficient by parallelising
98
- for await (const file of getFilesInFolder(dirPath)) {
99
- // TODO: "exclude:" config
100
- const { filePath } = await hashFile(file);
140
+ for await (const file of getFilesInFolder(siteAssets.baseDirectory)) {
141
+ if (!include(file)) {
142
+ continue;
143
+ }
144
+ if (exclude(file)) {
145
+ continue;
146
+ }
147
+
148
+ await validateAssetSize(file);
149
+ console.log(`reading ${file}...`);
150
+ const content = await readFile(file, "base64");
151
+
152
+ const assetKey = hashAsset(file, content);
153
+ validateAssetKey(assetKey);
154
+
101
155
  // now put each of the files into kv
102
- if (!keys.has(filePath)) {
103
- console.log(`uploading ${file}...`);
104
- const content = await readFile(file, "base64");
105
- if (content.length > 25 * 1024 * 1024) {
106
- throw new Error(`File ${file} is too big, it should be under 25 mb.`);
107
- }
156
+ if (!keys.has(assetKey)) {
157
+ console.log(`uploading as ${assetKey}...`);
108
158
  upload.push({
109
- key: filePath,
159
+ key: assetKey,
110
160
  value: content,
111
161
  base64: true,
112
162
  });
163
+ } else {
164
+ console.log(`skipping - already uploaded`);
113
165
  }
114
- manifest[path.relative(dirPath, file)] = filePath;
166
+ manifest[path.relative(siteAssets.baseDirectory, file)] = assetKey;
115
167
  }
116
168
  await putBulkKeyValue(accountId, namespace, JSON.stringify(upload));
117
169
  return { manifest, namespace };
118
170
  }
171
+
172
+ function createPatternMatcher(
173
+ patterns: string[],
174
+ exclude: boolean
175
+ ): (filePath: string) => boolean {
176
+ if (patterns.length === 0) {
177
+ return (_filePath) => !exclude;
178
+ } else {
179
+ const ignorer = ignore().add(patterns);
180
+ return (filePath) => ignorer.test(filePath).ignored;
181
+ }
182
+ }
183
+
184
+ async function validateAssetSize(filePath: string) {
185
+ const { size } = await stat(filePath);
186
+ if (size > 25 * 1024 * 1024) {
187
+ throw new Error(
188
+ `File ${filePath} is too big, it should be under 25 MiB. See https://developers.cloudflare.com/workers/platform/limits#kv-limits`
189
+ );
190
+ }
191
+ }
192
+
193
+ function validateAssetKey(assetKey: string) {
194
+ if (assetKey.length > 512) {
195
+ throw new Error(
196
+ `The asset path key "${assetKey}" exceeds the maximum key size limit of 512. See https://developers.cloudflare.com/workers/platform/limits#kv-limits",`
197
+ );
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Convert a filePath to be safe to use as a relative URL.
203
+ *
204
+ * Primarily this involves converting Windows backslashes to forward slashes.
205
+ */
206
+ function urlSafe(filePath: string): string {
207
+ return filePath.replace(/\\/g, "/");
208
+ }
209
+
210
+ /**
211
+ * Information about the assets that should be uploaded
212
+ */
213
+ export interface AssetPaths {
214
+ baseDirectory: string;
215
+ includePatterns: string[];
216
+ excludePatterns: string[];
217
+ }
218
+
219
+ /**
220
+ * Get an object that describes what assets to upload, if any.
221
+ *
222
+ * Uses the args (passed from the command line) if available,
223
+ * falling back to those defined in the config.
224
+ *
225
+ * // TODO: Support for environments
226
+ */
227
+ export function getAssetPaths(
228
+ config: Config,
229
+ baseDirectory = config.site?.bucket,
230
+ includePatterns = config.site?.include ?? [],
231
+ excludePatterns = config.site?.exclude ?? []
232
+ ): undefined | AssetPaths {
233
+ return baseDirectory
234
+ ? {
235
+ baseDirectory,
236
+ includePatterns,
237
+ excludePatterns,
238
+ }
239
+ : undefined;
240
+ }