diff --git a/.changeset/fifty-weeks-bake.md b/.changeset/fifty-weeks-bake.md
deleted file mode 100644
index 714be802e..000000000
--- a/.changeset/fifty-weeks-bake.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-'@astrojs/cloudflare': minor
----
-
-Add support for the following Node.js Runtime APIs, which are availabe in [Cloudflare](https://developers.cloudflare.com/workers/runtime-apis/nodejs) using the `node:` syntax.
-
-- assert
-- AsyncLocalStorage
-- Buffer
-- Diagnostics Channel
-- EventEmitter
-- path
-- process
-- Streams
-- StringDecoder
-- util
-
-```js
-import { Buffer } from 'node:buffer';
-```
diff --git a/.github/assets/banner.png b/.github/assets/banner.png
index 91dfc11e3..7696fafb7 100644
Binary files a/.github/assets/banner.png and b/.github/assets/banner.png differ
diff --git a/.github/assets/deepgram-dark.svg b/.github/assets/deepgram-dark.svg
index 3531c9554..9b66066ff 100644
--- a/.github/assets/deepgram-dark.svg
+++ b/.github/assets/deepgram-dark.svg
@@ -1,35 +1 @@
-
+
\ No newline at end of file
diff --git a/.github/assets/deepgram.svg b/.github/assets/deepgram.svg
index dd3090044..ec55ad3fa 100644
--- a/.github/assets/deepgram.svg
+++ b/.github/assets/deepgram.svg
@@ -1,35 +1 @@
-
+
\ No newline at end of file
diff --git a/.github/assets/monogram-dark.svg b/.github/assets/monogram-dark.svg
index ab2d43ac2..feccf6fe8 100644
--- a/.github/assets/monogram-dark.svg
+++ b/.github/assets/monogram-dark.svg
@@ -1,11 +1 @@
-
+
\ No newline at end of file
diff --git a/.github/assets/monogram.svg b/.github/assets/monogram.svg
index 35a377e5e..e60aa428e 100644
--- a/.github/assets/monogram.svg
+++ b/.github/assets/monogram.svg
@@ -1,11 +1 @@
-
+
\ No newline at end of file
diff --git a/.github/assets/netlify-dark.svg b/.github/assets/netlify-dark.svg
index d974206de..2fcff1dde 100644
--- a/.github/assets/netlify-dark.svg
+++ b/.github/assets/netlify-dark.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/.github/assets/netlify.svg b/.github/assets/netlify.svg
index 7a068f244..aa07cbcab 100644
--- a/.github/assets/netlify.svg
+++ b/.github/assets/netlify.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/.github/assets/qoddi-dark.png b/.github/assets/qoddi-dark.png
index 623654acf..b666799d9 100644
Binary files a/.github/assets/qoddi-dark.png and b/.github/assets/qoddi-dark.png differ
diff --git a/.github/assets/qoddi.png b/.github/assets/qoddi.png
index e664cfd28..e7a1c3c43 100644
Binary files a/.github/assets/qoddi.png and b/.github/assets/qoddi.png differ
diff --git a/.github/assets/sentry-dark.svg b/.github/assets/sentry-dark.svg
index f194ec36e..1e09e141b 100644
--- a/.github/assets/sentry-dark.svg
+++ b/.github/assets/sentry-dark.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/.github/assets/sentry.svg b/.github/assets/sentry.svg
index a4470b601..56e122e04 100644
--- a/.github/assets/sentry.svg
+++ b/.github/assets/sentry.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/.github/assets/shipshape-dark.svg b/.github/assets/shipshape-dark.svg
index d270d2748..038534ec9 100644
--- a/.github/assets/shipshape-dark.svg
+++ b/.github/assets/shipshape-dark.svg
@@ -1,20 +1 @@
-
-
+
\ No newline at end of file
diff --git a/.github/assets/shipshape.svg b/.github/assets/shipshape.svg
index f2d1a8dc3..6c2e2ee70 100644
--- a/.github/assets/shipshape.svg
+++ b/.github/assets/shipshape.svg
@@ -1,20 +1 @@
-
-
+
\ No newline at end of file
diff --git a/.github/assets/stackup-dark.svg b/.github/assets/stackup-dark.svg
index 3b8ed2746..b7a458410 100644
--- a/.github/assets/stackup-dark.svg
+++ b/.github/assets/stackup-dark.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/.github/assets/stackup.svg b/.github/assets/stackup.svg
index 207f28b2a..6faa178f5 100644
--- a/.github/assets/stackup.svg
+++ b/.github/assets/stackup.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/.github/assets/storyblok-dark.svg b/.github/assets/storyblok-dark.svg
index d9917a453..9df58c4bb 100644
--- a/.github/assets/storyblok-dark.svg
+++ b/.github/assets/storyblok-dark.svg
@@ -1,22 +1 @@
-
-
+
\ No newline at end of file
diff --git a/.github/assets/storyblok.svg b/.github/assets/storyblok.svg
index 7270bb296..ec7759356 100644
--- a/.github/assets/storyblok.svg
+++ b/.github/assets/storyblok.svg
@@ -1,15 +1 @@
-
-
+
\ No newline at end of file
diff --git a/.github/assets/vercel-dark.svg b/.github/assets/vercel-dark.svg
index 31c2d5b88..1fbf0317d 100644
--- a/.github/assets/vercel-dark.svg
+++ b/.github/assets/vercel-dark.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/.github/assets/vercel.svg b/.github/assets/vercel.svg
index d2b7685c5..6711aa56f 100644
--- a/.github/assets/vercel.svg
+++ b/.github/assets/vercel.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/examples/basics/package.json b/examples/basics/package.json
index 85b886d4b..311cb2931 100644
--- a/examples/basics/package.json
+++ b/examples/basics/package.json
@@ -11,6 +11,6 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^3.1.1"
+ "astro": "^3.1.2"
}
}
diff --git a/examples/blog/package.json b/examples/blog/package.json
index b2ad50da8..32741a476 100644
--- a/examples/blog/package.json
+++ b/examples/blog/package.json
@@ -14,6 +14,6 @@
"@astrojs/mdx": "^1.1.0",
"@astrojs/rss": "^3.0.0",
"@astrojs/sitemap": "^3.0.0",
- "astro": "^3.1.1"
+ "astro": "^3.1.2"
}
}
diff --git a/examples/blog/public/blog-placeholder-1.jpg b/examples/blog/public/blog-placeholder-1.jpg
index b4c20543e..74d4009b5 100644
Binary files a/examples/blog/public/blog-placeholder-1.jpg and b/examples/blog/public/blog-placeholder-1.jpg differ
diff --git a/examples/blog/public/blog-placeholder-2.jpg b/examples/blog/public/blog-placeholder-2.jpg
index a819b59c2..c4214b0e6 100644
Binary files a/examples/blog/public/blog-placeholder-2.jpg and b/examples/blog/public/blog-placeholder-2.jpg differ
diff --git a/examples/blog/public/blog-placeholder-3.jpg b/examples/blog/public/blog-placeholder-3.jpg
index 067802c0f..fbe2ac0cb 100644
Binary files a/examples/blog/public/blog-placeholder-3.jpg and b/examples/blog/public/blog-placeholder-3.jpg differ
diff --git a/examples/blog/public/blog-placeholder-4.jpg b/examples/blog/public/blog-placeholder-4.jpg
index 947e7eaab..f4fc88e29 100644
Binary files a/examples/blog/public/blog-placeholder-4.jpg and b/examples/blog/public/blog-placeholder-4.jpg differ
diff --git a/examples/blog/public/blog-placeholder-about.jpg b/examples/blog/public/blog-placeholder-about.jpg
index 7b4aafbec..cf5f68532 100644
Binary files a/examples/blog/public/blog-placeholder-about.jpg and b/examples/blog/public/blog-placeholder-about.jpg differ
diff --git a/examples/component/package.json b/examples/component/package.json
index fc0aa1e19..16b5ddf92 100644
--- a/examples/component/package.json
+++ b/examples/component/package.json
@@ -15,7 +15,7 @@
],
"scripts": {},
"devDependencies": {
- "astro": "^3.1.1"
+ "astro": "^3.1.2"
},
"peerDependencies": {
"astro": "^2.0.0-beta.0"
diff --git a/examples/deno/package.json b/examples/deno/package.json
index dbf19159e..d1f6b0fef 100644
--- a/examples/deno/package.json
+++ b/examples/deno/package.json
@@ -10,7 +10,7 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^3.1.1"
+ "astro": "^3.1.2"
},
"devDependencies": {
"@astrojs/deno": "^5.0.0"
diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json
index 34dfd5bb1..3c04a77e9 100644
--- a/examples/framework-alpine/package.json
+++ b/examples/framework-alpine/package.json
@@ -14,6 +14,6 @@
"@astrojs/alpinejs": "^0.3.0",
"@types/alpinejs": "^3.7.2",
"alpinejs": "^3.12.3",
- "astro": "^3.1.1"
+ "astro": "^3.1.2"
}
}
diff --git a/examples/framework-lit/package.json b/examples/framework-lit/package.json
index dc3b50539..ae14bd1f9 100644
--- a/examples/framework-lit/package.json
+++ b/examples/framework-lit/package.json
@@ -13,7 +13,7 @@
"dependencies": {
"@astrojs/lit": "^3.0.0",
"@webcomponents/template-shadowroot": "^0.2.1",
- "astro": "^3.1.1",
+ "astro": "^3.1.2",
"lit": "^2.8.0"
}
}
diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json
index c00d687e5..0ea2307fc 100644
--- a/examples/framework-multiple/package.json
+++ b/examples/framework-multiple/package.json
@@ -16,7 +16,7 @@
"@astrojs/solid-js": "^3.0.1",
"@astrojs/svelte": "^4.0.2",
"@astrojs/vue": "^3.0.0",
- "astro": "^3.1.1",
+ "astro": "^3.1.2",
"preact": "^10.17.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json
index 941a20c78..3cfa824f9 100644
--- a/examples/framework-preact/package.json
+++ b/examples/framework-preact/package.json
@@ -13,7 +13,7 @@
"dependencies": {
"@astrojs/preact": "^3.0.0",
"@preact/signals": "^1.2.1",
- "astro": "^3.1.1",
+ "astro": "^3.1.2",
"preact": "^10.17.1"
}
}
diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json
index f258a4ee6..ce8c0f8d9 100644
--- a/examples/framework-react/package.json
+++ b/examples/framework-react/package.json
@@ -14,7 +14,7 @@
"@astrojs/react": "^3.0.2",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
- "astro": "^3.1.1",
+ "astro": "^3.1.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json
index 101ce6424..587d086c1 100644
--- a/examples/framework-solid/package.json
+++ b/examples/framework-solid/package.json
@@ -12,7 +12,7 @@
},
"dependencies": {
"@astrojs/solid-js": "^3.0.1",
- "astro": "^3.1.1",
+ "astro": "^3.1.2",
"solid-js": "^1.7.11"
}
}
diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json
index 5b8426439..ffb7c047f 100644
--- a/examples/framework-svelte/package.json
+++ b/examples/framework-svelte/package.json
@@ -12,7 +12,7 @@
},
"dependencies": {
"@astrojs/svelte": "^4.0.2",
- "astro": "^3.1.1",
+ "astro": "^3.1.2",
"svelte": "^4.2.0"
}
}
diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json
index 13807d5aa..e1d7e60a3 100644
--- a/examples/framework-vue/package.json
+++ b/examples/framework-vue/package.json
@@ -12,7 +12,7 @@
},
"dependencies": {
"@astrojs/vue": "^3.0.0",
- "astro": "^3.1.1",
+ "astro": "^3.1.2",
"vue": "^3.3.4"
}
}
diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json
index 1624fca82..966db9df1 100644
--- a/examples/hackernews/package.json
+++ b/examples/hackernews/package.json
@@ -11,7 +11,7 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/node": "^6.0.0",
- "astro": "^3.1.1"
+ "@astrojs/node": "^6.0.1",
+ "astro": "^3.1.2"
}
}
diff --git a/examples/integration/package.json b/examples/integration/package.json
index 918b3ee79..0c5134723 100644
--- a/examples/integration/package.json
+++ b/examples/integration/package.json
@@ -15,7 +15,7 @@
],
"scripts": {},
"devDependencies": {
- "astro": "^3.1.1"
+ "astro": "^3.1.2"
},
"peerDependencies": {
"astro": "^2.0.0-beta.0"
diff --git a/examples/middleware/package.json b/examples/middleware/package.json
index aadcb936e..29476419a 100644
--- a/examples/middleware/package.json
+++ b/examples/middleware/package.json
@@ -12,8 +12,8 @@
"server": "node dist/server/entry.mjs"
},
"dependencies": {
- "@astrojs/node": "^6.0.0",
- "astro": "^3.1.1",
+ "@astrojs/node": "^6.0.1",
+ "astro": "^3.1.2",
"html-minifier": "^4.0.0"
}
}
diff --git a/examples/minimal/package.json b/examples/minimal/package.json
index d0afec388..f6509d12e 100644
--- a/examples/minimal/package.json
+++ b/examples/minimal/package.json
@@ -11,6 +11,6 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^3.1.1"
+ "astro": "^3.1.2"
}
}
diff --git a/examples/non-html-pages/package.json b/examples/non-html-pages/package.json
index 866aed7d4..2cc266069 100644
--- a/examples/non-html-pages/package.json
+++ b/examples/non-html-pages/package.json
@@ -11,6 +11,6 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^3.1.1"
+ "astro": "^3.1.2"
}
}
diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json
index c03444410..5bd0c74a3 100644
--- a/examples/portfolio/package.json
+++ b/examples/portfolio/package.json
@@ -11,6 +11,6 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^3.1.1"
+ "astro": "^3.1.2"
}
}
diff --git a/examples/portfolio/public/assets/portrait.jpg b/examples/portfolio/public/assets/portrait.jpg
index fce4bd355..f1c8984bd 100644
Binary files a/examples/portfolio/public/assets/portrait.jpg and b/examples/portfolio/public/assets/portrait.jpg differ
diff --git a/examples/portfolio/public/assets/stock-1.jpg b/examples/portfolio/public/assets/stock-1.jpg
index 5835b1464..c8dec6b96 100644
Binary files a/examples/portfolio/public/assets/stock-1.jpg and b/examples/portfolio/public/assets/stock-1.jpg differ
diff --git a/examples/portfolio/public/assets/stock-2.jpg b/examples/portfolio/public/assets/stock-2.jpg
index 0d49a725f..3ad4b7150 100644
Binary files a/examples/portfolio/public/assets/stock-2.jpg and b/examples/portfolio/public/assets/stock-2.jpg differ
diff --git a/examples/portfolio/public/assets/stock-3.jpg b/examples/portfolio/public/assets/stock-3.jpg
index b2bdff1b7..27068541c 100644
Binary files a/examples/portfolio/public/assets/stock-3.jpg and b/examples/portfolio/public/assets/stock-3.jpg differ
diff --git a/examples/ssr/package.json b/examples/ssr/package.json
index db4bedd88..4fe4216bd 100644
--- a/examples/ssr/package.json
+++ b/examples/ssr/package.json
@@ -12,9 +12,9 @@
"server": "node dist/server/entry.mjs"
},
"dependencies": {
- "@astrojs/node": "^6.0.0",
+ "@astrojs/node": "^6.0.1",
"@astrojs/svelte": "^4.0.2",
- "astro": "^3.1.1",
+ "astro": "^3.1.2",
"svelte": "^4.2.0"
}
}
diff --git a/examples/ssr/public/images/products/cereal.jpg b/examples/ssr/public/images/products/cereal.jpg
index c1f4cce4a..35601a789 100644
Binary files a/examples/ssr/public/images/products/cereal.jpg and b/examples/ssr/public/images/products/cereal.jpg differ
diff --git a/examples/ssr/public/images/products/muffins.jpg b/examples/ssr/public/images/products/muffins.jpg
index 897733ee8..ced2d9a91 100644
Binary files a/examples/ssr/public/images/products/muffins.jpg and b/examples/ssr/public/images/products/muffins.jpg differ
diff --git a/examples/ssr/public/images/products/oats.jpg b/examples/ssr/public/images/products/oats.jpg
index b8db72ae0..54ae1ebdb 100644
Binary files a/examples/ssr/public/images/products/oats.jpg and b/examples/ssr/public/images/products/oats.jpg differ
diff --git a/examples/ssr/public/images/products/yogurt.jpg b/examples/ssr/public/images/products/yogurt.jpg
index 9cd39666d..73c1b9a85 100644
Binary files a/examples/ssr/public/images/products/yogurt.jpg and b/examples/ssr/public/images/products/yogurt.jpg differ
diff --git a/examples/with-markdoc/package.json b/examples/with-markdoc/package.json
index 4fec42e94..91d86bbcc 100644
--- a/examples/with-markdoc/package.json
+++ b/examples/with-markdoc/package.json
@@ -12,6 +12,6 @@
},
"dependencies": {
"@astrojs/markdoc": "^0.5.0",
- "astro": "^3.1.1"
+ "astro": "^3.1.2"
}
}
diff --git a/examples/with-markdown-plugins/package.json b/examples/with-markdown-plugins/package.json
index fc8c83334..89ce661a9 100644
--- a/examples/with-markdown-plugins/package.json
+++ b/examples/with-markdown-plugins/package.json
@@ -12,7 +12,7 @@
},
"dependencies": {
"@astrojs/markdown-remark": "^3.2.0",
- "astro": "^3.1.1",
+ "astro": "^3.1.2",
"hast-util-select": "^5.0.5",
"rehype-autolink-headings": "^6.1.1",
"rehype-slug": "^5.1.0",
diff --git a/examples/with-markdown-shiki/package.json b/examples/with-markdown-shiki/package.json
index 3d7f11cca..abfe2fa13 100644
--- a/examples/with-markdown-shiki/package.json
+++ b/examples/with-markdown-shiki/package.json
@@ -11,6 +11,6 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^3.1.1"
+ "astro": "^3.1.2"
}
}
diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json
index facaf432e..b43afb47e 100644
--- a/examples/with-mdx/package.json
+++ b/examples/with-mdx/package.json
@@ -13,7 +13,7 @@
"dependencies": {
"@astrojs/mdx": "^1.1.0",
"@astrojs/preact": "^3.0.0",
- "astro": "^3.1.1",
+ "astro": "^3.1.2",
"preact": "^10.17.1"
}
}
diff --git a/examples/with-nanostores/package.json b/examples/with-nanostores/package.json
index 9efbc21cf..0889b23ba 100644
--- a/examples/with-nanostores/package.json
+++ b/examples/with-nanostores/package.json
@@ -13,7 +13,7 @@
"dependencies": {
"@astrojs/preact": "^3.0.0",
"@nanostores/preact": "^0.5.0",
- "astro": "^3.1.1",
+ "astro": "^3.1.2",
"nanostores": "^0.9.3",
"preact": "^10.17.1"
}
diff --git a/examples/with-nanostores/public/images/astronaut-figurine.png b/examples/with-nanostores/public/images/astronaut-figurine.png
index f5a278b9c..aac9b445e 100644
Binary files a/examples/with-nanostores/public/images/astronaut-figurine.png and b/examples/with-nanostores/public/images/astronaut-figurine.png differ
diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json
index 76b0d02e5..e2195bbd9 100644
--- a/examples/with-tailwindcss/package.json
+++ b/examples/with-tailwindcss/package.json
@@ -14,7 +14,7 @@
"@astrojs/mdx": "^1.1.0",
"@astrojs/tailwind": "^5.0.0",
"@types/canvas-confetti": "^1.6.0",
- "astro": "^3.1.1",
+ "astro": "^3.1.2",
"autoprefixer": "^10.4.15",
"canvas-confetti": "^1.6.0",
"postcss": "^8.4.28",
diff --git a/examples/with-vite-plugin-pwa/package.json b/examples/with-vite-plugin-pwa/package.json
index d8f176dc3..75c3916f4 100644
--- a/examples/with-vite-plugin-pwa/package.json
+++ b/examples/with-vite-plugin-pwa/package.json
@@ -11,7 +11,7 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^3.1.1",
+ "astro": "^3.1.2",
"vite-plugin-pwa": "0.16.4",
"workbox-window": "^7.0.0"
}
diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json
index d4fd8a86e..48ca02f98 100644
--- a/examples/with-vitest/package.json
+++ b/examples/with-vitest/package.json
@@ -12,7 +12,7 @@
"test": "vitest"
},
"dependencies": {
- "astro": "^3.1.1",
+ "astro": "^3.1.2",
"vitest": "^0.34.2"
}
}
diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md
index 1a9537e78..f4caccd22 100644
--- a/packages/astro/CHANGELOG.md
+++ b/packages/astro/CHANGELOG.md
@@ -1,5 +1,28 @@
# astro
+## 3.1.2
+
+### Patch Changes
+
+- [#8612](https://github.com/withastro/astro/pull/8612) [`bcad715ce`](https://github.com/withastro/astro/commit/bcad715ce67bc73a7927c941d1e7f02a82d638c2) Thanks [@matthewp](https://github.com/matthewp)! - Ensure cookies are attached when middleware changes the Response
+
+- [#8598](https://github.com/withastro/astro/pull/8598) [`bdd267d08`](https://github.com/withastro/astro/commit/bdd267d08937611984d074a2872af11ecf3e1a12) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fix relative images in Markdown breaking the build process in certain circumstances
+
+- [#8382](https://github.com/withastro/astro/pull/8382) [`e522a5eb4`](https://github.com/withastro/astro/commit/e522a5eb41c7df1e62c307c84cd14d53777439ff) Thanks [@DerTimonius](https://github.com/DerTimonius)! - Do not throw an error for an empty collection directory.
+
+- [#8600](https://github.com/withastro/astro/pull/8600) [`ed54d4644`](https://github.com/withastro/astro/commit/ed54d46449accc99ad117d6b0d50a8905e4d65d7) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Improve config info telemetry
+
+- [#8592](https://github.com/withastro/astro/pull/8592) [`70f2a8003`](https://github.com/withastro/astro/commit/70f2a80039d232731f63ea735e896997ec0eac7a) Thanks [@bluwy](https://github.com/bluwy)! - Fix alias plugin causing CSS ordering issue
+
+- [#8614](https://github.com/withastro/astro/pull/8614) [`4398e9298`](https://github.com/withastro/astro/commit/4398e929877dfadd2067af28413284afdfde9d8b) Thanks [@lilnasy](https://github.com/lilnasy)! - Fixed an issue where spaces and unicode characters in project path prevented middleware from running.
+
+- [#8603](https://github.com/withastro/astro/pull/8603) [`8f8b9069d`](https://github.com/withastro/astro/commit/8f8b9069ddd21cf57d37955ab3a92710492226f5) Thanks [@matthewp](https://github.com/matthewp)! - Prevent body scripts from re-executing on navigation
+
+- [#8609](https://github.com/withastro/astro/pull/8609) [`5a988eaf6`](https://github.com/withastro/astro/commit/5a988eaf609ddc1b9609acb0cdc2dda43d10a5c2) Thanks [@bluwy](https://github.com/bluwy)! - Fix Astro HMR from a CSS dependency
+
+- Updated dependencies [[`ed54d4644`](https://github.com/withastro/astro/commit/ed54d46449accc99ad117d6b0d50a8905e4d65d7)]:
+ - @astrojs/telemetry@3.0.2
+
## 3.1.1
### Patch Changes
diff --git a/packages/astro/components/ViewTransitions.astro b/packages/astro/components/ViewTransitions.astro
index 9c0a9dfdd..aa266af13 100644
--- a/packages/astro/components/ViewTransitions.astro
+++ b/packages/astro/components/ViewTransitions.astro
@@ -17,18 +17,26 @@ const { fallback = 'animate' } = Astro.props as Props;
index: number;
scrollX: number;
scrollY: number;
+ intraPage?: boolean;
};
type Events = 'astro:page-load' | 'astro:after-swap';
// only update history entries that are managed by us
// leave other entries alone and do not accidently add state.
const persistState = (state: State) => history.state && history.replaceState(state, '');
+ // @ts-expect-error: startViewTransition might exist
const supportsViewTransitions = !!document.startViewTransition;
const transitionEnabledOnThisPage = () =>
!!document.querySelector('[name="astro-view-transitions-enabled"]');
const triggerEvent = (name: Events) => document.dispatchEvent(new Event(name));
const onPageLoad = () => triggerEvent('astro:page-load');
const PERSIST_ATTR = 'data-astro-transition-persist';
+ const parser = new DOMParser();
+ // explained at its usage
+ let noopEl: HTMLDivElement;
+ if (import.meta.env.DEV) {
+ noopEl = document.createElement('div');
+ }
// The History API does not tell you if navigation is forward or back, so
// you can figure it using an index. On pushState the index is incremented so you
@@ -40,7 +48,7 @@ const { fallback = 'animate' } = Astro.props as Props;
currentHistoryIndex = history.state.index;
scrollTo({ left: history.state.scrollX, top: history.state.scrollY });
} else if (transitionEnabledOnThisPage()) {
- history.replaceState({ index: currentHistoryIndex, scrollX, scrollY }, '');
+ history.replaceState({ index: currentHistoryIndex, scrollX, scrollY, intraPage: false }, '');
}
const throttle = (cb: (...args: any[]) => any, delay: number) => {
let wait = false;
@@ -64,19 +72,28 @@ const { fallback = 'animate' } = Astro.props as Props;
};
};
- async function getHTML(href: string) {
+ // returns the contents of the page or null if the router can't deal with it.
+ async function fetchHTML(
+ href: string
+ ): Promise {
try {
const res = await fetch(href);
+ // drop potential charset (+ other name/value pairs) as parser needs the mediaType
+ const mediaType = res.headers.get('content-type')?.replace(/;.*$/, '');
+ // the DOMParser can handle two types of HTML
+ if (mediaType !== 'text/html' && mediaType !== 'application/xhtml+xml') {
+ // everything else (e.g. audio/mp3) will be handled by the browser but not by us
+ return null;
+ }
const html = await res.text();
return {
- ok: res.ok,
html,
redirected: res.redirected ? res.url : undefined,
- // drop potential charset (+ other name/value pairs) as parser needs the mediaType
- mediaType: res.headers.get('content-type')?.replace(/;.*$/, ''),
+ mediaType,
};
} catch (err) {
- return { ok: false };
+ // can't fetch, let someone else deal with it.
+ return null;
}
}
@@ -98,19 +115,19 @@ const { fallback = 'animate' } = Astro.props as Props;
let wait = Promise.resolve();
for (const script of Array.from(document.scripts)) {
if (script.dataset.astroExec === '') continue;
- const s = document.createElement('script');
- s.innerHTML = script.innerHTML;
+ const newScript = document.createElement('script');
+ newScript.innerHTML = script.innerHTML;
for (const attr of script.attributes) {
if (attr.name === 'src') {
const p = new Promise((r) => {
- s.onload = r;
+ newScript.onload = r;
});
wait = wait.then(() => p as any);
}
- s.setAttribute(attr.name, attr.value);
+ newScript.setAttribute(attr.name, attr.value);
}
- s.dataset.astroExec = '';
- script.replaceWith(s);
+ newScript.dataset.astroExec = '';
+ script.replaceWith(newScript);
}
return wait;
}
@@ -122,46 +139,60 @@ const { fallback = 'animate' } = Astro.props as Props;
return style.animationIterationCount === 'infinite';
}
- const parser = new DOMParser();
+ const updateHistoryAndScrollPosition = (toLocation) => {
+ if (toLocation.href !== location.href) {
+ history.pushState(
+ { index: ++currentHistoryIndex, scrollX: 0, scrollY: 0 },
+ '',
+ toLocation.href
+ );
+ // now we are on the new page for non-history navigations!
+ // (with history navigation page change happens before popstate is fired)
+ }
+ // freshly loaded pages start from the top
+ scrollTo({ left: 0, top: 0, behavior: 'instant' });
- // A noop element used to prevent styles from being removed
- if (import.meta.env.DEV) {
- var noopEl = document.createElement('div');
- }
+ if (toLocation.hash) {
+ // because we are already on the target page ...
+ // ... what comes next is a intra-page navigation
+ // that won't reload the page but instead scroll to the fragment
+ location.href = toLocation.href;
+ }
+ };
- async function updateDOM(doc: Document, loc: URL, state?: State, fallback?: Fallback) {
- // Check for a head element that should persist, either because it has the data
- // attribute or is a link el.
+ // replace head and body of the windows document with contents from newDocument
+ // if !popstate, update the history entry and scroll position according to toLocation
+ // if popState is given, this holds the scroll position for history navigation
+ // if fallback === "animate" then simulate view transitions
+ async function updateDOM(
+ newDocument: Document,
+ toLocation: URL,
+ popState?: State,
+ fallback?: Fallback
+ ) {
+ // Check for a head element that should persist and returns it,
+ // either because it has the data attribute or is a link el.
const persistedHeadElement = (el: HTMLElement): Element | null => {
const id = el.getAttribute(PERSIST_ATTR);
- const newEl = id && doc.head.querySelector(`[${PERSIST_ATTR}="${id}"]`);
+ const newEl = id && newDocument.head.querySelector(`[${PERSIST_ATTR}="${id}"]`);
if (newEl) {
return newEl;
}
if (el.matches('link[rel=stylesheet]')) {
const href = el.getAttribute('href');
- return doc.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
+ return newDocument.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
}
- if (el.tagName === 'SCRIPT') {
- let s1 = el as HTMLScriptElement;
- for (const s2 of doc.scripts) {
- if (
- // Inline
- (s1.textContent && s1.textContent === s2.textContent) ||
- // External
- (s1.type === s2.type && s1.src === s2.src)
- ) {
- return s2;
- }
- }
- }
- // Only run this in dev. This will get stripped from production builds and is not needed.
+ // What follows is a fix for an issue (#8472) with missing client:only styles after transition.
+ // That problem exists only in dev mode where styles are injected into the page by Vite.
+ // Returning a noop element ensures that the styles are not removed from the old document.
+ // Guarding the code below with the dev mode check
+ // allows tree shaking to remove this code in production.
if (import.meta.env.DEV) {
if (el.tagName === 'STYLE' && el.dataset.viteDevId) {
const devId = el.dataset.viteDevId;
// If this same style tag exists, remove it from the new page
return (
- doc.querySelector(`style[data-astro-dev-id="${devId}"]`) ||
+ newDocument.querySelector(`style[data-astro-dev-id="${devId}"]`) ||
// Otherwise, keep it anyways. This is client:only styles.
noopEl
);
@@ -171,10 +202,6 @@ const { fallback = 'animate' } = Astro.props as Props;
};
const swap = () => {
- // noscript tags inside head element are not honored on swap (#7969).
- // Remove them before swapping.
- doc.querySelectorAll('head noscript').forEach((el) => el.remove());
-
// swap attributes of the html element
// - delete all attributes from the current document
// - insert all attributes from doc
@@ -183,10 +210,26 @@ const { fallback = 'animate' } = Astro.props as Props;
const astro = [...html.attributes].filter(
({ name }) => (html.removeAttribute(name), name.startsWith('data-astro-'))
);
- [...doc.documentElement.attributes, ...astro].forEach(({ name, value }) =>
+ [...newDocument.documentElement.attributes, ...astro].forEach(({ name, value }) =>
html.setAttribute(name, value)
);
+ // Replace scripts in both the head and body.
+ for (const s1 of document.scripts) {
+ for (const s2 of newDocument.scripts) {
+ if (
+ // Inline
+ (s1.textContent && s1.textContent === s2.textContent) ||
+ // External
+ (s1.type === s2.type && s1.src === s2.src)
+ ) {
+ s2.remove();
+ } else {
+ s1.remove();
+ }
+ }
+ }
+
// Swap head
for (const el of Array.from(document.head.children)) {
const newEl = persistedHeadElement(el as HTMLElement);
@@ -199,12 +242,15 @@ const { fallback = 'animate' } = Astro.props as Props;
el.remove();
}
}
+
// Everything left in the new head is new, append it all.
- document.head.append(...doc.head.children);
+ document.head.append(...newDocument.head.children);
// Persist elements in the existing body
const oldBody = document.body;
- document.body.replaceWith(doc.body);
+
+ // this will reset scroll Position
+ document.body.replaceWith(newDocument.body);
for (const el of oldBody.querySelectorAll(`[${PERSIST_ATTR}]`)) {
const id = el.getAttribute(PERSIST_ATTR);
const newEl = document.querySelector(`[${PERSIST_ATTR}="${id}"]`);
@@ -215,39 +261,18 @@ const { fallback = 'animate' } = Astro.props as Props;
}
}
- // Simulate scroll behavior of Safari and
- // Chromium based browsers (Chrome, Edge, Opera, ...)
- scrollTo({ left: 0, top: 0, behavior: 'instant' });
-
- let initialScrollX = 0;
- let initialScrollY = 0;
- if (!state && loc.hash) {
- const id = decodeURIComponent(loc.hash.slice(1));
- const elem = document.getElementById(id);
- // prefer scrollIntoView() over scrollTo() because it takes scroll-padding into account
- if (elem) {
- elem.scrollIntoView();
- initialScrollX = Math.max(
- 0,
- elem.offsetLeft + elem.offsetWidth - document.documentElement.clientWidth
- );
- initialScrollY = elem.offsetTop;
- }
- } else if (state) {
- scrollTo(state.scrollX, state.scrollY); // usings default scrollBehavior
+ if (popState) {
+ scrollTo(popState.scrollX, popState.scrollY); // usings 'auto' scrollBehavior
+ } else {
+ updateHistoryAndScrollPosition(toLocation);
}
- !state &&
- history.pushState(
- { index: ++currentHistoryIndex, scrollX: initialScrollX, scrollY: initialScrollY },
- '',
- loc.href
- );
+
triggerEvent('astro:after-swap');
};
// Wait on links to finish, to prevent FOUC
const links: Promise[] = [];
- for (const el of doc.querySelectorAll('head link[rel=stylesheet]')) {
+ for (const el of newDocument.querySelectorAll('head link[rel=stylesheet]')) {
// Do not preload links that are already on the page.
if (
!document.querySelector(
@@ -287,32 +312,44 @@ const { fallback = 'animate' } = Astro.props as Props;
}
}
- async function navigate(dir: Direction, loc: URL, state?: State) {
+ async function transition(direction: Direction, toLocation: URL, popState?: State) {
let finished: Promise;
- const href = loc.href;
- const { html, ok, mediaType, redirected } = await getHTML(href);
- // if there was a redirection, show the final URL in the browser's address bar
- redirected && (loc = new URL(redirected));
+ const href = toLocation.href;
+ const response = await fetchHTML(href);
// If there is a problem fetching the new page, just do an MPA navigation to it.
- if (!ok || !(mediaType === 'text/html' || mediaType === 'application/xhtml+xml')) {
+ if (response === null) {
+ location.href = href;
+ return;
+ }
+ // if there was a redirection, show the final URL in the browser's address bar
+ if (response.redirected) {
+ toLocation = new URL(response.redirected);
+ }
+
+ const newDocument = parser.parseFromString(response.html, response.mediaType);
+ // The next line might look like a hack,
+ // but it is actually necessary as noscript elements
+ // and their contents are returned as markup by the parser,
+ // see https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString
+ newDocument.querySelectorAll('noscript').forEach((el) => el.remove());
+
+ if (!newDocument.querySelector('[name="astro-view-transitions-enabled"]')) {
location.href = href;
return;
}
- const doc = parser.parseFromString(html, mediaType);
- if (!doc.querySelector('[name="astro-view-transitions-enabled"]')) {
- location.href = href;
- return;
+ if (!popState) {
+ // save the current scroll position before we change the DOM and transition to the new page
+ history.replaceState({ ...history.state, scrollX, scrollY }, '');
}
-
- // Now we are sure that we will push state, and it is time to create a state if it is still missing.
- !state && history.replaceState({ index: currentHistoryIndex, scrollX, scrollY }, '');
-
- document.documentElement.dataset.astroTransition = dir;
+ document.documentElement.dataset.astroTransition = direction;
if (supportsViewTransitions) {
- finished = document.startViewTransition(() => updateDOM(doc, loc, state)).finished;
+ // @ts-expect-error: startViewTransition exist
+ finished = document.startViewTransition(() =>
+ updateDOM(newDocument, toLocation, popState)
+ ).finished;
} else {
- finished = updateDOM(doc, loc, state, getFallback());
+ finished = updateDOM(newDocument, toLocation, popState, getFallback());
}
try {
await finished;
@@ -328,7 +365,9 @@ const { fallback = 'animate' } = Astro.props as Props;
// Prefetching
function maybePrefetch(pathname: string) {
if (document.querySelector(`link[rel=prefetch][href="${pathname}"]`)) return;
+ // @ts-expect-error: connection might exist
if (navigator.connection) {
+ // @ts-expect-error: connection does exist
let conn = navigator.connection;
if (conn.saveData || /(2|3)g/.test(conn.effectiveType || '')) return;
}
@@ -339,8 +378,6 @@ const { fallback = 'animate' } = Astro.props as Props;
}
if (supportsViewTransitions || getFallback() !== 'none') {
- markScriptsExec();
-
document.addEventListener('click', (ev) => {
let link = ev.target;
if (link instanceof Element && link.tagName !== 'A') {
@@ -362,51 +399,59 @@ const { fallback = 'animate' } = Astro.props as Props;
ev.ctrlKey || // new tab (windows)
ev.altKey || // download
ev.shiftKey || // new window
- ev.defaultPrevented ||
- !transitionEnabledOnThisPage()
+ ev.defaultPrevented
) {
// No page transitions in these cases,
// Let the browser standard action handle this
return;
}
- // We do not need to handle same page links because there are no page transitions
- // Same page means same path and same query params (but different hash)
- if (location.pathname === link.pathname && location.search === link.search) {
- if (link.hash) {
- // The browser default action will handle navigations with hash fragments
- return;
- } else {
- // Special case: self link without hash
- // If handed to the browser it will reload the page
- // But we want to handle it like any other same page navigation
- // So we scroll to the top of the page but do not start page transitions
- ev.preventDefault();
- // push state on the first navigation but not if we were here already
- if (location.hash) {
- history.replaceState(
- { index: currentHistoryIndex, scrollX, scrollY: -(scrollY + 1) },
- ''
- );
- const newState: State = { index: ++currentHistoryIndex, scrollX: 0, scrollY: 0 };
- history.pushState(newState, '', link.href);
- }
- scrollTo({ left: 0, top: 0, behavior: 'instant' });
- return;
- }
- }
-
- // these are the cases we will handle: same origin, different page
ev.preventDefault();
- navigate('forward', new URL(link.href));
+ navigate(link.href);
});
+ function navigate(href) {
+ // not ours
+ if (!transitionEnabledOnThisPage()) {
+ location.href = href;
+ return;
+ }
+ const toLocation = new URL(href, location.href);
+ // We do not have page transitions on navigations to the same page (intra-page navigation)
+ // but we want to handle prevent reload on navigation to the same page
+ // Same page means same origin, path and query params (but maybe different hash)
+ if (
+ location.origin === toLocation.origin &&
+ location.pathname === toLocation.pathname &&
+ location.search === toLocation.search
+ ) {
+ // mark current position as non transition intra-page scrolling
+ if (location.href !== toLocation.href) {
+ history.replaceState({ ...history.state, intraPage: true }, '');
+ history.pushState(
+ { index: ++currentHistoryIndex, scrollX: 0, scrollY: 0 },
+ '',
+ toLocation.href
+ );
+ }
+ if (toLocation.hash) {
+ location.href = toLocation.href;
+ } else {
+ scrollTo({ left: 0, top: 0, behavior: 'instant' });
+ }
+ } else {
+ transition('forward', toLocation);
+ }
+ }
+
addEventListener('popstate', (ev) => {
if (!transitionEnabledOnThisPage() && ev.state) {
// The current page doesn't have View Transitions enabled
// but the page we navigate to does (because it set the state).
// Do a full page refresh to reload the client-side router from the new page.
// Scroll restauration will then happen during the reload when the router's code is re-executed
- history.scrollRestoration && (history.scrollRestoration = 'manual');
+ if (history.scrollRestoration) {
+ history.scrollRestoration = 'manual';
+ }
location.reload();
return;
}
@@ -429,13 +474,14 @@ const { fallback = 'animate' } = Astro.props as Props;
}
const state: State = history.state;
- const nextIndex = state.index;
- const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back';
- currentHistoryIndex = nextIndex;
- if (state.scrollY < 0) {
- scrollTo(state.scrollX, -(state.scrollY + 1));
+ if (state.intraPage) {
+ // this is non transition intra-page scrolling
+ scrollTo(state.scrollX, state.scrollY);
} else {
- navigate(direction, new URL(location.href), state);
+ const nextIndex = state.index;
+ const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back';
+ currentHistoryIndex = nextIndex;
+ transition(direction, new URL(location.href), state);
}
});
@@ -457,6 +503,7 @@ const { fallback = 'animate' } = Astro.props as Props;
{ passive: true, capture: true }
);
});
+
addEventListener('load', onPageLoad);
// There's not a good way to record scroll position before a back button.
// So the way we do it is by listening to scrollend if supported, and if not continuously record the scroll position.
@@ -466,5 +513,7 @@ const { fallback = 'animate' } = Astro.props as Props;
if ('onscrollend' in window) addEventListener('scrollend', updateState);
else addEventListener('scroll', throttle(updateState, 300));
+
+ markScriptsExec();
}
diff --git a/packages/astro/e2e/astro-component.test.js b/packages/astro/e2e/astro-component.test.js
index 7308ea292..c96e9b1c4 100644
--- a/packages/astro/e2e/astro-component.test.js
+++ b/packages/astro/e2e/astro-component.test.js
@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
-import { getColor, testFactory } from './test-utils.js';
+import { testFactory } from './test-utils.js';
const test = testFactory({ root: './fixtures/astro-component/' });
@@ -99,7 +99,7 @@ test.describe('Astro component HMR', () => {
test('update linked dep Astro style', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
let h1 = page.locator('#astro-linked-lib');
- expect(await getColor(h1)).toBe('rgb(255, 0, 0)');
+ await expect(h1).toHaveCSS('color', 'rgb(255, 0, 0)');
await Promise.all([
page.waitForLoadState('networkidle'),
await astro.editFile('../_deps/astro-linked-lib/Component.astro', (content) =>
@@ -107,6 +107,6 @@ test.describe('Astro component HMR', () => {
),
]);
h1 = page.locator('#astro-linked-lib');
- expect(await getColor(h1)).toBe('rgb(0, 128, 0)');
+ await expect(h1).toHaveCSS('color', 'rgb(0, 128, 0)');
});
});
diff --git a/packages/astro/e2e/css.test.js b/packages/astro/e2e/css.test.js
index 184e5dba3..b302d9d90 100644
--- a/packages/astro/e2e/css.test.js
+++ b/packages/astro/e2e/css.test.js
@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
-import { getColor, testFactory } from './test-utils.js';
+import { testFactory } from './test-utils.js';
const test = testFactory({
root: './fixtures/css/',
@@ -20,13 +20,13 @@ test.describe('CSS HMR', () => {
await page.goto(astro.resolveUrl('/'));
const h = page.locator('h1');
- expect(await getColor(h)).toBe('rgb(255, 0, 0)');
+ await expect(h).toHaveCSS('color', 'rgb(255, 0, 0)');
await astro.editFile('./src/styles/main.css', (original) =>
original.replace('--h1-color: red;', '--h1-color: green;')
);
- expect(await getColor(h)).toBe('rgb(0, 128, 0)');
+ await expect(h).toHaveCSS('color', 'rgb(0, 128, 0)');
});
test('removes Astro-injected CSS once Vite-injected CSS loads', async ({ page, astro }) => {
diff --git a/packages/astro/e2e/fixtures/invalidate-script-deps/package.json b/packages/astro/e2e/fixtures/hmr/package.json
similarity index 50%
rename from packages/astro/e2e/fixtures/invalidate-script-deps/package.json
rename to packages/astro/e2e/fixtures/hmr/package.json
index 4b45ad505..f5aa41460 100644
--- a/packages/astro/e2e/fixtures/invalidate-script-deps/package.json
+++ b/packages/astro/e2e/fixtures/hmr/package.json
@@ -1,8 +1,9 @@
{
- "name": "@e2e/invalidate-script-deps",
+ "name": "@e2e/hmr",
"version": "0.0.0",
"private": true,
"devDependencies": {
- "astro": "workspace:*"
+ "astro": "workspace:*",
+ "sass": "^1.66.1"
}
}
diff --git a/packages/astro/e2e/fixtures/hmr/src/pages/css-dep.astro b/packages/astro/e2e/fixtures/hmr/src/pages/css-dep.astro
new file mode 100644
index 000000000..38f4e7409
--- /dev/null
+++ b/packages/astro/e2e/fixtures/hmr/src/pages/css-dep.astro
@@ -0,0 +1,14 @@
+
+
+ Test
+
+
+ This is blue
+
+
+
diff --git a/packages/astro/e2e/fixtures/invalidate-script-deps/src/pages/index.astro b/packages/astro/e2e/fixtures/hmr/src/pages/script-dep.astro
similarity index 100%
rename from packages/astro/e2e/fixtures/invalidate-script-deps/src/pages/index.astro
rename to packages/astro/e2e/fixtures/hmr/src/pages/script-dep.astro
diff --git a/packages/astro/e2e/fixtures/invalidate-script-deps/src/scripts/heading.js b/packages/astro/e2e/fixtures/hmr/src/scripts/heading.js
similarity index 100%
rename from packages/astro/e2e/fixtures/invalidate-script-deps/src/scripts/heading.js
rename to packages/astro/e2e/fixtures/hmr/src/scripts/heading.js
diff --git a/packages/astro/e2e/fixtures/hmr/src/styles/vars.scss b/packages/astro/e2e/fixtures/hmr/src/styles/vars.scss
new file mode 100644
index 000000000..5deae109f
--- /dev/null
+++ b/packages/astro/e2e/fixtures/hmr/src/styles/vars.scss
@@ -0,0 +1 @@
+$color: blue;
\ No newline at end of file
diff --git a/packages/astro/e2e/fixtures/view-transitions/public/logo.svg b/packages/astro/e2e/fixtures/view-transitions/public/logo.svg
index e9c63b295..8a5f0edd8 100644
--- a/packages/astro/e2e/fixtures/view-transitions/public/logo.svg
+++ b/packages/astro/e2e/fixtures/view-transitions/public/logo.svg
@@ -1,13 +1 @@
-
+
\ No newline at end of file
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/assets/penguin.jpg b/packages/astro/e2e/fixtures/view-transitions/src/assets/penguin.jpg
index 9e859269c..b292daad5 100644
Binary files a/packages/astro/e2e/fixtures/view-transitions/src/assets/penguin.jpg and b/packages/astro/e2e/fixtures/view-transitions/src/assets/penguin.jpg differ
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/components/InlineScript.astro b/packages/astro/e2e/fixtures/view-transitions/src/components/InlineScript.astro
new file mode 100644
index 000000000..5418c5a64
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/src/components/InlineScript.astro
@@ -0,0 +1,9 @@
+Count
+
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/four.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/four.astro
index c6547dc20..82dda9475 100644
--- a/packages/astro/e2e/fixtures/view-transitions/src/pages/four.astro
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/four.astro
@@ -11,4 +11,5 @@ import Layout from '../components/Layout.astro';
load page / no navigation
load page / no navigation
+ load page / no navigation
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/inline-script-one.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/inline-script-one.astro
new file mode 100644
index 000000000..e887fe6a5
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/inline-script-one.astro
@@ -0,0 +1,8 @@
+---
+import Layout from '../components/Layout.astro';
+import InlineScript from '../components/InlineScript.astro';
+---
+
+
+ Go to 2
+
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/inline-script-two.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/inline-script-two.astro
new file mode 100644
index 000000000..430ad9465
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/inline-script-two.astro
@@ -0,0 +1,8 @@
+---
+import Layout from '../components/Layout.astro';
+import InlineScript from '../components/InlineScript.astro';
+---
+
+
+ Go to 1
+
diff --git a/packages/astro/e2e/invalidate-script-deps.test.js b/packages/astro/e2e/hmr.test.js
similarity index 57%
rename from packages/astro/e2e/invalidate-script-deps.test.js
rename to packages/astro/e2e/hmr.test.js
index fe37ece8f..091aa716d 100644
--- a/packages/astro/e2e/invalidate-script-deps.test.js
+++ b/packages/astro/e2e/hmr.test.js
@@ -2,7 +2,7 @@ import { expect } from '@playwright/test';
import { testFactory } from './test-utils.js';
const test = testFactory({
- root: './fixtures/invalidate-script-deps/',
+ root: './fixtures/hmr/',
});
let devServer;
@@ -17,7 +17,7 @@ test.afterAll(async () => {
test.describe('Scripts with dependencies', () => {
test('refresh with HMR', async ({ page, astro }) => {
- await page.goto(astro.resolveUrl('/'));
+ await page.goto(astro.resolveUrl('/script-dep'));
const h = page.locator('h1');
await expect(h, 'original text set').toHaveText('before');
@@ -29,3 +29,16 @@ test.describe('Scripts with dependencies', () => {
await expect(h, 'text changed').toHaveText('after');
});
});
+
+test.describe('Styles with dependencies', () => {
+ test('refresh with HMR', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/css-dep'));
+
+ const h = page.locator('h1');
+ await expect(h).toHaveCSS('color', 'rgb(0, 0, 255)');
+
+ await astro.editFile('./src/styles/vars.scss', (original) => original.replace('blue', 'red'));
+
+ await expect(h).toHaveCSS('color', 'rgb(255, 0, 0)');
+ });
+});
diff --git a/packages/astro/e2e/test-utils.js b/packages/astro/e2e/test-utils.js
index 0768bff81..79b7601b7 100644
--- a/packages/astro/e2e/test-utils.js
+++ b/packages/astro/e2e/test-utils.js
@@ -71,13 +71,6 @@ export async function getErrorOverlayContent(page) {
return { message, hint, absoluteFileLocation, fileLocation };
}
-/**
- * @returns {Promise}
- */
-export async function getColor(el) {
- return await el.evaluate((e) => getComputedStyle(e).color);
-}
-
/**
* Wait for `astro-island` that contains the `el` to hydrate
* @param {import('@playwright/test').Page} page
diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js
index 868660f9f..b06d5a988 100644
--- a/packages/astro/e2e/view-transitions.test.js
+++ b/packages/astro/e2e/view-transitions.test.js
@@ -293,12 +293,12 @@ test.describe('View Transitions', () => {
locator = page.locator('#click-one-again');
await expect(locator).toBeInViewport();
- // Scroll up to top fragment
+ // goto page 1
await page.click('#click-one-again');
locator = page.locator('#one');
await expect(locator).toHaveText('Page 1');
- // Back to middle of the page
+ // Back to middle of the previous page
await page.goBack();
locator = page.locator('#click-one-again');
await expect(locator).toBeInViewport();
@@ -520,6 +520,22 @@ test.describe('View Transitions', () => {
await downloadPromise;
});
+ test('data-astro-reload not required for non-html content', async ({ page, astro }) => {
+ const loads = [];
+ page.addListener('load', (p) => {
+ loads.push(p.title());
+ });
+ // Go to page 4
+ await page.goto(astro.resolveUrl('/four'));
+ let p = page.locator('#four');
+ await expect(p, 'should have content').toHaveText('Page 4');
+
+ await page.click('#click-svg');
+ p = page.locator('svg');
+ await expect(p).toBeVisible();
+ expect(loads.length, 'There should be 2 page load').toEqual(2);
+ });
+
test('Scroll position is restored on back navigation from page w/o ViewTransitions', async ({
page,
astro,
@@ -663,4 +679,22 @@ test.describe('View Transitions', () => {
locator = page.locator('#click-one');
await expect(locator).not.toBeInViewport();
});
+
+ test('body inline scripts do not re-execute on navigation', async ({ page, astro }) => {
+ const errors = [];
+ page.addListener('pageerror', (err) => {
+ errors.push(err);
+ });
+
+ await page.goto(astro.resolveUrl('/inline-script-one'));
+ let article = page.locator('#counter');
+ await expect(article, 'should have script content').toBeVisible('exists');
+
+ await page.click('#click-one');
+
+ article = page.locator('#counter');
+ await expect(article, 'should have script content').toHaveText('Count: 3');
+
+ expect(errors).toHaveLength(0);
+ });
});
diff --git a/packages/astro/package.json b/packages/astro/package.json
index f3fa8cf52..667dcf9bb 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -1,6 +1,6 @@
{
"name": "astro",
- "version": "3.1.1",
+ "version": "3.1.2",
"description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
"type": "module",
"author": "withastro",
diff --git a/packages/astro/src/assets/services/squoosh.ts b/packages/astro/src/assets/services/squoosh.ts
index cbee58336..32aee874d 100644
--- a/packages/astro/src/assets/services/squoosh.ts
+++ b/packages/astro/src/assets/services/squoosh.ts
@@ -29,8 +29,11 @@ const qualityTable: Record<
// Squoosh's PNG encoder does not support a quality setting, so we can skip that here
};
-async function getRotationForEXIF(inputBuffer: Buffer): Promise {
- const meta = await imageMetadata(inputBuffer);
+async function getRotationForEXIF(
+ inputBuffer: Buffer,
+ src?: string
+): Promise {
+ const meta = await imageMetadata(inputBuffer, src);
if (!meta) return undefined;
// EXIF orientations are a bit hard to read, but the numbers are actually standard. See https://exiftool.org/TagNames/EXIF.html for a list.
@@ -64,7 +67,7 @@ const service: LocalImageService = {
const operations: Operation[] = [];
- const rotation = await getRotationForEXIF(inputBuffer);
+ const rotation = await getRotationForEXIF(inputBuffer, transform.src);
if (rotation) {
operations.push(rotation);
diff --git a/packages/astro/src/assets/utils/emitAsset.ts b/packages/astro/src/assets/utils/emitAsset.ts
index 9b83a020a..b9ca146b7 100644
--- a/packages/astro/src/assets/utils/emitAsset.ts
+++ b/packages/astro/src/assets/utils/emitAsset.ts
@@ -22,11 +22,7 @@ export async function emitESMImage(
return undefined;
}
- const fileMetadata = await imageMetadata(fileData);
-
- if (!fileMetadata) {
- return undefined;
- }
+ const fileMetadata = await imageMetadata(fileData, id);
const emittedImage: ImageMetadata = {
src: '',
diff --git a/packages/astro/src/assets/utils/metadata.ts b/packages/astro/src/assets/utils/metadata.ts
index 7d7ee7457..fc89ca1ca 100644
--- a/packages/astro/src/assets/utils/metadata.ts
+++ b/packages/astro/src/assets/utils/metadata.ts
@@ -1,19 +1,23 @@
import probe from 'probe-image-size';
+import { AstroError, AstroErrorData } from '../../core/errors/index.js';
import type { ImageInputFormat, ImageMetadata } from '../types.js';
-export async function imageMetadata(data: Buffer): Promise | undefined> {
+export async function imageMetadata(
+ data: Buffer,
+ src?: string
+): Promise> {
const result = probe.sync(data);
+
if (result === null) {
- throw new Error('Failed to probe image size.');
+ throw new AstroError({
+ ...AstroErrorData.NoImageMetadata,
+ message: AstroErrorData.NoImageMetadata.message(src),
+ });
}
const { width, height, type, orientation } = result;
const isPortrait = (orientation || 0) >= 5;
- if (!width || !height || !type) {
- return undefined;
- }
-
return {
width: isPortrait ? height : width,
height: isPortrait ? width : height,
diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts
index cea8e068c..2be2f5586 100644
--- a/packages/astro/src/assets/vite-plugin-assets.ts
+++ b/packages/astro/src/assets/vite-plugin-assets.ts
@@ -2,6 +2,8 @@ import MagicString from 'magic-string';
import type * as vite from 'vite';
import { normalizePath } from 'vite';
import type { AstroPluginOptions, ImageTransform } from '../@types/astro.js';
+import { extendManualChunks } from '../core/build/plugins/util.js';
+import { AstroError, AstroErrorData } from '../core/errors/index.js';
import {
appendForwardSlash,
joinPaths,
@@ -28,6 +30,18 @@ export default function assets({
// Expose the components and different utilities from `astro:assets` and handle serving images from `/_image` in dev
{
name: 'astro:assets',
+ outputOptions(outputOptions) {
+ // Specifically split out chunk for asset files to prevent TLA deadlock
+ // caused by `getImage()` for markdown components.
+ // https://github.com/rollup/rollup/issues/4708
+ extendManualChunks(outputOptions, {
+ after(id) {
+ if (id.includes('astro/dist/assets/services/')) {
+ return `astro-assets-services`;
+ }
+ },
+ });
+ },
async resolveId(id) {
if (id === VIRTUAL_SERVICE_ID) {
return await this.resolve(settings.config.image.service.entrypoint);
@@ -126,6 +140,14 @@ export default function assets({
}
if (assetRegex.test(id)) {
const meta = await emitESMImage(id, this.meta.watchMode, this.emitFile);
+
+ if (!meta) {
+ throw new AstroError({
+ ...AstroErrorData.ImageNotFound,
+ message: AstroErrorData.ImageNotFound.message(id),
+ });
+ }
+
return `export default ${JSON.stringify(meta)}`;
}
},
diff --git a/packages/astro/src/cli/add/index.ts b/packages/astro/src/cli/add/index.ts
index 62cec7a71..afd63716b 100644
--- a/packages/astro/src/cli/add/index.ts
+++ b/packages/astro/src/cli/add/index.ts
@@ -9,7 +9,12 @@ import ora from 'ora';
import preferredPM from 'preferred-pm';
import prompts from 'prompts';
import type yargs from 'yargs-parser';
-import { loadTSConfig, resolveConfigPath, resolveRoot } from '../../core/config/index.js';
+import {
+ loadTSConfig,
+ resolveConfig,
+ resolveConfigPath,
+ resolveRoot,
+} from '../../core/config/index.js';
import {
defaultTSConfig,
presets,
@@ -23,7 +28,7 @@ import { appendForwardSlash } from '../../core/path.js';
import { apply as applyPolyfill } from '../../core/polyfill.js';
import { parseNpmName } from '../../core/util.js';
import { eventCliSession, telemetry } from '../../events/index.js';
-import { createLoggerFromFlags } from '../flags.js';
+import { createLoggerFromFlags, flagsToAstroInlineConfig } from '../flags.js';
import { generate, parse, t, visit } from './babel.js';
import { ensureImport } from './imports.js';
import { wrapDefaultExport } from './wrapper.js';
@@ -87,7 +92,9 @@ async function getRegistry(): Promise {
}
export async function add(names: string[], { flags }: AddOptions) {
- telemetry.record(eventCliSession('add'));
+ const inlineConfig = flagsToAstroInlineConfig(flags);
+ const { userConfig } = await resolveConfig(inlineConfig, 'add');
+ telemetry.record(eventCliSession('add', userConfig));
applyPolyfill();
if (flags.help || names.length === 0) {
printHelp({
diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts
index 7b8654ee7..eeaa60e6c 100644
--- a/packages/astro/src/content/runtime.ts
+++ b/packages/astro/src/content/runtime.ts
@@ -1,5 +1,6 @@
import type { MarkdownHeading } from '@astrojs/markdown-remark';
import { ZodIssueCode, string as zodString } from 'zod';
+import type { AstroIntegration } from '../@types/astro.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { prependForwardSlash } from '../core/path.js';
import {
@@ -55,10 +56,7 @@ export function createGetCollection({
} else if (collection in dataCollectionToEntryMap) {
type = 'data';
} else {
- throw new AstroError({
- ...AstroErrorData.CollectionDoesNotExistError,
- message: AstroErrorData.CollectionDoesNotExistError.message(collection),
- });
+ return warnOfEmptyCollection(collection);
}
const lazyImports = Object.values(
type === 'content'
@@ -392,3 +390,16 @@ type PropagatedAssetsModule = {
function isPropagatedAssetsModule(module: any): module is PropagatedAssetsModule {
return typeof module === 'object' && module != null && '__astroPropagation' in module;
}
+
+function warnOfEmptyCollection(collection: string): AstroIntegration {
+ return {
+ name: 'astro-collection',
+ hooks: {
+ 'astro:server:start': ({ logger }) => {
+ logger.warn(
+ `The collection **${collection}** does not exist or is empty. Ensure a collection directory with this name exists.`
+ );
+ },
+ },
+ };
+}
diff --git a/packages/astro/src/core/build/plugins/plugin-middleware.ts b/packages/astro/src/core/build/plugins/plugin-middleware.ts
index 47ff4b863..22d3f795b 100644
--- a/packages/astro/src/core/build/plugins/plugin-middleware.ts
+++ b/packages/astro/src/core/build/plugins/plugin-middleware.ts
@@ -25,7 +25,7 @@ export function vitePluginMiddleware(
async resolveId(id) {
if (id === MIDDLEWARE_MODULE_ID) {
const middlewareId = await this.resolve(
- `${opts.settings.config.srcDir.pathname}/${MIDDLEWARE_PATH_SEGMENT_NAME}`
+ `${decodeURI(opts.settings.config.srcDir.pathname)}${MIDDLEWARE_PATH_SEGMENT_NAME}`
);
if (middlewareId) {
resolvedMiddlewareId = middlewareId.id;
diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts
index b36017c22..4ac40d4c5 100644
--- a/packages/astro/src/core/config/schema.ts
+++ b/packages/astro/src/core/config/schema.ts
@@ -140,7 +140,6 @@ export const AstroConfigSchema = z.object({
.optional()
.default(ASTRO_CONFIG_DEFAULTS.build.excludeMiddleware),
})
- .optional()
.default({}),
server: z.preprocess(
// preprocess
@@ -158,7 +157,6 @@ export const AstroConfigSchema = z.object({
port: z.number().optional().default(ASTRO_CONFIG_DEFAULTS.server.port),
headers: z.custom().optional(),
})
- .optional()
.default({})
),
redirects: z
@@ -274,27 +272,11 @@ export const AstroConfigSchema = z.object({
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.optimizeHoistedScript),
})
- .passthrough()
- .refine(
- (d) => {
- const validKeys = Object.keys(ASTRO_CONFIG_DEFAULTS.experimental);
- const invalidKeys = Object.keys(d).filter((key) => !validKeys.includes(key));
- if (invalidKeys.length > 0) return false;
- return true;
- },
- (d) => {
- const validKeys = Object.keys(ASTRO_CONFIG_DEFAULTS.experimental);
- const invalidKeys = Object.keys(d).filter((key) => !validKeys.includes(key));
- return {
- message: `Invalid experimental key: \`${invalidKeys.join(
- ', '
- )}\`. \nMake sure the spelling is correct, and that your Astro version supports this experiment.\nSee https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for more information.`,
- };
- }
+ .strict(
+ `Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for a list of all current experiments.`
)
- .optional()
.default({}),
- legacy: z.object({}).optional().default({}),
+ legacy: z.object({}).default({}),
});
export type AstroConfigType = z.infer;
diff --git a/packages/astro/src/core/cookies/index.ts b/packages/astro/src/core/cookies/index.ts
index f3c7b6d61..c8869f9ae 100644
--- a/packages/astro/src/core/cookies/index.ts
+++ b/packages/astro/src/core/cookies/index.ts
@@ -1,2 +1,6 @@
export { AstroCookies } from './cookies.js';
-export { attachCookiesToResponse, getSetCookiesFromResponse } from './response.js';
+export {
+ attachCookiesToResponse,
+ getSetCookiesFromResponse,
+ responseHasCookies,
+} from './response.js';
diff --git a/packages/astro/src/core/cookies/response.ts b/packages/astro/src/core/cookies/response.ts
index 8dc35e8c7..013f836bf 100644
--- a/packages/astro/src/core/cookies/response.ts
+++ b/packages/astro/src/core/cookies/response.ts
@@ -6,6 +6,10 @@ export function attachCookiesToResponse(response: Response, cookies: AstroCookie
Reflect.set(response, astroCookiesSymbol, cookies);
}
+export function responseHasCookies(response: Response): boolean {
+ return Reflect.has(response, astroCookiesSymbol);
+}
+
function getFromResponse(response: Response): AstroCookies | undefined {
let cookies = Reflect.get(response, astroCookiesSymbol);
if (cookies != null) {
diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts
index 1f336e5f8..e4fe35540 100644
--- a/packages/astro/src/core/errors/errors-data.ts
+++ b/packages/astro/src/core/errors/errors-data.ts
@@ -620,8 +620,42 @@ export const ExpectedImageOptions = {
message: (options: string) =>
`Expected getImage() parameter to be an object. Received \`${options}\`.`,
} satisfies ErrorData;
+
/**
* @docs
+ * @see
+ * - [Images](https://docs.astro.build/en/guides/images/)
+ * @description
+ * Astro could not find an image you imported. Often, this is simply caused by a typo in the path.
+ *
+ * Images in Markdown are relative to the current file. To refer to an image that is located in the same folder as the `.md` file, the path should start with `./`
+ */
+export const ImageNotFound = {
+ name: 'ImageNotFound',
+ title: 'Image not found.',
+ message: (imagePath: string) => `Could not find requested image \`${imagePath}\`. Does it exist?`,
+ hint: 'This is often caused by a typo in the image path. Please make sure the file exists, and is spelled correctly.',
+} satisfies ErrorData;
+
+/**
+ * @docs
+ * @message Could not process image metadata for `IMAGE_PATH`.
+ * @see
+ * - [Images](https://docs.astro.build/en/guides/images/)
+ * @description
+ * Astro could not process the metadata of an image you imported. This is often caused by a corrupted or malformed image and re-exporting the image from your image editor may fix this issue.
+ */
+export const NoImageMetadata = {
+ name: 'NoImageMetadata',
+ title: 'Could not process image metadata.',
+ message: (imagePath: string | undefined) =>
+ `Could not process image metadata${imagePath ? ' for `${imagePath}`' : ''}.`,
+ hint: 'This is often caused by a corrupted or malformed image. Re-exporting the image from your image editor may fix this issue.',
+} satisfies ErrorData;
+
+/**
+ * @docs
+ * @deprecated This error is no longer Markdown specific and as such, as been replaced by `ImageNotFound`
* @message
* Could not find requested image `IMAGE_PATH` at `FULL_IMAGE_PATH`.
* @see
@@ -640,6 +674,7 @@ export const MarkdownImageNotFound = {
}`,
hint: 'This is often caused by a typo in the image path. Please make sure the file exists, and is spelled correctly.',
} satisfies ErrorData;
+
/**
* @docs
* @description
diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts
index 1725fd38d..40513c152 100644
--- a/packages/astro/src/core/middleware/callMiddleware.ts
+++ b/packages/astro/src/core/middleware/callMiddleware.ts
@@ -5,6 +5,7 @@ import type {
MiddlewareHandler,
MiddlewareNext,
} from '../../@types/astro.js';
+import { attachCookiesToResponse, responseHasCookies } from '../cookies/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import type { Environment } from '../render/index.js';
@@ -82,7 +83,7 @@ export async function callMiddleware(
if (value instanceof Response === false) {
throw new AstroError(AstroErrorData.MiddlewareNotAResponse);
}
- return value as R;
+ return ensureCookiesAttached(apiContext, value as Response);
} else {
/**
* Here we handle the case where `next` was called and returned nothing.
@@ -105,11 +106,18 @@ export async function callMiddleware(
throw new AstroError(AstroErrorData.MiddlewareNotAResponse);
} else {
// Middleware did not call resolve and returned a value
- return value as R;
+ return ensureCookiesAttached(apiContext, value as Response);
}
});
}
+function ensureCookiesAttached(apiContext: APIContext, response: Response): Response {
+ if (apiContext.cookies !== undefined && !responseHasCookies(response)) {
+ attachCookiesToResponse(response, apiContext.cookies);
+ }
+ return response;
+}
+
function isEndpointOutput(endpointResult: any): endpointResult is EndpointOutput {
return (
!(endpointResult instanceof Response) &&
diff --git a/packages/astro/src/core/middleware/loadMiddleware.ts b/packages/astro/src/core/middleware/loadMiddleware.ts
index 9a7f3e4bc..b8528eb4b 100644
--- a/packages/astro/src/core/middleware/loadMiddleware.ts
+++ b/packages/astro/src/core/middleware/loadMiddleware.ts
@@ -12,7 +12,7 @@ export async function loadMiddleware(
srcDir: AstroSettings['config']['srcDir']
) {
// can't use node Node.js builtins
- let middlewarePath = srcDir.pathname + '/' + MIDDLEWARE_PATH_SEGMENT_NAME;
+ let middlewarePath = `${decodeURI(srcDir.pathname)}${MIDDLEWARE_PATH_SEGMENT_NAME}`;
try {
const module = await moduleLoader.import(middlewarePath);
return module;
diff --git a/packages/astro/src/events/session.ts b/packages/astro/src/events/session.ts
index b37530342..d9c492cb0 100644
--- a/packages/astro/src/events/session.ts
+++ b/packages/astro/src/events/session.ts
@@ -1,25 +1,8 @@
-import type { AstroUserConfig } from '../@types/astro.js';
+import type { AstroIntegration, AstroUserConfig } from '../@types/astro.js';
+import { AstroConfigSchema } from '../core/config/schema.js';
const EVENT_SESSION = 'ASTRO_CLI_SESSION_STARTED';
-interface ConfigInfo {
- markdownPlugins: string[];
- adapter: string | null;
- integrations: string[];
- trailingSlash: undefined | 'always' | 'never' | 'ignore';
- build:
- | undefined
- | {
- format: undefined | 'file' | 'directory';
- };
- markdown:
- | undefined
- | {
- drafts: undefined | boolean;
- syntaxHighlight: undefined | 'shiki' | 'prism' | false;
- };
-}
-
interface EventPayload {
cliCommand: string;
config?: ConfigInfo;
@@ -28,87 +11,126 @@ interface EventPayload {
optionalIntegrations?: number;
}
-const multiLevelKeys = new Set([
- 'build',
- 'markdown',
- 'markdown.shikiConfig',
- 'server',
- 'vite',
- 'vite.resolve',
- 'vite.css',
- 'vite.json',
- 'vite.server',
- 'vite.server.fs',
- 'vite.build',
- 'vite.preview',
- 'vite.optimizeDeps',
- 'vite.ssr',
- 'vite.worker',
-]);
-function configKeys(obj: Record | undefined, parentKey: string): string[] {
- if (!obj) {
- return [];
+type ConfigInfoValue = string | boolean | string[] | undefined;
+type ConfigInfoRecord = Record;
+type ConfigInfoBase = {
+ [alias in keyof AstroUserConfig]: ConfigInfoValue | ConfigInfoRecord;
+};
+export interface ConfigInfo extends ConfigInfoBase {
+ build: ConfigInfoRecord;
+ image: ConfigInfoRecord;
+ markdown: ConfigInfoRecord;
+ experimental: ConfigInfoRecord;
+ legacy: ConfigInfoRecord;
+ vite: ConfigInfoRecord | undefined;
+}
+
+function measureIsDefined(val: unknown) {
+ // if val is undefined, measure undefined as a value
+ if (val === undefined) {
+ return undefined;
}
+ // otherwise, convert the value to a boolean
+ return Boolean(val);
+}
- return Object.entries(obj)
- .map(([key, value]) => {
- if (typeof value === 'object' && !Array.isArray(value)) {
- const localKey = parentKey ? parentKey + '.' + key : key;
- if (multiLevelKeys.has(localKey)) {
- let keys = configKeys(value, localKey).map((subkey) => key + '.' + subkey);
- keys.unshift(key);
- return keys;
- }
- }
+type StringLiteral = T extends string ? (string extends T ? never : T) : never;
- return key;
- })
- .flat(1);
+/**
+ * Measure supports string literal values. Passing a generic `string` type
+ * results in an error, to make sure generic user input is never measured directly.
+ */
+function measureStringLiteral(
+ val: StringLiteral | boolean | undefined
+): string | boolean | undefined {
+ return val;
+}
+
+function measureIntegration(val: AstroIntegration | false | null | undefined): string | undefined {
+ if (!val || !val.name) {
+ return undefined;
+ }
+ return val.name;
+}
+
+function sanitizeConfigInfo(obj: object | undefined, validKeys: string[]): ConfigInfoRecord {
+ if (!obj || validKeys.length === 0) {
+ return {};
+ }
+ return validKeys.reduce(
+ (result, key) => {
+ result[key] = measureIsDefined((obj as Record)[key]);
+ return result;
+ },
+ {} as Record
+ );
+}
+
+/**
+ * This function creates an anonymous ConfigInfo object from the user's config.
+ * All values are sanitized to preserve anonymity. Simple "exist" boolean checks
+ * are used by default, with a few additional sanitized values added manually.
+ * Helper functions should always be used to ensure correct sanitization.
+ */
+function createAnonymousConfigInfo(userConfig: AstroUserConfig) {
+ // Sanitize and measure the generic config object
+ // NOTE(fks): Using _def is the correct, documented way to get the `shape`
+ // from a Zod object that includes a wrapping default(), optional(), etc.
+ // Even though `_def` appears private, it is type-checked for us so that
+ // any changes between versions will be detected.
+ const configInfo: ConfigInfo = {
+ ...sanitizeConfigInfo(userConfig, Object.keys(AstroConfigSchema.shape)),
+ build: sanitizeConfigInfo(
+ userConfig.build,
+ Object.keys(AstroConfigSchema.shape.build._def.innerType.shape)
+ ),
+ image: sanitizeConfigInfo(
+ userConfig.image,
+ Object.keys(AstroConfigSchema.shape.image._def.innerType.shape)
+ ),
+ markdown: sanitizeConfigInfo(
+ userConfig.markdown,
+ Object.keys(AstroConfigSchema.shape.markdown._def.innerType.shape)
+ ),
+ experimental: sanitizeConfigInfo(
+ userConfig.experimental,
+ Object.keys(AstroConfigSchema.shape.experimental._def.innerType.shape)
+ ),
+ legacy: sanitizeConfigInfo(
+ userConfig.legacy,
+ Object.keys(AstroConfigSchema.shape.legacy._def.innerType.shape)
+ ),
+ vite: userConfig.vite
+ ? sanitizeConfigInfo(userConfig.vite, Object.keys(userConfig.vite))
+ : undefined,
+ };
+ // Measure string literal/enum configuration values
+ configInfo.build.format = measureStringLiteral(userConfig.build?.format);
+ configInfo.markdown.syntaxHighlight = measureStringLiteral(userConfig.markdown?.syntaxHighlight);
+ configInfo.output = measureStringLiteral(userConfig.output);
+ configInfo.scopedStyleStrategy = measureStringLiteral(userConfig.scopedStyleStrategy);
+ configInfo.trailingSlash = measureStringLiteral(userConfig.trailingSlash);
+ // Measure integration & adapter usage
+ configInfo.adapter = measureIntegration(userConfig.adapter);
+ configInfo.integrations = userConfig.integrations
+ ?.flat(100)
+ .map(measureIntegration)
+ .filter(Boolean) as string[];
+ // Return the sanitized ConfigInfo object
+ return configInfo;
}
export function eventCliSession(
cliCommand: string,
- userConfig?: AstroUserConfig,
+ userConfig: AstroUserConfig,
flags?: Record
): { eventName: string; payload: EventPayload }[] {
- // Filter out falsy integrations
- const configValues = userConfig
- ? {
- markdownPlugins: [
- ...(userConfig?.markdown?.remarkPlugins?.map((p) =>
- typeof p === 'string' ? p : typeof p
- ) ?? []),
- ...(userConfig?.markdown?.rehypePlugins?.map((p) =>
- typeof p === 'string' ? p : typeof p
- ) ?? []),
- ] as string[],
- adapter: userConfig?.adapter?.name ?? null,
- integrations: (userConfig?.integrations ?? [])
- .filter(Boolean)
- .flat()
- .map((i: any) => i?.name),
- trailingSlash: userConfig?.trailingSlash,
- build: userConfig?.build
- ? {
- format: userConfig?.build?.format,
- }
- : undefined,
- markdown: userConfig?.markdown
- ? {
- drafts: userConfig.markdown?.drafts,
- syntaxHighlight: userConfig.markdown?.syntaxHighlight,
- }
- : undefined,
- }
- : undefined;
-
// Filter out yargs default `_` flag which is the cli command
const cliFlags = flags ? Object.keys(flags).filter((name) => name != '_') : undefined;
const payload: EventPayload = {
cliCommand,
- configKeys: userConfig ? configKeys(userConfig, '') : undefined,
- config: configValues,
+ config: createAnonymousConfigInfo(userConfig),
flags: cliFlags,
};
return [{ eventName: EVENT_SESSION, payload }];
diff --git a/packages/astro/src/runtime/server/scripts.ts b/packages/astro/src/runtime/server/scripts.ts
index 791be3201..47cd122f1 100644
--- a/packages/astro/src/runtime/server/scripts.ts
+++ b/packages/astro/src/runtime/server/scripts.ts
@@ -1,7 +1,7 @@
import type { SSRResult } from '../../@types/astro.js';
import islandScript from './astro-island.prebuilt.js';
-const ISLAND_STYLES = ``;
+const ISLAND_STYLES = ``;
export function determineIfNeedsHydrationScript(result: SSRResult): boolean {
if (result._metadata.hasHydrationScript) {
@@ -36,12 +36,12 @@ export function getPrescripts(result: SSRResult, type: PrescriptType, directive:
// deps to be loaded immediately.
switch (type) {
case 'both':
- return `${ISLAND_STYLES}`;
case 'directive':
- return ``;
+ return ``;
}
return '';
}
diff --git a/packages/astro/src/vite-plugin-astro/hmr.ts b/packages/astro/src/vite-plugin-astro/hmr.ts
index 65186af5e..6600b2f42 100644
--- a/packages/astro/src/vite-plugin-astro/hmr.ts
+++ b/packages/astro/src/vite-plugin-astro/hmr.ts
@@ -90,7 +90,7 @@ export async function handleHotUpdate(
// Bugfix: sometimes style URLs get normalized and end with `lang.css=`
// These will cause full reloads, so filter them out here
- const mods = ctx.modules.filter((m) => !m.url.endsWith('='));
+ const mods = [...filtered].filter((m) => !m.url.endsWith('='));
const file = ctx.file.replace(config.root.pathname, '/');
// If only styles are changed, remove the component file from the update list
@@ -109,17 +109,6 @@ export async function handleHotUpdate(
}
}
- // If this is a module that is imported from a