@astrojs/cloudflare 7.1.1 → 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.
@@ -202,6 +331,33 @@ This will enable Cloudflare to serve files and process static redirects without
202
331
 
203
332
  See [Cloudflare's documentation](https://developers.cloudflare.com/pages/platform/functions/routing/#create-a-_routesjson-file) for more details.
204
333
 
334
+ ## Node.js compatibility
335
+
336
+ Astro's Cloudflare adapter allows you to use any Node.js runtime API supported by Cloudflare:
337
+
338
+ - assert
339
+ - AsyncLocalStorage
340
+ - Buffer
341
+ - Diagnostics Channel
342
+ - EventEmitter
343
+ - path
344
+ - process
345
+ - Streams
346
+ - StringDecoder
347
+ - util
348
+
349
+ To use these APIs, your page or endpoint must be server-side rendered (not pre-rendered) and must use the the `import {} from 'node:*'` import syntax.
350
+
351
+ ```js
352
+ // pages/api/endpoint.js
353
+ export const prerender = false;
354
+ import { Buffer } from 'node:buffer';
355
+ ```
356
+
357
+ Additionally, you'll need to enable the Compatibility Flag in Cloudflare. The configuration for this flag may vary based on where you deploy your Astro site.
358
+
359
+ For detailed guidance, please refer to the [Cloudflare documentation](https://developers.cloudflare.com/workers/runtime-apis/nodejs).
360
+
205
361
  ## Troubleshooting
206
362
 
207
363
  For help, check out the `#support` channel on [Discord](https://astro.build/chat). Our friendly Support Squad members are here to help!
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,23 +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
- entryPoints: entryPaths,
245
- outdir: outputDir,
246
- allowOverwrite: true,
247
- format: "esm",
248
- bundle: true,
249
- minify: _config.vite?.build?.minify !== false,
250
- banner: {
251
- js: SHIM
252
- },
253
- logOverride: {
254
- "ignored-bare-import": "silent"
255
- }
256
- });
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
+ }
257
301
  const outputFiles = await glob(`**/*`, {
258
302
  cwd: outputDir,
259
303
  filesOnly: true
@@ -287,6 +331,18 @@ function createIntegration(args) {
287
331
  target: "es2020",
288
332
  platform: "browser",
289
333
  conditions: ["workerd", "worker", "browser"],
334
+ external: [
335
+ "node:assert",
336
+ "node:async_hooks",
337
+ "node:buffer",
338
+ "node:diagnostics_channel",
339
+ "node:events",
340
+ "node:path",
341
+ "node:process",
342
+ "node:stream",
343
+ "node:string_decoder",
344
+ "node:util"
345
+ ],
290
346
  entryPoints: [entryPath],
291
347
  outfile: buildPath,
292
348
  allowOverwrite: true,
@@ -298,7 +354,12 @@ function createIntegration(args) {
298
354
  },
299
355
  logOverride: {
300
356
  "ignored-bare-import": "silent"
301
- }
357
+ },
358
+ plugins: !args?.wasmModuleImports ? [] : [
359
+ rewriteWasmImportPath({
360
+ relativePathToAssets: isModeDirectory ? relative(fileURLToPath(functionsUrl), fileURLToPath(assetsUrl)) : relative(fileURLToPath(_buildConfig.client), fileURLToPath(assetsUrl))
361
+ })
362
+ ]
302
363
  });
303
364
  await fs.promises.rename(buildPath, finalBuildUrl);
304
365
  if (isModeDirectory) {
@@ -320,6 +381,9 @@ function createIntegration(args) {
320
381
  }
321
382
  }
322
383
  }
384
+ if (!isModeDirectory) {
385
+ cloudflareSpecialFiles.push("_worker.js");
386
+ }
323
387
  const routesExists = await fs.promises.stat(new URL("./_routes.json", _config.outDir)).then((stat) => stat.isFile()).catch(() => false);
324
388
  if (!routesExists) {
325
389
  const functionEndpoints = routes.filter((route) => potentialFunctionRouteTypes.includes(route.type) && !route.prerender).map((route) => {
@@ -375,29 +439,34 @@ function createIntegration(args) {
375
439
  );
376
440
  }
377
441
  staticPathList.push(...routes.filter((r) => r.type === "redirect").map((r) => r.route));
378
- let include = deduplicatePatterns(
379
- functionEndpoints.map((endpoint) => endpoint.includePattern)
380
- );
381
- let exclude = deduplicatePatterns(
382
- staticPathList.filter(
383
- (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 ?? [])
384
451
  )
385
- );
386
- if (include.length === 0) {
387
- include = ["/"];
388
- exclude = ["/"];
389
- }
390
- if (include.length + exclude.length > staticPathList.length) {
391
- include = ["/*"];
392
- exclude = deduplicatePatterns(staticPathList);
452
+ };
453
+ if (includeStrategy?.include.length === 0) {
454
+ includeStrategy.include = ["/"];
455
+ includeStrategy.exclude = ["/"];
393
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;
394
464
  await fs.promises.writeFile(
395
465
  new URL("./_routes.json", _config.outDir),
396
466
  JSON.stringify(
397
467
  {
398
468
  version: 1,
399
- include,
400
- exclude
469
+ ...winningStrategy
401
470
  },
402
471
  null,
403
472
  2
@@ -423,6 +492,27 @@ function deduplicatePatterns(patterns) {
423
492
  return true;
424
493
  });
425
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
+ }
426
516
  export {
427
517
  createIntegration as default,
428
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.1.1",
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.1"
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.1",
56
+ "astro": "3.1.3",
57
57
  "astro-scripts": "0.0.14"
58
58
  },
59
59
  "scripts": {