@astrojs/cloudflare 7.2.0 → 7.3.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.
package/README.md CHANGED
@@ -75,6 +75,92 @@ export default defineConfig({
75
75
 
76
76
  Note that this adapter does not support using [Cloudflare Pages Middleware](https://developers.cloudflare.com/pages/platform/functions/middleware/). Astro will bundle the [Astro middleware](https://docs.astro.build/en/guides/middleware/) into each page.
77
77
 
78
+ ### routes.strategy
79
+
80
+ `routes.strategy: "auto" | "include" | "exclude"`
81
+
82
+ default `"auto"`
83
+
84
+ Determines how `routes.json` will be generated if no [custom `_routes.json`](#custom-_routesjson) is provided.
85
+
86
+ There are three options available:
87
+
88
+ - **`"auto"` (default):** Will automatically select the strategy that generates the fewest entries. This should almost always be sufficient, so choose this option unless you have a specific reason not to.
89
+
90
+ - **`include`:** Pages and endpoints that are not pre-rendered are listed as `include` entries, telling Cloudflare to invoke these routes as functions. `exclude` entries are only used to resolve conflicts. Usually the best strategy when your website has mostly static pages and only a few dynamic pages or endpoints.
91
+
92
+ Example: For `src/pages/index.astro` (static), `src/pages/company.astro` (static), `src/pages/users/faq.astro` (static) and `/src/pages/users/[id].astro` (SSR) this will produce the following `_routes.json`:
93
+
94
+ ```json
95
+ {
96
+ "version": 1,
97
+ "include": [
98
+ "/_image", // Astro's image endpoint
99
+ "/users/*" // Dynamic route
100
+ ],
101
+ "exclude": [
102
+ // Static routes that needs to be exempted from the dynamic wildcard route above
103
+ "/users/faq/",
104
+ "/users/faq/index.html"
105
+ ]
106
+ }
107
+ ```
108
+
109
+ - **`exclude`:** Pre-rendered pages are listed as `exclude` entries (telling Cloudflare to handle these routes as static assets). Usually the best strategy when your website has mostly dynamic pages or endpoints and only a few static pages.
110
+
111
+ Example: For the same pages as in the previous example this will produce the following `_routes.json`:
112
+
113
+ ```json
114
+ {
115
+ "version": 1,
116
+ "include": [
117
+ "/*" // Handle everything as function except the routes below
118
+ ],
119
+ "exclude": [
120
+ // All static assets
121
+ "/",
122
+ "/company/",
123
+ "/index.html",
124
+ "/users/faq/",
125
+ "/favicon.png",
126
+ "/company/index.html",
127
+ "/users/faq/index.html"
128
+ ]
129
+ }
130
+ ```
131
+
132
+ ### routes.include
133
+
134
+ `routes.include: string[]`
135
+
136
+ default `[]`
137
+
138
+ If you want to use the automatic `_routes.json` generation, but want to include additional routes (e.g. when having custom functions in the `functions` folder), you can use the `routes.include` option to add additional routes to the `include` array.
139
+
140
+ ### routes.exclude
141
+
142
+ `routes.exclude: string[]`
143
+
144
+ default `[]`
145
+
146
+ If you want to use the automatic `_routes.json` generation, but want to exclude additional routes, you can use the `routes.exclude` option to add additional routes to the `exclude` array.
147
+
148
+ The following example automatically generates `_routes.json` while including and excluding additional routes. Note that that is only necessary if you have custom functions in the `functions` folder that are not handled by Astro.
149
+
150
+ ```diff
151
+ // astro.config.mjs
152
+ export default defineConfig({
153
+ adapter: cloudflare({
154
+ mode: 'directory',
155
+ + routes: {
156
+ + strategy: 'include',
157
+ + include: ['/users/*'], // handled by custom function: functions/users/[id].js
158
+ + exclude: ['/users/faq'], // handled by static page: pages/users/faq.astro
159
+ + },
160
+ }),
161
+ });
162
+ ```
163
+
78
164
  ## Enabling Preview
79
165
 
80
166
  In order for preview to work you must install `wrangler`
@@ -191,6 +277,49 @@ export default defineConfig({
191
277
  });
192
278
  ```
193
279
 
280
+ ## Wasm module imports
281
+
282
+ `wasmModuleImports: boolean`
283
+
284
+ default: `false`
285
+
286
+ Whether or not to import `.wasm` files [directly as ES modules](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration).
287
+
288
+ Add `wasmModuleImports: true` to `astro.config.mjs` to enable in both the Cloudflare build and the Astro dev server.
289
+
290
+ ```diff
291
+ // astro.config.mjs
292
+ import {defineConfig} from "astro/config";
293
+ import cloudflare from '@astrojs/cloudflare';
294
+
295
+ export default defineConfig({
296
+ adapter: cloudflare({
297
+ + wasmModuleImports: true
298
+ }),
299
+ output: 'server'
300
+ })
301
+ ```
302
+
303
+ Once enabled, you can import a web assembly module in Astro with a `.wasm?module` import.
304
+
305
+ The following is an example of importing a Wasm module that then responds to requests by adding the request's number parameters together.
306
+
307
+ ```javascript
308
+ // pages/add/[a]/[b].js
309
+ import mod from '../util/add.wasm?module';
310
+
311
+ // instantiate ahead of time to share module
312
+ const addModule: any = new WebAssembly.Instance(mod);
313
+
314
+ export async function GET(context) {
315
+ const a = Number.parseInt(context.params.a);
316
+ const b = Number.parseInt(context.params.b);
317
+ return new Response(`${addModule.exports.add(a, b)}`);
318
+ }
319
+ ```
320
+
321
+ While this example is trivial, Wasm can be used to accelerate computationally intensive operations which do not involve significant I/O such as embedding an image processing library.
322
+
194
323
  ## Headers, Redirects and function invocation routes
195
324
 
196
325
  Cloudflare has support for adding custom [headers](https://developers.cloudflare.com/pages/platform/headers/), configuring static [redirects](https://developers.cloudflare.com/pages/platform/redirects/) and defining which routes should [invoke functions](https://developers.cloudflare.com/pages/platform/functions/routing/#function-invocation-routes). Cloudflare looks for `_headers`, `_redirects`, and `_routes.json` files in your build output directory to configure these features. This means they should be placed in your Astro project’s `public/` directory.
package/dist/index.d.ts CHANGED
@@ -4,12 +4,26 @@ export type { DirectoryRuntime } from './server.directory.js';
4
4
  type Options = {
5
5
  mode?: 'directory' | 'advanced';
6
6
  functionPerRoute?: boolean;
7
+ /** Configure automatic `routes.json` generation */
8
+ routes?: {
9
+ /** Strategy for generating `include` and `exclude` patterns
10
+ * - `auto`: Will use the strategy that generates the least amount of entries.
11
+ * - `include`: For each page or endpoint in your application that is not prerendered, an entry in the `include` array will be generated. For each page that is prerendered and whoose path is matched by an `include` entry, an entry in the `exclude` array will be generated.
12
+ * - `exclude`: One `"/*"` entry in the `include` array will be generated. For each page that is prerendered, an entry in the `exclude` array will be generated.
13
+ * */
14
+ strategy?: 'auto' | 'include' | 'exclude';
15
+ /** Additional `include` patterns */
16
+ include?: string[];
17
+ /** Additional `exclude` patterns */
18
+ exclude?: string[];
19
+ };
7
20
  /**
8
21
  * 'off': current behaviour (wrangler is needed)
9
22
  * 'local': use a static req.cf object, and env vars defined in wrangler.toml & .dev.vars (astro dev is enough)
10
23
  * 'remote': use a dynamic real-live req.cf object, and env vars defined in wrangler.toml & .dev.vars (astro dev is enough)
11
24
  */
12
25
  runtime?: 'off' | 'local' | 'remote';
26
+ wasmModuleImports?: boolean;
13
27
  };
14
28
  export declare function getAdapter({ isModeDirectory, functionPerRoute, }: {
15
29
  isModeDirectory: boolean;
package/dist/index.js CHANGED
@@ -6,10 +6,11 @@ import { AstroError } from "astro/errors";
6
6
  import esbuild from "esbuild";
7
7
  import * as fs from "node:fs";
8
8
  import * as os from "node:os";
9
- import { sep } from "node:path";
9
+ import { basename, dirname, relative, sep } from "node:path";
10
10
  import { fileURLToPath, pathToFileURL } from "node:url";
11
11
  import glob from "tiny-glob";
12
12
  import { getEnvVars } from "./parser.js";
13
+ import { wasmModuleLoader } from "./wasm-module-loader.js";
13
14
  class StorageFactory {
14
15
  storages = /* @__PURE__ */ new Map();
15
16
  storage(namespace) {
@@ -146,6 +147,15 @@ function createIntegration(args) {
146
147
  server: new URL(`.${SERVER_BUILD_FOLDER}`, config.outDir),
147
148
  serverEntry: "_worker.mjs",
148
149
  redirects: false
150
+ },
151
+ vite: {
152
+ // load .wasm files as WebAssembly modules
153
+ plugins: [
154
+ wasmModuleLoader({
155
+ disabled: !args?.wasmModuleImports,
156
+ assetsDirectory: config.build.assets
157
+ })
158
+ ]
149
159
  }
150
160
  });
151
161
  },
@@ -229,6 +239,7 @@ function createIntegration(args) {
229
239
  },
230
240
  "astro:build:done": async ({ pages, routes, dir }) => {
231
241
  const functionsUrl = new URL("functions/", _config.root);
242
+ const assetsUrl = new URL(_buildConfig.assets, _buildConfig.client);
232
243
  if (isModeDirectory) {
233
244
  await fs.promises.mkdir(functionsUrl, { recursive: true });
234
245
  }
@@ -237,35 +248,56 @@ function createIntegration(args) {
237
248
  const entryPaths = entryPointsURL.map((entry) => fileURLToPath(entry));
238
249
  const outputUrl = new URL("$astro", _buildConfig.server);
239
250
  const outputDir = fileURLToPath(outputUrl);
240
- await esbuild.build({
241
- target: "es2020",
242
- platform: "browser",
243
- conditions: ["workerd", "worker", "browser"],
244
- external: [
245
- "node:assert",
246
- "node:async_hooks",
247
- "node:buffer",
248
- "node:diagnostics_channel",
249
- "node:events",
250
- "node:path",
251
- "node:process",
252
- "node:stream",
253
- "node:string_decoder",
254
- "node:util"
255
- ],
256
- entryPoints: entryPaths,
257
- outdir: outputDir,
258
- allowOverwrite: true,
259
- format: "esm",
260
- bundle: true,
261
- minify: _config.vite?.build?.minify !== false,
262
- banner: {
263
- js: SHIM
264
- },
265
- logOverride: {
266
- "ignored-bare-import": "silent"
267
- }
268
- });
251
+ const entryPathsGroupedByDepth = !args.wasmModuleImports ? [entryPaths] : entryPaths.reduce((sum, thisPath) => {
252
+ const depthFromRoot = thisPath.split(sep).length;
253
+ sum.set(depthFromRoot, (sum.get(depthFromRoot) || []).concat(thisPath));
254
+ return sum;
255
+ }, /* @__PURE__ */ new Map()).values();
256
+ for (const pathsGroup of entryPathsGroupedByDepth) {
257
+ const pagesDirname = relative(fileURLToPath(_buildConfig.server), pathsGroup[0]).split(
258
+ sep
259
+ )[0];
260
+ const absolutePagesDirname = fileURLToPath(new URL(pagesDirname, _buildConfig.server));
261
+ const urlWithinFunctions = new URL(
262
+ relative(absolutePagesDirname, pathsGroup[0]),
263
+ functionsUrl
264
+ );
265
+ const relativePathToAssets = relative(
266
+ dirname(fileURLToPath(urlWithinFunctions)),
267
+ fileURLToPath(assetsUrl)
268
+ );
269
+ await esbuild.build({
270
+ target: "es2020",
271
+ platform: "browser",
272
+ conditions: ["workerd", "worker", "browser"],
273
+ external: [
274
+ "node:assert",
275
+ "node:async_hooks",
276
+ "node:buffer",
277
+ "node:diagnostics_channel",
278
+ "node:events",
279
+ "node:path",
280
+ "node:process",
281
+ "node:stream",
282
+ "node:string_decoder",
283
+ "node:util"
284
+ ],
285
+ entryPoints: pathsGroup,
286
+ outbase: absolutePagesDirname,
287
+ outdir: outputDir,
288
+ allowOverwrite: true,
289
+ format: "esm",
290
+ bundle: true,
291
+ minify: _config.vite?.build?.minify !== false,
292
+ banner: {
293
+ js: SHIM
294
+ },
295
+ logOverride: {
296
+ "ignored-bare-import": "silent"
297
+ },
298
+ plugins: !args?.wasmModuleImports ? [] : [rewriteWasmImportPath({ relativePathToAssets })]
299
+ });
300
+ }
269
301
  const outputFiles = await glob(`**/*`, {
270
302
  cwd: outputDir,
271
303
  filesOnly: true
@@ -322,7 +354,12 @@ function createIntegration(args) {
322
354
  },
323
355
  logOverride: {
324
356
  "ignored-bare-import": "silent"
325
- }
357
+ },
358
+ plugins: !args?.wasmModuleImports ? [] : [
359
+ rewriteWasmImportPath({
360
+ relativePathToAssets: isModeDirectory ? relative(fileURLToPath(functionsUrl), fileURLToPath(assetsUrl)) : relative(fileURLToPath(_buildConfig.client), fileURLToPath(assetsUrl))
361
+ })
362
+ ]
326
363
  });
327
364
  await fs.promises.rename(buildPath, finalBuildUrl);
328
365
  if (isModeDirectory) {
@@ -344,6 +381,9 @@ function createIntegration(args) {
344
381
  }
345
382
  }
346
383
  }
384
+ if (!isModeDirectory) {
385
+ cloudflareSpecialFiles.push("_worker.js");
386
+ }
347
387
  const routesExists = await fs.promises.stat(new URL("./_routes.json", _config.outDir)).then((stat) => stat.isFile()).catch(() => false);
348
388
  if (!routesExists) {
349
389
  const functionEndpoints = routes.filter((route) => potentialFunctionRouteTypes.includes(route.type) && !route.prerender).map((route) => {
@@ -399,29 +439,34 @@ function createIntegration(args) {
399
439
  );
400
440
  }
401
441
  staticPathList.push(...routes.filter((r) => r.type === "redirect").map((r) => r.route));
402
- let include = deduplicatePatterns(
403
- functionEndpoints.map((endpoint) => endpoint.includePattern)
404
- );
405
- let exclude = deduplicatePatterns(
406
- staticPathList.filter(
407
- (file) => functionEndpoints.some((endpoint) => endpoint.regexp.test(file))
442
+ const strategy = args?.routes?.strategy ?? "auto";
443
+ const includeStrategy = strategy === "exclude" ? void 0 : {
444
+ include: deduplicatePatterns(
445
+ functionEndpoints.map((endpoint) => endpoint.includePattern).concat(args?.routes?.include ?? [])
446
+ ),
447
+ exclude: deduplicatePatterns(
448
+ staticPathList.filter(
449
+ (file) => functionEndpoints.some((endpoint) => endpoint.regexp.test(file))
450
+ ).concat(args?.routes?.exclude ?? [])
408
451
  )
409
- );
410
- if (include.length === 0) {
411
- include = ["/"];
412
- exclude = ["/"];
413
- }
414
- if (include.length + exclude.length > staticPathList.length) {
415
- include = ["/*"];
416
- exclude = deduplicatePatterns(staticPathList);
452
+ };
453
+ if (includeStrategy?.include.length === 0) {
454
+ includeStrategy.include = ["/"];
455
+ includeStrategy.exclude = ["/"];
417
456
  }
457
+ const excludeStrategy = strategy === "include" ? void 0 : {
458
+ include: ["/*"],
459
+ exclude: deduplicatePatterns(staticPathList.concat(args?.routes?.exclude ?? []))
460
+ };
461
+ const includeStrategyLength = includeStrategy ? includeStrategy.include.length + includeStrategy.exclude.length : Infinity;
462
+ const excludeStrategyLength = excludeStrategy ? excludeStrategy.include.length + excludeStrategy.exclude.length : Infinity;
463
+ const winningStrategy = includeStrategyLength <= excludeStrategyLength ? includeStrategy : excludeStrategy;
418
464
  await fs.promises.writeFile(
419
465
  new URL("./_routes.json", _config.outDir),
420
466
  JSON.stringify(
421
467
  {
422
468
  version: 1,
423
- include,
424
- exclude
469
+ ...winningStrategy
425
470
  },
426
471
  null,
427
472
  2
@@ -447,6 +492,27 @@ function deduplicatePatterns(patterns) {
447
492
  return true;
448
493
  });
449
494
  }
495
+ function rewriteWasmImportPath({
496
+ relativePathToAssets
497
+ }) {
498
+ return {
499
+ name: "wasm-loader",
500
+ setup(build) {
501
+ build.onResolve({ filter: /.*\.wasm.mjs$/ }, (args) => {
502
+ const updatedPath = [
503
+ relativePathToAssets.replaceAll("\\", "/"),
504
+ basename(args.path).replace(/\.mjs$/, "")
505
+ ].join("/");
506
+ return {
507
+ path: updatedPath,
508
+ // change the reference to the changed module
509
+ external: true
510
+ // mark it as external in the bundle
511
+ };
512
+ });
513
+ }
514
+ };
515
+ }
450
516
  export {
451
517
  createIntegration as default,
452
518
  getAdapter
@@ -0,0 +1,14 @@
1
+ import { type Plugin } from 'vite';
2
+ /**
3
+ * Loads '*.wasm?module' imports as WebAssembly modules, which is the only way to load WASM in cloudflare workers.
4
+ * Current proposal for WASM modules: https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration
5
+ * Cloudflare worker WASM from javascript support: https://developers.cloudflare.com/workers/runtime-apis/webassembly/javascript/
6
+ * @param disabled - if true throws a helpful error message if wasm is encountered and wasm imports are not enabled,
7
+ * otherwise it will error obscurely in the esbuild and vite builds
8
+ * @param assetsDirectory - the folder name for the assets directory in the build directory. Usually '_astro'
9
+ * @returns Vite plugin to load WASM tagged with '?module' as a WASM modules
10
+ */
11
+ export declare function wasmModuleLoader({ disabled, assetsDirectory, }: {
12
+ disabled: boolean;
13
+ assetsDirectory: string;
14
+ }): Plugin;
@@ -0,0 +1,88 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import {} from "vite";
4
+ function wasmModuleLoader({
5
+ disabled,
6
+ assetsDirectory
7
+ }) {
8
+ const postfix = ".wasm?module";
9
+ let isDev = false;
10
+ return {
11
+ name: "vite:wasm-module-loader",
12
+ enforce: "pre",
13
+ configResolved(config) {
14
+ isDev = config.command === "serve";
15
+ },
16
+ config(_, __) {
17
+ return {
18
+ assetsInclude: ["**/*.wasm?module"],
19
+ build: { rollupOptions: { external: /^__WASM_ASSET__.+\.wasm\.mjs$/i } }
20
+ };
21
+ },
22
+ load(id, _) {
23
+ if (!id.endsWith(postfix)) {
24
+ return;
25
+ }
26
+ if (disabled) {
27
+ throw new Error(
28
+ `WASM module's cannot be loaded unless you add \`wasmModuleImports: true\` to your astro config.`
29
+ );
30
+ }
31
+ const filePath = id.slice(0, -1 * "?module".length);
32
+ const data = fs.readFileSync(filePath);
33
+ const base64 = data.toString("base64");
34
+ const base64Module = `
35
+ const wasmModule = new WebAssembly.Module(Uint8Array.from(atob("${base64}"), c => c.charCodeAt(0)));
36
+ export default wasmModule
37
+ `;
38
+ if (isDev) {
39
+ return base64Module;
40
+ } else {
41
+ let hash = hashString(base64);
42
+ const assetName = path.basename(filePath).split(".")[0] + "." + hash + ".wasm";
43
+ this.emitFile({
44
+ type: "asset",
45
+ // put it explicitly in the _astro assets directory with `fileName` rather than `name` so that
46
+ // vite doesn't give it a random id in its name. We need to be able to easily rewrite from
47
+ // the .mjs loader and the actual wasm asset later in the ESbuild for the worker
48
+ fileName: path.join(assetsDirectory, assetName),
49
+ source: fs.readFileSync(filePath)
50
+ });
51
+ const chunkId = this.emitFile({
52
+ type: "prebuilt-chunk",
53
+ fileName: assetName + ".mjs",
54
+ code: base64Module
55
+ });
56
+ return `
57
+ import wasmModule from "__WASM_ASSET__${chunkId}.wasm.mjs";
58
+ export default wasmModule;
59
+ `;
60
+ }
61
+ },
62
+ // output original wasm file relative to the chunk
63
+ renderChunk(code, chunk, _) {
64
+ if (isDev)
65
+ return;
66
+ if (!/__WASM_ASSET__/g.test(code))
67
+ return;
68
+ const final = code.replaceAll(/__WASM_ASSET__([a-z\d]+).wasm.mjs/g, (s, assetId) => {
69
+ const fileName = this.getFileName(assetId);
70
+ const relativePath = path.relative(path.dirname(chunk.fileName), fileName).replaceAll("\\", "/");
71
+ return `./${relativePath}`;
72
+ });
73
+ return { code: final };
74
+ }
75
+ };
76
+ }
77
+ function hashString(str) {
78
+ let hash = 0;
79
+ for (let i = 0; i < str.length; i++) {
80
+ const char = str.charCodeAt(i);
81
+ hash = (hash << 5) - hash + char;
82
+ hash &= hash;
83
+ }
84
+ return new Uint32Array([hash])[0].toString(36);
85
+ }
86
+ export {
87
+ wasmModuleLoader
88
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@astrojs/cloudflare",
3
3
  "description": "Deploy your site to Cloudflare Workers/Pages",
4
- "version": "7.2.0",
4
+ "version": "7.3.0",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
7
7
  "author": "withastro",
@@ -41,19 +41,19 @@
41
41
  "esbuild": "^0.19.2",
42
42
  "find-up": "^6.3.0",
43
43
  "tiny-glob": "^0.2.9",
44
+ "vite": "^4.4.9",
44
45
  "@astrojs/underscore-redirects": "0.3.0"
45
46
  },
46
47
  "peerDependencies": {
47
- "astro": "^3.1.2"
48
+ "astro": "^3.1.3"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@types/iarna__toml": "^2.0.2",
51
52
  "chai": "^4.3.7",
52
53
  "cheerio": "1.0.0-rc.12",
53
- "kill-port": "^2.0.1",
54
54
  "mocha": "^10.2.0",
55
55
  "wrangler": "^3.5.1",
56
- "astro": "3.1.2",
56
+ "astro": "3.1.3",
57
57
  "astro-scripts": "0.0.14"
58
58
  },
59
59
  "scripts": {