diff --git a/.changeset/angry-avocados-live.md b/.changeset/angry-avocados-live.md
new file mode 100644
index 0000000000000..c48db59488f2c
--- /dev/null
+++ b/.changeset/angry-avocados-live.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Prevent Partytown from hijacking history APIs
diff --git a/.changeset/brave-taxis-arrive.md b/.changeset/brave-taxis-arrive.md
deleted file mode 100644
index 3d2a5bd172edb..0000000000000
--- a/.changeset/brave-taxis-arrive.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'astro': patch
----
-
-Adds a warning if `astro add` fetches a package but returns a non-404 status
diff --git a/.changeset/lorem-ipsum-dolor b/.changeset/lorem-ipsum-dolor
new file mode 100644
index 0000000000000..15182e8f6cd5b
--- /dev/null
+++ b/.changeset/lorem-ipsum-dolor
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Fixes static builds when `config.outDir` is located outside of the astro project
diff --git a/.changeset/quick-toes-peel.md b/.changeset/quick-toes-peel.md
deleted file mode 100644
index 25d5c13c7130b..0000000000000
--- a/.changeset/quick-toes-peel.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'astro': patch
----
-
-Add a new property `Astro.currentLocale`, available when `i18n` is enabled.
diff --git a/.changeset/selfish-parents-leave.md b/.changeset/selfish-parents-leave.md
new file mode 100644
index 0000000000000..206fa08e12844
--- /dev/null
+++ b/.changeset/selfish-parents-leave.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Improves the error message when a middleware doesn't return a `Response`
diff --git a/.changeset/thirty-hats-bathe.md b/.changeset/thirty-hats-bathe.md
new file mode 100644
index 0000000000000..1ec2b932af389
--- /dev/null
+++ b/.changeset/thirty-hats-bathe.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Correctly merge headers from the original response when an error page is rendered
diff --git a/.devcontainer/with-vite-plugin-pwa/devcontainer.json b/.devcontainer/with-vite-plugin-pwa/devcontainer.json
deleted file mode 100644
index d716cded01b64..0000000000000
--- a/.devcontainer/with-vite-plugin-pwa/devcontainer.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "name": "Vite PWA",
- "build": {
- "dockerfile": "../examples.Dockerfile"
- },
-
- "workspaceFolder": "/workspaces/astro/examples/with-vite-plugin-pwa",
-
- "portsAttributes": {
- "4321": {
- "label": "Application",
- "onAutoForward": "openPreview"
- }
- },
-
- "forwardPorts": [4321],
-
- "postCreateCommand": "pnpm install && cd /workspaces/astro && pnpm run build",
-
- "waitFor": "postCreateCommand",
-
- "postAttachCommand": {
- "Server": "pnpm start --host"
- },
-
- "customizations": {
- "codespaces": {
- "openFiles": ["src/pages/index.astro"]
- },
- "vscode": {
- "extensions": ["astro-build.astro-vscode", "esbenp.prettier-vscode"]
- }
- }
-}
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index e6085ac647f67..95f00b4762897 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -58,6 +58,16 @@ module.exports = {
'@typescript-eslint/unbound-method': 'off',
'@typescript-eslint/no-explicit-any': 'off',
+ // Enforce separate type imports for type-only imports to avoid bundling unneeded code
+ '@typescript-eslint/consistent-type-imports': [
+ 'error',
+ {
+ prefer: 'type-imports',
+ fixStyle: 'separate-type-imports',
+ disallowTypeAnnotations: false,
+ },
+ ],
+
// These rules enabled by the preset configs don't work well for us
'@typescript-eslint/await-thenable': 'off',
'prefer-const': 'off',
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index feecf12a5aac5..30fd8867bcbad 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -6,6 +6,7 @@ on:
- main
- "1-legacy"
- "2-legacy"
+ - "3-legacy"
- next
defaults:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 47c9aa6b2a9f0..05a07e125484e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,8 +2,7 @@
We welcome contributions of any size and skill level. As an open source project, we believe in giving back to our contributors and are happy to help with guidance on PRs, technical writing, and turning any feature idea into a reality.
-> **Tip for new contributors:**
-> Take a look at [https://github.com/firstcontributions/first-contributions](https://github.com/firstcontributions/first-contributions) for helpful information on contributing
+> [!Tip] > **For new contributors:** Take a look at [https://github.com/firstcontributions/first-contributions](https://github.com/firstcontributions/first-contributions) for helpful information on contributing
## Quick Guide
@@ -46,7 +45,8 @@ To get started, create a codespace for this repository by clicking this 👇
Your new codespace will open in a web-based version of Visual Studio Code. All development dependencies will be preinstalled, and the tests will run automatically ensuring you've got a green base from which to start working.
-**Note**: Dev containers is now an open spec which is supported by [GitHub Codespaces](https://github.com/codespaces) and [other supporting tools](https://containers.dev/supporting).
+> [!Note]
+> Dev containers is now an open spec which is supported by [GitHub Codespaces](https://github.com/codespaces) and [other supporting tools](https://containers.dev/supporting).
### Development
@@ -187,7 +187,7 @@ Understanding in which environment code runs, and at which stage in the process,
Active Astro development happens on the [`main`](https://github.com/withastro/astro/tree/main) branch. `main` always reflects the latest code.
-> **Note:**
+> [!Note]
> During certain periods, we put `main` into a [**prerelease**](https://github.com/changesets/changesets/blob/main/docs/prereleases.md#prereleases) state. Read more about [Releasing Astro](#releasing-astro).
### `latest`
@@ -198,7 +198,8 @@ By default, `create-astro` and [astro.new](https://astro.new) point to this bran
## Releasing Astro
-_Note: Only [core maintainers (L3+)](https://github.com/withastro/.github/blob/main/GOVERNANCE.md#level-3-l3---core) can release new versions of Astro._
+> [!Note]
+> Only [core maintainers (L3+)](https://github.com/withastro/.github/blob/main/GOVERNANCE.md#level-3-l3---core) can release new versions of Astro.
The repo is set up with automatic releases, using the changeset GitHub action & bot.
@@ -247,7 +248,7 @@ If you have gotten permission from the core contributors, you can enter into pre
- Run: `pnpm exec changeset pre enter next` in the project root
- Create a new PR from the changes created by this command
- Review, approve, and more the PR to enter prerelease mode.
-- If successful, The "Version Packages" PR (if one exists) will now say "Version Packages (next)".
+- If successful, The "[ci] release" PR (if one exists) will now say "[ci] release (next)".
### Exiting prerelease mode
@@ -256,7 +257,7 @@ Exiting prerelease mode should happen once an experimental release is ready to g
- Run: `pnpm exec changeset pre exit` in the project root
- Create a new PR from the changes created by this command.
- Review, approve, and more the PR to enter prerelease mode.
-- If successful, The "Version Packages (next)" PR (if one exists) will now say "Version Packages".
+- If successful, The "[ci] release (next)" PR (if one exists) will now say "[ci] release".
### Releasing `astro@latest` while in prerelease mode
diff --git a/LICENSE b/LICENSE
index 1f0bcaa7dcefc..b3cd0c0f0e01d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -20,7 +20,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-
"""
This license applies to parts of the `packages/create-astro` and `packages/astro` subdirectories originating from the https://github.com/sveltejs/kit repository:
@@ -33,7 +32,6 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
-
"""
This license applies to parts of the `packages/create-astro` and `packages/astro` subdirectories originating from the https://github.com/vitejs/vite repository:
diff --git a/README.md b/README.md
index b608a3026a105..dc246b3d145f5 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,14 @@
+
+
+![Build Status](https://github.com/withastro/astro/actions/workflows/ci.yml/badge.svg)
+[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/withastro/astro/blob/main/LICENSE)
+[![npm version](https://badge.fury.io/js/astro.svg)](https://badge.fury.io/js/astro)
+
+
+
## Install
The **recommended** way to install the latest version of Astro is by running the command below:
@@ -60,7 +68,6 @@ Join us on [Discord](https://astro.build/chat) to meet other maintainers. We'll
| [@astrojs/tailwind](packages/integrations/tailwind) | [![astro version](https://img.shields.io/npm/v/@astrojs/tailwind.svg?label=%20)](packages/integrations/tailwind/CHANGELOG.md) |
| [@astrojs/alpinejs](packages/integrations/alpinejs) | [![astro version](https://img.shields.io/npm/v/@astrojs/alpinejs.svg?label=%20)](packages/integrations/alpinejs/CHANGELOG.md) |
| [@astrojs/mdx](packages/integrations/mdx) | [![astro version](https://img.shields.io/npm/v/@astrojs/mdx.svg?label=%20)](packages/integrations/mdx/CHANGELOG.md) |
-| [@astrojs/prefetch](packages/integrations/prefetch) | [![astro version](https://img.shields.io/npm/v/@astrojs/prefetch.svg?label=%20)](packages/integrations/prefetch/CHANGELOG.md) |
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6178/badge)](https://bestpractices.coreinfrastructure.org/projects/6178)
diff --git a/benchmark/packages/timer/package.json b/benchmark/packages/timer/package.json
index 8ac03985492ef..a4a3b8df1b81c 100644
--- a/benchmark/packages/timer/package.json
+++ b/benchmark/packages/timer/package.json
@@ -29,7 +29,7 @@
"astro": "workspace:*"
},
"devDependencies": {
- "@types/server-destroy": "^1.0.1",
+ "@types/server-destroy": "^1.0.3",
"astro": "workspace:*",
"astro-scripts": "workspace:*"
}
diff --git a/benchmark/packages/timer/src/index.ts b/benchmark/packages/timer/src/index.ts
index 49edcb5e86a5e..2ea41af6624d0 100644
--- a/benchmark/packages/timer/src/index.ts
+++ b/benchmark/packages/timer/src/index.ts
@@ -6,6 +6,7 @@ export function getAdapter(): AstroAdapter {
serverEntrypoint: '@benchmark/timer/server.js',
previewEntrypoint: '@benchmark/timer/preview.js',
exports: ['handler'],
+ supportedAstroFeatures: {},
};
}
diff --git a/examples/basics/package.json b/examples/basics/package.json
index 1be44bed6e74d..33bb8dbfeb6b9 100644
--- a/examples/basics/package.json
+++ b/examples/basics/package.json
@@ -11,6 +11,6 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^3.5.5"
+ "astro": "^4.0.5"
}
}
diff --git a/examples/blog/package.json b/examples/blog/package.json
index afee1f0281c66..0887b798078e4 100644
--- a/examples/blog/package.json
+++ b/examples/blog/package.json
@@ -11,9 +11,9 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/mdx": "^1.1.5",
- "@astrojs/rss": "^3.0.0",
+ "@astrojs/mdx": "^2.0.1",
+ "@astrojs/rss": "^4.0.1",
"@astrojs/sitemap": "^3.0.3",
- "astro": "^3.5.5"
+ "astro": "^4.0.5"
}
}
diff --git a/examples/blog/src/content/config.ts b/examples/blog/src/content/config.ts
index f0419223e7635..667a31cc73912 100644
--- a/examples/blog/src/content/config.ts
+++ b/examples/blog/src/content/config.ts
@@ -1,6 +1,7 @@
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
+ type: 'content',
// Type-check frontmatter using a schema
schema: z.object({
title: z.string(),
diff --git a/examples/component/package.json b/examples/component/package.json
index 2c781217268ba..3365f94cfac7a 100644
--- a/examples/component/package.json
+++ b/examples/component/package.json
@@ -15,7 +15,7 @@
],
"scripts": {},
"devDependencies": {
- "astro": "^3.5.5"
+ "astro": "^4.0.5"
},
"peerDependencies": {
"astro": "^3.0.0"
diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json
index 88c5f32295e46..9b649f82af342 100644
--- a/examples/framework-alpine/package.json
+++ b/examples/framework-alpine/package.json
@@ -12,8 +12,8 @@
},
"dependencies": {
"@astrojs/alpinejs": "^0.3.1",
- "@types/alpinejs": "^3.7.2",
- "alpinejs": "^3.12.3",
- "astro": "^3.5.5"
+ "@types/alpinejs": "^3.13.5",
+ "alpinejs": "^3.13.3",
+ "astro": "^4.0.5"
}
}
diff --git a/examples/framework-lit/package.json b/examples/framework-lit/package.json
index 97f9a4dcb29c9..478c491b88c60 100644
--- a/examples/framework-lit/package.json
+++ b/examples/framework-lit/package.json
@@ -11,9 +11,9 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/lit": "^3.0.3",
+ "@astrojs/lit": "^4.0.0",
"@webcomponents/template-shadowroot": "^0.2.1",
- "astro": "^3.5.5",
+ "astro": "^4.0.5",
"lit": "^2.8.0"
}
}
diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json
index af251b7d8796c..361459ccf6338 100644
--- a/examples/framework-multiple/package.json
+++ b/examples/framework-multiple/package.json
@@ -12,16 +12,16 @@
},
"dependencies": {
"@astrojs/preact": "^3.0.1",
- "@astrojs/react": "^3.0.5",
+ "@astrojs/react": "^3.0.7",
"@astrojs/solid-js": "^3.0.2",
- "@astrojs/svelte": "^4.0.4",
- "@astrojs/vue": "^3.0.4",
- "astro": "^3.5.5",
- "preact": "^10.17.1",
+ "@astrojs/svelte": "^5.0.1",
+ "@astrojs/vue": "^4.0.3",
+ "astro": "^4.0.5",
+ "preact": "^10.19.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "solid-js": "^1.7.11",
- "svelte": "^4.2.0",
- "vue": "^3.3.4"
+ "solid-js": "^1.8.5",
+ "svelte": "^4.2.5",
+ "vue": "^3.3.8"
}
}
diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json
index c8059410c45ec..311005c85883c 100644
--- a/examples/framework-preact/package.json
+++ b/examples/framework-preact/package.json
@@ -13,7 +13,7 @@
"dependencies": {
"@astrojs/preact": "^3.0.1",
"@preact/signals": "^1.2.1",
- "astro": "^3.5.5",
- "preact": "^10.17.1"
+ "astro": "^4.0.5",
+ "preact": "^10.19.2"
}
}
diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json
index 79b09fc45d4c9..208a234bdeb1b 100644
--- a/examples/framework-react/package.json
+++ b/examples/framework-react/package.json
@@ -11,10 +11,10 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/react": "^3.0.5",
- "@types/react": "^18.2.21",
- "@types/react-dom": "^18.2.7",
- "astro": "^3.5.5",
+ "@astrojs/react": "^3.0.7",
+ "@types/react": "^18.2.37",
+ "@types/react-dom": "^18.2.15",
+ "astro": "^4.0.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json
index 8b1a9a3157f06..7c6a3826962b7 100644
--- a/examples/framework-solid/package.json
+++ b/examples/framework-solid/package.json
@@ -12,7 +12,7 @@
},
"dependencies": {
"@astrojs/solid-js": "^3.0.2",
- "astro": "^3.5.5",
- "solid-js": "^1.7.11"
+ "astro": "^4.0.5",
+ "solid-js": "^1.8.5"
}
}
diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json
index 3badefefbdd66..2a3322bd6b44c 100644
--- a/examples/framework-svelte/package.json
+++ b/examples/framework-svelte/package.json
@@ -11,8 +11,8 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/svelte": "^4.0.4",
- "astro": "^3.5.5",
- "svelte": "^4.2.0"
+ "@astrojs/svelte": "^5.0.1",
+ "astro": "^4.0.5",
+ "svelte": "^4.2.5"
}
}
diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json
index 99a6b678f005b..90d2e02d4064e 100644
--- a/examples/framework-vue/package.json
+++ b/examples/framework-vue/package.json
@@ -11,8 +11,8 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/vue": "^3.0.4",
- "astro": "^3.5.5",
- "vue": "^3.3.4"
+ "@astrojs/vue": "^4.0.3",
+ "astro": "^4.0.5",
+ "vue": "^3.3.8"
}
}
diff --git a/examples/framework-vue/src/components/Counter.vue b/examples/framework-vue/src/components/Counter.vue
index 5ce5352b88e6e..11f2bf1b5051f 100644
--- a/examples/framework-vue/src/components/Counter.vue
+++ b/examples/framework-vue/src/components/Counter.vue
@@ -1,31 +1,22 @@
+
+
-
-
+
-
{{ count }}
-
+
+
+
-
-
-
diff --git a/examples/with-vite-plugin-pwa/src/env.d.ts b/examples/with-vite-plugin-pwa/src/env.d.ts
deleted file mode 100644
index f964fe0cffd88..0000000000000
--- a/examples/with-vite-plugin-pwa/src/env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-///
diff --git a/examples/with-vite-plugin-pwa/src/index.ts b/examples/with-vite-plugin-pwa/src/index.ts
deleted file mode 100644
index 2595fe3a89037..0000000000000
--- a/examples/with-vite-plugin-pwa/src/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { registerSW } from 'virtual:pwa-register';
-
-const updateSW = registerSW({
- onNeedRefresh() {},
- onOfflineReady() {
- console.log('Offline ready');
- },
-});
-
-updateSW();
diff --git a/examples/with-vite-plugin-pwa/src/pages/index.astro b/examples/with-vite-plugin-pwa/src/pages/index.astro
deleted file mode 100644
index 19956b3d24c7b..0000000000000
--- a/examples/with-vite-plugin-pwa/src/pages/index.astro
+++ /dev/null
@@ -1,18 +0,0 @@
----
-
----
-
-
-
-
-
-
-
- Welcome to Astro
-
-
-
- Welcome to Astro
-
-
-
diff --git a/examples/with-vite-plugin-pwa/src/vite-env.d.ts b/examples/with-vite-plugin-pwa/src/vite-env.d.ts
deleted file mode 100644
index 1a947fad00738..0000000000000
--- a/examples/with-vite-plugin-pwa/src/vite-env.d.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-declare module 'virtual:pwa-register' {
- export type RegisterSWOptions = {
- immediate?: boolean;
- onNeedRefresh?: () => void;
- onOfflineReady?: () => void;
- onRegistered?: (registration: ServiceWorkerRegistration | undefined) => void;
- onRegisterError?: (error: any) => void;
- };
-
- export function registerSW(options?: RegisterSWOptions): (reloadPage?: boolean) => Promise;
-}
diff --git a/examples/with-vite-plugin-pwa/tsconfig.json b/examples/with-vite-plugin-pwa/tsconfig.json
deleted file mode 100644
index d78f81ec4e8e9..0000000000000
--- a/examples/with-vite-plugin-pwa/tsconfig.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "extends": "astro/tsconfigs/base"
-}
diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json
index 5259966bb9e71..965d0dc123217 100644
--- a/examples/with-vitest/package.json
+++ b/examples/with-vitest/package.json
@@ -12,7 +12,7 @@
"test": "vitest"
},
"dependencies": {
- "astro": "^3.5.5",
+ "astro": "^4.0.5",
"vitest": "^0.34.2"
}
}
diff --git a/package.json b/package.json
index 8bf7dc459cb98..ad073d17d5137 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,7 @@
"build:examples": "turbo run build --filter=\"@example/*\"",
"dev": "turbo run dev --concurrency=40 --parallel --filter=astro --filter=create-astro --filter=\"@astrojs/*\" --filter=\"@benchmark/*\"",
"format": "pnpm run format:code",
- "format:ci": "pnpm run format:imports && pnpm run format:code",
+ "format:ci": "pnpm run format:code",
"format:code": "prettier -w \"**/*\" --ignore-unknown --cache",
"format:imports": "organize-imports-cli ./packages/*/tsconfig.json ./packages/*/*/tsconfig.json",
"test": "turbo run test --concurrency=1 --filter=astro --filter=create-astro --filter=\"@astrojs/*\"",
@@ -44,14 +44,51 @@
"pnpm": ">=8.6.12"
},
"packageManager": "pnpm@8.6.12",
+ "dependencies": {
+ "astro-benchmark": "workspace:*"
+ },
+ "devDependencies": {
+ "@astrojs/check": "^0.3.1",
+ "@changesets/changelog-github": "^0.4.8",
+ "@changesets/cli": "^2.26.2",
+ "@types/node": "^18.17.8",
+ "@typescript-eslint/eslint-plugin": "^6.11.0",
+ "@typescript-eslint/parser": "^6.11.0",
+ "esbuild": "^0.19.6",
+ "eslint": "^8.54.0",
+ "eslint-config-prettier": "^9.0.0",
+ "eslint-plugin-no-only-tests": "^3.1.0",
+ "eslint-plugin-prettier": "^5.0.0",
+ "only-allow": "^1.1.1",
+ "organize-imports-cli": "^0.10.0",
+ "prettier": "^3.1.0",
+ "prettier-plugin-astro": "^0.12.2",
+ "tiny-glob": "^0.2.9",
+ "turbo": "^1.10.12",
+ "typescript": "~5.2.2"
+ },
"pnpm": {
"packageExtensions": {
+ "vite-svg-loader": {
+ "peerDependenciesMeta": {
+ "vue": {
+ "optional": true
+ }
+ }
+ },
"svelte2tsx": {
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
+ },
+ "rehype-pretty-code": {
+ "peerDependenciesMeta": {
+ "shiki": {
+ "optional": true
+ }
+ }
}
},
"overrides": {
@@ -66,34 +103,14 @@
"vite",
"react",
"react-dom",
- "@types/react"
+ "@types/react",
+ "tslib",
+ "quill-delta",
+ "rxjs"
],
"allowAny": [
"astro"
]
}
- },
- "dependencies": {
- "astro-benchmark": "workspace:*"
- },
- "devDependencies": {
- "@astrojs/check": "^0.1.0",
- "@changesets/changelog-github": "^0.4.8",
- "@changesets/cli": "^2.26.2",
- "@types/node": "^18.17.8",
- "@typescript-eslint/eslint-plugin": "^6.4.1",
- "@typescript-eslint/parser": "^6.4.1",
- "esbuild": "^0.19.2",
- "eslint": "^8.47.0",
- "eslint-config-prettier": "^9.0.0",
- "eslint-plugin-no-only-tests": "^3.1.0",
- "eslint-plugin-prettier": "^5.0.0",
- "only-allow": "^1.1.1",
- "organize-imports-cli": "^0.10.0",
- "prettier": "^3.0.3",
- "prettier-plugin-astro": "^0.12.0",
- "tiny-glob": "^0.2.9",
- "turbo": "^1.10.12",
- "typescript": "~5.1.6"
}
}
diff --git a/packages/astro-prism/package.json b/packages/astro-prism/package.json
index eae841073a2df..d62e819a8253b 100644
--- a/packages/astro-prism/package.json
+++ b/packages/astro-prism/package.json
@@ -35,7 +35,7 @@
"prismjs": "^1.29.0"
},
"devDependencies": {
- "@types/prismjs": "1.26.0",
+ "@types/prismjs": "1.26.3",
"astro-scripts": "workspace:*"
},
"engines": {
diff --git a/packages/astro-rss/CHANGELOG.md b/packages/astro-rss/CHANGELOG.md
index 26e9c95d73c2f..df205874890d1 100644
--- a/packages/astro-rss/CHANGELOG.md
+++ b/packages/astro-rss/CHANGELOG.md
@@ -1,5 +1,23 @@
# @astrojs/rss
+## 4.0.1
+
+### Patch Changes
+
+- [#9299](https://github.com/withastro/astro/pull/9299) [`edfae50e6`](https://github.com/withastro/astro/commit/edfae50e6ea494f49c6d4fbf4bd4481870f994b1) Thanks [@cdvillard](https://github.com/cdvillard)! - Improves the `@astrojs/rss` error message thrown when the object passed to the `items` property is missing any of the three required keys or if one of those keys is mistyped.
+
+## 4.0.0
+
+### Major Changes
+
+- [#9168](https://github.com/withastro/astro/pull/9168) [`153a5abb9`](https://github.com/withastro/astro/commit/153a5abb905042ac68b712514dc9ec387d3e6b17) Thanks [@bluwy](https://github.com/bluwy)! - Removes the deprecated (in v3.0) `drafts` option as the feature is deprecated in Astro 3.0
+
+## 4.0.0-beta.0
+
+### Major Changes
+
+- [#9168](https://github.com/withastro/astro/pull/9168) [`153a5abb9`](https://github.com/withastro/astro/commit/153a5abb905042ac68b712514dc9ec387d3e6b17) Thanks [@bluwy](https://github.com/bluwy)! - Removes the `drafts` option as the feature is deprecated in Astro 3.0
+
## 3.0.0
### Major Changes
diff --git a/packages/astro-rss/README.md b/packages/astro-rss/README.md
index 268f58f26690f..c8485b02e3e8e 100644
--- a/packages/astro-rss/README.md
+++ b/packages/astro-rss/README.md
@@ -28,7 +28,7 @@ Start by [adding a `site` to your project's `astro.config` for link generation](
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
-export async function get(context) {
+export async function GET(context) {
const posts = await getCollection('blog');
return rss({
title: 'Buzz’s Blog',
@@ -55,7 +55,7 @@ Read **[Astro's RSS docs][astro-rss]** for more on using content collections, an
The `rss` default export offers a number of configuration options. Here's a quick reference:
```js
-export function get(context) {
+export function GET(context) {
return rss({
// `` field in output xml
title: 'Buzz’s Blog',
@@ -98,7 +98,7 @@ The base URL to use when generating RSS item links. We recommend using the [endp
```ts
import rss from '@astrojs/rss';
-export const get = (context) =>
+export const GET = (context) =>
rss({
site: context.site,
// ...
@@ -113,14 +113,6 @@ A list of formatted RSS feed items. See [Astro's RSS items documentation](https:
When providing a formatted RSS item list, see the [`RSSFeedItem` type reference](#rssfeeditem).
-### drafts
-
-Type: `boolean (optional)`
-
-**Deprecated**: Manually filter `items` instead.
-
-Set `drafts: true` to include [draft posts](https://docs.astro.build/en/guides/markdown-content/#draft-pages) in the feed output. By default, this option is `false` and draft posts are not included.
-
### stylesheet
Type: `string (optional)`
@@ -136,7 +128,7 @@ A string of valid XML to be injected between your feed's `` and ` rss({
+export const GET = () => rss({
...
customData: 'en-us ',
});
@@ -181,7 +173,7 @@ By default, the library will add trailing slashes to the emitted URLs. To preven
```js
import rss from '@astrojs/rss';
-export const get = () =>
+export const GET = () =>
rss({
trailingSlash: false,
});
@@ -361,7 +353,7 @@ This function assumes, but does not verify, you are globbing for items inside `s
// src/pages/rss.xml.js
import rss, { pagesGlobToRssItems } from '@astrojs/rss';
-export async function get(context) {
+export async function GET(context) {
return rss({
title: 'Buzz’s Blog',
description: 'A humble Astronaut’s guide to the stars',
@@ -379,7 +371,7 @@ As `rss()` returns a `Response`, you can also use `getRssString()` to get the RS
// src/pages/rss.xml.js
import { getRssString } from '@astrojs/rss';
-export async function get(context) {
+export async function GET(context) {
const rssString = await getRssString({
title: 'Buzz’s Blog',
...
diff --git a/packages/astro-rss/package.json b/packages/astro-rss/package.json
index c200e6c5942a4..c262452daf0b6 100644
--- a/packages/astro-rss/package.json
+++ b/packages/astro-rss/package.json
@@ -1,7 +1,7 @@
{
"name": "@astrojs/rss",
"description": "Add RSS feeds to your Astro projects",
- "version": "3.0.0",
+ "version": "4.0.1",
"type": "module",
"types": "./dist/index.d.ts",
"author": "withastro",
@@ -27,9 +27,9 @@
"test": "mocha --exit --timeout 20000"
},
"devDependencies": {
- "@types/chai": "^4.3.5",
- "@types/chai-as-promised": "^7.1.5",
- "@types/mocha": "^10.0.1",
+ "@types/chai": "^4.3.10",
+ "@types/chai-as-promised": "^7.1.8",
+ "@types/mocha": "^10.0.4",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"chai": "^4.3.7",
diff --git a/packages/astro-rss/src/index.ts b/packages/astro-rss/src/index.ts
index a611afc1637dd..c8cf19d6022e9 100644
--- a/packages/astro-rss/src/index.ts
+++ b/packages/astro-rss/src/index.ts
@@ -27,11 +27,6 @@ export type RSSOptions = {
stylesheet?: z.infer['stylesheet'];
/** Specify custom data in opening of file */
customData?: z.infer['customData'];
- /**
- * Whether to include drafts or not
- * @deprecated Deprecated since version 3.0. Use content collections instead.
- */
- drafts?: z.infer['drafts'];
trailingSlash?: z.infer['trailingSlash'];
};
@@ -48,11 +43,6 @@ export type RSSFeedItem = {
description?: z.infer['description'];
/** Append some other XML-valid data to this item */
customData?: z.infer['customData'];
- /**
- * Whether draft or not
- * @deprecated Deprecated since version 3.0. Use content collections instead.
- */
- draft?: z.infer['draft'];
/** Categories or tags related to the item */
categories?: z.infer['categories'];
/** The item author's email address */
@@ -92,7 +82,6 @@ const rssOptionsValidator = z.object({
return items;
}),
xmlns: z.record(z.string()).optional(),
- drafts: z.boolean().default(false),
stylesheet: z.union([z.string(), z.boolean()]).optional(),
customData: z.string().optional(),
trailingSlash: z.boolean().default(true),
@@ -120,9 +109,21 @@ async function validateRssOptions(rssOptions: RSSOptions) {
const formattedError = new Error(
[
`[RSS] Invalid or missing options:`,
- ...parsedResult.error.errors.map(
- (zodError) => `${zodError.message} (${zodError.path.join('.')})`
- ),
+ ...parsedResult.error.errors.map((zodError) => {
+ const path = zodError.path.join('.');
+ const message = `${zodError.message} (${path})`;
+ const code = zodError.code;
+
+ if (path === 'items' && code === 'invalid_union') {
+ return [
+ message,
+ `The \`items\` property requires properly typed \`title\`, \`pubDate\`, and \`link\` keys.`,
+ `Check your collection's schema, and visit https://docs.astro.build/en/guides/rss/#generating-items for more info.`,
+ ].join('\n');
+ }
+
+ return message;
+ }),
].join('\n')
);
throw formattedError;
@@ -159,10 +160,7 @@ export function pagesGlobToRssItems(items: GlobResult): Promise {
- const { site } = rssOptions;
- const items = rssOptions.drafts
- ? rssOptions.items
- : rssOptions.items.filter((item) => !item.draft);
+ const { items, site } = rssOptions;
const xmlOptions = {
ignoreAttributes: false,
diff --git a/packages/astro-rss/src/schema.ts b/packages/astro-rss/src/schema.ts
index eb15ecd584095..98aa35f812123 100644
--- a/packages/astro-rss/src/schema.ts
+++ b/packages/astro-rss/src/schema.ts
@@ -8,7 +8,6 @@ export const rssSchema = z.object({
.refine((value) => !isNaN(value.getTime())),
description: z.string().optional(),
customData: z.string().optional(),
- draft: z.boolean().optional(),
categories: z.array(z.string()).optional(),
author: z.string().optional(),
commentsUrl: z.string().optional(),
diff --git a/packages/astro-rss/test/rss.test.js b/packages/astro-rss/test/rss.test.js
index 5dfb48b32adf3..e6f68a272f5fb 100644
--- a/packages/astro-rss/test/rss.test.js
+++ b/packages/astro-rss/test/rss.test.js
@@ -1,18 +1,18 @@
-import rss, { getRssString } from '../dist/index.js';
-import { rssSchema } from '../dist/schema.js';
import chai from 'chai';
import chaiPromises from 'chai-as-promised';
import chaiXml from 'chai-xml';
+import rss, { getRssString } from '../dist/index.js';
+import { rssSchema } from '../dist/schema.js';
import {
- title,
description,
- site,
phpFeedItem,
phpFeedItemWithContent,
phpFeedItemWithCustomData,
+ site,
+ title,
web1FeedItem,
- web1FeedItemWithContent,
web1FeedItemWithAllData,
+ web1FeedItemWithContent,
} from './test-utils.js';
chai.use(chaiPromises);
@@ -156,36 +156,12 @@ describe('getRssString', () => {
chai.expect(str).to.contain(customData);
});
- it('should filter out entries marked as `draft`', async () => {
- const str = await getRssString({
- title,
- description,
- items: [phpFeedItem, { ...web1FeedItem, draft: true }],
- site,
- });
-
- chai.expect(str).xml.to.equal(validXmlWithoutWeb1FeedResult);
- });
-
- it('should respect drafts option', async () => {
- const str = await getRssString({
- title,
- description,
- items: [phpFeedItem, { ...web1FeedItem, draft: true }],
- site,
- drafts: true,
- });
-
- chai.expect(str).xml.to.equal(validXmlResult);
- });
-
it('should not append trailing slash to URLs with the given option', async () => {
const str = await getRssString({
title,
description,
- items: [phpFeedItem, { ...web1FeedItem, draft: true }],
+ items: [phpFeedItem],
site,
- drafts: true,
trailingSlash: false,
});
diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md
index a3061833944f2..fa073b5d58caf 100644
--- a/packages/astro/CHANGELOG.md
+++ b/packages/astro/CHANGELOG.md
@@ -1,5 +1,638 @@
# astro
+## 4.0.5
+
+### Patch Changes
+
+- [#9423](https://github.com/withastro/astro/pull/9423) [`bda1d294f`](https://github.com/withastro/astro/commit/bda1d294f2d50f31abfc9a32b5272fc9ac080e83) Thanks [@matthewp](https://github.com/matthewp)! - Error when getImage is passed an undefined src
+
+- [#9424](https://github.com/withastro/astro/pull/9424) [`e1a5a2d36`](https://github.com/withastro/astro/commit/e1a5a2d36ac3637f5c94a27b69128a121541bae8) Thanks [@matthewp](https://github.com/matthewp)! - Prevents dev server from crashing on unhandled rejections, and adds a helpful error message
+
+- [#9404](https://github.com/withastro/astro/pull/9404) [`8aa17a64b`](https://github.com/withastro/astro/commit/8aa17a64b46b8eaabfd1375fd6550ff93727aa81) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixed some newer HTML attributes not being included in our type definitions
+
+- [#9414](https://github.com/withastro/astro/pull/9414) [`bebf38c0c`](https://github.com/withastro/astro/commit/bebf38c0cb539de04007f5e721bf459300b895a1) Thanks [@Skn0tt](https://github.com/Skn0tt)! - Adds the feature name to logs about feature deprecation / experimental status.
+
+- [#9418](https://github.com/withastro/astro/pull/9418) [`2c168af67`](https://github.com/withastro/astro/commit/2c168af6745f5357e76ec323787595ef06d5fd73) Thanks [@alexnguyennz](https://github.com/alexnguyennz)! - Fix broken link in CI instructions
+
+- [#9407](https://github.com/withastro/astro/pull/9407) [`546d92c86`](https://github.com/withastro/astro/commit/546d92c862d08c69751039511a12c92ae38184c2) Thanks [@matthewp](https://github.com/matthewp)! - Allows file URLs as import specifiers
+
+## 4.0.4
+
+### Patch Changes
+
+- [#9380](https://github.com/withastro/astro/pull/9380) [`ea0918259`](https://github.com/withastro/astro/commit/ea0918259964947523827bac6abe88ad3841dbb9) Thanks [@ematipico](https://github.com/ematipico)! - Correctly handle the rendering of i18n routes when `output: "hybrid"` is set
+
+- [#9374](https://github.com/withastro/astro/pull/9374) [`65ddb0271`](https://github.com/withastro/astro/commit/65ddb027111514d41481f7455c0f0f03f8f608a8) Thanks [@bluwy](https://github.com/bluwy)! - Fixes an issue where prerendered route paths that end with `.mjs` were removed from the final build
+
+- [#9375](https://github.com/withastro/astro/pull/9375) [`26f7023d6`](https://github.com/withastro/astro/commit/26f7023d6928de75c363df0fa759a6255cb73ef3) Thanks [@bluwy](https://github.com/bluwy)! - Prettifies generated route names injected by integrations
+
+- [#9387](https://github.com/withastro/astro/pull/9387) [`a7c75b333`](https://github.com/withastro/astro/commit/a7c75b3339e6b1562d0d16ab6ef482840c51df68) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Fixes an edge case with `astro add` that could install a prerelease instead of a stable release version.
+
+ **Prior to this change**
+ `astro add svelte` installs `svelte@5.0.0-next.22`
+
+ **After this change**
+ `astro add svelte` installs `svelte@4.2.8`
+
+- Updated dependencies [[`270c6cc27`](https://github.com/withastro/astro/commit/270c6cc27f20995883fcdabbff9b56d7f041f9e4)]:
+ - @astrojs/markdown-remark@4.0.1
+
+## 4.0.3
+
+### Patch Changes
+
+- [#9342](https://github.com/withastro/astro/pull/9342) [`eb942942d`](https://github.com/withastro/astro/commit/eb942942d67508c07d7efaa859a7840f7c0223da) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fix missing `is:inline` type for the ` ` element
+
+- [#9343](https://github.com/withastro/astro/pull/9343) [`ab0281aee`](https://github.com/withastro/astro/commit/ab0281aee419e58c6079ca393987fe1ff0541dd5) Thanks [@martrapp](https://github.com/martrapp)! - Adds source file properties to HTML elements only if devToolbar is enabled
+
+- [#9336](https://github.com/withastro/astro/pull/9336) [`c76901065`](https://github.com/withastro/astro/commit/c76901065545f6a8d3de3e44d1c8ee5456a8a77a) Thanks [@FredKSchott](https://github.com/FredKSchott)! - dev: fix issue where 404 and 500 responses were logged as 200
+
+- [#9339](https://github.com/withastro/astro/pull/9339) [`0bb3d5322`](https://github.com/withastro/astro/commit/0bb3d532219fb90fc08bfb472fc981fab6543d16) Thanks [@morinokami](https://github.com/morinokami)! - Fixed the log message to correctly display 'enabled' and 'disabled' when toggling 'Disable notifications' in the Toolbar.
+
+## 4.0.2
+
+### Patch Changes
+
+- [#9331](https://github.com/withastro/astro/pull/9331) [`cfb20550d`](https://github.com/withastro/astro/commit/cfb20550d346a33e76e23453d5dcd084e5065c4d) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Updates an internal dependency ([`vitefu`](https://github.com/svitejs/vitefu)) to avoid a common `peerDependency` warning
+
+- [#9327](https://github.com/withastro/astro/pull/9327) [`3878a91be`](https://github.com/withastro/astro/commit/3878a91be4879988c7235f433e50a6dc82e32288) Thanks [@doseofted](https://github.com/doseofted)! - Fixes an edge case for `` elements.
+
+- [#9196](https://github.com/withastro/astro/pull/9196) [`37697a2c5`](https://github.com/withastro/astro/commit/37697a2c5511572dc29c0a4ea46f90c2f62be8e6) Thanks [@bluwy](https://github.com/bluwy)! - Removes support for Shiki custom language's `path` property. The language JSON file should be imported and passed to the option instead.
+
+ ```diff
+ // astro.config.js
+ + import customLang from './custom.tmLanguage.json'
+
+ export default defineConfig({
+ markdown: {
+ shikiConfig: {
+ langs: [
+ - { path: './custom.tmLanguage.json' },
+ + customLang,
+ ],
+ },
+ },
+ })
+ ```
+
+- [#9199](https://github.com/withastro/astro/pull/9199) [`49aa215a0`](https://github.com/withastro/astro/commit/49aa215a01ee1c4805316c85bb0aea6cfbc25a31) Thanks [@lilnasy](https://github.com/lilnasy)! - This change only affects maintainers of third-party adapters. In the Integration API, the `app.render()` method of the `App` class has been simplified.
+
+ Instead of two optional arguments, it now takes a single optional argument that is an object with two optional properties: `routeData` and `locals`.
+
+ ```diff
+ app.render(request)
+
+ - app.render(request, routeData)
+ + app.render(request, { routeData })
+
+ - app.render(request, routeData, locals)
+ + app.render(request, { routeData, locals })
+
+ - app.render(request, undefined, locals)
+ + app.render(request, { locals })
+ ```
+
+ The current signature is deprecated but will continue to function until next major version.
+
+- [#9212](https://github.com/withastro/astro/pull/9212) [`c0383ea0c`](https://github.com/withastro/astro/commit/c0383ea0c102cb62b7235823c706a090ba08715f) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - Removes deprecated `app.match()` option, `matchNotFound`
+
+- [#9168](https://github.com/withastro/astro/pull/9168) [`153a5abb9`](https://github.com/withastro/astro/commit/153a5abb905042ac68b712514dc9ec387d3e6b17) Thanks [@bluwy](https://github.com/bluwy)! - Removes deprecated features from Astro 3.0
+
+ - Adapters are now required to pass `supportedAstroFeatures` to specify a list of features they support.
+ - The `build.split` and `build.excludeMiddleware` options are removed. Use `functionPerRoute` and `edgeMiddleware` from adapters instead.
+ - The `markdown.drafts` option and draft feature is removed. Use content collections instead.
+ - Lowercase endpoint names are no longer supported. Use uppercase endpoint names instead.
+ - `getHeaders()` exported from markdown files is removed. Use `getHeadings()` instead.
+
+### Minor Changes
+
+- [#9105](https://github.com/withastro/astro/pull/9105) [`6201bbe96`](https://github.com/withastro/astro/commit/6201bbe96c2a083fb201e4a43a9bd88499821a3e) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Update CLI logging experience
+
+- [#9200](https://github.com/withastro/astro/pull/9200) [`b4b851f5a`](https://github.com/withastro/astro/commit/b4b851f5a46b32ee531db5dc39ccd2aa7af7bcfd) Thanks [@ematipico](https://github.com/ematipico)! - Adds a new way to configure the `i18n.locales` array.
+
+ Developers can now assign a custom URL path prefix that can span multiple language codes:
+
+ ```js
+ // astro.config.mjs
+ export default defineConfig({
+ experimental: {
+ i18n: {
+ defaultLocale: 'english',
+ locales: ['de', { path: 'english', codes: ['en', 'en-US'] }, 'fr'],
+ },
+ },
+ });
+ ```
+
+ With the above configuration, the URL prefix of the default locale will be `/english/`. When computing `Astro.preferredLocale`, Astro will use the `codes`.
+
+- [#9115](https://github.com/withastro/astro/pull/9115) [`3b77889b4`](https://github.com/withastro/astro/commit/3b77889b47750ed6e17c7858780dc4aae9201b58) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Adds the `astro preferences` command to manage user preferences. User preferences are specific to individual Astro users, unlike the `astro.config.mjs` file which changes behavior for everyone working on a project.
+
+ User preferences are scoped to the current project by default, stored in a local `.astro/settings.json` file. Using the `--global` flag, user preferences can also be applied to every Astro project on the current machine. Global user preferences are stored in an operating system-specific location.
+
+ ```sh
+ # Disable the dev overlay for the current user in the current project
+ npm run astro preferences disable devOverlay
+ # Disable the dev overlay for the current user in all Astro projects on this machine
+ npm run astro preferences --global disable devOverlay
+
+ # Check if the dev overlay is enabled for the current user
+ npm run astro preferences list devOverlay
+ ```
+
+- [#9139](https://github.com/withastro/astro/pull/9139) [`459b26436`](https://github.com/withastro/astro/commit/459b2643666db08dbd29a100ce3d8697b451d3fe) Thanks [@bluwy](https://github.com/bluwy)! - Reworks Vite's logger to use Astro's logger to correctly log HMR messages
+
+- [#9279](https://github.com/withastro/astro/pull/9279) [`6a9669b81`](https://github.com/withastro/astro/commit/6a9669b810ddfcae6c537165a438190ea1e7a4bc) Thanks [@martrapp](https://github.com/martrapp)! - Improves consistency between navigations with and without ``. See [#9279](https://github.com/withastro/astro/pull/9279) for more details.
+
+- [#9161](https://github.com/withastro/astro/pull/9161) [`bd0c2e9ae`](https://github.com/withastro/astro/commit/bd0c2e9ae3389a9d3085050c1e8134ae98dff299) Thanks [@bluwy](https://github.com/bluwy)! - Renames the `entryPoint` property of the `injectRoute` integrations API to `entrypoint` for consistency. A warning will be shown prompting you to update your code when using the old name.
+
+- [#9129](https://github.com/withastro/astro/pull/9129) [`8bfc20511`](https://github.com/withastro/astro/commit/8bfc20511918d675202cdc100d4efab293e5cbac) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Update error log formatting
+
+### Patch Changes
+
+- [#9118](https://github.com/withastro/astro/pull/9118) [`000e8f465`](https://github.com/withastro/astro/commit/000e8f4654cae9982e21e0a858366c4844139db6) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Redesign Dev Overlay main screen to show more information, such as the coolest integrations, your current Astro version and more.
+
+- [#9118](https://github.com/withastro/astro/pull/9118) [`000e8f465`](https://github.com/withastro/astro/commit/000e8f4654cae9982e21e0a858366c4844139db6) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes an issue where links with the same pathname as the current page, but different search params, were not prefetched.
+
+- [#9275](https://github.com/withastro/astro/pull/9275) [`0968cb1a3`](https://github.com/withastro/astro/commit/0968cb1a373b1101a649035d2ea2210d3d6412dc) Thanks [@lilnasy](https://github.com/lilnasy)! - Fixes an issue where html annotations relevant only to the dev server were included in the production build.
+
+- [#9252](https://github.com/withastro/astro/pull/9252) [`7b74ec4ba`](https://github.com/withastro/astro/commit/7b74ec4ba48e363a19d20e322212d0d264927f1b) Thanks [@ematipico](https://github.com/ematipico)! - Consistently emit fallback routes in the correct folders, and emit routes that consider `trailingSlash`
+
+- [#9222](https://github.com/withastro/astro/pull/9222) [`279e3c1b3`](https://github.com/withastro/astro/commit/279e3c1b3d06e7b48f01c0ef8285c3719ac74ace) Thanks [@matthewp](https://github.com/matthewp)! - Ensure the dev-overlay-window is anchored to the bottom
+
+- [#9292](https://github.com/withastro/astro/pull/9292) [`5428b3da0`](https://github.com/withastro/astro/commit/5428b3da08493d933981c4646d5d132fb31f0d25) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Improves display for `astro preferences list` command
+
+- [#9235](https://github.com/withastro/astro/pull/9235) [`9c2342c32`](https://github.com/withastro/astro/commit/9c2342c327a13d2f7d1eb387b743e81f431b9813) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fix SVG icons not showing properly in the extended dropdown menu of the dev overlay
+
+- [#9218](https://github.com/withastro/astro/pull/9218) [`f4401c8c1`](https://github.com/withastro/astro/commit/f4401c8c1fa203431b4e7b2e89381a91b4ef1ac6) Thanks [@matthewp](https://github.com/matthewp)! - Improve high contrast mode with the Dev Overlay
+
+- [#9254](https://github.com/withastro/astro/pull/9254) [`b750a161e`](https://github.com/withastro/astro/commit/b750a161e0e059de9cf814ce271d5891e4e97cbe) Thanks [@matthewp](https://github.com/matthewp)! - Improve highlight/tooltip positioning when in fixed positions
+
+- [#9230](https://github.com/withastro/astro/pull/9230) [`60cfa49e4`](https://github.com/withastro/astro/commit/60cfa49e445c926288612a6b1a30113ab988011c) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Update the look and feel of the dev overlay
+
+- [#9248](https://github.com/withastro/astro/pull/9248) [`43ddb5217`](https://github.com/withastro/astro/commit/43ddb5217691dc4112d8d98ae07511a8be6d4b94) Thanks [@martrapp](https://github.com/martrapp)! - Adds properties of the submit button (name, value) to the form data of a view transition
+
+- [#9170](https://github.com/withastro/astro/pull/9170) [`8a228fce0`](https://github.com/withastro/astro/commit/8a228fce0114daeea2100e50ddc5cf2ea0a03b5d) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Adds new accessibility audits to the Dev Toolbar's built-in Audits app.
+
+ The audits Astro performs are non-exhaustive and only capable of detecting a handful of common accessibility issues. Please take care to perform a thorough, **manual** audit of your site to ensure compliance with the [Web Content Accessibility Guidelines (WCAG) international standard](https://www.w3.org/WAI/standards-guidelines/wcag/) _before_ publishing your site.
+
+ 🧡 Huge thanks to the [Svelte](https://github.com/sveltejs/svelte) team for providing the basis of these accessibility audits!
+
+- [#9149](https://github.com/withastro/astro/pull/9149) [`0fe3a7ed5`](https://github.com/withastro/astro/commit/0fe3a7ed5d7bb1a9fce1623e84ba14104b51223c) Thanks [@bluwy](https://github.com/bluwy)! - Removes vendored Vite's `importMeta.d.ts` file in favour of Vite 5's new `vite/types/import-meta.d.ts` export
+
+- [#9295](https://github.com/withastro/astro/pull/9295) [`3d2dbb0e5`](https://github.com/withastro/astro/commit/3d2dbb0e5d2bf67b38ff8533d4dd938c94433812) Thanks [@matthewp](https://github.com/matthewp)! - Remove aria-query package
+
+ This is another CJS-only package that breaks usage.
+
+- [#9274](https://github.com/withastro/astro/pull/9274) [`feaba2c7f`](https://github.com/withastro/astro/commit/feaba2c7fc0a48d3af7dd98e6b750ec1e8274e33) Thanks [@TheOtterlord](https://github.com/TheOtterlord)! - Fix routing prefixes when `prefixDefaultLocale` is `true`
+
+- [#9273](https://github.com/withastro/astro/pull/9273) [`9887f2412`](https://github.com/withastro/astro/commit/9887f241241f800e2907afe7079db070f3bfcfab) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - Exports type for Dev Toolbar App under correct name
+
+- [#9150](https://github.com/withastro/astro/pull/9150) [`710be505c`](https://github.com/withastro/astro/commit/710be505c9ddf416e77a75343d8cae9c497d72c6) Thanks [@bluwy](https://github.com/bluwy)! - Refactors virtual modules exports. This should not break your project unless you import Astro's internal modules, including:
+
+ - `astro/middleware/namespace`
+ - `astro/transitions`
+ - `astro/transitions/router`
+ - `astro/transitions/events`
+ - `astro/transitions/types`
+ - `astro/prefetch`
+ - `astro/i18n`
+
+- [#9227](https://github.com/withastro/astro/pull/9227) [`4b8a42406`](https://github.com/withastro/astro/commit/4b8a42406bbdcc68604ea4ecc2a926721fbc4d52) Thanks [@matthewp](https://github.com/matthewp)! - Ensure overlay x-ray z-index is higher than the island
+
+- [#9255](https://github.com/withastro/astro/pull/9255) [`9ea3e0b94`](https://github.com/withastro/astro/commit/9ea3e0b94f7c4813c52bffd78043f90fd87dffda) Thanks [@matthewp](https://github.com/matthewp)! - Adds instructions on how to hide the dev overlay
+
+- [#9293](https://github.com/withastro/astro/pull/9293) [`cf5fa4376`](https://github.com/withastro/astro/commit/cf5fa437627ca6978ae3ff33c7894f278dfe75cd) Thanks [@matthewp](https://github.com/matthewp)! - Removes the 'a11y-role-has-required-aria-props' audit rule
+
+ This audit rule depends on a CommonJS module. To prevent blocking the 4.0 release the rule is being removed temporarily.
+
+- [#9214](https://github.com/withastro/astro/pull/9214) [`4fe523b00`](https://github.com/withastro/astro/commit/4fe523b0064b323ee46b2574339d96ea8bdb7b2d) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes a number of small user experience bugs with the dev overlay
+
+- [#9013](https://github.com/withastro/astro/pull/9013) [`ff8eadb95`](https://github.com/withastro/astro/commit/ff8eadb95d34833baaf3ec7575bf4f293eae97da) Thanks [@bayssmekanique](https://github.com/bayssmekanique)! - Returns the updated config in the integration `astro:config:setup` hook's `updateConfig()` API
+
+- Updated dependencies [[`abf601233`](https://github.com/withastro/astro/commit/abf601233f8188d118a8cb063c777478d8d9f1a3), [`addb57c8e`](https://github.com/withastro/astro/commit/addb57c8e80b7b67ec61224666f3a1db5c44410c), [`c7953645e`](https://github.com/withastro/astro/commit/c7953645eeaaf9e87c6db4494b0023d2c1878ff0)]:
+ - @astrojs/markdown-remark@4.0.0
+
+## 4.0.0-beta.7
+
+### Patch Changes
+
+- [#9295](https://github.com/withastro/astro/pull/9295) [`3d2dbb0e5`](https://github.com/withastro/astro/commit/3d2dbb0e5d2bf67b38ff8533d4dd938c94433812) Thanks [@matthewp](https://github.com/matthewp)! - Remove aria-query package
+
+ This is another CJS-only package that breaks usage.
+
+## 4.0.0-beta.6
+
+### Patch Changes
+
+- [#9275](https://github.com/withastro/astro/pull/9275) [`0968cb1a3`](https://github.com/withastro/astro/commit/0968cb1a373b1101a649035d2ea2210d3d6412dc) Thanks [@lilnasy](https://github.com/lilnasy)! - Fixes an issue where html annotations relevant only to the dev server were included in the production build.
+
+- [#9292](https://github.com/withastro/astro/pull/9292) [`5428b3da0`](https://github.com/withastro/astro/commit/5428b3da08493d933981c4646d5d132fb31f0d25) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Improves display for `astro preferences list` command
+
+- [#9293](https://github.com/withastro/astro/pull/9293) [`cf5fa4376`](https://github.com/withastro/astro/commit/cf5fa437627ca6978ae3ff33c7894f278dfe75cd) Thanks [@matthewp](https://github.com/matthewp)! - Removes the 'a11y-role-has-required-aria-props' audit rule
+
+ This audit rule depends on a CommonJS module. To prevent blocking the 4.0 release the rule is being removed temporarily.
+
+## 4.0.0-beta.5
+
+### Minor Changes
+
+- [#9279](https://github.com/withastro/astro/pull/9279) [`6a9669b81`](https://github.com/withastro/astro/commit/6a9669b810ddfcae6c537165a438190ea1e7a4bc) Thanks [@martrapp](https://github.com/martrapp)! - Improves consistency between navigations with and without ``. See [#9279](https://github.com/withastro/astro/pull/9279) for more details.
+
+### Patch Changes
+
+- [#9170](https://github.com/withastro/astro/pull/9170) [`8a228fce0`](https://github.com/withastro/astro/commit/8a228fce0114daeea2100e50ddc5cf2ea0a03b5d) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Adds new accessibility audits to the Dev Toolbar's built-in Audits app.
+
+ The audits Astro performs are non-exhaustive and only capable of detecting a handful of common accessibility issues. Please take care to perform a thorough, **manual** audit of your site to ensure compliance with the [Web Content Accessibility Guidelines (WCAG) international standard](https://www.w3.org/WAI/standards-guidelines/wcag/) _before_ publishing your site.
+
+ 🧡 Huge thanks to the [Svelte](https://github.com/sveltejs/svelte) team for providing the basis of these accessibility audits!
+
+- [#9274](https://github.com/withastro/astro/pull/9274) [`feaba2c7f`](https://github.com/withastro/astro/commit/feaba2c7fc0a48d3af7dd98e6b750ec1e8274e33) Thanks [@TheOtterlord](https://github.com/TheOtterlord)! - Fix routing prefixes when `prefixDefaultLocale` is `true`
+
+- [#9273](https://github.com/withastro/astro/pull/9273) [`9887f2412`](https://github.com/withastro/astro/commit/9887f241241f800e2907afe7079db070f3bfcfab) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - Exports type for Dev Toolbar App under correct name
+
+## 4.0.0-beta.4
+
+### Major Changes
+
+- [#9271](https://github.com/withastro/astro/pull/9271) [`47604bd5b`](https://github.com/withastro/astro/commit/47604bd5b5bb2ea63922b657bac104c010575c20) Thanks [@matthewp](https://github.com/matthewp)! - Renames Dev Overlay to Dev Toolbar
+
+ The previously named experimental Dev Overlay is now known as the Astro Dev Toolbar. Plugins have been renamed as Toolbar Apps. This updates our references to reflect.
+
+ To not break existing APIs, aliases for the Toolbar-based names have been created. The previous API names will continue to function but will be deprecated in the future. All documentation has been updated to reflect Toolbar-based names.
+
+## 4.0.0-beta.3
+
+### Major Changes
+
+- [#9263](https://github.com/withastro/astro/pull/9263) [`3cbd8ea75`](https://github.com/withastro/astro/commit/3cbd8ea7534910e3beae396dcfa93ce87dcdd91f) Thanks [@bluwy](https://github.com/bluwy)! - Removes additional deprecated APIs:
+
+ - The Astro preview server now returns a 404 status instead of a 301 redirect when requesting assets from the public directory without a base.
+ - Removes special handling when referencing the `astro/client-image` type. You should use the `astro/client` type instead.
+ - Removes deprecated built-in `rss` support in `getStaticPaths`. You should use `@astrojs/rss` instead.
+ - Removes deprecated `Astro.request.params` support. You should use `Astro.params` instead.
+
+### Minor Changes
+
+- [#9200](https://github.com/withastro/astro/pull/9200) [`b4b851f5a`](https://github.com/withastro/astro/commit/b4b851f5a46b32ee531db5dc39ccd2aa7af7bcfd) Thanks [@ematipico](https://github.com/ematipico)! - Adds a new way to configure the `i18n.locales` array.
+
+ Developers can now assign a custom URL path prefix that can span multiple language codes:
+
+ ```js
+ // astro.config.mjs
+ export default defineConfig({
+ experimental: {
+ i18n: {
+ defaultLocale: 'english',
+ locales: ['de', { path: 'english', codes: ['en', 'en-US'] }, 'fr'],
+ routingStrategy: 'prefix-always',
+ },
+ },
+ });
+ ```
+
+ With the above configuration, the URL prefix of the default locale will be `/english/`. When computing `Astro.preferredLocale`, Astro will use the `codes`.
+
+- [#9139](https://github.com/withastro/astro/pull/9139) [`459b26436`](https://github.com/withastro/astro/commit/459b2643666db08dbd29a100ce3d8697b451d3fe) Thanks [@bluwy](https://github.com/bluwy)! - Reworks Vite's logger to use Astro's logger to correctly log HMR messages
+
+### Patch Changes
+
+- [#9252](https://github.com/withastro/astro/pull/9252) [`7b74ec4ba`](https://github.com/withastro/astro/commit/7b74ec4ba48e363a19d20e322212d0d264927f1b) Thanks [@ematipico](https://github.com/ematipico)! - Consistently emit fallback routes in the correct folders, and emit routes that
+ consider `trailingSlash`
+
+- [#9235](https://github.com/withastro/astro/pull/9235) [`9c2342c32`](https://github.com/withastro/astro/commit/9c2342c327a13d2f7d1eb387b743e81f431b9813) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fix SVG icons not showing properly in the extended dropdown menu of the dev overlay
+
+- [#9254](https://github.com/withastro/astro/pull/9254) [`b750a161e`](https://github.com/withastro/astro/commit/b750a161e0e059de9cf814ce271d5891e4e97cbe) Thanks [@matthewp](https://github.com/matthewp)! - Improve highlight/tooltip positioning when in fixed positions
+
+- [#9230](https://github.com/withastro/astro/pull/9230) [`60cfa49e4`](https://github.com/withastro/astro/commit/60cfa49e445c926288612a6b1a30113ab988011c) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Update the look and feel of the dev overlay
+
+- [#9248](https://github.com/withastro/astro/pull/9248) [`43ddb5217`](https://github.com/withastro/astro/commit/43ddb5217691dc4112d8d98ae07511a8be6d4b94) Thanks [@martrapp](https://github.com/martrapp)! - Adds properties of the submit button (name, value) to the form data of a view transition
+
+- [#9255](https://github.com/withastro/astro/pull/9255) [`9ea3e0b94`](https://github.com/withastro/astro/commit/9ea3e0b94f7c4813c52bffd78043f90fd87dffda) Thanks [@matthewp](https://github.com/matthewp)! - Adds instructions on how to hide the dev overlay
+
+- [#9013](https://github.com/withastro/astro/pull/9013) [`ff8eadb95`](https://github.com/withastro/astro/commit/ff8eadb95d34833baaf3ec7575bf4f293eae97da) Thanks [@bayssmekanique](https://github.com/bayssmekanique)! - Returns the updated config in the integration `astro:config:setup` hook's `updateConfig()` API
+
+## 4.0.0-beta.2
+
+### Major Changes
+
+- [#9225](https://github.com/withastro/astro/pull/9225) [`c421a3d17`](https://github.com/withastro/astro/commit/c421a3d17911aeda29b5204f6d568ae87e329eaf) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Removes the opt-in `handleForms` property for ` `. Form submissions are now handled by default and can be disabled by setting `data-astro-reload` on relevant `` elements.
+
+- [#9199](https://github.com/withastro/astro/pull/9199) [`49aa215a0`](https://github.com/withastro/astro/commit/49aa215a01ee1c4805316c85bb0aea6cfbc25a31) Thanks [@lilnasy](https://github.com/lilnasy)! - This change only affects maintainers of third-party adapters. In the Integration API, the `app.render()` method of the `App` class has been simplified.
+
+ Instead of two optional arguments, it now takes a single optional argument that is an object with two optional properties: `routeData` and `locals`.
+
+ ```diff
+ app.render(request)
+
+ - app.render(request, routeData)
+ + app.render(request, { routeData })
+
+ - app.render(request, routeData, locals)
+ + app.render(request, { routeData, locals })
+
+ - app.render(request, undefined, locals)
+ + app.render(request, { locals })
+ ```
+
+ The current signature is deprecated but will continue to function until next major version.
+
+- [#9212](https://github.com/withastro/astro/pull/9212) [`c0383ea0c`](https://github.com/withastro/astro/commit/c0383ea0c102cb62b7235823c706a090ba08715f) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - Removes deprecated `app.match()` option, `matchNotFound`
+
+### Minor Changes
+
+- [#9115](https://github.com/withastro/astro/pull/9115) [`3b77889b4`](https://github.com/withastro/astro/commit/3b77889b47750ed6e17c7858780dc4aae9201b58) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Adds the `astro preferences` command to manage user preferences. User preferences are specific to individual Astro users, unlike the `astro.config.mjs` file which changes behavior for everyone working on a project.
+
+ User preferences are scoped to the current project by default, stored in a local `.astro/settings.json` file. Using the `--global` flag, user preferences can also be applied to every Astro project on the current machine. Global user preferences are stored in an operating system-specific location.
+
+ ```sh
+ # Disable the dev overlay for the current user in the current project
+ npm run astro preferences disable devOverlay
+ # Disable the dev overlay for the current user in all Astro projects on this machine
+ npm run astro preferences --global disable devOverlay
+
+ # Check if the dev overlay is enabled for the current user
+ npm run astro preferences list devOverlay
+ ```
+
+- [#9129](https://github.com/withastro/astro/pull/9129) [`8bfc20511`](https://github.com/withastro/astro/commit/8bfc20511918d675202cdc100d4efab293e5cbac) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Update error log formatting
+
+### Patch Changes
+
+- [#9222](https://github.com/withastro/astro/pull/9222) [`279e3c1b3`](https://github.com/withastro/astro/commit/279e3c1b3d06e7b48f01c0ef8285c3719ac74ace) Thanks [@matthewp](https://github.com/matthewp)! - Ensure the dev-overlay-window is anchored to the bottom
+
+- [#9218](https://github.com/withastro/astro/pull/9218) [`f4401c8c1`](https://github.com/withastro/astro/commit/f4401c8c1fa203431b4e7b2e89381a91b4ef1ac6) Thanks [@matthewp](https://github.com/matthewp)! - Improve high contrast mode with the Dev Overlay
+
+- [#9227](https://github.com/withastro/astro/pull/9227) [`4b8a42406`](https://github.com/withastro/astro/commit/4b8a42406bbdcc68604ea4ecc2a926721fbc4d52) Thanks [@matthewp](https://github.com/matthewp)! - Ensure overlay x-ray z-index is higher than the island
+
+- [#9214](https://github.com/withastro/astro/pull/9214) [`4fe523b00`](https://github.com/withastro/astro/commit/4fe523b0064b323ee46b2574339d96ea8bdb7b2d) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes a number of small user experience bugs with the dev overlay
+
+## 4.0.0-beta.1
+
+### Patch Changes
+
+- [#9118](https://github.com/withastro/astro/pull/9118) [`000e8f465`](https://github.com/withastro/astro/commit/000e8f4654cae9982e21e0a858366c4844139db6) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Redesign Dev Overlay main screen to show more information, such as the coolest integrations, your current Astro version and more.
+
+- [#9118](https://github.com/withastro/astro/pull/9118) [`000e8f465`](https://github.com/withastro/astro/commit/000e8f4654cae9982e21e0a858366c4844139db6) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes an issue where links with the same pathname as the current page, but different search params, were not prefetched.
+
+## 4.0.0-beta.0
+
+### Major Changes
+
+- [#9138](https://github.com/withastro/astro/pull/9138) [`abf601233`](https://github.com/withastro/astro/commit/abf601233f8188d118a8cb063c777478d8d9f1a3) Thanks [@bluwy](https://github.com/bluwy)! - Updates the unified, remark, and rehype dependencies to latest. Make sure to update your custom remark and rehype plugins as well to be compatible with the latest versions.
+
+ **Potentially breaking change:** The default value of `markdown.remarkRehype.footnoteBackLabel` is changed from `"Back to content"` to `"Back to reference 1"`. See the `mdast-util-to-hast` [commit](https://github.com/syntax-tree/mdast-util-to-hast/commit/56c88e45690be138fad9f0bf367b939d09816863) for more information.
+
+- [#9181](https://github.com/withastro/astro/pull/9181) [`cdabf6ef0`](https://github.com/withastro/astro/commit/cdabf6ef02be7220fd2b6bdcef924ceca089381e) Thanks [@bluwy](https://github.com/bluwy)! - Removes support for returning simple objects from endpoints (deprecated since Astro 3.0). You should return a `Response` instead.
+
+ `ResponseWithEncoding` is also removed. You can refactor the code to return a response with an array buffer instead, which is encoding agnostic.
+
+ The types for middlewares have also been revised. To type a middleware function, you should now use `MiddlewareHandler` instead of `MiddlewareResponseHandler`. If you used `defineMiddleware()` to type the function, no changes are needed.
+
+- [#9122](https://github.com/withastro/astro/pull/9122) [`1c48ed286`](https://github.com/withastro/astro/commit/1c48ed286538ab9e354eca4e4dcd7c6385c96721) Thanks [@bluwy](https://github.com/bluwy)! - Adds Vite 5 support. There are no breaking changes from Astro. Check the [Vite migration guide](https://vitejs.dev/guide/migration.html) for details of the breaking changes from Vite instead.
+
+- [#9196](https://github.com/withastro/astro/pull/9196) [`37697a2c5`](https://github.com/withastro/astro/commit/37697a2c5511572dc29c0a4ea46f90c2f62be8e6) Thanks [@bluwy](https://github.com/bluwy)! - Removes support for Shiki custom language's `path` property. The language JSON file should be imported and passed to the option instead.
+
+ ```diff
+ // astro.config.js
+ + import customLang from './custom.tmLanguage.json'
+
+ export default defineConfig({
+ markdown: {
+ shikiConfig: {
+ langs: [
+ - { path: './custom.tmLanguage.json' },
+ + customLang,
+ ],
+ },
+ },
+ })
+ ```
+
+- [#9168](https://github.com/withastro/astro/pull/9168) [`153a5abb9`](https://github.com/withastro/astro/commit/153a5abb905042ac68b712514dc9ec387d3e6b17) Thanks [@bluwy](https://github.com/bluwy)! - Removes deprecated features from Astro 3.0
+
+ - Adapters are now required to pass `supportedAstroFeatures` to specify a list of features they support.
+ - The `build.split` and `build.excludeMiddleware` options are removed. Use `functionPerRoute` and `edgeMiddleware` from adapters instead.
+ - The `markdown.drafts` option and draft feature is removed. Use content collections instead.
+ - Lowercase endpoint names are no longer supported. Use uppercase endpoint names instead.
+ - `getHeaders()` exported from markdown files is removed. Use `getHeadings()` instead.
+
+### Minor Changes
+
+- [#9105](https://github.com/withastro/astro/pull/9105) [`6201bbe96`](https://github.com/withastro/astro/commit/6201bbe96c2a083fb201e4a43a9bd88499821a3e) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Update CLI logging experience
+
+- [#9161](https://github.com/withastro/astro/pull/9161) [`bd0c2e9ae`](https://github.com/withastro/astro/commit/bd0c2e9ae3389a9d3085050c1e8134ae98dff299) Thanks [@bluwy](https://github.com/bluwy)! - Renames the `entryPoint` property of the `injectRoute` integrations API to `entrypoint` for consistency. A warning will be shown prompting you to update your code when using the old name.
+
+### Patch Changes
+
+- [#9149](https://github.com/withastro/astro/pull/9149) [`0fe3a7ed5`](https://github.com/withastro/astro/commit/0fe3a7ed5d7bb1a9fce1623e84ba14104b51223c) Thanks [@bluwy](https://github.com/bluwy)! - Removes vendored Vite's `importMeta.d.ts` file in favour of Vite 5's new `vite/types/import-meta.d.ts` export
+
+- [#9150](https://github.com/withastro/astro/pull/9150) [`710be505c`](https://github.com/withastro/astro/commit/710be505c9ddf416e77a75343d8cae9c497d72c6) Thanks [@bluwy](https://github.com/bluwy)! - Refactors virtual modules exports. This should not break your project unless you import Astro's internal modules, including:
+
+ - `astro/middleware/namespace`
+ - `astro/transitions`
+ - `astro/transitions/router`
+ - `astro/transitions/events`
+ - `astro/transitions/types`
+ - `astro/prefetch`
+ - `astro/i18n`
+
+- Updated dependencies [[`abf601233`](https://github.com/withastro/astro/commit/abf601233f8188d118a8cb063c777478d8d9f1a3), [`addb57c8e`](https://github.com/withastro/astro/commit/addb57c8e80b7b67ec61224666f3a1db5c44410c), [`c7953645e`](https://github.com/withastro/astro/commit/c7953645eeaaf9e87c6db4494b0023d2c1878ff0)]:
+ - @astrojs/markdown-remark@4.0.0-beta.0
+
+## 3.6.4
+
+### Patch Changes
+
+- [#9226](https://github.com/withastro/astro/pull/9226) [`8f8a40e93`](https://github.com/withastro/astro/commit/8f8a40e93d6a0774ba84a6f5db8c42cd81db005e) Thanks [@outofambit](https://github.com/outofambit)! - Fix i18n fallback routing with routing strategy of always-prefix
+
+- [#9179](https://github.com/withastro/astro/pull/9179) [`3f28336d9`](https://github.com/withastro/astro/commit/3f28336d9a52d7e4364d455ee3128d14d10a078a) Thanks [@lilnasy](https://github.com/lilnasy)! - Fixes an issue where the presence of a slot in a page led to an error.
+
+- [#9219](https://github.com/withastro/astro/pull/9219) [`067a65f5b`](https://github.com/withastro/astro/commit/067a65f5b4d163bf1944cf47e6bf891f0b93553f) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Fix edge case where `
-{handleForms ? : ''}
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/non-html-anchor.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/non-html-anchor.astro
new file mode 100644
index 0000000000000..8d5ea8d46c769
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/non-html-anchor.astro
@@ -0,0 +1,22 @@
+---
+import Layout from '../components/Layout.astro';
+---
+
+SVGA and Image Map links
+
+
+
+ text within a svga
+
+
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/vue-component/package.json b/packages/astro/e2e/fixtures/vue-component/package.json
index 81e7997830e35..091f3643558ce 100644
--- a/packages/astro/e2e/fixtures/vue-component/package.json
+++ b/packages/astro/e2e/fixtures/vue-component/package.json
@@ -6,6 +6,6 @@
"@astrojs/mdx": "workspace:*",
"@astrojs/vue": "workspace:*",
"astro": "workspace:*",
- "vue": "^3.3.4"
+ "vue": "^3.3.8"
}
}
diff --git a/packages/astro/e2e/hmr.test.js b/packages/astro/e2e/hmr.test.js
index 091aa716d6897..5e7e232af96fa 100644
--- a/packages/astro/e2e/hmr.test.js
+++ b/packages/astro/e2e/hmr.test.js
@@ -3,6 +3,9 @@ import { testFactory } from './test-utils.js';
const test = testFactory({
root: './fixtures/hmr/',
+ devToolbar: {
+ enabled: false,
+ },
});
let devServer;
diff --git a/packages/astro/e2e/prefetch.test.js b/packages/astro/e2e/prefetch.test.js
index dc29bde33f07f..a19c87680ecac 100644
--- a/packages/astro/e2e/prefetch.test.js
+++ b/packages/astro/e2e/prefetch.test.js
@@ -16,7 +16,8 @@ test.describe('Prefetch (default)', () => {
test.beforeEach(async ({ page }) => {
page.on('request', (req) => {
- reqUrls.push(new URL(req.url()).pathname);
+ const urlObj = new URL(req.url());
+ reqUrls.push(urlObj.pathname + urlObj.search);
});
});
@@ -38,6 +39,16 @@ test.describe('Prefetch (default)', () => {
expect(reqUrls).not.toContainEqual('/prefetch-false');
});
+ test('Link with search param should prefetch', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+ expect(reqUrls).not.toContainEqual('/?search-param=true');
+ await Promise.all([
+ page.waitForEvent('request'), // wait prefetch request
+ page.locator('#prefetch-search-param').hover(),
+ ]);
+ expect(reqUrls).toContainEqual('/?search-param=true');
+ });
+
test('data-astro-prefetch="tap" should prefetch on tap', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
expect(reqUrls).not.toContainEqual('/prefetch-tap');
@@ -102,7 +113,8 @@ test.describe("Prefetch (prefetchAll: true, defaultStrategy: 'tap')", () => {
test.beforeEach(async ({ page }) => {
page.on('request', (req) => {
- reqUrls.push(new URL(req.url()).pathname);
+ const urlObj = new URL(req.url());
+ reqUrls.push(urlObj.pathname + urlObj.search);
});
});
@@ -129,6 +141,16 @@ test.describe("Prefetch (prefetchAll: true, defaultStrategy: 'tap')", () => {
expect(reqUrls).not.toContainEqual('/prefetch-false');
});
+ test('Link with search param should prefetch', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+ expect(reqUrls).not.toContainEqual('/?search-param=true');
+ await Promise.all([
+ page.waitForEvent('request'), // wait prefetch request
+ page.locator('#prefetch-search-param').hover(),
+ ]);
+ expect(reqUrls).toContainEqual('/?search-param=true');
+ });
+
test('data-astro-prefetch="tap" should prefetch on tap', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
expect(reqUrls).not.toContainEqual('/prefetch-tap');
diff --git a/packages/astro/e2e/react-component.test.js b/packages/astro/e2e/react-component.test.js
index b19a071d664a1..361ee8d69a2a4 100644
--- a/packages/astro/e2e/react-component.test.js
+++ b/packages/astro/e2e/react-component.test.js
@@ -46,7 +46,6 @@ test.describe('React client id generation', () => {
const hydratedId1 = await components.nth(2).getAttribute('id');
const clientOnlyId0 = await components.nth(3).getAttribute('id');
const clientOnlyId1 = await components.nth(4).getAttribute('id');
- console.log('ho ho', staticId, hydratedId0, hydratedId1, clientOnlyId0, clientOnlyId1);
expect(staticId).not.toEqual(hydratedId0);
expect(hydratedId0).not.toEqual(hydratedId1);
expect(hydratedId1).not.toEqual(clientOnlyId0);
diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js
index 20ea8adbc302f..222c9dfdf2aa4 100644
--- a/packages/astro/e2e/view-transitions.test.js
+++ b/packages/astro/e2e/view-transitions.test.js
@@ -394,7 +394,10 @@ test.describe('View Transitions', () => {
await expect(locator).toBeInViewport();
// Scroll back to top
+ // back returns immediately, but we need to wait for navigate() to complete
+ const waitForReady = page.waitForEvent('console');
await page.goBack();
+ await waitForReady;
locator = page.locator('#longpage');
await expect(locator).toBeInViewport();
@@ -1016,4 +1019,76 @@ test.describe('View Transitions', () => {
const result = page.locator('#three-result');
await expect(result, 'should have content').toHaveText('Got: Testing');
});
+
+ test('click on an svg anchor should trigger navigation', async ({ page, astro }) => {
+ const loads = [];
+ page.addListener('load', (p) => {
+ loads.push(p.title());
+ });
+
+ await page.goto(astro.resolveUrl('/non-html-anchor'));
+ let locator = page.locator('#insidesvga');
+ await expect(locator, 'should have attribute').toHaveAttribute('x', '10');
+ await page.click('#svga');
+ const p = page.locator('#two');
+ await expect(p, 'should have content').toHaveText('Page 2');
+ expect(loads.length, 'There should only be 1 page load').toEqual(1);
+ });
+
+ test('click inside an svg anchor should trigger navigation', async ({ page, astro }) => {
+ const loads = [];
+ page.addListener('load', (p) => {
+ loads.push(p.title());
+ });
+ await page.goto(astro.resolveUrl('/non-html-anchor'));
+ let locator = page.locator('#insidesvga');
+ await expect(locator, 'should have content').toHaveText('text within a svga');
+ await page.click('#insidesvga');
+ const p = page.locator('#two');
+ await expect(p, 'should have content').toHaveText('Page 2');
+ expect(loads.length, 'There should only be 1 page load').toEqual(1);
+ });
+
+ test('click on an area in an image map should trigger navigation', async ({ page, astro }) => {
+ const loads = [];
+ page.addListener('load', (p) => {
+ loads.push(p.title());
+ });
+ await page.goto(astro.resolveUrl('/non-html-anchor'));
+ let locator = page.locator('#area');
+ await expect(locator, 'should have attribute').toHaveAttribute('shape', 'default');
+ await page.click('#logo');
+ const p = page.locator('#two');
+ await expect(p, 'should have content').toHaveText('Page 2');
+ expect(loads.length, 'There should only be 1 page load').toEqual(1);
+ });
+
+ test('Submitter with a name property is included in form data', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/form-four'));
+
+ let locator = page.locator('h2');
+ await expect(locator, 'should have content').toHaveText('Voting Form');
+
+ // Submit the form
+ const expected = page.url() + '?stars=3';
+ await page.click('#three');
+ await expect(page).toHaveURL(expected);
+ });
+
+ test('Dialog using form with method of "dialog" should not trigger navigation', async ({
+ page,
+ astro,
+ }) => {
+ await page.goto(astro.resolveUrl('/dialog'));
+
+ let requests = [];
+ page.on('request', (request) => requests.push(`${request.method()} ${request.url()}`));
+
+ await page.click('#open');
+ await expect(page.locator('dialog')).toHaveAttribute('open');
+ await page.click('#close');
+ await expect(page.locator('dialog')).not.toHaveAttribute('open');
+
+ expect(requests).toHaveLength(0);
+ });
});
diff --git a/packages/astro/import-meta.d.ts b/packages/astro/import-meta.d.ts
deleted file mode 100644
index 23d951cf2db18..0000000000000
--- a/packages/astro/import-meta.d.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-// File vendored from Vite itself, as a workaround to https://github.com/vitejs/vite/issues/13309 until Vite 5 comes out
-
-// This file is an augmentation to the built-in ImportMeta interface
-// Thus cannot contain any top-level imports
-//
-
-/* eslint-disable @typescript-eslint/consistent-type-imports */
-
-interface ImportMeta {
- url: string;
-
- readonly hot?: import('vite/types/hot').ViteHotContext;
-
- readonly env: ImportMetaEnv;
-
- glob: import('vite/types/importGlob').ImportGlobFunction;
- /**
- * @deprecated Use `import.meta.glob('*', { eager: true })` instead
- */
- globEager: import('vite/types/importGlob').ImportGlobEagerFunction;
-}
-
-interface ImportMetaEnv {
- [key: string]: any;
- BASE_URL: string;
- MODE: string;
- DEV: boolean;
- PROD: boolean;
- SSR: boolean;
-}
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 1cb40861cddba..37338f066e44d 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -1,6 +1,6 @@
{
"name": "astro",
- "version": "3.5.5",
+ "version": "4.0.5",
"description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
"type": "module",
"author": "withastro",
@@ -22,10 +22,7 @@
"./dist/core/app/*"
],
"middleware": [
- "./dist/core/middleware/index.d.ts"
- ],
- "middleware/namespace": [
- "./dist/core/middleware/namespace.d.ts"
+ "./dist/virtual-modules/middleware.d.ts"
]
}
},
@@ -73,14 +70,7 @@
"types": "./dist/core/middleware/index.d.ts",
"default": "./dist/core/middleware/index.js"
},
- "./middleware/namespace": {
- "types": "./dist/core/middleware/namespace.d.ts",
- "default": "./dist/core/middleware/namespace.js"
- },
- "./transitions": "./dist/transitions/index.js",
- "./transitions/router": "./dist/transitions/router.js",
- "./prefetch": "./dist/prefetch/index.js",
- "./i18n": "./dist/i18n/index.js"
+ "./virtual-modules/*": "./dist/virtual-modules/*"
},
"imports": {
"#astro/*": "./dist/*.js"
@@ -122,33 +112,36 @@
"test:e2e:match": "playwright test -g"
},
"dependencies": {
- "@astrojs/compiler": "^2.3.0",
+ "@astrojs/compiler": "^2.3.2",
"@astrojs/internal-helpers": "workspace:*",
"@astrojs/markdown-remark": "workspace:*",
"@astrojs/telemetry": "workspace:*",
- "@babel/core": "^7.22.10",
- "@babel/generator": "^7.22.10",
- "@babel/parser": "^7.22.10",
+ "@babel/core": "^7.23.3",
+ "@babel/generator": "^7.23.3",
+ "@babel/parser": "^7.23.3",
"@babel/plugin-transform-react-jsx": "^7.22.5",
- "@babel/traverse": "^7.22.10",
- "@babel/types": "^7.22.10",
- "@types/babel__core": "^7.20.1",
- "acorn": "^8.10.0",
+ "@babel/traverse": "^7.23.3",
+ "@babel/types": "^7.23.3",
+ "@types/babel__core": "^7.20.4",
+ "acorn": "^8.11.2",
"boxen": "^7.1.1",
"chokidar": "^3.5.3",
- "ci-info": "^3.8.0",
+ "ci-info": "^4.0.0",
"clsx": "^2.0.0",
"common-ancestor-path": "^1.0.1",
- "cookie": "^0.5.0",
+ "cookie": "^0.6.0",
"debug": "^4.3.4",
- "deterministic-object-hash": "^1.3.1",
+ "deterministic-object-hash": "^2.0.1",
"devalue": "^4.3.2",
"diff": "^5.1.0",
- "es-module-lexer": "^1.3.0",
- "esbuild": "^0.19.2",
+ "dlv": "^1.1.3",
+ "dset": "^3.1.3",
+ "es-module-lexer": "^1.4.1",
+ "esbuild": "^0.19.6",
"estree-walker": "^3.0.3",
"execa": "^8.0.1",
- "fast-glob": "^3.3.1",
+ "fast-glob": "^3.3.2",
+ "flattie": "^1.1.0",
"github-slugger": "^2.0.0",
"gray-matter": "^4.0.3",
"html-escaper": "^3.0.3",
@@ -156,27 +149,27 @@
"js-yaml": "^4.1.0",
"kleur": "^4.1.4",
"magic-string": "^0.30.3",
- "mdast-util-to-hast": "12.3.0",
+ "mdast-util-to-hast": "13.0.2",
"mime": "^3.0.0",
"ora": "^7.0.1",
- "p-limit": "^4.0.0",
+ "p-limit": "^5.0.0",
"p-queue": "^7.4.1",
"path-to-regexp": "^6.2.1",
"preferred-pm": "^3.1.2",
"probe-image-size": "^7.2.3",
"prompts": "^2.4.2",
- "rehype": "^12.0.1",
+ "rehype": "^13.0.1",
"resolve": "^1.22.4",
"semver": "^7.5.4",
"server-destroy": "^1.0.1",
- "shikiji": "^0.6.8",
- "string-width": "^6.1.0",
+ "shikiji": "^0.6.13",
+ "string-width": "^7.0.0",
"strip-ansi": "^7.1.0",
"tsconfck": "^3.0.0",
- "unist-util-visit": "^4.1.2",
- "vfile": "^5.3.7",
- "vite": "^4.4.9",
- "vitefu": "^0.2.4",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.1",
+ "vite": "^5.0.0",
+ "vitefu": "^0.2.5",
"which-pm": "^2.1.1",
"yargs-parser": "^21.1.1",
"zod": "^3.22.4"
@@ -185,31 +178,33 @@
"sharp": "^0.32.5"
},
"devDependencies": {
- "@astrojs/check": "^0.1.0",
- "@playwright/test": "1.40.0-alpha-nov-13-2023",
- "@types/babel__generator": "^7.6.4",
- "@types/babel__traverse": "^7.20.1",
- "@types/chai": "^4.3.5",
- "@types/common-ancestor-path": "^1.0.0",
- "@types/connect": "^3.4.35",
- "@types/cookie": "^0.5.1",
- "@types/debug": "^4.1.8",
- "@types/diff": "^5.0.3",
- "@types/dom-view-transitions": "^1.0.1",
- "@types/estree": "^1.0.1",
- "@types/hast": "^2.3.5",
- "@types/html-escaper": "^3.0.0",
- "@types/http-cache-semantics": "^4.0.1",
- "@types/js-yaml": "^4.0.5",
- "@types/mime": "^3.0.1",
- "@types/mocha": "^10.0.1",
- "@types/probe-image-size": "^7.2.0",
- "@types/prompts": "^2.4.4",
- "@types/resolve": "^1.20.2",
- "@types/send": "^0.17.1",
- "@types/server-destroy": "^1.0.1",
- "@types/unist": "^2.0.7",
- "@types/yargs-parser": "^21.0.0",
+ "@astrojs/check": "^0.3.1",
+ "@playwright/test": "1.40.0",
+ "@types/babel__generator": "^7.6.7",
+ "@types/babel__traverse": "^7.20.4",
+ "@types/chai": "^4.3.10",
+ "@types/common-ancestor-path": "^1.0.2",
+ "@types/connect": "^3.4.38",
+ "@types/cookie": "^0.5.4",
+ "@types/debug": "^4.1.12",
+ "@types/diff": "^5.0.8",
+ "@types/dlv": "^1.1.4",
+ "@types/dom-view-transitions": "^1.0.4",
+ "@types/estree": "^1.0.5",
+ "@types/hast": "^3.0.3",
+ "@types/html-escaper": "^3.0.2",
+ "@types/http-cache-semantics": "^4.0.4",
+ "@types/js-yaml": "^4.0.9",
+ "@types/mime": "^3.0.4",
+ "@types/mocha": "^10.0.4",
+ "@types/probe-image-size": "^7.2.3",
+ "@types/prompts": "^2.4.8",
+ "@types/resolve": "^1.20.5",
+ "@types/semver": "^7.5.2",
+ "@types/send": "^0.17.4",
+ "@types/server-destroy": "^1.0.3",
+ "@types/unist": "^3.0.2",
+ "@types/yargs-parser": "^21.0.3",
"astro-scripts": "workspace:*",
"chai": "^4.3.7",
"cheerio": "1.0.0-rc.12",
@@ -218,14 +213,14 @@
"mocha": "^10.2.0",
"node-mocks-http": "^1.13.0",
"parse-srcset": "^1.0.2",
- "rehype-autolink-headings": "^6.1.1",
- "rehype-slug": "^5.0.1",
+ "rehype-autolink-headings": "^7.1.0",
+ "rehype-slug": "^6.0.0",
"rehype-toc": "^3.0.2",
"remark-code-titles": "^0.1.2",
- "rollup": "^3.28.1",
- "sass": "^1.66.1",
+ "rollup": "^4.5.0",
+ "sass": "^1.69.5",
"srcset-parse": "^1.1.0",
- "unified": "^10.1.2"
+ "unified": "^11.0.4"
},
"engines": {
"node": ">=18.14.1",
diff --git a/packages/astro/performance/content-benchmark.mjs b/packages/astro/performance/content-benchmark.mjs
index 14f9b598e3b39..c2d1d69184def 100644
--- a/packages/astro/performance/content-benchmark.mjs
+++ b/packages/astro/performance/content-benchmark.mjs
@@ -44,14 +44,14 @@ async function benchmark({ fixtures, templates, numPosts }) {
const test = Array.isArray(flags.test)
? flags.test
: typeof flags.test === 'string'
- ? [flags.test]
- : ['simple', 'with-astro-components', 'with-react-components'];
+ ? [flags.test]
+ : ['simple', 'with-astro-components', 'with-react-components'];
const formats = Array.isArray(flags.format)
? flags.format
: typeof flags.format === 'string'
- ? [flags.format]
- : ['md', 'mdx', 'mdoc'];
+ ? [flags.format]
+ : ['md', 'mdx', 'mdoc'];
const numPosts = flags.numPosts || 1000;
diff --git a/packages/astro/performance/fixtures/md/package.json b/packages/astro/performance/fixtures/md/package.json
index ff7b1dec0df6c..4dea64d76530c 100644
--- a/packages/astro/performance/fixtures/md/package.json
+++ b/packages/astro/performance/fixtures/md/package.json
@@ -16,8 +16,8 @@
"dependencies": {
"@astrojs/react": "workspace:*",
"@performance/utils": "workspace:*",
- "@types/react": "^18.2.21",
- "@types/react-dom": "^18.2.7",
+ "@types/react": "^18.2.37",
+ "@types/react-dom": "^18.2.15",
"astro": "workspace:*",
"react": "^18.0.0",
"react-dom": "^18.0.0"
diff --git a/packages/astro/performance/fixtures/mdoc/package.json b/packages/astro/performance/fixtures/mdoc/package.json
index 351ef15e49488..a25e69c971b25 100644
--- a/packages/astro/performance/fixtures/mdoc/package.json
+++ b/packages/astro/performance/fixtures/mdoc/package.json
@@ -17,8 +17,8 @@
"@astrojs/markdoc": "workspace:*",
"@astrojs/react": "workspace:*",
"@performance/utils": "workspace:*",
- "@types/react": "^18.2.21",
- "@types/react-dom": "^18.2.7",
+ "@types/react": "^18.2.37",
+ "@types/react-dom": "^18.2.15",
"astro": "workspace:*",
"react": "^18.0.0",
"react-dom": "^18.0.0"
diff --git a/packages/astro/performance/fixtures/mdx/package.json b/packages/astro/performance/fixtures/mdx/package.json
index c2d4e368a8727..2144dc2258199 100644
--- a/packages/astro/performance/fixtures/mdx/package.json
+++ b/packages/astro/performance/fixtures/mdx/package.json
@@ -17,8 +17,8 @@
"@astrojs/mdx": "workspace:*",
"@astrojs/react": "workspace:*",
"@performance/utils": "workspace:*",
- "@types/react": "^18.2.21",
- "@types/react-dom": "^18.2.7",
+ "@types/react": "^18.2.37",
+ "@types/react-dom": "^18.2.15",
"astro": "workspace:*",
"react": "^18.0.0",
"react-dom": "^18.0.0"
diff --git a/packages/astro/performance/package.json b/packages/astro/performance/package.json
index e09f291736c20..25bfad16b4af8 100644
--- a/packages/astro/performance/package.json
+++ b/packages/astro/performance/package.json
@@ -11,7 +11,7 @@
"author": "",
"license": "ISC",
"devDependencies": {
- "@types/yargs-parser": "^21.0.0",
+ "@types/yargs-parser": "^21.0.3",
"cross-env": "^7.0.3",
"kleur": "^4.1.5",
"npm-run-all": "^4.1.5",
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 6477f738396ca..fa8c33920ac03 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -19,16 +19,22 @@ import type { AstroConfigType } from '../core/config/index.js';
import type { AstroTimer } from '../core/config/timer.js';
import type { TSConfig } from '../core/config/tsconfig.js';
import type { AstroCookies } from '../core/cookies/index.js';
-import type { ResponseWithEncoding } from '../core/endpoint/index.js';
import type { AstroIntegrationLogger, Logger, LoggerLevel } from '../core/logger/core.js';
+import type { AstroPreferences } from '../preferences/index.js';
import type { AstroDevOverlay, DevOverlayCanvas } from '../runtime/client/dev-overlay/overlay.js';
-import type { DevOverlayHighlight } from '../runtime/client/dev-overlay/ui-library/highlight.js';
import type { Icon } from '../runtime/client/dev-overlay/ui-library/icons.js';
-import type { DevOverlayToggle } from '../runtime/client/dev-overlay/ui-library/toggle.js';
-import type { DevOverlayTooltip } from '../runtime/client/dev-overlay/ui-library/tooltip.js';
-import type { DevOverlayWindow } from '../runtime/client/dev-overlay/ui-library/window.js';
+import type {
+ DevOverlayBadge,
+ DevOverlayButton,
+ DevOverlayCard,
+ DevOverlayHighlight,
+ DevOverlayIcon,
+ DevOverlayToggle,
+ DevOverlayTooltip,
+ DevOverlayWindow,
+} from '../runtime/client/dev-overlay/ui-library/index.js';
import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server/index.js';
-import type { OmitIndexSignature, Simplify } from '../type-utils.js';
+import type { DeepPartial, OmitIndexSignature, Simplify } from '../type-utils.js';
import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
export { type AstroIntegrationLogger };
@@ -126,6 +132,10 @@ export interface AstroScriptAttributes {
'is:inline'?: boolean;
}
+export interface AstroSlotAttributes {
+ 'is:inline'?: boolean;
+}
+
export interface AstroComponentMetadata {
displayName: string;
hydrate?: 'load' | 'idle' | 'visible' | 'media' | 'only';
@@ -143,7 +153,6 @@ export interface CLIFlags {
host?: string | boolean;
port?: number;
config?: string;
- drafts?: boolean;
open?: boolean;
}
@@ -886,33 +895,6 @@ export interface AstroUserConfig {
* ```
*/
inlineStylesheets?: 'always' | 'auto' | 'never';
-
- /**
- * @docs
- * @name build.split
- * @type {boolean}
- * @default `false`
- * @deprecated Deprecated since version 3.0.
- * @description
- * The build config option `build.split` has been replaced by the adapter configuration option [`functionPerRoute`](/en/reference/adapter-reference/#functionperroute).
- *
- * Please see your [SSR adapter's documentation](/en/guides/integrations-guide/#official-integrations) for using `functionPerRoute` to define how your SSR code is bundled.
- *
- */
- split?: boolean;
-
- /**
- * @docs
- * @name build.excludeMiddleware
- * @type {boolean}
- * @default `false`
- * @deprecated Deprecated since version 3.0.
- * @description
- * The build config option `build.excludeMiddleware` has been replaced by the adapter configuration option [`edgeMiddleware`](/en/reference/adapter-reference/#edgemiddleware).
- *
- * Please see your [SSR adapter's documentation](/en/guides/integrations-guide/#official-integrations) for using `edgeMiddleware` to define whether or not any SSR middleware code will be bundled when built.
- */
- excludeMiddleware?: boolean;
};
/**
@@ -1180,31 +1162,28 @@ export interface AstroUserConfig {
/**
* @docs
* @kind heading
- * @name Markdown Options
+ * @name Dev Toolbar Options
*/
- markdown?: {
+ devToolbar?: {
/**
* @docs
- * @name markdown.drafts
+ * @name devToolbar.enabled
* @type {boolean}
- * @default `false`
- * @deprecated Deprecated since version 3.0. Use content collections instead.
+ * @default `true`
* @description
- * Control whether Markdown draft pages should be included in the build.
- *
- * A Markdown page is considered a draft if it includes `draft: true` in its frontmatter. Draft pages are always included & visible during development (`astro dev`) but by default they will not be included in your final build.
+ * Whether to enable the Astro Dev Toolbar. This toolbar allows you to inspect your page islands, see helpful audits on performance and accessibility, and more.
*
- * ```js
- * {
- * markdown: {
- * // Example: Include all drafts in your final build
- * drafts: true,
- * }
- * }
- * ```
+ * This option is scoped to the entire project, to only disable the toolbar for yourself, run `npm run astro preferences disable devToolbar`. To disable the toolbar for all your Astro projects, run `npm run astro preferences disable devToolbar --global`.
*/
- drafts?: boolean;
+ enabled: boolean;
+ };
+ /**
+ * @docs
+ * @kind heading
+ * @name Markdown Options
+ */
+ markdown?: {
/**
* @docs
* @name markdown.shikiConfig
@@ -1317,7 +1296,7 @@ export interface AstroUserConfig {
* {
* markdown: {
* // Example: Translate the footnotes text to another language, here are the default English values
- * remarkRehype: { footnoteLabel: "Footnotes", footnoteBackLabel: "Back to content"},
+ * remarkRehype: { footnoteLabel: "Footnotes", footnoteBackLabel: "Back to reference 1"},
* },
* };
* ```
@@ -1382,6 +1361,119 @@ export interface AstroUserConfig {
*/
vite?: ViteUserConfig;
+ /**
+ * @docs
+ * @kind heading
+ * @name i18n
+ * @type {object}
+ * @version 3.5.0
+ * @type {object}
+ * @description
+ *
+ * Configures i18n routing and allows you to specify some customization options.
+ *
+ * See our guide for more information on [internationalization in Astro](/en/guides/internationalization/)
+ */
+ i18n?: {
+ /**
+ * @docs
+ * @name i18n.defaultLocale
+ * @type {string}
+ * @version 3.5.0
+ * @description
+ *
+ * The default locale of your website/application. This is a required field.
+ *
+ * No particular language format or syntax is enforced, but we suggest using lower-case and hyphens as needed (e.g. "es", "pt-br") for greatest compatibility.
+ */
+ defaultLocale: string;
+ /**
+ * @docs
+ * @name i18n.locales
+ * @type {Locales}
+ * @version 3.5.0
+ * @description
+ *
+ * A list of all locales supported by the website, including the `defaultLocale`. This is a required field.
+ *
+ * Languages can be listed either as individual codes (e.g. `['en', 'es', 'pt-br']`) or mapped to a shared `path` of codes (e.g. `{ path: "english", codes: ["en", "en-US"]}`). These codes will be used to determine the URL structure of your deployed site.
+ *
+ * No particular language code format or syntax is enforced, but your project folders containing your content files must match exactly the `locales` items in the list. In the case of multiple `codes` pointing to a custom URL path prefix, store your content files in a folder with the same name as the `path` configured.
+ */
+ locales: Locales;
+
+ /**
+ * @docs
+ * @name i18n.fallback
+ * @type {Record}
+ * @version 3.5.0
+ * @description
+ *
+ * The fallback strategy when navigating to pages that do not exist (e.g. a translated page has not been created).
+ *
+ * Use this object to declare a fallback `locale` route for each language you support. If no fallback is specified, then unavailable pages will return a 404.
+ *
+ * ##### Example
+ *
+ * The following example configures your content fallback strategy to redirect unavailable pages in `/pt-br/` to their `es` version, and unavailable pages in `/fr/` to their `en` version. Unavailable `/es/` pages will return a 404.
+ *
+ * ```js
+ * export default defineConfig({
+ * i18n: {
+ * defaultLocale: "en",
+ * locales: ["en", "fr", "pt-br", "es"],
+ * fallback: {
+ * pt: "es",
+ * fr: "en"
+ * }
+ * }
+ * })
+ * ```
+ */
+ fallback?: Record;
+
+ /**
+ * @docs
+ * @name i18n.routing
+ * @type {Routing}
+ * @version 3.7.0
+ * @description
+ *
+ * Controls the routing strategy to determine your site URLs. Set this based on your folder/URL path configuration for your default language.
+ */
+ routing?: {
+ /**
+ * @docs
+ * @name i18n.routing.prefixDefaultLocale
+ * @kind h4
+ * @type {boolean}
+ * @default `false`
+ * @version 3.7.0
+ * @description
+ *
+ * When `false`, only non-default languages will display a language prefix.
+ * The `defaultLocale` will not show a language prefix and content files do not exist in a localized folder.
+ * URLs will be of the form `example.com/[locale]/content/` for all non-default languages, but `example.com/content/` for the default locale.
+ *
+ * When `true`, all URLs will display a language prefix.
+ * URLs will be of the form `example.com/[locale]/content/` for every route, including the default language.
+ * Localized folders are used for every language, including the default.
+ */
+ prefixDefaultLocale: boolean;
+
+ /**
+ * @name i18n.routing.strategy
+ * @type {"pathname"}
+ * @default `"pathname"`
+ * @version 3.7.0
+ * @description
+ *
+ * - `"pathanme": The strategy is applied to the pathname of the URLs
+ */
+ strategy: 'pathname';
+ };
+ };
+
/**
* @docs
* @kind heading
@@ -1424,119 +1516,6 @@ export interface AstroUserConfig {
*/
optimizeHoistedScript?: boolean;
- /**
- * @docs
- * @name experimental.devOverlay
- * @type {boolean}
- * @default `false`
- * @version 3.4.0
- * @description
- * Enable a dev overlay in development mode. This overlay allows you to inspect your page islands, see helpful audits on performance and accessibility, and more.
- *
- * ```js
- * {
- * experimental: {
- * devOverlay: true,
- * },
- * }
- * ```
- */
- devOverlay?: boolean;
-
- /**
- * @docs
- * @name experimental.i18n
- * @type {object}
- * @version 3.5.0
- * @type {object}
- * @description
- *
- * Configures experimental i18n routing and allows you to specify some customization options.
- *
- * See our guide for more information on [internationalization in Astro](/en/guides/internationalization/)
- */
- i18n?: {
- /**
- * @docs
- * @kind h4
- * @name experimental.i18n.defaultLocale
- * @type {string}
- * @version 3.5.0
- * @description
- *
- * The default locale of your website/application. This is a required field.
- *
- * No particular language format or syntax is enforced, but we suggest using lower-case and hyphens as needed (e.g. "es", "pt-br") for greatest compatibility.
- */
- defaultLocale: string;
- /**
- * @docs
- * @kind h4
- * @name experimental.i18n.locales
- * @type {string[]}
- * @version 3.5.0
- * @description
- *
- * A list of all locales supported by the website (e.g. `['en', 'es', 'pt-br']`). This list should also include the `defaultLocale`. This is a required field.
- *
- * No particular language format or syntax is enforced, but your folder structure must match exactly the locales in the list.
- */
- locales: string[];
-
- /**
- * @docs
- * @kind h4
- * @name experimental.i18n.fallback
- * @type {Record}
- * @version 3.5.0
- * @description
- *
- * The fallback strategy when navigating to pages that do not exist (e.g. a translated page has not been created).
- *
- * Use this object to declare a fallback `locale` route for each language you support. If no fallback is specified, then unavailable pages will return a 404.
- *
- * ##### Example
- *
- * The following example configures your content fallback strategy to redirect unavailable pages in `/pt-br/` to their `es` version, and unavailable pages in `/fr/` to their `en` version. Unavailable `/es/` pages will return a 404.
- *
- * ```js
- * export default defineConfig({
- * experimental: {
- * i18n: {
- * defaultLocale: "en",
- * locales: ["en", "fr", "pt-br", "es"],
- * fallback: {
- * pt: "es",
- * fr: "en"
- * }
- * }
- * }
- * })
- * ```
- */
- fallback?: Record;
-
- /**
- * @docs
- * @kind h4
- * @name experimental.i18n.routingStrategy
- * @type {'prefix-always' | 'prefix-other-locales'}
- * @default 'prefix-other-locales'
- * @version 3.5.0
- * @description
- *
- * Controls the routing strategy to determine your site URLs. Set this based on your folder/URL path configuration for your default language:
- *
- * - `prefix-other-locales`(default): Only non-default languages will display a language prefix.
- * The `defaultLocale` will not show a language prefix and content files do not exist in a localized folder.
- * URLs will be of the form `example.com/[locale]/content/` for all non-default languages, but `example.com/content/` for the default locale.
- * - `prefix-always`: All URLs will display a language prefix.
- * URLs will be of the form `example.com/[locale]/content/` for every route, including the default language.
- * Localized folders are used for every language, including the default.
- *
- */
- routingStrategy?: 'prefix-always' | 'prefix-other-locales';
- };
/**
* @docs
* @name experimental.contentCollectionCache
@@ -1578,7 +1557,7 @@ export type InjectedScriptStage = 'before-hydration' | 'head-inline' | 'page' |
export interface InjectedRoute {
pattern: string;
- entryPoint: string;
+ entrypoint: string;
prerender?: boolean;
}
@@ -1714,6 +1693,7 @@ export interface AstroAdapterFeatures {
export interface AstroSettings {
config: AstroConfig;
adapter: AstroAdapter | undefined;
+ preferences: AstroPreferences;
injectedRoutes: InjectedRoute[];
resolvedInjectedRoutes: ResolvedInjectedRoute[];
pageExtensions: string[];
@@ -1728,7 +1708,7 @@ export interface AstroSettings {
* Map of directive name (e.g. `load`) to the directive script code
*/
clientDirectives: Map;
- devOverlayPlugins: string[];
+ devToolbarApps: string[];
middlewares: { pre: string[]; post: string[] };
tsConfig: TSConfig | undefined;
tsConfigPath: string | undefined;
@@ -1749,10 +1729,6 @@ export interface ComponentInstance {
css?: string[];
partial?: boolean;
prerender?: boolean;
- /**
- * Only used for logging if deprecated drafts feature is used
- */
- frontmatter?: Record;
getStaticPaths?: (options: GetStaticPathsOptions) => GetStaticPathsResult;
}
@@ -1809,13 +1785,9 @@ export type GetHydrateCallback = () => Promise<() => void | Promise>;
* getStaticPaths() options
*
* [Astro Reference](https://docs.astro.build/en/reference/api-reference/#getstaticpaths)
- */ export interface GetStaticPathsOptions {
+ */
+export interface GetStaticPathsOptions {
paginate: PaginateFunction;
- /**
- * The RSS helper has been removed from getStaticPaths! Try the new @astrojs/rss package instead.
- * @see https://docs.astro.build/en/guides/rss/
- */
- rss(): never;
}
export type GetStaticPathsItem = {
@@ -2044,6 +2016,8 @@ export interface AstroInternationalizationFeature {
detectBrowserLanguage?: SupportsKind;
}
+export type Locales = (string | { codes: string[]; path: string })[];
+
export interface AstroAdapter {
name: string;
serverEntrypoint?: string;
@@ -2056,11 +2030,9 @@ export interface AstroAdapter {
*
* If the adapter is not able to handle certain configurations, Astro will throw an error.
*/
- supportedAstroFeatures?: AstroFeatureMap;
+ supportedAstroFeatures: AstroFeatureMap;
}
-type Body = string;
-
export type ValidRedirectStatus = 300 | 301 | 302 | 303 | 304 | 307 | 308;
// Shared types between `Astro` global and API context object
@@ -2145,7 +2117,7 @@ export interface APIContext<
* ];
* }
*
- * export async function get({ params }) {
+ * export async function GET({ params }) {
* return {
* body: `Hello user ${params.id}!`,
* }
@@ -2168,7 +2140,7 @@ export interface APIContext<
* ];
* }
*
- * export function get({ props }) {
+ * export function GET({ props }) {
* return {
* body: `Hello ${props.name}!`,
* }
@@ -2184,7 +2156,7 @@ export interface APIContext<
* Example usage:
* ```ts
* // src/pages/secret.ts
- * export function get({ redirect }) {
+ * export function GET({ redirect }) {
* return redirect('/login');
* }
* ```
@@ -2217,10 +2189,9 @@ export interface APIContext<
* ```
*/
locals: App.Locals;
- ResponseWithEncoding: typeof ResponseWithEncoding;
/**
- * Available only when `experimental.i18n` enabled and in SSR.
+ * Available only when `i18n` configured and in SSR.
*
* It represents the preferred locale of the user. It's computed by checking the supported locales in `i18n.locales`
* and locales supported by the users's browser via the header `Accept-Language`
@@ -2233,7 +2204,7 @@ export interface APIContext<
preferredLocale: string | undefined;
/**
- * Available only when `experimental.i18n` enabled and in SSR.
+ * Available only when `i18n` configured and in SSR.
*
* It represents the list of the preferred locales that are supported by the application. The list is sorted via [quality value].
*
@@ -2253,22 +2224,18 @@ export interface APIContext<
currentLocale: string | undefined;
}
-export type EndpointOutput =
- | {
- body: Body;
- encoding?: BufferEncoding;
- }
- | {
- body: Uint8Array;
- encoding: 'binary';
- };
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+type Routing = {
+ prefixDefaultLocale: boolean;
+ strategy: 'pathname';
+};
export type APIRoute = Record> = (
context: APIContext
-) => EndpointOutput | Response | Promise;
+) => Response | Promise;
export interface EndpointHandler {
- [method: string]: APIRoute | ((params: Params, request: Request) => EndpointOutput | Response);
+ [method: string]: APIRoute | ((params: Params, request: Request) => Response);
}
export type Props = Record;
@@ -2311,13 +2278,17 @@ export interface AstroIntegration {
config: AstroConfig;
command: 'dev' | 'build' | 'preview';
isRestart: boolean;
- updateConfig: (newConfig: Record) => void;
+ updateConfig: (newConfig: DeepPartial) => AstroConfig;
addRenderer: (renderer: AstroRenderer) => void;
addWatchFile: (path: URL | string) => void;
injectScript: (stage: InjectedScriptStage, content: string) => void;
injectRoute: (injectRoute: InjectedRoute) => void;
addClientDirective: (directive: ClientDirectiveConfig) => void;
+ /**
+ * @deprecated Use `addDevToolbarApp` instead.
+ */
addDevOverlayPlugin: (entrypoint: string) => void;
+ addDevToolbarApp: (entrypoint: string) => void;
addMiddleware: (mid: AstroIntegrationMiddleware) => void;
logger: AstroIntegrationLogger;
// TODO: Add support for `injectElement()` for full HTML element injection, not just scripts.
@@ -2373,20 +2344,16 @@ export interface AstroIntegration {
};
}
-export type MiddlewareNext = () => Promise;
-export type MiddlewareHandler = (
+export type MiddlewareNext = () => Promise;
+export type MiddlewareHandler = (
context: APIContext,
- next: MiddlewareNext
-) => Promise | R | Promise | void;
-
-export type MiddlewareResponseHandler = MiddlewareHandler;
-export type MiddlewareEndpointHandler = MiddlewareHandler;
-export type MiddlewareNextResponse = MiddlewareNext;
+ next: MiddlewareNext
+) => Promise | Response | Promise | void;
// NOTE: when updating this file with other functions,
// remember to update `plugin-page.ts` too, to add that function as a no-op function.
-export type AstroMiddlewareInstance = {
- onRequest?: MiddlewareHandler;
+export type AstroMiddlewareInstance = {
+ onRequest?: MiddlewareHandler;
};
export type AstroIntegrationMiddleware = {
@@ -2434,16 +2401,21 @@ export interface RouteData {
prerender: boolean;
redirect?: RedirectConfig;
redirectRoute?: RouteData;
+ fallbackRoutes: RouteData[];
}
export type RedirectRouteData = RouteData & {
redirect: string;
};
-export type SerializedRouteData = Omit & {
+export type SerializedRouteData = Omit<
+ RouteData,
+ 'generate' | 'pattern' | 'redirectRoute' | 'fallbackRoutes'
+> & {
generate: undefined;
pattern: string;
redirectRoute: SerializedRouteData | undefined;
+ fallbackRoutes: SerializedRouteData[];
_meta: {
trailingSlash: AstroConfig['trailingSlash'];
};
@@ -2567,7 +2539,7 @@ export interface ClientDirectiveConfig {
entrypoint: string;
}
-export interface DevOverlayPlugin {
+export interface DevToolbarApp {
id: string;
name: string;
icon: Icon;
@@ -2575,20 +2547,40 @@ export interface DevOverlayPlugin {
beforeTogglingOff?(canvas: ShadowRoot): boolean | Promise;
}
+export type DevOverlayPlugin = DevToolbarApp;
+
export type DevOverlayMetadata = Window &
typeof globalThis & {
__astro_dev_overlay__: {
root: string;
+ version: string;
+ debugInfo: string;
};
};
declare global {
interface HTMLElementTagNameMap {
+ 'astro-dev-toolbar': AstroDevOverlay;
+ 'astro-dev-toolbar-window': DevOverlayWindow;
+ 'astro-dev-toolbar-plugin-canvas': DevOverlayCanvas;
+ 'astro-dev-toolbar-tooltip': DevOverlayTooltip;
+ 'astro-dev-toolbar-highlight': DevOverlayHighlight;
+ 'astro-dev-toolbar-toggle': DevOverlayToggle;
+ 'astro-dev-toolbar-badge': DevOverlayBadge;
+ 'astro-dev-toolbar-button': DevOverlayButton;
+ 'astro-dev-toolbar-icon': DevOverlayIcon;
+ 'astro-dev-toolbar-card': DevOverlayCard;
+
+ // Deprecated names
'astro-dev-overlay': AstroDevOverlay;
'astro-dev-overlay-window': DevOverlayWindow;
'astro-dev-overlay-plugin-canvas': DevOverlayCanvas;
'astro-dev-overlay-tooltip': DevOverlayTooltip;
'astro-dev-overlay-highlight': DevOverlayHighlight;
'astro-dev-overlay-toggle': DevOverlayToggle;
+ 'astro-dev-overlay-badge': DevOverlayBadge;
+ 'astro-dev-overlay-button': DevOverlayButton;
+ 'astro-dev-overlay-icon': DevOverlayIcon;
+ 'astro-dev-overlay-card': DevOverlayCard;
}
}
diff --git a/packages/astro/src/assets/build/generate.ts b/packages/astro/src/assets/build/generate.ts
index be637c26da7ad..1c55a93b98749 100644
--- a/packages/astro/src/assets/build/generate.ts
+++ b/packages/astro/src/assets/build/generate.ts
@@ -1,7 +1,7 @@
import { dim, green } from 'kleur/colors';
import fs, { readFileSync } from 'node:fs';
import { basename, join } from 'node:path/posix';
-import PQueue from 'p-queue';
+import type PQueue from 'p-queue';
import type { AstroConfig } from '../../@types/astro.js';
import type { BuildPipeline } from '../../core/build/buildPipeline.js';
import { getOutDirWithinCwd } from '../../core/build/common.js';
@@ -58,7 +58,7 @@ export async function prepareAssetsGenerationEnv(
await fs.promises.mkdir(assetsCacheDir, { recursive: true });
} catch (err) {
logger.warn(
- 'astro:assets',
+ null,
`An error was encountered while creating the cache directory. Proceeding without caching. Error: ${err}`
);
useCache = false;
@@ -231,7 +231,7 @@ export async function generateImagesForPath(
}
} catch (e) {
env.logger.warn(
- 'astro:assets',
+ null,
`An error was encountered while creating the cache directory. Proceeding without caching. Error: ${e}`
);
} finally {
diff --git a/packages/astro/src/assets/internal.ts b/packages/astro/src/assets/internal.ts
index 46319ed3bc4c6..06dad68524691 100644
--- a/packages/astro/src/assets/internal.ts
+++ b/packages/astro/src/assets/internal.ts
@@ -19,7 +19,7 @@ export function injectImageEndpoint(settings: AstroSettings, mode: 'dev' | 'buil
settings.injectedRoutes.push({
pattern: '/_image',
- entryPoint: endpointEntrypoint,
+ entrypoint: endpointEntrypoint,
prerender: false,
});
@@ -79,6 +79,16 @@ export async function getImage(
message: AstroErrorData.ExpectedImageOptions.message(JSON.stringify(options)),
});
}
+ if (typeof options.src === 'undefined') {
+ throw new AstroError({
+ ...AstroErrorData.ExpectedImage,
+ message: AstroErrorData.ExpectedImage.message(
+ options.src,
+ 'undefined',
+ JSON.stringify(options)
+ ),
+ });
+ }
const service = await getConfiguredImageService();
diff --git a/packages/astro/src/cli/add/index.ts b/packages/astro/src/cli/add/index.ts
index 80c0e10ff760b..76323ca0b7894 100644
--- a/packages/astro/src/cli/add/index.ts
+++ b/packages/astro/src/cli/add/index.ts
@@ -5,6 +5,7 @@ import { bold, cyan, dim, green, magenta, red, yellow } from 'kleur/colors';
import fsMod, { existsSync, promises as fs } from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
+import maxSatisfying from 'semver/ranges/max-satisfying.js';
import ora from 'ora';
import preferredPM from 'preferred-pm';
import prompts from 'prompts';
@@ -610,15 +611,7 @@ async function getInstallIntegrationsCommand({
logger.debug('add', `package manager: ${JSON.stringify(pm)}`);
if (!pm) return null;
- let dependencies = integrations
- .map<[string, string | null][]>((i) => [[i.packageName, null], ...i.dependencies])
- .flat(1)
- .filter((dep, i, arr) => arr.findIndex((d) => d[0] === dep[0]) === i)
- .map(([name, version]) =>
- version === null ? name : `${name}@${version.split(/\s*\|\|\s*/).pop()}`
- )
- .sort();
-
+ const dependencies = await convertIntegrationsToInstallSpecifiers(integrations);
switch (pm.name) {
case 'npm':
return { pm: 'npm', command: 'install', flags: [], dependencies };
@@ -633,6 +626,35 @@ async function getInstallIntegrationsCommand({
}
}
+async function convertIntegrationsToInstallSpecifiers(
+ integrations: IntegrationInfo[]
+): Promise {
+ const ranges: Record = {};
+ for (let { packageName, dependencies } of integrations) {
+ ranges[packageName] = '*';
+ for (const [name, range] of dependencies) {
+ ranges[name] = range;
+ }
+ }
+ return Promise.all(
+ Object.entries(ranges).map(([name, range]) => resolveRangeToInstallSpecifier(name, range))
+ );
+}
+
+/**
+ * Resolves package with a given range to a STABLE version
+ * peerDependencies might specify a compatible prerelease,
+ * but `astro add` should only ever install stable releases
+ */
+async function resolveRangeToInstallSpecifier(name: string, range: string): Promise {
+ const versions = await fetchPackageVersions(name);
+ if (versions instanceof Error) return name;
+ // Filter out any prerelease versions
+ const stableVersions = versions.filter((v) => !v.includes('-'));
+ const maxStable = maxSatisfying(stableVersions, range);
+ return `${name}@^${maxStable}`;
+}
+
// Allow forwarding of standard `npm install` flags
// See https://docs.npmjs.com/cli/v8/commands/npm-install#description
const INHERITED_FLAGS = new Set([
@@ -725,7 +747,7 @@ async function fetchPackageJson(
scope: string | undefined,
name: string,
tag: string
-): Promise {
+): Promise | Error> {
const packageName = `${scope ? `${scope}/` : ''}${name}`;
const registry = await getRegistry();
const res = await fetch(`${registry}/${packageName}/${tag}`);
@@ -739,6 +761,21 @@ async function fetchPackageJson(
}
}
+async function fetchPackageVersions(packageName: string): Promise {
+ const registry = await getRegistry();
+ const res = await fetch(`${registry}/${packageName}`, {
+ headers: { accept: 'application/vnd.npm.install-v1+json' },
+ });
+ if (res.status >= 200 && res.status < 300) {
+ return await res.json().then((data) => Object.keys(data.versions));
+ } else if (res.status === 404) {
+ // 404 means the package doesn't exist, so we don't need an error message here
+ return new Error();
+ } else {
+ return new Error(`Failed to fetch ${registry}/${packageName} - GET ${res.status}`);
+ }
+}
+
export async function validateIntegrations(integrations: string[]): Promise {
const spinner = ora('Resolving packages...').start();
try {
diff --git a/packages/astro/src/cli/build/index.ts b/packages/astro/src/cli/build/index.ts
index 1a7d5aa520481..15ff584317d93 100644
--- a/packages/astro/src/cli/build/index.ts
+++ b/packages/astro/src/cli/build/index.ts
@@ -14,7 +14,6 @@ export async function build({ flags }: BuildOptions) {
usage: '[...flags]',
tables: {
Flags: [
- ['--drafts', `Include Markdown draft pages in the build.`],
['--outDir ', `Specify the output directory for the build.`],
['--help (-h)', 'See all available flags.'],
],
diff --git a/packages/astro/src/cli/flags.ts b/packages/astro/src/cli/flags.ts
index c97f1801acf1d..c384d98671b86 100644
--- a/packages/astro/src/cli/flags.ts
+++ b/packages/astro/src/cli/flags.ts
@@ -15,9 +15,6 @@ export function flagsToAstroInlineConfig(flags: Flags): AstroInlineConfig {
site: typeof flags.site === 'string' ? flags.site : undefined,
base: typeof flags.base === 'string' ? flags.base : undefined,
outDir: typeof flags.outDir === 'string' ? flags.outDir : undefined,
- markdown: {
- drafts: typeof flags.drafts === 'boolean' ? flags.drafts : undefined,
- },
server: {
port: typeof flags.port === 'number' ? flags.port : undefined,
host:
diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts
index 0421258a51ae4..7ca4d21a9a7bd 100644
--- a/packages/astro/src/cli/index.ts
+++ b/packages/astro/src/cli/index.ts
@@ -14,6 +14,7 @@ type CLICommand =
| 'sync'
| 'check'
| 'info'
+ | 'preferences'
| 'telemetry';
/** Display --help flag */
@@ -33,6 +34,7 @@ async function printAstroHelp() {
['info', 'List info about your current Astro setup.'],
['preview', 'Preview your build locally.'],
['sync', 'Generate content collection types.'],
+ ['preferences', 'Configure user preferences.'],
['telemetry', 'Configure telemetry settings.'],
],
'Global Flags': [
@@ -64,6 +66,7 @@ function resolveCommand(flags: yargs.Arguments): CLICommand {
'add',
'sync',
'telemetry',
+ 'preferences',
'dev',
'build',
'preview',
@@ -114,6 +117,12 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {
const exitCode = await sync({ flags });
return process.exit(exitCode);
}
+ case 'preferences': {
+ const { preferences } = await import('./preferences/index.js');
+ const [subcommand, key, value] = flags._.slice(3).map((v) => v.toString());
+ const exitCode = await preferences(subcommand, key, value, { flags });
+ return process.exit(exitCode);
+ }
}
// In verbose/debug mode, we log the debug logs asap before any potential errors could appear
@@ -177,7 +186,7 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {
/** The primary CLI action */
export async function cli(args: string[]) {
- const flags = yargs(args);
+ const flags = yargs(args, { boolean: ['global'], alias: { g: 'global' } });
const cmd = resolveCommand(flags);
try {
await runCommand(cmd, flags);
diff --git a/packages/astro/src/cli/info/index.ts b/packages/astro/src/cli/info/index.ts
index 46e7d3c6978c7..c6586b28d2a4d 100644
--- a/packages/astro/src/cli/info/index.ts
+++ b/packages/astro/src/cli/info/index.ts
@@ -4,6 +4,7 @@ import { execSync } from 'node:child_process';
import { arch, platform } from 'node:os';
import prompts from 'prompts';
import type yargs from 'yargs-parser';
+import type { AstroConfig, AstroUserConfig } from '../../@types/astro.js';
import { resolveConfig } from '../../core/config/index.js';
import { ASTRO_VERSION } from '../../core/constants.js';
import { flagsToAstroInlineConfig } from '../flags.js';
@@ -12,7 +13,13 @@ interface InfoOptions {
flags: yargs.Arguments;
}
-export async function printInfo({ flags }: InfoOptions) {
+export async function getInfoOutput({
+ userConfig,
+ print,
+}: {
+ userConfig: AstroUserConfig | AstroConfig;
+ print: boolean;
+}): Promise {
const rows: Array<[string, string | string[]]> = [
['Astro', `v${ASTRO_VERSION}`],
['Node', process.version],
@@ -20,9 +27,7 @@ export async function printInfo({ flags }: InfoOptions) {
['Package Manager', getPackageManager()],
];
- const inlineConfig = flagsToAstroInlineConfig(flags);
try {
- const { userConfig } = await resolveConfig(inlineConfig, 'info');
rows.push(['Output', userConfig.output ?? 'static']);
rows.push(['Adapter', userConfig.adapter?.name ?? 'none']);
const integrations = (userConfig?.integrations ?? [])
@@ -35,10 +40,17 @@ export async function printInfo({ flags }: InfoOptions) {
let output = '';
for (const [label, value] of rows) {
- output += printRow(label, value);
+ output += printRow(label, value, print);
}
- await copyToClipboard(output.trim());
+ return output.trim();
+}
+
+export async function printInfo({ flags }: InfoOptions) {
+ const { userConfig } = await resolveConfig(flagsToAstroInlineConfig(flags), 'info');
+ const output = await getInfoOutput({ userConfig, print: true });
+
+ await copyToClipboard(output);
}
async function copyToClipboard(text: string) {
@@ -105,7 +117,7 @@ function getPackageManager() {
}
const MAX_PADDING = 25;
-function printRow(label: string, value: string | string[]) {
+function printRow(label: string, value: string | string[], print: boolean) {
const padding = MAX_PADDING - label.length;
const [first, ...rest] = Array.isArray(value) ? value : [value];
let plaintext = `${label}${' '.repeat(padding)}${first}`;
@@ -117,6 +129,8 @@ function printRow(label: string, value: string | string[]) {
}
}
plaintext += '\n';
- console.log(richtext);
+ if (print) {
+ console.log(richtext);
+ }
return plaintext;
}
diff --git a/packages/astro/src/cli/install-package.ts b/packages/astro/src/cli/install-package.ts
index 689f81e3e8791..667037a0cb3de 100644
--- a/packages/astro/src/cli/install-package.ts
+++ b/packages/astro/src/cli/install-package.ts
@@ -26,7 +26,7 @@ export async function getPackage(
packageImport = await import(packageName);
} catch (e) {
logger.info(
- '',
+ null,
`To continue, Astro requires the following dependency to be installed: ${bold(packageName)}.`
);
const result = await installPackage([packageName, ...otherDeps], options, logger);
diff --git a/packages/astro/src/cli/preferences/index.ts b/packages/astro/src/cli/preferences/index.ts
new file mode 100644
index 0000000000000..4fd29cdfea144
--- /dev/null
+++ b/packages/astro/src/cli/preferences/index.ts
@@ -0,0 +1,340 @@
+/* eslint-disable no-console */
+import type yargs from 'yargs-parser';
+import type { AstroSettings } from '../../@types/astro.js';
+
+import { bgGreen, black, bold, dim } from 'kleur/colors';
+import { fileURLToPath } from 'node:url';
+
+import dlv from 'dlv';
+import { resolveConfig } from '../../core/config/config.js';
+import { createSettings } from '../../core/config/settings.js';
+import * as msg from '../../core/messages.js';
+import { DEFAULT_PREFERENCES } from '../../preferences/defaults.js';
+import {
+ coerce,
+ isValidKey,
+ type PreferenceKey,
+ type PreferenceLocation,
+} from '../../preferences/index.js';
+import { createLoggerFromFlags, flagsToAstroInlineConfig } from '../flags.js';
+// @ts-expect-error flattie types are mispackaged
+import { flattie } from 'flattie';
+import { formatWithOptions } from 'node:util';
+import { collectErrorMetadata } from '../../core/errors/dev/utils.js';
+
+interface PreferencesOptions {
+ flags: yargs.Arguments;
+}
+
+const PREFERENCES_SUBCOMMANDS = [
+ 'get',
+ 'set',
+ 'enable',
+ 'disable',
+ 'delete',
+ 'reset',
+ 'list',
+] as const;
+export type Subcommand = (typeof PREFERENCES_SUBCOMMANDS)[number];
+
+function isValidSubcommand(subcommand: string): subcommand is Subcommand {
+ return PREFERENCES_SUBCOMMANDS.includes(subcommand as Subcommand);
+}
+
+export async function preferences(
+ subcommand: string,
+ key: string,
+ value: string | undefined,
+ { flags }: PreferencesOptions
+): Promise {
+ if (!isValidSubcommand(subcommand) || flags?.help || flags?.h) {
+ msg.printHelp({
+ commandName: 'astro preferences',
+ usage: '[command]',
+ tables: {
+ Commands: [
+ ['list', 'Pretty print all current preferences'],
+ ['list --json', 'Log all current preferences as a JSON object'],
+ ['get [key]', 'Log current preference value'],
+ ['set [key] [value]', 'Update preference value'],
+ ['reset [key]', 'Reset preference value to default'],
+ ['enable [key]', 'Set a boolean preference to true'],
+ ['disable [key]', 'Set a boolean preference to false'],
+ ],
+ Flags: [
+ [
+ '--global',
+ 'Scope command to global preferences (all Astro projects) rather than the current project',
+ ],
+ ],
+ },
+ });
+ return 0;
+ }
+
+ const inlineConfig = flagsToAstroInlineConfig(flags);
+ const logger = createLoggerFromFlags(flags);
+ const { astroConfig } = await resolveConfig(inlineConfig ?? {}, 'dev');
+ const settings = await createSettings(astroConfig, fileURLToPath(astroConfig.root));
+ const opts: SubcommandOptions = {
+ location: flags.global ? 'global' : undefined,
+ json: flags.json,
+ };
+
+ if (subcommand === 'list') {
+ return listPreferences(settings, opts);
+ }
+
+ if (subcommand === 'enable' || subcommand === 'disable') {
+ key = `${key}.enabled` as PreferenceKey;
+ }
+
+ if (!isValidKey(key)) {
+ logger.error('preferences', `Unknown preference "${key}"\n`);
+ return 1;
+ }
+
+ if (subcommand === 'set' && value === undefined) {
+ const type = typeof dlv(DEFAULT_PREFERENCES, key);
+ console.error(
+ msg.formatErrorMessage(
+ collectErrorMetadata(new Error(`Please provide a ${type} value for "${key}"`)),
+ true
+ )
+ );
+ return 1;
+ }
+
+ switch (subcommand) {
+ case 'get':
+ return getPreference(settings, key, opts);
+ case 'set':
+ return setPreference(settings, key, value, opts);
+ case 'reset':
+ case 'delete':
+ return resetPreference(settings, key, opts);
+ case 'enable':
+ return enablePreference(settings, key, opts);
+ case 'disable':
+ return disablePreference(settings, key, opts);
+ }
+}
+
+interface SubcommandOptions {
+ location?: 'global' | 'project';
+ json?: boolean;
+}
+
+// Default `location` to "project" to avoid reading default preferencesa
+async function getPreference(
+ settings: AstroSettings,
+ key: PreferenceKey,
+ { location = 'project' }: SubcommandOptions
+) {
+ try {
+ let value = await settings.preferences.get(key, { location });
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
+ if (Object.keys(value).length === 0) {
+ value = dlv(DEFAULT_PREFERENCES, key);
+ console.log(msg.preferenceDefaultIntro(key));
+ }
+ prettyPrint({ [key]: value });
+ return 0;
+ }
+ if (value === undefined) {
+ const defaultValue = await settings.preferences.get(key);
+ console.log(msg.preferenceDefault(key, defaultValue));
+ return 0;
+ }
+ console.log(msg.preferenceGet(key, value));
+ return 0;
+ } catch {}
+ return 1;
+}
+
+async function setPreference(
+ settings: AstroSettings,
+ key: PreferenceKey,
+ value: unknown,
+ { location }: SubcommandOptions
+) {
+ try {
+ const defaultType = typeof dlv(DEFAULT_PREFERENCES, key);
+ if (typeof coerce(key, value) !== defaultType) {
+ throw new Error(`${key} expects a "${defaultType}" value!`);
+ }
+
+ await settings.preferences.set(key, coerce(key, value), { location });
+ console.log(msg.preferenceSet(key, value));
+ return 0;
+ } catch (e) {
+ if (e instanceof Error) {
+ console.error(msg.formatErrorMessage(collectErrorMetadata(e), true));
+ return 1;
+ }
+ throw e;
+ }
+}
+
+async function enablePreference(
+ settings: AstroSettings,
+ key: PreferenceKey,
+ { location }: SubcommandOptions
+) {
+ try {
+ await settings.preferences.set(key, true, { location });
+ console.log(msg.preferenceEnabled(key.replace('.enabled', '')));
+ return 0;
+ } catch {}
+ return 1;
+}
+
+async function disablePreference(
+ settings: AstroSettings,
+ key: PreferenceKey,
+ { location }: SubcommandOptions
+) {
+ try {
+ await settings.preferences.set(key, false, { location });
+ console.log(msg.preferenceDisabled(key.replace('.enabled', '')));
+ return 0;
+ } catch {}
+ return 1;
+}
+
+async function resetPreference(
+ settings: AstroSettings,
+ key: PreferenceKey,
+ { location }: SubcommandOptions
+) {
+ try {
+ await settings.preferences.set(key, undefined as any, { location });
+ console.log(msg.preferenceReset(key));
+ return 0;
+ } catch {}
+ return 1;
+}
+
+async function listPreferences(settings: AstroSettings, { location, json }: SubcommandOptions) {
+ if (json) {
+ const resolved = await settings.preferences.getAll();
+ console.log(JSON.stringify(resolved, null, 2));
+ return 0;
+ }
+ const { global, project, defaults } = await settings.preferences.list({ location });
+ const flatProject = flattie(project);
+ const flatGlobal = flattie(global);
+ const flatUser = Object.assign({}, flatGlobal, flatProject);
+ for (let key of Object.keys(flatUser)) {
+ if (!isValidKey(key)) {
+ delete flatUser[key];
+ continue;
+ }
+ }
+
+ const flatDefault = flattie(defaults);
+ const userKeys = Object.keys(flatUser);
+
+ if (userKeys.length > 0) {
+ const badge = bgGreen(black(` Your Preferences `));
+ const table = formatTable(flatUser, ['Preference', 'Value']);
+
+ console.log(['', badge, table].join('\n'));
+ } else {
+ const badge = bgGreen(black(` Your Preferences `));
+ const message = dim('No preferences set');
+ console.log(['', badge, '', message].join('\n'));
+ }
+ const flatUnset = Object.assign({}, flatDefault);
+ for (const key of userKeys) {
+ delete flatUnset[key];
+ }
+ const unsetKeys = Object.keys(flatUnset);
+
+ if (unsetKeys.length > 0) {
+ const badge = bgGreen(black(` Default Preferences `));
+ const table = formatTable(flatUnset, ['Preference', 'Value']);
+
+ console.log(['', badge, table].join('\n'));
+ } else {
+ const badge = bgGreen(black(` Default Preferences `));
+ const message = dim('All preferences have been set');
+ console.log(['', badge, '', message].join('\n'));
+ }
+
+ return 0;
+}
+
+function prettyPrint(value: Record) {
+ const flattened = flattie(value);
+ const table = formatTable(flattened, ['Preference', 'Value']);
+ console.log(table);
+}
+
+const chars = {
+ h: '─',
+ hThick: '━',
+ hThickCross: '┿',
+ v: '│',
+ vRight: '├',
+ vRightThick: '┝',
+ vLeft: '┤',
+ vLeftThick: '┥',
+ hTop: '┴',
+ hBottom: '┬',
+ topLeft: '╭',
+ topRight: '╮',
+ bottomLeft: '╰',
+ bottomRight: '╯',
+};
+
+function formatTable(
+ object: Record,
+ columnLabels: [string, string]
+) {
+ const [colA, colB] = columnLabels;
+ const colALength = [colA, ...Object.keys(object)].reduce(longest, 0) + 3;
+ const colBLength = [colB, ...Object.values(object)].reduce(longest, 0) + 3;
+ function formatRow(
+ i: number,
+ a: string,
+ b: string | number | boolean,
+ style: (value: string | number | boolean) => string = (v) => v.toString()
+ ): string {
+ return `${dim(chars.v)} ${style(a)} ${space(colALength - a.length - 2)} ${dim(chars.v)} ${style(
+ b
+ )} ${space(colBLength - b.toString().length - 3)} ${dim(chars.v)}`;
+ }
+ const top = dim(
+ `${chars.topLeft}${chars.h.repeat(colALength + 1)}${chars.hBottom}${chars.h.repeat(
+ colBLength
+ )}${chars.topRight}`
+ );
+ const bottom = dim(
+ `${chars.bottomLeft}${chars.h.repeat(colALength + 1)}${chars.hTop}${chars.h.repeat(
+ colBLength
+ )}${chars.bottomRight}`
+ );
+ const divider = dim(
+ `${chars.vRightThick}${chars.hThick.repeat(colALength + 1)}${
+ chars.hThickCross
+ }${chars.hThick.repeat(colBLength)}${chars.vLeftThick}`
+ );
+ const rows: string[] = [top, formatRow(-1, colA, colB, bold), divider];
+ let i = 0;
+ for (const [key, value] of Object.entries(object)) {
+ rows.push(formatRow(i, key, value, (v) => formatWithOptions({ colors: true }, v)));
+ i++;
+ }
+ rows.push(bottom);
+ return rows.join('\n');
+}
+
+function space(len: number) {
+ return ' '.repeat(len);
+}
+
+const longest = (a: number, b: string | number | boolean) => {
+ const { length: len } = b.toString();
+ return a > len ? a : len;
+};
diff --git a/packages/astro/src/cli/telemetry/index.ts b/packages/astro/src/cli/telemetry/index.ts
index fd664fcc94414..277b1cab67216 100644
--- a/packages/astro/src/cli/telemetry/index.ts
+++ b/packages/astro/src/cli/telemetry/index.ts
@@ -1,23 +1,23 @@
/* eslint-disable no-console */
-import whichPm from 'which-pm';
import type yargs from 'yargs-parser';
import * as msg from '../../core/messages.js';
import { telemetry } from '../../events/index.js';
+import { createLoggerFromFlags } from '../flags.js';
interface TelemetryOptions {
flags: yargs.Arguments;
}
export async function notify() {
- const packageManager = (await whichPm(process.cwd()))?.name ?? 'npm';
await telemetry.notify(() => {
- console.log(msg.telemetryNotice(packageManager) + '\n');
+ console.log(msg.telemetryNotice() + '\n');
return true;
});
}
export async function update(subcommand: string, { flags }: TelemetryOptions) {
const isValid = ['enable', 'disable', 'reset'].includes(subcommand);
+ const logger = createLoggerFromFlags(flags);
if (flags.help || flags.h || !isValid) {
msg.printHelp({
@@ -37,17 +37,17 @@ export async function update(subcommand: string, { flags }: TelemetryOptions) {
switch (subcommand) {
case 'enable': {
telemetry.setEnabled(true);
- console.log(msg.telemetryEnabled());
+ logger.info('SKIP_FORMAT', msg.telemetryEnabled());
return;
}
case 'disable': {
telemetry.setEnabled(false);
- console.log(msg.telemetryDisabled());
+ logger.info('SKIP_FORMAT', msg.telemetryDisabled());
return;
}
case 'reset': {
telemetry.clear();
- console.log(msg.telemetryReset());
+ logger.info('SKIP_FORMAT', msg.telemetryReset());
return;
}
}
diff --git a/packages/astro/src/cli/throw-and-exit.ts b/packages/astro/src/cli/throw-and-exit.ts
index 3196092d2cc6e..1a8916ede9c59 100644
--- a/packages/astro/src/cli/throw-and-exit.ts
+++ b/packages/astro/src/cli/throw-and-exit.ts
@@ -20,7 +20,7 @@ export async function throwAndExit(cmd: string, err: unknown) {
const errorWithMetadata = collectErrorMetadata(createSafeError(err));
telemetryPromise = telemetry.record(eventError({ cmd, err: errorWithMetadata, isFatal: true }));
- errorMessage = formatErrorMessage(errorWithMetadata);
+ errorMessage = formatErrorMessage(errorWithMetadata, true);
// Timeout the error reporter (very short) because the user is waiting.
// NOTE(fks): It is better that we miss some events vs. holding too long.
diff --git a/packages/astro/src/content/server-listeners.ts b/packages/astro/src/content/server-listeners.ts
index 699d5f2710dd1..3ff3148cbdfc8 100644
--- a/packages/astro/src/content/server-listeners.ts
+++ b/packages/astro/src/content/server-listeners.ts
@@ -1,4 +1,4 @@
-import { bold, cyan } from 'kleur/colors';
+import { bold, cyan, underline } from 'kleur/colors';
import type fsMod from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -26,7 +26,7 @@ export async function attachContentServerListeners({
const contentPaths = getContentPaths(settings.config, fs);
if (fs.existsSync(contentPaths.contentDir)) {
- logger.info(
+ logger.debug(
'content',
`Watching ${cyan(
contentPaths.contentDir.href.replace(settings.config.root.href, '')
@@ -39,7 +39,7 @@ export async function attachContentServerListeners({
viteServer.watcher.on('addDir', contentDirListener);
async function contentDirListener(dir: string) {
if (appendForwardSlash(pathToFileURL(dir).href) === contentPaths.contentDir.href) {
- logger.info('content', `Content dir found. Watching for changes`);
+ logger.debug('content', `Content directory found. Watching for changes`);
await attachListeners();
viteServer.watcher.removeListener('addDir', contentDirListener);
}
@@ -55,7 +55,7 @@ export async function attachContentServerListeners({
contentConfigObserver: globalContentConfigObserver,
});
await contentGenerator.init();
- logger.info('content', 'Types generated');
+ logger.debug('content', 'Types generated');
viteServer.watcher.on('add', (entry) => {
contentGenerator.queueEvent({ name: 'add', entry });
@@ -90,9 +90,9 @@ function warnAllowJsIsFalse({
'true'
)} in your ${bold(tsConfigFileName)} file to have autocompletion in your ${bold(
contentConfigFileName
- )} file.
-See ${bold('https://www.typescriptlang.org/tsconfig#allowJs')} for more information.
- `
+ )} file. See ${underline(
+ cyan('https://www.typescriptlang.org/tsconfig#allowJs')
+ )} for more information.`
);
}
diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts
index b50c597fdf5e7..dc9c1ecc76edd 100644
--- a/packages/astro/src/content/types-generator.ts
+++ b/packages/astro/src/content/types-generator.ts
@@ -1,5 +1,5 @@
import glob from 'fast-glob';
-import { cyan } from 'kleur/colors';
+import { bold, cyan } from 'kleur/colors';
import type fsMod from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -56,13 +56,6 @@ type CreateContentGeneratorParams = {
fs: typeof fsMod;
};
-type EventOpts = { logLevel: 'info' | 'warn' };
-
-type EventWithOptions = {
- type: ContentEvent;
- opts: EventOpts | undefined;
-};
-
class UnsupportedFileTypeError extends Error {}
export async function createContentTypesGenerator({
@@ -78,7 +71,7 @@ export async function createContentTypesGenerator({
const contentEntryExts = [...contentEntryConfigByExt.keys()];
const dataEntryExts = getDataEntryExts(settings);
- let events: EventWithOptions[] = [];
+ let events: ContentEvent[] = [];
let debounceTimeout: NodeJS.Timeout | undefined;
const typeTemplateContent = await fs.promises.readFile(contentPaths.typesTemplate, 'utf-8');
@@ -90,10 +83,7 @@ export async function createContentTypesGenerator({
return { typesGenerated: false, reason: 'no-content-dir' };
}
- events.push({
- type: { name: 'add', entry: contentPaths.config.url },
- opts: { logLevel: 'warn' },
- });
+ events.push({ name: 'add', entry: contentPaths.config.url });
const globResult = await glob('**', {
cwd: fileURLToPath(contentPaths.contentDir),
@@ -110,12 +100,9 @@ export async function createContentTypesGenerator({
const entryURL = pathToFileURL(fullPath);
if (entryURL.href.startsWith(contentPaths.config.url.href)) continue;
if (entry.dirent.isFile()) {
- events.push({
- type: { name: 'add', entry: entryURL },
- opts: { logLevel: 'warn' },
- });
+ events.push({ name: 'add', entry: entryURL });
} else if (entry.dirent.isDirectory()) {
- events.push({ type: { name: 'addDir', entry: entryURL }, opts: { logLevel: 'warn' } });
+ events.push({ name: 'addDir', entry: entryURL });
}
}
await runEvents();
@@ -123,11 +110,8 @@ export async function createContentTypesGenerator({
}
async function handleEvent(
- event: ContentEvent,
- opts?: EventOpts
+ event: ContentEvent
): Promise<{ shouldGenerateTypes: boolean; error?: Error }> {
- const logLevel = opts?.logLevel ?? 'info';
-
if (event.name === 'addDir' || event.name === 'unlinkDir') {
const collection = normalizePath(
path.relative(fileURLToPath(contentPaths.contentDir), fileURLToPath(event.entry))
@@ -140,9 +124,7 @@ export async function createContentTypesGenerator({
switch (event.name) {
case 'addDir':
collectionEntryMap[JSON.stringify(collection)] = { type: 'unknown', entries: {} };
- if (logLevel === 'info') {
- logger.info('content', `${cyan(collection)} collection added`);
- }
+ logger.debug('content', `${cyan(collection)} collection added`);
break;
case 'unlinkDir':
if (collectionKey in collectionEntryMap) {
@@ -186,16 +168,14 @@ export async function createContentTypesGenerator({
const collection = getEntryCollectionName({ entry, contentDir });
if (collection === undefined) {
- if (['info', 'warn'].includes(logLevel)) {
- logger.warn(
- 'content',
- `${cyan(
- normalizePath(
- path.relative(fileURLToPath(contentPaths.contentDir), fileURLToPath(event.entry))
- )
- )} must be nested in a collection directory. Skipping.`
- );
- }
+ logger.warn(
+ 'content',
+ `${bold(
+ normalizePath(
+ path.relative(fileURLToPath(contentPaths.contentDir), fileURLToPath(event.entry))
+ )
+ )} must live in a ${bold('content/...')} collection subdirectory.`
+ );
return { shouldGenerateTypes: false };
}
@@ -308,22 +288,19 @@ export async function createContentTypesGenerator({
}
}
- function queueEvent(rawEvent: RawContentEvent, opts?: EventOpts) {
+ function queueEvent(rawEvent: RawContentEvent) {
const event = {
- type: {
- entry: pathToFileURL(rawEvent.entry),
- name: rawEvent.name,
- },
- opts,
+ entry: pathToFileURL(rawEvent.entry),
+ name: rawEvent.name,
};
- if (!event.type.entry.pathname.startsWith(contentPaths.contentDir.pathname)) return;
+ if (!event.entry.pathname.startsWith(contentPaths.contentDir.pathname)) return;
events.push(event);
debounceTimeout && clearTimeout(debounceTimeout);
const runEventsSafe = async () => {
try {
- await runEvents(opts);
+ await runEvents();
} catch {
// Prevent frontmatter errors from crashing the server. The errors
// are still reported on page reflects as desired.
@@ -333,30 +310,25 @@ export async function createContentTypesGenerator({
debounceTimeout = setTimeout(runEventsSafe, 50 /* debounce to batch chokidar events */);
}
- async function runEvents(opts?: EventOpts) {
- const logLevel = opts?.logLevel ?? 'info';
+ async function runEvents() {
const eventResponses = [];
for (const event of events) {
- const response = await handleEvent(event.type, event.opts);
+ const response = await handleEvent(event);
eventResponses.push(response);
}
events = [];
- let unsupportedFiles = [];
for (const response of eventResponses) {
if (response.error instanceof UnsupportedFileTypeError) {
- unsupportedFiles.push(response.error.message);
+ logger.warn(
+ 'content',
+ `Unsupported file type ${bold(
+ response.error.message
+ )} found. Prefix filename with an underscore (\`_\`) to ignore.`
+ );
}
}
- if (unsupportedFiles.length > 0 && ['info', 'warn'].includes(logLevel)) {
- logger.warn(
- 'content',
- `Unsupported file types found. Prefix with an underscore (\`_\`) to ignore:\n- ${unsupportedFiles.join(
- '\n'
- )}`
- );
- }
const observable = contentConfigObserver.get();
if (eventResponses.some((r) => r.shouldGenerateTypes)) {
await writeContentFiles({
@@ -369,7 +341,7 @@ export async function createContentTypesGenerator({
viteServer,
});
invalidateVirtualMod(viteServer);
- if (observable.status === 'loaded' && ['info', 'warn'].includes(logLevel)) {
+ if (observable.status === 'loaded') {
warnNonexistentCollections({
logger,
contentConfig: observable.config,
@@ -475,6 +447,7 @@ async function writeContentFiles({
let configPathRelativeToCacheDir = normalizePath(
path.relative(contentPaths.cacheDir.pathname, contentPaths.config.url.pathname)
);
+
if (!isRelativePath(configPathRelativeToCacheDir))
configPathRelativeToCacheDir = './' + configPathRelativeToCacheDir;
@@ -514,9 +487,9 @@ function warnNonexistentCollections({
if (!collectionEntryMap[JSON.stringify(configuredCollection)]) {
logger.warn(
'content',
- `The ${JSON.stringify(
- configuredCollection
- )} collection does not have an associated folder in your \`content\` directory. Make sure the folder exists, or check your content config for typos.`
+ `The ${bold(configuredCollection)} collection is defined but no ${bold(
+ 'content/' + configuredCollection
+ )} folder exists in the content directory. Create a new folder for the collection, or check your content configuration file for typos.`
);
}
}
diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts
index a941d7f487480..4e446bf9cf280 100644
--- a/packages/astro/src/core/app/index.ts
+++ b/packages/astro/src/core/app/index.ts
@@ -1,7 +1,6 @@
import type {
EndpointHandler,
ManifestData,
- MiddlewareEndpointHandler,
RouteData,
SSRElement,
SSRManifest,
@@ -36,9 +35,11 @@ const responseSentSymbol = Symbol.for('astro.responseSent');
const STATUS_CODES = new Set([404, 500]);
-export interface MatchOptions {
- matchNotFound?: boolean | undefined;
+export interface RenderOptions {
+ routeData?: RouteData;
+ locals?: object;
}
+
export interface RenderErrorOptions {
routeData?: RouteData;
response?: Response;
@@ -63,6 +64,7 @@ export class App {
#baseWithoutTrailingSlash: string;
#pipeline: SSRRoutePipeline;
#adapterLogger: AstroIntegrationLogger;
+ #renderOptionsDeprecationWarningShown = false;
constructor(manifest: SSRManifest, streaming = true) {
this.#manifest = manifest;
@@ -127,7 +129,14 @@ export class App {
}
return pathname;
}
- match(request: Request, _opts: MatchOptions = {}): RouteData | undefined {
+
+ #getPathnameFromRequest(request: Request): string {
+ const url = new URL(request.url);
+ const pathname = prependForwardSlash(this.removeBase(url.pathname));
+ return pathname;
+ }
+
+ match(request: Request): RouteData | undefined {
const url = new URL(request.url);
// ignore requests matching public assets
if (this.#manifest.assets.has(url.pathname)) return undefined;
@@ -138,7 +147,38 @@ export class App {
return routeData;
}
- async render(request: Request, routeData?: RouteData, locals?: object): Promise {
+ async render(request: Request, options?: RenderOptions): Promise;
+ /**
+ * @deprecated Instead of passing `RouteData` and locals individually, pass an object with `routeData` and `locals` properties.
+ * See https://github.com/withastro/astro/pull/9199 for more information.
+ */
+ async render(request: Request, routeData?: RouteData, locals?: object): Promise;
+ async render(
+ request: Request,
+ routeDataOrOptions?: RouteData | RenderOptions,
+ maybeLocals?: object
+ ): Promise {
+ let routeData: RouteData | undefined;
+ let locals: object | undefined;
+
+ if (
+ routeDataOrOptions &&
+ ('routeData' in routeDataOrOptions || 'locals' in routeDataOrOptions)
+ ) {
+ if ('routeData' in routeDataOrOptions) {
+ routeData = routeDataOrOptions.routeData;
+ }
+ if ('locals' in routeDataOrOptions) {
+ locals = routeDataOrOptions.locals;
+ }
+ } else {
+ routeData = routeDataOrOptions as RouteData | undefined;
+ locals = maybeLocals;
+ if (routeDataOrOptions || locals) {
+ this.#logRenderOptionsDeprecationWarning();
+ }
+ }
+
// Handle requests with duplicate slashes gracefully by cloning with a cleaned-up request URL
if (request.url !== collapseDuplicateSlashes(request.url)) {
request = new Request(collapseDuplicateSlashes(request.url), request);
@@ -149,9 +189,9 @@ export class App {
if (!routeData) {
return this.#renderError(request, { status: 404 });
}
-
Reflect.set(request, clientLocalsSymbol, locals ?? {});
- const defaultStatus = this.#getDefaultStatusCode(routeData.route);
+ const pathname = this.#getPathnameFromRequest(request);
+ const defaultStatus = this.#getDefaultStatusCode(routeData, pathname);
const mod = await this.#getModuleForRoute(routeData);
const pageModule = (await mod.page()) as any;
@@ -173,16 +213,14 @@ export class App {
);
if (i18nMiddleware) {
if (mod.onRequest) {
- this.#pipeline.setMiddlewareFunction(
- sequence(i18nMiddleware, mod.onRequest as MiddlewareEndpointHandler)
- );
+ this.#pipeline.setMiddlewareFunction(sequence(i18nMiddleware, mod.onRequest));
} else {
this.#pipeline.setMiddlewareFunction(i18nMiddleware);
}
this.#pipeline.onBeforeRenderRoute(i18nPipelineHook);
} else {
if (mod.onRequest) {
- this.#pipeline.setMiddlewareFunction(mod.onRequest as MiddlewareEndpointHandler);
+ this.#pipeline.setMiddlewareFunction(mod.onRequest);
}
}
response = await this.#pipeline.renderRoute(renderContext, pageModule);
@@ -190,7 +228,7 @@ export class App {
if (err instanceof EndpointNotFoundError) {
return this.#renderError(request, { status: 404, response: err.originalResponse });
} else {
- this.#logger.error('ssr', err.stack || err.message || String(err));
+ this.#logger.error(null, err.stack || err.message || String(err));
return this.#renderError(request, { status: 500 });
}
}
@@ -208,6 +246,15 @@ export class App {
return response;
}
+ #logRenderOptionsDeprecationWarning() {
+ if (this.#renderOptionsDeprecationWarningShown) return;
+ this.#logger.warn(
+ 'deprecated',
+ `The adapter ${this.#manifest.adapterName} is using a deprecated signature of the 'app.render()' method. From Astro 4.0, locals and routeData are provided as properties on an optional object to this method. Using the old signature will cause an error in Astro 5.0. See https://github.com/withastro/astro/pull/9199 for more information.`
+ );
+ this.#renderOptionsDeprecationWarningShown = true;
+ }
+
setCookieHeaders(response: Response) {
return getSetCookiesFromResponse(response);
}
@@ -235,7 +282,7 @@ export class App {
env: this.#pipeline.env,
mod: handler as any,
locales: this.#manifest.i18n?.locales,
- routingStrategy: this.#manifest.i18n?.routingStrategy,
+ routing: this.#manifest.i18n?.routing,
defaultLocale: this.#manifest.i18n?.defaultLocale,
});
} else {
@@ -272,7 +319,7 @@ export class App {
mod,
env: this.#pipeline.env,
locales: this.#manifest.i18n?.locales,
- routingStrategy: this.#manifest.i18n?.routingStrategy,
+ routing: this.#manifest.i18n?.routing,
defaultLocale: this.#manifest.i18n?.defaultLocale,
});
}
@@ -315,7 +362,7 @@ export class App {
);
const page = (await mod.page()) as any;
if (skipMiddleware === false && mod.onRequest) {
- this.#pipeline.setMiddlewareFunction(mod.onRequest as MiddlewareEndpointHandler);
+ this.#pipeline.setMiddlewareFunction(mod.onRequest);
}
if (skipMiddleware) {
// make sure middleware set by other requests is cleared out
@@ -340,8 +387,12 @@ export class App {
return response;
}
- #mergeResponses(newResponse: Response, oldResponse?: Response, override?: { status: 404 | 500 }) {
- if (!oldResponse) {
+ #mergeResponses(
+ newResponse: Response,
+ originalResponse?: Response,
+ override?: { status: 404 | 500 }
+ ) {
+ if (!originalResponse) {
if (override !== undefined) {
return new Response(newResponse.body, {
status: override.status,
@@ -352,26 +403,43 @@ export class App {
return newResponse;
}
- const { statusText, headers } = oldResponse;
-
// If the new response did not have a meaningful status, an override may have been provided
// If the original status was 200 (default), override it with the new status (probably 404 or 500)
// Otherwise, the user set a specific status while rendering and we should respect that one
const status = override?.status
? override.status
- : oldResponse.status === 200
- ? newResponse.status
- : oldResponse.status;
+ : originalResponse.status === 200
+ ? newResponse.status
+ : originalResponse.status;
+ try {
+ // this function could throw an error...
+ originalResponse.headers.delete('Content-type');
+ } catch {}
return new Response(newResponse.body, {
status,
- statusText: status === 200 ? newResponse.statusText : statusText,
- headers: new Headers(Array.from(headers)),
+ statusText: status === 200 ? newResponse.statusText : originalResponse.statusText,
+ // If you're looking at here for possible bugs, it means that it's not a bug.
+ // With the middleware, users can meddle with headers, and we should pass to the 404/500.
+ // If users see something weird, it's because they are setting some headers they should not.
+ //
+ // Although, we don't want it to replace the content-type, because the error page must return `text/html`
+ headers: new Headers([
+ ...Array.from(newResponse.headers),
+ ...Array.from(originalResponse.headers),
+ ]),
});
}
- #getDefaultStatusCode(route: string): number {
- route = removeTrailingForwardSlash(route);
+ #getDefaultStatusCode(routeData: RouteData, pathname: string): number {
+ if (!routeData.pattern.exec(pathname)) {
+ for (const fallbackRoute of routeData.fallbackRoutes) {
+ if (fallbackRoute.pattern.test(pathname)) {
+ return 302;
+ }
+ }
+ }
+ const route = removeTrailingForwardSlash(routeData.route);
if (route.endsWith('/404')) return 404;
if (route.endsWith('/500')) return 500;
return 200;
diff --git a/packages/astro/src/core/app/node.ts b/packages/astro/src/core/app/node.ts
index 1df931eca22e6..f5ea38cc18d2b 100644
--- a/packages/astro/src/core/app/node.ts
+++ b/packages/astro/src/core/app/node.ts
@@ -1,11 +1,12 @@
import type { RouteData } from '../../@types/astro.js';
+import type { RenderOptions } from './index.js';
import type { SerializedSSRManifest, SSRManifest } from './types.js';
import * as fs from 'node:fs';
import { IncomingMessage } from 'node:http';
import { TLSSocket } from 'node:tls';
import { deserializeManifest } from './common.js';
-import { App, type MatchOptions } from './index.js';
+import { App } from './index.js';
export { apply as applyPolyfills } from '../polyfill.js';
const clientAddressSymbol = Symbol.for('astro.clientAddress');
@@ -108,19 +109,34 @@ class NodeIncomingMessage extends IncomingMessage {
}
export class NodeApp extends App {
- match(req: NodeIncomingMessage | Request, opts: MatchOptions = {}) {
+ match(req: NodeIncomingMessage | Request) {
if (!(req instanceof Request)) {
req = createRequestFromNodeRequest(req, {
emptyBody: true,
});
}
- return super.match(req, opts);
+ return super.match(req);
}
- render(req: NodeIncomingMessage | Request, routeData?: RouteData, locals?: object) {
+ render(request: NodeIncomingMessage | Request, options?: RenderOptions): Promise;
+ /**
+ * @deprecated Instead of passing `RouteData` and locals individually, pass an object with `routeData` and `locals` properties.
+ * See https://github.com/withastro/astro/pull/9199 for more information.
+ */
+ render(
+ request: NodeIncomingMessage | Request,
+ routeData?: RouteData,
+ locals?: object
+ ): Promise;
+ render(
+ req: NodeIncomingMessage | Request,
+ routeDataOrOptions?: RouteData | RenderOptions,
+ maybeLocals?: object
+ ) {
if (!(req instanceof Request)) {
req = createRequestFromNodeRequest(req);
}
- return super.render(req, routeData, locals);
+ // @ts-expect-error The call would have succeeded against the implementation, but implementation signatures of overloads are not externally visible.
+ return super.render(req, routeDataOrOptions, maybeLocals);
}
}
diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts
index 9f9d80f445117..ab4a4fc2cdf7f 100644
--- a/packages/astro/src/core/app/types.ts
+++ b/packages/astro/src/core/app/types.ts
@@ -1,4 +1,5 @@
import type {
+ Locales,
RouteData,
SerializedRouteData,
SSRComponentMetadata,
@@ -55,8 +56,8 @@ export type SSRManifest = {
export type SSRManifestI18n = {
fallback?: Record;
- routingStrategy?: 'prefix-always' | 'prefix-other-locales';
- locales: string[];
+ routing?: 'prefix-always' | 'prefix-other-locales';
+ locales: Locales;
defaultLocale: string;
};
diff --git a/packages/astro/src/core/build/buildPipeline.ts b/packages/astro/src/core/build/buildPipeline.ts
index fc315ff7dba24..166e42a2f8cb2 100644
--- a/packages/astro/src/core/build/buildPipeline.ts
+++ b/packages/astro/src/core/build/buildPipeline.ts
@@ -2,13 +2,16 @@ import type { AstroConfig, AstroSettings, SSRLoadedRenderer } from '../../@types
import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js';
import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
import type { SSRManifest } from '../app/types.js';
-import { Logger } from '../logger/core.js';
+import type { Logger } from '../logger/core.js';
import { Pipeline } from '../pipeline.js';
import { routeIsFallback, routeIsRedirect } from '../redirects/helpers.js';
import { createEnvironment } from '../render/index.js';
import { createAssetLink } from '../render/ssr-element.js';
import type { BuildInternals } from './internal.js';
-import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js';
+import {
+ ASTRO_PAGE_RESOLVED_MODULE_ID,
+ getVirtualModulePageNameFromPath,
+} from './plugins/plugin-pages.js';
import { RESOLVED_SPLIT_MODULE_ID } from './plugins/plugin-ssr.js';
import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js';
import type { PageBuildData, StaticBuildOptions } from './types.js';
@@ -142,15 +145,15 @@ export class BuildPipeline extends Pipeline {
retrieveRoutesToGenerate(): Map {
const pages = new Map();
- for (const [entryPoint, filePath] of this.#internals.entrySpecifierToBundleMap) {
+ for (const [entrypoint, filePath] of this.#internals.entrySpecifierToBundleMap) {
// virtual pages can be emitted with different prefixes:
// - the classic way are pages emitted with prefix ASTRO_PAGE_RESOLVED_MODULE_ID -> plugin-pages
// - pages emitted using `build.split`, in this case pages are emitted with prefix RESOLVED_SPLIT_MODULE_ID
if (
- entryPoint.includes(ASTRO_PAGE_RESOLVED_MODULE_ID) ||
- entryPoint.includes(RESOLVED_SPLIT_MODULE_ID)
+ entrypoint.includes(ASTRO_PAGE_RESOLVED_MODULE_ID) ||
+ entrypoint.includes(RESOLVED_SPLIT_MODULE_ID)
) {
- const [, pageName] = entryPoint.split(':');
+ const [, pageName] = entrypoint.split(':');
const pageData = this.#internals.pagesByComponent.get(
`${pageName.replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.')}`
);
@@ -164,16 +167,26 @@ export class BuildPipeline extends Pipeline {
}
}
- for (const [path, pageDataList] of this.#internals.pagesByComponents.entries()) {
- for (const pageData of pageDataList) {
- if (routeIsRedirect(pageData.route)) {
- pages.set(pageData, path);
- } else if (
- routeIsFallback(pageData.route) &&
- (i18nHasFallback(this.getConfig()) ||
- (routeIsFallback(pageData.route) && pageData.route.route === '/'))
- ) {
- pages.set(pageData, path);
+ for (const [path, pageData] of this.#internals.pagesByComponent.entries()) {
+ if (routeIsRedirect(pageData.route)) {
+ pages.set(pageData, path);
+ } else if (
+ routeIsFallback(pageData.route) &&
+ (i18nHasFallback(this.getConfig()) ||
+ (routeIsFallback(pageData.route) && pageData.route.route === '/'))
+ ) {
+ // The original component is transformed during the first build, so we have to retrieve
+ // the actual `.mjs` that was created.
+ // During the build, we transform the names of our pages with some weird name, and those weird names become the keys of a map.
+ // The values of the map are the actual `.mjs` files that are generated during the build
+
+ // Here, we take the component path and transform it in the virtual module name
+ const moduleSpecifier = getVirtualModulePageNameFromPath(path);
+ // We retrieve the original JS module
+ const filePath = this.#internals.entrySpecifierToBundleMap.get(moduleSpecifier);
+ if (filePath) {
+ // it exists, added it to pages to render, using the file path that we jus retrieved
+ pages.set(pageData, filePath);
}
}
}
diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts
index 20854f779b0e2..486fac9fbcf03 100644
--- a/packages/astro/src/core/build/generate.ts
+++ b/packages/astro/src/core/build/generate.ts
@@ -1,16 +1,13 @@
-import * as colors from 'kleur/colors';
-import { bgGreen, black, cyan, dim, green, magenta } from 'kleur/colors';
+import { bgGreen, black, blue, bold, dim, green, magenta, red } from 'kleur/colors';
import fs from 'node:fs';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
import PQueue from 'p-queue';
import type { OutputAsset, OutputChunk } from 'rollup';
-import type { BufferEncoding } from 'vfile';
import type {
AstroSettings,
ComponentInstance,
GetStaticPathsItem,
- MiddlewareEndpointHandler,
RouteData,
RouteType,
SSRError,
@@ -113,16 +110,6 @@ async function getEntryForFallbackRoute(
return RedirectSinglePageBuiltModule;
}
-function shouldSkipDraft(pageModule: ComponentInstance, settings: AstroSettings): boolean {
- return (
- // Drafts are disabled
- !settings.config.markdown.drafts &&
- // This is a draft post
- 'frontmatter' in pageModule &&
- (pageModule as any).frontmatter?.draft === true
- );
-}
-
// Gives back a facadeId that is relative to the root.
// ie, src/pages/index.astro instead of /Users/name..../src/pages/index.astro
export function rootRelativeFacadeId(facadeId: string, settings: AstroSettings): string {
@@ -149,7 +136,7 @@ export function chunkIsPage(
}
export async function generatePages(opts: StaticBuildOptions, internals: BuildInternals) {
- const timer = performance.now();
+ const generatePagesTimer = performance.now();
const ssr = isServerLikeOutput(opts.settings.config);
let manifest: SSRManifest;
if (ssr) {
@@ -179,7 +166,7 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
}
const verb = ssr ? 'prerendering' : 'generating';
- logger.info(null, `\n${bgGreen(black(` ${verb} static routes `))}`);
+ logger.info('SKIP_FORMAT', `\n${bgGreen(black(` ${verb} static routes `))}`);
const builtPaths = new Set();
const pagesToGenerate = pipeline.retrieveRoutesToGenerate();
if (ssr) {
@@ -187,11 +174,7 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
if (pageData.route.prerender) {
const ssrEntryURLPage = createEntryURL(filePath, outFolder);
const ssrEntryPage = await import(ssrEntryURLPage.toString());
- if (
- // TODO: remove in Astro 4.0
- opts.settings.config.build.split ||
- opts.settings.adapter?.adapterFeatures?.functionPerRoute
- ) {
+ if (opts.settings.adapter?.adapterFeatures?.functionPerRoute) {
// forcing to use undefined, so we fail in an expected way if the module is not even there.
const ssrEntry = ssrEntryPage?.pageModule;
if (ssrEntry) {
@@ -223,12 +206,14 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
}
}
}
-
- logger.info(null, dim(`Completed in ${getTimeStat(timer, performance.now())}.\n`));
+ logger.info(
+ null,
+ green(`✓ Completed in ${getTimeStat(generatePagesTimer, performance.now())}.\n`)
+ );
const staticImageList = getStaticImageList();
if (staticImageList.size) {
- logger.info(null, `\n${bgGreen(black(` generating optimized images `))}`);
+ logger.info('SKIP_FORMAT', `${bgGreen(black(` generating optimized images `))}`);
const totalCount = Array.from(staticImageList.values())
.map((x) => x.transforms.size)
@@ -244,7 +229,7 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
await queue.onIdle();
const assetsTimeEnd = performance.now();
- logger.info(null, dim(`Completed in ${getTimeStat(assetsTimer, assetsTimeEnd)}.\n`));
+ logger.info(null, green(`✓ Completed in ${getTimeStat(assetsTimer, assetsTimeEnd)}.\n`));
delete globalThis?.astroAsset?.addStaticImage;
}
@@ -261,37 +246,36 @@ async function generatePage(
builtPaths: Set,
pipeline: BuildPipeline
) {
- let timeStart = performance.now();
+ // prepare information we need
const logger = pipeline.getLogger();
const config = pipeline.getConfig();
+ const pageModulePromise = ssrEntry.page;
+ const onRequest = ssrEntry.onRequest;
const pageInfo = getPageDataByComponent(pipeline.getInternals(), pageData.route.component);
- // may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
- const linkIds: [] = [];
- const scripts = pageInfo?.hoistedScript ?? null;
+ // Calculate information of the page, like scripts, links and styles
const styles = pageData.styles
.sort(cssOrder)
.map(({ sheet }) => sheet)
.reduce(mergeInlineCss, []);
-
- const pageModulePromise = ssrEntry.page;
- const onRequest = ssrEntry.onRequest;
+ // may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
+ const linkIds: [] = [];
+ const scripts = pageInfo?.hoistedScript ?? null;
+ // prepare the middleware
const i18nMiddleware = createI18nMiddleware(
pipeline.getManifest().i18n,
pipeline.getManifest().base,
pipeline.getManifest().trailingSlash
);
- if (config.experimental.i18n && i18nMiddleware) {
+ if (config.i18n && i18nMiddleware) {
if (onRequest) {
- pipeline.setMiddlewareFunction(
- sequence(i18nMiddleware, onRequest as MiddlewareEndpointHandler)
- );
+ pipeline.setMiddlewareFunction(sequence(i18nMiddleware, onRequest));
} else {
pipeline.setMiddlewareFunction(i18nMiddleware);
}
pipeline.onBeforeRenderRoute(i18nPipelineHook);
} else if (onRequest) {
- pipeline.setMiddlewareFunction(onRequest as MiddlewareEndpointHandler);
+ pipeline.setMiddlewareFunction(onRequest);
}
if (!pageModulePromise) {
throw new Error(
@@ -299,16 +283,6 @@ async function generatePage(
);
}
const pageModule = await pageModulePromise();
- if (shouldSkipDraft(pageModule, pipeline.getSettings())) {
- logger.info(null, `${magenta('⚠️')} Skipping draft ${pageData.route.component}`);
- // TODO: Remove in Astro 4.0
- logger.warn(
- 'astro',
- `The drafts feature is deprecated. You should migrate to content collections instead. See https://docs.astro.build/en/guides/content-collections/#filtering-collection-queries for more information.`
- );
- return;
- }
-
const generationOptions: Readonly = {
pageData,
linkIds,
@@ -316,38 +290,41 @@ async function generatePage(
styles,
mod: pageModule,
};
-
- const icon =
- pageData.route.type === 'page' ||
- pageData.route.type === 'redirect' ||
- pageData.route.type === 'fallback'
- ? green('▶')
- : magenta('λ');
- if (isRelativePath(pageData.route.component)) {
- logger.info(null, `${icon} ${pageData.route.route}`);
- } else {
- logger.info(null, `${icon} ${pageData.route.component}`);
+ // Now we explode the routes. A route render itself, and it can render its fallbacks (i18n routing)
+ for (const route of eachRouteInRouteData(pageData)) {
+ const icon =
+ route.type === 'page' || route.type === 'redirect' || route.type === 'fallback'
+ ? green('▶')
+ : magenta('λ');
+ logger.info(null, `${icon} ${getPrettyRouteName(route)}`);
+ // Get paths for the route, calling getStaticPaths if needed.
+ const paths = await getPathsForRoute(route, pageModule, pipeline, builtPaths);
+ let timeStart = performance.now();
+ let prevTimeEnd = timeStart;
+ for (let i = 0; i < paths.length; i++) {
+ const path = paths[i];
+ pipeline.getEnvironment().logger.debug('build', `Generating: ${path}`);
+ await generatePath(path, pipeline, generationOptions, route);
+ const timeEnd = performance.now();
+ const timeChange = getTimeStat(prevTimeEnd, timeEnd);
+ const timeIncrease = `(+${timeChange})`;
+ const filePath = getOutputFilename(pipeline.getConfig(), path, pageData.route.type);
+ const lineIcon = i === paths.length - 1 ? '└─' : '├─';
+ logger.info(null, ` ${blue(lineIcon)} ${dim(filePath)} ${dim(timeIncrease)}`);
+ prevTimeEnd = timeEnd;
+ }
}
+}
- // Get paths for the route, calling getStaticPaths if needed.
- const paths = await getPathsForRoute(pageData, pageModule, pipeline, builtPaths);
-
- let prevTimeEnd = timeStart;
- for (let i = 0; i < paths.length; i++) {
- const path = paths[i];
- await generatePath(path, generationOptions, pipeline);
- const timeEnd = performance.now();
- const timeChange = getTimeStat(prevTimeEnd, timeEnd);
- const timeIncrease = `(+${timeChange})`;
- const filePath = getOutputFilename(pipeline.getConfig(), path, pageData.route.type);
- const lineIcon = i === paths.length - 1 ? '└─' : '├─';
- logger.info(null, ` ${cyan(lineIcon)} ${dim(filePath)} ${dim(timeIncrease)}`);
- prevTimeEnd = timeEnd;
+function* eachRouteInRouteData(data: PageBuildData) {
+ yield data.route;
+ for (const fallbackRoute of data.route.fallbackRoutes) {
+ yield fallbackRoute;
}
}
async function getPathsForRoute(
- pageData: PageBuildData,
+ route: RouteData,
mod: ComponentInstance,
pipeline: BuildPipeline,
builtPaths: Set
@@ -355,11 +332,16 @@ async function getPathsForRoute(
const opts = pipeline.getStaticBuildOptions();
const logger = pipeline.getLogger();
let paths: Array = [];
- if (pageData.route.pathname) {
- paths.push(pageData.route.pathname);
- builtPaths.add(pageData.route.pathname);
+ if (route.pathname) {
+ paths.push(route.pathname);
+ builtPaths.add(route.pathname);
+ for (const virtualRoute of route.fallbackRoutes) {
+ if (virtualRoute.pathname) {
+ paths.push(virtualRoute.pathname);
+ builtPaths.add(virtualRoute.pathname);
+ }
+ }
} else {
- const route = pageData.route;
const staticPaths = await callGetStaticPaths({
mod,
route,
@@ -367,16 +349,14 @@ async function getPathsForRoute(
logger,
ssr: isServerLikeOutput(opts.settings.config),
}).catch((err) => {
- logger.debug('build', `├── ${colors.bold(colors.red('✗'))} ${route.component}`);
+ logger.debug('build', `├── ${bold(red('✗'))} ${route.component}`);
throw err;
});
const label = staticPaths.length === 1 ? 'page' : 'pages';
logger.debug(
'build',
- `├── ${colors.bold(colors.green('✔'))} ${route.component} → ${colors.magenta(
- `[${staticPaths.length} ${label}]`
- )}`
+ `├── ${bold(green('✔'))} ${route.component} → ${magenta(`[${staticPaths.length} ${label}]`)}`
);
paths = staticPaths
@@ -490,18 +470,24 @@ function getUrlForPath(
return url;
}
-async function generatePath(pathname: string, gopts: GeneratePathOptions, pipeline: BuildPipeline) {
- const manifest = pipeline.getManifest();
+interface GeneratePathOptions {
+ pageData: PageBuildData;
+ linkIds: string[];
+ scripts: { type: 'inline' | 'external'; value: string } | null;
+ styles: StylesheetAsset[];
+ mod: ComponentInstance;
+}
+async function generatePath(
+ pathname: string,
+ pipeline: BuildPipeline,
+ gopts: GeneratePathOptions,
+ route: RouteData
+) {
const { mod, scripts: hoistedScripts, styles: _styles, pageData } = gopts;
-
- // This adds the page name to the array so it can be shown as part of stats.
- if (pageData.route.type === 'page') {
- addPageName(pathname, pipeline.getStaticBuildOptions());
- }
-
+ const manifest = pipeline.getManifest();
+ const logger = pipeline.getLogger();
pipeline.getEnvironment().logger.debug('build', `Generating: ${pathname}`);
- // may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
const links = new Set();
const scripts = createModuleScriptsSet(
hoistedScripts ? [hoistedScripts] : [],
@@ -532,13 +518,18 @@ async function generatePath(pathname: string, gopts: GeneratePathOptions, pipeli
}
}
+ // This adds the page name to the array so it can be shown as part of stats.
+ if (route.type === 'page') {
+ addPageName(pathname, pipeline.getStaticBuildOptions());
+ }
+
const ssr = isServerLikeOutput(pipeline.getConfig());
const url = getUrlForPath(
pathname,
pipeline.getConfig().base,
pipeline.getStaticBuildOptions().origin,
pipeline.getConfig().build.format,
- pageData.route.type
+ route.type
);
const request = createRequest({
@@ -547,7 +538,8 @@ async function generatePath(pathname: string, gopts: GeneratePathOptions, pipeli
logger: pipeline.getLogger(),
ssr,
});
- const i18n = pipeline.getConfig().experimental.i18n;
+ const i18n = pipeline.getConfig().i18n;
+
const renderContext = await createRenderContext({
pathname,
request,
@@ -555,23 +547,22 @@ async function generatePath(pathname: string, gopts: GeneratePathOptions, pipeli
scripts,
styles,
links,
- route: pageData.route,
+ route,
env: pipeline.getEnvironment(),
mod,
locales: i18n?.locales,
- routingStrategy: i18n?.routingStrategy,
+ routing: i18n?.routing,
defaultLocale: i18n?.defaultLocale,
});
let body: string | Uint8Array;
- let encoding: BufferEncoding | undefined;
let response: Response;
try {
response = await pipeline.renderRoute(renderContext, mod);
} catch (err) {
if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') {
- (err as SSRError).id = pageData.component;
+ (err as SSRError).id = route.component;
}
throw err;
}
@@ -600,22 +591,33 @@ async function generatePath(pathname: string, gopts: GeneratePathOptions, pipeli
body = body.replaceAll('\n', '');
}
// A dynamic redirect, set the location so that integrations know about it.
- if (pageData.route.type !== 'redirect') {
- pageData.route.redirect = location.toString();
+ if (route.type !== 'redirect') {
+ route.redirect = location.toString();
}
} else {
// If there's no body, do nothing
if (!response.body) return;
body = Buffer.from(await response.arrayBuffer());
- encoding = (response.headers.get('X-Astro-Encoding') as BufferEncoding | null) ?? 'utf-8';
}
- const outFolder = getOutFolder(pipeline.getConfig(), pathname, pageData.route.type);
- const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, pageData.route.type);
- pageData.route.distURL = outFile;
+ const outFolder = getOutFolder(pipeline.getConfig(), pathname, route.type);
+ const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, route.type);
+ route.distURL = outFile;
await fs.promises.mkdir(outFolder, { recursive: true });
- await fs.promises.writeFile(outFile, body, encoding);
+ await fs.promises.writeFile(outFile, body);
+}
+
+function getPrettyRouteName(route: RouteData): string {
+ if (isRelativePath(route.component)) {
+ return route.route;
+ } else if (route.component.includes('node_modules/')) {
+ // For routes from node_modules (usually injected by integrations),
+ // prettify it by only grabbing the part after the last `node_modules/`
+ return route.component.match(/.*node_modules\/(.+)/)?.[1] ?? route.component;
+ } else {
+ return route.component;
+ }
}
/**
@@ -631,12 +633,12 @@ export function createBuildManifest(
renderers: SSRLoadedRenderer[]
): SSRManifest {
let i18nManifest: SSRManifestI18n | undefined = undefined;
- if (settings.config.experimental.i18n) {
+ if (settings.config.i18n) {
i18nManifest = {
- fallback: settings.config.experimental.i18n.fallback,
- routingStrategy: settings.config.experimental.i18n.routingStrategy,
- defaultLocale: settings.config.experimental.i18n.defaultLocale,
- locales: settings.config.experimental.i18n.locales,
+ fallback: settings.config.i18n.fallback,
+ routing: settings.config.i18n.routing,
+ defaultLocale: settings.config.i18n.defaultLocale,
+ locales: settings.config.i18n.locales,
};
}
return {
diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts
index f096b8f767e96..551e686dc472c 100644
--- a/packages/astro/src/core/build/index.ts
+++ b/packages/astro/src/core/build/index.ts
@@ -1,4 +1,4 @@
-import * as colors from 'kleur/colors';
+import { blue, bold, green } from 'kleur/colors';
import fs from 'node:fs';
import { performance } from 'node:perf_hooks';
import { fileURLToPath } from 'node:url';
@@ -24,7 +24,8 @@ import { resolveConfig } from '../config/config.js';
import { createNodeLogger } from '../config/logging.js';
import { createSettings } from '../config/settings.js';
import { createVite } from '../create-vite.js';
-import { Logger, levels, timerMessage } from '../logger/core.js';
+import type { Logger } from '../logger/core.js';
+import { levels, timerMessage } from '../logger/core.js';
import { apply as applyPolyfill } from '../polyfill.js';
import { RouteCache } from '../render/route-cache.js';
import { createRouteManifest } from '../routing/index.js';
@@ -69,8 +70,9 @@ export default async function build(
if (astroConfig.experimental.contentCollectionCache && options.force) {
const contentCacheDir = new URL('./content/', astroConfig.cacheDir);
if (fs.existsSync(contentCacheDir)) {
- logger.warn('content', 'clearing cache');
+ logger.debug('content', 'clearing content cache');
await fs.promises.rm(contentCacheDir, { force: true, recursive: true });
+ logger.warn('content', 'content cache cleared (force)');
}
}
@@ -157,9 +159,10 @@ class AstroBuilder {
await runHookBuildStart({ config: this.settings.config, logging: this.logger });
this.validateConfig();
- this.logger.info('build', `output target: ${colors.green(this.settings.config.output)}`);
+ this.logger.info('build', `output: ${blue('"' + this.settings.config.output + '"')}`);
+ this.logger.info('build', `directory: ${blue(fileURLToPath(this.settings.config.outDir))}`);
if (this.settings.adapter) {
- this.logger.info('build', `deploy adapter: ${colors.green(this.settings.adapter.name)}`);
+ this.logger.info('build', `adapter: ${green(this.settings.adapter.name)}`);
}
this.logger.info('build', 'Collecting build info...');
this.timer.loadStart = performance.now();
@@ -179,7 +182,7 @@ class AstroBuilder {
this.timer.buildStart = performance.now();
this.logger.info(
'build',
- colors.dim(`Completed in ${getTimeStat(this.timer.init, performance.now())}.`)
+ green(`✓ Completed in ${getTimeStat(this.timer.init, performance.now())}.`)
);
const opts: StaticBuildOptions = {
@@ -195,8 +198,8 @@ class AstroBuilder {
viteConfig,
};
- const { internals } = await viteBuild(opts);
- await staticBuild(opts, internals);
+ const { internals, ssrOutputChunkNames } = await viteBuild(opts);
+ await staticBuild(opts, internals, ssrOutputChunkNames);
// Write any additionally generated assets to disk.
this.timer.assetsStart = performance.now();
@@ -251,31 +254,6 @@ class AstroBuilder {
`the outDir cannot be the root folder. Please build to a folder such as dist.`
);
}
-
- if (config.build.split === true) {
- if (config.output === 'static') {
- this.logger.warn(
- 'configuration',
- 'The option `build.split` won\'t take effect, because `output` is not `"server"` or `"hybrid"`.'
- );
- }
- this.logger.warn(
- 'configuration',
- 'The option `build.split` is deprecated. Use the adapter options.'
- );
- }
- if (config.build.excludeMiddleware === true) {
- if (config.output === 'static') {
- this.logger.warn(
- 'configuration',
- 'The option `build.excludeMiddleware` won\'t take effect, because `output` is not `"server"` or `"hybrid"`.'
- );
- }
- this.logger.warn(
- 'configuration',
- 'The option `build.excludeMiddleware` is deprecated. Use the adapter options.'
- );
- }
}
/** Stats */
@@ -294,12 +272,12 @@ class AstroBuilder {
let messages: string[] = [];
if (buildMode === 'static') {
- messages = [`${pageCount} page(s) built in`, colors.bold(total)];
+ messages = [`${pageCount} page(s) built in`, bold(total)];
} else {
- messages = ['Server built in', colors.bold(total)];
+ messages = ['Server built in', bold(total)];
}
logger.info('build', messages.join(' '));
- logger.info('build', `${colors.bold('Complete!')}`);
+ logger.info('build', `${bold('Complete!')}`);
}
}
diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts
index 1dc38e73566b4..34e76ab1df1bc 100644
--- a/packages/astro/src/core/build/internal.ts
+++ b/packages/astro/src/core/build/internal.ts
@@ -2,7 +2,6 @@ import type { Rollup } from 'vite';
import type { RouteData, SSRResult } from '../../@types/astro.js';
import type { PageOptions } from '../../vite-plugin-astro/types.js';
import { prependForwardSlash, removeFileExtension } from '../path.js';
-import { routeIsFallback } from '../redirects/helpers.js';
import { viteID } from '../util.js';
import {
ASTRO_PAGE_RESOLVED_MODULE_ID,
@@ -38,16 +37,9 @@ export interface BuildInternals {
/**
* A map for page-specific information.
- * // TODO: Remove in Astro 4.0
- * @deprecated
*/
pagesByComponent: Map;
- /**
- * TODO: Use this in Astro 4.0
- */
- pagesByComponents: Map;
-
/**
* A map for page-specific output.
*/
@@ -126,7 +118,6 @@ export function createBuildInternals(): BuildInternals {
entrySpecifierToBundleMap: new Map(),
pageToBundleMap: new Map(),
pagesByComponent: new Map(),
- pagesByComponents: new Map(),
pageOptionsByPage: new Map(),
pagesByViteID: new Map(),
pagesByClientOnly: new Map(),
@@ -152,16 +143,7 @@ export function trackPageData(
componentURL: URL
): void {
pageData.moduleSpecifier = componentModuleId;
- if (!routeIsFallback(pageData.route)) {
- internals.pagesByComponent.set(component, pageData);
- }
- const list = internals.pagesByComponents.get(component);
- if (list) {
- list.push(pageData);
- internals.pagesByComponents.set(component, list);
- } else {
- internals.pagesByComponents.set(component, [pageData]);
- }
+ internals.pagesByComponent.set(component, pageData);
internals.pagesByViteID.set(viteID(componentURL), pageData);
}
@@ -258,25 +240,23 @@ export function* eachPageData(internals: BuildInternals) {
}
export function* eachPageFromAllPages(allPages: AllPagesData): Generator<[string, PageBuildData]> {
- for (const [path, list] of Object.entries(allPages)) {
- for (const pageData of list) {
- yield [path, pageData];
- }
+ for (const [path, pageData] of Object.entries(allPages)) {
+ yield [path, pageData];
}
}
export function* eachPageDataFromEntryPoint(
internals: BuildInternals
): Generator<[PageBuildData, string]> {
- for (const [entryPoint, filePath] of internals.entrySpecifierToBundleMap) {
+ for (const [entrypoint, filePath] of internals.entrySpecifierToBundleMap) {
// virtual pages can be emitted with different prefixes:
// - the classic way are pages emitted with prefix ASTRO_PAGE_RESOLVED_MODULE_ID -> plugin-pages
// - pages emitted using `build.split`, in this case pages are emitted with prefix RESOLVED_SPLIT_MODULE_ID
if (
- entryPoint.includes(ASTRO_PAGE_RESOLVED_MODULE_ID) ||
- entryPoint.includes(RESOLVED_SPLIT_MODULE_ID)
+ entrypoint.includes(ASTRO_PAGE_RESOLVED_MODULE_ID) ||
+ entrypoint.includes(RESOLVED_SPLIT_MODULE_ID)
) {
- const [, pageName] = entryPoint.split(':');
+ const [, pageName] = entrypoint.split(':');
const pageData = internals.pagesByComponent.get(
`${pageName.replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.')}`
);
diff --git a/packages/astro/src/core/build/page-data.ts b/packages/astro/src/core/build/page-data.ts
index 7292cb4e8f94f..89eca3ffc5efd 100644
--- a/packages/astro/src/core/build/page-data.ts
+++ b/packages/astro/src/core/build/page-data.ts
@@ -47,29 +47,16 @@ export async function collectPagesData(
clearInterval(routeCollectionLogTimeout);
}, 10000);
builtPaths.add(route.pathname);
- if (allPages[route.component]) {
- allPages[route.component].push({
- component: route.component,
- route,
- moduleSpecifier: '',
- styles: [],
- propagatedStyles: new Map(),
- propagatedScripts: new Map(),
- hoistedScript: undefined,
- });
- } else {
- allPages[route.component] = [
- {
- component: route.component,
- route,
- moduleSpecifier: '',
- styles: [],
- propagatedStyles: new Map(),
- propagatedScripts: new Map(),
- hoistedScript: undefined,
- },
- ];
- }
+
+ allPages[route.component] = {
+ component: route.component,
+ route,
+ moduleSpecifier: '',
+ styles: [],
+ propagatedStyles: new Map(),
+ propagatedScripts: new Map(),
+ hoistedScript: undefined,
+ };
clearInterval(routeCollectionLogTimeout);
if (settings.config.output === 'static') {
@@ -84,29 +71,16 @@ export async function collectPagesData(
continue;
}
// dynamic route:
- if (allPages[route.component]) {
- allPages[route.component].push({
- component: route.component,
- route,
- moduleSpecifier: '',
- styles: [],
- propagatedStyles: new Map(),
- propagatedScripts: new Map(),
- hoistedScript: undefined,
- });
- } else {
- allPages[route.component] = [
- {
- component: route.component,
- route,
- moduleSpecifier: '',
- styles: [],
- propagatedStyles: new Map(),
- propagatedScripts: new Map(),
- hoistedScript: undefined,
- },
- ];
- }
+
+ allPages[route.component] = {
+ component: route.component,
+ route,
+ moduleSpecifier: '',
+ styles: [],
+ propagatedStyles: new Map(),
+ propagatedScripts: new Map(),
+ hoistedScript: undefined,
+ };
}
clearInterval(dataCollectionLogTimeout);
diff --git a/packages/astro/src/core/build/plugin.ts b/packages/astro/src/core/build/plugin.ts
index c611c5186ed54..68474b0576b76 100644
--- a/packages/astro/src/core/build/plugin.ts
+++ b/packages/astro/src/core/build/plugin.ts
@@ -1,4 +1,4 @@
-import type { Plugin as VitePlugin } from 'vite';
+import type { Plugin as VitePlugin, Rollup } from 'vite';
import type { BuildInternals } from './internal.js';
import type { StaticBuildOptions, ViteBuildReturn } from './types.js';
@@ -68,7 +68,7 @@ export function createPluginContainer(options: StaticBuildOptions, internals: Bu
};
},
- async runPostHook(ssrReturn: ViteBuildReturn, clientReturn: ViteBuildReturn | null) {
+ async runPostHook(ssrOutputs: Rollup.RollupOutput[], clientOutputs: Rollup.RollupOutput[]) {
const mutations = new Map<
string,
{
@@ -76,20 +76,6 @@ export function createPluginContainer(options: StaticBuildOptions, internals: Bu
code: string;
}
>();
- const ssrOutputs: RollupOutputArray = [];
- const clientOutputs: RollupOutputArray = [];
-
- if (Array.isArray(ssrReturn)) {
- ssrOutputs.push(...ssrReturn);
- } else if ('output' in ssrReturn) {
- ssrOutputs.push(ssrReturn);
- }
-
- if (Array.isArray(clientReturn)) {
- clientOutputs.push(...clientReturn);
- } else if (clientReturn && 'output' in clientReturn) {
- clientOutputs.push(clientReturn);
- }
const mutate: MutateChunk = (chunk, targets, newCode) => {
chunk.code = newCode;
diff --git a/packages/astro/src/core/build/plugins/index.ts b/packages/astro/src/core/build/plugins/index.ts
index 879f72210f92c..9c1f9b5500806 100644
--- a/packages/astro/src/core/build/plugins/index.ts
+++ b/packages/astro/src/core/build/plugins/index.ts
@@ -1,7 +1,6 @@
import { astroConfigBuildPlugin } from '../../../content/vite-plugin-content-assets.js';
import { astroHeadBuildPlugin } from '../../../vite-plugin-head/index.js';
import type { AstroBuildPluginContainer } from '../plugin.js';
-import { pluginAliasResolve } from './plugin-alias-resolve.js';
import { pluginAnalyzer } from './plugin-analyzer.js';
import { pluginChunks } from './plugin-chunks.js';
import { pluginComponentEntry } from './plugin-component-entry.js';
@@ -18,7 +17,6 @@ import { pluginSSR, pluginSSRSplit } from './plugin-ssr.js';
export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) {
register(pluginComponentEntry(internals));
- register(pluginAliasResolve(internals));
register(pluginAnalyzer(options, internals));
register(pluginInternals(internals));
register(pluginManifest(options, internals));
diff --git a/packages/astro/src/core/build/plugins/plugin-alias-resolve.ts b/packages/astro/src/core/build/plugins/plugin-alias-resolve.ts
deleted file mode 100644
index 6fb09acf8c437..0000000000000
--- a/packages/astro/src/core/build/plugins/plugin-alias-resolve.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import type { Alias, Plugin as VitePlugin } from 'vite';
-import type { BuildInternals } from '../internal.js';
-import type { AstroBuildPlugin } from '../plugin.js';
-
-/**
- * `@rollup/plugin-alias` doesn't resolve aliases in Rollup input by default. This plugin fixes it
- * with a partial fork of it's resolve function. https://github.com/rollup/plugins/blob/master/packages/alias/src/index.ts
- * When https://github.com/rollup/plugins/pull/1402 is merged, we can remove this plugin.
- */
-export function vitePluginAliasResolve(internals: BuildInternals): VitePlugin {
- let aliases: Alias[];
-
- return {
- name: '@astro/plugin-alias-resolve',
- enforce: 'pre',
- configResolved(config) {
- aliases = config.resolve.alias;
- },
- async resolveId(id, importer, opts) {
- if (
- !importer &&
- (internals.discoveredHydratedComponents.has(id) ||
- internals.discoveredClientOnlyComponents.has(id))
- ) {
- const matchedEntry = aliases.find((entry) => matches(entry.find, id));
- if (!matchedEntry) {
- return null;
- }
-
- const updatedId = id.replace(matchedEntry.find, matchedEntry.replacement);
-
- return this.resolve(updatedId, importer, Object.assign({ skipSelf: true }, opts)).then(
- (resolved) => resolved || { id: updatedId }
- );
- }
- },
- };
-}
-
-function matches(pattern: string | RegExp, importee: string) {
- if (pattern instanceof RegExp) {
- return pattern.test(importee);
- }
- if (importee.length < pattern.length) {
- return false;
- }
- if (importee === pattern) {
- return true;
- }
- return importee.startsWith(pattern + '/');
-}
-
-export function pluginAliasResolve(internals: BuildInternals): AstroBuildPlugin {
- return {
- targets: ['client'],
- hooks: {
- 'build:before': () => {
- return {
- vitePlugin: vitePluginAliasResolve(internals),
- };
- },
- },
- };
-}
diff --git a/packages/astro/src/core/build/plugins/plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts
index f8611898f0a06..318a1fee545ea 100644
--- a/packages/astro/src/core/build/plugins/plugin-css.ts
+++ b/packages/astro/src/core/build/plugins/plugin-css.ts
@@ -223,8 +223,8 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
inlineConfig === 'always'
? true
: inlineConfig === 'never'
- ? false
- : assetSize <= assetsInlineLimit;
+ ? false
+ : assetSize <= assetsInlineLimit;
// there should be a single js object for each stylesheet,
// allowing the single reference to be shared and checked for duplicates
diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts
index 83065ecacbe42..1a313b6bbdb40 100644
--- a/packages/astro/src/core/build/plugins/plugin-manifest.ts
+++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts
@@ -98,8 +98,6 @@ export function pluginManifest(
const manifest = await createManifest(options, internals);
const shouldPassMiddlewareEntryPoint =
- // TODO: remove in Astro 4.0
- options.settings.config.build.excludeMiddleware ||
options.settings.adapter?.adapterFeatures?.edgeMiddleware;
await runHookBuildSsr({
config: options.settings.config,
@@ -242,12 +240,12 @@ function buildManifest(
entryModules[BEFORE_HYDRATION_SCRIPT_ID] = '';
}
let i18nManifest: SSRManifestI18n | undefined = undefined;
- if (settings.config.experimental.i18n) {
+ if (settings.config.i18n) {
i18nManifest = {
- fallback: settings.config.experimental.i18n.fallback,
- routingStrategy: settings.config.experimental.i18n.routingStrategy,
- locales: settings.config.experimental.i18n.locales,
- defaultLocale: settings.config.experimental.i18n.defaultLocale,
+ fallback: settings.config.i18n.fallback,
+ routing: settings.config.i18n.routing,
+ locales: settings.config.i18n.locales,
+ defaultLocale: settings.config.i18n.defaultLocale,
};
}
diff --git a/packages/astro/src/core/build/plugins/plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts
index 2a348f18fece2..fee94071ee25b 100644
--- a/packages/astro/src/core/build/plugins/plugin-pages.ts
+++ b/packages/astro/src/core/build/plugins/plugin-pages.ts
@@ -92,10 +92,6 @@ function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): V
}
export function shouldBundleMiddleware(settings: AstroSettings) {
- // TODO: Remove in Astro 4.0
- if (settings.config.build.excludeMiddleware === true) {
- return false;
- }
if (settings.adapter?.adapterFeatures?.edgeMiddleware === true) {
return false;
}
diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts
index fd892c9b63c74..6a6dd224af4fd 100644
--- a/packages/astro/src/core/build/plugins/plugin-ssr.ts
+++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts
@@ -102,10 +102,7 @@ export function pluginSSR(
hooks: {
'build:before': () => {
let vitePlugin =
- ssr &&
- // TODO: Remove in Astro 4.0
- options.settings.config.build.split === false &&
- functionPerRouteEnabled === false
+ ssr && functionPerRouteEnabled === false
? vitePluginSSR(internals, options.settings.adapter!, options)
: undefined;
@@ -119,7 +116,7 @@ export function pluginSSR(
return;
}
- if (options.settings.config.build.split || functionPerRouteEnabled) {
+ if (functionPerRouteEnabled) {
return;
}
@@ -146,7 +143,7 @@ function vitePluginSSRSplit(
name: '@astrojs/vite-plugin-astro-ssr-split',
enforce: 'post',
options(opts) {
- if (options.settings.config.build.split || functionPerRouteEnabled) {
+ if (functionPerRouteEnabled) {
const inputs = new Set();
for (const [path, pageData] of eachPageFromAllPages(options.allPages)) {
@@ -223,7 +220,7 @@ export function pluginSSRSplit(
hooks: {
'build:before': () => {
let vitePlugin =
- ssr && (options.settings.config.build.split || functionPerRouteEnabled)
+ ssr && functionPerRouteEnabled
? vitePluginSSRSplit(internals, options.settings.adapter!, options)
: undefined;
@@ -240,7 +237,7 @@ function generateSSRCode(config: AstroConfig, adapter: AstroAdapter) {
const imports: string[] = [];
const contents: string[] = [];
let pageMap;
- if (config.build.split || isFunctionPerRouteEnabled(adapter)) {
+ if (isFunctionPerRouteEnabled(adapter)) {
pageMap = 'pageModule';
} else {
pageMap = 'pageMap';
diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts
index 81dcdb4a00b01..cd642aca0ed10 100644
--- a/packages/astro/src/core/build/static-build.ts
+++ b/packages/astro/src/core/build/static-build.ts
@@ -1,7 +1,7 @@
import { teardown } from '@astrojs/compiler';
import * as eslexer from 'es-module-lexer';
import glob from 'fast-glob';
-import { bgGreen, bgMagenta, black, dim } from 'kleur/colors';
+import { bgGreen, bgMagenta, black, green } from 'kleur/colors';
import fs from 'node:fs';
import path, { extname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -33,7 +33,7 @@ import { RESOLVED_RENDERERS_MODULE_ID } from './plugins/plugin-renderers.js';
import { RESOLVED_SPLIT_MODULE_ID, RESOLVED_SSR_VIRTUAL_MODULE_ID } from './plugins/plugin-ssr.js';
import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js';
import type { StaticBuildOptions } from './types.js';
-import { encodeName, getTimeStat } from './util.js';
+import { encodeName, getTimeStat, viteBuildReturnToRollupOutputs } from './util.js';
export async function viteBuild(opts: StaticBuildOptions) {
const { allPages, settings } = opts;
@@ -51,17 +51,15 @@ export async function viteBuild(opts: StaticBuildOptions) {
// Build internals needed by the CSS plugin
const internals = createBuildInternals();
- for (const [component, pageDataList] of Object.entries(allPages)) {
- for (const pageData of pageDataList) {
- const astroModuleURL = new URL('./' + component, settings.config.root);
- const astroModuleId = prependForwardSlash(component);
+ for (const [component, pageData] of Object.entries(allPages)) {
+ const astroModuleURL = new URL('./' + component, settings.config.root);
+ const astroModuleId = prependForwardSlash(component);
- // Track the page data in internals
- trackPageData(internals, component, pageData, astroModuleId, astroModuleURL);
+ // Track the page data in internals
+ trackPageData(internals, component, pageData, astroModuleId, astroModuleURL);
- if (!routeIsRedirect(pageData.route)) {
- pageInput.add(astroModuleId);
- }
+ if (!routeIsRedirect(pageData.route)) {
+ pageInput.add(astroModuleId);
}
}
@@ -80,7 +78,8 @@ export async function viteBuild(opts: StaticBuildOptions) {
const ssrTime = performance.now();
opts.logger.info('build', `Building ${settings.config.output} entrypoints...`);
const ssrOutput = await ssrBuild(opts, internals, pageInput, container);
- opts.logger.info('build', dim(`Completed in ${getTimeStat(ssrTime, performance.now())}.`));
+ opts.logger.info('build', green(`✓ Completed in ${getTimeStat(ssrTime, performance.now())}.`));
+
settings.timer.end('SSR build');
settings.timer.start('Client build');
@@ -104,7 +103,9 @@ export async function viteBuild(opts: StaticBuildOptions) {
// Run client build first, so the assets can be fed into the SSR rendered version.
const clientOutput = await clientBuild(opts, internals, clientInput, container);
- await runPostBuildHooks(container, ssrOutput, clientOutput);
+ const ssrOutputs = viteBuildReturnToRollupOutputs(ssrOutput);
+ const clientOutputs = viteBuildReturnToRollupOutputs(clientOutput ?? []);
+ await runPostBuildHooks(container, ssrOutputs, clientOutputs);
settings.timer.end('Client build');
@@ -114,23 +115,38 @@ export async function viteBuild(opts: StaticBuildOptions) {
teardown();
}
- return { internals };
+ // For static builds, the SSR output output won't be needed anymore after page generation.
+ // We keep track of the names here so we only remove these specific files when finished.
+ const ssrOutputChunkNames: string[] = [];
+ for (const output of ssrOutputs) {
+ for (const chunk of output.output) {
+ if (chunk.type === 'chunk') {
+ ssrOutputChunkNames.push(chunk.fileName);
+ }
+ }
+ }
+
+ return { internals, ssrOutputChunkNames };
}
-export async function staticBuild(opts: StaticBuildOptions, internals: BuildInternals) {
+export async function staticBuild(
+ opts: StaticBuildOptions,
+ internals: BuildInternals,
+ ssrOutputChunkNames: string[]
+) {
const { settings } = opts;
switch (true) {
case settings.config.output === 'static': {
settings.timer.start('Static generate');
await generatePages(opts, internals);
- await cleanServerOutput(opts);
+ await cleanServerOutput(opts, ssrOutputChunkNames);
settings.timer.end('Static generate');
return;
}
case isServerLikeOutput(settings.config): {
settings.timer.start('Server generate');
await generatePages(opts, internals);
- await cleanStaticOutput(opts, internals);
+ await cleanStaticOutput(opts, internals, ssrOutputChunkNames);
opts.logger.info(null, `\n${bgMagenta(black(' finalizing server assets '))}\n`);
await ssrMoveAssets(opts);
settings.timer.end('Server generate');
@@ -149,17 +165,14 @@ async function ssrBuild(
const { allPages, settings, viteConfig } = opts;
const ssr = isServerLikeOutput(settings.config);
const out = getOutputDirectory(settings.config);
- const routes = Object.values(allPages)
- .flat()
- .map((pageData) => pageData.route);
+ const routes = Object.values(allPages).flatMap((pageData) => pageData.route);
const isContentCache = !ssr && settings.config.experimental.contentCollectionCache;
const { lastVitePlugins, vitePlugins } = await container.runBeforeHook('server', input);
const viteBuildConfig: vite.InlineConfig = {
...viteConfig,
mode: viteConfig.mode || 'production',
- // Check using `settings...` as `viteConfig` always defaults to `warn` by Astro
- logLevel: settings.config.vite.logLevel ?? 'error',
+ logLevel: viteConfig.logLevel ?? 'error',
build: {
target: 'esnext',
// Vite defaults cssMinify to false in SSR by default, but we want to minify it
@@ -272,7 +285,6 @@ async function clientBuild(
container: AstroBuildPluginContainer
) {
const { settings, viteConfig } = opts;
- const timer = performance.now();
const ssr = isServerLikeOutput(settings.config);
const out = ssr ? settings.config.build.client : getOutDirWithinCwd(settings.config.outDir);
@@ -287,13 +299,11 @@ async function clientBuild(
}
const { lastVitePlugins, vitePlugins } = await container.runBeforeHook('client', input);
- opts.logger.info(null, `\n${bgGreen(black(' building client '))}`);
+ opts.logger.info('SKIP_FORMAT', `\n${bgGreen(black(' building client (vite) '))}`);
const viteBuildConfig: vite.InlineConfig = {
...viteConfig,
mode: viteConfig.mode || 'production',
- // Check using `settings...` as `viteConfig` always defaults to `warn` by Astro
- logLevel: settings.config.vite.logLevel ?? 'info',
build: {
target: 'esnext',
...viteConfig.build,
@@ -326,16 +336,15 @@ async function clientBuild(
});
const buildResult = await vite.build(viteBuildConfig);
- opts.logger.info(null, dim(`Completed in ${getTimeStat(timer, performance.now())}.\n`));
return buildResult;
}
async function runPostBuildHooks(
container: AstroBuildPluginContainer,
- ssrReturn: Awaited>,
- clientReturn: Awaited>
+ ssrOutputs: vite.Rollup.RollupOutput[],
+ clientOutputs: vite.Rollup.RollupOutput[]
) {
- const mutations = await container.runPostHook(ssrReturn, clientReturn);
+ const mutations = await container.runPostHook(ssrOutputs, clientOutputs);
const config = container.options.settings.config;
const build = container.options.settings.config.build;
for (const [fileName, mutation] of mutations) {
@@ -343,7 +352,7 @@ async function runPostBuildHooks(
? mutation.targets.includes('server')
? build.server
: build.client
- : config.outDir;
+ : getOutDirWithinCwd(config.outDir);
const fullPath = path.join(fileURLToPath(root), fileName);
const fileURL = pathToFileURL(fullPath);
await fs.promises.mkdir(new URL('./', fileURL), { recursive: true });
@@ -355,7 +364,11 @@ async function runPostBuildHooks(
* For each statically prerendered page, replace their SSR file with a noop.
* This allows us to run the SSR build only once, but still remove dependencies for statically rendered routes.
*/
-async function cleanStaticOutput(opts: StaticBuildOptions, internals: BuildInternals) {
+async function cleanStaticOutput(
+ opts: StaticBuildOptions,
+ internals: BuildInternals,
+ ssrOutputChunkNames: string[]
+) {
const allStaticFiles = new Set();
for (const pageData of eachPageData(internals)) {
if (pageData.route.prerender) {
@@ -369,10 +382,8 @@ async function cleanStaticOutput(opts: StaticBuildOptions, internals: BuildInter
const out = ssr
? opts.settings.config.build.server
: getOutDirWithinCwd(opts.settings.config.outDir);
- // The SSR output is all .mjs files, the client output is not.
- const files = await glob('**/*.mjs', {
- cwd: fileURLToPath(out),
- });
+ // The SSR output chunks for Astro are all .mjs files
+ const files = ssrOutputChunkNames.filter((f) => f.endsWith('.mjs'));
if (files.length) {
await eslexer.init;
@@ -402,14 +413,10 @@ async function cleanStaticOutput(opts: StaticBuildOptions, internals: BuildInter
}
}
-async function cleanServerOutput(opts: StaticBuildOptions) {
+async function cleanServerOutput(opts: StaticBuildOptions, ssrOutputChunkNames: string[]) {
const out = getOutDirWithinCwd(opts.settings.config.outDir);
- // The SSR output is all .mjs files, the client output is not.
- const files = await glob('**/*.mjs', {
- cwd: fileURLToPath(out),
- // Important! Also cleanup dotfiles like `node_modules/.pnpm/**`
- dot: true,
- });
+ // The SSR output chunks for Astro are all .mjs files
+ const files = ssrOutputChunkNames.filter((f) => f.endsWith('.mjs'));
if (files.length) {
// Remove all the SSR generated .mjs files
await Promise.all(
diff --git a/packages/astro/src/core/build/types.ts b/packages/astro/src/core/build/types.ts
index 00d6ce0461bff..0722a293dcad5 100644
--- a/packages/astro/src/core/build/types.ts
+++ b/packages/astro/src/core/build/types.ts
@@ -30,7 +30,8 @@ export interface PageBuildData {
hoistedScript: { type: 'inline' | 'external'; value: string } | undefined;
styles: Array<{ depth: number; order: number; sheet: StylesheetAsset }>;
}
-export type AllPagesData = Record;
+
+export type AllPagesData = Record;
/** Options for the static build */
export interface StaticBuildOptions {
@@ -53,7 +54,7 @@ export interface SinglePageBuiltModule {
/**
* The `onRequest` hook exported by the middleware
*/
- onRequest?: MiddlewareHandler;
+ onRequest?: MiddlewareHandler;
renderers: SSRLoadedRenderer[];
}
diff --git a/packages/astro/src/core/build/util.ts b/packages/astro/src/core/build/util.ts
index e46a0713a0b42..fde296a6d246c 100644
--- a/packages/astro/src/core/build/util.ts
+++ b/packages/astro/src/core/build/util.ts
@@ -1,8 +1,10 @@
+import type { Rollup } from 'vite';
import type { AstroConfig } from '../../@types/astro.js';
+import type { ViteBuildReturn } from './types.js';
export function getTimeStat(timeStart: number, timeEnd: number) {
const buildTime = timeEnd - timeStart;
- return buildTime < 750 ? `${Math.round(buildTime)}ms` : `${(buildTime / 1000).toFixed(2)}s`;
+ return buildTime < 1000 ? `${Math.round(buildTime)}ms` : `${(buildTime / 1000).toFixed(2)}s`;
}
/**
@@ -29,9 +31,9 @@ export function shouldAppendForwardSlash(
}
export function i18nHasFallback(config: AstroConfig): boolean {
- if (config.experimental.i18n && config.experimental.i18n.fallback) {
+ if (config.i18n && config.i18n.fallback) {
// we have some fallback and the control is not none
- return Object.keys(config.experimental.i18n.fallback).length > 0;
+ return Object.keys(config.i18n.fallback).length > 0;
}
return false;
@@ -52,3 +54,15 @@ export function encodeName(name: string): string {
return name;
}
+
+export function viteBuildReturnToRollupOutputs(
+ viteBuildReturn: ViteBuildReturn
+): Rollup.RollupOutput[] {
+ const result: Rollup.RollupOutput[] = [];
+ if (Array.isArray(viteBuildReturn)) {
+ result.push(...viteBuildReturn);
+ } else if ('output' in viteBuildReturn) {
+ result.push(viteBuildReturn);
+ }
+ return result;
+}
diff --git a/packages/astro/src/core/compile/compile.ts b/packages/astro/src/core/compile/compile.ts
index f270e123eba94..97625f021a551 100644
--- a/packages/astro/src/core/compile/compile.ts
+++ b/packages/astro/src/core/compile/compile.ts
@@ -5,14 +5,17 @@ import type { AstroConfig } from '../../@types/astro.js';
import { transform } from '@astrojs/compiler';
import { fileURLToPath } from 'node:url';
import { normalizePath } from 'vite';
-import { AggregateError, AstroError, CompilerError } from '../errors/errors.js';
+import type { AstroError } from '../errors/errors.js';
+import { AggregateError, CompilerError } from '../errors/errors.js';
import { AstroErrorData } from '../errors/index.js';
import { resolvePath } from '../util.js';
import { createStylePreprocessor } from './style.js';
+import type { AstroPreferences } from '../../preferences/index.js';
export interface CompileProps {
astroConfig: AstroConfig;
viteConfig: ResolvedConfig;
+ preferences: AstroPreferences;
filename: string;
source: string;
}
@@ -25,6 +28,7 @@ export interface CompileResult extends TransformResult {
export async function compile({
astroConfig,
viteConfig,
+ preferences,
filename,
source,
}: CompileProps): Promise {
@@ -46,7 +50,11 @@ export async function compile({
scopedStyleStrategy: astroConfig.scopedStyleStrategy,
resultScopedSlot: true,
transitionsAnimationURL: 'astro/components/viewtransitions.css',
- annotateSourceFile: !viteConfig.isProduction && astroConfig.experimental.devOverlay,
+ annotateSourceFile:
+ viteConfig.command === 'serve' &&
+ astroConfig.devToolbar &&
+ astroConfig.devToolbar.enabled &&
+ (await preferences.get('devToolbar.enabled')),
preprocessStyle: createStylePreprocessor({
filename,
viteConfig,
diff --git a/packages/astro/src/core/config/config.ts b/packages/astro/src/core/config/config.ts
index 53acb6924fcbc..82bb872b15939 100644
--- a/packages/astro/src/core/config/config.ts
+++ b/packages/astro/src/core/config/config.ts
@@ -67,7 +67,6 @@ export function resolveFlags(flags: Partial): CLIFlags {
config: typeof flags.config === 'string' ? flags.config : undefined,
host:
typeof flags.host === 'string' || typeof flags.host === 'boolean' ? flags.host : undefined,
- drafts: typeof flags.drafts === 'boolean' ? flags.drafts : undefined,
};
}
diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts
index eaa11786a5c9a..08910720a13ee 100644
--- a/packages/astro/src/core/config/schema.ts
+++ b/packages/astro/src/core/config/schema.ts
@@ -8,10 +8,9 @@ import { markdownConfigDefaults } from '@astrojs/markdown-remark';
import { bundledThemes, type BuiltinTheme } from 'shikiji';
import type { AstroUserConfig, ViteUserConfig } from '../../@types/astro.js';
-import fs from 'node:fs';
import type { OutgoingHttpHeaders } from 'node:http';
import path from 'node:path';
-import { fileURLToPath, pathToFileURL } from 'node:url';
+import { pathToFileURL } from 'node:url';
import { z } from 'zod';
import { appendForwardSlash, prependForwardSlash, removeTrailingForwardSlash } from '../path.js';
@@ -38,12 +37,13 @@ const ASTRO_CONFIG_DEFAULTS = {
serverEntry: 'entry.mjs',
redirects: true,
inlineStylesheets: 'auto',
- split: false,
- excludeMiddleware: false,
},
image: {
service: { entrypoint: 'astro/assets/services/sharp', config: {} },
},
+ devToolbar: {
+ enabled: true,
+ },
compressHTML: true,
server: {
host: false,
@@ -51,20 +51,18 @@ const ASTRO_CONFIG_DEFAULTS = {
open: false,
},
integrations: [],
- markdown: {
- drafts: false,
- ...markdownConfigDefaults,
- },
+ markdown: markdownConfigDefaults,
vite: {},
legacy: {},
redirects: {},
experimental: {
optimizeHoistedScript: false,
- devOverlay: false,
contentCollectionCache: false,
},
} satisfies AstroUserConfig & { server: { open: boolean } };
+type RoutingStrategies = 'prefix-always' | 'prefix-other-locales';
+
export const AstroConfigSchema = z.object({
root: z
.string()
@@ -139,20 +137,6 @@ export const AstroConfigSchema = z.object({
.enum(['always', 'auto', 'never'])
.optional()
.default(ASTRO_CONFIG_DEFAULTS.build.inlineStylesheets),
-
- /**
- * @deprecated
- * Use the adapter feature instead
- */
- split: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.build.split),
- /**
- * @deprecated
- * Use the adapter feature instead
- */
- excludeMiddleware: z
- .boolean()
- .optional()
- .default(ASTRO_CONFIG_DEFAULTS.build.excludeMiddleware),
})
.default({}),
server: z.preprocess(
@@ -243,9 +227,13 @@ export const AstroConfigSchema = z.object({
.default([]),
})
.default(ASTRO_CONFIG_DEFAULTS.image),
+ devToolbar: z
+ .object({
+ enabled: z.boolean().default(ASTRO_CONFIG_DEFAULTS.devToolbar.enabled),
+ })
+ .default(ASTRO_CONFIG_DEFAULTS.devToolbar),
markdown: z
.object({
- drafts: z.boolean().default(false),
syntaxHighlight: z
.union([z.literal('shiki'), z.literal('prism'), z.literal(false)])
.default(ASTRO_CONFIG_DEFAULTS.markdown.syntaxHighlight),
@@ -258,25 +246,6 @@ export const AstroConfigSchema = z.object({
for (const lang of langs) {
// shiki -> shikiji compat
if (typeof lang === 'object') {
- // shikiji does not support `path`
- // https://github.com/shikijs/shiki/blob/facb6ff37996129626f8066a5dccb4608e45f649/packages/shiki/src/loader.ts#L98
- const langPath = (lang as any).path;
- if (langPath) {
- // shiki resolves path from within its package directory :shrug:
- const astroRoot = fileURLToPath(new URL('../../../', import.meta.url));
- const normalizedPath = path.isAbsolute(langPath)
- ? langPath
- : path.resolve(astroRoot, langPath);
- try {
- const content = fs.readFileSync(normalizedPath, 'utf-8');
- const parsed = JSON.parse(content);
- Object.assign(lang, parsed);
- } catch (e) {
- throw new Error(`Unable to find language file at ${normalizedPath}`, {
- cause: e,
- });
- }
- }
// `id` renamed to `name` (always override)
if ((lang as any).id) {
lang.name = (lang as any).id;
@@ -333,62 +302,90 @@ export const AstroConfigSchema = z.object({
vite: z
.custom((data) => data instanceof Object && !Array.isArray(data))
.default(ASTRO_CONFIG_DEFAULTS.vite),
- experimental: z
- .object({
- optimizeHoistedScript: z
- .boolean()
- .optional()
- .default(ASTRO_CONFIG_DEFAULTS.experimental.optimizeHoistedScript),
- devOverlay: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.devOverlay),
- i18n: z.optional(
- z
+ i18n: z.optional(
+ z
+ .object({
+ defaultLocale: z.string(),
+ locales: z.array(
+ z.union([
+ z.string(),
+ z.object({
+ path: z.string(),
+ codes: z.string().array().nonempty(),
+ }),
+ ])
+ ),
+ fallback: z.record(z.string(), z.string()).optional(),
+ routing: z
.object({
- defaultLocale: z.string(),
- locales: z.string().array(),
- fallback: z.record(z.string(), z.string()).optional(),
- // TODO: properly add default when the feature goes of experimental
- routingStrategy: z
- .enum(['prefix-always', 'prefix-other-locales'])
- .optional()
- .default('prefix-other-locales'),
+ prefixDefaultLocale: z.boolean().default(false),
+ strategy: z.enum(['pathname']).default('pathname'),
})
- .optional()
- .superRefine((i18n, ctx) => {
- if (i18n) {
- const { defaultLocale, locales, fallback } = i18n;
- if (!locales.includes(defaultLocale)) {
+ .default({})
+ .transform((routing) => {
+ let strategy: RoutingStrategies;
+ switch (routing.strategy) {
+ case 'pathname': {
+ if (routing.prefixDefaultLocale === true) {
+ strategy = 'prefix-always';
+ } else {
+ strategy = 'prefix-other-locales';
+ }
+ }
+ }
+ return strategy;
+ }),
+ })
+ .optional()
+ .superRefine((i18n, ctx) => {
+ if (i18n) {
+ const { defaultLocale, locales: _locales, fallback } = i18n;
+ const locales = _locales.map((locale) => {
+ if (typeof locale === 'string') {
+ return locale;
+ } else {
+ return locale.path;
+ }
+ });
+ if (!locales.includes(defaultLocale)) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: `The default locale \`${defaultLocale}\` is not present in the \`i18n.locales\` array.`,
+ });
+ }
+ if (fallback) {
+ for (const [fallbackFrom, fallbackTo] of Object.entries(fallback)) {
+ if (!locales.includes(fallbackFrom)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
- message: `The default locale \`${defaultLocale}\` is not present in the \`i18n.locales\` array.`,
+ message: `The locale \`${fallbackFrom}\` key in the \`i18n.fallback\` record doesn't exist in the \`i18n.locales\` array.`,
});
}
- if (fallback) {
- for (const [fallbackFrom, fallbackTo] of Object.entries(fallback)) {
- if (!locales.includes(fallbackFrom)) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: `The locale \`${fallbackFrom}\` key in the \`i18n.fallback\` record doesn't exist in the \`i18n.locales\` array.`,
- });
- }
- if (fallbackFrom === defaultLocale) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: `You can't use the default locale as a key. The default locale can only be used as value.`,
- });
- }
+ if (fallbackFrom === defaultLocale) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: `You can't use the default locale as a key. The default locale can only be used as value.`,
+ });
+ }
- if (!locales.includes(fallbackTo)) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: `The locale \`${fallbackTo}\` value in the \`i18n.fallback\` record doesn't exist in the \`i18n.locales\` array.`,
- });
- }
- }
+ if (!locales.includes(fallbackTo)) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: `The locale \`${fallbackTo}\` value in the \`i18n.fallback\` record doesn't exist in the \`i18n.locales\` array.`,
+ });
}
}
- })
- ),
+ }
+ }
+ })
+ ),
+ experimental: z
+ .object({
+ optimizeHoistedScript: z
+ .boolean()
+ .optional()
+ .default(ASTRO_CONFIG_DEFAULTS.experimental.optimizeHoistedScript),
contentCollectionCache: z
.boolean()
.optional()
@@ -452,12 +449,6 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: string) {
.enum(['always', 'auto', 'never'])
.optional()
.default(ASTRO_CONFIG_DEFAULTS.build.inlineStylesheets),
-
- split: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.build.split),
- excludeMiddleware: z
- .boolean()
- .optional()
- .default(ASTRO_CONFIG_DEFAULTS.build.excludeMiddleware),
})
.optional()
.default({}),
diff --git a/packages/astro/src/core/config/settings.ts b/packages/astro/src/core/config/settings.ts
index fca392c976723..ef92d6e7d9be7 100644
--- a/packages/astro/src/core/config/settings.ts
+++ b/packages/astro/src/core/config/settings.ts
@@ -3,6 +3,7 @@ import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { AstroConfig, AstroSettings } from '../../@types/astro.js';
import { getContentPaths } from '../../content/index.js';
+import createPreferences from '../../preferences/index.js';
import { markdownContentEntryType } from '../../vite-plugin-markdown/content-entry-type.js';
import { getDefaultClientDirectives } from '../client-directive/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
@@ -13,8 +14,10 @@ import { loadTSConfig } from './tsconfig.js';
export function createBaseSettings(config: AstroConfig): AstroSettings {
const { contentDir } = getContentPaths(config);
+ const preferences = createPreferences(config);
return {
config,
+ preferences,
tsConfig: undefined,
tsConfigPath: undefined,
adapter: undefined,
@@ -99,7 +102,7 @@ export function createBaseSettings(config: AstroConfig): AstroSettings {
clientDirectives: getDefaultClientDirectives(),
middlewares: { pre: [], post: [] },
watchFiles: [],
- devOverlayPlugins: [],
+ devToolbarApps: [],
timer: new AstroTimer(),
};
}
diff --git a/packages/astro/src/core/config/tsconfig.ts b/packages/astro/src/core/config/tsconfig.ts
index db4b1392ff2aa..3375be9c8b878 100644
--- a/packages/astro/src/core/config/tsconfig.ts
+++ b/packages/astro/src/core/config/tsconfig.ts
@@ -162,14 +162,14 @@ export type StripEnums> = {
[K in keyof T]: T[K] extends boolean
? T[K]
: T[K] extends string
- ? T[K]
- : T[K] extends object
- ? T[K]
- : T[K] extends Array
- ? T[K]
- : T[K] extends undefined
- ? undefined
- : any;
+ ? T[K]
+ : T[K] extends object
+ ? T[K]
+ : T[K] extends Array
+ ? T[K]
+ : T[K] extends undefined
+ ? undefined
+ : any;
};
export interface TSConfig {
diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts
index de644372996c7..a3edff3b917cb 100644
--- a/packages/astro/src/core/create-vite.ts
+++ b/packages/astro/src/core/create-vite.ts
@@ -1,10 +1,8 @@
-import type { AstroSettings } from '../@types/astro.js';
-import type { Logger } from './logger/core.js';
-
import nodeFs from 'node:fs';
import { fileURLToPath } from 'node:url';
import * as vite from 'vite';
import { crawlFrameworkPkgs } from 'vitefu';
+import type { AstroSettings } from '../@types/astro.js';
import astroAssetsPlugin from '../assets/vite-plugin-assets.js';
import {
astroContentAssetPropagationPlugin,
@@ -31,8 +29,11 @@ import astroScannerPlugin from '../vite-plugin-scanner/index.js';
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
import { vitePluginSSRManifest } from '../vite-plugin-ssr-manifest/index.js';
+import type { Logger } from './logger/core.js';
+import { createViteLogger } from './logger/vite.js';
import { vitePluginMiddleware } from './middleware/vite-plugin.js';
import { joinPaths } from './path.js';
+import vitePluginFileURL from '../vite-plugin-fileurl/index.js';
interface CreateViteOptions {
settings: AstroSettings;
@@ -108,7 +109,7 @@ export async function createVite(
configFile: false,
cacheDir: fileURLToPath(new URL('./node_modules/.vite/', settings.config.root)), // using local caches allows Astro to be used in monorepos, etc.
clearScreen: false, // we want to control the output, not Vite
- logLevel: 'warn', // log warnings and errors only
+ customLogger: createViteLogger(logger, settings.config.vite.logLevel),
appType: 'custom',
optimizeDeps: {
entries: ['src/**/*'],
@@ -141,7 +142,8 @@ export async function createVite(
astroPrefetch({ settings }),
astroTransitions({ settings }),
astroDevOverlay({ settings, logger }),
- !!settings.config.experimental.i18n && astroInternationalization({ settings }),
+ vitePluginFileURL({}),
+ !!settings.config.i18n && astroInternationalization({ settings }),
],
publicDir: fileURLToPath(settings.config.publicDir),
root: fileURLToPath(settings.config.root),
@@ -180,7 +182,7 @@ export async function createVite(
},
{
find: 'astro:middleware',
- replacement: 'astro/middleware/namespace',
+ replacement: 'astro/virtual-modules/middleware.js',
},
{
find: 'astro:components',
diff --git a/packages/astro/src/core/dev/dev.ts b/packages/astro/src/core/dev/dev.ts
index 02ba9d872ffcf..c0c1b9b8ea642 100644
--- a/packages/astro/src/core/dev/dev.ts
+++ b/packages/astro/src/core/dev/dev.ts
@@ -1,8 +1,9 @@
+import { green } from 'kleur/colors';
+import type * as vite from 'vite';
import fs from 'node:fs';
import type http from 'node:http';
import type { AddressInfo } from 'node:net';
import { performance } from 'perf_hooks';
-import type * as vite from 'vite';
import type { AstroInlineConfig } from '../../@types/astro.js';
import { attachContentServerListeners } from '../../content/index.js';
import { telemetry } from '../../events/index.js';
@@ -33,9 +34,8 @@ export default async function dev(inlineConfig: AstroInlineConfig): Promise 0) {
@@ -71,7 +74,10 @@ export async function restartContainer(container: Container): Promise handleServerRestart('Restarting...');
+ restart.container.viteServer.restart = () => handleServerRestart();
}
addWatches();
return restart;
diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts
index 80af2358d13be..c04c9b2b50212 100644
--- a/packages/astro/src/core/endpoint/index.ts
+++ b/packages/astro/src/core/endpoint/index.ts
@@ -1,9 +1,7 @@
-import mime from 'mime';
import type {
APIContext,
EndpointHandler,
- EndpointOutput,
- MiddlewareEndpointHandler,
+ Locales,
MiddlewareHandler,
Params,
} from '../../@types/astro.js';
@@ -19,8 +17,6 @@ import {
} from '../render/context.js';
import { type Environment, type RenderContext } from '../render/index.js';
-const encoder = new TextEncoder();
-
const clientAddressSymbol = Symbol.for('astro.clientAddress');
const clientLocalsSymbol = Symbol.for('astro.locals');
@@ -30,7 +26,7 @@ type CreateAPIContext = {
site?: string;
props: Record;
adapterName?: string;
- locales: string[] | undefined;
+ locales: Locales | undefined;
routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined;
defaultLocale: string | undefined;
};
@@ -69,7 +65,6 @@ export function createAPIContext({
},
});
},
- ResponseWithEncoding,
get preferredLocale(): string | undefined {
if (preferredLocale) {
return preferredLocale;
@@ -143,36 +138,11 @@ export function createAPIContext({
return context;
}
-type ResponseParameters = ConstructorParameters;
-
-export class ResponseWithEncoding extends Response {
- constructor(body: ResponseParameters[0], init: ResponseParameters[1], encoding?: BufferEncoding) {
- // If a body string is given, try to encode it to preserve the behaviour as simple objects.
- // We don't do the full handling as simple objects so users can control how headers are set instead.
- if (typeof body === 'string') {
- // In NodeJS, we can use Buffer.from which supports all BufferEncoding
- if (typeof Buffer !== 'undefined' && Buffer.from) {
- body = Buffer.from(body, encoding);
- }
- // In non-NodeJS, use the web-standard TextEncoder for utf-8 strings
- else if (encoding == null || encoding === 'utf8' || encoding === 'utf-8') {
- body = encoder.encode(body);
- }
- }
-
- super(body, init);
-
- if (encoding) {
- this.headers.set('X-Astro-Encoding', encoding);
- }
- }
-}
-
-export async function callEndpoint(
+export async function callEndpoint(
mod: EndpointHandler,
env: Environment,
ctx: RenderContext,
- onRequest: MiddlewareHandler | undefined
+ onRequest: MiddlewareHandler | undefined
): Promise {
const context = createAPIContext({
request: ctx.request,
@@ -180,114 +150,20 @@ export async function callEndpoint
props: ctx.props,
site: env.site,
adapterName: env.adapterName,
- routingStrategy: ctx.routingStrategy,
+ routingStrategy: ctx.routing,
defaultLocale: ctx.defaultLocale,
locales: ctx.locales,
});
let response;
if (onRequest) {
- response = await callMiddleware(
- env.logger,
- onRequest as MiddlewareEndpointHandler,
- context,
- async () => {
- return await renderEndpoint(mod, context, env.ssr, env.logger);
- }
- );
+ response = await callMiddleware(onRequest, context, async () => {
+ return await renderEndpoint(mod, context, env.ssr, env.logger);
+ });
} else {
response = await renderEndpoint(mod, context, env.ssr, env.logger);
}
- const isEndpointSSR = env.ssr && !ctx.route?.prerender;
-
- if (response instanceof Response) {
- if (isEndpointSSR && response.headers.get('X-Astro-Encoding')) {
- env.logger.warn(
- 'ssr',
- '`ResponseWithEncoding` is ignored in SSR. Please return an instance of Response. See https://docs.astro.build/en/core-concepts/endpoints/#server-endpoints-api-routes for more information.'
- );
- }
- attachCookiesToResponse(response, context.cookies);
- return response;
- }
-
- // The endpoint returned a simple object, convert it to a Response
-
- // TODO: Remove in Astro 4.0
- env.logger.warn(
- 'astro',
- `${ctx.route.component} returns a simple object which is deprecated. Please return an instance of Response. See https://docs.astro.build/en/core-concepts/endpoints/#server-endpoints-api-routes for more information.`
- );
-
- if (isEndpointSSR) {
- if (response.hasOwnProperty('headers')) {
- env.logger.warn(
- 'ssr',
- 'Setting headers is not supported when returning an object. Please return an instance of Response. See https://docs.astro.build/en/core-concepts/endpoints/#server-endpoints-api-routes for more information.'
- );
- }
-
- if (response.encoding) {
- env.logger.warn(
- 'ssr',
- '`encoding` is ignored in SSR. To return a charset other than UTF-8, please return an instance of Response. See https://docs.astro.build/en/core-concepts/endpoints/#server-endpoints-api-routes for more information.'
- );
- }
- }
-
- let body: BodyInit;
- const headers = new Headers();
-
- // Try to get the MIME type for this route
- const pathname = ctx.route
- ? // Try the static route `pathname`
- ctx.route.pathname ??
- // Dynamic routes don't include `pathname`, so synthesize a path for these (e.g. 'src/pages/[slug].svg')
- ctx.route.segments.map((s) => s.map((p) => p.content).join('')).join('/')
- : // Fallback to pathname of the request
- ctx.pathname;
- const mimeType = mime.getType(pathname) || 'text/plain';
- headers.set('Content-Type', `${mimeType};charset=utf-8`);
-
- // Save encoding to X-Astro-Encoding to be used later during SSG with `fs.writeFile`.
- // It won't work in SSR and is already warned above.
- if (response.encoding) {
- headers.set('X-Astro-Encoding', response.encoding);
- }
-
- // For Uint8Array (binary), it can passed to Response directly
- if (response.body instanceof Uint8Array) {
- body = response.body;
- headers.set('Content-Length', body.byteLength.toString());
- }
- // In NodeJS, we can use Buffer.from which supports all BufferEncoding
- else if (typeof Buffer !== 'undefined' && Buffer.from) {
- body = Buffer.from(response.body, response.encoding);
- headers.set('Content-Length', body.byteLength.toString());
- }
- // In non-NodeJS, use the web-standard TextEncoder for utf-8 strings only
- // to calculate the content length
- else if (
- response.encoding == null ||
- response.encoding === 'utf8' ||
- response.encoding === 'utf-8'
- ) {
- body = encoder.encode(response.body);
- headers.set('Content-Length', body.byteLength.toString());
- }
- // Fallback pass it to Response directly. It will mainly rely on X-Astro-Encoding
- // to be further processed in SSG.
- else {
- body = response.body;
- // NOTE: Can't calculate the content length as we can't encode to figure out the real length.
- // But also because we don't need the length for SSG as it's only being written to disk.
- }
-
- response = new Response(body, {
- status: 200,
- headers,
- });
attachCookiesToResponse(response, context.cookies);
return response;
}
diff --git a/packages/astro/src/core/errors/dev/vite.ts b/packages/astro/src/core/errors/dev/vite.ts
index b3e49234d6033..7d67806f8fa4f 100644
--- a/packages/astro/src/core/errors/dev/vite.ts
+++ b/packages/astro/src/core/errors/dev/vite.ts
@@ -83,7 +83,6 @@ export function enhanceViteSSRError({
if (globPattern) {
safeError.message = InvalidGlob.message(globPattern);
safeError.name = 'InvalidGlob';
- safeError.hint = InvalidGlob.hint;
safeError.title = InvalidGlob.title;
const line = lns.findIndex((ln) => ln.includes(globPattern));
diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts
index ec84888d4cc9c..047bb533721ce 100644
--- a/packages/astro/src/core/errors/errors-data.ts
+++ b/packages/astro/src/core/errors/errors-data.ts
@@ -292,6 +292,7 @@ export const InvalidGetStaticPathsReturn = {
/**
* @docs
+ * @deprecated Deprecated since Astro 4.0. The RSS helper no longer exists with an error fallback.
* @see
* - [RSS Guide](https://docs.astro.build/en/guides/rss/)
* @description
@@ -490,9 +491,10 @@ export const PageNumberParamNotFound = {
*/
export const ImageMissingAlt = {
name: 'ImageMissingAlt',
- title: 'Missing alt property.',
- message: 'The alt property is required.',
- hint: "The `alt` property is important for the purpose of accessibility, without it users using screen readers or other assistive technologies won't be able to understand what your image is supposed to represent. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-alt for more information.",
+ title: 'Image missing required "alt" property.',
+ message:
+ 'Image missing "alt" property. "alt" text is required to describe important images on the page.',
+ hint: 'Use an empty string ("") for decorative images.',
} satisfies ErrorData;
/**
* @docs
@@ -731,8 +733,9 @@ export const ResponseSentError = {
*/
export const MiddlewareNoDataOrNextCalled = {
name: 'MiddlewareNoDataOrNextCalled',
- title: "The middleware didn't return a response or call `next`.",
- message: 'The middleware needs to either return a `Response` object or call the `next` function.',
+ title: "The middleware didn't return a `Response`.",
+ message:
+ 'Make sure your middleware returns a `Response` object, either directly or by returning the `Response` from calling the `next` function.',
} satisfies ErrorData;
/**
@@ -1275,10 +1278,8 @@ export const UnsupportedConfigTransformError = {
export const MissingLocale = {
name: 'MissingLocaleError',
title: 'The provided locale does not exist.',
- message: (locale: string, locales: string[]) => {
- return `The locale \`${locale}\` does not exist in the configured locales. Available locales: ${locales.join(
- ', '
- )}.`;
+ message: (locale: string) => {
+ return `The locale/path \`${locale}\` does not exist in the configured \`i18n.locales\`.`;
},
} satisfies ErrorData;
@@ -1292,3 +1293,12 @@ export const CantRenderPage = {
// Generic catch-all - Only use this in extreme cases, like if there was a cosmic ray bit flip
export const UnknownError = { name: 'UnknownError', title: 'Unknown Error.' } satisfies ErrorData;
+
+export const UnhandledRejection = {
+ name: 'UnhandledRejection',
+ title: 'Unhandled rejection',
+ message: (stack: string) => {
+ return `Astro detected an unhandled rejection. Here's the stack trace:\n${stack}`;
+ },
+ hint: 'Make sure your promises all have an `await` or a `.catch()` handler.',
+};
diff --git a/packages/astro/src/core/errors/utils.ts b/packages/astro/src/core/errors/utils.ts
index 300787dd32032..ef0a10b4f6ded 100644
--- a/packages/astro/src/core/errors/utils.ts
+++ b/packages/astro/src/core/errors/utils.ts
@@ -93,9 +93,8 @@ export function createSafeError(err: any): Error {
} else {
const error = new Error(JSON.stringify(err));
- (
- error as SSRError
- ).hint = `To get as much information as possible from your errors, make sure to throw Error objects instead of \`${typeof err}\`. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error for more information.`;
+ (error as SSRError).hint =
+ `To get as much information as possible from your errors, make sure to throw Error objects instead of \`${typeof err}\`. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error for more information.`;
return error;
}
diff --git a/packages/astro/src/core/logger/console.ts b/packages/astro/src/core/logger/console.ts
index f39f6b74d2332..d55318d4ccac3 100644
--- a/packages/astro/src/core/logger/console.ts
+++ b/packages/astro/src/core/logger/console.ts
@@ -1,10 +1,6 @@
-import { bold, cyan, dim, red, reset, yellow } from 'kleur/colors';
-import type { LogMessage } from './core.js';
-import { dateTimeFormat, levels } from './core.js';
+import { getEventPrefix, levels, type LogMessage, type LogWritable } from './core.js';
-let lastMessage: string;
-let lastMessageCount = 1;
-export const consoleLogDestination = {
+export const consoleLogDestination: LogWritable = {
write(event: LogMessage) {
// eslint-disable-next-line no-console
let dest = console.error;
@@ -12,37 +8,11 @@ export const consoleLogDestination = {
// eslint-disable-next-line no-console
dest = console.log;
}
-
- function getPrefix() {
- let prefix = '';
- let type = event.label;
- if (type) {
- // hide timestamp when type is undefined
- prefix += dim(dateTimeFormat.format(new Date()) + ' ');
- if (event.level === 'info') {
- type = bold(cyan(`[${type}]`));
- } else if (event.level === 'warn') {
- type = bold(yellow(`[${type}]`));
- } else if (event.level === 'error') {
- type = bold(red(`[${type}]`));
- }
-
- prefix += `${type} `;
- }
- return reset(prefix);
- }
-
- let message = event.message;
- // For repeat messages, only update the message counter
- if (message === lastMessage) {
- lastMessageCount++;
- message = `${message} ${yellow(`(x${lastMessageCount})`)}`;
+ if (event.label === 'SKIP_FORMAT') {
+ dest(event.message);
} else {
- lastMessage = message;
- lastMessageCount = 1;
+ dest(getEventPrefix(event) + ' ' + event.message);
}
- const outMessage = getPrefix() + message;
- dest(outMessage);
return true;
},
};
diff --git a/packages/astro/src/core/logger/core.ts b/packages/astro/src/core/logger/core.ts
index 11804dd01ab71..db571d8f14da4 100644
--- a/packages/astro/src/core/logger/core.ts
+++ b/packages/astro/src/core/logger/core.ts
@@ -1,12 +1,36 @@
-import { dim } from 'kleur/colors';
+import { blue, bold, dim, red, yellow } from 'kleur/colors';
import stringWidth from 'string-width';
-interface LogWritable {
+export interface LogWritable {
write: (chunk: T) => boolean;
}
export type LoggerLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; // same as Pino
+/**
+ * Defined logger labels. Add more as needed, but keep them high-level & reusable,
+ * rather than specific to a single command, function, use, etc. The label will be
+ * shown in the log message to the user, so it should be relevant.
+ */
+export type LoggerLabel =
+ | 'add'
+ | 'build'
+ | 'check'
+ | 'config'
+ | 'content'
+ | 'deprecated'
+ | 'markdown'
+ | 'router'
+ | 'types'
+ | 'vite'
+ | 'watch'
+ | 'middleware'
+ | 'preferences'
+ | 'redirects'
+ // SKIP_FORMAT: A special label that tells the logger not to apply any formatting.
+ // Useful for messages that are already formatted, like the server start message.
+ | 'SKIP_FORMAT';
+
export interface LogOptions {
dest: LogWritable;
level: LoggerLevel;
@@ -25,6 +49,7 @@ export const dateTimeFormat = new Intl.DateTimeFormat([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
+ hour12: false,
});
export interface LogMessage {
@@ -52,13 +77,17 @@ export function log(opts: LogOptions, level: LoggerLevel, label: string | null,
};
// test if this level is enabled or not
- if (levels[logLevel] > levels[level]) {
+ if (!isLogLevelEnabled(logLevel, level)) {
return; // do nothing
}
dest.write(event);
}
+export function isLogLevelEnabled(configuredLogLevel: LoggerLevel, level: LoggerLevel) {
+ return levels[configuredLogLevel] <= levels[level];
+}
+
/** Emit a user-facing message. Useful for UI and other console messages. */
export function info(opts: LogOptions, label: string | null, message: string) {
return log(opts, 'info', label, message);
@@ -98,6 +127,35 @@ function padStr(str: string, len: number) {
return str + spaces;
}
+/**
+ * Get the prefix for a log message.
+ * This includes the timestamp, log level, and label all properly formatted
+ * with colors. This is shared across different loggers, so it's defined here.
+ */
+export function getEventPrefix({ level, label }: LogMessage) {
+ const timestamp = `${dateTimeFormat.format(new Date())}`;
+ const prefix = [];
+ if (level === 'error' || level === 'warn') {
+ prefix.push(bold(timestamp));
+ prefix.push(`[${level.toUpperCase()}]`);
+ } else {
+ prefix.push(timestamp);
+ }
+ if (label) {
+ prefix.push(`[${label}]`);
+ }
+ if (level === 'error') {
+ return red(prefix.join(' '));
+ }
+ if (level === 'warn') {
+ return yellow(prefix.join(' '));
+ }
+ if (prefix.length === 1) {
+ return dim(prefix[0]);
+ }
+ return dim(prefix[0]) + ' ' + blue(prefix.splice(1).join(' '));
+}
+
export let defaultLogLevel: LoggerLevel;
if (typeof process !== 'undefined') {
// This could be a shimmed environment so we don't know that `process` is the full
@@ -133,16 +191,16 @@ export class Logger {
this.options = options;
}
- info(label: string | null, message: string) {
+ info(label: LoggerLabel | null, message: string) {
info(this.options, label, message);
}
- warn(label: string | null, message: string) {
+ warn(label: LoggerLabel | null, message: string) {
warn(this.options, label, message);
}
- error(label: string | null, message: string) {
+ error(label: LoggerLabel | null, message: string) {
error(this.options, label, message);
}
- debug(label: string | null, ...messages: any[]) {
+ debug(label: LoggerLabel, ...messages: any[]) {
debug(label, ...messages);
}
diff --git a/packages/astro/src/core/logger/node.ts b/packages/astro/src/core/logger/node.ts
index 57aa59ed02d40..2c75968d2f85f 100644
--- a/packages/astro/src/core/logger/node.ts
+++ b/packages/astro/src/core/logger/node.ts
@@ -1,113 +1,35 @@
import debugPackage from 'debug';
-import { bold, cyan, dim, red, reset, yellow } from 'kleur/colors';
-import * as readline from 'node:readline';
-import { Writable } from 'node:stream';
-import stringWidth from 'string-width';
-import { dateTimeFormat, error, info, warn } from './core.js';
+import type { Writable } from 'node:stream';
+import { getEventPrefix, levels, type LogMessage, type LogWritable } from './core.js';
type ConsoleStream = Writable & {
fd: 1 | 2;
};
-let lastMessage: string;
-let lastMessageCount = 1;
-export const nodeLogDestination = new Writable({
- objectMode: true,
- write(event: LogMessage, _, callback) {
+export const nodeLogDestination: LogWritable = {
+ write(event: LogMessage) {
let dest: ConsoleStream = process.stderr;
if (levels[event.level] < levels['error']) {
dest = process.stdout;
}
-
- function getPrefix() {
- let prefix = '';
- let label = event.label;
- if (label) {
- // hide timestamp when type is undefined
- prefix += dim(dateTimeFormat.format(new Date()) + ' ');
- if (event.level === 'info') {
- label = bold(cyan(`[${label}]`));
- } else if (event.level === 'warn') {
- label = bold(yellow(`[${label}]`));
- } else if (event.level === 'error') {
- label = bold(red(`[${label}]`));
- }
-
- prefix += `${label} `;
- }
- return reset(prefix);
- }
-
- // console.log({msg: event.message, args: event.args});
- let message = event.message;
- // For repeat messages, only update the message counter
- if (message === lastMessage) {
- lastMessageCount++;
- if (levels[event.level] < levels['error']) {
- let lines = 1;
- let len = stringWidth(`${getPrefix()}${message}`);
- let cols = (dest as unknown as typeof process.stdout).columns;
- if (len > cols) {
- lines = Math.ceil(len / cols);
- }
- for (let i = 0; i < lines; i++) {
- readline.clearLine(dest, 0);
- readline.cursorTo(dest, 0);
- readline.moveCursor(dest, 0, -1);
- }
- }
- message = `${message} ${yellow(`(x${lastMessageCount})`)}`;
+ if (event.label === 'SKIP_FORMAT') {
+ dest.write(event.message + '\n');
} else {
- lastMessage = message;
- lastMessageCount = 1;
+ dest.write(getEventPrefix(event) + ' ' + event.message + '\n');
}
-
- dest.write(getPrefix());
- dest.write(message);
- dest.write('\n');
- callback();
+ return true;
},
-});
-
-interface LogWritable {
- write: (chunk: T) => boolean;
-}
-
-export type LoggerLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; // same as Pino
-export type LoggerEvent = 'info' | 'warn' | 'error';
-
-export interface LogOptions {
- dest?: LogWritable;
- level?: LoggerLevel;
-}
-
-export const nodeLogOptions: Required = {
- dest: nodeLogDestination,
- level: 'info',
-};
-
-export interface LogMessage {
- label: string | null;
- level: LoggerLevel;
- message: string;
-}
-
-export const levels: Record = {
- debug: 20,
- info: 30,
- warn: 40,
- error: 50,
- silent: 90,
};
const debuggers: Record = {};
+
/**
* Emit a message only shown in debug mode.
* Astro (along with many of its dependencies) uses the `debug` package for debug logging.
* You can enable these logs with the `DEBUG=astro:*` environment variable.
* More info https://github.com/debug-js/debug#environment-variables
*/
-export function debug(type: string, ...messages: Array) {
+function debug(type: string, ...messages: Array) {
const namespace = `astro:${type}`;
debuggers[namespace] = debuggers[namespace] || debugPackage(namespace);
return debuggers[namespace](...messages);
@@ -116,16 +38,9 @@ export function debug(type: string, ...messages: Array) {
// This is gross, but necessary since we are depending on globals.
(globalThis as any)._astroGlobalDebug = debug;
-// A default logger for when too lazy to pass LogOptions around.
-export const logger = {
- info: info.bind(null, nodeLogOptions),
- warn: warn.bind(null, nodeLogOptions),
- error: error.bind(null, nodeLogOptions),
-};
-
export function enableVerboseLogging() {
- debugPackage.enable('*,-babel');
- debug('cli', '--verbose flag enabled! Enabling: DEBUG="*,-babel"');
+ debugPackage.enable('astro:*,vite:*');
+ debug('cli', '--verbose flag enabled! Enabling: DEBUG="astro:*,vite:*"');
debug(
'cli',
'Tip: Set the DEBUG env variable directly for more control. Example: "DEBUG=astro:*,vite:* astro build".'
diff --git a/packages/astro/src/core/logger/vite.ts b/packages/astro/src/core/logger/vite.ts
new file mode 100644
index 0000000000000..ac48369a36939
--- /dev/null
+++ b/packages/astro/src/core/logger/vite.ts
@@ -0,0 +1,97 @@
+import { fileURLToPath } from 'url';
+import stripAnsi from 'strip-ansi';
+import type { Logger as ViteLogger, Rollup, LogLevel } from 'vite';
+import { isAstroError } from '../errors/errors.js';
+import { isLogLevelEnabled, type Logger as AstroLogger } from './core.js';
+
+const PKG_PREFIX = fileURLToPath(new URL('../../../', import.meta.url));
+const E2E_PREFIX = fileURLToPath(new URL('../../../e2e', import.meta.url));
+export function isAstroSrcFile(id: string | null) {
+ return id?.startsWith(PKG_PREFIX) && !id.startsWith(E2E_PREFIX);
+}
+
+// capture "page reload some/Component.vue (additional info)" messages
+const vitePageReloadMsg = /page reload (.*)( \(.*\))?/;
+// capture "hmr update some/Component.vue" messages
+const viteHmrUpdateMsg = /hmr update (.*)/;
+// capture "vite v5.0.0 building SSR bundle for production..." and "vite v5.0.0 building for production..." messages
+const viteBuildMsg = /vite.*building.*for production/;
+
+export function createViteLogger(
+ astroLogger: AstroLogger,
+ viteLogLevel: LogLevel = 'info'
+): ViteLogger {
+ const warnedMessages = new Set();
+ const loggedErrors = new WeakSet();
+
+ const logger: ViteLogger = {
+ hasWarned: false,
+ info(msg) {
+ if (!isLogLevelEnabled(viteLogLevel, 'info')) return;
+
+ const stripped = stripAnsi(msg);
+ let m;
+ // Rewrite HMR page reload message
+ if ((m = vitePageReloadMsg.exec(stripped))) {
+ if (isAstroSrcFile(m[1])) return;
+ const extra = m[2] ?? '';
+ astroLogger.info('watch', m[1] + extra);
+ }
+ // Rewrite HMR update message
+ else if ((m = viteHmrUpdateMsg.exec(stripped))) {
+ if (isAstroSrcFile(m[1])) return;
+ astroLogger.info('watch', m[1]);
+ }
+ // Don't log Vite build messages
+ else if (viteBuildMsg.test(stripped)) {
+ // noop
+ }
+ // Fallback
+ else {
+ astroLogger.info('vite', msg);
+ }
+ },
+ warn(msg) {
+ if (!isLogLevelEnabled(viteLogLevel, 'warn')) return;
+
+ logger.hasWarned = true;
+ astroLogger.warn('vite', msg);
+ },
+ warnOnce(msg) {
+ if (!isLogLevelEnabled(viteLogLevel, 'warn')) return;
+
+ if (warnedMessages.has(msg)) return;
+ logger.hasWarned = true;
+ astroLogger.warn('vite', msg);
+ warnedMessages.add(msg);
+ },
+ error(msg, opts) {
+ if (!isLogLevelEnabled(viteLogLevel, 'error')) return;
+
+ logger.hasWarned = true;
+
+ const err = opts?.error;
+ if (err) loggedErrors.add(err);
+ // Astro errors are already logged by us, skip logging
+ if (err && isAstroError(err)) return;
+ // SSR module and pre-transform errors are always handled by us,
+ // send to debug logs
+ if (
+ msg.includes('Error when evaluating SSR module') ||
+ msg.includes('Pre-transform error:')
+ ) {
+ astroLogger.debug('vite', msg);
+ return;
+ }
+
+ astroLogger.error('vite', msg);
+ },
+ // Don't allow clear screen
+ clearScreen: () => {},
+ hasErrorLogged(error) {
+ return loggedErrors.has(error);
+ },
+ };
+
+ return logger;
+}
diff --git a/packages/astro/src/core/messages.ts b/packages/astro/src/core/messages.ts
index 758f5e5813c46..b105e985cb052 100644
--- a/packages/astro/src/core/messages.ts
+++ b/packages/astro/src/core/messages.ts
@@ -5,6 +5,7 @@ import {
bgWhite,
bgYellow,
black,
+ blue,
bold,
cyan,
dim,
@@ -22,36 +23,29 @@ import {
CompilerError,
type ErrorWithMetadata,
} from './errors/index.js';
-import { emoji, padMultilineString } from './util.js';
-
-const PREFIX_PADDING = 6;
+import { padMultilineString } from './util.js';
/** Display */
export function req({
url,
+ method,
statusCode,
reqTime,
}: {
url: string;
statusCode: number;
+ method?: string;
reqTime?: number;
}): string {
- let color = dim;
- if (statusCode >= 500) color = red;
- else if (statusCode >= 400) color = yellow;
- else if (statusCode >= 300) color = dim;
- else if (statusCode >= 200) color = green;
- return `${bold(color(`${statusCode}`.padStart(PREFIX_PADDING)))} ${url.padStart(40)} ${
- reqTime ? dim(Math.round(reqTime) + 'ms') : ''
- }`.trim();
-}
-
-export function reload({ file }: { file: string }): string {
- return `${green('reload'.padStart(PREFIX_PADDING))} ${file}`;
-}
-
-export function hmr({ file, style = false }: { file: string; style?: boolean }): string {
- return `${green('update'.padStart(PREFIX_PADDING))} ${file}${style ? ` ${dim('style')}` : ''}`;
+ const color = statusCode >= 500 ? red : statusCode >= 300 ? yellow : blue;
+ return (
+ color(`[${statusCode}]`) +
+ ` ` +
+ (method && method !== 'GET' ? color(method) + ' ' : '') +
+ url +
+ ` ` +
+ (reqTime ? dim(Math.round(reqTime) + 'ms') : '')
+ );
}
/** Display server host and startup time */
@@ -60,13 +54,11 @@ export function serverStart({
resolvedUrls,
host,
base,
- isRestart = false,
}: {
startupTime: number;
resolvedUrls: ResolvedServerUrls;
host: string | boolean;
base: string;
- isRestart?: boolean;
}): string {
// PACKAGE_VERSION is injected at build-time
const version = process.env.PACKAGE_VERSION ?? '0.0.0';
@@ -75,10 +67,10 @@ export function serverStart({
const emptyPrefix = ' '.repeat(11);
const localUrlMessages = resolvedUrls.local.map((url, i) => {
- return `${i === 0 ? localPrefix : emptyPrefix}${bold(cyan(new URL(url).origin + base))}`;
+ return `${i === 0 ? localPrefix : emptyPrefix}${cyan(new URL(url).origin + base)}`;
});
const networkUrlMessages = resolvedUrls.network.map((url, i) => {
- return `${i === 0 ? networkPrefix : emptyPrefix}${bold(cyan(new URL(url).origin + base))}`;
+ return `${i === 0 ? networkPrefix : emptyPrefix}${cyan(new URL(url).origin + base)}`;
});
if (networkUrlMessages.length === 0) {
@@ -91,58 +83,88 @@ export function serverStart({
}
const messages = [
- `${emoji('🚀 ', '')}${bgGreen(black(` astro `))} ${green(`v${version}`)} ${dim(
- `${isRestart ? 're' : ''}started in ${Math.round(startupTime)}ms`
- )}`,
+ '',
+ `${bgGreen(bold(` astro `))} ${green(`v${version}`)} ${dim(`ready in`)} ${Math.round(
+ startupTime
+ )} ${dim('ms')}`,
'',
...localUrlMessages,
...networkUrlMessages,
'',
];
- return messages
- .filter((msg) => typeof msg === 'string')
- .map((msg) => ` ${msg}`)
- .join('\n');
+ return messages.filter((msg) => typeof msg === 'string').join('\n');
}
-export function telemetryNotice(packageManager = 'npm') {
- const headline = `${cyan('◆')} Astro collects completely anonymous usage data.`;
- const why = dim(' This optional program helps shape our roadmap.');
- const disable = dim(` Run \`${packageManager} run astro telemetry disable\` to opt-out.`);
- const details = ` Details: ${underline('https://astro.build/telemetry')}`;
- return [headline, why, disable, details].map((v) => ' ' + v).join('\n');
+export function telemetryNotice() {
+ const headline = blue(`▶ Astro collects anonymous usage data.`);
+ const why = ' This information helps us improve Astro.';
+ const disable = ` Run "astro telemetry disable" to opt-out.`;
+ const details = ` ${cyan(underline('https://astro.build/telemetry'))}`;
+ return [headline, why, disable, details].join('\n');
}
export function telemetryEnabled() {
- return `${green('◉')} Anonymous telemetry is now ${bgGreen(black(' enabled '))}\n ${dim(
- 'Thank you for improving Astro!'
+ return [
+ green('▶ Anonymous telemetry ') + bgGreen(' enabled '),
+ ` Thank you for helping us improve Astro!`,
+ ``,
+ ].join('\n');
+}
+
+export function preferenceEnabled(name: string) {
+ return `${green('◉')} ${name} is now ${bgGreen(black(' enabled '))}\n`;
+}
+
+export function preferenceSet(name: string, value: any) {
+ return `${green('◉')} ${name} has been set to ${bgGreen(black(` ${JSON.stringify(value)} `))}\n`;
+}
+
+export function preferenceGet(name: string, value: any) {
+ return `${green('◉')} ${name} is set to ${bgGreen(black(` ${JSON.stringify(value)} `))}\n`;
+}
+
+export function preferenceDefaultIntro(name: string) {
+ return `${yellow('◯')} ${name} has not been set. It defaults to\n`;
+}
+
+export function preferenceDefault(name: string, value: any) {
+ return `${yellow('◯')} ${name} has not been set. It defaults to ${bgYellow(
+ black(` ${JSON.stringify(value)} `)
)}\n`;
}
+export function preferenceDisabled(name: string) {
+ return `${yellow('◯')} ${name} is now ${bgYellow(black(' disabled '))}\n`;
+}
+
+export function preferenceReset(name: string) {
+ return `${cyan('◆')} ${name} has been ${bgCyan(black(' reset '))}\n`;
+}
+
export function telemetryDisabled() {
- return `${yellow('◯')} Anonymous telemetry is now ${bgYellow(black(' disabled '))}\n ${dim(
- "We won't ever record your usage data."
- )}\n`;
+ return [
+ green('▶ Anonymous telemetry ') + bgGreen(' disabled '),
+ ` Astro is no longer collecting anonymous usage data.`,
+ ``,
+ ].join('\n');
}
export function telemetryReset() {
- return `${cyan('◆')} Anonymous telemetry has been ${bgCyan(black(' reset '))}\n ${dim(
- 'You may be prompted again.'
- )}\n`;
+ return [green('▶ Anonymous telemetry preferences reset.'), ``].join('\n');
}
export function fsStrictWarning() {
- return yellow(
- '⚠️ Serving with vite.server.fs.strict: false. Note that all files on your machine will be accessible to anyone on your network!'
- );
+ const title = yellow('▶ ' + `${bold('vite.server.fs.strict')} has been disabled!`);
+ const subtitle = ` Files on your machine are likely accessible on your network.`;
+ return `${title}\n${subtitle}\n`;
}
export function prerelease({ currentVersion }: { currentVersion: string }) {
- const tag = currentVersion.split('-').slice(1).join('-').replace(/\..*$/, '');
+ const tag = currentVersion.split('-').slice(1).join('-').replace(/\..*$/, '') || 'unknown';
const badge = bgYellow(black(` ${tag} `));
- const headline = yellow(`▶ This is a ${badge} prerelease build`);
- const warning = ` Feedback? ${underline('https://astro.build/issues')}`;
- return [headline, warning, ''].map((msg) => ` ${msg}`).join('\n');
+ const title = yellow('▶ ' + `This is a ${badge} prerelease build!`);
+ const subtitle = ` Report issues here: ${cyan(underline('https://astro.build/issues'))}`;
+ return `${title}\n${subtitle}\n`;
}
export function success(message: string, tip?: string) {
@@ -196,58 +218,81 @@ export function formatConfigErrorMessage(err: ZodError) {
)}`;
}
-export function formatErrorMessage(err: ErrorWithMetadata, args: string[] = []): string {
+// a regex to match the first line of a stack trace
+const STACK_LINE_REGEXP = /^\s+at /g;
+const IRRELEVANT_STACK_REGEXP = /(node_modules|astro[\/\\]dist)/g;
+function formatErrorStackTrace(
+ err: Error | ErrorWithMetadata,
+ showFullStacktrace: boolean
+): string {
+ const stackLines = (err.stack || '').split('\n').filter((line) => STACK_LINE_REGEXP.test(line));
+ // If full details are required, just return the entire stack trace.
+ if (showFullStacktrace) {
+ return stackLines.join('\n');
+ }
+ // Grab every string from the user's codebase, exit when you hit node_modules or astro/dist
+ const irrelevantStackIndex = stackLines.findIndex((line) => IRRELEVANT_STACK_REGEXP.test(line));
+ if (irrelevantStackIndex <= 0) {
+ const errorId = (err as ErrorWithMetadata).id;
+ const errorLoc = (err as ErrorWithMetadata).loc;
+ if (errorId || errorLoc?.file) {
+ const prettyLocation = ` at ${errorId ?? errorLoc?.file}${
+ errorLoc?.line && errorLoc.column ? `:${errorLoc.line}:${errorLoc.column}` : ''
+ }`;
+ return (
+ prettyLocation + '\n [...] See full stack trace in the browser, or rerun with --verbose.'
+ );
+ } else {
+ return stackLines.join('\n');
+ }
+ }
+ // If the error occurred inside of a dependency, grab the entire stack.
+ // Otherwise, only grab the part of the stack that is relevant to the user's codebase.
+ return (
+ stackLines.splice(0, irrelevantStackIndex).join('\n') +
+ '\n [...] See full stack trace in the browser, or rerun with --verbose.'
+ );
+}
+
+export function formatErrorMessage(err: ErrorWithMetadata, showFullStacktrace: boolean): string {
const isOurError = AstroError.is(err) || CompilerError.is(err) || AstroUserError.is(err);
+ let message = '';
+ if (isOurError) {
+ message += red(`[${err.name}]`) + ' ' + renderErrorMarkdown(err.message, 'cli');
+ } else {
+ message += err.message;
+ }
+ const output = [message];
- args.push(
- `${bgRed(black(` error `))}${red(
- padMultilineString(isOurError ? renderErrorMarkdown(err.message, 'cli') : err.message)
- )}`
- );
if (err.hint) {
- args.push(` ${bold('Hint:')}`);
- args.push(
- yellow(padMultilineString(isOurError ? renderErrorMarkdown(err.hint, 'cli') : err.hint, 4))
- );
+ output.push(` ${bold('Hint:')}`);
+ output.push(yellow(padMultilineString(renderErrorMarkdown(err.hint, 'cli'), 4)));
}
+
const docsLink = getDocsForError(err);
if (docsLink) {
- args.push(` ${bold('Error reference:')}`);
- args.push(` ${underline(docsLink)}`);
+ output.push(` ${bold('Error reference:')}`);
+ output.push(` ${cyan(underline(docsLink))}`);
}
- if (err.id || err.loc?.file) {
- args.push(` ${bold('File:')}`);
- args.push(
- red(
- ` ${err.id ?? err.loc?.file}${
- err.loc?.line && err.loc.column ? `:${err.loc.line}:${err.loc.column}` : ''
- }`
- )
- );
- }
- if (err.frame) {
- args.push(` ${bold('Code:')}`);
- args.push(red(padMultilineString(err.frame.trim(), 4)));
- }
- if (args.length === 1 && err.stack) {
- args.push(dim(err.stack));
- } else if (err.stack) {
- args.push(` ${bold('Stacktrace:')}`);
- args.push(dim(err.stack));
- args.push(``);
+
+ if (err.stack) {
+ output.push(` ${bold('Stack trace:')}`);
+ output.push(dim(formatErrorStackTrace(err, showFullStacktrace)));
}
if (err.cause) {
- args.push(` ${bold('Cause:')}`);
+ output.push(` ${bold('Caused by:')}`);
+ let causeMessage = ' ';
if (err.cause instanceof Error) {
- args.push(dim(err.cause.stack ?? err.cause.toString()));
+ causeMessage +=
+ err.cause.message + '\n' + formatErrorStackTrace(err.cause, showFullStacktrace);
} else {
- args.push(JSON.stringify(err.cause));
+ causeMessage += JSON.stringify(err.cause);
}
-
- args.push(``);
+ output.push(dim(causeMessage));
}
- return args.join('\n');
+
+ return output.join('\n');
}
export function printHelp({
diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts
index 40513c152ff2a..4d79cd566364a 100644
--- a/packages/astro/src/core/middleware/callMiddleware.ts
+++ b/packages/astro/src/core/middleware/callMiddleware.ts
@@ -1,13 +1,6 @@
-import { bold } from 'kleur/colors';
-import type {
- APIContext,
- EndpointOutput,
- MiddlewareHandler,
- MiddlewareNext,
-} from '../../@types/astro.js';
+import type { APIContext, 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';
/**
* Utility function that is in charge of calling the middleware.
@@ -43,15 +36,14 @@ import type { Environment } from '../render/index.js';
* @param apiContext The API context
* @param responseFunction A callback function that should return a promise with the response
*/
-export async function callMiddleware(
- logger: Environment['logger'],
- onRequest: MiddlewareHandler,
+export async function callMiddleware(
+ onRequest: MiddlewareHandler,
apiContext: APIContext,
- responseFunction: () => Promise
-): Promise {
+ responseFunction: () => Promise
+): Promise {
let nextCalled = false;
- let responseFunctionPromise: Promise | undefined = undefined;
- const next: MiddlewareNext = async () => {
+ let responseFunctionPromise: Promise | undefined = undefined;
+ const next: MiddlewareNext = async () => {
nextCalled = true;
responseFunctionPromise = responseFunction();
return responseFunctionPromise;
@@ -60,14 +52,6 @@ export async function callMiddleware(
let middlewarePromise = onRequest(apiContext, next);
return await Promise.resolve(middlewarePromise).then(async (value) => {
- if (isEndpointOutput(value)) {
- logger.warn(
- 'middleware',
- 'Using simple endpoints can cause unexpected issues in the chain of middleware functions.' +
- `\nIt's strongly suggested to use full ${bold('Response')} objects.`
- );
- }
-
// first we check if `next` was called
if (nextCalled) {
/**
@@ -83,7 +67,7 @@ export async function callMiddleware(
if (value instanceof Response === false) {
throw new AstroError(AstroErrorData.MiddlewareNotAResponse);
}
- return ensureCookiesAttached(apiContext, value as Response);
+ return ensureCookiesAttached(apiContext, value);
} else {
/**
* Here we handle the case where `next` was called and returned nothing.
@@ -106,7 +90,7 @@ export async function callMiddleware(
throw new AstroError(AstroErrorData.MiddlewareNotAResponse);
} else {
// Middleware did not call resolve and returned a value
- return ensureCookiesAttached(apiContext, value as Response);
+ return ensureCookiesAttached(apiContext, value);
}
});
}
@@ -117,11 +101,3 @@ function ensureCookiesAttached(apiContext: APIContext, response: Response): Resp
}
return response;
}
-
-function isEndpointOutput(endpointResult: any): endpointResult is EndpointOutput {
- return (
- !(endpointResult instanceof Response) &&
- typeof endpointResult === 'object' &&
- typeof endpointResult.body === 'string'
- );
-}
diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts
index c02761351d3cf..ffaafb3e56b2c 100644
--- a/packages/astro/src/core/middleware/index.ts
+++ b/packages/astro/src/core/middleware/index.ts
@@ -1,8 +1,8 @@
-import type { MiddlewareEndpointHandler, Params } from '../../@types/astro.js';
+import type { MiddlewareHandler, Params } from '../../@types/astro.js';
import { createAPIContext } from '../endpoint/index.js';
import { sequence } from './sequence.js';
-function defineMiddleware(fn: MiddlewareEndpointHandler) {
+function defineMiddleware(fn: MiddlewareHandler) {
return fn;
}
diff --git a/packages/astro/src/core/middleware/namespace.ts b/packages/astro/src/core/middleware/namespace.ts
deleted file mode 100644
index 55a84f66620c1..0000000000000
--- a/packages/astro/src/core/middleware/namespace.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { defineMiddleware, sequence } from './index.js';
diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts
index d20641ee31691..9a68963945ecf 100644
--- a/packages/astro/src/core/middleware/sequence.ts
+++ b/packages/astro/src/core/middleware/sequence.ts
@@ -1,4 +1,4 @@
-import type { APIContext, MiddlewareEndpointHandler } from '../../@types/astro.js';
+import type { APIContext, MiddlewareHandler } from '../../@types/astro.js';
import { defineMiddleware } from './index.js';
// From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js
@@ -6,11 +6,11 @@ import { defineMiddleware } from './index.js';
*
* It accepts one or more middleware handlers and makes sure that they are run in sequence.
*/
-export function sequence(...handlers: MiddlewareEndpointHandler[]): MiddlewareEndpointHandler {
+export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler {
const filtered = handlers.filter((h) => !!h);
const length = filtered.length;
if (!length) {
- const handler: MiddlewareEndpointHandler = defineMiddleware((context, next) => {
+ const handler: MiddlewareHandler = defineMiddleware((context, next) => {
return next();
});
return handler;
diff --git a/packages/astro/src/core/middleware/vite-plugin.ts b/packages/astro/src/core/middleware/vite-plugin.ts
index 0447134910f0c..918c7c9526c59 100644
--- a/packages/astro/src/core/middleware/vite-plugin.ts
+++ b/packages/astro/src/core/middleware/vite-plugin.ts
@@ -7,7 +7,7 @@ import type { BuildInternals } from '../build/internal.js';
import type { StaticBuildOptions } from '../build/types.js';
import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../constants.js';
-export const MIDDLEWARE_MODULE_ID = '@astro-middleware';
+export const MIDDLEWARE_MODULE_ID = '\0astro-internal:middleware';
const EMPTY_MIDDLEWARE = '\0empty-middleware';
export function vitePluginMiddleware({ settings }: { settings: AstroSettings }): VitePlugin {
diff --git a/packages/astro/src/core/pipeline.ts b/packages/astro/src/core/pipeline.ts
index 87f833ee5cc4f..67081a47e93f9 100644
--- a/packages/astro/src/core/pipeline.ts
+++ b/packages/astro/src/core/pipeline.ts
@@ -1,10 +1,4 @@
-import type {
- ComponentInstance,
- EndpointHandler,
- MiddlewareEndpointHandler,
- MiddlewareHandler,
- MiddlewareResponseHandler,
-} from '../@types/astro.js';
+import type { ComponentInstance, EndpointHandler, MiddlewareHandler } from '../@types/astro.js';
import { callEndpoint, createAPIContext } from './endpoint/index.js';
import { callMiddleware } from './middleware/callMiddleware.js';
import { renderPage } from './render/core.js';
@@ -28,7 +22,7 @@ export type PipelineHookFunction = (ctx: RenderContext, mod: ComponentInstance |
*/
export class Pipeline {
env: Environment;
- #onRequest?: MiddlewareEndpointHandler;
+ #onRequest?: MiddlewareHandler;
#hooks: PipelineHooks = {
before: [],
};
@@ -60,7 +54,7 @@ export class Pipeline {
/**
* A middleware function that will be called before each request.
*/
- setMiddlewareFunction(onRequest: MiddlewareEndpointHandler) {
+ setMiddlewareFunction(onRequest: MiddlewareHandler) {
this.#onRequest = onRequest;
}
@@ -115,11 +109,11 @@ export class Pipeline {
*
* It throws an error if the page can't be rendered.
*/
- async #tryRenderRoute(
+ async #tryRenderRoute(
renderContext: Readonly,
env: Readonly,
mod: Readonly | undefined,
- onRequest?: MiddlewareHandler
+ onRequest?: MiddlewareHandler
): Promise {
const apiContext = createAPIContext({
request: renderContext.request,
@@ -128,7 +122,7 @@ export class Pipeline {
site: env.site,
adapterName: env.adapterName,
locales: renderContext.locales,
- routingStrategy: renderContext.routingStrategy,
+ routingStrategy: renderContext.routing,
defaultLocale: renderContext.defaultLocale,
});
@@ -137,19 +131,14 @@ export class Pipeline {
case 'fallback':
case 'redirect': {
if (onRequest) {
- return await callMiddleware(
- env.logger,
- onRequest as MiddlewareResponseHandler,
- apiContext,
- () => {
- return renderPage({
- mod,
- renderContext,
- env,
- cookies: apiContext.cookies,
- });
- }
- );
+ return await callMiddleware(onRequest, apiContext, () => {
+ return renderPage({
+ mod,
+ renderContext,
+ env,
+ cookies: apiContext.cookies,
+ });
+ });
} else {
return await renderPage({
mod,
diff --git a/packages/astro/src/core/preview/static-preview-server.ts b/packages/astro/src/core/preview/static-preview-server.ts
index 937ba1c99a1ef..ba692611e9396 100644
--- a/packages/astro/src/core/preview/static-preview-server.ts
+++ b/packages/astro/src/core/preview/static-preview-server.ts
@@ -42,7 +42,7 @@ export default async function createStaticPreviewServer(
});
} catch (err) {
if (err instanceof Error) {
- logger.error('astro', err.stack || err.message);
+ logger.error(null, err.stack || err.message);
}
throw err;
}
@@ -51,7 +51,7 @@ export default async function createStaticPreviewServer(
// Log server start URLs
logger.info(
- null,
+ 'SKIP_FORMAT',
msg.serverStart({
startupTime: performance.now() - startServerTime,
resolvedUrls: previewServer.resolvedUrls ?? { local: [], network: [] },
@@ -72,8 +72,6 @@ export default async function createStaticPreviewServer(
host: getResolvedHostForHttpServer(settings.config.server.host),
port: settings.config.server.port,
closed,
- // In Vite 5, `httpServer` may be a `Http2SecureServer`, but we know we are only starting a HTTP server
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
server: previewServer.httpServer as http.Server,
stop: async () => {
await new Promise((resolve, reject) => {
diff --git a/packages/astro/src/core/preview/vite-plugin-astro-preview.ts b/packages/astro/src/core/preview/vite-plugin-astro-preview.ts
index 7f99792756070..aafd69cb4f257 100644
--- a/packages/astro/src/core/preview/vite-plugin-astro-preview.ts
+++ b/packages/astro/src/core/preview/vite-plugin-astro-preview.ts
@@ -1,13 +1,13 @@
import fs from 'node:fs';
+import type { IncomingMessage, ServerResponse } from 'node:http';
import { fileURLToPath } from 'node:url';
import type { Connect, Plugin } from 'vite';
-import { version } from 'vite';
import type { AstroSettings } from '../../@types/astro.js';
import { notFoundTemplate, subpathNotUsedTemplate } from '../../template/4xx.js';
+import { cleanUrl } from '../../vite-plugin-utils/index.js';
import { stripBase } from './util.js';
const HAS_FILE_EXTENSION_REGEXP = /^.*\.[^\\]+$/;
-const IS_VITE_5 = version.startsWith('5.');
export function vitePluginAstroPreview(settings: AstroSettings): Plugin {
const { base, outDir, trailingSlash } = settings.config;
@@ -24,8 +24,7 @@ export function vitePluginAstroPreview(settings: AstroSettings): Plugin {
return;
}
- const strippedPathname = stripBase(req.url!, base);
- const pathname = new URL(strippedPathname, 'https://a.b').pathname;
+ const pathname = cleanUrl(stripBase(req.url!, base));
const isRoot = pathname === '/';
// Validate trailingSlash
@@ -53,29 +52,49 @@ export function vitePluginAstroPreview(settings: AstroSettings): Plugin {
});
return () => {
- const fourOhFourMiddleware: Connect.NextHandleFunction = (req, res) => {
- const errorPagePath = fileURLToPath(outDir + '/404.html');
- if (fs.existsSync(errorPagePath)) {
- res.statusCode = 404;
- res.setHeader('Content-Type', 'text/html;charset=utf-8');
- res.end(fs.readFileSync(errorPagePath));
- } else {
- const pathname = stripBase(req.url!, base);
- res.statusCode = 404;
- res.end(notFoundTemplate(pathname, 'Not Found'));
- }
- };
+ // NOTE: the `base` is stripped from `req.url` for post middlewares
- // Vite 5 has its own 404 middleware, we replace it with ours instead.
- if (IS_VITE_5) {
- for (const middleware of server.middlewares.stack) {
- // This hardcoded name will not break between Vite versions
- if ((middleware.handle as Connect.HandleFunction).name === 'vite404Middleware') {
- middleware.handle = fourOhFourMiddleware;
+ server.middlewares.use((req, res, next) => {
+ const pathname = cleanUrl(req.url!);
+
+ // Vite doesn't handle /foo/ if /foo.html exists, we handle it anyways
+ if (pathname.endsWith('/')) {
+ const pathnameWithoutSlash = pathname.slice(0, -1);
+ const htmlPath = fileURLToPath(outDir + pathnameWithoutSlash + '.html');
+ if (fs.existsSync(htmlPath)) {
+ req.url = pathnameWithoutSlash + '.html';
+ return next();
}
}
- } else {
- server.middlewares.use(fourOhFourMiddleware);
+ // Vite doesn't handle /foo if /foo/index.html exists, we handle it anyways
+ else {
+ const htmlPath = fileURLToPath(outDir + pathname + '/index.html');
+ if (fs.existsSync(htmlPath)) {
+ req.url = pathname + '/index.html';
+ return next();
+ }
+ }
+
+ next();
+ });
+
+ // Vite has its own 404 middleware, we replace it with ours instead.
+ for (const middleware of server.middlewares.stack) {
+ // This hardcoded name will not break between Vite versions
+ if ((middleware.handle as Connect.HandleFunction).name === 'vite404Middleware') {
+ // Fallback to 404 page if it exists
+ middleware.handle = (req: IncomingMessage, res: ServerResponse) => {
+ const errorPagePath = fileURLToPath(outDir + '/404.html');
+ if (fs.existsSync(errorPagePath)) {
+ res.statusCode = 404;
+ res.setHeader('Content-Type', 'text/html;charset=utf-8');
+ res.end(fs.readFileSync(errorPagePath));
+ } else {
+ res.statusCode = 404;
+ res.end(notFoundTemplate(req.url!, 'Not Found'));
+ }
+ };
+ }
}
};
},
diff --git a/packages/astro/src/core/render/context.ts b/packages/astro/src/core/render/context.ts
index 0f0bf39b04654..459b2b8b48cca 100644
--- a/packages/astro/src/core/render/context.ts
+++ b/packages/astro/src/core/render/context.ts
@@ -1,12 +1,13 @@
import type {
ComponentInstance,
+ Locales,
Params,
Props,
RouteData,
SSRElement,
SSRResult,
} from '../../@types/astro.js';
-import { normalizeTheLocale } from '../../i18n/index.js';
+import { normalizeTheLocale, toCodes } from '../../i18n/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import type { Environment } from './environment.js';
import { getParamsAndProps } from './params-and-props.js';
@@ -28,9 +29,9 @@ export interface RenderContext {
params: Params;
props: Props;
locals?: object;
- locales: string[] | undefined;
+ locales: Locales | undefined;
defaultLocale: string | undefined;
- routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined;
+ routing: 'prefix-always' | 'prefix-other-locales' | undefined;
}
export type CreateRenderContextArgs = Partial<
@@ -62,7 +63,7 @@ export async function createRenderContext(
params,
props,
locales: options.locales,
- routingStrategy: options.routingStrategy,
+ routing: options.routing,
defaultLocale: options.defaultLocale,
};
@@ -143,8 +144,8 @@ export function parseLocale(header: string): BrowserLocale[] {
return result;
}
-function sortAndFilterLocales(browserLocaleList: BrowserLocale[], locales: string[]) {
- const normalizedLocales = locales.map(normalizeTheLocale);
+function sortAndFilterLocales(browserLocaleList: BrowserLocale[], locales: Locales) {
+ const normalizedLocales = toCodes(locales).map(normalizeTheLocale);
return browserLocaleList
.filter((browserLocale) => {
if (browserLocale.locale !== '*') {
@@ -170,18 +171,26 @@ function sortAndFilterLocales(browserLocaleList: BrowserLocale[], locales: strin
* If multiple locales are present in the header, they are sorted by their quality value and the highest is selected as current locale.
*
*/
-export function computePreferredLocale(request: Request, locales: string[]): string | undefined {
+export function computePreferredLocale(request: Request, locales: Locales): string | undefined {
const acceptHeader = request.headers.get('Accept-Language');
let result: string | undefined = undefined;
if (acceptHeader) {
const browserLocaleList = sortAndFilterLocales(parseLocale(acceptHeader), locales);
const firstResult = browserLocaleList.at(0);
- if (firstResult) {
- if (firstResult.locale !== '*') {
- result = locales.find(
- (locale) => normalizeTheLocale(locale) === normalizeTheLocale(firstResult.locale)
- );
+ if (firstResult && firstResult.locale !== '*') {
+ for (const currentLocale of locales) {
+ if (typeof currentLocale === 'string') {
+ if (normalizeTheLocale(currentLocale) === normalizeTheLocale(firstResult.locale)) {
+ result = currentLocale;
+ }
+ } else {
+ for (const currentCode of currentLocale.codes) {
+ if (normalizeTheLocale(currentCode) === normalizeTheLocale(firstResult.locale)) {
+ result = currentLocale.path;
+ }
+ }
+ }
}
}
}
@@ -189,7 +198,7 @@ export function computePreferredLocale(request: Request, locales: string[]): str
return result;
}
-export function computePreferredLocaleList(request: Request, locales: string[]) {
+export function computePreferredLocaleList(request: Request, locales: Locales): string[] {
const acceptHeader = request.headers.get('Accept-Language');
let result: string[] = [];
if (acceptHeader) {
@@ -197,14 +206,28 @@ export function computePreferredLocaleList(request: Request, locales: string[])
// SAFETY: bang operator is safe because checked by the previous condition
if (browserLocaleList.length === 1 && browserLocaleList.at(0)!.locale === '*') {
- return locales;
+ return locales.map((locale) => {
+ if (typeof locale === 'string') {
+ return locale;
+ } else {
+ // SAFETY: codes is never empty
+ return locale.codes.at(0)!;
+ }
+ });
} else if (browserLocaleList.length > 0) {
for (const browserLocale of browserLocaleList) {
- const found = locales.find(
- (l) => normalizeTheLocale(l) === normalizeTheLocale(browserLocale.locale)
- );
- if (found) {
- result.push(found);
+ for (const loopLocale of locales) {
+ if (typeof loopLocale === 'string') {
+ if (normalizeTheLocale(loopLocale) === normalizeTheLocale(browserLocale.locale)) {
+ result.push(loopLocale);
+ }
+ } else {
+ for (const code of loopLocale.codes) {
+ if (code === browserLocale.locale) {
+ result.push(loopLocale.path);
+ }
+ }
+ }
}
}
}
@@ -215,15 +238,21 @@ export function computePreferredLocaleList(request: Request, locales: string[])
export function computeCurrentLocale(
request: Request,
- locales: string[],
+ locales: Locales,
routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined,
defaultLocale: string | undefined
): undefined | string {
const requestUrl = new URL(request.url);
for (const segment of requestUrl.pathname.split('/')) {
for (const locale of locales) {
- if (normalizeTheLocale(locale) === normalizeTheLocale(segment)) {
- return locale;
+ if (typeof locale === 'string') {
+ if (normalizeTheLocale(locale) === normalizeTheLocale(segment)) {
+ return locale;
+ }
+ } else {
+ if (locale.path === segment) {
+ return locale.codes.at(0);
+ }
}
}
}
diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts
index ed9ea7fdbb508..1175f55d7bc3d 100644
--- a/packages/astro/src/core/render/core.ts
+++ b/packages/astro/src/core/render/core.ts
@@ -61,22 +61,14 @@ export async function renderPage({ mod, renderContext, env, cookies }: RenderPag
locals: renderContext.locals ?? {},
locales: renderContext.locales,
defaultLocale: renderContext.defaultLocale,
- routingStrategy: renderContext.routingStrategy,
+ routingStrategy: renderContext.routing,
});
- // TODO: Remove in Astro 4.0
- if (mod.frontmatter && typeof mod.frontmatter === 'object' && 'draft' in mod.frontmatter) {
- env.logger.warn(
- 'astro',
- `The drafts feature is deprecated and used in ${renderContext.route.component}. You should migrate to content collections instead. See https://docs.astro.build/en/guides/content-collections/#filtering-collection-queries for more information.`
- );
- }
-
const response = await runtimeRenderPage(
result,
Component,
renderContext.props,
- null,
+ {},
env.streaming,
renderContext.route
);
diff --git a/packages/astro/src/core/render/index.ts b/packages/astro/src/core/render/index.ts
index 6d466c4117d82..5f3a702a3083b 100644
--- a/packages/astro/src/core/render/index.ts
+++ b/packages/astro/src/core/render/index.ts
@@ -24,5 +24,5 @@ export interface SSROptions {
/**
* Optional middlewares
*/
- middleware?: AstroMiddlewareInstance;
+ middleware?: AstroMiddlewareInstance;
}
diff --git a/packages/astro/src/core/render/params-and-props.ts b/packages/astro/src/core/render/params-and-props.ts
index 0ad5df205f5b1..3532c5f837266 100644
--- a/packages/astro/src/core/render/params-and-props.ts
+++ b/packages/astro/src/core/render/params-and-props.ts
@@ -4,7 +4,8 @@ import type { Logger } from '../logger/core.js';
import { routeIsFallback } from '../redirects/helpers.js';
import { routeIsRedirect } from '../redirects/index.js';
import { getParams } from '../routing/params.js';
-import { RouteCache, callGetStaticPaths, findPathItemByKey } from './route-cache.js';
+import type { RouteCache } from './route-cache.js';
+import { callGetStaticPaths, findPathItemByKey } from './route-cache.js';
interface GetParamsAndPropsOptions {
mod: ComponentInstance | undefined;
diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts
index e9c8302a1ed74..9a745fd5a95fc 100644
--- a/packages/astro/src/core/render/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -1,6 +1,7 @@
import type {
AstroGlobal,
AstroGlobalPartial,
+ Locales,
Params,
SSRElement,
SSRLoadedRenderer,
@@ -50,7 +51,7 @@ export interface CreateResultArgs {
status: number;
locals: App.Locals;
cookies?: AstroCookies;
- locales: string[] | undefined;
+ locales: Locales | undefined;
defaultLocale: string | undefined;
routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined;
}
@@ -100,7 +101,7 @@ class Slots {
const result = this.#result;
if (!Array.isArray(args)) {
this.#logger.warn(
- 'Astro.slots.render',
+ null,
`Expected second parameter to be an array, received a ${typeof args}. If you're trying to pass an array as a single argument and getting unexpected results, make sure you're passing your array as a item of an array. Ex: Astro.slots.render('default', [["Hello", "World"]])`
);
} else if (args.length > 0) {
diff --git a/packages/astro/src/core/render/route-cache.ts b/packages/astro/src/core/render/route-cache.ts
index c318b8c44308e..8a4e821a523cb 100644
--- a/packages/astro/src/core/render/route-cache.ts
+++ b/packages/astro/src/core/render/route-cache.ts
@@ -8,7 +8,6 @@ import type {
RouteData,
RuntimeMode,
} from '../../@types/astro.js';
-import { AstroError, AstroErrorData } from '../errors/index.js';
import type { Logger } from '../logger/core.js';
import { stringifyParams } from '../routing/params.js';
@@ -59,9 +58,6 @@ export async function callGetStaticPaths({
// Q: Why the cast?
// A: So users downstream can have nicer typings, we have to make some sacrifice in our internal typings, which necessitate a cast here
paginate: generatePaginateFunction(route) as PaginateFunction,
- rss() {
- throw new AstroError(AstroErrorData.GetStaticPathsRemovedRSSHelper);
- },
});
validateGetStaticPathsResult(staticPaths, logger, route);
@@ -107,10 +103,7 @@ export class RouteCache {
// Warn here so that an unexpected double-call of getStaticPaths()
// isn't invisible and developer can track down the issue.
if (this.mode === 'production' && this.cache[route.component]?.staticPaths) {
- this.logger.warn(
- 'routeCache',
- `Internal Warning: route cache overwritten. (${route.component})`
- );
+ this.logger.warn(null, `Internal Warning: route cache overwritten. (${route.component})`);
}
this.cache[route.component] = entry;
}
@@ -131,5 +124,5 @@ export function findPathItemByKey(
if (matchedStaticPath) {
return matchedStaticPath;
}
- logger.debug('findPathItemByKey', `Unexpected cache miss looking for ${paramsKey}`);
+ logger.debug('router', `findPathItemByKey() - Unexpected cache miss looking for ${paramsKey}`);
}
diff --git a/packages/astro/src/core/request.ts b/packages/astro/src/core/request.ts
index f478b0a320db9..8bf52d11a1a62 100644
--- a/packages/astro/src/core/request.ts
+++ b/packages/astro/src/core/request.ts
@@ -39,15 +39,6 @@ export function createRequest({
body,
});
- Object.defineProperties(request, {
- params: {
- get() {
- logger.warn('deprecation', `Astro.request.params has been moved to Astro.params`);
- return undefined;
- },
- },
- });
-
if (!ssr) {
// Warn when accessing headers in SSG mode
const _headers = request.headers;
@@ -56,8 +47,8 @@ export function createRequest({
...headersDesc,
get() {
logger.warn(
- 'ssg',
- `Headers are not exposed in static (SSG) output mode. To enable headers: set \`output: "server"\` in your config file.`
+ null,
+ `\`Astro.request.headers\` is not available in "static" output mode. To enable header access: set \`output: "server"\` or \`output: "hybrid"\` in your config file.`
);
return _headers;
},
diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts
index 6a57972e07120..19e6e3006d163 100644
--- a/packages/astro/src/core/routing/manifest/create.ts
+++ b/packages/astro/src/core/routing/manifest/create.ts
@@ -8,6 +8,7 @@ import type {
} from '../../../@types/astro.js';
import type { Logger } from '../../logger/core.js';
+import { bold } from 'kleur/colors';
import { createRequire } from 'module';
import nodeFs from 'node:fs';
import path from 'node:path';
@@ -183,13 +184,13 @@ function comparator(a: Item, b: Item) {
function injectedRouteToItem(
{ config, cwd }: { config: AstroConfig; cwd?: string },
- { pattern, entryPoint }: InjectedRoute
+ { pattern, entrypoint }: InjectedRoute
): Item {
let resolved: string;
try {
- resolved = require.resolve(entryPoint, { paths: [cwd || fileURLToPath(config.root)] });
+ resolved = require.resolve(entrypoint, { paths: [cwd || fileURLToPath(config.root)] });
} catch (e) {
- resolved = fileURLToPath(new URL(entryPoint, config.root));
+ resolved = fileURLToPath(new URL(entrypoint, config.root));
}
const ext = path.extname(pattern);
@@ -234,8 +235,6 @@ export function createRouteManifest(
const localFs = fsMod ?? nodeFs;
const prerender = getPrerenderDefault(settings.config);
- const foundInvalidFileExtensions = new Set();
-
function walk(
fs: typeof nodeFs,
dir: string,
@@ -259,10 +258,12 @@ export function createRouteManifest(
}
// filter out "foo.astro_tmp" files, etc
if (!isDir && !validPageExtensions.has(ext) && !validEndpointExtensions.has(ext)) {
- if (!foundInvalidFileExtensions.has(ext)) {
- foundInvalidFileExtensions.add(ext);
- logger.warn('astro', `Invalid file extension for Pages: ${ext}`);
- }
+ logger.warn(
+ null,
+ `Unsupported file type ${bold(
+ resolved
+ )} found. Prefix filename with an underscore (\`_\`) to ignore.`
+ );
return;
}
@@ -346,6 +347,7 @@ export function createRouteManifest(
generate,
pathname: pathname || undefined,
prerender,
+ fallbackRoutes: [],
});
}
});
@@ -358,8 +360,7 @@ export function createRouteManifest(
walk(localFs, fileURLToPath(pages), [], []);
} else if (settings.injectedRoutes.length === 0) {
const pagesDirRootRelative = pages.href.slice(settings.config.root.href.length);
-
- logger.warn('astro', `Missing pages directory: ${pagesDirRootRelative}`);
+ logger.warn(null, `Missing pages directory: ${pagesDirRootRelative}`);
}
settings.injectedRoutes
@@ -368,12 +369,12 @@ export function createRouteManifest(
comparator(injectedRouteToItem({ config, cwd }, a), injectedRouteToItem({ config, cwd }, b))
)
.reverse() // prepend to the routes array from lowest to highest priority
- .forEach(({ pattern: name, entryPoint, prerender: prerenderInjected }) => {
+ .forEach(({ pattern: name, entrypoint, prerender: prerenderInjected }) => {
let resolved: string;
try {
- resolved = require.resolve(entryPoint, { paths: [cwd || fileURLToPath(config.root)] });
+ resolved = require.resolve(entrypoint, { paths: [cwd || fileURLToPath(config.root)] });
} catch (e) {
- resolved = fileURLToPath(new URL(entryPoint, config.root));
+ resolved = fileURLToPath(new URL(entrypoint, config.root));
}
const component = slash(path.relative(cwd || fileURLToPath(config.root), resolved));
@@ -422,6 +423,7 @@ export function createRouteManifest(
generate,
pathname: pathname || void 0,
prerender: prerenderInjected ?? prerender,
+ fallbackRoutes: [],
});
});
@@ -449,6 +451,21 @@ export function createRouteManifest(
.map(([{ dynamic, content }]) => (dynamic ? `[${content}]` : content))
.join('/')}`.toLowerCase();
+ {
+ let destination: string;
+ if (typeof to === 'string') {
+ destination = to;
+ } else {
+ destination = to.destination;
+ }
+ if (/^https?:\/\//.test(destination)) {
+ logger.warn(
+ 'redirects',
+ `Redirecting to an external URL is not officially supported: ${from} -> ${to}`
+ );
+ }
+ }
+
const routeData: RouteData = {
type: 'redirect',
route,
@@ -461,6 +478,7 @@ export function createRouteManifest(
prerender: false,
redirect: to,
redirectRoute: routes.find((r) => r.route === to),
+ fallbackRoutes: [],
};
const lastSegmentIsDynamic = (r: RouteData) => !!r.segments.at(-1)?.at(-1)?.dynamic;
@@ -487,7 +505,7 @@ export function createRouteManifest(
// Didn't find a good place, insert last
routes.push(routeData);
});
- const i18n = settings.config.experimental.i18n;
+ const i18n = settings.config.i18n;
if (i18n) {
// In this block of code we group routes based on their locale
@@ -495,11 +513,24 @@ export function createRouteManifest(
const routesByLocale = new Map();
// This type is here only as a helper. We copy the routes and make them unique, so we don't "process" the same route twice.
// The assumption is that a route in the file system belongs to only one locale.
- const setRoutes = new Set(routes);
+ const setRoutes = new Set(routes.filter((route) => route.type === 'page'));
// First loop
// We loop over the locales minus the default locale and add only the routes that contain `/`.
- for (const locale of i18n.locales.filter((loc) => loc !== i18n.defaultLocale)) {
+ const filteredLocales = i18n.locales
+ .filter((loc) => {
+ if (typeof loc === 'string') {
+ return loc !== i18n.defaultLocale;
+ }
+ return loc.path !== i18n.defaultLocale;
+ })
+ .map((locale) => {
+ if (typeof locale === 'string') {
+ return locale;
+ }
+ return locale.path;
+ });
+ for (const locale of filteredLocales) {
for (const route of setRoutes) {
if (!route.route.includes(`/${locale}`)) {
continue;
@@ -529,7 +560,7 @@ export function createRouteManifest(
// Work done, now we start creating "fallback" routes based on the configuration
- if (i18n.routingStrategy === 'prefix-always') {
+ if (i18n.routing === 'prefix-always') {
// we attempt to retrieve the index page of the default locale
const defaultLocaleRoutes = routesByLocale.get(i18n.defaultLocale);
if (defaultLocaleRoutes) {
@@ -549,6 +580,7 @@ export function createRouteManifest(
validateSegment(s);
return getParts(s, route);
});
+
routes.push({
...indexDefaultRoute,
pathname,
@@ -599,22 +631,22 @@ export function createRouteManifest(
if (!hasRoute) {
let pathname: string | undefined;
let route: string;
- if (fallbackToLocale === i18n.defaultLocale) {
+ if (
+ fallbackToLocale === i18n.defaultLocale &&
+ i18n.routing === 'prefix-other-locales'
+ ) {
if (fallbackToRoute.pathname) {
pathname = `/${fallbackFromLocale}${fallbackToRoute.pathname}`;
}
route = `/${fallbackFromLocale}${fallbackToRoute.route}`;
} else {
- pathname = fallbackToRoute.pathname?.replace(
- `/${fallbackToLocale}`,
- `/${fallbackFromLocale}`
- );
- route = fallbackToRoute.route.replace(
- `/${fallbackToLocale}`,
- `/${fallbackFromLocale}`
- );
+ pathname = fallbackToRoute.pathname
+ ?.replace(`/${fallbackToLocale}/`, `/${fallbackFromLocale}/`)
+ .replace(`/${fallbackToLocale}`, `/${fallbackFromLocale}`);
+ route = fallbackToRoute.route
+ .replace(`/${fallbackToLocale}`, `/${fallbackFromLocale}`)
+ .replace(`/${fallbackToLocale}/`, `/${fallbackFromLocale}/`);
}
-
const segments = removeLeadingForwardSlash(route)
.split(path.posix.sep)
.filter(Boolean)
@@ -622,14 +654,22 @@ export function createRouteManifest(
validateSegment(s);
return getParts(s, route);
});
- routes.push({
- ...fallbackToRoute,
- pathname,
- route,
- segments,
- pattern: getPattern(segments, config, config.trailingSlash),
- type: 'fallback',
- });
+ const generate = getRouteGenerator(segments, config.trailingSlash);
+ const index = routes.findIndex((r) => r === fallbackToRoute);
+ if (index >= 0) {
+ const fallbackRoute: RouteData = {
+ ...fallbackToRoute,
+ pathname,
+ route,
+ segments,
+ generate,
+ pattern: getPattern(segments, config, config.trailingSlash),
+ type: 'fallback',
+ fallbackRoutes: [],
+ };
+ const routeData = routes[index];
+ routeData.fallbackRoutes.push(fallbackRoute);
+ }
}
}
}
diff --git a/packages/astro/src/core/routing/manifest/serialization.ts b/packages/astro/src/core/routing/manifest/serialization.ts
index 71ffc221dd54b..f70aa84dd0aca 100644
--- a/packages/astro/src/core/routing/manifest/serialization.ts
+++ b/packages/astro/src/core/routing/manifest/serialization.ts
@@ -13,6 +13,9 @@ export function serializeRouteData(
redirectRoute: routeData.redirectRoute
? serializeRouteData(routeData.redirectRoute, trailingSlash)
: undefined,
+ fallbackRoutes: routeData.fallbackRoutes.map((fallbackRoute) => {
+ return serializeRouteData(fallbackRoute, trailingSlash);
+ }),
_meta: { trailingSlash },
};
}
@@ -32,5 +35,8 @@ export function deserializeRouteData(rawRouteData: SerializedRouteData): RouteDa
redirectRoute: rawRouteData.redirectRoute
? deserializeRouteData(rawRouteData.redirectRoute)
: undefined,
+ fallbackRoutes: rawRouteData.fallbackRoutes.map((fallback) => {
+ return deserializeRouteData(fallback);
+ }),
};
}
diff --git a/packages/astro/src/core/routing/match.ts b/packages/astro/src/core/routing/match.ts
index 9b91e1e9a2f3c..97659253e32eb 100644
--- a/packages/astro/src/core/routing/match.ts
+++ b/packages/astro/src/core/routing/match.ts
@@ -2,7 +2,13 @@ import type { ManifestData, RouteData } from '../../@types/astro.js';
/** Find matching route from pathname */
export function matchRoute(pathname: string, manifest: ManifestData): RouteData | undefined {
- return manifest.routes.find((route) => route.pattern.test(decodeURI(pathname)));
+ const decodedPathname = decodeURI(pathname);
+ return manifest.routes.find((route) => {
+ return (
+ route.pattern.test(decodedPathname) ||
+ route.fallbackRoutes.some((fallbackRoute) => fallbackRoute.pattern.test(decodedPathname))
+ );
+ });
}
/** Finds all matching routes from pathname */
diff --git a/packages/astro/src/core/routing/validation.ts b/packages/astro/src/core/routing/validation.ts
index 0261c865a433f..b68d5f3e89939 100644
--- a/packages/astro/src/core/routing/validation.ts
+++ b/packages/astro/src/core/routing/validation.ts
@@ -79,16 +79,16 @@ export function validateGetStaticPathsResult(
for (const [key, val] of Object.entries(pathObject.params)) {
if (!(typeof val === 'undefined' || typeof val === 'string' || typeof val === 'number')) {
logger.warn(
- 'getStaticPaths',
- `invalid path param: ${key}. A string, number or undefined value was expected, but got \`${JSON.stringify(
+ 'router',
+ `getStaticPaths() returned an invalid path param: "${key}". A string, number or undefined value was expected, but got \`${JSON.stringify(
val
)}\`.`
);
}
if (typeof val === 'string' && val === '') {
logger.warn(
- 'getStaticPaths',
- `invalid path param: ${key}. \`undefined\` expected for an optional param, but got empty string.`
+ 'router',
+ `getStaticPaths() returned an invalid path param: "${key}". \`undefined\` expected for an optional param, but got empty string.`
);
}
}
diff --git a/packages/astro/src/core/sync/index.ts b/packages/astro/src/core/sync/index.ts
index 0c7b81c3aa676..966454845b302 100644
--- a/packages/astro/src/core/sync/index.ts
+++ b/packages/astro/src/core/sync/index.ts
@@ -117,7 +117,7 @@ export async function syncInternal(
switch (typesResult.reason) {
case 'no-content-dir':
default:
- logger.info('content', 'No content directory found. Skipping type generation.');
+ logger.debug('types', 'No content directory found. Skipping type generation.');
return 0;
}
}
@@ -137,7 +137,7 @@ export async function syncInternal(
await tempViteServer.close();
}
- logger.info('content', `Types generated ${dim(getTimeStat(timerStart, performance.now()))}`);
+ logger.info(null, `Types generated ${dim(getTimeStat(timerStart, performance.now()))}`);
await setUpEnvTs({ settings, logger, fs: fs ?? fsMod });
return 0;
diff --git a/packages/astro/src/i18n/index.ts b/packages/astro/src/i18n/index.ts
index 937daf279a676..947689d516117 100644
--- a/packages/astro/src/i18n/index.ts
+++ b/packages/astro/src/i18n/index.ts
@@ -1,5 +1,5 @@
import { appendForwardSlash, joinPaths } from '@astrojs/internal-helpers/path';
-import type { AstroConfig } from '../@types/astro.js';
+import type { AstroConfig, Locales } from '../@types/astro.js';
import { shouldAppendForwardSlash } from '../core/build/util.js';
import { MissingLocale } from '../core/errors/errors-data.js';
import { AstroError } from '../core/errors/index.js';
@@ -7,10 +7,10 @@ import { AstroError } from '../core/errors/index.js';
type GetLocaleRelativeUrl = GetLocaleOptions & {
locale: string;
base: string;
- locales: string[];
+ locales: Locales;
trailingSlash: AstroConfig['trailingSlash'];
format: AstroConfig['build']['format'];
- routingStrategy?: 'prefix-always' | 'prefix-other-locales';
+ routing?: 'prefix-always' | 'prefix-other-locales';
defaultLocale: string;
};
@@ -39,24 +39,25 @@ type GetLocaleAbsoluteUrl = GetLocaleRelativeUrl & {
export function getLocaleRelativeUrl({
locale,
base,
- locales,
+ locales: _locales,
trailingSlash,
format,
path,
prependWith,
normalizeLocale = true,
- routingStrategy = 'prefix-other-locales',
+ routing = 'prefix-other-locales',
defaultLocale,
}: GetLocaleRelativeUrl) {
- if (!locales.includes(locale)) {
+ const codeToUse = peekCodePathToUse(_locales, locale);
+ if (!codeToUse) {
throw new AstroError({
...MissingLocale,
- message: MissingLocale.message(locale, locales),
+ message: MissingLocale.message(locale),
});
}
const pathsToJoin = [base, prependWith];
- const normalizedLocale = normalizeLocale ? normalizeTheLocale(locale) : locale;
- if (routingStrategy === 'prefix-always') {
+ const normalizedLocale = normalizeLocale ? normalizeTheLocale(codeToUse) : codeToUse;
+ if (routing === 'prefix-always') {
pathsToJoin.push(normalizedLocale);
} else if (locale !== defaultLocale) {
pathsToJoin.push(normalizedLocale);
@@ -84,29 +85,30 @@ export function getLocaleAbsoluteUrl({ site, ...rest }: GetLocaleAbsoluteUrl) {
type GetLocalesBaseUrl = GetLocaleOptions & {
base: string;
- locales: string[];
+ locales: Locales;
trailingSlash: AstroConfig['trailingSlash'];
format: AstroConfig['build']['format'];
- routingStrategy?: 'prefix-always' | 'prefix-other-locales';
+ routing?: 'prefix-always' | 'prefix-other-locales';
defaultLocale: string;
};
export function getLocaleRelativeUrlList({
base,
- locales,
+ locales: _locales,
trailingSlash,
format,
path,
prependWith,
normalizeLocale = false,
- routingStrategy = 'prefix-other-locales',
+ routing = 'prefix-other-locales',
defaultLocale,
}: GetLocalesBaseUrl) {
+ const locales = toPaths(_locales);
return locales.map((locale) => {
const pathsToJoin = [base, prependWith];
const normalizedLocale = normalizeLocale ? normalizeTheLocale(locale) : locale;
- if (routingStrategy === 'prefix-always') {
+ if (routing === 'prefix-always') {
pathsToJoin.push(normalizedLocale);
} else if (locale !== defaultLocale) {
pathsToJoin.push(normalizedLocale);
@@ -131,6 +133,45 @@ export function getLocaleAbsoluteUrlList({ site, ...rest }: GetLocaleAbsoluteUrl
});
}
+/**
+ * Given a locale (code), it returns its corresponding path
+ * @param locale
+ * @param locales
+ */
+export function getPathByLocale(locale: string, locales: Locales) {
+ for (const loopLocale of locales) {
+ if (typeof loopLocale === 'string') {
+ if (loopLocale === locale) {
+ return loopLocale;
+ }
+ } else {
+ for (const code of loopLocale.codes) {
+ if (code === locale) {
+ return loopLocale.path;
+ }
+ }
+ }
+ }
+}
+
+/**
+ * An utility function that retrieves the preferred locale that correspond to a path.
+ *
+ * @param locale
+ * @param locales
+ */
+export function getLocaleByPath(path: string, locales: Locales): string | undefined {
+ for (const locale of locales) {
+ if (typeof locale !== 'string') {
+ // the first code is the one that user usually wants
+ const code = locale.codes.at(0);
+ return code;
+ }
+ 1;
+ }
+ return undefined;
+}
+
/**
*
* Given a locale, this function:
@@ -140,3 +181,53 @@ export function getLocaleAbsoluteUrlList({ site, ...rest }: GetLocaleAbsoluteUrl
export function normalizeTheLocale(locale: string): string {
return locale.replaceAll('_', '-').toLowerCase();
}
+
+/**
+ * Returns an array of only locales, by picking the `code`
+ * @param locales
+ */
+export function toCodes(locales: Locales): string[] {
+ const codes: string[] = [];
+ for (const locale of locales) {
+ if (typeof locale === 'string') {
+ codes.push(locale);
+ } else {
+ for (const code of locale.codes) {
+ codes.push(code);
+ }
+ }
+ }
+ return codes;
+}
+
+/**
+ * It returns the array of paths
+ * @param locales
+ */
+export function toPaths(locales: Locales): string[] {
+ return locales.map((loopLocale) => {
+ if (typeof loopLocale === 'string') {
+ return loopLocale;
+ } else {
+ return loopLocale.path;
+ }
+ });
+}
+
+function peekCodePathToUse(locales: Locales, locale: string): undefined | string {
+ for (const loopLocale of locales) {
+ if (typeof loopLocale === 'string') {
+ if (loopLocale === locale) {
+ return loopLocale;
+ }
+ } else {
+ for (const code of loopLocale.codes) {
+ if (code === locale) {
+ return loopLocale.path;
+ }
+ }
+ }
+ }
+
+ return undefined;
+}
diff --git a/packages/astro/src/i18n/middleware.ts b/packages/astro/src/i18n/middleware.ts
index 854a39b77cda3..12732d880eb02 100644
--- a/packages/astro/src/i18n/middleware.ts
+++ b/packages/astro/src/i18n/middleware.ts
@@ -1,25 +1,33 @@
import { appendForwardSlash, joinPaths } from '@astrojs/internal-helpers/path';
-import type { MiddlewareEndpointHandler, RouteData, SSRManifest } from '../@types/astro.js';
+import type { Locales, MiddlewareHandler, RouteData, SSRManifest } from '../@types/astro.js';
import type { PipelineHookFunction } from '../core/pipeline.js';
+import { getPathByLocale, normalizeTheLocale } from './index.js';
const routeDataSymbol = Symbol.for('astro.routeData');
-// Checks if the pathname doesn't have any locale, exception for the defaultLocale, which is ignored on purpose
-function checkIsLocaleFree(pathname: string, locales: string[]): boolean {
- for (const locale of locales) {
- if (pathname.includes(`/${locale}`)) {
- return false;
+// Checks if the pathname has any locale, exception for the defaultLocale, which is ignored on purpose.
+function pathnameHasLocale(pathname: string, locales: Locales): boolean {
+ const segments = pathname.split('/');
+ for (const segment of segments) {
+ for (const locale of locales) {
+ if (typeof locale === 'string') {
+ if (normalizeTheLocale(segment) === normalizeTheLocale(locale)) {
+ return true;
+ }
+ } else if (segment === locale.path) {
+ return true;
+ }
}
}
- return true;
+ return false;
}
export function createI18nMiddleware(
i18n: SSRManifest['i18n'],
base: SSRManifest['base'],
trailingSlash: SSRManifest['trailingSlash']
-): MiddlewareEndpointHandler | undefined {
+): MiddlewareHandler | undefined {
if (!i18n) {
return undefined;
}
@@ -41,21 +49,19 @@ export function createI18nMiddleware(
}
const url = context.url;
- const { locales, defaultLocale, fallback } = i18n;
+ const { locales, defaultLocale, fallback, routing } = i18n;
const response = await next();
if (response instanceof Response) {
- const separators = url.pathname.split('/');
const pathnameContainsDefaultLocale = url.pathname.includes(`/${defaultLocale}`);
- const isLocaleFree = checkIsLocaleFree(url.pathname, i18n.locales);
- if (i18n.routingStrategy === 'prefix-other-locales' && pathnameContainsDefaultLocale) {
+ if (i18n.routing === 'prefix-other-locales' && pathnameContainsDefaultLocale) {
const newLocation = url.pathname.replace(`/${defaultLocale}`, '');
response.headers.set('Location', newLocation);
return new Response(null, {
status: 404,
headers: response.headers,
});
- } else if (i18n.routingStrategy === 'prefix-always') {
+ } else if (i18n.routing === 'prefix-always') {
if (url.pathname === base + '/' || url.pathname === base) {
if (trailingSlash === 'always') {
return context.redirect(`${appendForwardSlash(joinPaths(base, i18n.defaultLocale))}`);
@@ -65,7 +71,7 @@ export function createI18nMiddleware(
}
// Astro can't know where the default locale is supposed to be, so it returns a 404 with no content.
- else if (isLocaleFree) {
+ else if (!pathnameHasLocale(url.pathname, i18n.locales)) {
return new Response(null, {
status: 404,
headers: response.headers,
@@ -75,17 +81,32 @@ export function createI18nMiddleware(
if (response.status >= 300 && fallback) {
const fallbackKeys = i18n.fallback ? Object.keys(i18n.fallback) : [];
- const urlLocale = separators.find((s) => locales.includes(s));
+ // we split the URL using the `/`, and then check in the returned array we have the locale
+ const segments = url.pathname.split('/');
+ const urlLocale = segments.find((segment) => {
+ for (const locale of locales) {
+ if (typeof locale === 'string') {
+ if (locale === segment) {
+ return true;
+ }
+ } else if (locale.path === segment) {
+ return true;
+ }
+ }
+ return false;
+ });
if (urlLocale && fallbackKeys.includes(urlLocale)) {
const fallbackLocale = fallback[urlLocale];
+ // the user might have configured the locale using the granular locales, so we want to retrieve its corresponding path instead
+ const pathFallbackLocale = getPathByLocale(fallbackLocale, locales);
let newPathname: string;
// If a locale falls back to the default locale, we want to **remove** the locale because
// the default locale doesn't have a prefix
- if (fallbackLocale === defaultLocale) {
+ if (pathFallbackLocale === defaultLocale && routing === 'prefix-other-locales') {
newPathname = url.pathname.replace(`/${urlLocale}`, ``);
} else {
- newPathname = url.pathname.replace(`/${urlLocale}`, `/${fallbackLocale}`);
+ newPathname = url.pathname.replace(`/${urlLocale}`, `/${pathFallbackLocale}`);
}
return context.redirect(newPathname);
diff --git a/packages/astro/src/i18n/vite-plugin-i18n.ts b/packages/astro/src/i18n/vite-plugin-i18n.ts
index 4aa6ee42e4f52..cd4c3f854bd3b 100644
--- a/packages/astro/src/i18n/vite-plugin-i18n.ts
+++ b/packages/astro/src/i18n/vite-plugin-i18n.ts
@@ -1,4 +1,4 @@
-import * as vite from 'vite';
+import type * as vite from 'vite';
import type { AstroSettings } from '../@types/astro.js';
const virtualModuleId = 'astro:i18n';
@@ -27,14 +27,15 @@ export default function astroInternationalization({
getLocaleRelativeUrlList as _getLocaleRelativeUrlList,
getLocaleAbsoluteUrl as _getLocaleAbsoluteUrl,
getLocaleAbsoluteUrlList as _getLocaleAbsoluteUrlList,
-
- } from "astro/i18n";
+ getPathByLocale as _getPathByLocale,
+ getLocaleByPath as _getLocaleByPath,
+ } from "astro/virtual-modules/i18n.js";
const base = ${JSON.stringify(settings.config.base)};
const trailingSlash = ${JSON.stringify(settings.config.trailingSlash)};
const format = ${JSON.stringify(settings.config.build.format)};
const site = ${JSON.stringify(settings.config.site)};
- const i18n = ${JSON.stringify(settings.config.experimental.i18n)};
+ const i18n = ${JSON.stringify(settings.config.i18n)};
export const getRelativeLocaleUrl = (locale, path = "", opts) => _getLocaleRelativeUrl({
locale,
@@ -59,6 +60,9 @@ export default function astroInternationalization({
export const getRelativeLocaleUrlList = (path = "", opts) => _getLocaleRelativeUrlList({
base, path, trailingSlash, format, ...i18n, ...opts });
export const getAbsoluteLocaleUrlList = (path = "", opts) => _getLocaleAbsoluteUrlList({ base, path, trailingSlash, format, site, ...i18n, ...opts });
+
+ export const getPathByLocale = (locale) => _getPathByLocale(locale, i18n.locales);
+ export const getLocaleByPath = (locale) => _getLocaleByPath(locale, i18n.locales);
`;
}
},
diff --git a/packages/astro/src/integrations/astroFeaturesValidation.ts b/packages/astro/src/integrations/astroFeaturesValidation.ts
index a26f42afbe4c0..8bae77846c290 100644
--- a/packages/astro/src/integrations/astroFeaturesValidation.ts
+++ b/packages/astro/src/integrations/astroFeaturesValidation.ts
@@ -17,17 +17,6 @@ const UNSUPPORTED_ASSETS_FEATURE: AstroAssetsFeature = {
isSharpCompatible: false,
};
-// NOTE: remove for Astro 4.0
-const ALL_UNSUPPORTED: Required = {
- serverOutput: UNSUPPORTED,
- staticOutput: UNSUPPORTED,
- hybridOutput: UNSUPPORTED,
- assets: UNSUPPORTED_ASSETS_FEATURE,
- i18n: {
- detectBrowserLanguage: UNSUPPORTED,
- },
-};
-
type ValidationResult = {
[Property in keyof AstroFeatureMap]: boolean;
};
@@ -41,7 +30,7 @@ type ValidationResult = {
*/
export function validateSupportedFeatures(
adapterName: string,
- featureMap: AstroFeatureMap = ALL_UNSUPPORTED,
+ featureMap: AstroFeatureMap,
config: AstroConfig,
logger: Logger
): ValidationResult {
@@ -91,9 +80,9 @@ function validateSupportKind(
if (supportKind === STABLE) {
return true;
} else if (supportKind === DEPRECATED) {
- featureIsDeprecated(adapterName, logger);
+ featureIsDeprecated(adapterName, logger, featureName);
} else if (supportKind === EXPERIMENTAL) {
- featureIsExperimental(adapterName, logger);
+ featureIsExperimental(adapterName, logger, featureName);
}
if (hasCorrectConfig() && supportKind === UNSUPPORTED) {
@@ -105,18 +94,21 @@ function validateSupportKind(
}
function featureIsUnsupported(adapterName: string, logger: Logger, featureName: string) {
- logger.error(
- `${adapterName}`,
- `The feature ${featureName} is not supported by the adapter ${adapterName}.`
- );
+ logger.error('config', `The feature "${featureName}" is not supported (used by ${adapterName}).`);
}
-function featureIsExperimental(adapterName: string, logger: Logger) {
- logger.warn(`${adapterName}`, 'The feature is experimental and subject to issues or changes.');
+function featureIsExperimental(adapterName: string, logger: Logger, featureName: string) {
+ logger.warn(
+ 'config',
+ `The feature "${featureName}" is experimental and subject to change (used by ${adapterName}).`
+ );
}
-function featureIsDeprecated(adapterName: string, logger: Logger) {
- logger.warn(`${adapterName}`, 'The feature is deprecated and will be moved in the next release.');
+function featureIsDeprecated(adapterName: string, logger: Logger, featureName: string) {
+ logger.warn(
+ 'config',
+ `The feature "${featureName}" is deprecated and will be removed in the future (used by ${adapterName}).`
+ );
}
const SHARP_SERVICE = 'astro/assets/services/sharp';
@@ -135,7 +127,7 @@ function validateAssetsFeature(
} = assets;
if (config?.image?.service?.entrypoint === SHARP_SERVICE && !isSharpCompatible) {
logger.warn(
- 'astro',
+ null,
`The currently selected adapter \`${adapterName}\` is not compatible with the image service "Sharp".`
);
return false;
@@ -143,7 +135,7 @@ function validateAssetsFeature(
if (config?.image?.service?.entrypoint === SQUOOSH_SERVICE && !isSquooshCompatible) {
logger.warn(
- 'astro',
+ null,
`The currently selected adapter \`${adapterName}\` is not compatible with the image service "Squoosh".`
);
return false;
diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts
index 8b40e5825fdb3..d082c438fbaa5 100644
--- a/packages/astro/src/integrations/index.ts
+++ b/packages/astro/src/integrations/index.ts
@@ -18,23 +18,30 @@ import type { SerializedSSRManifest } from '../core/app/types.js';
import type { PageBuildData } from '../core/build/types.js';
import { buildClientDirectiveEntrypoint } from '../core/client-directive/index.js';
import { mergeConfig } from '../core/config/index.js';
-import { AstroIntegrationLogger, type Logger } from '../core/logger/core.js';
+import type { AstroIntegrationLogger, Logger } from '../core/logger/core.js';
import { isServerLikeOutput } from '../prerender/utils.js';
import { validateSupportedFeatures } from './astroFeaturesValidation.js';
async function withTakingALongTimeMsg({
name,
+ hookName,
hookResult,
timeoutMs = 3000,
logger,
}: {
name: string;
+ hookName: string;
hookResult: T | Promise;
timeoutMs?: number;
logger: Logger;
}): Promise {
const timeout = setTimeout(() => {
- logger.info('build', `Waiting for the ${bold(name)} integration...`);
+ logger.info(
+ 'build',
+ `Waiting for integration ${bold(JSON.stringify(name))}, hook ${bold(
+ JSON.stringify(hookName)
+ )}...`
+ );
}, timeoutMs);
const result = await hookResult;
clearTimeout(timeout);
@@ -118,15 +125,27 @@ export async function runHookConfigSetup({
},
updateConfig: (newConfig) => {
updatedConfig = mergeConfig(updatedConfig, newConfig) as AstroConfig;
+ return { ...updatedConfig };
},
injectRoute: (injectRoute) => {
+ if (injectRoute.entrypoint == null && 'entryPoint' in injectRoute) {
+ logger.warn(
+ null,
+ `The injected route "${injectRoute.pattern}" by ${integration.name} specifies the entry point with the "entryPoint" property. This property is deprecated, please use "entrypoint" instead.`
+ );
+ injectRoute.entrypoint = injectRoute.entryPoint as string;
+ }
updatedSettings.injectedRoutes.push(injectRoute);
},
addWatchFile: (path) => {
updatedSettings.watchFiles.push(path instanceof URL ? fileURLToPath(path) : path);
},
addDevOverlayPlugin: (entrypoint) => {
- updatedSettings.devOverlayPlugins.push(entrypoint);
+ // TODO add a deprecation warning in Astro 5.
+ hooks.addDevToolbarApp(entrypoint);
+ },
+ addDevToolbarApp: (entrypoint) => {
+ updatedSettings.devToolbarApps.push(entrypoint);
},
addClientDirective: ({ name, entrypoint }) => {
if (updatedSettings.clientDirectives.has(name) || addedClientDirectives.has(name)) {
@@ -188,6 +207,7 @@ export async function runHookConfigSetup({
await withTakingALongTimeMsg({
name: integration.name,
+ hookName: 'astro:config:setup',
hookResult: integration.hooks['astro:config:setup'](hooks),
logger,
});
@@ -219,6 +239,7 @@ export async function runHookConfigDone({
if (integration?.hooks?.['astro:config:done']) {
await withTakingALongTimeMsg({
name: integration.name,
+ hookName: 'astro:config:done',
hookResult: integration.hooks['astro:config:done']({
config: settings.config,
setAdapter(adapter) {
@@ -228,10 +249,8 @@ export async function runHookConfigDone({
);
}
if (!adapter.supportedAstroFeatures) {
- // NOTE: throw an error in Astro 4.0
- logger.warn(
- 'astro',
- `The adapter ${adapter.name} doesn't provide a feature map. From Astro 3.0, an adapter can provide a feature map. Not providing a feature map will cause an error in Astro 4.0.`
+ throw new Error(
+ `The adapter ${adapter.name} doesn't provide a feature map. It is required in Astro 4.0.`
);
} else {
const validationResult = validateSupportedFeatures(
@@ -247,7 +266,7 @@ export async function runHookConfigDone({
// if we would refactor the validation to support more than boolean, we could still be able to differentiate between the two cases
if (!supported && featureName !== 'assets') {
logger.error(
- 'astro',
+ null,
`The adapter ${adapter.name} doesn't support the feature ${featureName}. Your project won't be built. You should not use it.`
);
}
@@ -276,6 +295,7 @@ export async function runHookServerSetup({
if (integration?.hooks?.['astro:server:setup']) {
await withTakingALongTimeMsg({
name: integration.name,
+ hookName: 'astro:server:setup',
hookResult: integration.hooks['astro:server:setup']({
server,
logger: getLogger(integration, logger),
@@ -299,6 +319,7 @@ export async function runHookServerStart({
if (integration?.hooks?.['astro:server:start']) {
await withTakingALongTimeMsg({
name: integration.name,
+ hookName: 'astro:server:start',
hookResult: integration.hooks['astro:server:start']({
address,
logger: getLogger(integration, logger),
@@ -320,6 +341,7 @@ export async function runHookServerDone({
if (integration?.hooks?.['astro:server:done']) {
await withTakingALongTimeMsg({
name: integration.name,
+ hookName: 'astro:server:done',
hookResult: integration.hooks['astro:server:done']({
logger: getLogger(integration, logger),
}),
@@ -342,6 +364,7 @@ export async function runHookBuildStart({
await withTakingALongTimeMsg({
name: integration.name,
+ hookName: 'astro:build:start',
hookResult: integration.hooks['astro:build:start']({ logger }),
logger: logging,
});
@@ -368,12 +391,14 @@ export async function runHookBuildSetup({
if (integration?.hooks?.['astro:build:setup']) {
await withTakingALongTimeMsg({
name: integration.name,
+ hookName: 'astro:build:setup',
hookResult: integration.hooks['astro:build:setup']({
vite,
pages,
target,
updateConfig: (newConfig) => {
updatedConfig = mergeConfig(updatedConfig, newConfig);
+ return { ...updatedConfig };
},
logger: getLogger(integration, logger),
}),
@@ -404,6 +429,7 @@ export async function runHookBuildSsr({
if (integration?.hooks?.['astro:build:ssr']) {
await withTakingALongTimeMsg({
name: integration.name,
+ hookName: 'astro:build:ssr',
hookResult: integration.hooks['astro:build:ssr']({
manifest,
entryPoints,
@@ -429,6 +455,7 @@ export async function runHookBuildGenerated({
if (integration?.hooks?.['astro:build:generated']) {
await withTakingALongTimeMsg({
name: integration.name,
+ hookName: 'astro:build:generated',
hookResult: integration.hooks['astro:build:generated']({
dir,
logger: getLogger(integration, logger),
@@ -456,6 +483,7 @@ export async function runHookBuildDone({ config, pages, routes, logging }: RunHo
await withTakingALongTimeMsg({
name: integration.name,
+ hookName: 'astro:build:done',
hookResult: integration.hooks['astro:build:done']({
pages: pages.map((p) => ({ pathname: p })),
dir,
diff --git a/packages/astro/src/preferences/README.md b/packages/astro/src/preferences/README.md
new file mode 100644
index 0000000000000..4234ebac1b67b
--- /dev/null
+++ b/packages/astro/src/preferences/README.md
@@ -0,0 +1,33 @@
+# Preferences
+
+The preferences module implements global and local user preferences for controlling certain Astro behavior. Whereas the `astro.config.mjs` file controls project-specific behavior for every user of a project, preferences are user-specific.
+
+The design of Preferences is inspired by [Git](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration) and [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/settings). Both systems implement similar layering approaches with project-specific and global settings.
+
+## `AstroPreferences`
+
+The `AstroPreferences` interface exposes both a `get` and `set` function.
+
+### Reading a preference
+
+`preferences.get("dot.separated.value")` will read a preference value from multiple sources if needed. Local project preferences are read from `.astro/settings.json`, if it exists. Next, global user preferences are read from `//astro/settings.json`. If neither of those are found, the default preferences defined in [`./defaults.ts`](./defaults.ts) will apply.
+
+In order to read a preference from a specific location, you can pass the `location: "global" | "project"` option.
+
+```js
+await preferences.get('dot.separated.value', { location: 'global' });
+```
+
+### Writing a preference
+
+`preferences.set("dot.separated.value", true)` will store a preference value. By default, preferences are stored locally in a project.
+
+In order to set a global user preference, you can pass the `location: "global"` option.
+
+```js
+await preferences.set('dot.separated.value', 'value', { location: 'global' });
+```
+
+## Relation to Telemetry
+
+This module evolved from the existing `@astrojs/telemetry` package, but has been generalized for user-facing `astro` preferences. At some point, we'll need to merge the logic in `@astrojs/telemetry` and the logic in this module so that all preferences are stored in the same location.
diff --git a/packages/astro/src/preferences/defaults.ts b/packages/astro/src/preferences/defaults.ts
new file mode 100644
index 0000000000000..f1c4d78135cbb
--- /dev/null
+++ b/packages/astro/src/preferences/defaults.ts
@@ -0,0 +1,8 @@
+export const DEFAULT_PREFERENCES = {
+ devToolbar: {
+ /** Specifies whether the user has the Dev Overlay enabled */
+ enabled: true,
+ },
+};
+
+export type Preferences = typeof DEFAULT_PREFERENCES;
diff --git a/packages/astro/src/preferences/index.ts b/packages/astro/src/preferences/index.ts
new file mode 100644
index 0000000000000..ee50261158bc4
--- /dev/null
+++ b/packages/astro/src/preferences/index.ts
@@ -0,0 +1,125 @@
+import type { AstroConfig } from '../@types/astro.js';
+
+import os from 'node:os';
+import path from 'node:path';
+import process from 'node:process';
+import { fileURLToPath } from 'node:url';
+
+import dget from 'dlv';
+import { DEFAULT_PREFERENCES, type Preferences } from './defaults.js';
+import { PreferenceStore } from './store.js';
+
+type DotKeys = T extends object
+ ? {
+ [K in keyof T]: `${Exclude}${DotKeys extends never
+ ? ''
+ : `.${DotKeys}`}`;
+ }[keyof T]
+ : never;
+
+export type GetDotKey<
+ T extends Record,
+ K extends string,
+> = K extends `${infer U}.${infer Rest}` ? GetDotKey : T[K];
+
+export type PreferenceLocation = 'global' | 'project';
+export interface PreferenceOptions {
+ location?: PreferenceLocation;
+}
+
+type DeepPartial = T extends object
+ ? {
+ [P in keyof T]?: DeepPartial;
+ }
+ : T;
+
+export type PreferenceKey = DotKeys;
+export interface PreferenceList extends Record> {
+ defaults: Preferences;
+}
+
+export interface AstroPreferences {
+ get(
+ key: Key,
+ opts?: PreferenceOptions
+ ): Promise>;
+ set(
+ key: Key,
+ value: GetDotKey,
+ opts?: PreferenceOptions
+ ): Promise;
+ getAll(): Promise;
+ list(opts?: PreferenceOptions): Promise;
+}
+
+export function isValidKey(key: string): key is PreferenceKey {
+ return dget(DEFAULT_PREFERENCES, key) !== undefined;
+}
+export function coerce(key: string, value: unknown) {
+ const type = typeof dget(DEFAULT_PREFERENCES, key);
+ switch (type) {
+ case 'string':
+ return value;
+ case 'number':
+ return Number(value);
+ case 'boolean': {
+ if (value === 'true' || value === 1) return true;
+ if (value === 'false' || value === 0) return false;
+ }
+ }
+ return value as any;
+}
+
+export default function createPreferences(config: AstroConfig): AstroPreferences {
+ const global = new PreferenceStore(getGlobalPreferenceDir());
+ const project = new PreferenceStore(fileURLToPath(new URL('./.astro/', config.root)));
+ const stores: Record = { global, project };
+
+ return {
+ async get(key, { location } = {}) {
+ if (!location) return project.get(key) ?? global.get(key) ?? dget(DEFAULT_PREFERENCES, key);
+ return stores[location].get(key);
+ },
+ async set(key, value, { location = 'project' } = {}) {
+ stores[location].set(key, value);
+ },
+ async getAll() {
+ return Object.assign(
+ {},
+ DEFAULT_PREFERENCES,
+ stores['global'].getAll(),
+ stores['project'].getAll()
+ );
+ },
+ async list() {
+ return {
+ global: stores['global'].getAll(),
+ project: stores['project'].getAll(),
+ defaults: DEFAULT_PREFERENCES,
+ };
+ },
+ };
+}
+
+// Adapted from https://github.com/sindresorhus/env-paths
+export function getGlobalPreferenceDir() {
+ const name = 'astro';
+ const homedir = os.homedir();
+ const macos = () => path.join(homedir, 'Library', 'Preferences', name);
+ const win = () => {
+ const { APPDATA = path.join(homedir, 'AppData', 'Roaming') } = process.env;
+ return path.join(APPDATA, name, 'Config');
+ };
+ const linux = () => {
+ const { XDG_CONFIG_HOME = path.join(homedir, '.config') } = process.env;
+ return path.join(XDG_CONFIG_HOME, name);
+ };
+ switch (process.platform) {
+ case 'darwin':
+ return macos();
+ case 'win32':
+ return win();
+ default:
+ return linux();
+ }
+}
diff --git a/packages/astro/src/preferences/store.ts b/packages/astro/src/preferences/store.ts
new file mode 100644
index 0000000000000..1bf9c46c70df5
--- /dev/null
+++ b/packages/astro/src/preferences/store.ts
@@ -0,0 +1,62 @@
+import dget from 'dlv';
+import { dset } from 'dset';
+import fs from 'node:fs';
+import path from 'node:path';
+
+export class PreferenceStore {
+ private file: string;
+
+ constructor(
+ private dir: string,
+ filename = 'settings.json'
+ ) {
+ this.file = path.join(this.dir, filename);
+ }
+
+ private _store?: Record;
+ private get store(): Record {
+ if (this._store) return this._store;
+ if (fs.existsSync(this.file)) {
+ try {
+ this._store = JSON.parse(fs.readFileSync(this.file).toString());
+ } catch {}
+ }
+ if (!this._store) {
+ this._store = {};
+ this.write();
+ }
+ return this._store;
+ }
+ private set store(value: Record) {
+ this._store = value;
+ this.write();
+ }
+ write() {
+ if (!this._store || Object.keys(this._store).length === 0) return;
+ fs.mkdirSync(this.dir, { recursive: true });
+ fs.writeFileSync(this.file, JSON.stringify(this.store, null, '\t'));
+ }
+ clear(): void {
+ this.store = {};
+ fs.rmSync(this.file, { recursive: true });
+ }
+ delete(key: string): boolean {
+ dset(this.store, key, undefined);
+ this.write();
+ return true;
+ }
+ get(key: string): any {
+ return dget(this.store, key);
+ }
+ has(key: string): boolean {
+ return typeof this.get(key) !== 'undefined';
+ }
+ set(key: string, value: any): void {
+ if (this.get(key) === value) return;
+ dset(this.store, key, value);
+ this.write();
+ }
+ getAll(): Record {
+ return this.store;
+ }
+}
diff --git a/packages/astro/src/prefetch/index.ts b/packages/astro/src/prefetch/index.ts
index f47cff0606f98..15f4ef0ccd952 100644
--- a/packages/astro/src/prefetch/index.ts
+++ b/packages/astro/src/prefetch/index.ts
@@ -56,7 +56,7 @@ function initTapStrategy() {
event,
(e) => {
if (elMatchesStrategy(e.target, 'tap')) {
- prefetch(e.target.href, { with: 'fetch' });
+ prefetch(e.target.href, { with: 'fetch', ignoreSlowConnection: true });
}
},
{ passive: true }
@@ -176,6 +176,10 @@ export interface PrefetchOptions {
* - `'fetch'`: use `fetch()`, has higher loading priority.
*/
with?: 'link' | 'fetch';
+ /**
+ * Should prefetch even on data saver mode or slow connection. (default `false`)
+ */
+ ignoreSlowConnection?: boolean;
}
/**
@@ -190,7 +194,8 @@ export interface PrefetchOptions {
* @param opts Additional options for prefetching.
*/
export function prefetch(url: string, opts?: PrefetchOptions) {
- if (!canPrefetchUrl(url)) return;
+ const ignoreSlowConnection = opts?.ignoreSlowConnection ?? false;
+ if (!canPrefetchUrl(url, ignoreSlowConnection)) return;
prefetchedUrls.add(url);
const priority = opts?.with ?? 'link';
@@ -211,21 +216,17 @@ export function prefetch(url: string, opts?: PrefetchOptions) {
}
}
-function canPrefetchUrl(url: string) {
+function canPrefetchUrl(url: string, ignoreSlowConnection: boolean) {
// Skip prefetch if offline
if (!navigator.onLine) return false;
- if ('connection' in navigator) {
- // Untyped Chrome-only feature: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection
- const conn = navigator.connection as any;
- // Skip prefetch if using data saver mode or slow connection
- if (conn.saveData || /(2|3)g/.test(conn.effectiveType)) return false;
- }
+ // Skip prefetch if using data saver mode or slow connection
+ if (!ignoreSlowConnection && isSlowConnection()) return false;
// Else check if URL is within the same origin, not the current page, and not already prefetched
try {
const urlObj = new URL(url, location.href);
return (
location.origin === urlObj.origin &&
- location.pathname !== urlObj.pathname &&
+ (location.pathname !== urlObj.pathname || location.search !== urlObj.search) &&
!prefetchedUrls.has(url)
);
} catch {}
@@ -241,6 +242,12 @@ function elMatchesStrategy(el: EventTarget | null, strategy: string): el is HTML
if (attrValue === 'false') {
return false;
}
+
+ // Fallback to tap strategy if using data saver mode or slow connection
+ if (strategy === 'tap' && (attrValue != null || prefetchAll) && isSlowConnection()) {
+ return true;
+ }
+
// If anchor has no dataset but we want to prefetch all, or has dataset but no value,
// check against fallback default strategy
if ((attrValue == null && prefetchAll) || attrValue === '') {
@@ -254,6 +261,15 @@ function elMatchesStrategy(el: EventTarget | null, strategy: string): el is HTML
return false;
}
+function isSlowConnection() {
+ if ('connection' in navigator) {
+ // Untyped Chrome-only feature: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection
+ const conn = navigator.connection as any;
+ return conn.saveData || /(2|3)g/.test(conn.effectiveType);
+ }
+ return false;
+}
+
/**
* Listen to page loads and handle Astro's View Transition specific events
*/
diff --git a/packages/astro/src/prefetch/vite-plugin-prefetch.ts b/packages/astro/src/prefetch/vite-plugin-prefetch.ts
index 73ae53f63b53b..83c3c3ff7c592 100644
--- a/packages/astro/src/prefetch/vite-plugin-prefetch.ts
+++ b/packages/astro/src/prefetch/vite-plugin-prefetch.ts
@@ -1,10 +1,10 @@
-import * as vite from 'vite';
+import type * as vite from 'vite';
import type { AstroSettings } from '../@types/astro.js';
const virtualModuleId = 'astro:prefetch';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
const prefetchInternalModuleFsSubpath = 'astro/dist/prefetch/index.js';
-const prefetchCode = `import { init } from 'astro/prefetch';init()`;
+const prefetchCode = `import { init } from 'astro/virtual-modules/prefetch.js';init()`;
export default function astroPrefetch({ settings }: { settings: AstroSettings }): vite.Plugin {
const prefetchOption = settings.config.prefetch;
@@ -19,7 +19,7 @@ export default function astroPrefetch({ settings }: { settings: AstroSettings })
// Inject prefetch script to all pages
settings.scripts.push({
stage: 'page',
- content: `import { init } from 'astro/prefetch';init()`,
+ content: `import { init } from 'astro/virtual-modules/prefetch.js';init()`,
});
}
@@ -40,7 +40,7 @@ export default function astroPrefetch({ settings }: { settings: AstroSettings })
load(id) {
if (id === resolvedVirtualModuleId) {
if (!prefetch) throwPrefetchNotEnabledError();
- return `export { prefetch } from "astro/prefetch";`;
+ return `export { prefetch } from "astro/virtual-modules/prefetch.js";`;
}
},
transform(code, id) {
diff --git a/packages/astro/src/runtime/client/dev-overlay/entrypoint.ts b/packages/astro/src/runtime/client/dev-overlay/entrypoint.ts
index 65e50c98e03ef..fa3020b03c980 100644
--- a/packages/astro/src/runtime/client/dev-overlay/entrypoint.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/entrypoint.ts
@@ -1,51 +1,65 @@
import type { DevOverlayPlugin as DevOverlayPluginDefinition } from '../../../@types/astro.js';
-import { type AstroDevOverlay, type DevOverlayPlugin } from './overlay.js';
-
+import type { AstroDevOverlay, DevOverlayPlugin } from './overlay.js';
import { settings } from './settings.js';
-import type { Icon } from './ui-library/icons.js';
+// @ts-expect-error
+import { loadDevOverlayPlugins } from 'astro:dev-overlay';
let overlay: AstroDevOverlay;
document.addEventListener('DOMContentLoaded', async () => {
const [
- { loadDevOverlayPlugins },
+ customPluginsDefinitions,
{ default: astroDevToolPlugin },
{ default: astroAuditPlugin },
{ default: astroXrayPlugin },
{ default: astroSettingsPlugin },
- { AstroDevOverlay, DevOverlayCanvas },
- { DevOverlayCard },
- { DevOverlayHighlight },
- { DevOverlayTooltip },
- { DevOverlayWindow },
- { DevOverlayToggle },
- { getIconElement, isDefinedIcon },
+ { AstroDevOverlay, DevOverlayCanvas, getPluginIcon },
+ {
+ DevOverlayCard,
+ DevOverlayHighlight,
+ DevOverlayTooltip,
+ DevOverlayWindow,
+ DevOverlayToggle,
+ DevOverlayButton,
+ DevOverlayBadge,
+ DevOverlayIcon,
+ },
] = await Promise.all([
- // @ts-expect-error
- import('astro:dev-overlay'),
+ loadDevOverlayPlugins() as DevOverlayPluginDefinition[],
import('./plugins/astro.js'),
- import('./plugins/audit.js'),
+ import('./plugins/audit/index.js'),
import('./plugins/xray.js'),
import('./plugins/settings.js'),
import('./overlay.js'),
- import('./ui-library/card.js'),
- import('./ui-library/highlight.js'),
- import('./ui-library/tooltip.js'),
- import('./ui-library/window.js'),
- import('./ui-library/toggle.js'),
- import('./ui-library/icons.js'),
+ import('./ui-library/index.js'),
]);
// Register custom elements
- customElements.define('astro-dev-overlay', AstroDevOverlay);
- customElements.define('astro-dev-overlay-window', DevOverlayWindow);
- customElements.define('astro-dev-overlay-plugin-canvas', DevOverlayCanvas);
- customElements.define('astro-dev-overlay-tooltip', DevOverlayTooltip);
- customElements.define('astro-dev-overlay-highlight', DevOverlayHighlight);
- customElements.define('astro-dev-overlay-card', DevOverlayCard);
- customElements.define('astro-dev-overlay-toggle', DevOverlayToggle);
-
- overlay = document.createElement('astro-dev-overlay');
+ customElements.define('astro-dev-toolbar', AstroDevOverlay);
+ customElements.define('astro-dev-toolbar-window', DevOverlayWindow);
+ customElements.define('astro-dev-toolbar-plugin-canvas', DevOverlayCanvas);
+ customElements.define('astro-dev-toolbar-tooltip', DevOverlayTooltip);
+ customElements.define('astro-dev-toolbar-highlight', DevOverlayHighlight);
+ customElements.define('astro-dev-toolbar-card', DevOverlayCard);
+ customElements.define('astro-dev-toolbar-toggle', DevOverlayToggle);
+ customElements.define('astro-dev-toolbar-button', DevOverlayButton);
+ customElements.define('astro-dev-toolbar-badge', DevOverlayBadge);
+ customElements.define('astro-dev-toolbar-icon', DevOverlayIcon);
+
+ // Add deprecated names
+ const deprecated = (Parent: any) => class extends Parent {};
+ customElements.define('astro-dev-overlay', deprecated(AstroDevOverlay));
+ customElements.define('astro-dev-overlay-window', deprecated(DevOverlayWindow));
+ customElements.define('astro-dev-overlay-plugin-canvas', deprecated(DevOverlayCanvas));
+ customElements.define('astro-dev-overlay-tooltip', deprecated(DevOverlayTooltip));
+ customElements.define('astro-dev-overlay-highlight', deprecated(DevOverlayHighlight));
+ customElements.define('astro-dev-overlay-card', deprecated(DevOverlayCard));
+ customElements.define('astro-dev-overlay-toggle', deprecated(DevOverlayToggle));
+ customElements.define('astro-dev-overlay-button', deprecated(DevOverlayButton));
+ customElements.define('astro-dev-overlay-badge', deprecated(DevOverlayBadge));
+ customElements.define('astro-dev-overlay-icon', deprecated(DevOverlayIcon));
+
+ overlay = document.createElement('astro-dev-toolbar');
const preparePlugin = (
pluginDefinition: DevOverlayPluginDefinition,
@@ -73,19 +87,21 @@ document.addEventListener('DOMContentLoaded', async () => {
plugin.notification.state = newState;
- if (settings.config.disablePluginNotification === false) {
- target.querySelector('.notification')?.toggleAttribute('data-active', newState);
- }
+ target.querySelector('.notification')?.toggleAttribute('data-active', newState);
});
- eventTarget.addEventListener('toggle-plugin', async (evt) => {
+ const onToggleApp = async (evt: Event) => {
let newState = undefined;
if (evt instanceof CustomEvent) {
newState = evt.detail.state ?? true;
}
- await overlay.togglePluginStatus(plugin, newState);
- });
+ await overlay.setPluginStatus(plugin, newState);
+ };
+
+ eventTarget.addEventListener('toggle-app', onToggleApp);
+ // Deprecated
+ eventTarget.addEventListener('toggle-plugin', onToggleApp);
return plugin;
};
@@ -109,9 +125,13 @@ document.addEventListener('DOMContentLoaded', async () => {
border: 1px solid rgba(52, 56, 65, 1);
border-radius: 12px;
box-shadow: 0px 0px 0px 0px rgba(19, 21, 26, 0.30), 0px 1px 2px 0px rgba(19, 21, 26, 0.29), 0px 4px 4px 0px rgba(19, 21, 26, 0.26), 0px 10px 6px 0px rgba(19, 21, 26, 0.15), 0px 17px 7px 0px rgba(19, 21, 26, 0.04), 0px 26px 7px 0px rgba(19, 21, 26, 0.01);
- width: 180px;
+ width: 192px;
padding: 8px;
- z-index: 9999999999;
+ z-index: 2000000010;
+ transform: translate(-50%, 0%);
+ position: fixed;
+ bottom: 72px;
+ left: 50%;
}
.notification {
@@ -126,7 +146,7 @@ document.addEventListener('DOMContentLoaded', async () => {
background: #B33E66;
}
- .notification[data-active] {
+ #dropdown:not([data-no-notification]) .notification[data-active] {
display: block;
}
@@ -135,20 +155,19 @@ document.addEventListener('DOMContentLoaded', async () => {
background: transparent;
color: white;
font-family: system-ui, sans-serif;
- font-size: 16px;
- line-height: 1.2;
+ font-size: 14px;
white-space: nowrap;
text-decoration: none;
margin: 0;
display: flex;
- align-items: center;
+ align-items: center;
width: 100%;
padding: 8px;
border-radius: 8px;
}
#dropdown button:hover, #dropdown button:focus-visible {
- background: rgba(27, 30, 36, 1);
+ background: #FFFFFF20;
cursor: pointer;
}
@@ -158,8 +177,9 @@ document.addEventListener('DOMContentLoaded', async () => {
#dropdown .icon {
position: relative;
- height: 24px;
- width: 24px;
+ height: 20px;
+ width: 20px;
+ padding: 1px;
margin-right: 0.5em;
}
@@ -172,6 +192,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const dropdown = document.createElement('div');
dropdown.id = 'dropdown';
+ dropdown.toggleAttribute('data-no-notification', settings.config.disablePluginNotification);
for (const plugin of hiddenPlugins) {
const buttonContainer = document.createElement('div');
@@ -180,8 +201,9 @@ document.addEventListener('DOMContentLoaded', async () => {
button.setAttribute('data-plugin-id', plugin.id);
const iconContainer = document.createElement('div');
- const iconElement = getPluginIcon(plugin.icon);
- iconContainer.append(iconElement);
+ const iconElement = document.createElement('template');
+ iconElement.innerHTML = getPluginIcon(plugin.icon);
+ iconContainer.append(iconElement.content.cloneNode(true));
const notification = document.createElement('div');
notification.classList.add('notification');
@@ -198,15 +220,10 @@ document.addEventListener('DOMContentLoaded', async () => {
dropdown.append(buttonContainer);
- eventTarget.addEventListener('plugin-toggled', positionDropdown);
- window.addEventListener('resize', positionDropdown);
-
plugin.eventTarget.addEventListener('toggle-notification', (evt) => {
if (!(evt instanceof CustomEvent)) return;
- if (settings.config.disablePluginNotification === false) {
- notification.toggleAttribute('data-active', evt.detail.state ?? true);
- }
+ notification.toggleAttribute('data-active', evt.detail.state ?? true);
eventTarget.dispatchEvent(
new CustomEvent('toggle-notification', {
@@ -219,34 +236,10 @@ document.addEventListener('DOMContentLoaded', async () => {
}
canvas.append(dropdown);
-
- function getPluginIcon(icon: Icon) {
- if (isDefinedIcon(icon)) {
- return getIconElement(icon)!;
- }
-
- return icon;
- }
-
- function positionDropdown() {
- const moreButtonRect = overlay.shadowRoot
- .querySelector('[data-plugin-id="astro:more"]')
- ?.getBoundingClientRect();
- const dropdownRect = dropdown.getBoundingClientRect();
-
- if (moreButtonRect && dropdownRect) {
- dropdown.style.position = 'absolute';
- dropdown.style.top = `${moreButtonRect.top - dropdownRect.height - 12}px`;
- dropdown.style.left = `${
- moreButtonRect.left + moreButtonRect.width - dropdownRect.width
- }px`;
- }
- }
}
},
} satisfies DevOverlayPluginDefinition;
- const customPluginsDefinitions = (await loadDevOverlayPlugins()) as DevOverlayPluginDefinition[];
const plugins: DevOverlayPlugin[] = [
...[
astroDevToolPlugin,
diff --git a/packages/astro/src/runtime/client/dev-overlay/overlay.ts b/packages/astro/src/runtime/client/dev-overlay/overlay.ts
index 900c3fb0fe608..48241ded9f774 100644
--- a/packages/astro/src/runtime/client/dev-overlay/overlay.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/overlay.ts
@@ -1,5 +1,8 @@
/* eslint-disable no-console */
-import type { DevOverlayPlugin as DevOverlayPluginDefinition } from '../../../@types/astro.js';
+import type {
+ DevOverlayMetadata,
+ DevOverlayPlugin as DevOverlayPluginDefinition,
+} from '../../../@types/astro.js';
import { settings } from './settings.js';
import { getIconElement, isDefinedIcon, type Icon } from './ui-library/icons.js';
@@ -12,17 +15,17 @@ export type DevOverlayPlugin = DevOverlayPluginDefinition & {
};
eventTarget: EventTarget;
};
-
-const WS_EVENT_NAME = 'astro-dev-overlay';
+const WS_EVENT_NAME = 'astro-dev-toolbar';
+const WS_EVENT_NAME_DEPRECATED = 'astro-dev-overlay';
+const HOVER_DELAY = 2 * 1000;
export class AstroDevOverlay extends HTMLElement {
shadowRoot: ShadowRoot;
- hoverTimeout: number | undefined;
- isHidden: () => boolean = () => this.devOverlay?.hasAttribute('data-hidden') ?? true;
+ delayedHideTimeout: number | undefined;
devOverlay: HTMLDivElement | undefined;
plugins: DevOverlayPlugin[] = [];
- HOVER_DELAY = 750;
hasBeenInitialized = false;
+ // TODO: This should be dynamic based on the screen size or at least configurable, erika - 2023-11-29
customPluginsToShow = 3;
constructor() {
@@ -30,31 +33,34 @@ export class AstroDevOverlay extends HTMLElement {
this.shadowRoot = this.attachShadow({ mode: 'open' });
}
- // Happens whenever the component is connected to the DOM
- // When view transitions are enabled, this happens every time the view changes
- async connectedCallback() {
- if (!this.hasBeenInitialized) {
- this.shadowRoot.innerHTML = `
-
-
-
+
+
+
${this.plugins
@@ -267,135 +262,100 @@ export class AstroDevOverlay extends HTMLElement {
)}
-
${getIconElement('arrow-down')?.outerHTML}
+
`;
- this.devOverlay = this.shadowRoot.querySelector
('#dev-overlay')!;
- this.attachEvents();
- }
+
+ this.devOverlay = this.shadowRoot.querySelector('#dev-overlay')!;
+ this.attachEvents();
// Create plugin canvases
this.plugins.forEach(async (plugin) => {
- if (!this.hasBeenInitialized) {
- if (settings.config.verbose) console.log(`Creating plugin canvas for ${plugin.id}`);
-
- const pluginCanvas = document.createElement('astro-dev-overlay-plugin-canvas');
- pluginCanvas.dataset.pluginId = plugin.id;
- this.shadowRoot?.append(pluginCanvas);
- }
-
- await this.togglePluginStatus(plugin, plugin.active);
+ if (settings.config.verbose) console.log(`Creating plugin canvas for ${plugin.id}`);
+ const pluginCanvas = document.createElement('astro-dev-toolbar-plugin-canvas');
+ pluginCanvas.dataset.pluginId = plugin.id;
+ this.shadowRoot?.append(pluginCanvas);
});
- // Init plugin lazily - This is safe to do here because only plugins that are not initialized yet will be affected
+ // Init plugin lazily, so that the page can load faster.
+ // Fallback to setTimeout for Safari (sad!)
if ('requestIdleCallback' in window) {
- window.requestIdleCallback(async () => {
- await this.initAllPlugins();
- });
+ window.requestIdleCallback(
+ async () => {
+ this.plugins.map((plugin) => this.initPlugin(plugin));
+ },
+ { timeout: 300 }
+ );
} else {
- // Fallback to setTimeout for.. Safari...
setTimeout(async () => {
- await this.initAllPlugins();
- }, 200);
+ this.plugins.map((plugin) => this.initPlugin(plugin));
+ }, 300);
}
+ }
- this.hasBeenInitialized = true;
+ // This is called whenever the component is connected to the DOM.
+ // This happens on first page load, and on each page change when
+ // view transitions are used.
+ connectedCallback() {
+ if (!this.hasBeenInitialized) {
+ this.init();
+ this.hasBeenInitialized = true;
+ }
+
+ // Run this every time to make sure the correct plugin is open.
+ this.plugins.forEach(async (plugin) => {
+ await this.setPluginStatus(plugin, plugin.active);
+ });
}
attachEvents() {
const items = this.shadowRoot.querySelectorAll('.item');
items.forEach((item) => {
- item.addEventListener('click', async (e) => {
- const target = e.currentTarget;
+ item.addEventListener('click', async (event) => {
+ const target = event.currentTarget;
if (!target || !(target instanceof HTMLElement)) return;
-
const id = target.dataset.pluginId;
if (!id) return;
-
const plugin = this.getPluginById(id);
if (!plugin) return;
-
- if (plugin.status === 'loading') {
- await this.initPlugin(plugin);
- }
-
+ event.stopPropagation();
await this.togglePluginStatus(plugin);
});
});
- const minimizeButton = this.shadowRoot.querySelector('#minimize-button');
- if (minimizeButton && this.devOverlay) {
- minimizeButton.addEventListener('click', () => {
- this.toggleOverlay(false);
- this.toggleMinimizeButton(false);
- });
- }
-
- const devBar = this.shadowRoot.querySelector('#dev-bar');
- if (devBar) {
- // On hover:
- // - If the overlay is hidden, show it after the hover delay
- // - If the overlay is visible, show the minimize button after the hover delay
- (['mouseenter', 'focusin'] as const).forEach((event) => {
- devBar.addEventListener(event, () => {
- if (this.hoverTimeout) {
- window.clearTimeout(this.hoverTimeout);
- }
-
- if (this.isHidden()) {
- this.hoverTimeout = window.setTimeout(() => {
- this.toggleOverlay(true);
- }, this.HOVER_DELAY + 200); // Slightly higher delay here to prevent users opening the overlay by accident
- } else {
- this.hoverTimeout = window.setTimeout(() => {
- this.toggleMinimizeButton(true);
- }, this.HOVER_DELAY);
- }
- });
- });
-
- // On unhover:
- // - Reset every timeout, as to avoid showing the overlay/minimize button when the user didn't really want to hover
- // - If the overlay is visible, hide the minimize button after the hover delay
- devBar.addEventListener('mouseleave', () => {
- if (this.hoverTimeout) {
- window.clearTimeout(this.hoverTimeout);
- }
-
- if (!this.isHidden()) {
- this.hoverTimeout = window.setTimeout(() => {
- this.toggleMinimizeButton(false);
- }, this.HOVER_DELAY);
+ (['mouseenter', 'focusin'] as const).forEach((event) => {
+ this.devOverlay!.addEventListener(event, () => {
+ this.clearDelayedHide();
+ if (this.isHidden()) {
+ this.setOverlayVisible(true);
}
});
+ });
- // On click, show the overlay if it's hidden, it's likely the user wants to interact with it
- devBar.addEventListener('click', () => {
- if (!this.isHidden()) return;
- this.toggleOverlay(true);
- });
-
- devBar.addEventListener('keyup', (event) => {
- if (event.code === 'Space' || event.code === 'Enter') {
- if (!this.isHidden()) return;
- this.toggleOverlay(true);
+ (['mouseleave', 'focusout'] as const).forEach((event) => {
+ this.devOverlay!.addEventListener(event, () => {
+ this.clearDelayedHide();
+ if (this.getActivePlugin() || this.isHidden()) {
+ return;
}
+ this.triggerDelayedHide();
});
- }
- }
+ });
- async initAllPlugins() {
- await Promise.all(
- this.plugins
- .filter((plugin) => plugin.status === 'loading')
- .map((plugin) => this.initPlugin(plugin))
- );
+ document.addEventListener('keyup', (event) => {
+ if (event.key !== 'Escape') return;
+ if (this.isHidden()) return;
+ const activePlugin = this.getActivePlugin();
+ if (activePlugin) {
+ this.togglePluginStatus(activePlugin);
+ } else {
+ this.setOverlayVisible(false);
+ }
+ });
}
async initPlugin(plugin: DevOverlayPlugin) {
- if (plugin.status === 'ready') return;
-
const shadowRoot = this.getPluginCanvasById(plugin.id)!.shadowRoot!;
-
+ plugin.status = 'loading';
try {
if (settings.config.verbose) console.info(`Initializing plugin ${plugin.id}`);
@@ -404,6 +364,7 @@ export class AstroDevOverlay extends HTMLElement {
if (import.meta.hot) {
import.meta.hot.send(`${WS_EVENT_NAME}:${plugin.id}:initialized`);
+ import.meta.hot.send(`${WS_EVENT_NAME_DEPRECATED}:${plugin.id}:initialized`);
}
} catch (e) {
console.error(`Failed to init plugin ${plugin.id}, error: ${e}`);
@@ -413,42 +374,51 @@ export class AstroDevOverlay extends HTMLElement {
getPluginTemplate(plugin: DevOverlayPlugin) {
return `
- ${this.getPluginIcon(plugin.icon)}
+ ${getPluginIcon(plugin.icon)}
${plugin.name}
`;
}
- getPluginIcon(icon: Icon) {
- if (isDefinedIcon(icon)) {
- return getIconElement(icon)?.outerHTML;
- }
-
- return icon;
- }
-
getPluginById(id: string) {
return this.plugins.find((plugin) => plugin.id === id);
}
getPluginCanvasById(id: string) {
return this.shadowRoot.querySelector(
- `astro-dev-overlay-plugin-canvas[data-plugin-id="${id}"]`
+ `astro-dev-toolbar-plugin-canvas[data-plugin-id="${id}"]`
);
}
- /**
- * @param plugin The plugin to toggle the status of
- * @param newStatus Optionally, force the plugin into a specific state
- */
- async togglePluginStatus(plugin: DevOverlayPlugin, newStatus?: boolean) {
+ async togglePluginStatus(plugin: DevOverlayPlugin) {
+ const activePlugin = this.getActivePlugin();
+ if (activePlugin) {
+ const closePlugin = await this.setPluginStatus(activePlugin, false);
+
+ // If the plugin returned false, don't open the new plugin, the old plugin didn't want to close
+ if (!closePlugin) return;
+ }
+
+ // TODO(fks): Handle a plugin that hasn't loaded yet.
+ // Currently, this will just do nothing.
+ if (plugin.status !== 'ready') return;
+
+ // Open the selected plugin. If the selected plugin was
+ // already the active plugin then the desired outcome
+ // was to close that plugin, so no action needed.
+ if (plugin !== activePlugin) {
+ await this.setPluginStatus(plugin, true);
+ }
+ }
+
+ async setPluginStatus(plugin: DevOverlayPlugin, newStatus: boolean) {
const pluginCanvas = this.getPluginCanvasById(plugin.id);
- if (!pluginCanvas) return;
+ if (!pluginCanvas) return false;
if (plugin.active && !newStatus && plugin.beforeTogglingOff) {
const shouldToggleOff = await plugin.beforeTogglingOff(pluginCanvas.shadowRoot!);
// If the plugin returned false, don't toggle it off, maybe the plugin showed a confirmation dialog or similar
- if (!shouldToggleOff) return;
+ if (!shouldToggleOff) return false;
}
plugin.active = newStatus ?? !plugin.active;
@@ -465,12 +435,21 @@ export class AstroDevOverlay extends HTMLElement {
moreBarButton.classList.toggle('active', plugin.active);
}
- pluginCanvas.style.display = plugin.active ? 'block' : 'none';
+ if (plugin.active) {
+ pluginCanvas.style.display = 'block';
+ pluginCanvas.setAttribute('data-active', '');
+ } else {
+ pluginCanvas.style.display = 'none';
+ pluginCanvas.removeAttribute('data-active');
+ }
- window.requestAnimationFrame(() => {
- pluginCanvas.toggleAttribute('data-active', plugin.active);
+ [
+ 'app-toggled',
+ // Deprecated
+ 'plugin-toggled',
+ ].forEach((eventName) => {
plugin.eventTarget.dispatchEvent(
- new CustomEvent('plugin-toggled', {
+ new CustomEvent(eventName, {
detail: {
state: plugin.active,
plugin,
@@ -481,53 +460,60 @@ export class AstroDevOverlay extends HTMLElement {
if (import.meta.hot) {
import.meta.hot.send(`${WS_EVENT_NAME}:${plugin.id}:toggled`, { state: plugin.active });
+ import.meta.hot.send(`${WS_EVENT_NAME_DEPRECATED}:${plugin.id}:toggled`, {
+ state: plugin.active,
+ });
}
+
+ return true;
}
- /**
- * @param newStatus Optionally, force the minimize button into a specific state
- */
- toggleMinimizeButton(newStatus?: boolean) {
- const minimizeButton = this.shadowRoot.querySelector('#minimize-button');
- if (!minimizeButton) return;
-
- if (newStatus !== undefined) {
- if (newStatus === true) {
- minimizeButton.removeAttribute('inert');
- minimizeButton.style.opacity = '1';
- } else {
- minimizeButton.setAttribute('inert', '');
- minimizeButton.style.opacity = '0';
- }
- } else {
- minimizeButton.toggleAttribute('inert');
- minimizeButton.style.opacity = minimizeButton.hasAttribute('inert') ? '0' : '1';
- }
+ isHidden(): boolean {
+ return this.devOverlay?.hasAttribute('data-hidden') ?? true;
+ }
+
+ getActivePlugin(): DevOverlayPlugin | undefined {
+ return this.plugins.find((plugin) => plugin.active);
+ }
+
+ clearDelayedHide() {
+ window.clearTimeout(this.delayedHideTimeout);
+ this.delayedHideTimeout = undefined;
+ }
+
+ triggerDelayedHide() {
+ this.clearDelayedHide();
+ this.delayedHideTimeout = window.setTimeout(() => {
+ this.setOverlayVisible(false);
+ this.delayedHideTimeout = undefined;
+ }, HOVER_DELAY);
}
- toggleOverlay(newStatus?: boolean) {
+ setOverlayVisible(newStatus: boolean) {
const barContainer = this.shadowRoot.querySelector('#bar-container');
const devBar = this.shadowRoot.querySelector('#dev-bar');
-
- if (newStatus !== undefined) {
- if (newStatus === true) {
- this.devOverlay?.removeAttribute('data-hidden');
- barContainer?.removeAttribute('inert');
- devBar?.removeAttribute('tabindex');
- } else {
- this.devOverlay?.setAttribute('data-hidden', '');
- barContainer?.setAttribute('inert', '');
- devBar?.setAttribute('tabindex', '0');
- }
- } else {
- this.devOverlay?.toggleAttribute('data-hidden');
- barContainer?.toggleAttribute('inert');
- if (this.isHidden()) {
- devBar?.setAttribute('tabindex', '0');
- } else {
- devBar?.removeAttribute('tabindex');
- }
+ if (newStatus === true) {
+ this.devOverlay?.removeAttribute('data-hidden');
+ barContainer?.removeAttribute('inert');
+ devBar?.removeAttribute('tabindex');
+ return;
}
+ if (newStatus === false) {
+ this.devOverlay?.setAttribute('data-hidden', '');
+ barContainer?.setAttribute('inert', '');
+ devBar?.setAttribute('tabindex', '0');
+ return;
+ }
+ }
+
+ setNotificationVisible(newStatus: boolean) {
+ const devOverlayElement = this.shadowRoot.querySelector('#dev-overlay');
+ devOverlayElement?.toggleAttribute('data-no-notification', !newStatus);
+
+ const moreCanvas = this.getPluginCanvasById('astro:more');
+ moreCanvas?.shadowRoot
+ ?.querySelector('#dropdown')
+ ?.toggleAttribute('data-no-notification', !newStatus);
}
}
@@ -550,3 +536,11 @@ export class DevOverlayCanvas extends HTMLElement {
`;
}
}
+
+export function getPluginIcon(icon: Icon) {
+ if (isDefinedIcon(icon)) {
+ return getIconElement(icon).outerHTML;
+ }
+
+ return icon;
+}
diff --git a/packages/astro/src/runtime/client/dev-overlay/plugins/astro.ts b/packages/astro/src/runtime/client/dev-overlay/plugins/astro.ts
index 352a018e11e22..d3f0d03e6789a 100644
--- a/packages/astro/src/runtime/client/dev-overlay/plugins/astro.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/plugins/astro.ts
@@ -1,19 +1,85 @@
-import type { DevOverlayPlugin } from '../../../../@types/astro.js';
-import { createWindowWithTransition, waitForTransition } from './utils/window.js';
+import type { DevOverlayMetadata, DevOverlayPlugin } from '../../../../@types/astro.js';
+import { isDefinedIcon, type Icon } from '../ui-library/icons.js';
+import { colorForIntegration, iconForIntegration } from './utils/icons.js';
+import { createWindowElement } from './utils/window.js';
+
+const astroLogo =
+ ' ';
+
+export interface Integration {
+ name: string;
+ title: string;
+ description: string;
+ image?: string;
+ categories: string[];
+ repoUrl: string;
+ npmUrl: string;
+ homepageUrl: string;
+ official: boolean;
+ featured: number;
+ downloads: number;
+}
+
+interface IntegrationData {
+ data: Integration[];
+}
+
+let integrationData: IntegrationData;
export default {
id: 'astro',
- name: 'Astro',
+ name: 'Menu',
icon: 'astro:logo',
- init(canvas) {
- createWindow();
+ async init(canvas, eventTarget) {
+ createCanvas();
+
+ document.addEventListener('astro:after-swap', createCanvas);
+
+ eventTarget.addEventListener('plugin-toggled', async (event) => {
+ resetDebugButton();
+ if (!(event instanceof CustomEvent)) return;
+
+ if (event.detail.state === true) {
+ if (!integrationData)
+ fetch('https://astro.build/api/v1/dev-overlay/', {
+ cache: 'no-cache',
+ })
+ .then((res) => res.json())
+ .then((data) => {
+ integrationData = data;
+ integrationData.data = integrationData.data.map((integration) => {
+ return integration;
+ });
+ refreshIntegrationList();
+ });
+ }
+ });
- document.addEventListener('astro:after-swap', createWindow);
+ function createCanvas() {
+ const links: { icon: Icon; name: string; link: string }[] = [
+ {
+ icon: 'bug',
+ name: 'Report a Bug',
+ link: 'https://github.com/withastro/astro/issues/new/choose',
+ },
+ {
+ icon: 'lightbulb',
+ name: 'Feedback',
+ link: 'https://github.com/withastro/roadmap/discussions/new/choose',
+ },
+ {
+ icon: 'file-search',
+ name: 'Documentation',
+ link: 'https://docs.astro.build',
+ },
+ {
+ icon: ' ',
+ name: 'Community',
+ link: 'https://astro.build/chat',
+ },
+ ];
- function createWindow() {
- const window = createWindowWithTransition(
- 'Astro',
- 'astro:logo',
+ const windowComponent = createWindowElement(
`
+
+
+ ${astroLogo}
+ ${
+ (window as DevOverlayMetadata).__astro_dev_overlay__.version
+ }
+
+ Copy debug info
+
+
+
-
Welcome to Astro!
-
+
+
-
+
`
);
- canvas.append(window);
+ const copyDebugButton =
+ windowComponent.querySelector('#copy-debug-button');
+
+ copyDebugButton?.addEventListener('click', () => {
+ navigator.clipboard.writeText(
+ '```\n' + (window as DevOverlayMetadata).__astro_dev_overlay__.debugInfo + '\n```'
+ );
+ copyDebugButton.textContent = 'Copied to clipboard!';
+
+ setTimeout(() => {
+ resetDebugButton();
+ }, 3500);
+ });
+
+ canvas.append(windowComponent);
+ }
+
+ function resetDebugButton() {
+ const copyDebugButton = canvas.querySelector('#copy-debug-button');
+ if (!copyDebugButton) return;
+
+ copyDebugButton.innerHTML = 'Copy debug info ';
+ }
+
+ function refreshIntegrationList() {
+ const integrationList = canvas.querySelector('#integration-list');
+
+ if (!integrationList) return;
+ integrationList.innerHTML = '';
+
+ const fragment = document.createDocumentFragment();
+ for (const integration of integrationData.data) {
+ const integrationComponent = document.createElement('astro-dev-toolbar-card');
+ integrationComponent.link = integration.homepageUrl;
+
+ const integrationContainer = document.createElement('div');
+ integrationContainer.className = 'integration-container';
+
+ const integrationImage = document.createElement('div');
+ integrationImage.className = 'integration-image';
+
+ if (integration.image) {
+ const img = document.createElement('img');
+ img.src = integration.image;
+ img.alt = integration.title;
+ integrationImage.append(img);
+ } else {
+ const icon = document.createElement('astro-dev-toolbar-icon');
+ icon.icon = iconForIntegration(integration);
+ integrationImage.append(icon);
+ integrationImage.style.setProperty(
+ '--integration-image-background',
+ colorForIntegration()
+ );
+ }
+
+ integrationContainer.append(integrationImage);
+
+ let integrationTitle = document.createElement('h3');
+ integrationTitle.textContent = integration.title;
+ if (integration.official || integration.categories.includes('official')) {
+ integrationTitle.innerHTML +=
+ ' ';
+ }
+ integrationContainer.append(integrationTitle);
+
+ const integrationDescription = document.createElement('p');
+ integrationDescription.textContent =
+ integration.description.length > 90
+ ? integration.description.slice(0, 90) + '…'
+ : integration.description;
+
+ integrationContainer.append(integrationDescription);
+ integrationComponent.append(integrationContainer);
+
+ fragment.append(integrationComponent);
+ }
+
+ integrationList.append(fragment);
}
- },
- async beforeTogglingOff(canvas) {
- return await waitForTransition(canvas);
},
} satisfies DevOverlayPlugin;
diff --git a/packages/astro/src/runtime/client/dev-overlay/plugins/audit.ts b/packages/astro/src/runtime/client/dev-overlay/plugins/audit.ts
deleted file mode 100644
index 6961e12273294..0000000000000
--- a/packages/astro/src/runtime/client/dev-overlay/plugins/audit.ts
+++ /dev/null
@@ -1,213 +0,0 @@
-import type { DevOverlayMetadata, DevOverlayPlugin } from '../../../../@types/astro.js';
-import type { DevOverlayHighlight } from '../ui-library/highlight.js';
-import { getIconElement } from '../ui-library/icons.js';
-import { attachTooltipToHighlight, createHighlight, positionHighlight } from './utils/highlight.js';
-
-const icon =
- ' ';
-
-interface AuditRule {
- title: string;
- message: string;
-}
-
-const selectorBasedRules: (AuditRule & { selector: string })[] = [
- {
- title: 'Missing `alt` tag',
- message: 'The alt attribute is important for accessibility.',
- selector: 'img:not([alt])',
- },
-];
-
-export default {
- id: 'astro:audit',
- name: 'Audit',
- icon: icon,
- async init(canvas, eventTarget) {
- let audits: { highlightElement: DevOverlayHighlight; auditedElement: HTMLElement }[] = [];
-
- await lint();
-
- document.addEventListener('astro:after-swap', async () => lint());
- document.addEventListener('astro:page-load', async () => refreshLintPositions);
-
- async function lint() {
- initStyle();
-
- audits.forEach(({ highlightElement }) => {
- highlightElement.remove();
- });
- audits = [];
- canvas.getElementById('no-audit')?.remove();
-
- for (const rule of selectorBasedRules) {
- const elements = document.querySelectorAll(rule.selector);
-
- for (const element of elements) {
- await createAuditProblem(rule, element);
- }
- }
-
- if (audits.length > 0) {
- eventTarget.dispatchEvent(
- new CustomEvent('toggle-notification', {
- detail: {
- state: true,
- },
- })
- );
- } else {
- eventTarget.dispatchEvent(
- new CustomEvent('toggle-notification', {
- detail: {
- state: false,
- },
- })
- );
-
- const noAuditBlock = document.createElement('div');
- noAuditBlock.id = 'no-audit';
-
- const noAuditIcon = getIconElement('check-circle');
- const text = document.createElement('div');
- text.textContent = 'No issues found!';
-
- if (noAuditIcon) {
- noAuditIcon.style.width = '24px';
- noAuditBlock.append(noAuditIcon);
- }
- noAuditBlock.append(text);
-
- canvas.append(noAuditBlock);
- }
-
- (['scroll', 'resize'] as const).forEach((event) => {
- window.addEventListener(event, refreshLintPositions);
- });
- }
-
- function refreshLintPositions() {
- const noAuditBlock = canvas.getElementById('no-audit');
- if (noAuditBlock) {
- const devOverlayRect = document
- .querySelector('astro-dev-overlay')
- ?.shadowRoot.querySelector('#dev-overlay')
- ?.getBoundingClientRect();
-
- noAuditBlock.style.top = `${
- (devOverlayRect?.top ?? 0) - (devOverlayRect?.height ?? 0) - 16
- }px`;
- }
-
- audits.forEach(({ highlightElement, auditedElement }) => {
- const rect = auditedElement.getBoundingClientRect();
- positionHighlight(highlightElement, rect);
- });
- }
-
- async function createAuditProblem(rule: AuditRule, originalElement: Element) {
- const computedStyle = window.getComputedStyle(originalElement);
- const targetedElement = (originalElement.children[0] as HTMLElement) || originalElement;
-
- // If the element is hidden, don't do anything
- if (targetedElement.offsetParent === null || computedStyle.display === 'none') {
- return;
- }
-
- // If the element is an image, wait for it to load
- if (originalElement.nodeName === 'IMG' && !(originalElement as HTMLImageElement).complete) {
- await (originalElement as HTMLImageElement).decode();
- }
-
- const rect = originalElement.getBoundingClientRect();
- const highlight = createHighlight(rect, 'warning');
- const tooltip = buildAuditTooltip(rule, originalElement);
- attachTooltipToHighlight(highlight, tooltip, originalElement);
-
- canvas.append(highlight);
- audits.push({ highlightElement: highlight, auditedElement: originalElement as HTMLElement });
- }
-
- function buildAuditTooltip(rule: AuditRule, element: Element) {
- const tooltip = document.createElement('astro-dev-overlay-tooltip');
-
- tooltip.sections = [
- {
- icon: 'warning',
- title: rule.title,
- },
- {
- content: rule.message,
- },
- ];
-
- const elementFile = element.getAttribute('data-astro-source-file');
- const elementPosition = element.getAttribute('data-astro-source-loc');
-
- if (elementFile) {
- const elementFileWithPosition =
- elementFile + (elementPosition ? ':' + elementPosition : '');
-
- tooltip.sections.push({
- content: elementFileWithPosition.slice(
- (window as DevOverlayMetadata).__astro_dev_overlay__.root.length - 1 // We want to keep the final slash, so minus one.
- ),
- clickDescription: 'Click to go to file',
- async clickAction() {
- // NOTE: The path here has to be absolute and without any errors (no double slashes etc)
- // or Vite will silently fail to open the file. Quite annoying.
- await fetch('/__open-in-editor?file=' + encodeURIComponent(elementFileWithPosition));
- },
- });
- }
-
- return tooltip;
- }
-
- function initStyle() {
- const devOverlayRect = document
- .querySelector('astro-dev-overlay')
- ?.shadowRoot.querySelector('#dev-overlay')
- ?.getBoundingClientRect();
-
- const style = document.createElement('style');
- style.textContent = `
- :host {
- opacity: 0;
- transition: opacity 0.1s ease-in-out;
- }
-
- :host([data-active]) {
- opacity: 1;
- }
-
- #no-audit {
- border: 1px solid rgba(113, 24, 226, 1);
- background-color: #310A65;
- box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.30), 0px 1px 2px 0px rgba(0, 0, 0, 0.29), 0px 4px 4px 0px rgba(0, 0, 0, 0.26), 0px 10px 6px 0px rgba(0, 0, 0, 0.15), 0px 17px 7px 0px rgba(0, 0, 0, 0.04), 0px 26px 7px 0px rgba(0, 0, 0, 0.01);
- color: white;
- text-align: center;
- border-radius: 4px;
- padding: 8px;
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
- position: fixed;
- transform: translate(-50%, 0);
- top: ${(devOverlayRect?.top ?? 0) - (devOverlayRect?.height ?? 0) - 16}px;
- left: calc(50% + 12px);
- width: 200px;
- }
- `;
-
- canvas.append(style);
- }
- },
- async beforeTogglingOff(canvas) {
- canvas.host?.removeAttribute('data-active');
-
- await new Promise((resolve) => {
- canvas.host.addEventListener('transitionend', resolve);
- });
-
- return true;
- },
-} satisfies DevOverlayPlugin;
diff --git a/packages/astro/src/runtime/client/dev-overlay/plugins/audit/a11y.ts b/packages/astro/src/runtime/client/dev-overlay/plugins/audit/a11y.ts
new file mode 100644
index 0000000000000..a5e6e5073ad58
--- /dev/null
+++ b/packages/astro/src/runtime/client/dev-overlay/plugins/audit/a11y.ts
@@ -0,0 +1,481 @@
+/**
+ * https://github.com/sveltejs/svelte/blob/61e5e53eee82e895c1a5b4fd36efb87eafa1fc2d/LICENSE.md
+ * @license MIT
+ *
+ * Copyright (c) 2016-23 [these people](https://github.com/sveltejs/svelte/graphs/contributors)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import type { AuditRuleWithSelector } from './index.js';
+
+const a11y_required_attributes = {
+ a: ['href'],
+ area: ['alt', 'aria-label', 'aria-labelledby'],
+ // html-has-lang
+ html: ['lang'],
+ // iframe-has-title
+ iframe: ['title'],
+ img: ['alt'],
+ object: ['title', 'aria-label', 'aria-labelledby'],
+};
+
+const interactiveElements = ['button', 'details', 'embed', 'iframe', 'label', 'select', 'textarea'];
+
+const aria_non_interactive_roles = [
+ 'alert',
+ 'alertdialog',
+ 'application',
+ 'article',
+ 'banner',
+ 'button',
+ 'cell',
+ 'checkbox',
+ 'columnheader',
+ 'combobox',
+ 'complementary',
+ 'contentinfo',
+ 'definition',
+ 'dialog',
+ 'directory',
+ 'document',
+ 'feed',
+ 'figure',
+ 'form',
+ 'grid',
+ 'gridcell',
+ 'group',
+ 'heading',
+ 'img',
+ 'link',
+ 'list',
+ 'listbox',
+ 'listitem',
+ 'log',
+ 'main',
+ 'marquee',
+ 'math',
+ 'menu',
+ 'menubar',
+ 'menuitem',
+ 'menuitemcheckbox',
+ 'menuitemradio',
+ 'navigation',
+ 'none',
+ 'note',
+ 'option',
+ 'presentation',
+ 'progressbar',
+ 'radio',
+ 'radiogroup',
+ 'region',
+ 'row',
+ 'rowgroup',
+ 'rowheader',
+ 'scrollbar',
+ 'search',
+ 'searchbox',
+ 'separator',
+ 'slider',
+ 'spinbutton',
+ 'status',
+ 'switch',
+ 'tab',
+ 'tablist',
+ 'tabpanel',
+ 'term',
+ 'textbox',
+ 'timer',
+ 'toolbar',
+ 'tooltip',
+ 'tree',
+ 'treegrid',
+ 'treeitem',
+];
+
+const a11y_required_content = [
+ // anchor-has-content
+ 'a',
+ // heading-has-content
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+];
+
+const a11y_distracting_elements = ['blink', 'marquee'];
+
+const a11y_nested_implicit_semantics = new Map([
+ ['header', 'banner'],
+ ['footer', 'contentinfo'],
+]);
+const a11y_implicit_semantics = new Map([
+ ['a', 'link'],
+ ['area', 'link'],
+ ['article', 'article'],
+ ['aside', 'complementary'],
+ ['body', 'document'],
+ ['button', 'button'],
+ ['datalist', 'listbox'],
+ ['dd', 'definition'],
+ ['dfn', 'term'],
+ ['dialog', 'dialog'],
+ ['details', 'group'],
+ ['dt', 'term'],
+ ['fieldset', 'group'],
+ ['figure', 'figure'],
+ ['form', 'form'],
+ ['h1', 'heading'],
+ ['h2', 'heading'],
+ ['h3', 'heading'],
+ ['h4', 'heading'],
+ ['h5', 'heading'],
+ ['h6', 'heading'],
+ ['hr', 'separator'],
+ ['img', 'img'],
+ ['li', 'listitem'],
+ ['link', 'link'],
+ ['main', 'main'],
+ ['menu', 'list'],
+ ['meter', 'progressbar'],
+ ['nav', 'navigation'],
+ ['ol', 'list'],
+ ['option', 'option'],
+ ['optgroup', 'group'],
+ ['output', 'status'],
+ ['progress', 'progressbar'],
+ ['section', 'region'],
+ ['summary', 'button'],
+ ['table', 'table'],
+ ['tbody', 'rowgroup'],
+ ['textarea', 'textbox'],
+ ['tfoot', 'rowgroup'],
+ ['thead', 'rowgroup'],
+ ['tr', 'row'],
+ ['ul', 'list'],
+]);
+const menuitem_type_to_implicit_role = new Map([
+ ['command', 'menuitem'],
+ ['checkbox', 'menuitemcheckbox'],
+ ['radio', 'menuitemradio'],
+]);
+const input_type_to_implicit_role = new Map([
+ ['button', 'button'],
+ ['image', 'button'],
+ ['reset', 'button'],
+ ['submit', 'button'],
+ ['checkbox', 'checkbox'],
+ ['radio', 'radio'],
+ ['range', 'slider'],
+ ['number', 'spinbutton'],
+ ['email', 'textbox'],
+ ['search', 'searchbox'],
+ ['tel', 'textbox'],
+ ['text', 'textbox'],
+ ['url', 'textbox'],
+]);
+
+const ariaAttributes = new Set(
+ 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(
+ ' '
+ )
+);
+
+const ariaRoles = new Set(
+ 'alert alertdialog application article banner button cell checkbox columnheader combobox complementary contentinfo definition dialog directory document feed figure form grid gridcell group heading img link list listbox listitem log main marquee math menu menubar menuitem menuitemcheckbox menuitemradio navigation none note option presentation progressbar radio radiogroup region row rowgroup rowheader scrollbar search searchbox separator slider spinbutton status tab tablist tabpanel textbox timer toolbar tooltip tree treegrid treeitem'.split(
+ ' '
+ )
+);
+
+export const a11y: AuditRuleWithSelector[] = [
+ {
+ code: 'a11y-accesskey',
+ title: 'Avoid using `accesskey`',
+ message:
+ "The `accesskey` attribute can cause accessibility issues. The shortcuts can conflict with the browser's or operating system's shortcuts, and they are difficult for users to discover and use.",
+ selector: '[accesskey]',
+ },
+ {
+ code: 'a11y-aria-activedescendant-has-tabindex',
+ title: 'Elements with attribute `aria-activedescendant` must be tabbable',
+ message:
+ 'This element must either have an inherent `tabindex` or declare `tabindex` as an attribute.',
+ selector: '[aria-activedescendant]',
+ match(element) {
+ if (!(element as HTMLElement).tabIndex && !element.hasAttribute('tabindex')) return true;
+ },
+ },
+ {
+ code: 'a11y-aria-attributes',
+ title: 'Element does not support ARIA roles.',
+ message: 'Elements like `meta`, `html`, `script`, `style` do not support having ARIA roles.',
+ selector: ':is(meta, html, script, style)[role]',
+ match(element) {
+ for (const attribute of element.attributes) {
+ if (attribute.name.startsWith('aria-')) return true;
+ }
+ },
+ },
+ {
+ code: 'a11y-autofocus',
+ title: 'Avoid using `autofocus`',
+ message:
+ 'The `autofocus` attribute can cause accessibility issues, as it can cause the focus to move around unexpectedly for screen reader users.',
+ selector: '[autofocus]',
+ },
+ {
+ code: 'a11y-distracting-elements',
+ title: 'Distracting elements should not be used',
+ message:
+ 'Elements that can be visually distracting like `` or `` can cause accessibility issues for visually impaired users and should be avoided.',
+ selector: `:is(${a11y_distracting_elements.join(',')})`,
+ },
+ {
+ code: 'a11y-hidden',
+ title: 'Certain DOM elements are useful for screen reader navigation and should not be hidden',
+ message: (element) => `${element.localName} element should not be hidden.`,
+ selector: '[aria-hidden]:is(h1,h2,h3,h4,h5,h6)',
+ },
+ {
+ code: 'a11y-img-redundant-alt',
+ title: 'Redundant text in alt attribute',
+ message:
+ 'Screen readers already announce `img` elements as an image. There is no need to use words such as "image", "photo", and/or "picture".',
+ selector: 'img[alt]:not([aria-hidden])',
+ match: (img: HTMLImageElement) => /\b(image|picture|photo)\b/i.test(img.alt),
+ },
+ {
+ code: 'a11y-incorrect-aria-attribute-type',
+ title: 'Incorrect value for ARIA attribute.',
+ message: '`aria-hidden` should only receive a boolean.',
+ selector: '[aria-hidden]',
+ match(element) {
+ const value = element.getAttribute('aria-hidden');
+ if (!value) return true;
+ if (!['true', 'false'].includes(value)) return true;
+ },
+ },
+ {
+ code: 'a11y-invalid-attribute',
+ title: 'Attributes important for accessibility should have a valid value',
+ message: "`href` should not be empty, `'#'`, or `javascript:`.",
+ selector: 'a[href]:is([href=""], [href="#"], [href^="javascript:" i])',
+ },
+ {
+ code: 'a11y-label-has-associated-control',
+ title: '`label` tag should have an associated control and a text content.',
+ message:
+ 'The `label` tag must be associated with a control using either `for` or having a nested input. Additionally, the `label` tag must have text content.',
+ selector: 'label:not([for])',
+ match(element) {
+ const inputChild = element.querySelector('input');
+ if (!inputChild?.textContent) return true;
+ },
+ },
+ {
+ code: 'a11y-media-has-caption',
+ title: 'Unmuted video elements should have captions',
+ message:
+ 'Videos without captions can be difficult for deaf and hard-of-hearing users to follow along with. If the video does not need captions, add the `muted` attribute.',
+ selector: 'video:not([muted])',
+ match(element) {
+ const tracks = element.querySelectorAll('track');
+ if (!tracks.length) return true;
+
+ const hasCaptionTrack = Array.from(tracks).some(
+ (track) => track.getAttribute('kind') === 'captions'
+ );
+
+ return !hasCaptionTrack;
+ },
+ },
+ {
+ code: 'a11y-misplaced-scope',
+ title: 'The `scope` attribute should only be used on `` elements',
+ message:
+ 'The `scope` attribute tells the browser and screen readers how to navigate tables. In HTML5, it should only be used on ` ` elements.',
+ selector: ':not(th)[scope]',
+ },
+ {
+ code: 'a11y-missing-attribute',
+ title: 'Required attributes missing.',
+ message: (element) => {
+ const requiredAttributes =
+ a11y_required_attributes[element.localName as keyof typeof a11y_required_attributes];
+
+ const missingAttributes = requiredAttributes.filter(
+ (attribute) => !element.hasAttribute(attribute)
+ );
+
+ return `${
+ element.localName
+ } element is missing required attributes for accessibility: ${missingAttributes.join(', ')} `;
+ },
+ selector: Object.keys(a11y_required_attributes).join(','),
+ match(element) {
+ const requiredAttributes =
+ a11y_required_attributes[element.localName as keyof typeof a11y_required_attributes];
+
+ if (!requiredAttributes) return true;
+ for (const attribute of requiredAttributes) {
+ if (!element.hasAttribute(attribute)) return true;
+ }
+
+ return false;
+ },
+ },
+ {
+ code: 'a11y-missing-content',
+ title: 'Missing content on element important for accessibility',
+ message: 'Headings and anchors must have content to be accessible.',
+ selector: a11y_required_content.join(','),
+ match(element) {
+ if (!element.textContent) return true;
+ },
+ },
+ {
+ code: 'a11y-no-redundant-roles',
+ title: 'HTML element has redundant ARIA roles',
+ message:
+ 'Giving these elements an ARIA role that is already set by the browser has no effect and is redundant.',
+ selector: [...a11y_implicit_semantics.keys()].join(','),
+ match(element) {
+ const role = element.getAttribute('role');
+
+ if (element.localName === 'input') {
+ const type = element.getAttribute('type');
+ if (!type) return true;
+
+ const implicitRoleForType = input_type_to_implicit_role.get(type);
+ if (!implicitRoleForType) return true;
+
+ if (role === implicitRoleForType) return false;
+ }
+
+ // TODO: Handle menuitem and elements that inherit their role from their parent
+
+ const implicitRole = a11y_implicit_semantics.get(element.localName);
+ if (!implicitRole) return true;
+
+ if (role === implicitRole) return false;
+ },
+ },
+ {
+ code: 'a11y-no-interactive-element-to-noninteractive-role',
+ title: 'Non-interactive ARIA role used on interactive HTML element.',
+ message:
+ 'Interactive HTML elements like `` and `` cannot use non-interactive roles like `heading`, `list`, `menu`, and `toolbar`.',
+ selector: `[role]:is(${interactiveElements.join(',')})`,
+ match(element) {
+ const role = element.getAttribute('role');
+ if (!role) return false;
+ if (!ariaRoles.has(role)) return false;
+
+ if (aria_non_interactive_roles.includes(role)) return true;
+ },
+ },
+ {
+ code: 'a11y-no-noninteractive-element-to-interactive-role',
+ title: 'Interactive ARIA role used on non-interactive HTML element.',
+ message:
+ 'Interactive roles should not be used to convert a non-interactive element to an interactive element',
+ selector: `[role]:not(${interactiveElements.join(',')})`,
+ match(element) {
+ const role = element.getAttribute('role');
+ if (!role) return false;
+ if (!ariaRoles.has(role)) return false;
+ const exceptions =
+ a11y_non_interactive_element_to_interactive_role_exceptions[
+ element.localName as keyof typeof a11y_non_interactive_element_to_interactive_role_exceptions
+ ];
+ if (exceptions?.includes(role)) return false;
+
+ if (!aria_non_interactive_roles.includes(role)) return true;
+ },
+ },
+ {
+ code: 'a11y-no-noninteractive-tabindex',
+ title: 'Invalid `tabindex` on non-interactive element',
+ message: (element) => `${element.localName} elements should not have \`tabindex\` attribute`,
+ selector: '[tabindex]',
+ match(element) {
+ // Scrollable elements are considered interactive
+ // See: https://www.w3.org/WAI/standards-guidelines/act/rules/0ssw9k/proposed/
+ const isScrollable =
+ element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth;
+ if (isScrollable) return false;
+
+ if (!interactiveElements.includes(element.localName)) return true;
+ },
+ },
+ {
+ code: 'a11y-positive-tabindex',
+ title: 'Avoid positive `tabindex` property values',
+ message:
+ 'This will move elements out of the expected tab order, creating a confusing experience for keyboard users.',
+ selector: '[tabindex]:not([tabindex="-1"]):not([tabindex="0"])',
+ },
+ {
+ code: 'a11y-structure',
+ title: 'Invalid DOM structure',
+ message:
+ 'The DOM structure must be valid for accessibility of the page, for example `figcaption` must be a direct child of `figure`.',
+ selector: 'figcaption:not(figure > figcaption)',
+ },
+ {
+ code: 'a11y-unknown-aria-attribute',
+ title: 'Unknown ARIA attribute',
+ message: 'ARIA attributes prefixed with `aria-` must be valid, non-abstract ARIA attributes.',
+ selector: '*',
+ match(element) {
+ for (const attribute of element.attributes) {
+ if (attribute.name.startsWith('aria-')) {
+ if (!ariaAttributes.has(attribute.name.slice('aria-'.length))) return true;
+ }
+ }
+ },
+ },
+ {
+ code: 'a11y-unknown-role',
+ title: 'Unknown ARIA role',
+ message: 'ARIA roles must be valid, non-abstract ARIA roles.',
+ selector: '[role]',
+ match(element) {
+ const role = element.getAttribute('role');
+ if (!role) return true;
+ if (!ariaRoles.has(role)) return true;
+ },
+ },
+];
+
+/**
+ * Exceptions to the rule which follows common A11y conventions
+ * TODO make this configurable by the user
+ * @type {Record}
+ */
+const a11y_non_interactive_element_to_interactive_role_exceptions = {
+ ul: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
+ ol: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
+ li: ['menuitem', 'option', 'row', 'tab', 'treeitem'],
+ table: ['grid'],
+ td: ['gridcell'],
+ fieldset: ['radiogroup', 'presentation'],
+};
diff --git a/packages/astro/src/runtime/client/dev-overlay/plugins/audit/index.ts b/packages/astro/src/runtime/client/dev-overlay/plugins/audit/index.ts
new file mode 100644
index 0000000000000..175fe0ab66c1d
--- /dev/null
+++ b/packages/astro/src/runtime/client/dev-overlay/plugins/audit/index.ts
@@ -0,0 +1,266 @@
+import type { DevOverlayMetadata, DevOverlayPlugin } from '../../../../../@types/astro.js';
+import type { DevOverlayHighlight } from '../../ui-library/highlight.js';
+import {
+ attachTooltipToHighlight,
+ createHighlight,
+ getElementsPositionInDocument,
+ positionHighlight,
+} from '../utils/highlight.js';
+import { createWindowElement } from '../utils/window.js';
+import { a11y } from './a11y.js';
+
+const icon =
+ ' ';
+
+type DynamicString = string | ((element: Element) => string);
+
+export interface AuditRule {
+ code: string;
+ title: DynamicString;
+ message: DynamicString;
+}
+
+export interface ResolvedAuditRule {
+ code: string;
+ title: string;
+ message: string;
+}
+
+export interface AuditRuleWithSelector extends AuditRule {
+ selector: string;
+ match?: (element: Element) => boolean | null | undefined | void;
+}
+
+const rules = [...a11y];
+
+const dynamicAuditRuleKeys: Array = ['title', 'message'];
+function resolveAuditRule(rule: AuditRule, element: Element): ResolvedAuditRule {
+ let resolved: ResolvedAuditRule = { ...rule } as any;
+ for (const key of dynamicAuditRuleKeys) {
+ const value = rule[key];
+ if (typeof value === 'string') continue;
+ resolved[key] = value(element);
+ }
+ return resolved;
+}
+
+export default {
+ id: 'astro:audit',
+ name: 'Audit',
+ icon: icon,
+ async init(canvas, eventTarget) {
+ let audits: { highlightElement: DevOverlayHighlight; auditedElement: HTMLElement }[] = [];
+
+ await lint();
+
+ document.addEventListener('astro:after-swap', async () => lint());
+ document.addEventListener('astro:page-load', async () => refreshLintPositions);
+
+ function onPageClick(event: MouseEvent) {
+ const target = event.target as Element | null;
+ if (!target) return;
+ if (!target.closest) return;
+ if (target.closest('astro-dev-toolbar')) return;
+ eventTarget.dispatchEvent(
+ new CustomEvent('toggle-plugin', {
+ detail: {
+ state: false,
+ },
+ })
+ );
+ }
+ eventTarget.addEventListener('plugin-toggled', (event: any) => {
+ if (event.detail.state === true) {
+ document.addEventListener('click', onPageClick, true);
+ } else {
+ document.removeEventListener('click', onPageClick, true);
+ }
+ });
+
+ async function lint() {
+ audits.forEach(({ highlightElement }) => {
+ highlightElement.remove();
+ });
+ audits = [];
+ canvas.getElementById('no-audit')?.remove();
+ const selectorCache = new Map>();
+
+ for (const rule of rules) {
+ const elements =
+ selectorCache.get(rule.selector) ?? document.querySelectorAll(rule.selector);
+ let matches: Element[] = [];
+ if (typeof rule.match === 'undefined') {
+ matches = Array.from(elements);
+ } else {
+ for (const element of elements) {
+ if (rule.match(element)) {
+ matches.push(element);
+ }
+ }
+ }
+ for (const element of matches) {
+ await createAuditProblem(rule, element);
+ }
+ }
+
+ if (audits.length > 0) {
+ eventTarget.dispatchEvent(
+ new CustomEvent('toggle-notification', {
+ detail: {
+ state: true,
+ },
+ })
+ );
+ } else {
+ eventTarget.dispatchEvent(
+ new CustomEvent('toggle-notification', {
+ detail: {
+ state: false,
+ },
+ })
+ );
+
+ const window = createWindowElement(
+ `
+
+ No accessibility issues detected.
+
+
+ Nice work! This app scans the page and highlights common accessibility issues for you, like a missing "alt" attribute on an image.
+
+ `
+ );
+
+ canvas.append(window);
+ }
+
+ (['scroll', 'resize'] as const).forEach((event) => {
+ window.addEventListener(event, refreshLintPositions);
+ });
+ }
+
+ function refreshLintPositions() {
+ const noAuditBlock = canvas.getElementById('no-audit');
+ if (noAuditBlock) {
+ const devOverlayRect = document
+ .querySelector('astro-dev-toolbar')
+ ?.shadowRoot.querySelector('#dev-overlay')
+ ?.getBoundingClientRect();
+
+ noAuditBlock.style.top = `${
+ (devOverlayRect?.top ?? 0) - (devOverlayRect?.height ?? 0) - 16
+ }px`;
+ }
+
+ audits.forEach(({ highlightElement, auditedElement }) => {
+ const rect = auditedElement.getBoundingClientRect();
+ positionHighlight(highlightElement, rect);
+ });
+ }
+
+ async function createAuditProblem(rule: AuditRule, originalElement: Element) {
+ const computedStyle = window.getComputedStyle(originalElement);
+ const targetedElement = (originalElement.children[0] as HTMLElement) || originalElement;
+
+ // If the element is hidden, don't do anything
+ if (targetedElement.offsetParent === null || computedStyle.display === 'none') {
+ return;
+ }
+
+ // If the element is an image but not yet loaded, ignore it
+ // TODO: We shouldn't ignore this, because it is valid for an image to not be loaded at start (e.g. lazy loading)
+ if (originalElement.nodeName === 'IMG' && !(originalElement as HTMLImageElement).complete) {
+ return;
+ }
+
+ const rect = originalElement.getBoundingClientRect();
+ const highlight = createHighlight(rect, 'warning');
+ const tooltip = buildAuditTooltip(rule, originalElement);
+
+ // Set the highlight/tooltip as being fixed position the highlighted element
+ // is fixed. We do this so that we don't mistakenly take scroll position
+ // into account when setting the tooltip/highlight positioning.
+ //
+ // We only do this once due to how expensive computed styles are to calculate,
+ // and are unlikely to change. If that turns out to be wrong, reconsider this.
+ const { isFixed } = getElementsPositionInDocument(originalElement);
+ if (isFixed) {
+ tooltip.style.position = highlight.style.position = 'fixed';
+ }
+
+ attachTooltipToHighlight(highlight, tooltip, originalElement);
+
+ canvas.append(highlight);
+ audits.push({ highlightElement: highlight, auditedElement: originalElement as HTMLElement });
+ }
+
+ function buildAuditTooltip(rule: AuditRule, element: Element) {
+ const tooltip = document.createElement('astro-dev-toolbar-tooltip');
+ const { title, message } = resolveAuditRule(rule, element);
+
+ tooltip.sections = [
+ {
+ icon: 'warning',
+ title: escapeHtml(title),
+ },
+ {
+ content: escapeHtml(message),
+ },
+ ];
+
+ const elementFile = element.getAttribute('data-astro-source-file');
+ const elementPosition = element.getAttribute('data-astro-source-loc');
+
+ if (elementFile) {
+ const elementFileWithPosition =
+ elementFile + (elementPosition ? ':' + elementPosition : '');
+
+ tooltip.sections.push({
+ content: elementFileWithPosition.slice(
+ (window as DevOverlayMetadata).__astro_dev_overlay__.root.length - 1 // We want to keep the final slash, so minus one.
+ ),
+ clickDescription: 'Click to go to file',
+ async clickAction() {
+ // NOTE: The path here has to be absolute and without any errors (no double slashes etc)
+ // or Vite will silently fail to open the file. Quite annoying.
+ await fetch('/__open-in-editor?file=' + encodeURIComponent(elementFileWithPosition));
+ },
+ });
+ }
+
+ return tooltip;
+ }
+
+ function escapeHtml(unsafe: string) {
+ return unsafe
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ }
+ },
+} satisfies DevOverlayPlugin;
diff --git a/packages/astro/src/runtime/client/dev-overlay/plugins/settings.ts b/packages/astro/src/runtime/client/dev-overlay/plugins/settings.ts
index e0d3384463ef3..dc5fe4ae3637c 100644
--- a/packages/astro/src/runtime/client/dev-overlay/plugins/settings.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/plugins/settings.ts
@@ -1,6 +1,6 @@
import type { DevOverlayPlugin } from '../../../../@types/astro.js';
import { settings, type Settings } from '../settings.js';
-import { createWindowWithTransition, waitForTransition } from './utils/window.js';
+import { createWindowElement } from './utils/window.js';
interface SettingRow {
name: string;
@@ -13,23 +13,33 @@ interface SettingRow {
const settingsRows = [
{
name: 'Disable notifications',
- description: 'Notification bubbles will not be shown when this is enabled.',
+ description: 'Hide notification badges in the toolbar.',
input: 'checkbox',
settingKey: 'disablePluginNotification',
changeEvent: (evt: Event) => {
if (evt.currentTarget instanceof HTMLInputElement) {
+ const devOverlay = document.querySelector('astro-dev-toolbar');
+
+ if (devOverlay) {
+ devOverlay.setNotificationVisible(!evt.currentTarget.checked);
+ }
+
settings.updateSetting('disablePluginNotification', evt.currentTarget.checked);
+ const action = evt.currentTarget.checked ? 'disabled' : 'enabled';
+ settings.log(`Plugin notification badges ${action}`);
}
},
},
{
name: 'Verbose logging',
- description: 'Log additional information to the console.',
+ description: 'Logs dev overlay events in the browser console.',
input: 'checkbox',
settingKey: 'verbose',
changeEvent: (evt: Event) => {
if (evt.currentTarget instanceof HTMLInputElement) {
settings.updateSetting('verbose', evt.currentTarget.checked);
+ const action = evt.currentTarget.checked ? 'enabled' : 'disabled';
+ settings.log(`Verbose logging ${action}`);
}
},
},
@@ -37,7 +47,7 @@ const settingsRows = [
export default {
id: 'astro:settings',
- name: 'Overlay settings',
+ name: 'Settings',
icon: 'gear',
init(canvas) {
createSettingsWindow();
@@ -45,10 +55,17 @@ export default {
document.addEventListener('astro:after-swap', createSettingsWindow);
function createSettingsWindow() {
- const window = createWindowWithTransition(
- 'Settings',
- 'gear',
+ const windowElement = createWindowElement(
`
- General
- `,
- settingsRows.flatMap((setting) => [
- getElementForSettingAsString(setting),
- document.createElement('hr'),
- ])
+
+
+
+
+
+
+ Hide toolbar
+ Run astro preferences disable devToolbar
in your terminal to disable the toolbar. Learn more .
+
+
+ `
);
- canvas.append(window);
+ const general = windowElement.querySelector('#general')!;
+ for (const settingsRow of settingsRows) {
+ general.after(document.createElement('hr'));
+ general.after(getElementForSettingAsString(settingsRow));
+ }
+ canvas.append(windowElement);
function getElementForSettingAsString(setting: SettingRow) {
const label = document.createElement('label');
@@ -89,7 +157,7 @@ export default {
switch (setting.input) {
case 'checkbox': {
- const astroToggle = document.createElement('astro-dev-overlay-toggle');
+ const astroToggle = document.createElement('astro-dev-toolbar-toggle');
astroToggle.input.addEventListener('change', setting.changeEvent);
astroToggle.input.checked = settings.config[setting.settingKey];
label.append(astroToggle);
@@ -100,7 +168,4 @@ export default {
}
}
},
- async beforeTogglingOff(canvas) {
- return await waitForTransition(canvas);
- },
} satisfies DevOverlayPlugin;
diff --git a/packages/astro/src/runtime/client/dev-overlay/plugins/utils/highlight.ts b/packages/astro/src/runtime/client/dev-overlay/plugins/utils/highlight.ts
index 3af467ecdc15b..6ad37aad2af24 100644
--- a/packages/astro/src/runtime/client/dev-overlay/plugins/utils/highlight.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/plugins/utils/highlight.ts
@@ -2,7 +2,7 @@ import type { DevOverlayHighlight } from '../../ui-library/highlight.js';
import type { Icon } from '../../ui-library/icons.js';
export function createHighlight(rect: DOMRect, icon?: Icon) {
- const highlight = document.createElement('astro-dev-overlay-highlight');
+ const highlight = document.createElement('astro-dev-toolbar-highlight');
if (icon) highlight.icon = icon;
highlight.tabIndex = 0;
@@ -15,10 +15,30 @@ export function createHighlight(rect: DOMRect, icon?: Icon) {
return highlight;
}
+// Figures out the element's position, based on it's parents.
+export function getElementsPositionInDocument(el: Element) {
+ let isFixed = false;
+ let current: Element | ParentNode | null = el;
+ while (current instanceof Element) {
+ // all the way up the tree. We are only doing so when the app initializes, so the cost is one-time
+ // If perf becomes an issue we'll want to refactor this somehow so that it reads this info in a rAF
+ let style = getComputedStyle(current);
+ if (style.position === 'fixed') {
+ isFixed = true;
+ }
+ current = current.parentNode;
+ }
+ return {
+ isFixed,
+ };
+}
+
export function positionHighlight(highlight: DevOverlayHighlight, rect: DOMRect) {
highlight.style.display = 'block';
+ // If the highlight is fixed, don't position based on scroll
+ const scrollY = highlight.style.position === 'fixed' ? 0 : window.scrollY;
// Make an highlight that is 10px bigger than the element on all sides
- highlight.style.top = `${Math.max(rect.top + window.scrollY - 10, 0)}px`;
+ highlight.style.top = `${Math.max(rect.top + scrollY - 10, 0)}px`;
highlight.style.left = `${Math.max(rect.left + window.scrollX - 10, 0)}px`;
highlight.style.width = `${rect.width + 15}px`;
highlight.style.height = `${rect.height + 15}px`;
diff --git a/packages/astro/src/runtime/client/dev-overlay/plugins/utils/icons.ts b/packages/astro/src/runtime/client/dev-overlay/plugins/utils/icons.ts
new file mode 100644
index 0000000000000..ec5a34ad89a10
--- /dev/null
+++ b/packages/astro/src/runtime/client/dev-overlay/plugins/utils/icons.ts
@@ -0,0 +1,43 @@
+import type { Integration } from '../astro.js';
+
+function randomFromArray(list: T[]) {
+ return list[Math.floor(Math.random() * list.length)];
+}
+
+const categoryIcons = new Map(
+ Object.entries({
+ frameworks: ['puzzle', 'grid'],
+ adapters: ['puzzle', 'grid', 'compress'],
+ 'css+ui': ['compress', 'grid', 'image', 'resizeImage', 'puzzle'],
+ 'performance+seo': ['approveUser', 'checkCircle', 'compress', 'robot', 'searchFile', 'sitemap'],
+ analytics: ['checkCircle', 'compress', 'searchFile'],
+ accessibility: ['approveUser', 'checkCircle'],
+ other: ['checkCircle', 'grid', 'puzzle', 'sitemap'],
+ })
+);
+
+export function iconForIntegration(integration: Integration) {
+ const icons = integration.categories
+ .filter((category: string) => categoryIcons.has(category))
+ .map((category: string) => categoryIcons.get(category)!)
+ .flat();
+
+ return randomFromArray(icons);
+}
+
+const iconColors = [
+ '#BC52EE',
+ '#6D6AF0',
+ '#52EEBD',
+ '#52B7EE',
+ '#52EE55',
+ '#B7EE52',
+ '#EEBD52',
+ '#EE5552',
+ '#EE52B7',
+ '#858B98',
+];
+
+export function colorForIntegration() {
+ return randomFromArray(iconColors);
+}
diff --git a/packages/astro/src/runtime/client/dev-overlay/plugins/utils/window.ts b/packages/astro/src/runtime/client/dev-overlay/plugins/utils/window.ts
index 04f09d6e64735..fb107e19c8027 100644
--- a/packages/astro/src/runtime/client/dev-overlay/plugins/utils/window.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/plugins/utils/window.ts
@@ -1,56 +1,5 @@
-import type { Icon } from '../../ui-library/icons.js';
-
-export function createWindowWithTransition(
- title: string,
- icon: Icon,
- windowContent: string,
- addedNodes: Node[] = []
-): DocumentFragment {
- const fragment = document.createDocumentFragment();
-
- const style = document.createElement('style');
- style.textContent = `
- :host {
- opacity: 0;
- transition: opacity 0.15s ease-in-out;
- }
-
- :host([data-active]) {
- opacity: 1;
- }
-
- @media screen and (prefers-reduced-motion: no-preference) {
- :host astro-dev-overlay-window {
- transform: translateY(55px) translate(-50%, -50%);
- transition: transform 0.15s ease-in-out;
- transform-origin: center bottom;
- }
-
- :host([data-active]) astro-dev-overlay-window {
- transform: translateY(0) translate(-50%, -50%);
- }
- }
- `;
- fragment.append(style);
-
- const window = document.createElement('astro-dev-overlay-window');
- window.windowTitle = title;
- window.windowIcon = icon;
- window.innerHTML = windowContent;
-
- window.append(...addedNodes);
-
- fragment.append(window);
-
- return fragment;
-}
-
-export async function waitForTransition(canvas: ShadowRoot): Promise {
- canvas.host?.removeAttribute('data-active');
-
- await new Promise((resolve) => {
- canvas.host.addEventListener('transitionend', resolve);
- });
-
- return true;
+export function createWindowElement(content: string) {
+ const windowElement = document.createElement('astro-dev-toolbar-window');
+ windowElement.innerHTML = content;
+ return windowElement;
}
diff --git a/packages/astro/src/runtime/client/dev-overlay/plugins/xray.ts b/packages/astro/src/runtime/client/dev-overlay/plugins/xray.ts
index ab927bacc2353..edc038df7afeb 100644
--- a/packages/astro/src/runtime/client/dev-overlay/plugins/xray.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/plugins/xray.ts
@@ -1,15 +1,21 @@
import type { DevOverlayMetadata, DevOverlayPlugin } from '../../../../@types/astro.js';
import type { DevOverlayHighlight } from '../ui-library/highlight.js';
-import { attachTooltipToHighlight, createHighlight, positionHighlight } from './utils/highlight.js';
+import {
+ attachTooltipToHighlight,
+ createHighlight,
+ getElementsPositionInDocument,
+ positionHighlight,
+} from './utils/highlight.js';
+import { createWindowElement } from './utils/window.js';
const icon =
' ';
export default {
id: 'astro:xray',
- name: 'Xray',
+ name: 'Inspect',
icon: icon,
- init(canvas) {
+ init(canvas, eventTarget) {
let islandsOverlays: { highlightElement: DevOverlayHighlight; island: HTMLElement }[] = [];
addIslandsOverlay();
@@ -17,8 +23,31 @@ export default {
document.addEventListener('astro:after-swap', addIslandsOverlay);
document.addEventListener('astro:page-load', refreshIslandsOverlayPositions);
+ function onPageClick(event: MouseEvent) {
+ const target = event.target as Element | null;
+ if (!target) return;
+ if (!target.closest) return;
+ if (target.closest('astro-dev-toolbar')) return;
+ event.preventDefault();
+ event.stopPropagation();
+ eventTarget.dispatchEvent(
+ new CustomEvent('toggle-plugin', {
+ detail: {
+ state: false,
+ },
+ })
+ );
+ }
+
+ eventTarget.addEventListener('plugin-toggled', (event: any) => {
+ if (event.detail.state === true) {
+ document.addEventListener('click', onPageClick, true);
+ } else {
+ document.removeEventListener('click', onPageClick, true);
+ }
+ });
+
function addIslandsOverlay() {
- initStyle();
islandsOverlays.forEach(({ highlightElement }) => {
highlightElement.remove();
});
@@ -26,6 +55,45 @@ export default {
const islands = document.querySelectorAll('astro-island');
+ if (islands.length === 0) {
+ const window = createWindowElement(
+ `
+
+
+ It looks like there are no interactive component islands on this page. Did you forget to add a client directive to your interactive UI component?
+
+ `
+ );
+
+ canvas.append(window);
+ return;
+ }
+
islands.forEach((island) => {
const computedStyle = window.getComputedStyle(island);
const islandElement = (island.children[0] as HTMLElement) || island;
@@ -39,8 +107,19 @@ export default {
const rect = islandElement.getBoundingClientRect();
const highlight = createHighlight(rect);
const tooltip = buildIslandTooltip(island);
- attachTooltipToHighlight(highlight, tooltip, islandElement);
+ // Set the highlight/tooltip as being fixed position the highlighted element
+ // is fixed. We do this so that we don't mistakenly take scroll position
+ // into account when setting the tooltip/highlight positioning.
+ //
+ // We only do this once due to how expensive computed styles are to calculate,
+ // and are unlikely to change. If that turns out to be wrong, reconsider this.
+ const { isFixed } = getElementsPositionInDocument(islandElement);
+ if (isFixed) {
+ tooltip.style.position = highlight.style.position = 'fixed';
+ }
+
+ attachTooltipToHighlight(highlight, tooltip, islandElement);
canvas.append(highlight);
islandsOverlays.push({ highlightElement: highlight, island: islandElement });
});
@@ -58,7 +137,7 @@ export default {
}
function buildIslandTooltip(island: HTMLElement) {
- const tooltip = document.createElement('astro-dev-overlay-tooltip');
+ const tooltip = document.createElement('astro-dev-toolbar-tooltip');
tooltip.sections = [];
const islandProps = island.getAttribute('props')
@@ -74,13 +153,19 @@ export default {
});
}
- // Add the props if we have any
- if (Object.keys(islandProps).length > 0) {
+ // Display the props if we have any
+ // Ignore the "data-astro-cid-XXXXXX" prop (internal)
+ const islandPropsEntries = Object.entries(islandProps).filter(
+ (prop: any) => !prop[0].startsWith('data-astro-cid-')
+ );
+ if (islandPropsEntries.length > 0) {
tooltip.sections.push({
title: 'Props',
- content: `${Object.entries(islandProps)
- .map((prop) => `${prop[0]}=${getPropValue(prop[1] as any)}
`)
- .join(', ')}`,
+ content: `${JSON.stringify(
+ Object.fromEntries(islandPropsEntries.map((prop: any) => [prop[0], prop[1][1]])),
+ undefined,
+ 2
+ )}
`,
});
}
@@ -106,35 +191,5 @@ export default {
return tooltip;
}
-
- function getPropValue(prop: [number, any]) {
- const [_, value] = prop;
- return JSON.stringify(value, null, 2);
- }
-
- function initStyle() {
- const style = document.createElement('style');
- style.textContent = `
- :host {
- opacity: 0;
- transition: opacity 0.1s ease-in-out;
- }
-
- :host([data-active]) {
- opacity: 1;
- }
- `;
-
- canvas.append(style);
- }
- },
- async beforeTogglingOff(canvas) {
- canvas.host?.removeAttribute('data-active');
-
- await new Promise((resolve) => {
- canvas.host.addEventListener('transitionend', resolve);
- });
-
- return true;
},
} satisfies DevOverlayPlugin;
diff --git a/packages/astro/src/runtime/client/dev-overlay/settings.ts b/packages/astro/src/runtime/client/dev-overlay/settings.ts
index 7ba12f2dbf6f3..d3f1c0d369891 100644
--- a/packages/astro/src/runtime/client/dev-overlay/settings.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/settings.ts
@@ -23,10 +23,20 @@ function getSettings() {
localStorage.setItem('astro:dev-overlay:settings', JSON.stringify(_settings));
}
+ function log(message: string) {
+ // eslint-disable-next-line no-console
+ console.log(
+ `%cAstro`,
+ 'background: linear-gradient(66.77deg, #D83333 0%, #F041FF 100%); color: white; padding-inline: 4px; border-radius: 2px; font-family: monospace;',
+ message
+ );
+ }
+
return {
get config() {
return _settings;
},
updateSetting,
+ log,
};
}
diff --git a/packages/astro/src/runtime/client/dev-overlay/ui-library/badge.ts b/packages/astro/src/runtime/client/dev-overlay/ui-library/badge.ts
new file mode 100644
index 0000000000000..5a8eea07ed9f8
--- /dev/null
+++ b/packages/astro/src/runtime/client/dev-overlay/ui-library/badge.ts
@@ -0,0 +1,71 @@
+type BadgeSize = 'small' | 'large';
+type BadgeStyle = 'purple' | 'gray' | 'red' | 'green' | 'yellow';
+
+export class DevOverlayBadge extends HTMLElement {
+ size: BadgeSize = 'small';
+ badgeStyle: BadgeStyle = 'purple';
+
+ shadowRoot: ShadowRoot;
+
+ constructor() {
+ super();
+ this.shadowRoot = this.attachShadow({ mode: 'open' });
+
+ if (this.hasAttribute('size')) this.size = this.getAttribute('size') as BadgeSize;
+
+ if (this.hasAttribute('badge-style'))
+ this.badgeStyle = this.getAttribute('badge-style') as BadgeStyle;
+
+ const classes = [`badge--${this.size}`, `badge--${this.badgeStyle}`];
+ this.shadowRoot.innerHTML = `
+
+
+
+
+
+ `;
+ }
+}
diff --git a/packages/astro/src/runtime/client/dev-overlay/ui-library/button.ts b/packages/astro/src/runtime/client/dev-overlay/ui-library/button.ts
new file mode 100644
index 0000000000000..f2bd75d70269b
--- /dev/null
+++ b/packages/astro/src/runtime/client/dev-overlay/ui-library/button.ts
@@ -0,0 +1,89 @@
+type ButtonSize = 'small' | 'medium' | 'large';
+type ButtonStyle = 'ghost' | 'outline' | 'purple' | 'gray' | 'red';
+
+export class DevOverlayButton extends HTMLElement {
+ size: ButtonSize = 'small';
+ buttonStyle: ButtonStyle = 'purple';
+
+ shadowRoot: ShadowRoot;
+
+ constructor() {
+ super();
+ this.shadowRoot = this.attachShadow({ mode: 'open' });
+
+ if (this.hasAttribute('size')) this.size = this.getAttribute('size') as ButtonSize;
+
+ if (this.hasAttribute('button-style'))
+ this.buttonStyle = this.getAttribute('button-style') as ButtonStyle;
+
+ const classes = [`button--${this.size}`, `button--${this.buttonStyle}`];
+
+ this.shadowRoot.innerHTML = `
+
+
+
+
+
+ `;
+ }
+}
diff --git a/packages/astro/src/runtime/client/dev-overlay/ui-library/card.ts b/packages/astro/src/runtime/client/dev-overlay/ui-library/card.ts
index 9d7062f8babfe..90d4739f165f3 100644
--- a/packages/astro/src/runtime/client/dev-overlay/ui-library/card.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/ui-library/card.ts
@@ -1,8 +1,6 @@
-import { getIconElement, isDefinedIcon, type Icon } from './icons.js';
-
export class DevOverlayCard extends HTMLElement {
- icon?: Icon;
link?: string | undefined | null;
+ clickAction?: () => void | (() => Promise);
shadowRoot: ShadowRoot;
constructor() {
@@ -10,26 +8,30 @@ export class DevOverlayCard extends HTMLElement {
this.shadowRoot = this.attachShadow({ mode: 'open' });
this.link = this.getAttribute('link');
- this.icon = this.hasAttribute('icon') ? (this.getAttribute('icon') as Icon) : undefined;
}
connectedCallback() {
- const element = this.link ? 'a' : 'button';
+ const element = this.link ? 'a' : this.clickAction ? 'button' : 'div';
this.shadowRoot.innerHTML = `
- <${element}${this.link ? ` href="${this.link}" target="_blank"` : ``}>
- ${this.icon ? this.getElementForIcon(this.icon) : ''}
-
+ <${element}${this.link ? ` href="${this.link}" target="_blank"` : ``} id="astro-overlay-card">
+
${element}>
`;
- }
- getElementForIcon(icon: Icon) {
- let iconElement;
- if (isDefinedIcon(icon)) {
- iconElement = getIconElement(icon);
- } else {
- iconElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
- iconElement.setAttribute('viewBox', '0 0 16 16');
- iconElement.innerHTML = icon;
+ if (this.clickAction) {
+ this.shadowRoot
+ .getElementById('astro-overlay-card')
+ ?.addEventListener('click', this.clickAction);
}
-
- iconElement?.style.setProperty('height', '24px');
- iconElement?.style.setProperty('width', '24px');
-
- return iconElement?.outerHTML ?? '';
}
}
diff --git a/packages/astro/src/runtime/client/dev-overlay/ui-library/highlight.ts b/packages/astro/src/runtime/client/dev-overlay/ui-library/highlight.ts
index 7d91535e0a40e..16d02df0cc7a3 100644
--- a/packages/astro/src/runtime/client/dev-overlay/ui-library/highlight.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/ui-library/highlight.ts
@@ -21,11 +21,13 @@ export class DevOverlayHighlight extends HTMLElement {
width: 100%;
height: 100%;
position: absolute;
+ z-index: 2000000000;
}
.icon {
width: 24px;
height: 24px;
+ color: white;
background: linear-gradient(0deg, #B33E66, #B33E66), linear-gradient(0deg, #351722, #351722);
border: 1px solid rgba(53, 23, 34, 1);
border-radius: 9999px;
diff --git a/packages/astro/src/runtime/client/dev-overlay/ui-library/icon.ts b/packages/astro/src/runtime/client/dev-overlay/ui-library/icon.ts
new file mode 100644
index 0000000000000..3211e1857620b
--- /dev/null
+++ b/packages/astro/src/runtime/client/dev-overlay/ui-library/icon.ts
@@ -0,0 +1,51 @@
+import { getIconElement, isDefinedIcon, type Icon } from './icons.js';
+
+export class DevOverlayIcon extends HTMLElement {
+ _icon: Icon | undefined = undefined;
+ shadowRoot: ShadowRoot;
+
+ get icon() {
+ return this._icon;
+ }
+ set icon(name: Icon | undefined) {
+ this._icon = name;
+ this.buildTemplate();
+ }
+
+ constructor() {
+ super();
+
+ this.shadowRoot = this.attachShadow({ mode: 'open' });
+
+ if (this.hasAttribute('icon')) {
+ this.icon = this.getAttribute('icon') as Icon;
+ } else {
+ this.buildTemplate();
+ }
+ }
+
+ getIconHTML(icon: Icon | undefined) {
+ if (icon && isDefinedIcon(icon)) {
+ return getIconElement(icon)?.outerHTML ?? '';
+ }
+
+ // If the icon that was passed isn't one of the predefined one, assume that they're passing it in as a slot
+ return ' ';
+ }
+
+ buildTemplate() {
+ this.shadowRoot.innerHTML = `
+ \n${this.getIconHTML(this._icon)}`;
+ }
+}
diff --git a/packages/astro/src/runtime/client/dev-overlay/ui-library/icons.ts b/packages/astro/src/runtime/client/dev-overlay/ui-library/icons.ts
index d9445e44acf8a..28878ef11c6bb 100644
--- a/packages/astro/src/runtime/client/dev-overlay/ui-library/icons.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/ui-library/icons.ts
@@ -5,10 +5,12 @@ export function isDefinedIcon(icon: Icon): icon is DefinedIcon {
return icon in icons;
}
+export function getIconElement(name: DefinedIcon): SVGElement;
+export function getIconElement(name: string & NonNullable): undefined;
export function getIconElement(
- name: keyof typeof icons | (string & NonNullable)
+ name: DefinedIcon | (string & NonNullable)
): SVGElement | undefined {
- const icon = icons[name as keyof typeof icons];
+ const icon = icons[name as DefinedIcon];
if (!icon) {
return undefined;
@@ -22,15 +24,41 @@ export function getIconElement(
const icons = {
'astro:logo': ` `,
- warning: ` `,
+ warning: ` `,
'arrow-down':
- ' ',
- bug: ' ',
- 'file-search':
- ' ',
+ ' ',
+ bug: ' ',
+ '': ' ',
'check-circle':
- ' ',
- gear: ' ',
+ ' ',
+ gear: ' ',
+ lightbulb:
+ ' ',
+ 'file-search':
+ ' ',
+ star: ' ',
+ checkmark:
+ ' ',
'dots-three':
' ',
+ copy: ' ',
+ compress:
+ ' ',
+ grid: ' ',
+ puzzle:
+ ' ',
+ approveUser:
+ ' ',
+ checkCircle:
+ ' ',
+ resizeImage:
+ ' ',
+ searchFile:
+ ' ',
+ image:
+ ' ',
+ robot:
+ ' ',
+ sitemap:
+ ' ',
} as const;
diff --git a/packages/astro/src/runtime/client/dev-overlay/ui-library/index.ts b/packages/astro/src/runtime/client/dev-overlay/ui-library/index.ts
new file mode 100644
index 0000000000000..a9c039a417ff1
--- /dev/null
+++ b/packages/astro/src/runtime/client/dev-overlay/ui-library/index.ts
@@ -0,0 +1,8 @@
+export { DevOverlayBadge } from './badge.js';
+export { DevOverlayButton } from './button.js';
+export { DevOverlayCard } from './card.js';
+export { DevOverlayHighlight } from './highlight.js';
+export { DevOverlayIcon } from './icon.js';
+export { DevOverlayToggle } from './toggle.js';
+export { DevOverlayTooltip } from './tooltip.js';
+export { DevOverlayWindow } from './window.js';
diff --git a/packages/astro/src/runtime/client/dev-overlay/ui-library/toggle.ts b/packages/astro/src/runtime/client/dev-overlay/ui-library/toggle.ts
index 63dcba65e304d..1fb0b686a9d1a 100644
--- a/packages/astro/src/runtime/client/dev-overlay/ui-library/toggle.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/ui-library/toggle.ts
@@ -30,6 +30,14 @@ export class DevOverlayToggle extends HTMLElement {
position: relative;
}
+ @media (forced-colors: active) {
+ input::after {
+ border: 1px solid black;
+ top: 0px;
+ left: 0px;
+ }
+ }
+
input:checked {
border: 1px solid rgba(213, 249, 196, 1);
background-color: rgba(61, 125, 31, 1);
diff --git a/packages/astro/src/runtime/client/dev-overlay/ui-library/tooltip.ts b/packages/astro/src/runtime/client/dev-overlay/ui-library/tooltip.ts
index dbd7add5dd516..9f7224ca479fc 100644
--- a/packages/astro/src/runtime/client/dev-overlay/ui-library/tooltip.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/ui-library/tooltip.ts
@@ -32,10 +32,10 @@ export class DevOverlayTooltip extends HTMLElement {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 14px;
margin: 0;
- z-index: 9999999;
+ z-index: 2000000001;
max-width: 45ch;
width: fit-content;
- min-width: 27ch;
+ min-width: 30ch;
box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.30), 0px 1px 2px 0px rgba(0, 0, 0, 0.29), 0px 4px 4px 0px rgba(0, 0, 0, 0.26), 0px 10px 6px 0px rgba(0, 0, 0, 0.15), 0px 17px 7px 0px rgba(0, 0, 0, 0.04), 0px 26px 7px 0px rgba(0, 0, 0, 0.01);
}
@@ -101,13 +101,18 @@ export class DevOverlayTooltip extends HTMLElement {
cursor: pointer;
}
- code {
- background: rgba(136, 58, 234, 0.33);
+ pre, code {
+ background: rgb(78, 27, 145);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
border-radius: 2px;
font-size: 14px;
padding: 2px;
}
+ pre {
+ padding: 1em;
+ margin: 0 0;
+ overflow: auto;
+ }
`;
const fragment = new DocumentFragment();
diff --git a/packages/astro/src/runtime/client/dev-overlay/ui-library/window.ts b/packages/astro/src/runtime/client/dev-overlay/ui-library/window.ts
index 18b515429ad82..bab936d4bbd87 100644
--- a/packages/astro/src/runtime/client/dev-overlay/ui-library/window.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/ui-library/window.ts
@@ -1,18 +1,9 @@
-import { getIconElement, isDefinedIcon, type Icon } from './icons.js';
-
export class DevOverlayWindow extends HTMLElement {
- windowTitle?: string | undefined | null;
- windowIcon?: Icon | undefined | null;
shadowRoot: ShadowRoot;
constructor() {
super();
this.shadowRoot = this.attachShadow({ mode: 'open' });
-
- this.windowTitle = this.getAttribute('window-title');
- this.windowIcon = this.hasAttribute('window-icon')
- ? (this.getAttribute('window-icon') as Icon)
- : undefined;
}
async connectedCallback() {
@@ -25,31 +16,34 @@ export class DevOverlayWindow extends HTMLElement {
background: linear-gradient(0deg, #13151A, #13151A), linear-gradient(0deg, #343841, #343841);
border: 1px solid rgba(52, 56, 65, 1);
width: min(640px, 100%);
- height: 480px;
+ max-height: 480px;
border-radius: 12px;
padding: 24px;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
- color: rgba(204, 206, 216, 1);
+ color: rgba(191, 193, 201, 1);
position: fixed;
z-index: 999999999;
- top: 55%;
+ bottom: 72px;
left: 50%;
- transform: translate(-50%, -50%);
+ transform: translateX(-50%);
box-shadow: 0px 0px 0px 0px rgba(19, 21, 26, 0.30), 0px 1px 2px 0px rgba(19, 21, 26, 0.29), 0px 4px 4px 0px rgba(19, 21, 26, 0.26), 0px 10px 6px 0px rgba(19, 21, 26, 0.15), 0px 17px 7px 0px rgba(19, 21, 26, 0.04), 0px 26px 7px 0px rgba(19, 21, 26, 0.01);
}
- ::slotted(h1), ::slotted(h2), ::slotted(h3), ::slotted(h4), ::slotted(h5) {
- font-weight: 600;
- color: #fff;
+ @media (forced-colors: active) {
+ :host {
+ background: white;
+ }
+ }
+
+ @media (max-width: 640px) {
+ :host {
+ border-radius: 0;
+ }
}
- #window-title {
- display: flex;
- align-items: center;
+ ::slotted(h1), ::slotted(h2), ::slotted(h3), ::slotted(h4), ::slotted(h5) {
font-weight: 600;
color: #fff;
- margin: 0;
- font-size: 22px;
}
::slotted(h1) {
@@ -72,37 +66,17 @@ export class DevOverlayWindow extends HTMLElement {
font-size: 14px;
}
- #window-title svg {
- margin-right: 8px;
- height: 1em;
- }
-
hr, ::slotted(hr) {
border: 1px solid rgba(27, 30, 36, 1);
margin: 1em 0;
}
+
+ p, ::slotted(p) {
+ line-height: 1.5em;
+ }
- ${this.windowIcon ? this.getElementForIcon(this.windowIcon) : ''}${
- this.windowTitle ?? ''
- }
-
`;
}
-
- getElementForIcon(icon: Icon) {
- if (isDefinedIcon(icon)) {
- const iconElement = getIconElement(icon);
- iconElement?.style.setProperty('height', '1em');
-
- return iconElement?.outerHTML;
- } else {
- const iconElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
- iconElement.setAttribute('viewBox', '0 0 16 16');
- iconElement.innerHTML = icon;
-
- return iconElement.outerHTML;
- }
- }
}
diff --git a/packages/astro/src/runtime/server/endpoint.ts b/packages/astro/src/runtime/server/endpoint.ts
index 7db5f07ee6fff..f8fc1c071c78f 100644
--- a/packages/astro/src/runtime/server/endpoint.ts
+++ b/packages/astro/src/runtime/server/endpoint.ts
@@ -1,35 +1,12 @@
+import { bold } from 'kleur/colors';
import type { APIContext, EndpointHandler, Params } from '../../@types/astro.js';
import type { Logger } from '../../core/logger/core.js';
-function getHandlerFromModule(mod: EndpointHandler, method: string, logger: Logger) {
- const lowerCaseMethod = method.toLowerCase();
-
- // TODO: remove in Astro 4.0
- if (mod[lowerCaseMethod]) {
- logger.warn(
- 'astro',
- `Lower case endpoint names are deprecated and will not be supported in Astro 4.0. Rename the endpoint ${lowerCaseMethod} to ${method}.`
- );
- }
+function getHandlerFromModule(mod: EndpointHandler, method: string) {
// If there was an exact match on `method`, return that function.
if (mod[method]) {
return mod[method];
}
-
- // TODO: remove in Astro 4.0
- if (mod[lowerCaseMethod]) {
- return mod[lowerCaseMethod];
- }
- // TODO: remove in Astro 4.0
- // Handle `del` instead of `delete`, since `delete` is a reserved word in JS.
- if (method === 'delete' && mod['del']) {
- return mod['del'];
- }
- // TODO: remove in Astro 4.0
- // If a single `all` handler was used, return that function.
- if (mod['all']) {
- return mod['all'];
- }
if (mod['ALL']) {
return mod['ALL'];
}
@@ -44,15 +21,17 @@ export async function renderEndpoint(
ssr: boolean,
logger: Logger
) {
- const { request } = context;
+ const { request, url } = context;
const chosenMethod = request.method?.toUpperCase();
- const handler = getHandlerFromModule(mod, chosenMethod, logger);
- // TODO: remove the 'get' check in Astro 4.0
- if (!ssr && ssr === false && chosenMethod && chosenMethod !== 'GET' && chosenMethod !== 'get') {
- // eslint-disable-next-line no-console
- console.warn(`
-${chosenMethod} requests are not available when building a static site. Update your config to \`output: 'server'\` or \`output: 'hybrid'\` with an \`export const prerender = false\` to handle ${chosenMethod} requests.`);
+ const handler = getHandlerFromModule(mod, chosenMethod);
+ if (!ssr && ssr === false && chosenMethod && chosenMethod !== 'GET') {
+ logger.warn(
+ null,
+ `${url.pathname} ${bold(
+ chosenMethod
+ )} requests are not available for a static site. Update your config to \`output: 'server'\` or \`output: 'hybrid'\` to enable.`
+ );
}
if (!handler || typeof handler !== 'function') {
// No handler found, so this should be a 404. Using a custom header
diff --git a/packages/astro/src/runtime/server/render/common.ts b/packages/astro/src/runtime/server/render/common.ts
index e5a5d5e860e06..f595dc78cf130 100644
--- a/packages/astro/src/runtime/server/render/common.ts
+++ b/packages/astro/src/runtime/server/render/common.ts
@@ -1,7 +1,8 @@
import type { SSRResult } from '../../../@types/astro.js';
import type { RenderInstruction } from './instruction.js';
-import { HTMLBytes, HTMLString, markHTMLString } from '../escape.js';
+import type { HTMLBytes, HTMLString } from '../escape.js';
+import { markHTMLString } from '../escape.js';
import {
determineIfNeedsHydrationScript,
determinesIfNeedsDirectiveScript,
@@ -67,8 +68,8 @@ function stringifyChunk(
let prescriptType: PrescriptType = needsHydrationScript
? 'both'
: needsDirectiveScript
- ? 'directive'
- : null;
+ ? 'directive'
+ : null;
if (prescriptType) {
let prescripts = getPrescripts(result, prescriptType, hydration.directive);
return markHTMLString(prescripts);
diff --git a/packages/astro/src/runtime/server/render/component.ts b/packages/astro/src/runtime/server/render/component.ts
index dfc5d6c5b62af..42987f011e905 100644
--- a/packages/astro/src/runtime/server/render/component.ts
+++ b/packages/astro/src/runtime/server/render/component.ts
@@ -8,7 +8,8 @@ import { createRenderInstruction, type RenderInstruction } from './instruction.j
import { clsx } from 'clsx';
import { AstroError, AstroErrorData } from '../../../core/errors/index.js';
-import { HTMLBytes, markHTMLString } from '../escape.js';
+import type { HTMLBytes } from '../escape.js';
+import { markHTMLString } from '../escape.js';
import { extractDirectives, generateHydrateScript } from '../hydration.js';
import { serializeProps } from '../serialize.js';
import { shorthash } from '../shorthash.js';
diff --git a/packages/astro/src/runtime/server/render/page.ts b/packages/astro/src/runtime/server/render/page.ts
index 8bc5366cfb346..fbfe567a8380e 100644
--- a/packages/astro/src/runtime/server/render/page.ts
+++ b/packages/astro/src/runtime/server/render/page.ts
@@ -25,7 +25,7 @@ export async function renderPage(
componentFactory.name,
componentFactory,
pageProps,
- null,
+ {},
true,
route
);
diff --git a/packages/astro/src/runtime/server/transition.ts b/packages/astro/src/runtime/server/transition.ts
index 17eece1d98198..0fed017dfdc97 100644
--- a/packages/astro/src/runtime/server/transition.ts
+++ b/packages/astro/src/runtime/server/transition.ts
@@ -1,7 +1,9 @@
import type {
SSRResult,
TransitionAnimation,
+ TransitionAnimationPair,
TransitionAnimationValue,
+ TransitionDirectionalAnimations,
} from '../../@types/astro.js';
import { fade, slide } from '../../transitions/index.js';
import { markHTMLString } from './escape.js';
@@ -34,6 +36,19 @@ const getAnimations = (name: TransitionAnimationValue) => {
if (typeof name === 'object') return name;
};
+const addPairs = (
+ animations: TransitionDirectionalAnimations | Record,
+ stylesheet: ViewTransitionStyleSheet
+) => {
+ for (const [direction, images] of Object.entries(animations) as Entries) {
+ for (const [image, rules] of Object.entries(images) as Entries<
+ (typeof animations)[typeof direction]
+ >) {
+ stylesheet.addAnimationPair(direction, image, rules);
+ }
+ }
+};
+
export function renderTransition(
result: SSRResult,
hash: string,
@@ -48,13 +63,7 @@ export function renderTransition(
const animations = getAnimations(animationName);
if (animations) {
- for (const [direction, images] of Object.entries(animations) as Entries) {
- for (const [image, rules] of Object.entries(images) as Entries<
- (typeof animations)[typeof direction]
- >) {
- sheet.addAnimationPair(direction, image, rules);
- }
- }
+ addPairs(animations, sheet);
} else if (animationName === 'none') {
sheet.addFallback('old', 'animation: none; mix-blend-mode: normal;');
sheet.addModern('old', 'animation: none; opacity: 0; mix-blend-mode: normal;');
@@ -65,6 +74,19 @@ export function renderTransition(
return scope;
}
+export function createAnimationScope(
+ transitionName: string,
+ animations: Record
+) {
+ const hash = Math.random().toString(36).slice(2, 8);
+ const scope = `astro-${hash}`;
+ const sheet = new ViewTransitionStyleSheet(scope, transitionName);
+
+ addPairs(animations, sheet);
+
+ return { scope, styles: sheet.toString().replaceAll('"', '') };
+}
+
class ViewTransitionStyleSheet {
private modern: string[] = [];
private fallback: string[] = [];
@@ -113,13 +135,18 @@ class ViewTransitionStyleSheet {
}
addAnimationPair(
- direction: 'forwards' | 'backwards',
+ direction: 'forwards' | 'backwards' | string,
image: 'old' | 'new',
rules: TransitionAnimation | TransitionAnimation[]
) {
const { scope, name } = this;
const animation = stringifyAnimation(rules);
- const prefix = direction === 'backwards' ? `[data-astro-transition=back]` : '';
+ const prefix =
+ direction === 'backwards'
+ ? `[data-astro-transition=back]`
+ : direction === 'forwards'
+ ? ''
+ : `[data-astro-transition=${direction}]`;
this.addRule('modern', `${prefix}::view-transition-${image}(${name}) { ${animation} }`);
this.addRule(
'fallback',
diff --git a/packages/astro/src/transitions/events.ts b/packages/astro/src/transitions/events.ts
new file mode 100644
index 0000000000000..b3921b31f0c96
--- /dev/null
+++ b/packages/astro/src/transitions/events.ts
@@ -0,0 +1,184 @@
+import { updateScrollPosition } from './router.js';
+import type { Direction, NavigationTypeString } from './types.js';
+
+export const TRANSITION_BEFORE_PREPARATION = 'astro:before-preparation';
+export const TRANSITION_AFTER_PREPARATION = 'astro:after-preparation';
+export const TRANSITION_BEFORE_SWAP = 'astro:before-swap';
+export const TRANSITION_AFTER_SWAP = 'astro:after-swap';
+export const TRANSITION_PAGE_LOAD = 'astro:page-load';
+
+type Events =
+ | typeof TRANSITION_AFTER_PREPARATION
+ | typeof TRANSITION_AFTER_SWAP
+ | typeof TRANSITION_PAGE_LOAD;
+export const triggerEvent = (name: Events) => document.dispatchEvent(new Event(name));
+export const onPageLoad = () => triggerEvent(TRANSITION_PAGE_LOAD);
+
+/*
+ * Common stuff
+ */
+class BeforeEvent extends Event {
+ readonly from: URL;
+ to: URL;
+ direction: Direction | string;
+ readonly navigationType: NavigationTypeString;
+ readonly sourceElement: Element | undefined;
+ readonly info: any;
+ newDocument: Document;
+
+ constructor(
+ type: string,
+ eventInitDict: EventInit | undefined,
+ from: URL,
+ to: URL,
+ direction: Direction | string,
+ navigationType: NavigationTypeString,
+ sourceElement: Element | undefined,
+ info: any,
+ newDocument: Document
+ ) {
+ super(type, eventInitDict);
+ this.from = from;
+ this.to = to;
+ this.direction = direction;
+ this.navigationType = navigationType;
+ this.sourceElement = sourceElement;
+ this.info = info;
+ this.newDocument = newDocument;
+
+ Object.defineProperties(this, {
+ from: { enumerable: true },
+ to: { enumerable: true, writable: true },
+ direction: { enumerable: true, writable: true },
+ navigationType: { enumerable: true },
+ sourceElement: { enumerable: true },
+ info: { enumerable: true },
+ newDocument: { enumerable: true, writable: true },
+ });
+ }
+}
+
+/*
+ * TransitionBeforePreparationEvent
+
+ */
+export const isTransitionBeforePreparationEvent = (
+ value: any
+): value is TransitionBeforePreparationEvent => value.type === TRANSITION_BEFORE_PREPARATION;
+export class TransitionBeforePreparationEvent extends BeforeEvent {
+ formData: FormData | undefined;
+ loader: () => Promise;
+ constructor(
+ from: URL,
+ to: URL,
+ direction: Direction | string,
+ navigationType: NavigationTypeString,
+ sourceElement: Element | undefined,
+ info: any,
+ newDocument: Document,
+ formData: FormData | undefined,
+ loader: (event: TransitionBeforePreparationEvent) => Promise
+ ) {
+ super(
+ TRANSITION_BEFORE_PREPARATION,
+ { cancelable: true },
+ from,
+ to,
+ direction,
+ navigationType,
+ sourceElement,
+ info,
+ newDocument
+ );
+ this.formData = formData;
+ this.loader = loader.bind(this, this);
+ Object.defineProperties(this, {
+ formData: { enumerable: true },
+ loader: { enumerable: true, writable: true },
+ });
+ }
+}
+
+/*
+ * TransitionBeforeSwapEvent
+ */
+
+export const isTransitionBeforeSwapEvent = (value: any): value is TransitionBeforeSwapEvent =>
+ value.type === TRANSITION_BEFORE_SWAP;
+export class TransitionBeforeSwapEvent extends BeforeEvent {
+ readonly direction: Direction | string;
+ readonly viewTransition: ViewTransition;
+ swap: () => void;
+
+ constructor(
+ afterPreparation: BeforeEvent,
+ viewTransition: ViewTransition,
+ swap: (event: TransitionBeforeSwapEvent) => void
+ ) {
+ super(
+ TRANSITION_BEFORE_SWAP,
+ undefined,
+ afterPreparation.from,
+ afterPreparation.to,
+ afterPreparation.direction,
+ afterPreparation.navigationType,
+ afterPreparation.sourceElement,
+ afterPreparation.info,
+ afterPreparation.newDocument
+ );
+ this.direction = afterPreparation.direction;
+ this.viewTransition = viewTransition;
+ this.swap = swap.bind(this, this);
+
+ Object.defineProperties(this, {
+ direction: { enumerable: true },
+ viewTransition: { enumerable: true },
+ swap: { enumerable: true, writable: true },
+ });
+ }
+}
+
+export async function doPreparation(
+ from: URL,
+ to: URL,
+ direction: Direction | string,
+ navigationType: NavigationTypeString,
+ sourceElement: Element | undefined,
+ info: any,
+ formData: FormData | undefined,
+ defaultLoader: (event: TransitionBeforePreparationEvent) => Promise
+) {
+ const event = new TransitionBeforePreparationEvent(
+ from,
+ to,
+ direction,
+ navigationType,
+ sourceElement,
+ info,
+ window.document,
+ formData,
+ defaultLoader
+ );
+ if (document.dispatchEvent(event)) {
+ await event.loader();
+ if (!event.defaultPrevented) {
+ triggerEvent(TRANSITION_AFTER_PREPARATION);
+ if (event.navigationType !== 'traverse') {
+ // save the current scroll position before we change the DOM and transition to the new page
+ updateScrollPosition({ scrollX, scrollY });
+ }
+ }
+ }
+ return event;
+}
+
+export async function doSwap(
+ afterPreparation: BeforeEvent,
+ viewTransition: ViewTransition,
+ defaultSwap: (event: TransitionBeforeSwapEvent) => void
+) {
+ const event = new TransitionBeforeSwapEvent(afterPreparation, viewTransition, defaultSwap);
+ document.dispatchEvent(event);
+ event.swap();
+ return event;
+}
diff --git a/packages/astro/src/transitions/index.ts b/packages/astro/src/transitions/index.ts
index 0a58d2d4b48ae..d87052f2daf23 100644
--- a/packages/astro/src/transitions/index.ts
+++ b/packages/astro/src/transitions/index.ts
@@ -1,4 +1,5 @@
import type { TransitionAnimationPair, TransitionDirectionalAnimations } from '../@types/astro.js';
+export { createAnimationScope } from '../runtime/server/transition.js';
const EASE_IN_OUT_QUART = 'cubic-bezier(0.76, 0, 0.24, 1)';
diff --git a/packages/astro/src/transitions/router.ts b/packages/astro/src/transitions/router.ts
index c4da38c2c8bea..e710c2e1b2e8c 100644
--- a/packages/astro/src/transitions/router.ts
+++ b/packages/astro/src/transitions/router.ts
@@ -1,32 +1,48 @@
-export type Fallback = 'none' | 'animate' | 'swap';
-export type Direction = 'forward' | 'back';
-export type Options = {
- history?: 'auto' | 'push' | 'replace';
- formData?: FormData;
-};
+import type { TransitionBeforePreparationEvent, TransitionBeforeSwapEvent } from './events.js';
+import { TRANSITION_AFTER_SWAP, doPreparation, doSwap } from './events.js';
+import type { Direction, Fallback, Options } from './types.js';
type State = {
index: number;
scrollX: number;
scrollY: number;
- intraPage?: boolean;
};
type Events = 'astro:page-load' | 'astro:after-swap';
+// Create bound versions of pushState/replaceState so that Partytown doesn't hijack them,
+// which breaks Firefox.
+const inBrowser = import.meta.env.SSR === false;
+const pushState = (inBrowser && history.pushState.bind(history)) as typeof history.pushState;
+const replaceState = (inBrowser &&
+ history.replaceState.bind(history)) as typeof history.replaceState;
+
// only update history entries that are managed by us
// leave other entries alone and do not accidently add state.
-const updateScrollPosition = (positions: { scrollX: number; scrollY: number }) =>
- history.state && history.replaceState({ ...history.state, ...positions }, '');
-
-const inBrowser = import.meta.env.SSR === false;
+export const updateScrollPosition = (positions: { scrollX: number; scrollY: number }) => {
+ if (history.state) {
+ history.scrollRestoration = 'manual';
+ replaceState({ ...history.state, ...positions }, '');
+ }
+};
export const supportsViewTransitions = inBrowser && !!document.startViewTransition;
export const transitionEnabledOnThisPage = () =>
inBrowser && !!document.querySelector('[name="astro-view-transitions-enabled"]');
-const samePage = (otherLocation: URL) =>
- location.pathname === otherLocation.pathname && location.search === otherLocation.search;
+const samePage = (thisLocation: URL, otherLocation: URL) =>
+ thisLocation.pathname === otherLocation.pathname && thisLocation.search === otherLocation.search;
+
+// When we traverse the history, the window.location is already set to the new location.
+// This variable tells us where we came from
+let originalLocation: URL;
+// The result of startViewTransition (browser or simulation)
+let viewTransition: ViewTransition | undefined;
+// skip transition flag for fallback simulation
+let skipTransition = false;
+// The resolve function of the finished promise for fallback simulation
+let viewTransitionFinished: () => void;
+
const triggerEvent = (name: Events) => document.dispatchEvent(new Event(name));
const onPageLoad = () => triggerEvent('astro:page-load');
const announce = () => {
@@ -48,6 +64,9 @@ const announce = () => {
};
const PERSIST_ATTR = 'data-astro-transition-persist';
+const DIRECTION_ATTR = 'data-astro-transition';
+const OLD_NEW_ATTR = 'data-astro-transition-fallback';
+
const VITE_ID = 'data-vite-dev-id';
let parser: DOMParser;
@@ -66,7 +85,8 @@ if (inBrowser) {
} else if (transitionEnabledOnThisPage()) {
// This page is loaded from the browser addressbar or via a link from extern,
// it needs a state in the history
- history.replaceState({ index: currentHistoryIndex, scrollX, scrollY, intraPage: false }, '');
+ replaceState({ index: currentHistoryIndex, scrollX, scrollY }, '');
+ history.scrollRestoration = 'manual';
}
}
@@ -147,50 +167,62 @@ function runScripts() {
return wait;
}
-function isInfinite(animation: Animation) {
- const effect = animation.effect;
- if (!effect || !(effect instanceof KeyframeEffect) || !effect.target) return false;
- const style = window.getComputedStyle(effect.target, effect.pseudoElement);
- return style.animationIterationCount === 'infinite';
-}
-
// Add a new entry to the browser history. This also sets the new page in the browser addressbar.
// Sets the scroll position according to the hash fragment of the new location.
-const moveToLocation = (toLocation: URL, replace: boolean, intraPage: boolean) => {
- const fresh = !samePage(toLocation);
+const moveToLocation = (to: URL, from: URL, options: Options, historyState?: State) => {
+ const intraPage = samePage(from, to);
+
let scrolledToTop = false;
- if (toLocation.href !== location.href) {
- if (replace) {
- history.replaceState({ ...history.state }, '', toLocation.href);
+ if (to.href !== location.href && !historyState) {
+ if (options.history === 'replace') {
+ const current = history.state;
+ replaceState(
+ {
+ ...options.state,
+ index: current.index,
+ scrollX: current.scrollX,
+ scrollY: current.scrollY,
+ },
+ '',
+ to.href
+ );
} else {
- history.replaceState({ ...history.state, intraPage }, '');
- history.pushState(
- { index: ++currentHistoryIndex, scrollX: 0, scrollY: 0 },
+ pushState(
+ { ...options.state, index: ++currentHistoryIndex, scrollX: 0, scrollY: 0 },
'',
- toLocation.href
+ to.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
- if (fresh) {
- scrollTo({ left: 0, top: 0, behavior: 'instant' });
- scrolledToTop = true;
- }
}
- 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;
+ // now we are on the new page for non-history navigations!
+ // (with history navigation page change happens before popstate is fired)
+ originalLocation = to;
+
+ // freshly loaded pages start from the top
+ if (!intraPage) {
+ scrollTo({ left: 0, top: 0, behavior: 'instant' });
+ scrolledToTop = true;
+ }
+
+ if (historyState) {
+ scrollTo(historyState.scrollX, historyState.scrollY);
} else {
- if (!scrolledToTop) {
- scrollTo({ left: 0, top: 0, behavior: 'instant' });
+ if (to.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
+ history.scrollRestoration = 'auto';
+ location.href = to.href;
+ } else {
+ if (!scrolledToTop) {
+ scrollTo({ left: 0, top: 0, behavior: 'instant' });
+ }
}
+ history.scrollRestoration = 'manual';
}
};
-function stylePreloadLinks(newDocument: Document) {
+function preloadStyleLinks(newDocument: Document) {
const links: Promise[] = [];
for (const el of newDocument.querySelectorAll('head link[rel=stylesheet]')) {
// Do not preload links that are already on the page.
@@ -221,24 +253,23 @@ function stylePreloadLinks(newDocument: Document) {
// 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,
+ preparationEvent: TransitionBeforePreparationEvent,
options: Options,
- popState?: State,
+ historyState?: 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.
// Returns null if the element is not part of the new head, undefined if it should be left alone.
- const persistedHeadElement = (el: HTMLElement): Element | null => {
+ const persistedHeadElement = (el: HTMLElement, newDoc: Document): Element | null => {
const id = el.getAttribute(PERSIST_ATTR);
- const newEl = id && newDocument.head.querySelector(`[${PERSIST_ATTR}="${id}"]`);
+ const newEl = id && newDoc.head.querySelector(`[${PERSIST_ATTR}="${id}"]`);
if (newEl) {
return newEl;
}
if (el.matches('link[rel=stylesheet]')) {
const href = el.getAttribute('href');
- return newDocument.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
+ return newDoc.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
}
return null;
};
@@ -282,22 +313,22 @@ async function updateDOM(
}
};
- const swap = () => {
+ const defaultSwap = (beforeSwapEvent: TransitionBeforeSwapEvent) => {
// swap attributes of the html element
// - delete all attributes from the current document
// - insert all attributes from doc
// - reinsert all original attributes that are named 'data-astro-*'
const html = document.documentElement;
- const astro = [...html.attributes].filter(
+ const astroAttributes = [...html.attributes].filter(
({ name }) => (html.removeAttribute(name), name.startsWith('data-astro-'))
);
- [...newDocument.documentElement.attributes, ...astro].forEach(({ name, value }) =>
- html.setAttribute(name, value)
+ [...beforeSwapEvent.newDocument.documentElement.attributes, ...astroAttributes].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) {
+ for (const s2 of beforeSwapEvent.newDocument.scripts) {
if (
// Inline
(!s1.src && s1.textContent === s2.textContent) ||
@@ -313,7 +344,7 @@ async function updateDOM(
// Swap head
for (const el of Array.from(document.head.children)) {
- const newEl = persistedHeadElement(el as HTMLElement);
+ const newEl = persistedHeadElement(el as HTMLElement, beforeSwapEvent.newDocument);
// If the element exists in the document already, remove it
// from the new document and leave the current node alone
if (newEl) {
@@ -325,7 +356,7 @@ async function updateDOM(
}
// Everything left in the new head is new, append it all.
- document.head.append(...newDocument.head.children);
+ document.head.append(...beforeSwapEvent.newDocument.head.children);
// Persist elements in the existing body
const oldBody = document.body;
@@ -333,7 +364,7 @@ async function updateDOM(
const savedFocus = saveFocus();
// this will reset scroll Position
- document.body.replaceWith(newDocument.body);
+ document.body.replaceWith(beforeSwapEvent.newDocument.body);
for (const el of oldBody.querySelectorAll(`[${PERSIST_ATTR}]`)) {
const id = el.getAttribute(PERSIST_ATTR);
@@ -345,103 +376,180 @@ async function updateDOM(
}
}
restoreFocus(savedFocus);
-
- if (popState) {
- scrollTo(popState.scrollX, popState.scrollY); // usings 'auto' scrollBehavior
- } else {
- moveToLocation(toLocation, options.history === 'replace', false);
- }
-
- triggerEvent('astro:after-swap');
};
- const links = stylePreloadLinks(newDocument);
- links.length && (await Promise.all(links));
-
- if (fallback === 'animate') {
+ async function animate(phase: string) {
+ function isInfinite(animation: Animation) {
+ const effect = animation.effect;
+ if (!effect || !(effect instanceof KeyframeEffect) || !effect.target) return false;
+ const style = window.getComputedStyle(effect.target, effect.pseudoElement);
+ return style.animationIterationCount === 'infinite';
+ }
// Trigger the animations
const currentAnimations = document.getAnimations();
- document.documentElement.dataset.astroTransitionFallback = 'old';
- const newAnimations = document
- .getAnimations()
- .filter((a) => !currentAnimations.includes(a) && !isInfinite(a));
- const finished = Promise.all(newAnimations.map((a) => a.finished));
- await finished;
- swap();
- document.documentElement.dataset.astroTransitionFallback = 'new';
+ document.documentElement.setAttribute(OLD_NEW_ATTR, phase);
+ const nextAnimations = document.getAnimations();
+ const newAnimations = nextAnimations.filter(
+ (a) => !currentAnimations.includes(a) && !isInfinite(a)
+ );
+ return Promise.all(newAnimations.map((a) => a.finished));
+ }
+
+ if (!skipTransition) {
+ document.documentElement.setAttribute(DIRECTION_ATTR, preparationEvent.direction);
+
+ if (fallback === 'animate') {
+ await animate('old');
+ }
} else {
- swap();
+ // that's what Chrome does
+ throw new DOMException('Transition was skipped');
+ }
+
+ const swapEvent = await doSwap(preparationEvent, viewTransition!, defaultSwap);
+ moveToLocation(swapEvent.to, swapEvent.from, options, historyState);
+ triggerEvent(TRANSITION_AFTER_SWAP);
+
+ if (fallback === 'animate' && !skipTransition) {
+ animate('new').then(() => viewTransitionFinished());
}
}
async function transition(
direction: Direction,
- toLocation: URL,
+ from: URL,
+ to: URL,
options: Options,
- popState?: State
+ historyState?: State
) {
- let finished: Promise;
- const href = toLocation.href;
- const init: RequestInit = {};
- if (options.formData) {
- init.method = 'POST';
- init.body = options.formData;
- }
- const response = await fetchHTML(href, init);
- // If there is a problem fetching the new page, just do an MPA navigation to it.
- if (response === null) {
- location.href = href;
+ // not ours
+ if (!transitionEnabledOnThisPage() || location.origin !== to.origin) {
+ location.href = to.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);
- }
- parser ??= new DOMParser();
+ const navigationType = historyState
+ ? 'traverse'
+ : options.history === 'replace'
+ ? 'replace'
+ : 'push';
- 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 (navigationType !== 'traverse') {
+ updateScrollPosition({ scrollX, scrollY });
+ }
+ if (samePage(from, to) && !!to.hash) {
+ moveToLocation(to, from, options, historyState);
+ return;
+ }
- // If ViewTransitions is not enabled on the incoming page, do a full page load to it.
- // Unless this was a form submission, in which case we do not want to trigger another mutation.
- if (!newDocument.querySelector('[name="astro-view-transitions-enabled"]') && !options.formData) {
- location.href = href;
+ const prepEvent = await doPreparation(
+ from,
+ to,
+ direction,
+ navigationType,
+ options.sourceElement,
+ options.info,
+ options.formData,
+ defaultLoader
+ );
+ if (prepEvent.defaultPrevented) {
+ location.href = to.href;
return;
}
- if (import.meta.env.DEV) await prepareForClientOnlyComponents(newDocument, toLocation);
+ async function defaultLoader(preparationEvent: TransitionBeforePreparationEvent) {
+ const href = preparationEvent.to.href;
+ const init: RequestInit = {};
+ if (preparationEvent.formData) {
+ init.method = 'POST';
+ init.body = preparationEvent.formData;
+ }
+ const response = await fetchHTML(href, init);
+ // If there is a problem fetching the new page, just do an MPA navigation to it.
+ if (response === null) {
+ preparationEvent.preventDefault();
+ return;
+ }
+ // if there was a redirection, show the final URL in the browser's address bar
+ if (response.redirected) {
+ preparationEvent.to = new URL(response.redirected);
+ }
+
+ parser ??= new DOMParser();
+
+ preparationEvent.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
+ preparationEvent.newDocument.querySelectorAll('noscript').forEach((el) => el.remove());
+
+ // If ViewTransitions is not enabled on the incoming page, do a full page load to it.
+ // Unless this was a form submission, in which case we do not want to trigger another mutation.
+ if (
+ !preparationEvent.newDocument.querySelector('[name="astro-view-transitions-enabled"]') &&
+ !preparationEvent.formData
+ ) {
+ preparationEvent.preventDefault();
+ return;
+ }
+
+ const links = preloadStyleLinks(preparationEvent.newDocument);
+ links.length && (await Promise.all(links));
- if (!popState) {
- // save the current scroll position before we change the DOM and transition to the new page
- history.replaceState({ ...history.state, scrollX, scrollY }, '');
+ if (import.meta.env.DEV)
+ await prepareForClientOnlyComponents(preparationEvent.newDocument, preparationEvent.to);
}
- document.documentElement.dataset.astroTransition = direction;
+
+ skipTransition = false;
if (supportsViewTransitions) {
- finished = document.startViewTransition(() =>
- updateDOM(newDocument, toLocation, options, popState)
- ).finished;
+ viewTransition = document.startViewTransition(
+ async () => await updateDOM(prepEvent, options, historyState)
+ );
} else {
- finished = updateDOM(newDocument, toLocation, options, popState, getFallback());
+ const updateDone = (async () => {
+ // immediatelly paused to setup the ViewTransition object for Fallback mode
+ await new Promise((r) => setTimeout(r));
+ await updateDOM(prepEvent, options, historyState, getFallback());
+ })();
+
+ // When the updateDone promise is settled,
+ // we have run and awaited all swap functions and the after-swap event
+ // This qualifies for "updateCallbackDone".
+ //
+ // For the build in ViewTransition, "ready" settles shortly after "updateCallbackDone",
+ // i.e. after all pseudo elements are created and the animation is about to start.
+ // In simulation mode the "old" animation starts before swap,
+ // the "new" animation starts after swap. That is not really comparable.
+ // Thus we go with "very, very shortly after updateCallbackDone" and make both equal.
+ //
+ // "finished" resolves after all animations are done.
+
+ viewTransition = {
+ updateCallbackDone: updateDone, // this is about correct
+ ready: updateDone, // good enough
+ finished: new Promise((r) => (viewTransitionFinished = r)), // see end of updateDOM
+ skipTransition: () => {
+ skipTransition = true;
+ },
+ };
}
- try {
- await finished;
- } finally {
- // skip this for the moment as it tends to stop fallback animations
- // document.documentElement.removeAttribute('data-astro-transition');
+
+ viewTransition.ready.then(async () => {
await runScripts();
onPageLoad();
announce();
- }
+ });
+ viewTransition.finished.then(() => {
+ document.documentElement.removeAttribute(DIRECTION_ATTR);
+ document.documentElement.removeAttribute(OLD_NEW_ATTR);
+ });
+ await viewTransition.ready;
}
let navigateOnServerWarned = false;
-export function navigate(href: string, options?: Options) {
+export async function navigate(href: string, options?: Options) {
if (inBrowser === false) {
if (!navigateOnServerWarned) {
// instantiate an error for the stacktrace to show to user.
@@ -455,23 +563,7 @@ export function navigate(href: string, options?: Options) {
}
return;
}
-
- // 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)
- // *unless* they are form posts which have side-effects and so need to happen
- // 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 && samePage(toLocation) && !options?.formData) {
- moveToLocation(toLocation, options?.history === 'replace', true);
- } else {
- // different origin will be detected by fetch
- transition('forward', toLocation, options ?? {});
- }
+ await transition('forward', originalLocation, new URL(href, location.href), options ?? {});
}
function onPopState(ev: PopStateEvent) {
@@ -479,10 +571,6 @@ function onPopState(ev: PopStateEvent) {
// 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
- if (history.scrollRestoration) {
- history.scrollRestoration = 'manual';
- }
location.reload();
return;
}
@@ -492,28 +580,13 @@ function onPopState(ev: PopStateEvent) {
// Just ignore stateless entries.
// The browser will handle navigation fine without our help
if (ev.state === null) {
- if (history.scrollRestoration) {
- history.scrollRestoration = 'auto';
- }
return;
}
-
- // With the default "auto", the browser will jump to the old scroll position
- // before the ViewTransition is complete.
- if (history.scrollRestoration) {
- history.scrollRestoration = 'manual';
- }
-
const state: State = history.state;
- if (state.intraPage) {
- // this is non transition intra-page scrolling
- scrollTo(state.scrollX, state.scrollY);
- } else {
- const nextIndex = state.index;
- const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back';
- currentHistoryIndex = nextIndex;
- transition(direction, new URL(location.href), {}, state);
- }
+ const nextIndex = state.index;
+ const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back';
+ currentHistoryIndex = nextIndex;
+ transition(direction, originalLocation, new URL(location.href), {}, state);
}
// There's not a good way to record scroll position before a back button.
@@ -522,8 +595,10 @@ const onScroll = () => {
updateScrollPosition({ scrollX, scrollY });
};
+// initialization
if (inBrowser) {
if (supportsViewTransitions || getFallback() !== 'none') {
+ originalLocation = new URL(location.href);
addEventListener('popstate', onPopState);
addEventListener('load', onPageLoad);
if ('onscrollend' in window) addEventListener('scrollend', onScroll);
diff --git a/packages/astro/src/transitions/types.ts b/packages/astro/src/transitions/types.ts
new file mode 100644
index 0000000000000..0e70825e59673
--- /dev/null
+++ b/packages/astro/src/transitions/types.ts
@@ -0,0 +1,10 @@
+export type Fallback = 'none' | 'animate' | 'swap';
+export type Direction = 'forward' | 'back';
+export type NavigationTypeString = 'push' | 'replace' | 'traverse';
+export type Options = {
+ history?: 'auto' | 'push' | 'replace';
+ info?: any;
+ state?: any;
+ formData?: FormData;
+ sourceElement?: Element; // more than HTMLElement, e.g. SVGAElement
+};
diff --git a/packages/astro/src/transitions/vite-plugin-transitions.ts b/packages/astro/src/transitions/vite-plugin-transitions.ts
index 8d5dbe553804b..16cb174baf444 100644
--- a/packages/astro/src/transitions/vite-plugin-transitions.ts
+++ b/packages/astro/src/transitions/vite-plugin-transitions.ts
@@ -1,4 +1,4 @@
-import * as vite from 'vite';
+import type * as vite from 'vite';
import type { AstroSettings } from '../@types/astro.js';
const virtualModuleId = 'astro:transitions';
@@ -21,13 +21,20 @@ export default function astroTransitions({ settings }: { settings: AstroSettings
load(id) {
if (id === resolvedVirtualModuleId) {
return `
- export * from "astro/transitions";
+ export * from "astro/virtual-modules/transitions.js";
export { default as ViewTransitions } from "astro/components/ViewTransitions.astro";
`;
}
if (id === resolvedVirtualClientModuleId) {
return `
- export * from "astro/transitions/router";
+ export { navigate, supportsViewTransitions, transitionEnabledOnThisPage } from "astro/virtual-modules/transitions-router.js";
+ export * from "astro/virtual-modules/transitions-types.js";
+ export {
+ TRANSITION_BEFORE_PREPARATION, isTransitionBeforePreparationEvent, TransitionBeforePreparationEvent,
+ TRANSITION_AFTER_PREPARATION,
+ TRANSITION_BEFORE_SWAP, isTransitionBeforeSwapEvent, TransitionBeforeSwapEvent,
+ TRANSITION_AFTER_SWAP, TRANSITION_PAGE_LOAD
+ } from "astro/virtual-modules/transitions-events.js";
`;
}
},
diff --git a/packages/astro/src/type-utils.ts b/packages/astro/src/type-utils.ts
index d777a9f287893..4304438dbfdef 100644
--- a/packages/astro/src/type-utils.ts
+++ b/packages/astro/src/type-utils.ts
@@ -30,3 +30,13 @@ export type ValueOf = T[keyof T];
// Gets the type of the values of a Map
export type MapValue = T extends Map ? V : never;
+
+// Allow the user to create a type where all keys are optional.
+// Useful for functions where props are merged.
+export type DeepPartial = {
+ [P in keyof T]?: T[P] extends (infer U)[]
+ ? DeepPartial[]
+ : T[P] extends object | undefined
+ ? DeepPartial
+ : T[P];
+};
diff --git a/packages/astro/src/virtual-modules/README.md b/packages/astro/src/virtual-modules/README.md
new file mode 100644
index 0000000000000..137e2e16f59a5
--- /dev/null
+++ b/packages/astro/src/virtual-modules/README.md
@@ -0,0 +1,3 @@
+# virtual-modules
+
+This directory contains the entry points for Astro virtual modules. For example, `astro:foobar` would re-export or use `astro/virtual-modules/foobar.js` which maps to the internal file `astro/dist/virtual-modules/foobar.js`.
diff --git a/packages/astro/src/virtual-modules/i18n.ts b/packages/astro/src/virtual-modules/i18n.ts
new file mode 100644
index 0000000000000..a55c1f6cfb59c
--- /dev/null
+++ b/packages/astro/src/virtual-modules/i18n.ts
@@ -0,0 +1 @@
+export * from '../i18n/index.js';
diff --git a/packages/astro/src/virtual-modules/middleware.ts b/packages/astro/src/virtual-modules/middleware.ts
new file mode 100644
index 0000000000000..4874c88d00a40
--- /dev/null
+++ b/packages/astro/src/virtual-modules/middleware.ts
@@ -0,0 +1 @@
+export { defineMiddleware, sequence } from '../core/middleware/index.js';
diff --git a/packages/astro/src/virtual-modules/prefetch.ts b/packages/astro/src/virtual-modules/prefetch.ts
new file mode 100644
index 0000000000000..72bc23e2d04e3
--- /dev/null
+++ b/packages/astro/src/virtual-modules/prefetch.ts
@@ -0,0 +1 @@
+export * from '../prefetch/index.js';
diff --git a/packages/astro/src/virtual-modules/transitions-events.ts b/packages/astro/src/virtual-modules/transitions-events.ts
new file mode 100644
index 0000000000000..35ecaf64f8974
--- /dev/null
+++ b/packages/astro/src/virtual-modules/transitions-events.ts
@@ -0,0 +1 @@
+export * from '../transitions/events.js';
diff --git a/packages/astro/src/virtual-modules/transitions-router.ts b/packages/astro/src/virtual-modules/transitions-router.ts
new file mode 100644
index 0000000000000..666089f3f6fcb
--- /dev/null
+++ b/packages/astro/src/virtual-modules/transitions-router.ts
@@ -0,0 +1 @@
+export * from '../transitions/router.js';
diff --git a/packages/astro/src/virtual-modules/transitions-types.ts b/packages/astro/src/virtual-modules/transitions-types.ts
new file mode 100644
index 0000000000000..66dfb1d0ef6c5
--- /dev/null
+++ b/packages/astro/src/virtual-modules/transitions-types.ts
@@ -0,0 +1 @@
+export * from '../transitions/types.js';
diff --git a/packages/astro/src/virtual-modules/transitions.ts b/packages/astro/src/virtual-modules/transitions.ts
new file mode 100644
index 0000000000000..84aeb3a2cfdf4
--- /dev/null
+++ b/packages/astro/src/virtual-modules/transitions.ts
@@ -0,0 +1 @@
+export * from '../transitions/index.js';
diff --git a/packages/astro/src/vite-plugin-astro-server/base.ts b/packages/astro/src/vite-plugin-astro-server/base.ts
index 7bf57d10a3949..68eeae21a3e65 100644
--- a/packages/astro/src/vite-plugin-astro-server/base.ts
+++ b/packages/astro/src/vite-plugin-astro-server/base.ts
@@ -1,10 +1,10 @@
import type * as vite from 'vite';
import type { AstroSettings } from '../@types/astro.js';
+import { bold } from 'kleur/colors';
import * as fs from 'node:fs';
import type { Logger } from '../core/logger/core.js';
import notFoundTemplate, { subpathNotUsedTemplate } from '../template/4xx.js';
-import { log404 } from './common.js';
import { writeHtmlResponse } from './response.js';
export function baseMiddleware(
@@ -28,13 +28,11 @@ export function baseMiddleware(
}
if (pathname === '/' || pathname === '/index.html') {
- log404(logger, pathname);
const html = subpathNotUsedTemplate(devRoot, pathname);
return writeHtmlResponse(res, 404, html);
}
if (req.headers.accept?.includes('text/html')) {
- log404(logger, pathname);
const html = notFoundTemplate({
statusCode: 404,
title: 'Not found',
@@ -49,14 +47,14 @@ export function baseMiddleware(
fs.stat(publicPath, (_err, stats) => {
if (stats) {
const expectedLocation = new URL('.' + url, devRootURL).pathname;
- logger.warn(
- 'dev',
- `Requests for items in your public folder must also include your base. ${url} should be ${expectedLocation}. Omitting the base will break in production.`
+ logger.error(
+ 'router',
+ `Request URLs for ${bold(
+ 'public/'
+ )} assets must also include your base. "${expectedLocation}" expected, but received "${url}".`
);
- res.writeHead(301, {
- Location: expectedLocation,
- });
- res.end();
+ const html = subpathNotUsedTemplate(devRoot, pathname);
+ return writeHtmlResponse(res, 404, html);
} else {
next();
}
diff --git a/packages/astro/src/vite-plugin-astro-server/common.ts b/packages/astro/src/vite-plugin-astro-server/common.ts
deleted file mode 100644
index 9e331232c1c72..0000000000000
--- a/packages/astro/src/vite-plugin-astro-server/common.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import type { Logger } from '../core/logger/core.js';
-import * as msg from '../core/messages.js';
-
-export function log404(logger: Logger, pathname: string) {
- logger.info('serve', msg.req({ url: pathname, statusCode: 404 }));
-}
diff --git a/packages/astro/src/vite-plugin-astro-server/error.ts b/packages/astro/src/vite-plugin-astro-server/error.ts
new file mode 100644
index 0000000000000..cbab6e7be0904
--- /dev/null
+++ b/packages/astro/src/vite-plugin-astro-server/error.ts
@@ -0,0 +1,38 @@
+import type { ModuleLoader } from '../core/module-loader/index.js';
+import type { AstroConfig } from '../@types/astro.js';
+import type DevPipeline from './devPipeline.js';
+
+import { collectErrorMetadata } from '../core/errors/dev/index.js';
+import { createSafeError } from '../core/errors/index.js';
+import { formatErrorMessage } from '../core/messages.js';
+import { eventError, telemetry } from '../events/index.js';
+
+export function recordServerError(
+ loader: ModuleLoader,
+ config: AstroConfig,
+ pipeline: DevPipeline,
+ _err: unknown
+) {
+ const err = createSafeError(_err);
+
+ // This could be a runtime error from Vite's SSR module, so try to fix it here
+ try {
+ loader.fixStacktrace(err);
+ } catch {}
+
+ // This is our last line of defense regarding errors where we still might have some information about the request
+ // Our error should already be complete, but let's try to add a bit more through some guesswork
+ const errorWithMetadata = collectErrorMetadata(err, config.root);
+
+ telemetry.record(eventError({ cmd: 'dev', err: errorWithMetadata, isFatal: false }));
+
+ pipeline.logger.error(
+ null,
+ formatErrorMessage(errorWithMetadata, pipeline.logger.level() === 'debug')
+ );
+
+ return {
+ error: err,
+ errorWithMetadata,
+ };
+}
diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts
index b8f4ab661aba9..67aef0babadd4 100644
--- a/packages/astro/src/vite-plugin-astro-server/plugin.ts
+++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts
@@ -10,6 +10,12 @@ import { baseMiddleware } from './base.js';
import { createController } from './controller.js';
import DevPipeline from './devPipeline.js';
import { handleRequest } from './request.js';
+import { AstroError, AstroErrorData } from '../core/errors/index.js';
+import { getViteErrorPayload } from '../core/errors/dev/index.js';
+import { AsyncLocalStorage } from 'node:async_hooks';
+import { IncomingMessage } from 'node:http';
+import { setRouteError } from './server-state.js';
+import { recordServerError } from './error.js';
export interface AstroPluginOptions {
settings: AstroSettings;
@@ -30,6 +36,7 @@ export default function createVitePluginAstroServer({
const pipeline = new DevPipeline({ logger, manifest, settings, loader });
let manifestData: ManifestData = createRouteManifest({ settings, fsMod }, logger);
const controller = createController({ loader });
+ const localStorage = new AsyncLocalStorage();
/** rebuild the route cache + manifest, as needed. */
function rebuildManifest(needsManifestRebuild: boolean) {
@@ -43,6 +50,25 @@ export default function createVitePluginAstroServer({
viteServer.watcher.on('unlink', rebuildManifest.bind(null, true));
viteServer.watcher.on('change', rebuildManifest.bind(null, false));
+ function handleUnhandledRejection(rejection: any) {
+ const error = new AstroError({
+ ...AstroErrorData.UnhandledRejection,
+ message: AstroErrorData.UnhandledRejection.message(rejection?.stack || rejection),
+ });
+ const store = localStorage.getStore();
+ if (store instanceof IncomingMessage) {
+ const request = store;
+ setRouteError(controller.state, request.url!, error);
+ }
+ const { errorWithMetadata } = recordServerError(loader, settings.config, pipeline, error);
+ setTimeout(
+ async () => loader.webSocketSend(await getViteErrorPayload(errorWithMetadata)),
+ 200
+ );
+ }
+
+ process.on('unhandledRejection', handleUnhandledRejection);
+
return () => {
// Push this middleware to the front of the stack so that it can intercept responses.
// fix(#6067): always inject this to ensure zombie base handling is killed after restarts
@@ -57,13 +83,15 @@ export default function createVitePluginAstroServer({
response.end();
return;
}
- handleRequest({
- pipeline,
- manifestData,
- controller,
- incomingRequest: request,
- incomingResponse: response,
- manifest,
+ localStorage.run(request, () => {
+ handleRequest({
+ pipeline,
+ manifestData,
+ controller,
+ incomingRequest: request,
+ incomingResponse: response,
+ manifest,
+ });
});
});
};
@@ -87,12 +115,12 @@ export default function createVitePluginAstroServer({
*/
export function createDevelopmentManifest(settings: AstroSettings): SSRManifest {
let i18nManifest: SSRManifestI18n | undefined = undefined;
- if (settings.config.experimental.i18n) {
+ if (settings.config.i18n) {
i18nManifest = {
- fallback: settings.config.experimental.i18n.fallback,
- routingStrategy: settings.config.experimental.i18n.routingStrategy,
- defaultLocale: settings.config.experimental.i18n.defaultLocale,
- locales: settings.config.experimental.i18n.locales,
+ fallback: settings.config.i18n.fallback,
+ routing: settings.config.i18n.routing,
+ defaultLocale: settings.config.i18n.defaultLocale,
+ locales: settings.config.i18n.locales,
};
}
return {
diff --git a/packages/astro/src/vite-plugin-astro-server/request.ts b/packages/astro/src/vite-plugin-astro-server/request.ts
index 65e97b96ff1a8..5dd507c75b8d1 100644
--- a/packages/astro/src/vite-plugin-astro-server/request.ts
+++ b/packages/astro/src/vite-plugin-astro-server/request.ts
@@ -1,16 +1,13 @@
import type http from 'node:http';
import type { ManifestData, SSRManifest } from '../@types/astro.js';
-import { collectErrorMetadata } from '../core/errors/dev/index.js';
-import { createSafeError } from '../core/errors/index.js';
-import * as msg from '../core/messages.js';
import { collapseDuplicateSlashes, removeTrailingForwardSlash } from '../core/path.js';
-import { eventError, telemetry } from '../events/index.js';
import { isServerLikeOutput } from '../prerender/utils.js';
import type { DevServerController } from './controller.js';
import { runWithErrorHandling } from './controller.js';
import type DevPipeline from './devPipeline.js';
import { handle500Response } from './response.js';
import { handleRoute, matchRoute } from './route.js';
+import { recordServerError } from './error.js';
type HandleRequest = {
pipeline: DevPipeline;
@@ -89,23 +86,9 @@ export async function handleRequest({
});
},
onError(_err) {
- const err = createSafeError(_err);
-
- // This could be a runtime error from Vite's SSR module, so try to fix it here
- try {
- moduleLoader.fixStacktrace(err);
- } catch {}
-
- // This is our last line of defense regarding errors where we still might have some information about the request
- // Our error should already be complete, but let's try to add a bit more through some guesswork
- const errorWithMetadata = collectErrorMetadata(err, config.root);
-
- telemetry.record(eventError({ cmd: 'dev', err: errorWithMetadata, isFatal: false }));
-
- pipeline.logger.error(null, msg.formatErrorMessage(errorWithMetadata));
+ const { error, errorWithMetadata } = recordServerError(moduleLoader, config, pipeline, _err);
handle500Response(moduleLoader, incomingResponse, errorWithMetadata);
-
- return err;
+ return error;
},
});
}
diff --git a/packages/astro/src/vite-plugin-astro-server/response.ts b/packages/astro/src/vite-plugin-astro-server/response.ts
index b1c9480956b41..ac36e703b0723 100644
--- a/packages/astro/src/vite-plugin-astro-server/response.ts
+++ b/packages/astro/src/vite-plugin-astro-server/response.ts
@@ -68,7 +68,7 @@ export async function writeWebResponse(res: http.ServerResponse, webResponse: Re
// Previously, `headers.entries()` would already have these merged, but it seems like this isn't the case anymore.
if (headers.has('set-cookie')) {
if ('getSetCookie' in headers && typeof headers.getSetCookie === 'function') {
- _headers['set-cookie'] = headers.getSetCookie();
+ _headers['set-cookie'] = headers.getSetCookie().toString();
} else {
_headers['set-cookie'] = headers.get('set-cookie')!;
}
diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts
index 48f89db043a30..3196b951a796a 100644
--- a/packages/astro/src/vite-plugin-astro-server/route.ts
+++ b/packages/astro/src/vite-plugin-astro-server/route.ts
@@ -2,13 +2,17 @@ import type http from 'node:http';
import { fileURLToPath } from 'node:url';
import type {
ComponentInstance,
+ DevOverlayMetadata,
ManifestData,
- MiddlewareEndpointHandler,
+ MiddlewareHandler,
RouteData,
SSRElement,
SSRManifest,
} from '../@types/astro.js';
+import { getInfoOutput } from '../cli/info/index.js';
+import { ASTRO_VERSION } from '../core/constants.js';
import { AstroErrorData, isAstroError } from '../core/errors/index.js';
+import { req } from '../core/messages.js';
import { sequence } from '../core/middleware/index.js';
import { loadMiddleware } from '../core/middleware/loadMiddleware.js';
import {
@@ -24,13 +28,13 @@ import { createI18nMiddleware, i18nPipelineHook } from '../i18n/middleware.js';
import { getSortedPreloadedMatches } from '../prerender/routing.js';
import { isServerLikeOutput } from '../prerender/utils.js';
import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
-import { log404 } from './common.js';
import { getStylesForURL } from './css.js';
import type DevPipeline from './devPipeline.js';
import { preload } from './index.js';
import { getComponentMetadata } from './metadata.js';
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
import { getScriptsForURL } from './scripts.js';
+import { normalizeTheLocale } from '../i18n/index.js';
const clientLocalsSymbol = Symbol.for('astro.locals');
@@ -48,6 +52,10 @@ export interface MatchedRoute {
mod: ComponentInstance;
}
+function isLoggedRequest(url: string) {
+ return url !== '/favicon.ico';
+}
+
function getCustom404Route(manifestData: ManifestData): RouteData | undefined {
const route404 = /^\/404\/?$/;
return manifestData.routes.find((r) => route404.test(r.route));
@@ -108,14 +116,13 @@ export async function matchRoute(
const possibleRoutes = matches.flatMap((route) => route.component);
pipeline.logger.warn(
- 'getStaticPaths',
+ 'router',
`${AstroErrorData.NoMatchingStaticPathFound.message(
pathname
)}\n\n${AstroErrorData.NoMatchingStaticPathFound.hint(possibleRoutes)}`
);
}
- log404(logger, pathname);
const custom404 = getCustom404Route(manifestData);
if (custom404) {
@@ -161,11 +168,15 @@ export async function handleRoute({
incomingResponse,
manifest,
}: HandleRoute): Promise {
+ const timeStart = performance.now();
const env = pipeline.getEnvironment();
const config = pipeline.getConfig();
const moduleLoader = pipeline.getModuleLoader();
const { logger } = env;
- if (!matchedRoute && !config.experimental.i18n) {
+ if (!matchedRoute && !config.i18n) {
+ if (isLoggedRequest(pathname)) {
+ logger.info(null, req({ url: pathname, method: incomingRequest.method, statusCode: 404 }));
+ }
return handle404Response(origin, incomingRequest, incomingResponse);
}
@@ -179,13 +190,27 @@ export async function handleRoute({
const middleware = await loadMiddleware(moduleLoader);
if (!matchedRoute) {
- if (config.experimental.i18n) {
- const locales = config.experimental.i18n.locales;
+ if (config.i18n) {
+ const locales = config.i18n.locales;
const pathNameHasLocale = pathname
.split('/')
.filter(Boolean)
.some((segment) => {
- return locales.includes(segment);
+ let found = false;
+ for (const locale of locales) {
+ if (typeof locale === 'string') {
+ if (normalizeTheLocale(locale) === normalizeTheLocale(segment)) {
+ found = true;
+ break;
+ }
+ } else {
+ if (locale.path === segment) {
+ found = true;
+ break;
+ }
+ }
+ }
+ return found;
});
// Even when we have `config.base`, the pathname is still `/` because it gets stripped before
if (!pathNameHasLocale && pathname !== '/') {
@@ -208,6 +233,7 @@ export async function handleRoute({
segments: [],
type: 'fallback',
route: '',
+ fallbackRoutes: [],
};
renderContext = await createRenderContext({
request,
@@ -216,7 +242,7 @@ export async function handleRoute({
mod,
route,
locales: manifest.i18n?.locales,
- routingStrategy: manifest.i18n?.routingStrategy,
+ routing: manifest.i18n?.routing,
defaultLocale: manifest.i18n?.defaultLocale,
});
} else {
@@ -262,7 +288,7 @@ export async function handleRoute({
filePath: options.filePath,
});
- const i18n = pipeline.getConfig().experimental.i18n;
+ const i18n = pipeline.getConfig().i18n;
renderContext = await createRenderContext({
request: options.request,
@@ -275,18 +301,14 @@ export async function handleRoute({
mod,
env,
locales: i18n?.locales,
- routingStrategy: i18n?.routingStrategy,
+ routing: i18n?.routing,
defaultLocale: i18n?.defaultLocale,
});
}
- const onRequest = middleware?.onRequest as MiddlewareEndpointHandler | undefined;
- if (config.experimental.i18n) {
- const i18Middleware = createI18nMiddleware(
- config.experimental.i18n,
- config.base,
- config.trailingSlash
- );
+ const onRequest = middleware?.onRequest as MiddlewareHandler | undefined;
+ if (config.i18n) {
+ const i18Middleware = createI18nMiddleware(config.i18n, config.base, config.trailingSlash);
if (i18Middleware) {
if (onRequest) {
@@ -303,6 +325,18 @@ export async function handleRoute({
}
let response = await pipeline.renderRoute(renderContext, mod);
+ if (isLoggedRequest(pathname)) {
+ const timeEnd = performance.now();
+ logger.info(
+ null,
+ req({
+ url: pathname,
+ method: incomingRequest.method,
+ statusCode: status ?? response.status,
+ reqTime: timeEnd - timeStart,
+ })
+ );
+ }
if (response.status === 404 && has404Route(manifestData)) {
const fourOhFourRoute = await matchRoute('/404', manifestData, pipeline);
if (options && fourOhFourRoute?.route !== options.route)
@@ -322,24 +356,23 @@ export async function handleRoute({
}
if (route.type === 'endpoint') {
await writeWebResponse(incomingResponse, response);
- } else {
- if (
- // We are in a recursion, and it's possible that this function is called itself with a status code
- // By default, the status code passed via parameters is computed by the matched route.
- //
- // By default, we should give priority to the status code passed, although it's possible that
- // the `Response` emitted by the user is a redirect. If so, then return the returned response.
- response.status < 400 &&
- response.status >= 300
- ) {
- await writeSSRResult(request, response, incomingResponse);
- return;
- } else if (status && response.status !== status && (status === 404 || status === 500)) {
- // Response.status is read-only, so a clone is required to override
- response = new Response(response.body, { ...response, status });
- }
+ return;
+ }
+ // We are in a recursion, and it's possible that this function is called itself with a status code
+ // By default, the status code passed via parameters is computed by the matched route.
+ //
+ // By default, we should give priority to the status code passed, although it's possible that
+ // the `Response` emitted by the user is a redirect. If so, then return the returned response.
+ if (response.status < 400 && response.status >= 300) {
await writeSSRResult(request, response, incomingResponse);
+ return;
+ }
+ // Apply the `status` override to the response object before responding.
+ // Response.status is read-only, so a clone is required to override.
+ if (status && response.status !== status && (status === 404 || status === 500)) {
+ response = new Response(response.body, { ...response, status });
}
+ await writeSSRResult(request, response, incomingResponse);
}
interface GetScriptsAndStylesParams {
@@ -361,7 +394,10 @@ async function getScriptsAndStyles({ pipeline, filePath }: GetScriptsAndStylesPa
children: '',
});
- if (settings.config.experimental.devOverlay) {
+ if (
+ settings.config.devToolbar.enabled &&
+ (await settings.preferences.get('devToolbar.enabled'))
+ ) {
scripts.add({
props: {
type: 'module',
@@ -370,12 +406,16 @@ async function getScriptsAndStyles({ pipeline, filePath }: GetScriptsAndStylesPa
children: '',
});
+ const additionalMetadata: DevOverlayMetadata['__astro_dev_overlay__'] = {
+ root: fileURLToPath(settings.config.root),
+ version: ASTRO_VERSION,
+ debugInfo: await getInfoOutput({ userConfig: settings.config, print: false }),
+ };
+
// Additional data for the dev overlay
scripts.add({
props: {},
- children: `window.__astro_dev_overlay__ = {root: ${JSON.stringify(
- fileURLToPath(settings.config.root)
- )}}`,
+ children: `window.__astro_dev_overlay__ = ${JSON.stringify(additionalMetadata)}`,
});
}
}
diff --git a/packages/astro/src/vite-plugin-astro/hmr.ts b/packages/astro/src/vite-plugin-astro/hmr.ts
index 27cc2d10fca81..d06a8338f6624 100644
--- a/packages/astro/src/vite-plugin-astro/hmr.ts
+++ b/packages/astro/src/vite-plugin-astro/hmr.ts
@@ -1,23 +1,11 @@
-import { slash } from '@astrojs/internal-helpers/path';
-import { fileURLToPath } from 'node:url';
import type { HmrContext, ModuleNode } from 'vite';
import type { AstroConfig } from '../@types/astro.js';
-import {
- cachedCompilation,
- invalidateCompilation,
- isCached,
- type CompileResult,
-} from '../core/compile/index.js';
+import type { cachedCompilation } from '../core/compile/index.js';
+import { invalidateCompilation, isCached, type CompileResult } from '../core/compile/index.js';
import type { Logger } from '../core/logger/core.js';
-import * as msg from '../core/messages.js';
+import { isAstroSrcFile } from '../core/logger/vite.js';
import { isAstroScript } from './query.js';
-const PKG_PREFIX = fileURLToPath(new URL('../../', import.meta.url));
-const E2E_PREFIX = fileURLToPath(new URL('../../e2e', import.meta.url));
-const isPkgFile = (id: string | null) => {
- return id?.startsWith(PKG_PREFIX) && !id.startsWith(E2E_PREFIX);
-};
-
export interface HandleHotUpdateOptions {
config: AstroConfig;
logger: Logger;
@@ -47,7 +35,7 @@ export async function handleHotUpdate(
}
// Skip monorepo files to avoid console spam
- if (isPkgFile(ctx.file)) {
+ if (isAstroSrcFile(ctx.file)) {
return;
}
@@ -57,7 +45,7 @@ export async function handleHotUpdate(
const files = new Set();
for (const mod of ctx.modules) {
// Skip monorepo files to avoid console spam
- if (isPkgFile(mod.id ?? mod.file)) {
+ if (isAstroSrcFile(mod.id ?? mod.file)) {
filtered.delete(mod);
continue;
}
@@ -92,13 +80,12 @@ 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 = [...filtered].filter((m) => !m.url.endsWith('='));
- const file = ctx.file.replace(slash(fileURLToPath(config.root)), '/');
// If only styles are changed, remove the component file from the update list
if (isStyleOnlyChange) {
- logger.info('astro', msg.hmr({ file, style: true }));
- // remove base file and hoisted scripts
- return mods.filter((mod) => mod.id !== ctx.file && !mod.id?.endsWith('.ts'));
+ logger.debug('watch', 'style-only change');
+ // Only return the Astro styles that have changed!
+ return mods.filter((mod) => mod.id?.includes('astro&type=style'));
}
// Add hoisted scripts so these get invalidated
@@ -110,15 +97,6 @@ export async function handleHotUpdate(
}
}
- // TODO: Svelte files should be marked as `isSelfAccepting` but they don't appear to be
- const isSelfAccepting = mods.every((m) => m.isSelfAccepting || m.url.endsWith('.svelte'));
- if (isSelfAccepting) {
- if (/astro\.config\.[cm][jt]s$/.test(file)) return mods;
- logger.info('astro', msg.hmr({ file }));
- } else {
- logger.info('astro', msg.reload({ file }));
- }
-
return mods;
}
diff --git a/packages/astro/src/vite-plugin-astro/index.ts b/packages/astro/src/vite-plugin-astro/index.ts
index 1649d80691513..d02d78d6ae6a3 100644
--- a/packages/astro/src/vite-plugin-astro/index.ts
+++ b/packages/astro/src/vite-plugin-astro/index.ts
@@ -139,6 +139,7 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl
const compileProps: CompileProps = {
astroConfig: config,
viteConfig: resolvedConfig,
+ preferences: settings.preferences,
filename: normalizePath(parsedId.filename),
source,
};
@@ -173,18 +174,21 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl
},
async handleHotUpdate(context) {
if (context.server.config.isProduction) return;
- const compileProps: CompileProps = {
- astroConfig: config,
- viteConfig: resolvedConfig,
- filename: context.file,
- source: await context.read(),
- };
- const compile = () => cachedCompilation(compileProps);
+ const filename = context.file;
+ const source = await context.read();
+ const compile = () =>
+ cachedCompilation({
+ astroConfig: config,
+ viteConfig: resolvedConfig,
+ preferences: settings.preferences,
+ filename,
+ source,
+ });
return handleHotUpdate(context, {
config,
logger,
compile,
- source: compileProps.source,
+ source,
});
},
};
diff --git a/packages/astro/src/vite-plugin-dev-overlay/vite-plugin-dev-overlay.ts b/packages/astro/src/vite-plugin-dev-overlay/vite-plugin-dev-overlay.ts
index 5c3aabe5ac6d8..201e6aac63f57 100644
--- a/packages/astro/src/vite-plugin-dev-overlay/vite-plugin-dev-overlay.ts
+++ b/packages/astro/src/vite-plugin-dev-overlay/vite-plugin-dev-overlay.ts
@@ -16,7 +16,7 @@ export default function astroDevOverlay({ settings }: AstroPluginOptions): vite.
if (id === resolvedVirtualModuleId) {
return `
export const loadDevOverlayPlugins = async () => {
- return [${settings.devOverlayPlugins
+ return [${settings.devToolbarApps
.map((plugin) => `(await import('${plugin}')).default`)
.join(',')}];
};
diff --git a/packages/astro/src/vite-plugin-fileurl/index.ts b/packages/astro/src/vite-plugin-fileurl/index.ts
new file mode 100644
index 0000000000000..4a14323a07597
--- /dev/null
+++ b/packages/astro/src/vite-plugin-fileurl/index.ts
@@ -0,0 +1,13 @@
+import type { Plugin as VitePlugin } from 'vite';
+
+export default function vitePluginFileURL({}): VitePlugin {
+ return {
+ name: 'astro:vite-plugin-file-url',
+ resolveId(source, importer) {
+ if (source.startsWith('file://')) {
+ const rest = source.slice(7);
+ return this.resolve(rest, importer);
+ }
+ },
+ };
+}
diff --git a/packages/astro/src/vite-plugin-inject-env-ts/index.ts b/packages/astro/src/vite-plugin-inject-env-ts/index.ts
index d884075ab09e7..855d3a2e05386 100644
--- a/packages/astro/src/vite-plugin-inject-env-ts/index.ts
+++ b/packages/astro/src/vite-plugin-inject-env-ts/index.ts
@@ -50,16 +50,6 @@ export async function setUpEnvTs({
if (fs.existsSync(envTsPath)) {
let typesEnvContents = await fs.promises.readFile(envTsPath, 'utf-8');
- // TODO: Remove this in 4.0, this code is only to help users migrate away from assets being experimental for a long time
- if (typesEnvContents.includes('types="astro/client-image"')) {
- typesEnvContents = typesEnvContents.replace(
- 'types="astro/client-image"',
- 'types="astro/client"'
- );
- await fs.promises.writeFile(envTsPath, typesEnvContents, 'utf-8');
- logger.info('assets', `Removed ${bold(envTsPathRelativetoRoot)} types`);
- }
-
if (!fs.existsSync(dotAstroDir))
// Add `.astro` types reference if none exists
return;
@@ -68,7 +58,7 @@ export async function setUpEnvTs({
if (!typesEnvContents.includes(expectedTypeReference)) {
typesEnvContents = `${expectedTypeReference}\n${typesEnvContents}`;
await fs.promises.writeFile(envTsPath, typesEnvContents, 'utf-8');
- logger.info('content', `Added ${bold(envTsPathRelativetoRoot)} types`);
+ logger.info('types', `Added ${bold(envTsPathRelativetoRoot)} type declarations`);
}
} else {
// Otherwise, inject the `env.d.ts` file
@@ -81,6 +71,6 @@ export async function setUpEnvTs({
await fs.promises.mkdir(settings.config.srcDir, { recursive: true });
await fs.promises.writeFile(envTsPath, referenceDefs.join('\n'), 'utf-8');
- logger.info('astro', `Added ${bold(envTsPathRelativetoRoot)} types`);
+ logger.info('types', `Added ${bold(envTsPathRelativetoRoot)} type declarations`);
}
}
diff --git a/packages/astro/src/vite-plugin-integrations-container/index.ts b/packages/astro/src/vite-plugin-integrations-container/index.ts
index b18d2e5e99561..2125df857d9fb 100644
--- a/packages/astro/src/vite-plugin-integrations-container/index.ts
+++ b/packages/astro/src/vite-plugin-integrations-container/index.ts
@@ -34,7 +34,7 @@ async function resolveEntryPoint(
this: PluginContext,
route: InjectedRoute
): Promise {
- const resolvedId = await this.resolve(route.entryPoint)
+ const resolvedId = await this.resolve(route.entrypoint)
.then((res) => res?.id)
.catch(() => undefined);
if (!resolvedId) return route;
diff --git a/packages/astro/src/vite-plugin-load-fallback/index.ts b/packages/astro/src/vite-plugin-load-fallback/index.ts
index e11f317ca8f08..80db39edd4330 100644
--- a/packages/astro/src/vite-plugin-load-fallback/index.ts
+++ b/packages/astro/src/vite-plugin-load-fallback/index.ts
@@ -2,6 +2,7 @@ import nodeFs from 'node:fs';
import npath from 'node:path';
import type * as vite from 'vite';
import { slash } from '../core/path.js';
+import { cleanUrl } from '../vite-plugin-utils/index.js';
type NodeFileSystemModule = typeof nodeFs;
@@ -77,8 +78,3 @@ export default function loadFallbackPlugin({
},
];
}
-
-const queryRE = /\?.*$/s;
-const hashRE = /#.*$/s;
-
-const cleanUrl = (url: string): string => url.replace(hashRE, '').replace(queryRE, '');
diff --git a/packages/astro/src/vite-plugin-scanner/index.ts b/packages/astro/src/vite-plugin-scanner/index.ts
index d48aed203dde7..678f3ac187d8a 100644
--- a/packages/astro/src/vite-plugin-scanner/index.ts
+++ b/packages/astro/src/vite-plugin-scanner/index.ts
@@ -52,12 +52,11 @@ export default function astroScannerPlugin({
// this should only be valid for `.astro`, `.js` and `.ts` files
KNOWN_FILE_EXTENSIONS.includes(extname(filename))
) {
- const reason = ` because \`output: "${settings.config.output}"\` is set`;
logger.warn(
- 'getStaticPaths',
- `The getStaticPaths() statement in ${bold(
+ 'router',
+ `getStaticPaths() ignored in dynamic page ${bold(
rootRelativePath(settings.config.root, fileURL, true)
- )} has been ignored${reason}.\n\nAdd \`export const prerender = true;\` to prerender this page.`
+ )}. Add \`export const prerender = true;\` to prerender the page as static HTML during the build process.`
);
}
diff --git a/packages/astro/src/vite-plugin-scanner/scan.ts b/packages/astro/src/vite-plugin-scanner/scan.ts
index 3eb834ea24db5..6c277567d4161 100644
--- a/packages/astro/src/vite-plugin-scanner/scan.ts
+++ b/packages/astro/src/vite-plugin-scanner/scan.ts
@@ -72,7 +72,7 @@ export async function scan(
message: AstroErrorData.InvalidPrerenderExport.message(
prefix,
suffix,
- settings?.config.output === 'hybrid' ?? false
+ settings?.config.output === 'hybrid'
),
location: { file: id },
});
diff --git a/packages/astro/src/vite-plugin-scripts/index.ts b/packages/astro/src/vite-plugin-scripts/index.ts
index 0066b98f5e703..9b2848923752a 100644
--- a/packages/astro/src/vite-plugin-scripts/index.ts
+++ b/packages/astro/src/vite-plugin-scripts/index.ts
@@ -50,8 +50,7 @@ export default function astroScriptsPlugin({ settings }: { settings: AstroSettin
},
buildStart() {
const hasHydrationScripts = settings.scripts.some((s) => s.stage === 'before-hydration');
- // @ts-expect-error Vite 5 renamed `ssrBuild` to `isSsrBuild`
- const isSsrBuild = env?.ssrBuild || env?.isSsrBuild;
+ const isSsrBuild = env?.isSsrBuild;
if (hasHydrationScripts && env?.command === 'build' && !isSsrBuild) {
this.emitFile({
type: 'chunk',
diff --git a/packages/astro/src/vite-plugin-utils/index.ts b/packages/astro/src/vite-plugin-utils/index.ts
index 51f0e6cc4c1c1..7bf9f092f618d 100644
--- a/packages/astro/src/vite-plugin-utils/index.ts
+++ b/packages/astro/src/vite-plugin-utils/index.ts
@@ -56,3 +56,8 @@ export function normalizeFilename(filename: string, root: URL) {
}
return removeLeadingForwardSlashWindows(filename);
}
+
+const postfixRE = /[?#].*$/s;
+export function cleanUrl(url: string): string {
+ return url.replace(postfixRE, '');
+}
diff --git a/packages/astro/test/astro-basic.test.js b/packages/astro/test/astro-basic.test.js
index 37a5693cf315b..78c3a0ed5e7b6 100644
--- a/packages/astro/test/astro-basic.test.js
+++ b/packages/astro/test/astro-basic.test.js
@@ -151,6 +151,19 @@ describe('Astro basics', () => {
expect($('body > :nth-child(5)').prop('outerHTML')).to.equal('');
});
+ it('Generates pages that end with .mjs', async () => {
+ const content1 = await fixture.readFile('/get-static-paths-with-mjs/example.mjs');
+ expect(content1).to.be.ok;
+ const content2 = await fixture.readFile('/get-static-paths-with-mjs/example.js');
+ expect(content2).to.be.ok;
+ });
+
+ it('allows file:// urls as module specifiers', async () => {
+ const html = await fixture.readFile('/fileurl/index.html');
+ const $ = cheerio.load(html);
+ expect($('h1').text()).to.equal('WORKS');
+ });
+
describe('preview', () => {
it('returns 200 for valid URLs', async () => {
const result = await fixture.fetch('/');
diff --git a/packages/astro/test/astro-markdown-drafts.test.js b/packages/astro/test/astro-markdown-drafts.test.js
deleted file mode 100644
index 8f6c753e4c5f8..0000000000000
--- a/packages/astro/test/astro-markdown-drafts.test.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { expect } from 'chai';
-import * as cheerio from 'cheerio';
-import { loadFixture } from './test-utils.js';
-
-describe('Astro Markdown with draft posts disabled', () => {
- let fixture;
-
- before(async () => {
- fixture = await loadFixture({
- root: './fixtures/astro-markdown-drafts/',
- });
- await fixture.build();
- });
- it('Does not render the draft post', async () => {
- let renderedDraft = false;
- try {
- await fixture.readFile('/wip/index.html');
- renderedDraft = true;
- } catch (err) {
- expect(err.code).to.equal('ENOENT');
- }
- expect(renderedDraft).to.equal(false, 'Rendered a draft post');
- });
-});
-
-describe('Astro Markdown with draft posts enabled', () => {
- let fixture;
-
- before(async () => {
- fixture = await loadFixture({
- root: './fixtures/astro-markdown-drafts/',
- markdown: {
- drafts: true,
- },
- });
- await fixture.build();
- });
- it('Renders the draft post', async () => {
- const html = await fixture.readFile('/wip/index.html');
- const $ = cheerio.load(html);
- expect($('h1').length).to.be.ok;
- expect($('h1').text()).to.equal('WIP');
- });
-});
diff --git a/packages/astro/test/astro-markdown-remarkRehype.test.js b/packages/astro/test/astro-markdown-remarkRehype.test.js
index 59b0eeda6f158..17b20b51ea220 100644
--- a/packages/astro/test/astro-markdown-remarkRehype.test.js
+++ b/packages/astro/test/astro-markdown-remarkRehype.test.js
@@ -15,7 +15,7 @@ describe('Astro Markdown without remark-rehype config', () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
expect($('#footnote-label').text()).to.equal('Footnotes');
- expect($('.data-footnote-backref').first().attr('aria-label')).to.equal('Back to content');
+ expect($('.data-footnote-backref').first().attr('aria-label')).to.equal('Back to reference 1');
});
});
diff --git a/packages/astro/test/astro-markdown-shiki.test.js b/packages/astro/test/astro-markdown-shiki.test.js
index 6315edbffbad3..459fc646457bd 100644
--- a/packages/astro/test/astro-markdown-shiki.test.js
+++ b/packages/astro/test/astro-markdown-shiki.test.js
@@ -95,10 +95,6 @@ describe('Astro Markdown Shiki', () => {
const unknownLang = $('.astro-code').get(1);
expect(unknownLang.attribs.style).to.contain('background-color:#24292e;color:#e1e4e8;');
-
- const caddyLang = $('.astro-code').last();
- const caddySegments = caddyLang.find('.line');
- expect(caddySegments.get(1).children[0].attribs.style).to.contain('color:#B392F0');
});
});
diff --git a/packages/astro/test/astro-scripts.test.js b/packages/astro/test/astro-scripts.test.js
index ae2268d8097f9..f36e24d924d3e 100644
--- a/packages/astro/test/astro-scripts.test.js
+++ b/packages/astro/test/astro-scripts.test.js
@@ -145,7 +145,7 @@ describe('Scripts (hoisted and not)', () => {
hooks: {
'astro:config:setup': ({ injectRoute, injectScript }) => {
injectScript('page', `import '/src/scripts/something.js';`);
- injectRoute({ pattern: 'injected-route', entryPoint: 'src/external-page.astro' });
+ injectRoute({ pattern: 'injected-route', entrypoint: 'src/external-page.astro' });
},
},
},
diff --git a/packages/astro/test/astro-slots.test.js b/packages/astro/test/astro-slots.test.js
index 34f1d82255ccd..69a0025e1f275 100644
--- a/packages/astro/test/astro-slots.test.js
+++ b/packages/astro/test/astro-slots.test.js
@@ -40,13 +40,20 @@ describe('Slots', () => {
expect($('#default').text().trim()).to.equal('Default');
});
- it('Slots render fallback content by default', async () => {
+ it('Slots of a component render fallback content by default', async () => {
const html = await fixture.readFile('/fallback/index.html');
const $ = cheerio.load(html);
expect($('#default')).to.have.lengthOf(1);
});
+ it('Slots of a page render fallback content', async () => {
+ const html = await fixture.readFile('/fallback-own/index.html');
+ const $ = cheerio.load(html);
+
+ expect($('#default')).to.have.lengthOf(1);
+ });
+
it('Slots override fallback content', async () => {
const html = await fixture.readFile('/fallback-override/index.html');
const $ = cheerio.load(html);
diff --git a/packages/astro/test/cli.test.js b/packages/astro/test/cli.test.js
index 82cf7a12dca8e..caec4241daceb 100644
--- a/packages/astro/test/cli.test.js
+++ b/packages/astro/test/cli.test.js
@@ -106,7 +106,7 @@ describe('astro cli', () => {
expect(messages[0]).to.contain('astro');
expect(messages[0]).to.contain(pkgVersion);
- expect(messages[0]).to.contain('started in');
+ expect(messages[0]).to.contain('ready in');
});
['dev', 'preview'].forEach((cmd) => {
diff --git a/packages/astro/test/core-image.test.js b/packages/astro/test/core-image.test.js
index 74f86ba7ff6e8..0d9b690b4817f 100644
--- a/packages/astro/test/core-image.test.js
+++ b/packages/astro/test/core-image.test.js
@@ -640,14 +640,13 @@ describe('astro:image', () => {
expect(logs[0].message).to.contain('Expected getImage() parameter');
});
- // TODO: For some reason, this error crashes the dev server?
- it.skip('properly error when src is undefined', async () => {
+ it('properly error when src is undefined', async () => {
logs.length = 0;
let res = await fixture.fetch('/get-image-undefined');
await res.text();
expect(logs).to.have.a.lengthOf(1);
- expect(logs[0].message).to.contain('Expected src to be an image.');
+ expect(logs[0].message).to.contain('Expected `src` property');
});
it('properly error image in Markdown frontmatter is not found', async () => {
@@ -663,7 +662,6 @@ describe('astro:image', () => {
logs.length = 0;
let res = await fixture.fetch('/post');
await res.text();
-
expect(logs).to.have.a.lengthOf(1);
expect(logs[0].message).to.contain('Could not find requested image');
});
diff --git a/packages/astro/test/events.test.js b/packages/astro/test/events.test.js
index b0732a15c61ba..c0c6266d9f1e7 100644
--- a/packages/astro/test/events.test.js
+++ b/packages/astro/test/events.test.js
@@ -105,7 +105,6 @@ describe('Events', () => {
config: 'path/to/config.mjs',
experimentalSsr: true,
experimentalIntegrations: true,
- drafts: true,
};
const [{ payload }] = events.eventCliSession(
{
@@ -122,7 +121,6 @@ describe('Events', () => {
'config',
'experimentalSsr',
'experimentalIntegrations',
- 'drafts',
]);
});
});
diff --git a/packages/astro/test/featuresSupport.test.js b/packages/astro/test/featuresSupport.test.js
index c25d9d5f8f08c..15606b927c1de 100644
--- a/packages/astro/test/featuresSupport.test.js
+++ b/packages/astro/test/featuresSupport.test.js
@@ -10,14 +10,9 @@ describe('Adapter', () => {
fixture = await loadFixture({
root: './fixtures/middleware space/',
output: 'server',
- build: {
- excludeMiddleware: true,
- },
adapter: testAdapter({
extendAdapter: {
- supportsFeatures: {
- edgeMiddleware: 'Unsupported',
- },
+ supportedAstroFeatures: {},
},
}),
});
@@ -34,14 +29,9 @@ describe('Adapter', () => {
fixture = await loadFixture({
root: './fixtures/middleware space/',
output: 'server',
- build: {
- split: true,
- },
adapter: testAdapter({
extendAdapter: {
- supportsFeatures: {
- functionPerPage: 'Unsupported',
- },
+ supportedAstroFeatures: {},
},
}),
});
diff --git a/packages/astro/test/fixtures/0-css/package.json b/packages/astro/test/fixtures/0-css/package.json
index 6ecfb1d9e925a..fca80fd91b260 100644
--- a/packages/astro/test/fixtures/0-css/package.json
+++ b/packages/astro/test/fixtures/0-css/package.json
@@ -9,7 +9,7 @@
"astro": "workspace:*",
"react": "^18.1.0",
"react-dom": "^18.1.0",
- "svelte": "^4.2.0",
- "vue": "^3.3.4"
+ "svelte": "^4.2.5",
+ "vue": "^3.3.8"
}
}
diff --git a/packages/astro/test/fixtures/alias-tsconfig-baseurl-only/package.json b/packages/astro/test/fixtures/alias-tsconfig-baseurl-only/package.json
index a708628bbc154..a632b84638dcc 100644
--- a/packages/astro/test/fixtures/alias-tsconfig-baseurl-only/package.json
+++ b/packages/astro/test/fixtures/alias-tsconfig-baseurl-only/package.json
@@ -5,6 +5,6 @@
"dependencies": {
"@astrojs/svelte": "workspace:*",
"astro": "workspace:*",
- "svelte": "^4.2.0"
+ "svelte": "^4.2.5"
}
}
diff --git a/packages/astro/test/fixtures/alias-tsconfig/package.json b/packages/astro/test/fixtures/alias-tsconfig/package.json
index e27fc9130d95c..80033bdf9e01f 100644
--- a/packages/astro/test/fixtures/alias-tsconfig/package.json
+++ b/packages/astro/test/fixtures/alias-tsconfig/package.json
@@ -6,6 +6,6 @@
"@astrojs/svelte": "workspace:*",
"@test/namespace-package": "workspace:*",
"astro": "workspace:*",
- "svelte": "^4.2.0"
+ "svelte": "^4.2.5"
}
}
diff --git a/packages/astro/test/fixtures/alias/package.json b/packages/astro/test/fixtures/alias/package.json
index e23fb4d581ef2..74351e6e39e6e 100644
--- a/packages/astro/test/fixtures/alias/package.json
+++ b/packages/astro/test/fixtures/alias/package.json
@@ -5,6 +5,6 @@
"dependencies": {
"@astrojs/svelte": "workspace:*",
"astro": "workspace:*",
- "svelte": "^4.2.0"
+ "svelte": "^4.2.5"
}
}
diff --git a/packages/astro/test/fixtures/api-routes/src/pages/context/data/[param].json.js b/packages/astro/test/fixtures/api-routes/src/pages/context/data/[param].json.js
index d18eb086f9a5e..cb9308ba45540 100644
--- a/packages/astro/test/fixtures/api-routes/src/pages/context/data/[param].json.js
+++ b/packages/astro/test/fixtures/api-routes/src/pages/context/data/[param].json.js
@@ -15,10 +15,10 @@ export function getStaticPaths() {
}
export function GET({ params, request }) {
- return {
- body: JSON.stringify({
+ return new Response(
+ JSON.stringify({
param: params.param,
pathname: new URL(request.url).pathname
})
- };
+ );
}
diff --git a/packages/astro/test/fixtures/astro-basic/package.json b/packages/astro/test/fixtures/astro-basic/package.json
index c2e0be656281d..417636229c93b 100644
--- a/packages/astro/test/fixtures/astro-basic/package.json
+++ b/packages/astro/test/fixtures/astro-basic/package.json
@@ -6,6 +6,6 @@
"@astrojs/mdx": "workspace:*",
"@astrojs/preact": "workspace:*",
"astro": "workspace:*",
- "preact": "^10.17.1"
+ "preact": "^10.19.2"
}
}
diff --git a/packages/astro/test/fixtures/astro-basic/src/pages/fileurl.astro b/packages/astro/test/fixtures/astro-basic/src/pages/fileurl.astro
new file mode 100644
index 0000000000000..e85507941c5db
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-basic/src/pages/fileurl.astro
@@ -0,0 +1,10 @@
+---
+import {capitalize} from 'file://../strings.js';
+---
+
+
+ Testing
+
+ {capitalize('works')
+
+
diff --git a/packages/astro/test/fixtures/astro-basic/src/pages/get-static-paths-with-mjs/[...file].js b/packages/astro/test/fixtures/astro-basic/src/pages/get-static-paths-with-mjs/[...file].js
new file mode 100644
index 0000000000000..8aae81326907e
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-basic/src/pages/get-static-paths-with-mjs/[...file].js
@@ -0,0 +1,10 @@
+export function getStaticPaths() {
+ return [
+ { params: { file: 'example.mjs' } },
+ { params: { file: 'example.js' } },
+ ];
+}
+
+export function GET() {
+ return new Response('console.log("fileContent");')
+}
diff --git a/packages/astro/test/fixtures/astro-basic/src/strings.js b/packages/astro/test/fixtures/astro-basic/src/strings.js
new file mode 100644
index 0000000000000..8d34ca3077f01
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-basic/src/strings.js
@@ -0,0 +1,4 @@
+
+export function capitalize(str) {
+ return str.toUpperCase();
+}
diff --git a/packages/astro/test/fixtures/astro-children/package.json b/packages/astro/test/fixtures/astro-children/package.json
index 5fc2951be5973..1ed097ea12716 100644
--- a/packages/astro/test/fixtures/astro-children/package.json
+++ b/packages/astro/test/fixtures/astro-children/package.json
@@ -7,8 +7,8 @@
"@astrojs/svelte": "workspace:*",
"@astrojs/vue": "workspace:*",
"astro": "workspace:*",
- "preact": "^10.17.1",
- "svelte": "^4.2.0",
- "vue": "^3.3.4"
+ "preact": "^10.19.2",
+ "svelte": "^4.2.5",
+ "vue": "^3.3.8"
}
}
diff --git a/packages/astro/test/fixtures/astro-client-only/package.json b/packages/astro/test/fixtures/astro-client-only/package.json
index a550b3a7b6361..2d55fe99cb4c6 100644
--- a/packages/astro/test/fixtures/astro-client-only/package.json
+++ b/packages/astro/test/fixtures/astro-client-only/package.json
@@ -9,6 +9,6 @@
"astro": "workspace:*",
"react": "^18.1.0",
"react-dom": "^18.1.0",
- "svelte": "^4.2.0"
+ "svelte": "^4.2.5"
}
}
diff --git a/packages/astro/test/fixtures/astro-dynamic/package.json b/packages/astro/test/fixtures/astro-dynamic/package.json
index 2dc810458302c..6209dabe9120c 100644
--- a/packages/astro/test/fixtures/astro-dynamic/package.json
+++ b/packages/astro/test/fixtures/astro-dynamic/package.json
@@ -8,6 +8,6 @@
"astro": "workspace:*",
"react": "^18.1.0",
"react-dom": "^18.1.0",
- "svelte": "^4.2.0"
+ "svelte": "^4.2.5"
}
}
diff --git a/packages/astro/test/fixtures/astro-envs/package.json b/packages/astro/test/fixtures/astro-envs/package.json
index c46f4d6814acb..86bad5a8713c6 100644
--- a/packages/astro/test/fixtures/astro-envs/package.json
+++ b/packages/astro/test/fixtures/astro-envs/package.json
@@ -5,6 +5,6 @@
"dependencies": {
"@astrojs/vue": "workspace:*",
"astro": "workspace:*",
- "vue": "^3.3.4"
+ "vue": "^3.3.8"
}
}
diff --git a/packages/astro/test/fixtures/astro-expr/package.json b/packages/astro/test/fixtures/astro-expr/package.json
index c6dce6e21a6a3..ac553abf2fe77 100644
--- a/packages/astro/test/fixtures/astro-expr/package.json
+++ b/packages/astro/test/fixtures/astro-expr/package.json
@@ -5,6 +5,6 @@
"dependencies": {
"@astrojs/preact": "workspace:*",
"astro": "workspace:*",
- "preact": "^10.17.1"
+ "preact": "^10.19.2"
}
}
diff --git a/packages/astro/test/fixtures/astro-fallback/package.json b/packages/astro/test/fixtures/astro-fallback/package.json
index ea2281e0811d5..8726d73a91aa2 100644
--- a/packages/astro/test/fixtures/astro-fallback/package.json
+++ b/packages/astro/test/fixtures/astro-fallback/package.json
@@ -5,6 +5,6 @@
"dependencies": {
"@astrojs/preact": "workspace:*",
"astro": "workspace:*",
- "preact": "^10.17.1"
+ "preact": "^10.19.2"
}
}
diff --git a/packages/astro/test/fixtures/astro-get-static-paths/src/pages/data/[slug].json.ts b/packages/astro/test/fixtures/astro-get-static-paths/src/pages/data/[slug].json.ts
index 32a0fd140f123..21df537cd4a28 100644
--- a/packages/astro/test/fixtures/astro-get-static-paths/src/pages/data/[slug].json.ts
+++ b/packages/astro/test/fixtures/astro-get-static-paths/src/pages/data/[slug].json.ts
@@ -1,14 +1,12 @@
export async function getStaticPaths() {
- return [
+ return [
{ params: { slug: 'thing1' } },
{ params: { slug: 'thing2' } }
];
}
export async function GET() {
- return {
- body: JSON.stringify({
- title: '[slug]'
- }, null, 4)
- };
+ return Response.json({
+ title: '[slug]',
+ });
}
diff --git a/packages/astro/test/fixtures/astro-markdown-drafts/package.json b/packages/astro/test/fixtures/astro-markdown-drafts/package.json
deleted file mode 100644
index fa376d6f134f8..0000000000000
--- a/packages/astro/test/fixtures/astro-markdown-drafts/package.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "name": "@test/astro-markdown-drafts",
- "version": "0.0.0",
- "private": true,
- "dependencies": {
- "astro": "workspace:*"
- }
-}
diff --git a/packages/astro/test/fixtures/astro-markdown-drafts/src/pages/index.md b/packages/astro/test/fixtures/astro-markdown-drafts/src/pages/index.md
deleted file mode 100644
index 0e06444da3ccd..0000000000000
--- a/packages/astro/test/fixtures/astro-markdown-drafts/src/pages/index.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-foo: bar
----
-
-# Hello world
-
-This should be visible.
\ No newline at end of file
diff --git a/packages/astro/test/fixtures/astro-markdown-drafts/src/pages/wip.md b/packages/astro/test/fixtures/astro-markdown-drafts/src/pages/wip.md
deleted file mode 100644
index 27b8296140d76..0000000000000
--- a/packages/astro/test/fixtures/astro-markdown-drafts/src/pages/wip.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-foo: bar
-draft: true
----
-
-# WIP
-
-This is a draft. Don't build me!
\ No newline at end of file
diff --git a/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/glob.json.js b/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/glob.json.js
index 3aae6d89a8a90..1916378c93c09 100644
--- a/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/glob.json.js
+++ b/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/glob.json.js
@@ -1,6 +1,4 @@
export async function GET() {
const docs = await import.meta.glob('./*.md', { eager: true });
- return {
- body: JSON.stringify(Object.values(docs).map(doc => doc.frontmatter)),
- }
+ return Response.json(Object.values(docs).map((doc) => doc.frontmatter));
}
diff --git a/packages/astro/test/fixtures/astro-markdown-plugins/package.json b/packages/astro/test/fixtures/astro-markdown-plugins/package.json
index a7953b785825a..bd48d532e2412 100644
--- a/packages/astro/test/fixtures/astro-markdown-plugins/package.json
+++ b/packages/astro/test/fixtures/astro-markdown-plugins/package.json
@@ -4,7 +4,7 @@
"private": true,
"dependencies": {
"astro": "workspace:*",
- "hast-util-select": "^6.0.0",
- "rehype-slug": "^5.0.1"
+ "hast-util-select": "^6.0.2",
+ "rehype-slug": "^6.0.0"
}
}
diff --git a/packages/astro/test/fixtures/astro-markdown-shiki/langs/astro.config.mjs b/packages/astro/test/fixtures/astro-markdown-shiki/langs/astro.config.mjs
index 9a5f2eced5523..130596b0c4c53 100644
--- a/packages/astro/test/fixtures/astro-markdown-shiki/langs/astro.config.mjs
+++ b/packages/astro/test/fixtures/astro-markdown-shiki/langs/astro.config.mjs
@@ -13,14 +13,6 @@ export default {
grammar: riGrammar,
aliases: ['ri'],
},
- {
- id: 'caddy',
- scopeName: 'source.Caddyfile',
- // shiki compat: resolves from astro package directory.
- // careful as astro is linked, this relative path is based on astro/packages/astro.
- // it's weird but we're testing to prevent regressions.
- path: './test/fixtures/astro-markdown-shiki/langs/src/caddyfile.tmLanguage.json',
- },
],
},
},
diff --git a/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/caddyfile.tmLanguage.json b/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/caddyfile.tmLanguage.json
deleted file mode 100644
index 8a9f87c870cb1..0000000000000
--- a/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/caddyfile.tmLanguage.json
+++ /dev/null
@@ -1,365 +0,0 @@
-{
- "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
-
- "name": "Caddyfile",
- "fileTypes": ["Caddyfile"],
- "scopeName": "source.Caddyfile",
-
- "patterns": [
- { "include": "#comments" },
- { "include": "#strings" },
- { "include": "#domains" },
- { "include": "#status_codes" },
- { "include": "#path" },
- { "include": "#global_options" },
- { "include": "#matchers" },
- { "include": "#directive" },
- { "include": "#site_block_common" }
- ],
-
- "repository": {
- "comments": {
- "patterns": [
- {
- "name": "comment.line.Caddyfile",
- "match": "\\s#.*"
- },
- {
- "name": "comment.line.Caddyfile",
- "match": "^#.*"
- }
- ]
- },
-
- "strings": {
- "patterns": [
- {
- "comment": "Double Quoted Strings",
- "begin": "\"",
- "end": "\"",
- "name": "string.quoted.double.Caddyfile",
- "patterns": [
- {
- "name": "constant.character.escape.Caddyfile",
- "match": "\\\\\""
- }
- ]
- },
- {
- "comment": "Backtick Strings",
- "begin": "`",
- "end": "`",
- "name": "string.quoted.single.Caddyfile"
- }
- ]
- },
-
- "status_codes": {
- "patterns": [
- {
- "name": "constant.numeric.decimal",
- "match": "\\s[0-9]{3}(?!\\.)"
- }
- ]
- },
-
- "path": {
- "patterns": [
- {
- "name": "keyword.control.caddyfile",
- "match": "(unix/)*/[a-zA-Z0-9_\\-./*]+"
- },
- {
- "name": "variable.other.property.caddyfile",
- "match": "\\*.[a-z]{1,5}"
- },
- {
- "name": "variable.other.property.caddyfile",
- "match": "\\*/?"
- },
- {
- "name": "variable.other.property.caddyfile",
- "match": "\\?/"
- }
- ]
- },
-
- "domains": {
- "patterns": [
- {
- "comment": "Domains and URLs",
- "name": "keyword.control.caddyfile",
- "match": "(https?://)*[a-z0-9-\\*]*(?:\\.[a-zA-Z]{2,})+(:[0-9]+)*\\S*"
- },
- {
- "comment": "localhost",
- "name": "keyword.control.caddyfile",
- "match": "localhost(:[0-9]+)*"
- },
- {
- "comment": "IPv4",
- "name": "keyword.control.caddyfile",
- "match": "((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
- },
- {
- "comment": "IPv6",
- "name": "keyword.control.caddyfile",
- "match": "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"
- },
- {
- "comment": "Ports",
- "name": "keyword.control.caddyfile",
- "match": ":[0-9]+"
- }
- ]
- },
-
- "global_options": {
- "patterns": [
- {
- "begin": "^(\\{)$",
- "end": "^(\\})$",
-
- "beginCaptures": {
- "0": { "name": "punctuation.definition.dictionary.begin" }
- },
-
- "endCaptures": {
- "0": { "name": "punctuation.definition.dictionary.end" }
- },
-
- "patterns": [
- { "include": "#comments" },
- {
- "name": "support.constant.Caddyfile",
- "match": "^\\s*(debug|https?_port|default_bind|order|storage|storage_clean_interval|renew_interval|ocsp_interval|admin|log|grace_period|shutdown_delay|auto_https|email|default_sni|local_certs|skip_install_trust|acme_ca|acme_ca_root|acme_eab|acme_dns|on_demand_tls|key_type|cert_issuer|ocsp_stapling|preferred_chains|servers|pki|events)"
- }
- ]
- }
- ]
- },
-
- "site_block_common": {
- "patterns": [{ "include": "#placeholders" }, { "include": "#block" }]
- },
-
- "matchers": {
- "patterns": [
- {
- "comment": "Matchers",
- "name": "support.function.Caddyfile",
- "match": "@[^\\s]+(?=\\s)"
- }
- ]
- },
-
- "placeholders": {
- "patterns": [
- {
- "name": "keyword.control.Caddyfile",
- "match": "\\{[\\[\\]\\w.\\$+-]+\\}"
- }
- ]
- },
-
- "directive": {
- "patterns": [
- {
- "name": "entity.name.function.Caddyfile",
- "match": "^\\s*[a-zA-Z_\\-+]+"
- },
- { "include": "#content_types" },
- { "include": "#heredoc" }
- ]
- },
-
- "content_types": {
- "patterns": [
- {
- "comment": "Content Types",
- "name": "variable.other.property.caddyfile",
- "match": "(application|audio|example|font|image|message|model|multipart|text|video)/[a-zA-Z0-9*+\\-.]+;* *[a-zA-Z0-9=\\-]*"
- }
- ]
- },
-
- "block": {
- "patterns": [
- {
- "begin": "\\{",
- "end": "\\}",
-
- "patterns": [{ "include": "#block_content" }]
- }
- ]
- },
-
- "block_content": {
- "patterns": [
- {
- "patterns": [
- { "include": "#comments" },
- { "include": "#strings" },
- { "include": "#domains" },
- { "include": "#status_codes" },
- { "include": "#path" },
- { "include": "#matchers" },
- { "include": "#placeholders" },
- { "include": "#directive" },
- { "include": "#block" }
- ]
- }
- ]
- },
-
- "heredoc": {
- "patterns": [
- {
- "begin": "(?i)(?=<<\\s*([a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)\\s*$)",
- "end": "(?!\\G)",
- "name": "string.unquoted.heredoc.caddyfile",
- "patterns": [{ "include": "#heredoc_interior" }]
- }
- ]
- },
-
- "heredoc_interior": {
- "patterns": [
- {
- "comment": "CSS",
-
- "name": "meta.embedded.css",
- "contentName": "source.css",
-
- "begin": "(<<)\\s*(CSS)(\\s*)$",
- "beginCaptures": {
- "0": { "name": "punctuation.section.embedded.begin.caddyfile" },
- "1": { "name": "punctuation.definition.string.begin" },
- "2": { "name": "keyword.operator.heredoc.caddyfile" },
- "4": { "name": "invalid.illegal.trailing-whitespace.caddyfile" }
- },
-
- "end": "^\\s*(\\2)(?![A-Za-z0-9_\\x{7f}-\\x{10ffff}])",
- "endCaptures": {
- "0": { "name": "punctuation.section.embedded.end.caddyfile" },
- "1": { "name": "keyword.operator.heredoc.caddyfile" }
- },
-
- "patterns": [{ "include": "source.css" }]
- },
-
- {
- "comment": "HTML",
-
- "name": "meta.embedded.html",
- "contentName": "text.html",
-
- "begin": "(<<)\\s*(HTML)(\\s*)$",
- "beginCaptures": {
- "0": { "name": "punctuation.section.embedded.begin.caddyfile" },
- "1": { "name": "punctuation.definition.string.begin" },
- "2": { "name": "keyword.operator.heredoc.caddyfile" },
- "4": { "name": "invalid.illegal.trailing-whitespace.caddyfile" }
- },
-
- "end": "^\\s*(\\2)(?![A-Za-z0-9_\\x{7f}-\\x{10ffff}])",
- "endCaptures": {
- "0": { "name": "punctuation.section.embedded.end.caddyfile" },
- "1": { "name": "keyword.operator.heredoc.caddyfile" }
- },
-
- "patterns": [{ "include": "text.html.basic" }]
- },
-
- {
- "comment": "JavaScript",
-
- "name": "meta.embedded.js",
- "contentName": "source.js",
-
- "begin": "(<<)\\s*(JAVASCRIPT|JS)(\\s*)$",
- "beginCaptures": {
- "0": { "name": "punctuation.section.embedded.begin.caddyfile" },
- "1": { "name": "punctuation.definition.string.begin" },
- "2": { "name": "keyword.operator.heredoc.caddyfile" },
- "4": { "name": "invalid.illegal.trailing-whitespace.caddyfile" }
- },
-
- "end": "^\\s*(\\2)(?![A-Za-z0-9_\\x{7f}-\\x{10ffff}])",
- "endCaptures": {
- "0": { "name": "punctuation.section.embedded.end.caddyfile" },
- "1": { "name": "keyword.operator.heredoc.caddyfile" }
- },
-
- "patterns": [{ "include": "source.js" }]
- },
-
- {
- "comment": "JSON",
-
- "name": "meta.embedded.json",
- "contentName": "source.json",
-
- "begin": "(<<)\\s*(JSON)(\\s*)$",
- "beginCaptures": {
- "0": { "name": "punctuation.section.embedded.begin.caddyfile" },
- "1": { "name": "punctuation.definition.string.begin" },
- "2": { "name": "keyword.operator.heredoc.caddyfile" },
- "4": { "name": "invalid.illegal.trailing-whitespace.caddyfile" }
- },
-
- "end": "^\\s*(\\2)(?![A-Za-z0-9_\\x{7f}-\\x{10ffff}])",
- "endCaptures": {
- "0": { "name": "punctuation.section.embedded.end.caddyfile" },
- "1": { "name": "keyword.operator.heredoc.caddyfile" }
- },
-
- "patterns": [{ "include": "source.json" }]
- },
-
- {
- "comment": "XML",
-
- "name": "meta.embedded.xml",
- "contentName": "text.xml",
-
- "begin": "(<<)\\s*(XML)(\\s*)$",
- "beginCaptures": {
- "0": { "name": "punctuation.section.embedded.begin.caddyfile" },
- "1": { "name": "punctuation.definition.string.begin" },
- "2": { "name": "keyword.operator.heredoc.caddyfile" },
- "4": { "name": "invalid.illegal.trailing-whitespace.caddyfile" }
- },
-
- "end": "^\\s*(\\2)(?![A-Za-z0-9_\\x{7f}-\\x{10ffff}])",
- "endCaptures": {
- "0": { "name": "punctuation.section.embedded.end.caddyfile" },
- "1": { "name": "keyword.operator.heredoc.caddyfile" }
- },
-
- "patterns": [{ "include": "text.xml" }]
- },
-
- {
- "comment": "Any other heredoc",
-
- "begin": "(?i)(<<)\\s*([a-z_\\x{7f}-\\x{10ffff}]+[a-z0-9_\\x{7f}-\\x{10ffff}]*)(\\s*)",
- "beginCaptures": {
- "0": { "name": "punctuation.section.embedded.begin.caddyfile" },
- "1": { "name": "punctuation.definition.string.caddyfile" },
- "2": { "name": "keyword.operator.heredoc.caddyfile" },
- "4": { "name": "invalid.illegal.trailing-whitespace.caddyfile" }
- },
-
- "end": "^\\s*(\\2)(?![A-Za-z0-9_\\x{7f}-\\x{10ffff}])",
- "endCaptures": {
- "0": { "name": "punctuation.section.embedded.end.caddyfile" },
- "1": { "name": "keyword.operator.heredoc.caddyfile" }
- },
-
- "patterns": []
- }
- ]
- }
- }
-}
diff --git a/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/pages/index.md b/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/pages/index.md
index cdd74060f3a89..d2d756b95dc1f 100644
--- a/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/pages/index.md
+++ b/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/pages/index.md
@@ -24,12 +24,3 @@ fin
```unknown
This language does not exist
```
-
-```caddy
-example.com {
- root * /var/www/wordpress
- encode gzip
- php_fastcgi unix//run/php/php-version-fpm.sock
- file_server
-}
-```
diff --git a/packages/astro/test/fixtures/astro-markdown/src/pages/headings-glob.json.js b/packages/astro/test/fixtures/astro-markdown/src/pages/headings-glob.json.js
index b2c9ea6ea25f3..3cc852c6f565b 100644
--- a/packages/astro/test/fixtures/astro-markdown/src/pages/headings-glob.json.js
+++ b/packages/astro/test/fixtures/astro-markdown/src/pages/headings-glob.json.js
@@ -1,9 +1,7 @@
import { getHeadings } from './with-layout.md';
export async function GET() {
- return {
- body: JSON.stringify({
- headings: getHeadings(),
- }),
- }
+ return Response.json({
+ headings: getHeadings(),
+ });
}
diff --git a/packages/astro/test/fixtures/astro-markdown/src/pages/raw-content.json.js b/packages/astro/test/fixtures/astro-markdown/src/pages/raw-content.json.js
index 82977443dc11a..cf499bb39c018 100644
--- a/packages/astro/test/fixtures/astro-markdown/src/pages/raw-content.json.js
+++ b/packages/astro/test/fixtures/astro-markdown/src/pages/raw-content.json.js
@@ -1,10 +1,8 @@
import { rawContent, compiledContent } from './basic.md';
export async function GET() {
- return {
- body: JSON.stringify({
- raw: rawContent(),
- compiled: await compiledContent(),
- }),
- }
+ return Response.json({
+ raw: rawContent(),
+ compiled: await compiledContent(),
+ });
}
diff --git a/packages/astro/test/fixtures/astro-markdown/src/pages/vite-env-vars-glob.json.js b/packages/astro/test/fixtures/astro-markdown/src/pages/vite-env-vars-glob.json.js
index 7a5d00f47ac08..7dfe96294701a 100644
--- a/packages/astro/test/fixtures/astro-markdown/src/pages/vite-env-vars-glob.json.js
+++ b/packages/astro/test/fixtures/astro-markdown/src/pages/vite-env-vars-glob.json.js
@@ -1,7 +1,5 @@
import { frontmatter } from './vite-env-vars.md';
export async function GET() {
- return {
- body: JSON.stringify(frontmatter),
- }
+ return Response.json(frontmatter);
}
diff --git a/packages/astro/test/fixtures/astro-markdown/src/pages/vite-env-vars.md b/packages/astro/test/fixtures/astro-markdown/src/pages/vite-env-vars.md
index f4983af2c75b9..5fac527272449 100644
--- a/packages/astro/test/fixtures/astro-markdown/src/pages/vite-env-vars.md
+++ b/packages/astro/test/fixtures/astro-markdown/src/pages/vite-env-vars.md
@@ -21,7 +21,7 @@ This should also work outside of code blocks:
// src/pages/rss.xml.js
import rss from '@astrojs/rss';
-export const get = () => rss({
+export const GET = () => rss({
// Use Vite env vars with import.meta.env
site: import.meta.env.SITE,
title: import.meta.env.TITLE,
diff --git a/packages/astro/test/fixtures/astro-slot-with-client/package.json b/packages/astro/test/fixtures/astro-slot-with-client/package.json
index 830c205cfaf90..eb88bae982ff3 100644
--- a/packages/astro/test/fixtures/astro-slot-with-client/package.json
+++ b/packages/astro/test/fixtures/astro-slot-with-client/package.json
@@ -4,6 +4,6 @@
"dependencies": {
"@astrojs/preact": "workspace:*",
"astro": "workspace:*",
- "preact": "^10.17.1"
+ "preact": "^10.19.2"
}
}
diff --git a/packages/astro/test/fixtures/astro-slots-nested/package.json b/packages/astro/test/fixtures/astro-slots-nested/package.json
index e40fa03968cb0..2e1a6bef901cb 100644
--- a/packages/astro/test/fixtures/astro-slots-nested/package.json
+++ b/packages/astro/test/fixtures/astro-slots-nested/package.json
@@ -9,11 +9,11 @@
"@astrojs/svelte": "workspace:*",
"@astrojs/vue": "workspace:*",
"astro": "workspace:*",
- "preact": "^10.17.1",
+ "preact": "^10.19.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "solid-js": "^1.7.11",
- "svelte": "^4.2.0",
- "vue": "^3.3.4"
+ "solid-js": "^1.8.5",
+ "svelte": "^4.2.5",
+ "vue": "^3.3.8"
}
}
diff --git a/packages/astro/test/fixtures/astro-slots/src/pages/fallback-own.astro b/packages/astro/test/fixtures/astro-slots/src/pages/fallback-own.astro
new file mode 100644
index 0000000000000..89319f8b18581
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-slots/src/pages/fallback-own.astro
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/astro/test/fixtures/astro-slots/src/pages/fallback.astro b/packages/astro/test/fixtures/astro-slots/src/pages/fallback.astro
index 88aba06e9fa59..aa1a4795859d0 100644
--- a/packages/astro/test/fixtures/astro-slots/src/pages/fallback.astro
+++ b/packages/astro/test/fixtures/astro-slots/src/pages/fallback.astro
@@ -7,8 +7,6 @@ import Fallback from '../components/Fallback.astro';
-
-
-
+