mirror of
https://github.com/withastro/astro.git
synced 2025-04-07 23:41:43 -05:00
Merge branch 'main' into server-islands
This commit is contained in:
commit
b5ceb65ac2
326 changed files with 12582 additions and 13438 deletions
5
.changeset/eleven-hats-tell.md
Normal file
5
.changeset/eleven-hats-tell.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@astrojs/markdown-remark': patch
|
||||
---
|
||||
|
||||
Handles encoded image paths in internal rehype plugins and return decoded paths from markdown vfile's `data.imagePaths`
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
"@astrojs/partytown": patch
|
||||
---
|
||||
|
||||
Prevent Partytown from crashing when View Transitions are enabled
|
||||
|
||||
When View Transitions are turned on, Partytown executes on every transition.
|
||||
It's not meant to be like that, and therefore it breaks the integration completely.
|
||||
Starting from now, Partytown will be executed only once.
|
16
.changeset/funny-cats-sell.md
Normal file
16
.changeset/funny-cats-sell.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
'@astrojs/underscore-redirects': patch
|
||||
---
|
||||
|
||||
Adds support for forced redirects
|
||||
|
||||
Redirects can be forced by setting `force` to `true`:
|
||||
|
||||
```ts
|
||||
redirects.add({
|
||||
// ...
|
||||
force: true
|
||||
})
|
||||
```
|
||||
|
||||
It will append a `!` after the status.
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Improve error message when using `getLocaleByPath` on path that doesn't contain any locales.
|
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Adds support for enums to `astro:env`
|
||||
|
||||
You can now call `envField.enum`:
|
||||
|
||||
```js
|
||||
import { defineConfig, envField } from 'astro/config'
|
||||
|
||||
export default defineConfig({
|
||||
experimental: {
|
||||
env: {
|
||||
schema: {
|
||||
API_VERSION: envField.enum({
|
||||
context: 'server',
|
||||
access: 'secret',
|
||||
values: ['v1', 'v2'],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Close the iterator only after rendering is complete
|
|
@ -1,38 +0,0 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Adds additional validation options to `astro:env`
|
||||
|
||||
`astro:env` schema datatypes `string` and `number` now have new optional validation rules:
|
||||
|
||||
```js
|
||||
import { defineConfig, envField } from 'astro/config'
|
||||
|
||||
export default defineConfig({
|
||||
experimental: {
|
||||
env: {
|
||||
schema: {
|
||||
FOO: envField.string({
|
||||
// ...
|
||||
max: 32,
|
||||
min: 3,
|
||||
length: 12,
|
||||
url: true,
|
||||
includes: 'foo',
|
||||
startsWith: 'bar',
|
||||
endsWith: 'baz'
|
||||
}),
|
||||
BAR: envField.number({
|
||||
// ...
|
||||
gt: 2,
|
||||
min: 3,
|
||||
lt: 10,
|
||||
max: 9,
|
||||
int: true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Let middleware handle the original request URL
|
5
.changeset/small-badgers-juggle.md
Normal file
5
.changeset/small-badgers-juggle.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Fixes an issue with the view transition router when redirecting to an URL with different origin.
|
6
.changeset/small-sloths-yawn.md
Normal file
6
.changeset/small-sloths-yawn.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
'@astrojs/vercel': patch
|
||||
'@astrojs/node': patch
|
||||
---
|
||||
|
||||
Fixes `astro:env` getSecret compatibility
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"astro": patch
|
||||
---
|
||||
|
||||
Fixes an issue where a leading slash created incorrect conflict resolution between pages generated from static routes and catch-all dynamic routes
|
5
.changeset/tricky-mirrors-carry.md
Normal file
5
.changeset/tricky-mirrors-carry.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@astrojs/rss': patch
|
||||
---
|
||||
|
||||
Fixes an issue where the `pagesGlobToRssItems` returned an incorrect type for `items`
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
'@astrojs/db': patch
|
||||
---
|
||||
|
||||
Export type `Database` from `@astrojs/db/runtime`
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
'@astrojs/vercel': minor
|
||||
'@astrojs/node': minor
|
||||
---
|
||||
|
||||
Adds support for experimental `astro:env` released in Astro 4.10
|
|
@ -12,10 +12,12 @@ We welcome contributions of any size and skill level. As an open source project,
|
|||
|
||||
```shell
|
||||
node: "^>=18.17.1"
|
||||
pnpm: "^8.6.12"
|
||||
pnpm: "^9.3.0"
|
||||
# otherwise, your build will fail
|
||||
```
|
||||
|
||||
We recommend using Corepack, [read PNPM docs](https://pnpm.io/installation#using-corepack).
|
||||
|
||||
### Setting up your local repo
|
||||
|
||||
Astro uses pnpm workspaces, so you should **always run `pnpm install` from the top-level project directory**. Running `pnpm install` in the top-level project root will install dependencies for `astro`, and every package in the repo.
|
||||
|
@ -109,6 +111,53 @@ pnpm run test -m "$STRING_MATCH"
|
|||
node --test ./test/foo.test.js
|
||||
```
|
||||
|
||||
#### Running a single test
|
||||
|
||||
Sometimes you want to run a single test case (`it` or `describe`) or a single test file. You can do so by using Node.js.
|
||||
|
||||
To run a single test file, for example `test/astro-basic.test.js`:
|
||||
|
||||
```shell
|
||||
node --test test/astro-basic.test.js
|
||||
```
|
||||
|
||||
If you wish to run a single test case, you have to postfix `it` and `describe` functions with `.only`:
|
||||
|
||||
```diff
|
||||
// test/astro-basic.test.js
|
||||
- describe("description", () => {
|
||||
+ describe.only("description", () => {
|
||||
- it("description", () => {
|
||||
+ it.only("description", () => {})
|
||||
})
|
||||
```
|
||||
|
||||
Then, you have to pass the `--test-only` option to the Node.js:
|
||||
|
||||
```shell
|
||||
node --test --test-only test/astro-basic.test.js
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> 1. If you have nested `describe`, all of them must postfix with `.only`
|
||||
> 2. `--test-only` and `--test` must be placed **before** declaring the path to the file. Failing to do so will test all files
|
||||
|
||||
#### Debugging tests in CI
|
||||
|
||||
There might be occasions where some tests fail in certain CI runs due to some timeout issue. If this happens, it will be very difficult to understand which file cause the timeout. That's caused by come quirks of the Node.js test runner combined with our architecture.
|
||||
|
||||
To understand which file causes the issue, you can modify the `test` script inside the `package.json` by adding the `--parallel` option:
|
||||
|
||||
```diff
|
||||
{
|
||||
- "test": "astro-scripts test \"test/**/*.test.js\"",
|
||||
+ "test": "astro-scripts test --parallel \"test/**/*.test.js\"",
|
||||
}
|
||||
```
|
||||
|
||||
Save the change and **push it** to your PR. This change will make the test CI slower, but it will allow to see which files causes the timeout. Once you fixed the issue **revert the change and push it**.
|
||||
|
||||
#### E2E tests
|
||||
|
||||
Certain features, like HMR and client hydration, need end-to-end tests to verify functionality in the dev server. [Playwright](https://playwright.dev/) is used to test against the dev server.
|
||||
|
@ -127,6 +176,12 @@ Any tests for `astro build` output should use the main `mocha` tests rather than
|
|||
|
||||
If a test needs to validate what happens on the page after it's loading in the browser, that's a perfect use for E2E dev server tests, i.e. to verify that hot-module reloading works in `astro dev` or that components were client hydrated and are interactive.
|
||||
|
||||
#### Creating tests
|
||||
|
||||
When creating new tests, it's best to reference other existing test files and replicate the same setup. Some other tips include:
|
||||
|
||||
- When re-using a fixture multiple times with different configurations, you should also configure unique `outDir`, `build.client`, and `build.server` values so the build output runtime isn't cached and shared by ESM between test runs.
|
||||
|
||||
### Other useful commands
|
||||
|
||||
```shell
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<div align="center">
|
||||
|
||||

|
||||
[](https://github.com/withastro/astro/actions/workflows/ci.yml)
|
||||
[](https://github.com/withastro/astro/blob/main/LICENSE)
|
||||
[](https://badge.fury.io/js/astro)
|
||||
|
||||
|
|
17
biome.json
17
biome.json
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.7.1/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/1.8.1/schema.json",
|
||||
"files": {
|
||||
"ignore": [
|
||||
"vendor",
|
||||
|
@ -20,7 +20,6 @@
|
|||
"benchmark/results/",
|
||||
".changeset",
|
||||
"pnpm-lock.yaml",
|
||||
"package.json",
|
||||
"*.astro"
|
||||
]
|
||||
},
|
||||
|
@ -30,7 +29,7 @@
|
|||
"linter": { "enabled": false },
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"trailingComma": "es5",
|
||||
"trailingCommas": "es5",
|
||||
"quoteStyle": "single",
|
||||
"semicolons": "always"
|
||||
}
|
||||
|
@ -44,5 +43,15 @@
|
|||
"indentStyle": "space",
|
||||
"trailingCommas": "none"
|
||||
}
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": ["package.json"],
|
||||
"json": {
|
||||
"formatter": {
|
||||
"lineWidth": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^4.10.1"
|
||||
"astro": "^4.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^3.1.0",
|
||||
"@astrojs/mdx": "^3.1.1",
|
||||
"@astrojs/rss": "^4.0.6",
|
||||
"@astrojs/sitemap": "^3.1.5",
|
||||
"astro": "^4.10.1"
|
||||
"@astrojs/sitemap": "^3.1.6",
|
||||
"astro": "^4.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
],
|
||||
"scripts": {},
|
||||
"devDependencies": {
|
||||
"astro": "^4.10.1"
|
||||
"astro": "^4.11.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"astro": "^4.0.0"
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^4.10.1",
|
||||
"@astrojs/react": "^3.5.0",
|
||||
"astro": "^4.11.0",
|
||||
"@astrojs/react": "^3.6.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"vitest": "^1.6.0"
|
||||
|
|
|
@ -14,6 +14,6 @@
|
|||
"@astrojs/alpinejs": "^0.4.0",
|
||||
"@types/alpinejs": "^3.13.10",
|
||||
"alpinejs": "^3.14.0",
|
||||
"astro": "^4.10.1"
|
||||
"astro": "^4.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/lit": "^4.2.0",
|
||||
"@astrojs/lit": "^4.3.0",
|
||||
"@webcomponents/template-shadowroot": "^0.2.1",
|
||||
"astro": "^4.10.1",
|
||||
"lit": "^3.1.3"
|
||||
"astro": "^4.11.0",
|
||||
"lit": "^3.1.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,19 +11,19 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/preact": "^3.4.0",
|
||||
"@astrojs/react": "^3.5.0",
|
||||
"@astrojs/solid-js": "^4.3.0",
|
||||
"@astrojs/svelte": "^5.5.0",
|
||||
"@astrojs/vue": "^4.4.0",
|
||||
"@astrojs/preact": "^3.5.0",
|
||||
"@astrojs/react": "^3.6.0",
|
||||
"@astrojs/solid-js": "^4.4.0",
|
||||
"@astrojs/svelte": "^5.6.0",
|
||||
"@astrojs/vue": "^4.5.0",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"astro": "^4.10.1",
|
||||
"astro": "^4.11.0",
|
||||
"preact": "^10.22.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"solid-js": "^1.8.17",
|
||||
"svelte": "^4.2.17",
|
||||
"vue": "^3.4.27"
|
||||
"svelte": "^4.2.18",
|
||||
"vue": "^3.4.29"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/preact": "^3.4.0",
|
||||
"@astrojs/preact": "^3.5.0",
|
||||
"@preact/signals": "^1.2.3",
|
||||
"astro": "^4.10.1",
|
||||
"astro": "^4.11.0",
|
||||
"preact": "^10.22.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,10 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/react": "^3.5.0",
|
||||
"@astrojs/react": "^3.6.0",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"astro": "^4.10.1",
|
||||
"astro": "^4.11.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/solid-js": "^4.3.0",
|
||||
"astro": "^4.10.1",
|
||||
"@astrojs/solid-js": "^4.4.0",
|
||||
"astro": "^4.11.0",
|
||||
"solid-js": "^1.8.17"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/svelte": "^5.5.0",
|
||||
"astro": "^4.10.1",
|
||||
"svelte": "^4.2.17"
|
||||
"@astrojs/svelte": "^5.6.0",
|
||||
"astro": "^4.11.0",
|
||||
"svelte": "^4.2.18"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/vue": "^4.4.0",
|
||||
"astro": "^4.10.1",
|
||||
"vue": "^3.4.27"
|
||||
"@astrojs/vue": "^4.5.0",
|
||||
"astro": "^4.11.0",
|
||||
"vue": "^3.4.29"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^8.2.6",
|
||||
"astro": "^4.10.1"
|
||||
"@astrojs/node": "^8.3.1",
|
||||
"astro": "^4.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
],
|
||||
"scripts": {},
|
||||
"devDependencies": {
|
||||
"astro": "^4.10.1"
|
||||
"astro": "^4.11.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"astro": "^4.0.0"
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
"server": "node dist/server/entry.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^8.2.6",
|
||||
"astro": "^4.10.1",
|
||||
"@astrojs/node": "^8.3.1",
|
||||
"astro": "^4.11.0",
|
||||
"html-minifier": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^4.10.1"
|
||||
"astro": "^4.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^4.10.1"
|
||||
"astro": "^4.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^4.10.1"
|
||||
"astro": "^4.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,9 @@
|
|||
"server": "node dist/server/entry.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^8.2.6",
|
||||
"@astrojs/svelte": "^5.5.0",
|
||||
"astro": "^4.10.1",
|
||||
"svelte": "^4.2.17"
|
||||
"@astrojs/node": "^8.3.1",
|
||||
"@astrojs/svelte": "^5.6.0",
|
||||
"astro": "^4.11.0",
|
||||
"svelte": "^4.2.18"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^4.10.1",
|
||||
"sass": "^1.77.3",
|
||||
"astro": "^4.11.0",
|
||||
"sass": "^1.77.5",
|
||||
"sharp": "^0.33.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,6 @@
|
|||
"./app": "./dist/app.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"astro": "^4.10.1"
|
||||
"astro": "^4.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@astrojs/node": "^8.2.6",
|
||||
"astro": "^4.10.1"
|
||||
"@astrojs/node": "^8.3.1",
|
||||
"astro": "^4.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@astrojs/markdoc": "^0.11.0",
|
||||
"astro": "^4.10.1"
|
||||
"astro": "^4.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@astrojs/markdown-remark": "^5.1.0",
|
||||
"astro": "^4.10.1",
|
||||
"astro": "^4.11.0",
|
||||
"hast-util-select": "^6.0.2",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^4.10.1"
|
||||
"astro": "^4.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^3.1.0",
|
||||
"@astrojs/preact": "^3.4.0",
|
||||
"astro": "^4.10.1",
|
||||
"@astrojs/mdx": "^3.1.1",
|
||||
"@astrojs/preact": "^3.5.0",
|
||||
"astro": "^4.11.0",
|
||||
"preact": "^10.22.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/preact": "^3.4.0",
|
||||
"@astrojs/preact": "^3.5.0",
|
||||
"@nanostores/preact": "^0.5.1",
|
||||
"astro": "^4.10.1",
|
||||
"astro": "^4.11.0",
|
||||
"nanostores": "^0.10.3",
|
||||
"preact": "^10.22.0"
|
||||
}
|
||||
|
|
|
@ -11,13 +11,13 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^3.1.0",
|
||||
"@astrojs/mdx": "^3.1.1",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@types/canvas-confetti": "^1.6.4",
|
||||
"astro": "^4.10.1",
|
||||
"astro": "^4.11.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3"
|
||||
"tailwindcss": "^3.4.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^4.10.1",
|
||||
"astro": "^4.11.0",
|
||||
"vitest": "^1.6.0"
|
||||
}
|
||||
}
|
||||
|
|
25
package.json
25
package.json
|
@ -16,10 +16,10 @@
|
|||
"dev": "turbo run dev --concurrency=40 --parallel --filter=astro --filter=create-astro --filter=\"@astrojs/*\" --filter=\"@benchmark/*\"",
|
||||
"format": "pnpm run format:code && pnpm run format:imports",
|
||||
"format:ci": "pnpm run format:code:ci && pnpm run format:imports:ci",
|
||||
"format:code": "biome format ./ --write && prettier -w \"**/*\" --ignore-unknown --cache",
|
||||
"format:code:ci": "biome format ./ && prettier -w \"**/*\" --ignore-unknown --cache --check",
|
||||
"format:imports": "biome check --apply .",
|
||||
"format:imports:ci": "biome ci .",
|
||||
"format:code": "biome format --write && prettier -w \"**/*\" --ignore-unknown --cache",
|
||||
"format:code:ci": "biome format && prettier -w \"**/*\" --ignore-unknown --cache --check",
|
||||
"format:imports": "biome check --formatter-enabled=false --write",
|
||||
"format:imports:ci": "biome ci --formatter-enabled=false",
|
||||
"test": "turbo run test --concurrency=1 --filter=astro --filter=create-astro --filter=\"@astrojs/*\"",
|
||||
"test:citgm": "pnpm -r --filter=astro test",
|
||||
"test:match": "cd packages/astro && pnpm run test:match",
|
||||
|
@ -44,33 +44,32 @@
|
|||
"packages/*"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.1 || ^20.3.0 || >=21.0.0",
|
||||
"pnpm": ">=8.6.12"
|
||||
"node": "^18.17.1 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@8.6.12",
|
||||
"packageManager": "pnpm@9.3.0",
|
||||
"dependencies": {
|
||||
"astro-benchmark": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.7.0",
|
||||
"@biomejs/biome": "1.7.1",
|
||||
"@biomejs/biome": "1.8.1",
|
||||
"@changesets/changelog-github": "^0.5.0",
|
||||
"@changesets/cli": "^2.27.5",
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@types/node": "^18.17.8",
|
||||
"esbuild": "^0.21.4",
|
||||
"eslint": "^9.3.0",
|
||||
"esbuild": "^0.21.5",
|
||||
"eslint": "^9.5.0",
|
||||
"eslint-plugin-no-only-tests": "^3.1.0",
|
||||
"eslint-plugin-regexp": "^2.6.0",
|
||||
"globby": "^14.0.1",
|
||||
"only-allow": "^1.2.1",
|
||||
"organize-imports-cli": "^0.10.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-astro": "^0.14.0",
|
||||
"tiny-glob": "^0.2.9",
|
||||
"turbo": "^1.13.3",
|
||||
"turbo": "^1.13.4",
|
||||
"typescript": "~5.4.5",
|
||||
"typescript-eslint": "^7.11.0"
|
||||
"typescript-eslint": "^7.13.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"packageExtensions": {
|
||||
|
|
|
@ -32,13 +32,13 @@ export type RSSOptions = {
|
|||
|
||||
export type RSSFeedItem = {
|
||||
/** Link to item */
|
||||
link: z.infer<typeof rssSchema>['link'];
|
||||
link?: z.infer<typeof rssSchema>['link'];
|
||||
/** Full content of the item. Should be valid HTML */
|
||||
content?: z.infer<typeof rssSchema>['content'];
|
||||
/** Title of item */
|
||||
title: z.infer<typeof rssSchema>['title'];
|
||||
title?: z.infer<typeof rssSchema>['title'];
|
||||
/** Publication date of item */
|
||||
pubDate: z.infer<typeof rssSchema>['pubDate'];
|
||||
pubDate?: z.infer<typeof rssSchema>['pubDate'];
|
||||
/** Item description */
|
||||
description?: z.infer<typeof rssSchema>['description'];
|
||||
/** Append some other XML-valid data to this item */
|
||||
|
|
|
@ -1,5 +1,221 @@
|
|||
# astro
|
||||
|
||||
## 4.11.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#11197](https://github.com/withastro/astro/pull/11197) [`4b46bd9`](https://github.com/withastro/astro/commit/4b46bd9bdcbb302f294aa27b8aa07099e104fa17) Thanks [@braebo](https://github.com/braebo)! - Adds [`ShikiTransformer`](https://shiki.style/packages/transformers#shikijs-transformers) support to the [`<Code />`](https://docs.astro.build/en/reference/api-reference/#code-) component with a new `transformers` prop.
|
||||
|
||||
Note that `transformers` only applies classes and you must provide your own CSS rules to target the elements of your code block.
|
||||
|
||||
```astro
|
||||
---
|
||||
import { transformerNotationFocus } from '@shikijs/transformers';
|
||||
import { Code } from 'astro:components';
|
||||
|
||||
const code = `const foo = 'hello'
|
||||
const bar = ' world'
|
||||
console.log(foo + bar) // [!code focus]
|
||||
`;
|
||||
---
|
||||
|
||||
<Code {code} lang="js" transformers={[transformerNotationFocus()]} />
|
||||
|
||||
<style is:global>
|
||||
pre.has-focused .line:not(.focused) {
|
||||
filter: blur(1px);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
- [#11134](https://github.com/withastro/astro/pull/11134) [`9042be0`](https://github.com/withastro/astro/commit/9042be049157ce859355f911565bc0c3d68f0aa1) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Improves the developer experience of the `500.astro` file by passing it a new `error` prop.
|
||||
|
||||
When an error is thrown, the special `src/pages/500.astro` page now automatically receives the error as a prop. This allows you to display more specific information about the error on a custom 500 page.
|
||||
|
||||
```astro
|
||||
---
|
||||
// src/pages/500.astro
|
||||
interface Props {
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
const { error } = Astro.props;
|
||||
---
|
||||
|
||||
<div>{error instanceof Error ? error.message : 'Unknown error'}</div>
|
||||
```
|
||||
|
||||
If an error occurs rendering this page, your host's default 500 error page will be shown to your visitor in production, and Astro's default error overlay will be shown in development.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#11280](https://github.com/withastro/astro/pull/11280) [`fd3645f`](https://github.com/withastro/astro/commit/fd3645fe8364ec5e280b6802d1468867890d463c) Thanks [@ascorbic](https://github.com/ascorbic)! - Fixes a bug that prevented cookies from being set when using experimental rewrites
|
||||
|
||||
- [#11275](https://github.com/withastro/astro/pull/11275) [`bab700d`](https://github.com/withastro/astro/commit/bab700d69085b1de8f03fc1b0b31651f709cbfe3) Thanks [@syhily](https://github.com/syhily)! - Drop duplicated brackets in data collections schema generation.
|
||||
|
||||
- [#11272](https://github.com/withastro/astro/pull/11272) [`ea987d7`](https://github.com/withastro/astro/commit/ea987d7da589ead9aa4b550f167f5e2f6c939d2e) Thanks [@ematipico](https://github.com/ematipico)! - Fixes a case where rewriting `/` would cause an issue, when `trailingSlash` was set to `"never"`.
|
||||
|
||||
- [#11272](https://github.com/withastro/astro/pull/11272) [`ea987d7`](https://github.com/withastro/astro/commit/ea987d7da589ead9aa4b550f167f5e2f6c939d2e) Thanks [@ematipico](https://github.com/ematipico)! - Reverts a logic where it wasn't possible to rewrite `/404` in static mode. It's **now possible** again
|
||||
|
||||
- [#11264](https://github.com/withastro/astro/pull/11264) [`5a9c9a6`](https://github.com/withastro/astro/commit/5a9c9a60e7c32aa461b86b5bc667cb955e23d4d9) Thanks [@Fryuni](https://github.com/Fryuni)! - Fixes type generation for empty content collections
|
||||
|
||||
- [#11279](https://github.com/withastro/astro/pull/11279) [`9a08d74`](https://github.com/withastro/astro/commit/9a08d74bc00ae2c3bc254f99580a22ce4df1d002) Thanks [@ascorbic](https://github.com/ascorbic)! - Improves type-checking and error handling to catch case where an image import is passed directly to `getImage()`
|
||||
|
||||
- [#11292](https://github.com/withastro/astro/pull/11292) [`7f8f347`](https://github.com/withastro/astro/commit/7f8f34799528ed0b2011e1ea273bd0636f6e767d) Thanks [@jdtjenkins](https://github.com/jdtjenkins)! - Fixes a case where `defineAction` autocomplete for the `accept` prop would not show `"form"` as a possible value
|
||||
|
||||
- [#11273](https://github.com/withastro/astro/pull/11273) [`cb4d078`](https://github.com/withastro/astro/commit/cb4d07819f0dbdfd94bc4f084edf7720ada01323) Thanks [@ascorbic](https://github.com/ascorbic)! - Corrects an inconsistency in dev where middleware would run for prerendered 404 routes.
|
||||
Middleware is not run for prerendered 404 routes in production, so this was incorrect.
|
||||
|
||||
- [#11284](https://github.com/withastro/astro/pull/11284) [`f4b029b`](https://github.com/withastro/astro/commit/f4b029b08264268c68fc81ea25b264e81f47e683) Thanks [@ascorbic](https://github.com/ascorbic)! - Fixes an issue that would break `Astro.request.url` and `Astro.request.headers` in `astro dev` if HTTP/2 was enabled.
|
||||
|
||||
HTTP/2 is now enabled by default in `astro dev` if `https` is configured in the Vite config.
|
||||
|
||||
## 4.10.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#11213](https://github.com/withastro/astro/pull/11213) [`94ac7ef`](https://github.com/withastro/astro/commit/94ac7efd70fd264b10887805a02d5d1877af8701) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Removes the `PUBLIC_` prefix constraint for `astro:env` public variables
|
||||
|
||||
- [#11213](https://github.com/withastro/astro/pull/11213) [`94ac7ef`](https://github.com/withastro/astro/commit/94ac7efd70fd264b10887805a02d5d1877af8701) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - **BREAKING CHANGE to the experimental `astro:env` feature only**
|
||||
|
||||
Server secrets specified in the schema must now be imported from `astro:env/server`. Using `getSecret()` is no longer required to use these environment variables in your schema:
|
||||
|
||||
```diff
|
||||
- import { getSecret } from 'astro:env/server'
|
||||
- const API_SECRET = getSecret("API_SECRET")
|
||||
+ import { API_SECRET } from 'astro:env/server'
|
||||
```
|
||||
|
||||
Note that using `getSecret()` with these keys is still possible, but no longer involves any special handling and the raw value will be returned, just like retrieving secrets not specified in your schema.
|
||||
|
||||
- [#11234](https://github.com/withastro/astro/pull/11234) [`4385bf7`](https://github.com/withastro/astro/commit/4385bf7a4dc9c65bff53a30c660f7a909fcabfc9) Thanks [@ematipico](https://github.com/ematipico)! - Adds a new function called `addServerRenderer` to the Container API. Use this function to manually store renderers inside the instance of your container.
|
||||
|
||||
This new function should be preferred when using the Container API in environments like on-demand pages:
|
||||
|
||||
```ts
|
||||
import type { APIRoute } from 'astro';
|
||||
import { experimental_AstroContainer } from 'astro/container';
|
||||
import reactRenderer from '@astrojs/react/server.js';
|
||||
import vueRenderer from '@astrojs/vue/server.js';
|
||||
import ReactComponent from '../components/button.jsx';
|
||||
import VueComponent from '../components/button.vue';
|
||||
|
||||
// MDX runtime is contained inside the Astro core
|
||||
import mdxRenderer from 'astro/jsx/server.js';
|
||||
|
||||
// In case you need to import a custom renderer
|
||||
import customRenderer from '../renderers/customRenderer.js';
|
||||
|
||||
export const GET: APIRoute = async (ctx) => {
|
||||
const container = await experimental_AstroContainer.create();
|
||||
container.addServerRenderer({ renderer: reactRenderer });
|
||||
container.addServerRenderer({ renderer: vueRenderer });
|
||||
container.addServerRenderer({ renderer: customRenderer });
|
||||
// You can pass a custom name too
|
||||
container.addServerRenderer({
|
||||
name: 'customRenderer',
|
||||
renderer: customRenderer,
|
||||
});
|
||||
const vueComponent = await container.renderToString(VueComponent);
|
||||
return await container.renderToResponse(Component);
|
||||
};
|
||||
```
|
||||
|
||||
- [#11249](https://github.com/withastro/astro/pull/11249) [`de60c69`](https://github.com/withastro/astro/commit/de60c69aa06c41f76a5510cc1d0bee4c8a5326a5) Thanks [@markgaze](https://github.com/markgaze)! - Fixes a performance issue with JSON schema generation
|
||||
|
||||
- [#11242](https://github.com/withastro/astro/pull/11242) [`e4fc2a0`](https://github.com/withastro/astro/commit/e4fc2a0bafb4723566552d0c5954b25447890f51) Thanks [@ematipico](https://github.com/ematipico)! - Fixes a case where the virtual module `astro:container` wasn't resolved
|
||||
|
||||
- [#11236](https://github.com/withastro/astro/pull/11236) [`39bc3a5`](https://github.com/withastro/astro/commit/39bc3a5e8161232d6fdc6cc52b1f246083966d8e) Thanks [@ascorbic](https://github.com/ascorbic)! - Fixes a case where symlinked content collection directories were not correctly resolved
|
||||
|
||||
- [#11258](https://github.com/withastro/astro/pull/11258) [`d996db6`](https://github.com/withastro/astro/commit/d996db6f0bf361ebd84b23d022db7bb10fb316e6) Thanks [@ascorbic](https://github.com/ascorbic)! - Adds a new error `RewriteWithBodyUsed` that throws when `Astro.rewrite` is used after the request body has already been read.
|
||||
|
||||
- [#11243](https://github.com/withastro/astro/pull/11243) [`ba2b14c`](https://github.com/withastro/astro/commit/ba2b14cc28bd219c241313cdf142b736e7442014) Thanks [@V3RON](https://github.com/V3RON)! - Fixes a prerendering issue for libraries in `node_modules` when a folder with an underscore is in the path.
|
||||
|
||||
- [#11244](https://github.com/withastro/astro/pull/11244) [`d07d2f7`](https://github.com/withastro/astro/commit/d07d2f7ac9d87af907beaca700ba4116dc1d6f37) Thanks [@ematipico](https://github.com/ematipico)! - Improves the developer experience of the custom `500.astro` page in development mode.
|
||||
|
||||
Before, in development, an error thrown during the rendering phase would display the default error overlay, even when users had the `500.astro` page.
|
||||
|
||||
Now, the development server will display the `500.astro` and the original error is logged in the console.
|
||||
|
||||
- [#11240](https://github.com/withastro/astro/pull/11240) [`2851b0a`](https://github.com/withastro/astro/commit/2851b0aa2e2abe80ea603b53c67770e94980a8d3) Thanks [@ascorbic](https://github.com/ascorbic)! - Ignores query strings in module identifiers when matching ".astro" file extensions in Vite plugin
|
||||
|
||||
- [#11245](https://github.com/withastro/astro/pull/11245) [`e22be22`](https://github.com/withastro/astro/commit/e22be22e5729e60220726e92b52d2833c937fd1c) Thanks [@bluwy](https://github.com/bluwy)! - Refactors prerendering chunk handling to correctly remove unused code during the SSR runtime
|
||||
|
||||
## 4.10.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#11231](https://github.com/withastro/astro/pull/11231) [`58d7dbb`](https://github.com/withastro/astro/commit/58d7dbb5e0cabea1ac7a35af5b46685fce50d723) Thanks [@ematipico](https://github.com/ematipico)! - Fixes a regression for `getViteConfig`, where the inline config wasn't merged in the final config.
|
||||
|
||||
- [#11228](https://github.com/withastro/astro/pull/11228) [`1e293a1`](https://github.com/withastro/astro/commit/1e293a1b819024f16bfe482f272df0678cdd7874) Thanks [@ascorbic](https://github.com/ascorbic)! - Updates `getCollection()` to always return a cloned array
|
||||
|
||||
- [#11207](https://github.com/withastro/astro/pull/11207) [`7d9aac3`](https://github.com/withastro/astro/commit/7d9aac376c4b8844917901f7f566f7259d7f66c8) Thanks [@ematipico](https://github.com/ematipico)! - Fixes an issue in the rewriting logic where old data was not purged during the rewrite flow. This caused some false positives when checking the validity of URL path names during the rendering phase.
|
||||
|
||||
- [#11189](https://github.com/withastro/astro/pull/11189) [`75a8fe7`](https://github.com/withastro/astro/commit/75a8fe7e72b95f20c36f034de2b51b6a9550e27e) Thanks [@ematipico](https://github.com/ematipico)! - Improve error message when using `getLocaleByPath` on path that doesn't contain any locales.
|
||||
|
||||
- [#11195](https://github.com/withastro/astro/pull/11195) [`0a6ab6f`](https://github.com/withastro/astro/commit/0a6ab6f562651b558ca90761feed5c07f54f2633) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Adds support for enums to `astro:env`
|
||||
|
||||
You can now call `envField.enum`:
|
||||
|
||||
```js
|
||||
import { defineConfig, envField } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
experimental: {
|
||||
env: {
|
||||
schema: {
|
||||
API_VERSION: envField.enum({
|
||||
context: 'server',
|
||||
access: 'secret',
|
||||
values: ['v1', 'v2'],
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
- [#11210](https://github.com/withastro/astro/pull/11210) [`66fc028`](https://github.com/withastro/astro/commit/66fc0283d3f1d1a4f17d7db65ca3521a01fb5bec) Thanks [@matthewp](https://github.com/matthewp)! - Close the iterator only after rendering is complete
|
||||
|
||||
- [#11195](https://github.com/withastro/astro/pull/11195) [`0a6ab6f`](https://github.com/withastro/astro/commit/0a6ab6f562651b558ca90761feed5c07f54f2633) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Adds additional validation options to `astro:env`
|
||||
|
||||
`astro:env` schema datatypes `string` and `number` now have new optional validation rules:
|
||||
|
||||
```js
|
||||
import { defineConfig, envField } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
experimental: {
|
||||
env: {
|
||||
schema: {
|
||||
FOO: envField.string({
|
||||
// ...
|
||||
max: 32,
|
||||
min: 3,
|
||||
length: 12,
|
||||
url: true,
|
||||
includes: 'foo',
|
||||
startsWith: 'bar',
|
||||
endsWith: 'baz',
|
||||
}),
|
||||
BAR: envField.number({
|
||||
// ...
|
||||
gt: 2,
|
||||
min: 3,
|
||||
lt: 10,
|
||||
max: 9,
|
||||
int: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
- [#11211](https://github.com/withastro/astro/pull/11211) [`97724da`](https://github.com/withastro/astro/commit/97724da93ed7b1db19632c0cdb4b3aab1ff84812) Thanks [@matthewp](https://github.com/matthewp)! - Let middleware handle the original request URL
|
||||
|
||||
- [#10607](https://github.com/withastro/astro/pull/10607) [`7327c6a`](https://github.com/withastro/astro/commit/7327c6acb197e1f2ea6cf94cfbc5700bc755f982) Thanks [@frankbits](https://github.com/frankbits)! - Fixes an issue where a leading slash created incorrect conflict resolution between pages generated from static routes and catch-all dynamic routes
|
||||
|
||||
## 4.10.1
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
@ -3,6 +3,7 @@ import type { ThemePresets } from '@astrojs/markdown-remark';
|
|||
import type {
|
||||
BuiltinLanguage,
|
||||
LanguageRegistration,
|
||||
ShikiTransformer,
|
||||
SpecialLanguage,
|
||||
ThemeRegistration,
|
||||
ThemeRegistrationRaw,
|
||||
|
@ -50,6 +51,12 @@ interface Props extends Omit<HTMLAttributes<'pre'>, 'lang'> {
|
|||
* @default false
|
||||
*/
|
||||
inline?: boolean;
|
||||
/**
|
||||
* Shiki transformers to customize the generated HTML by manipulating the hast tree.
|
||||
* Supports all transformers listed here: https://shiki.style/packages/transformers#transformers
|
||||
* Instructions for custom transformers: https://shiki.style/guide/transformers
|
||||
*/
|
||||
transformers?: ShikiTransformer[];
|
||||
}
|
||||
|
||||
const {
|
||||
|
@ -59,6 +66,7 @@ const {
|
|||
themes = {},
|
||||
wrap = false,
|
||||
inline = false,
|
||||
transformers = [],
|
||||
...rest
|
||||
} = Astro.props;
|
||||
|
||||
|
@ -85,6 +93,7 @@ const highlighter = await getCachedHighlighter({
|
|||
theme,
|
||||
themes,
|
||||
wrap,
|
||||
transformers,
|
||||
});
|
||||
|
||||
const html = await highlighter.highlight(code, typeof lang === 'string' ? lang : lang.name, {
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
"@types/react": "npm:types-react",
|
||||
"@types/react-dom": "npm:types-react-dom",
|
||||
"astro": "workspace:*",
|
||||
"react": "19.0.0-rc-f994737d14-20240522",
|
||||
"react-dom": "19.0.0-rc-f994737d14-20240522",
|
||||
"react": "19.0.0-rc-fb9a90fa48-20240614",
|
||||
"react-dom": "19.0.0-rc-fb9a90fa48-20240614",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"overrides": {
|
||||
|
|
|
@ -5,6 +5,6 @@
|
|||
"dependencies": {
|
||||
"@astrojs/vue": "workspace:*",
|
||||
"astro": "workspace:*",
|
||||
"vue": "^3.4.27"
|
||||
"vue": "^3.4.29"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"solid-js": "^1.8.17",
|
||||
"svelte": "^4.2.17",
|
||||
"vue": "^3.4.27"
|
||||
"svelte": "^4.2.18",
|
||||
"vue": "^3.4.29"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"sass": "^1.77.3"
|
||||
"sass": "^1.77.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,9 @@
|
|||
"preact": "^10.22.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"sass": "^1.77.3",
|
||||
"sass": "^1.77.5",
|
||||
"solid-js": "^1.8.17",
|
||||
"svelte": "^4.2.17",
|
||||
"vue": "^3.4.27"
|
||||
"svelte": "^4.2.18",
|
||||
"vue": "^3.4.29"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
"private": true,
|
||||
"devDependencies": {
|
||||
"astro": "workspace:*",
|
||||
"sass": "^1.77.3"
|
||||
"sass": "^1.77.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,6 @@
|
|||
"@astrojs/lit": "workspace:*",
|
||||
"@webcomponents/template-shadowroot": "^0.2.1",
|
||||
"astro": "workspace:*",
|
||||
"lit": "^3.1.3"
|
||||
"lit": "^3.1.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,12 +13,12 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@webcomponents/template-shadowroot": "^0.2.1",
|
||||
"lit": "^3.1.3",
|
||||
"lit": "^3.1.4",
|
||||
"preact": "^10.22.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"solid-js": "^1.8.17",
|
||||
"svelte": "^4.2.17",
|
||||
"vue": "^3.4.27"
|
||||
"svelte": "^4.2.18",
|
||||
"vue": "^3.4.29"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"solid-js": "^1.8.17",
|
||||
"svelte": "^4.2.17",
|
||||
"vue": "^3.4.27"
|
||||
"svelte": "^4.2.18",
|
||||
"vue": "^3.4.29"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"solid-js": "^1.8.17",
|
||||
"svelte": "^4.2.17",
|
||||
"vue": "^3.4.27"
|
||||
"svelte": "^4.2.18",
|
||||
"vue": "^3.4.29"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"solid-js": "^1.8.17",
|
||||
"svelte": "^4.2.17",
|
||||
"vue": "^3.4.27"
|
||||
"svelte": "^4.2.18",
|
||||
"vue": "^3.4.29"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"solid-js": "^1.8.17",
|
||||
"svelte": "^4.2.17",
|
||||
"vue": "^3.4.27"
|
||||
"svelte": "^4.2.18",
|
||||
"vue": "^3.4.29"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"solid-js": "^1.8.17",
|
||||
"svelte": "^4.2.17",
|
||||
"vue": "^3.4.27"
|
||||
"svelte": "^4.2.18",
|
||||
"vue": "^3.4.29"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"solid-js": "^1.8.17",
|
||||
"svelte": "^4.2.17",
|
||||
"vue": "^3.4.27"
|
||||
"svelte": "^4.2.18",
|
||||
"vue": "^3.4.29"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "astro dev"
|
||||
|
|
|
@ -6,6 +6,6 @@
|
|||
"@astrojs/mdx": "workspace:*",
|
||||
"@astrojs/svelte": "workspace:*",
|
||||
"astro": "workspace:*",
|
||||
"svelte": "^4.2.17"
|
||||
"svelte": "^4.2.18"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,6 @@
|
|||
"astro": "workspace:*",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3"
|
||||
"tailwindcss": "^3.4.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"astro": "workspace:*",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"svelte": "^4.2.17",
|
||||
"vue": "^3.4.27"
|
||||
"svelte": "^4.2.18",
|
||||
"vue": "^3.4.29"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import Layout from '../components/Layout.astro';
|
|||
<a id="click-self" href="">go to top</a>
|
||||
<a id="click-redirect-two" href="/redirect-two">go to redirect 2</a>
|
||||
<a id="click-redirect-external" href="/redirect-external">go to a redirect external</a>
|
||||
<a id="click-redirect" href="/redirect">redirect cross-origin</a>
|
||||
<a id="click-404" href="/undefined-page">go to undefined page</a>
|
||||
<custom-a id="custom-click-two">
|
||||
<template shadowrootmode="open">
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
const myURL = Astro.request.url;
|
||||
const redirectTo = (myURL.startsWith("http://localhost")
|
||||
? myURL.replace("http://localhost","http://127.0.0.1")
|
||||
: myURL.replace("http://127.0.0.1", "http://localhost"))
|
||||
.replace("redirect","two");
|
||||
return Astro.redirect(redirectTo);
|
||||
---
|
||||
|
||||
<h1>Should not see this</h1>
|
|
@ -6,6 +6,6 @@
|
|||
"@astrojs/mdx": "workspace:*",
|
||||
"@astrojs/vue": "workspace:*",
|
||||
"astro": "workspace:*",
|
||||
"vue": "^3.4.27"
|
||||
"vue": "^3.4.29"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -788,6 +788,25 @@ test.describe('View Transitions', () => {
|
|||
expect(loads.length, 'There should be 2 page loads').toEqual(2);
|
||||
});
|
||||
|
||||
test('Cross origin redirects do not raise errors', async ({ page, astro }) => {
|
||||
let consoleErrors = [];
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
consoleErrors.push(msg.text());
|
||||
}
|
||||
});
|
||||
// Go to page 1
|
||||
await page.goto(astro.resolveUrl('/one'));
|
||||
let p = page.locator('#one');
|
||||
await expect(p, 'should have content').toHaveText('Page 1');
|
||||
|
||||
await page.click('#click-redirect');
|
||||
p = page.locator('#two');
|
||||
await expect(p, 'should have content').toHaveText('Page 2');
|
||||
|
||||
expect(consoleErrors.length, 'There should be no errors').toEqual(0);
|
||||
});
|
||||
|
||||
test('client:only styles are retained on transition (1/2)', async ({ page, astro }) => {
|
||||
const totalExpectedStyles = 9;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "astro",
|
||||
"version": "4.10.1",
|
||||
"version": "4.11.0",
|
||||
"description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
|
||||
"type": "module",
|
||||
"author": "withastro",
|
||||
|
@ -127,15 +127,15 @@
|
|||
"@astrojs/internal-helpers": "workspace:*",
|
||||
"@astrojs/markdown-remark": "workspace:*",
|
||||
"@astrojs/telemetry": "workspace:*",
|
||||
"@babel/core": "^7.24.6",
|
||||
"@babel/generator": "^7.24.6",
|
||||
"@babel/parser": "^7.24.6",
|
||||
"@babel/plugin-transform-react-jsx": "^7.24.6",
|
||||
"@babel/traverse": "^7.24.6",
|
||||
"@babel/types": "^7.24.6",
|
||||
"@babel/core": "^7.24.7",
|
||||
"@babel/generator": "^7.24.7",
|
||||
"@babel/parser": "^7.24.7",
|
||||
"@babel/plugin-transform-react-jsx": "^7.24.7",
|
||||
"@babel/traverse": "^7.24.7",
|
||||
"@babel/types": "^7.24.7",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"acorn": "^8.11.3",
|
||||
"acorn": "^8.12.0",
|
||||
"aria-query": "^5.3.0",
|
||||
"axobject-query": "^4.0.0",
|
||||
"boxen": "^7.1.1",
|
||||
|
@ -145,14 +145,14 @@
|
|||
"common-ancestor-path": "^1.0.1",
|
||||
"cookie": "^0.6.0",
|
||||
"cssesc": "^3.0.0",
|
||||
"debug": "^4.3.4",
|
||||
"debug": "^4.3.5",
|
||||
"deterministic-object-hash": "^2.0.2",
|
||||
"devalue": "^5.0.0",
|
||||
"diff": "^5.2.0",
|
||||
"dlv": "^1.1.3",
|
||||
"dset": "^3.1.3",
|
||||
"es-module-lexer": "^1.5.3",
|
||||
"esbuild": "^0.21.4",
|
||||
"esbuild": "^0.21.5",
|
||||
"estree-walker": "^3.0.3",
|
||||
"execa": "^8.0.1",
|
||||
"fast-glob": "^3.3.2",
|
||||
|
@ -174,13 +174,13 @@
|
|||
"rehype": "^13.0.1",
|
||||
"resolve": "^1.22.8",
|
||||
"semver": "^7.6.2",
|
||||
"shiki": "^1.6.1",
|
||||
"shiki": "^1.6.5",
|
||||
"string-width": "^7.1.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"tsconfck": "^3.1.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.1",
|
||||
"vite": "^5.2.12",
|
||||
"vite": "^5.3.1",
|
||||
"vitefu": "^0.2.5",
|
||||
"which-pm": "^2.2.0",
|
||||
"yargs-parser": "^21.1.1",
|
||||
|
@ -219,7 +219,7 @@
|
|||
"eol": "^0.9.1",
|
||||
"mdast-util-mdx": "^3.0.0",
|
||||
"mdast-util-mdx-jsx": "^3.1.2",
|
||||
"memfs": "^4.9.2",
|
||||
"memfs": "^4.9.3",
|
||||
"node-mocks-http": "^1.14.1",
|
||||
"parse-srcset": "^1.0.2",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
|
@ -227,8 +227,9 @@
|
|||
"rehype-toc": "^3.0.2",
|
||||
"remark-code-titles": "^0.1.2",
|
||||
"rollup": "^4.18.0",
|
||||
"sass": "^1.77.3",
|
||||
"sass": "^1.77.5",
|
||||
"srcset-parse": "^1.1.0",
|
||||
"undici": "^6.19.2",
|
||||
"unified": "^11.0.4"
|
||||
},
|
||||
"engines": {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { defineConfig } from '@playwright/test';
|
||||
// NOTE: Sometimes, tests fail with `TypeError: process.stdout.clearLine is not a function`
|
||||
// for some reason. This comes from Vite, and is conditionally called based on `isTTY`.
|
||||
// We set it to false here to skip this odd behavior.
|
||||
process.stdout.isTTY = false;
|
||||
|
||||
const config = {
|
||||
export default defineConfig({
|
||||
testMatch: 'e2e/*.test.js',
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 40 * 1000,
|
||||
|
@ -37,6 +38,4 @@ const config = {
|
|||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
});
|
||||
|
|
|
@ -285,7 +285,7 @@ export interface AstroGlobal<
|
|||
/**
|
||||
* The <Astro.self /> element allows a component to reference itself recursively.
|
||||
*
|
||||
* [Astro reference](https://docs.astro.build/en/guides/api-reference/#astroself)
|
||||
* [Astro reference](https://docs.astro.build/en/reference/api-reference/#astroself)
|
||||
*/
|
||||
self: Self;
|
||||
/** Utility functions for modifying an Astro component’s slotted children
|
||||
|
@ -1417,7 +1417,7 @@ export interface AstroUserConfig {
|
|||
* import remarkToc from 'remark-toc';
|
||||
* {
|
||||
* markdown: {
|
||||
* remarkPlugins: [remarkToc]
|
||||
* remarkPlugins: [ [remarkToc, { heading: "contents"} ] ]
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
|
@ -2070,17 +2070,17 @@ export interface AstroUserConfig {
|
|||
*
|
||||
* ```astro
|
||||
* ---
|
||||
* import { PUBLIC_APP_ID } from "astro:env/client"
|
||||
* import { PUBLIC_API_URL, getSecret } from "astro:env/server"
|
||||
* const API_TOKEN = getSecret("API_TOKEN")
|
||||
* import { APP_ID } from "astro:env/client"
|
||||
* import { API_URL, API_TOKEN, getSecret } from "astro:env/server"
|
||||
* const NODE_ENV = getSecret("NODE_ENV")
|
||||
*
|
||||
* const data = await fetch(`${PUBLIC_API_URL}/users`, {
|
||||
* const data = await fetch(`${API_URL}/users`, {
|
||||
* method: "POST",
|
||||
* headers: {
|
||||
* "Content-Type": "application/json",
|
||||
* "Authorization": `Bearer ${API_TOKEN}`
|
||||
* },
|
||||
* body: JSON.stringify({ appId: PUBLIC_APP_ID })
|
||||
* body: JSON.stringify({ appId: APP_ID, nodeEnv: NODE_ENV })
|
||||
* })
|
||||
* ---
|
||||
* ```
|
||||
|
@ -2095,8 +2095,8 @@ export interface AstroUserConfig {
|
|||
* experimental: {
|
||||
* env: {
|
||||
* schema: {
|
||||
* PUBLIC_API_URL: envField.string({ context: "client", access: "public", optional: true }),
|
||||
* PUBLIC_PORT: envField.number({ context: "server", access: "public", default: 4321 }),
|
||||
* API_URL: envField.string({ context: "client", access: "public", optional: true }),
|
||||
* PORT: envField.number({ context: "server", access: "public", default: 4321 }),
|
||||
* API_SECRET: envField.string({ context: "server", access: "secret" }),
|
||||
* }
|
||||
* }
|
||||
|
@ -2104,28 +2104,27 @@ export interface AstroUserConfig {
|
|||
* })
|
||||
* ```
|
||||
*
|
||||
* There are currently three data types supported: strings, numbers and booleans.
|
||||
* There are currently four data types supported: strings, numbers, booleans and enums.
|
||||
*
|
||||
* There are three kinds of environment variables, determined by the combination of `context` (client or server) and `access` (secret or public) settings defined in your [`env.schema`](#experimentalenvschema):
|
||||
*
|
||||
* - **Public client variables**: These variables end up in both your final client and server bundles, and can be accessed from both client and server through the `astro:env/client` module:
|
||||
*
|
||||
* ```js
|
||||
* import { PUBLIC_API_URL } from "astro:env/client"
|
||||
* import { API_URL } from "astro:env/client"
|
||||
* ```
|
||||
*
|
||||
* - **Public server variables**: These variables end up in your final server bundle and can be accessed on the server through the `astro:env/server` module:
|
||||
*
|
||||
* ```js
|
||||
* import { PUBLIC_PORT } from "astro:env/server"
|
||||
* import { PORT } from "astro:env/server"
|
||||
* ```
|
||||
*
|
||||
* - **Secret server variables**: These variables are not part of your final bundle and can be accessed on the server through the `getSecret()` helper function available from the `astro:env/server` module:
|
||||
* - **Secret server variables**: These variables are not part of your final bundle and can be accessed on the server through the `astro:env/server` module. The `getSecret()` helper function can be used to retrieve secrets not specified in the schema:
|
||||
*
|
||||
* ```js
|
||||
* import { getSecret } from "astro:env/server"
|
||||
* import { API_SECRET, getSecret } from "astro:env/server"
|
||||
*
|
||||
* const API_SECRET = getSecret("API_SECRET") // typed
|
||||
* const SECRET_NOT_IN_SCHEMA = getSecret("SECRET_NOT_IN_SCHEMA") // string | undefined
|
||||
* ```
|
||||
*
|
||||
|
@ -2152,8 +2151,8 @@ export interface AstroUserConfig {
|
|||
* experimental: {
|
||||
* env: {
|
||||
* schema: {
|
||||
* PUBLIC_API_URL: envField.string({ context: "client", access: "public", optional: true }),
|
||||
* PUBLIC_PORT: envField.number({ context: "server", access: "public", default: 4321 }),
|
||||
* API_URL: envField.string({ context: "client", access: "public", optional: true }),
|
||||
* PORT: envField.number({ context: "server", access: "public", default: 4321 }),
|
||||
* API_SECRET: envField.string({ context: "server", access: "secret" }),
|
||||
* }
|
||||
* }
|
||||
|
@ -2860,7 +2859,7 @@ export interface APIContext<
|
|||
* }
|
||||
* ```
|
||||
*
|
||||
* [Reference](https://docs.astro.build/en/guides/api-reference/#contextprops)
|
||||
* [Reference](https://docs.astro.build/en/reference/api-reference/#contextprops)
|
||||
*/
|
||||
props: AstroSharedContext<Props, APIParams>['props'];
|
||||
/**
|
||||
|
@ -2980,27 +2979,34 @@ export interface AstroRenderer {
|
|||
jsxTransformOptions?: JSXTransformFn;
|
||||
}
|
||||
|
||||
export interface SSRLoadedRenderer extends AstroRenderer {
|
||||
ssr: {
|
||||
check: AsyncRendererComponentFn<boolean>;
|
||||
renderToStaticMarkup: AsyncRendererComponentFn<{
|
||||
html: string;
|
||||
attrs?: Record<string, string>;
|
||||
}>;
|
||||
supportsAstroStaticSlot?: boolean;
|
||||
/**
|
||||
* If provided, Astro will call this function and inject the returned
|
||||
* script in the HTML before the first component handled by this renderer.
|
||||
*
|
||||
* This feature is needed by some renderers (in particular, by Solid). The
|
||||
* Solid official hydration script sets up a page-level data structure.
|
||||
* It is mainly used to transfer data between the server side render phase
|
||||
* and the browser application state. Solid Components rendered later in
|
||||
* the HTML may inject tiny scripts into the HTML that call into this
|
||||
* page-level data structure.
|
||||
*/
|
||||
renderHydrationScript?: () => string;
|
||||
};
|
||||
export interface NamedSSRLoadedRendererValue extends SSRLoadedRendererValue {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SSRLoadedRendererValue {
|
||||
name?: string;
|
||||
check: AsyncRendererComponentFn<boolean>;
|
||||
renderToStaticMarkup: AsyncRendererComponentFn<{
|
||||
html: string;
|
||||
attrs?: Record<string, string>;
|
||||
}>;
|
||||
supportsAstroStaticSlot?: boolean;
|
||||
/**
|
||||
* If provided, Astro will call this function and inject the returned
|
||||
* script in the HTML before the first component handled by this renderer.
|
||||
*
|
||||
* This feature is needed by some renderers (in particular, by Solid). The
|
||||
* Solid official hydration script sets up a page-level data structure.
|
||||
* It is mainly used to transfer data between the server side render phase
|
||||
* and the browser application state. Solid Components rendered later in
|
||||
* the HTML may inject tiny scripts into the HTML that call into this
|
||||
* page-level data structure.
|
||||
*/
|
||||
renderHydrationScript?: () => string;
|
||||
}
|
||||
|
||||
export interface SSRLoadedRenderer extends Pick<AstroRenderer, 'name' | 'clientEntrypoint'> {
|
||||
ssr: SSRLoadedRendererValue;
|
||||
}
|
||||
|
||||
export type HookParameters<
|
||||
|
|
|
@ -50,7 +50,7 @@ export type ActionClient<
|
|||
|
||||
export function defineAction<
|
||||
TOutput,
|
||||
TAccept extends Accept = 'json',
|
||||
TAccept extends Accept,
|
||||
TInputSchema extends InputSchema<Accept> | undefined = TAccept extends 'form'
|
||||
? // If `input` is omitted, default to `FormData` for forms and `any` for JSON.
|
||||
z.ZodType<FormData>
|
||||
|
|
|
@ -2,11 +2,12 @@ import type { AstroConfig } from '../@types/astro.js';
|
|||
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
||||
import { DEFAULT_HASH_PROPS } from './consts.js';
|
||||
import { type ImageService, isLocalService } from './services/service.js';
|
||||
import type {
|
||||
GetImageResult,
|
||||
ImageTransform,
|
||||
SrcSetValue,
|
||||
UnresolvedImageTransform,
|
||||
import {
|
||||
type GetImageResult,
|
||||
type ImageTransform,
|
||||
type SrcSetValue,
|
||||
type UnresolvedImageTransform,
|
||||
isImageMetadata,
|
||||
} from './types.js';
|
||||
import { isESMImportedImage, isRemoteImage, resolveSrc } from './utils/imageKind.js';
|
||||
import { probe } from './utils/remoteProbe.js';
|
||||
|
@ -51,6 +52,10 @@ export async function getImage(
|
|||
});
|
||||
}
|
||||
|
||||
if (isImageMetadata(options)) {
|
||||
throw new AstroError(AstroErrorData.ExpectedNotESMImage);
|
||||
}
|
||||
|
||||
const service = await getConfiguredImageService();
|
||||
|
||||
// If the user inlined an import, something fairly common especially in MDX, or passed a function that returns an Image, await it for them
|
||||
|
|
|
@ -28,10 +28,14 @@ declare global {
|
|||
};
|
||||
}
|
||||
|
||||
const isESMImport = Symbol('#isESM');
|
||||
|
||||
export type OmitBrand<T> = Omit<T, typeof isESMImport>;
|
||||
|
||||
/**
|
||||
* Type returned by ESM imports of images
|
||||
*/
|
||||
export interface ImageMetadata {
|
||||
export type ImageMetadata = {
|
||||
src: string;
|
||||
width: number;
|
||||
height: number;
|
||||
|
@ -39,6 +43,12 @@ export interface ImageMetadata {
|
|||
orientation?: number;
|
||||
/** @internal */
|
||||
fsPath: string;
|
||||
[isESMImport]?: true;
|
||||
};
|
||||
|
||||
export function isImageMetadata(src: any): src is ImageMetadata {
|
||||
// For ESM-imported images the fsPath property is set but not enumerable
|
||||
return src.fsPath && !('fsPath' in src);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -60,6 +70,8 @@ export type SrcSetValue = UnresolvedSrcSetValue & {
|
|||
export type UnresolvedImageTransform = Omit<ImageTransform, 'src'> & {
|
||||
src: ImageMetadata | string | Promise<{ default: ImageMetadata }>;
|
||||
inferSize?: boolean;
|
||||
} & {
|
||||
[isESMImport]?: never;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { UserConfig } from 'vite';
|
||||
import type { UserConfig as ViteUserConfig } from 'vite';
|
||||
import type { AstroInlineConfig, AstroUserConfig } from '../@types/astro.js';
|
||||
import { Logger } from '../core/logger/core.js';
|
||||
|
||||
|
@ -6,7 +6,10 @@ export function defineConfig(config: AstroUserConfig) {
|
|||
return config;
|
||||
}
|
||||
|
||||
export function getViteConfig(inlineConfig: UserConfig, inlineAstroConfig: AstroInlineConfig = {}) {
|
||||
export function getViteConfig(
|
||||
userViteConfig: ViteUserConfig,
|
||||
inlineAstroConfig: AstroInlineConfig = {}
|
||||
) {
|
||||
// Return an async Vite config getter which exposes a resolved `mode` and `command`
|
||||
return async ({ mode, command }: { mode: string; command: 'serve' | 'build' }) => {
|
||||
// Vite `command` is `serve | build`, but Astro uses `dev | build`
|
||||
|
@ -34,8 +37,8 @@ export function getViteConfig(inlineConfig: UserConfig, inlineAstroConfig: Astro
|
|||
dest: nodeLogDestination,
|
||||
level: 'info',
|
||||
});
|
||||
const { astroConfig: config, userConfig } = await resolveConfig(inlineAstroConfig, cmd);
|
||||
let settings = await createSettings(config, inlineConfig.root);
|
||||
const { astroConfig: config } = await resolveConfig(inlineAstroConfig, cmd);
|
||||
let settings = await createSettings(config, userViteConfig.root);
|
||||
settings = await runHookConfigSetup({ settings, command: cmd, logger });
|
||||
const viteConfig = await createVite(
|
||||
{
|
||||
|
@ -48,6 +51,6 @@ export function getViteConfig(inlineConfig: UserConfig, inlineAstroConfig: Astro
|
|||
{ settings, logger, mode }
|
||||
);
|
||||
await runHookConfigDone({ settings, logger });
|
||||
return mergeConfig(viteConfig, userConfig);
|
||||
return mergeConfig(viteConfig, userViteConfig);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { posix } from 'node:path';
|
||||
import type {
|
||||
AstroConfig,
|
||||
AstroRenderer,
|
||||
AstroUserConfig,
|
||||
ComponentInstance,
|
||||
ContainerImportRendererFn,
|
||||
ContainerRenderer,
|
||||
MiddlewareHandler,
|
||||
NamedSSRLoadedRendererValue,
|
||||
Props,
|
||||
RouteData,
|
||||
RouteType,
|
||||
SSRLoadedRenderer,
|
||||
SSRLoadedRendererValue,
|
||||
SSRManifest,
|
||||
SSRResult,
|
||||
} from '../@types/astro.js';
|
||||
|
@ -85,6 +85,16 @@ export type ContainerRenderOptions = {
|
|||
props?: Props;
|
||||
};
|
||||
|
||||
export type AddServerRenderer =
|
||||
| {
|
||||
renderer: NamedSSRLoadedRendererValue;
|
||||
name: never;
|
||||
}
|
||||
| {
|
||||
renderer: SSRLoadedRendererValue;
|
||||
name: string;
|
||||
};
|
||||
|
||||
function createManifest(
|
||||
manifest?: AstroContainerManifest,
|
||||
renderers?: SSRLoadedRenderer[],
|
||||
|
@ -270,6 +280,50 @@ export class experimental_AstroContainer {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this function to manually add a renderer to the container.
|
||||
*
|
||||
* This function is preferred when you require to use the container with a renderer in environments such as on-demand pages.
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* ```js
|
||||
* import reactRenderer from "@astrojs/react/server.js";
|
||||
* import vueRenderer from "@astrojs/vue/server.js";
|
||||
* import customRenderer from "../renderer/customRenderer.js";
|
||||
* import { experimental_AstroContainer as AstroContainer } from "astro/container"
|
||||
*
|
||||
* const container = await AstroContainer.create();
|
||||
* container.addServerRenderer(reactRenderer);
|
||||
* container.addServerRenderer(vueRenderer);
|
||||
* container.addServerRenderer("customRenderer", customRenderer);
|
||||
* ```
|
||||
*
|
||||
* @param options {object}
|
||||
* @param options.name The name of the renderer. The name **isn't** arbitrary, and it should match the name of the package.
|
||||
* @param options.renderer The server renderer exported by integration.
|
||||
*/
|
||||
public addServerRenderer(options: AddServerRenderer): void {
|
||||
const { renderer, name } = options;
|
||||
if (!renderer.check || !renderer.renderToStaticMarkup) {
|
||||
throw new Error(
|
||||
"The renderer you passed isn't valid. A renderer is usually an object that exposes the `check` and `renderToStaticMarkup` functions.\n" +
|
||||
"Usually, the renderer is exported by a /server.js entrypoint e.g. `import renderer from '@astrojs/react/server.js'`"
|
||||
);
|
||||
}
|
||||
if (isNamedRenderer(renderer)) {
|
||||
this.#pipeline.manifest.renderers.push({
|
||||
name: renderer.name,
|
||||
ssr: renderer,
|
||||
});
|
||||
} else {
|
||||
this.#pipeline.manifest.renderers.push({
|
||||
name,
|
||||
ssr: renderer,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: we keep this private via TS instead via `#` so it's still available on the surface, so we can play with it.
|
||||
// @ematipico: I plan to use it for a possible integration that could help people
|
||||
private static async createFromManifest(
|
||||
|
@ -440,3 +494,7 @@ export class experimental_AstroContainer {
|
|||
return { default: componentFactory };
|
||||
}
|
||||
}
|
||||
|
||||
function isNamedRenderer(renderer: any): renderer is NamedSSRLoadedRendererValue {
|
||||
return !!renderer?.name;
|
||||
}
|
||||
|
|
|
@ -7,12 +7,11 @@ import type {
|
|||
} from '../@types/astro.js';
|
||||
import { type HeadElements, Pipeline } from '../core/base-pipeline.js';
|
||||
import type { SinglePageBuiltModule } from '../core/build/types.js';
|
||||
import { InvalidRewrite404, RouteNotFound } from '../core/errors/errors-data.js';
|
||||
import { AstroError } from '../core/errors/index.js';
|
||||
import {
|
||||
createModuleScriptElement,
|
||||
createStylesheetElementSet,
|
||||
} from '../core/render/ssr-element.js';
|
||||
import { findRouteToRewrite } from '../core/routing/rewrite.js';
|
||||
|
||||
export class ContainerPipeline extends Pipeline {
|
||||
/**
|
||||
|
@ -69,33 +68,21 @@ export class ContainerPipeline extends Pipeline {
|
|||
return { links, styles, scripts };
|
||||
}
|
||||
|
||||
async tryRewrite(rewritePayload: RewritePayload): Promise<[RouteData, ComponentInstance]> {
|
||||
let foundRoute: RouteData | undefined;
|
||||
// options.manifest is the actual type that contains the information
|
||||
for (const route of this.manifest.routes) {
|
||||
const routeData = route.routeData;
|
||||
if (rewritePayload instanceof URL) {
|
||||
if (routeData.pattern.test(rewritePayload.pathname)) {
|
||||
foundRoute = routeData;
|
||||
break;
|
||||
}
|
||||
} else if (rewritePayload instanceof Request) {
|
||||
const url = new URL(rewritePayload.url);
|
||||
if (routeData.pattern.test(url.pathname)) {
|
||||
foundRoute = routeData;
|
||||
break;
|
||||
}
|
||||
} else if (routeData.pattern.test(decodeURI(rewritePayload))) {
|
||||
foundRoute = routeData;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundRoute) {
|
||||
const componentInstance = await this.getComponentByRoute(foundRoute);
|
||||
return [foundRoute, componentInstance];
|
||||
} else {
|
||||
throw new AstroError(RouteNotFound);
|
||||
}
|
||||
async tryRewrite(
|
||||
payload: RewritePayload,
|
||||
request: Request
|
||||
): Promise<[RouteData, ComponentInstance, URL]> {
|
||||
const [foundRoute, finalUrl] = findRouteToRewrite({
|
||||
payload,
|
||||
request,
|
||||
routes: this.manifest?.routes.map((r) => r.routeData),
|
||||
trailingSlash: this.manifest.trailingSlash,
|
||||
buildFormat: this.manifest.buildFormat,
|
||||
base: this.manifest.base,
|
||||
});
|
||||
|
||||
const componentInstance = await this.getComponentByRoute(foundRoute);
|
||||
return [foundRoute, componentInstance, finalUrl];
|
||||
}
|
||||
|
||||
insertRoute(route: RouteData, componentInstance: ComponentInstance): void {
|
||||
|
@ -111,12 +98,4 @@ export class ContainerPipeline extends Pipeline {
|
|||
// At the moment it's not used by the container via any public API
|
||||
// @ts-expect-error It needs to be implemented.
|
||||
async getComponentByRoute(_routeData: RouteData): Promise<ComponentInstance> {}
|
||||
|
||||
rewriteKnownRoute(pathname: string, _sourceRoute: RouteData): ComponentInstance {
|
||||
if (pathname === '/404') {
|
||||
return { default: default404Page } as any as ComponentInstance;
|
||||
}
|
||||
|
||||
throw new AstroError(InvalidRewrite404);
|
||||
}
|
||||
}
|
||||
|
|
15
packages/astro/src/container/vite-plugin-container.ts
Normal file
15
packages/astro/src/container/vite-plugin-container.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import type * as vite from 'vite';
|
||||
|
||||
const virtualModuleId = 'astro:container';
|
||||
|
||||
export default function astroContainer(): vite.Plugin {
|
||||
return {
|
||||
name: 'astro:container',
|
||||
enforce: 'pre',
|
||||
resolveId(id) {
|
||||
if (id === virtualModuleId) {
|
||||
return this.resolve('astro/virtual-modules/container.js');
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import type { PluginContext } from 'rollup';
|
||||
import { z } from 'zod';
|
||||
import type { ImageMetadata, OmitBrand } from '../assets/types.js';
|
||||
import { emitESMImage } from '../assets/utils/emitAsset.js';
|
||||
|
||||
export function createImage(
|
||||
|
@ -10,11 +11,11 @@ export function createImage(
|
|||
return () => {
|
||||
return z.string().transform(async (imagePath, ctx) => {
|
||||
const resolvedFilePath = (await pluginContext.resolve(imagePath, entryFilePath))?.id;
|
||||
const metadata = await emitESMImage(
|
||||
const metadata = (await emitESMImage(
|
||||
resolvedFilePath,
|
||||
pluginContext.meta.watchMode,
|
||||
shouldEmitFile ? pluginContext.emitFile : undefined
|
||||
);
|
||||
)) as OmitBrand<ImageMetadata>;
|
||||
|
||||
if (!metadata) {
|
||||
ctx.addIssue({
|
||||
|
|
|
@ -79,8 +79,7 @@ export function createGetCollection({
|
|||
// Cache `getCollection()` calls in production only
|
||||
// prevents stale cache in development
|
||||
if (!import.meta.env?.DEV && cacheEntriesByCollection.has(collection)) {
|
||||
// Always return a new instance so consumers can safely mutate it
|
||||
entries = [...cacheEntriesByCollection.get(collection)!];
|
||||
entries = cacheEntriesByCollection.get(collection)!;
|
||||
} else {
|
||||
const limit = pLimit(10);
|
||||
entries = await Promise.all(
|
||||
|
@ -115,7 +114,9 @@ export function createGetCollection({
|
|||
if (typeof filter === 'function') {
|
||||
return entries.filter(filter);
|
||||
} else {
|
||||
return entries;
|
||||
// Clone the array so users can safely mutate it.
|
||||
// slice() is faster than ...spread for large arrays.
|
||||
return entries.slice();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -429,12 +429,17 @@ async function writeContentFiles({
|
|||
collectionConfig?.type ?? 'data'
|
||||
: collection.type;
|
||||
|
||||
const collectionEntryKeys = Object.keys(collection.entries).sort();
|
||||
const dataType = collectionConfig?.schema ? `InferEntrySchema<${collectionKey}>` : 'any';
|
||||
switch (resolvedType) {
|
||||
case 'content':
|
||||
if (collectionEntryKeys.length === 0) {
|
||||
contentTypesStr += `${collectionKey}: Record<string, {\n id: string;\n slug: string;\n body: string;\n collection: ${collectionKey};\n data: ${dataType};\n render(): Render[".md"];\n}>;\n`;
|
||||
break;
|
||||
}
|
||||
contentTypesStr += `${collectionKey}: {\n`;
|
||||
for (const entryKey of Object.keys(collection.entries).sort()) {
|
||||
for (const entryKey of collectionEntryKeys) {
|
||||
const entryMetadata = collection.entries[entryKey];
|
||||
const dataType = collectionConfig?.schema ? `InferEntrySchema<${collectionKey}>` : 'any';
|
||||
const renderType = `{ render(): Render[${JSON.stringify(
|
||||
path.extname(JSON.parse(entryKey))
|
||||
)}] }`;
|
||||
|
@ -445,45 +450,46 @@ async function writeContentFiles({
|
|||
contentTypesStr += `};\n`;
|
||||
break;
|
||||
case 'data':
|
||||
dataTypesStr += `${collectionKey}: {\n`;
|
||||
for (const entryKey of Object.keys(collection.entries).sort()) {
|
||||
const dataType = collectionConfig?.schema ? `InferEntrySchema<${collectionKey}>` : 'any';
|
||||
dataTypesStr += `${entryKey}: {\n id: ${entryKey};\n collection: ${collectionKey};\n data: ${dataType}\n};\n`;
|
||||
if (
|
||||
settings.config.experimental.contentCollectionJsonSchema &&
|
||||
collectionConfig?.schema
|
||||
) {
|
||||
let zodSchemaForJson =
|
||||
typeof collectionConfig.schema === 'function'
|
||||
? collectionConfig.schema({ image: () => z.string() })
|
||||
: collectionConfig.schema;
|
||||
if (zodSchemaForJson instanceof z.ZodObject) {
|
||||
zodSchemaForJson = zodSchemaForJson.extend({
|
||||
$schema: z.string().optional(),
|
||||
});
|
||||
}
|
||||
try {
|
||||
await fs.promises.writeFile(
|
||||
new URL(`./${collectionKey.replace(/"/g, '')}.schema.json`, collectionSchemasDir),
|
||||
JSON.stringify(
|
||||
zodToJsonSchema(zodSchemaForJson, {
|
||||
name: collectionKey.replace(/"/g, ''),
|
||||
markdownDescription: true,
|
||||
errorMessages: true,
|
||||
}),
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
'content',
|
||||
`An error was encountered while creating the JSON schema for the ${entryKey} entry in ${collectionKey} collection. Proceeding without it. Error: ${err}`
|
||||
);
|
||||
}
|
||||
if (collectionEntryKeys.length === 0) {
|
||||
dataTypesStr += `${collectionKey}: Record<string, {\n id: string;\n collection: ${collectionKey};\n data: ${dataType};\n}>;\n`;
|
||||
} else {
|
||||
dataTypesStr += `${collectionKey}: {\n`;
|
||||
for (const entryKey of collectionEntryKeys) {
|
||||
dataTypesStr += `${entryKey}: {\n id: ${entryKey};\n collection: ${collectionKey};\n data: ${dataType}\n};\n`;
|
||||
}
|
||||
dataTypesStr += `};\n`;
|
||||
}
|
||||
|
||||
if (settings.config.experimental.contentCollectionJsonSchema && collectionConfig?.schema) {
|
||||
let zodSchemaForJson =
|
||||
typeof collectionConfig.schema === 'function'
|
||||
? collectionConfig.schema({ image: () => z.string() })
|
||||
: collectionConfig.schema;
|
||||
if (zodSchemaForJson instanceof z.ZodObject) {
|
||||
zodSchemaForJson = zodSchemaForJson.extend({
|
||||
$schema: z.string().optional(),
|
||||
});
|
||||
}
|
||||
try {
|
||||
await fs.promises.writeFile(
|
||||
new URL(`./${collectionKey.replace(/"/g, '')}.schema.json`, collectionSchemasDir),
|
||||
JSON.stringify(
|
||||
zodToJsonSchema(zodSchemaForJson, {
|
||||
name: collectionKey.replace(/"/g, ''),
|
||||
markdownDescription: true,
|
||||
errorMessages: true,
|
||||
}),
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
'content',
|
||||
`An error was encountered while creating the JSON schema for the ${collectionKey} collection. Proceeding without it. Error: ${err}`
|
||||
);
|
||||
}
|
||||
}
|
||||
dataTypesStr += `};\n`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,9 +14,9 @@ import type {
|
|||
} from '../@types/astro.js';
|
||||
import { AstroError, AstroErrorData, MarkdownError, errorMap } from '../core/errors/index.js';
|
||||
import { isYAMLException } from '../core/errors/utils.js';
|
||||
import type { Logger } from '../core/logger/core.js';
|
||||
import { CONTENT_FLAGS, PROPAGATED_ASSET_FLAG } from './consts.js';
|
||||
import { createImage } from './runtime-assets.js';
|
||||
|
||||
/**
|
||||
* Amap from a collection + slug to the local file path.
|
||||
* This is used internally to resolve entry imports when using `getEntry()`.
|
||||
|
@ -167,6 +167,67 @@ export function getEntryConfigByExtMap<TEntryType extends ContentEntryType | Dat
|
|||
return map;
|
||||
}
|
||||
|
||||
export async function getSymlinkedContentCollections({
|
||||
contentDir,
|
||||
logger,
|
||||
fs,
|
||||
}: {
|
||||
contentDir: URL;
|
||||
logger: Logger;
|
||||
fs: typeof fsMod;
|
||||
}): Promise<Map<string, string>> {
|
||||
const contentPaths = new Map<string, string>();
|
||||
const contentDirPath = fileURLToPath(contentDir);
|
||||
try {
|
||||
if (!fs.existsSync(contentDirPath) || !fs.lstatSync(contentDirPath).isDirectory()) {
|
||||
return contentPaths;
|
||||
}
|
||||
} catch {
|
||||
// Ignore if there isn't a valid content directory
|
||||
return contentPaths;
|
||||
}
|
||||
try {
|
||||
const contentDirEntries = await fs.promises.readdir(contentDir, { withFileTypes: true });
|
||||
for (const entry of contentDirEntries) {
|
||||
if (entry.isSymbolicLink()) {
|
||||
const entryPath = path.join(contentDirPath, entry.name);
|
||||
const realPath = await fs.promises.realpath(entryPath);
|
||||
contentPaths.set(normalizePath(realPath), entry.name);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('content', `Error when reading content directory "${contentDir}"`);
|
||||
logger.debug('content', e);
|
||||
// If there's an error, return an empty map
|
||||
return new Map<string, string>();
|
||||
}
|
||||
|
||||
return contentPaths;
|
||||
}
|
||||
|
||||
export function reverseSymlink({
|
||||
entry,
|
||||
symlinks,
|
||||
contentDir,
|
||||
}: {
|
||||
entry: string | URL;
|
||||
contentDir: string | URL;
|
||||
symlinks?: Map<string, string>;
|
||||
}): string {
|
||||
const entryPath = normalizePath(typeof entry === 'string' ? entry : fileURLToPath(entry));
|
||||
const contentDirPath = typeof contentDir === 'string' ? contentDir : fileURLToPath(contentDir);
|
||||
if (!symlinks || symlinks.size === 0) {
|
||||
return entryPath;
|
||||
}
|
||||
|
||||
for (const [realPath, symlinkName] of symlinks) {
|
||||
if (entryPath.startsWith(realPath)) {
|
||||
return normalizePath(path.join(contentDirPath, symlinkName, entryPath.replace(realPath, '')));
|
||||
}
|
||||
}
|
||||
return entryPath;
|
||||
}
|
||||
|
||||
export function getEntryCollectionName({
|
||||
contentDir,
|
||||
entry,
|
||||
|
|
|
@ -15,6 +15,7 @@ import type {
|
|||
import { getProxyCode } from '../assets/utils/proxy.js';
|
||||
import { AstroError } from '../core/errors/errors.js';
|
||||
import { AstroErrorData } from '../core/errors/index.js';
|
||||
import type { Logger } from '../core/logger/core.js';
|
||||
import { isServerLikeOutput } from '../core/util.js';
|
||||
import { CONTENT_FLAG, DATA_FLAG } from './consts.js';
|
||||
import {
|
||||
|
@ -28,10 +29,12 @@ import {
|
|||
getEntryConfigByExtMap,
|
||||
getEntryData,
|
||||
getEntryType,
|
||||
getSymlinkedContentCollections,
|
||||
globalContentConfigObserver,
|
||||
hasContentFlag,
|
||||
parseEntrySlug,
|
||||
reloadContentConfigObserver,
|
||||
reverseSymlink,
|
||||
} from './utils.js';
|
||||
|
||||
function getContentRendererByViteId(
|
||||
|
@ -63,9 +66,11 @@ const COLLECTION_TYPES_TO_INVALIDATE_ON = ['data', 'content', 'config'];
|
|||
export function astroContentImportPlugin({
|
||||
fs,
|
||||
settings,
|
||||
logger,
|
||||
}: {
|
||||
fs: typeof fsMod;
|
||||
settings: AstroSettings;
|
||||
logger: Logger;
|
||||
}): Plugin[] {
|
||||
const contentPaths = getContentPaths(settings.config, fs);
|
||||
const contentEntryExts = getContentEntryExts(settings);
|
||||
|
@ -75,16 +80,26 @@ export function astroContentImportPlugin({
|
|||
const dataEntryConfigByExt = getEntryConfigByExtMap(settings.dataEntryTypes);
|
||||
const { contentDir } = contentPaths;
|
||||
let shouldEmitFile = false;
|
||||
|
||||
let symlinks: Map<string, string>;
|
||||
const plugins: Plugin[] = [
|
||||
{
|
||||
name: 'astro:content-imports',
|
||||
config(_config, env) {
|
||||
shouldEmitFile = env.command === 'build';
|
||||
},
|
||||
async buildStart() {
|
||||
// Get symlinks once at build start
|
||||
symlinks = await getSymlinkedContentCollections({ contentDir, logger, fs });
|
||||
},
|
||||
async transform(_, viteId) {
|
||||
if (hasContentFlag(viteId, DATA_FLAG)) {
|
||||
const fileId = viteId.split('?')[0] ?? viteId;
|
||||
// By default, Vite will resolve symlinks to their targets. We need to reverse this for
|
||||
// content entries, so we can get the path relative to the content directory.
|
||||
const fileId = reverseSymlink({
|
||||
entry: viteId.split('?')[0] ?? viteId,
|
||||
contentDir,
|
||||
symlinks,
|
||||
});
|
||||
// Data collections don't need to rely on the module cache.
|
||||
// This cache only exists for the `render()` function specific to content.
|
||||
const { id, data, collection, _internal } = await getDataEntryModule({
|
||||
|
@ -109,7 +124,7 @@ export const _internal = {
|
|||
`;
|
||||
return code;
|
||||
} else if (hasContentFlag(viteId, CONTENT_FLAG)) {
|
||||
const fileId = viteId.split('?')[0];
|
||||
const fileId = reverseSymlink({ entry: viteId.split('?')[0], contentDir, symlinks });
|
||||
const { id, slug, collection, body, data, _internal } = await getContentEntryModule({
|
||||
fileId,
|
||||
entryConfigByExt: contentEntryConfigByExt,
|
||||
|
|
|
@ -67,6 +67,10 @@ export interface RenderErrorOptions {
|
|||
* Whether to skip middleware while rendering the error page. Defaults to false.
|
||||
*/
|
||||
skipMiddleware?: boolean;
|
||||
/**
|
||||
* Allows passing an error to 500.astro. It will be available through `Astro.props.error`.
|
||||
*/
|
||||
error?: unknown;
|
||||
}
|
||||
|
||||
export class App {
|
||||
|
@ -289,8 +293,9 @@ export class App {
|
|||
}
|
||||
if (locals) {
|
||||
if (typeof locals !== 'object') {
|
||||
this.#logger.error(null, new AstroError(AstroErrorData.LocalsNotAnObject).stack!);
|
||||
return this.#renderError(request, { status: 500 });
|
||||
const error = new AstroError(AstroErrorData.LocalsNotAnObject);
|
||||
this.#logger.error(null, error.stack!);
|
||||
return this.#renderError(request, { status: 500, error });
|
||||
}
|
||||
Reflect.set(request, clientLocalsSymbol, locals);
|
||||
}
|
||||
|
@ -324,7 +329,7 @@ export class App {
|
|||
response = await renderContext.render(await mod.page());
|
||||
} catch (err: any) {
|
||||
this.#logger.error(null, err.stack || err.message || String(err));
|
||||
return this.#renderError(request, { locals, status: 500 });
|
||||
return this.#renderError(request, { locals, status: 500, error: err });
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -335,6 +340,9 @@ export class App {
|
|||
locals,
|
||||
response,
|
||||
status: response.status as 404 | 500,
|
||||
// We don't have an error to report here. Passing null means we pass nothing intentionally
|
||||
// while undefined means there's no error
|
||||
error: response.status === 500 ? null : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -385,7 +393,13 @@ export class App {
|
|||
*/
|
||||
async #renderError(
|
||||
request: Request,
|
||||
{ locals, status, response: originalResponse, skipMiddleware = false }: RenderErrorOptions
|
||||
{
|
||||
locals,
|
||||
status,
|
||||
response: originalResponse,
|
||||
skipMiddleware = false,
|
||||
error,
|
||||
}: RenderErrorOptions
|
||||
): Promise<Response> {
|
||||
const errorRoutePath = `/${status}${this.#manifest.trailingSlash === 'always' ? '/' : ''}`;
|
||||
const errorRouteData = matchRoute(errorRoutePath, this.#manifestData);
|
||||
|
@ -415,6 +429,7 @@ export class App {
|
|||
request,
|
||||
routeData: errorRouteData,
|
||||
status,
|
||||
props: { error },
|
||||
});
|
||||
const response = await renderContext.render(await mod.page());
|
||||
return this.#mergeResponses(response, originalResponse);
|
||||
|
|
|
@ -14,6 +14,7 @@ import { AstroError } from '../errors/index.js';
|
|||
import { RedirectSinglePageBuiltModule } from '../redirects/component.js';
|
||||
import { createModuleScriptElement, createStylesheetElementSet } from '../render/ssr-element.js';
|
||||
import { DEFAULT_404_ROUTE } from '../routing/astro-designed-error-pages.js';
|
||||
import { findRouteToRewrite } from '../routing/rewrite.js';
|
||||
|
||||
export class AppPipeline extends Pipeline {
|
||||
#manifestData: ManifestData | undefined;
|
||||
|
@ -86,43 +87,19 @@ export class AppPipeline extends Pipeline {
|
|||
async tryRewrite(
|
||||
payload: RewritePayload,
|
||||
request: Request,
|
||||
sourceRoute: RouteData
|
||||
): Promise<[RouteData, ComponentInstance]> {
|
||||
let foundRoute;
|
||||
|
||||
for (const route of this.#manifestData!.routes) {
|
||||
let finalUrl: URL | undefined = undefined;
|
||||
|
||||
if (payload instanceof URL) {
|
||||
finalUrl = payload;
|
||||
} else if (payload instanceof Request) {
|
||||
finalUrl = new URL(payload.url);
|
||||
} else {
|
||||
finalUrl = new URL(payload, new URL(request.url).origin);
|
||||
}
|
||||
|
||||
if (route.pattern.test(decodeURI(finalUrl.pathname))) {
|
||||
foundRoute = route;
|
||||
break;
|
||||
} else if (finalUrl.pathname === '/404') {
|
||||
foundRoute = DEFAULT_404_ROUTE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundRoute) {
|
||||
if (foundRoute.pathname === '/404') {
|
||||
const componentInstance = this.rewriteKnownRoute(foundRoute.pathname, sourceRoute);
|
||||
return [foundRoute, componentInstance];
|
||||
} else {
|
||||
const componentInstance = await this.getComponentByRoute(foundRoute);
|
||||
return [foundRoute, componentInstance];
|
||||
}
|
||||
}
|
||||
throw new AstroError({
|
||||
...RewriteEncounteredAnError,
|
||||
message: RewriteEncounteredAnError.message(payload.toString()),
|
||||
_sourceRoute: RouteData
|
||||
): Promise<[RouteData, ComponentInstance, URL]> {
|
||||
const [foundRoute, finalUrl] = findRouteToRewrite({
|
||||
payload,
|
||||
request,
|
||||
routes: this.manifest?.routes.map((r) => r.routeData),
|
||||
trailingSlash: this.manifest.trailingSlash,
|
||||
buildFormat: this.manifest.buildFormat,
|
||||
base: this.manifest.base,
|
||||
});
|
||||
|
||||
const componentInstance = await this.getComponentByRoute(foundRoute);
|
||||
return [foundRoute, componentInstance, finalUrl];
|
||||
}
|
||||
|
||||
async getModuleForRoute(route: RouteData): Promise<SinglePageBuiltModule> {
|
||||
|
@ -152,13 +129,4 @@ export class AppPipeline extends Pipeline {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
// We don't need to check the source route, we already are in SSR
|
||||
rewriteKnownRoute(pathname: string, _sourceRoute: RouteData): ComponentInstance {
|
||||
if (pathname === '/404') {
|
||||
return { default: () => new Response(null, { status: 404 }) } as ComponentInstance;
|
||||
} else {
|
||||
return { default: () => new Response(null, { status: 500 }) } as ComponentInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ export abstract class Pipeline {
|
|||
if (callSetGetEnv && manifest.experimentalEnvGetSecretEnabled) {
|
||||
setGetEnv(() => {
|
||||
throw new AstroError(AstroErrorData.EnvUnsupportedGetSecret);
|
||||
});
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,23 +88,13 @@ export abstract class Pipeline {
|
|||
rewritePayload: RewritePayload,
|
||||
request: Request,
|
||||
sourceRoute: RouteData
|
||||
): Promise<[RouteData, ComponentInstance]>;
|
||||
): Promise<[RouteData, ComponentInstance, URL]>;
|
||||
|
||||
/**
|
||||
* Tells the pipeline how to retrieve a component give a `RouteData`
|
||||
* @param routeData
|
||||
*/
|
||||
abstract getComponentByRoute(routeData: RouteData): Promise<ComponentInstance>;
|
||||
|
||||
/**
|
||||
* Attempts to execute a rewrite of a known Astro route:
|
||||
* - /404 -> src/pages/404.astro -> template
|
||||
* - /500 -> src/pages/500.astro
|
||||
*
|
||||
* @param pathname The pathname where the user wants to rewrite e.g. "/404"
|
||||
* @param sourceRoute The original route where the first request started. This is needed in case a pipeline wants to check if the current route is pre-rendered or not
|
||||
*/
|
||||
abstract rewriteKnownRoute(pathname: string, sourceRoute: RouteData): ComponentInstance;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
|
|
|
@ -113,6 +113,11 @@ export interface BuildInternals {
|
|||
ssrSplitEntryChunks: Map<string, Rollup.OutputChunk>;
|
||||
componentMetadata: SSRResult['componentMetadata'];
|
||||
middlewareEntryPoint?: URL;
|
||||
|
||||
/**
|
||||
* Chunks in the bundle that are only used in prerendering that we can delete later
|
||||
*/
|
||||
prerenderOnlyChunks: Rollup.OutputChunk[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -151,6 +156,7 @@ export function createBuildInternals(): BuildInternals {
|
|||
ssrSplitEntryChunks: new Map(),
|
||||
entryPoints: new Map(),
|
||||
cacheManifestUsed: false,
|
||||
prerenderOnlyChunks: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -57,7 +57,6 @@ export async function collectPagesData(
|
|||
moduleSpecifier: '',
|
||||
styles: [],
|
||||
hoistedScript: undefined,
|
||||
hasSharedModules: false,
|
||||
};
|
||||
|
||||
clearInterval(routeCollectionLogTimeout);
|
||||
|
@ -80,7 +79,6 @@ export async function collectPagesData(
|
|||
moduleSpecifier: '',
|
||||
styles: [],
|
||||
hoistedScript: undefined,
|
||||
hasSharedModules: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -8,8 +8,7 @@ import type {
|
|||
import { getOutputDirectory } from '../../prerender/utils.js';
|
||||
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||
import type { SSRManifest } from '../app/types.js';
|
||||
import { InvalidRewrite404, RewriteEncounteredAnError } from '../errors/errors-data.js';
|
||||
import { AstroError } from '../errors/index.js';
|
||||
import { DEFAULT_404_COMPONENT } from '../constants.js';
|
||||
import { routeIsFallback, routeIsRedirect } from '../redirects/helpers.js';
|
||||
import { RedirectSinglePageBuiltModule } from '../redirects/index.js';
|
||||
import { Pipeline } from '../render/index.js';
|
||||
|
@ -18,7 +17,8 @@ import {
|
|||
createModuleScriptsSet,
|
||||
createStylesheetElementSet,
|
||||
} from '../render/ssr-element.js';
|
||||
import { DEFAULT_404_ROUTE } from '../routing/astro-designed-error-pages.js';
|
||||
import { default404Page } from '../routing/astro-designed-error-pages.js';
|
||||
import { findRouteToRewrite } from '../routing/rewrite.js';
|
||||
import { isServerLikeOutput } from '../util.js';
|
||||
import { getOutDirWithinCwd } from './common.js';
|
||||
import { type BuildInternals, cssOrder, getPageData, mergeInlineCss } from './internal.js';
|
||||
|
@ -269,6 +269,8 @@ export class BuildPipeline extends Pipeline {
|
|||
// SAFETY: checked before
|
||||
const entry = this.#componentsInterner.get(routeData)!;
|
||||
return await entry.page();
|
||||
} else if (routeData.component === DEFAULT_404_COMPONENT) {
|
||||
return { default: default404Page };
|
||||
} else {
|
||||
// SAFETY: the pipeline calls `retrieveRoutesToGenerate`, which is in charge to fill the cache.
|
||||
const filePath = this.#routesByFilePath.get(routeData)!;
|
||||
|
@ -280,43 +282,19 @@ export class BuildPipeline extends Pipeline {
|
|||
async tryRewrite(
|
||||
payload: RewritePayload,
|
||||
request: Request,
|
||||
sourceRoute: RouteData
|
||||
): Promise<[RouteData, ComponentInstance]> {
|
||||
let foundRoute: RouteData | undefined;
|
||||
// options.manifest is the actual type that contains the information
|
||||
for (const route of this.options.manifest.routes) {
|
||||
let finalUrl: URL | undefined = undefined;
|
||||
_sourceRoute: RouteData
|
||||
): Promise<[RouteData, ComponentInstance, URL]> {
|
||||
const [foundRoute, finalUrl] = findRouteToRewrite({
|
||||
payload,
|
||||
request,
|
||||
routes: this.options.manifest.routes,
|
||||
trailingSlash: this.config.trailingSlash,
|
||||
buildFormat: this.config.build.format,
|
||||
base: this.config.base,
|
||||
});
|
||||
|
||||
if (payload instanceof URL) {
|
||||
finalUrl = payload;
|
||||
} else if (payload instanceof Request) {
|
||||
finalUrl = new URL(payload.url);
|
||||
} else {
|
||||
finalUrl = new URL(payload, new URL(request.url).origin);
|
||||
}
|
||||
|
||||
if (route.pattern.test(decodeURI(finalUrl.pathname))) {
|
||||
foundRoute = route;
|
||||
break;
|
||||
} else if (finalUrl.pathname === '/404') {
|
||||
foundRoute = DEFAULT_404_ROUTE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundRoute) {
|
||||
if (foundRoute.pathname === '/404') {
|
||||
const componentInstance = await this.rewriteKnownRoute(foundRoute.pathname, sourceRoute);
|
||||
return [foundRoute, componentInstance];
|
||||
} else {
|
||||
const componentInstance = await this.getComponentByRoute(foundRoute);
|
||||
return [foundRoute, componentInstance];
|
||||
}
|
||||
} else {
|
||||
throw new AstroError({
|
||||
...RewriteEncounteredAnError,
|
||||
message: RewriteEncounteredAnError.message(payload.toString()),
|
||||
});
|
||||
}
|
||||
const componentInstance = await this.getComponentByRoute(foundRoute);
|
||||
return [foundRoute, componentInstance, finalUrl];
|
||||
}
|
||||
|
||||
async retrieveSsrEntry(route: RouteData, filePath: string): Promise<SinglePageBuiltModule> {
|
||||
|
@ -376,13 +354,6 @@ export class BuildPipeline extends Pipeline {
|
|||
|
||||
return RedirectSinglePageBuiltModule;
|
||||
}
|
||||
|
||||
rewriteKnownRoute(_pathname: string, sourceRoute: RouteData): ComponentInstance {
|
||||
if (!isServerLikeOutput(this.config) || sourceRoute.prerender) {
|
||||
throw new AstroError(InvalidRewrite404);
|
||||
}
|
||||
throw new Error(`Unreachable, in SSG this route shouldn't be generated`);
|
||||
}
|
||||
}
|
||||
|
||||
function createEntryURL(filePath: string, outFolder: URL) {
|
||||
|
|
|
@ -32,7 +32,7 @@ export function vitePluginAnalyzer(
|
|||
): VitePlugin {
|
||||
function hoistedScriptScanner() {
|
||||
const uniqueHoistedIds = new Map<string, string>();
|
||||
const pageScripts = new Map<
|
||||
const pageScriptsMap = new Map<
|
||||
string,
|
||||
{
|
||||
hoistedSet: Set<string>;
|
||||
|
@ -54,20 +54,22 @@ export function vitePluginAnalyzer(
|
|||
if (hoistedScripts.size) {
|
||||
for (const parentInfo of getParentModuleInfos(from, this, isPropagatedAsset)) {
|
||||
if (isPropagatedAsset(parentInfo.id)) {
|
||||
if (!internals.propagatedScriptsMap.has(parentInfo.id)) {
|
||||
internals.propagatedScriptsMap.set(parentInfo.id, new Set());
|
||||
}
|
||||
const propagatedScripts = internals.propagatedScriptsMap.get(parentInfo.id)!;
|
||||
for (const hid of hoistedScripts) {
|
||||
if (!internals.propagatedScriptsMap.has(parentInfo.id)) {
|
||||
internals.propagatedScriptsMap.set(parentInfo.id, new Set());
|
||||
}
|
||||
internals.propagatedScriptsMap.get(parentInfo.id)?.add(hid);
|
||||
propagatedScripts.add(hid);
|
||||
}
|
||||
} else if (moduleIsTopLevelPage(parentInfo)) {
|
||||
if (!pageScriptsMap.has(parentInfo.id)) {
|
||||
pageScriptsMap.set(parentInfo.id, {
|
||||
hoistedSet: new Set(),
|
||||
});
|
||||
}
|
||||
const pageScripts = pageScriptsMap.get(parentInfo.id)!;
|
||||
for (const hid of hoistedScripts) {
|
||||
if (!pageScripts.has(parentInfo.id)) {
|
||||
pageScripts.set(parentInfo.id, {
|
||||
hoistedSet: new Set(),
|
||||
});
|
||||
}
|
||||
pageScripts.get(parentInfo.id)?.hoistedSet.add(hid);
|
||||
pageScripts.hoistedSet.add(hid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83,7 +85,7 @@ export function vitePluginAnalyzer(
|
|||
}
|
||||
}
|
||||
|
||||
for (const [pageId, { hoistedSet }] of pageScripts) {
|
||||
for (const [pageId, { hoistedSet }] of pageScriptsMap) {
|
||||
const pageData = getPageDataByViteID(internals, pageId);
|
||||
if (!pageData) continue;
|
||||
|
||||
|
|
|
@ -12,6 +12,15 @@ export function vitePluginChunks(): VitePlugin {
|
|||
if (id.includes('astro/dist/runtime/server/')) {
|
||||
return 'astro/server';
|
||||
}
|
||||
// Split the Astro runtime into a separate chunk for readability
|
||||
if (id.includes('astro/dist/runtime')) {
|
||||
return 'astro';
|
||||
}
|
||||
// Place `astro/env/setup` import in its own chunk to prevent Rollup's TLA bug
|
||||
// https://github.com/rollup/rollup/issues/4708
|
||||
if (id.includes('astro/dist/env/setup')) {
|
||||
return 'astro/env-setup';
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,87 +1,105 @@
|
|||
import path from 'node:path';
|
||||
import type { Plugin as VitePlugin } from 'vite';
|
||||
import type { Rollup, Plugin as VitePlugin } from 'vite';
|
||||
import { getPrerenderMetadata } from '../../../prerender/metadata.js';
|
||||
import type { BuildInternals } from '../internal.js';
|
||||
import type { AstroBuildPlugin } from '../plugin.js';
|
||||
import type { StaticBuildOptions } from '../types.js';
|
||||
import { extendManualChunks } from './util.js';
|
||||
import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugin-pages.js';
|
||||
import { getPagesFromVirtualModulePageName } from './util.js';
|
||||
|
||||
function vitePluginPrerender(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
|
||||
function vitePluginPrerender(internals: BuildInternals): VitePlugin {
|
||||
return {
|
||||
name: 'astro:rollup-plugin-prerender',
|
||||
|
||||
outputOptions(outputOptions) {
|
||||
extendManualChunks(outputOptions, {
|
||||
after(id, meta) {
|
||||
// Split the Astro runtime into a separate chunk for readability
|
||||
if (id.includes('astro/dist/runtime')) {
|
||||
return 'astro';
|
||||
}
|
||||
const pageInfo = internals.pagesByViteID.get(id);
|
||||
let hasSharedModules = false;
|
||||
if (pageInfo) {
|
||||
// prerendered pages should be split into their own chunk
|
||||
// Important: this can't be in the `pages/` directory!
|
||||
if (getPrerenderMetadata(meta.getModuleInfo(id)!)) {
|
||||
const infoMeta = meta.getModuleInfo(id)!;
|
||||
generateBundle(_, bundle) {
|
||||
const moduleIds = this.getModuleIds();
|
||||
for (const id of moduleIds) {
|
||||
const pageInfo = internals.pagesByViteID.get(id);
|
||||
if (!pageInfo) continue;
|
||||
const moduleInfo = this.getModuleInfo(id);
|
||||
if (!moduleInfo) continue;
|
||||
|
||||
// Here, we check if this page is importing modules that are shared among other modules e.g. middleware, other SSR pages, etc.
|
||||
// we loop the modules that the current page imports
|
||||
for (const moduleId of infoMeta.importedIds) {
|
||||
// we retrieve the metadata of the module
|
||||
const moduleMeta = meta.getModuleInfo(moduleId)!;
|
||||
if (
|
||||
// a shared modules should be inside the `src/` folder, at least
|
||||
moduleMeta.id.startsWith(opts.settings.config.srcDir.pathname) &&
|
||||
// and has at least two importers: the current page and something else
|
||||
moduleMeta.importers.length > 1
|
||||
) {
|
||||
// Now, we have to trace back the modules imported and analyze them;
|
||||
// understanding if a module is eventually shared between two pages isn't easy, because a module could
|
||||
// be imported by a page and a component that is eventually imported by a page.
|
||||
//
|
||||
// Given the previous statement, we only check if
|
||||
// - the module is a page, and it's not pre-rendered
|
||||
// - the module is the middleware
|
||||
// If one of these conditions is met, we need a separate chunk
|
||||
for (const importer of moduleMeta.importedIds) {
|
||||
// we don't want to analyze the same module again, so we skip it
|
||||
if (importer !== id) {
|
||||
const importerModuleMeta = meta.getModuleInfo(importer);
|
||||
if (importerModuleMeta) {
|
||||
// if the module is inside the pages
|
||||
if (importerModuleMeta.id.includes('/pages')) {
|
||||
// we check if it's not pre-rendered
|
||||
if (getPrerenderMetadata(importerModuleMeta) === false) {
|
||||
hasSharedModules = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// module isn't an Astro route/page, it could be a middleware
|
||||
else if (importerModuleMeta.id.includes('/middleware')) {
|
||||
hasSharedModules = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const prerender = !!getPrerenderMetadata(moduleInfo);
|
||||
pageInfo.route.prerender = prerender;
|
||||
}
|
||||
|
||||
pageInfo.hasSharedModules = hasSharedModules;
|
||||
pageInfo.route.prerender = true;
|
||||
return 'prerender';
|
||||
}
|
||||
pageInfo.route.prerender = false;
|
||||
// dynamic pages should all go in their own chunk in the pages/* directory
|
||||
return `pages/${path.basename(pageInfo.component)}`;
|
||||
}
|
||||
},
|
||||
});
|
||||
// Find all chunks used in the SSR runtime (that aren't used for prerendering only), then use
|
||||
// the Set to find the inverse, where chunks that are only used for prerendering. It's faster
|
||||
// to compute `internals.prerenderOnlyChunks` this way. The prerendered chunks will be deleted
|
||||
// after we finish prerendering.
|
||||
const nonPrerenderOnlyChunks = getNonPrerenderOnlyChunks(bundle, internals);
|
||||
internals.prerenderOnlyChunks = Object.values(bundle).filter((chunk) => {
|
||||
return chunk.type === 'chunk' && !nonPrerenderOnlyChunks.has(chunk);
|
||||
}) as Rollup.OutputChunk[];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getNonPrerenderOnlyChunks(bundle: Rollup.OutputBundle, internals: BuildInternals) {
|
||||
const chunks = Object.values(bundle);
|
||||
|
||||
const prerenderOnlyEntryChunks = new Set<Rollup.OutputChunk>();
|
||||
const nonPrerenderOnlyEntryChunks = new Set<Rollup.OutputChunk>();
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.type === 'chunk' && (chunk.isEntry || chunk.isDynamicEntry)) {
|
||||
// See if this entry chunk is prerendered, if so, skip it
|
||||
if (chunk.facadeModuleId?.startsWith(ASTRO_PAGE_RESOLVED_MODULE_ID)) {
|
||||
const pageDatas = getPagesFromVirtualModulePageName(
|
||||
internals,
|
||||
ASTRO_PAGE_RESOLVED_MODULE_ID,
|
||||
chunk.facadeModuleId
|
||||
);
|
||||
const prerender = pageDatas.every((pageData) => pageData.route.prerender);
|
||||
if (prerender) {
|
||||
prerenderOnlyEntryChunks.add(chunk);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Ideally we should record entries when `functionPerRoute` is enabled, but this breaks some tests
|
||||
// that expect the entrypoint to still exist even if it should be unused.
|
||||
// TODO: Revisit this so we can delete additional unused chunks
|
||||
// else if (chunk.facadeModuleId?.startsWith(RESOLVED_SPLIT_MODULE_ID)) {
|
||||
// const pageDatas = getPagesFromVirtualModulePageName(
|
||||
// internals,
|
||||
// RESOLVED_SPLIT_MODULE_ID,
|
||||
// chunk.facadeModuleId
|
||||
// );
|
||||
// const prerender = pageDatas.every((pageData) => pageData.route.prerender);
|
||||
// if (prerender) {
|
||||
// prerenderOnlyEntryChunks.add(chunk);
|
||||
// continue;
|
||||
// }
|
||||
// }
|
||||
|
||||
nonPrerenderOnlyEntryChunks.add(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
// From the `nonPrerenderedEntryChunks`, we crawl all the imports/dynamicImports to find all
|
||||
// other chunks that are use by the non-prerendered runtime
|
||||
const nonPrerenderOnlyChunks = new Set(nonPrerenderOnlyEntryChunks);
|
||||
for (const chunk of nonPrerenderOnlyChunks) {
|
||||
for (const importFileName of chunk.imports) {
|
||||
const importChunk = bundle[importFileName];
|
||||
if (importChunk?.type === 'chunk') {
|
||||
nonPrerenderOnlyChunks.add(importChunk);
|
||||
}
|
||||
}
|
||||
for (const dynamicImportFileName of chunk.dynamicImports) {
|
||||
const dynamicImportChunk = bundle[dynamicImportFileName];
|
||||
// The main server entry (entry.mjs) may import a prerender-only entry chunk, we skip in this case
|
||||
// to prevent incorrectly marking it as non-prerendered.
|
||||
if (
|
||||
dynamicImportChunk?.type === 'chunk' &&
|
||||
!prerenderOnlyEntryChunks.has(dynamicImportChunk)
|
||||
) {
|
||||
nonPrerenderOnlyChunks.add(dynamicImportChunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nonPrerenderOnlyChunks;
|
||||
}
|
||||
|
||||
export function pluginPrerender(
|
||||
opts: StaticBuildOptions,
|
||||
internals: BuildInternals
|
||||
|
@ -96,7 +114,7 @@ export function pluginPrerender(
|
|||
hooks: {
|
||||
'build:before': () => {
|
||||
return {
|
||||
vitePlugin: vitePluginPrerender(opts, internals),
|
||||
vitePlugin: vitePluginPrerender(internals),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
@ -27,7 +27,17 @@ function vitePluginSSR(
|
|||
name: '@astrojs/vite-plugin-astro-ssr-server',
|
||||
enforce: 'post',
|
||||
options(opts) {
|
||||
return addRollupInput(opts, [SSR_VIRTUAL_MODULE_ID]);
|
||||
const inputs = new Set<string>();
|
||||
|
||||
for (const pageData of Object.values(options.allPages)) {
|
||||
if (routeIsRedirect(pageData.route)) {
|
||||
continue;
|
||||
}
|
||||
inputs.add(getVirtualModulePageName(ASTRO_PAGE_MODULE_ID, pageData.component));
|
||||
}
|
||||
|
||||
inputs.add(SSR_VIRTUAL_MODULE_ID);
|
||||
return addRollupInput(opts, Array.from(inputs));
|
||||
},
|
||||
resolveId(id) {
|
||||
if (id === SSR_VIRTUAL_MODULE_ID) {
|
||||
|
@ -72,7 +82,6 @@ function vitePluginSSR(
|
|||
contents.push(...ssrCode.contents);
|
||||
return [...imports, ...contents, ...exports].join('\n');
|
||||
}
|
||||
return void 0;
|
||||
},
|
||||
async generateBundle(_opts, bundle) {
|
||||
// Add assets from this SSR chunk as well.
|
||||
|
@ -141,23 +150,20 @@ function vitePluginSSRSplit(
|
|||
adapter: AstroAdapter,
|
||||
options: StaticBuildOptions
|
||||
): VitePlugin {
|
||||
const functionPerRouteEnabled = isFunctionPerRouteEnabled(options.settings.adapter);
|
||||
return {
|
||||
name: '@astrojs/vite-plugin-astro-ssr-split',
|
||||
enforce: 'post',
|
||||
options(opts) {
|
||||
if (functionPerRouteEnabled) {
|
||||
const inputs = new Set<string>();
|
||||
const inputs = new Set<string>();
|
||||
|
||||
for (const pageData of Object.values(options.allPages)) {
|
||||
if (routeIsRedirect(pageData.route)) {
|
||||
continue;
|
||||
}
|
||||
inputs.add(getVirtualModulePageName(SPLIT_MODULE_ID, pageData.component));
|
||||
for (const pageData of Object.values(options.allPages)) {
|
||||
if (routeIsRedirect(pageData.route)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return addRollupInput(opts, Array.from(inputs));
|
||||
inputs.add(getVirtualModulePageName(SPLIT_MODULE_ID, pageData.component));
|
||||
}
|
||||
|
||||
return addRollupInput(opts, Array.from(inputs));
|
||||
},
|
||||
resolveId(id) {
|
||||
if (id.startsWith(SPLIT_MODULE_ID)) {
|
||||
|
@ -185,7 +191,6 @@ function vitePluginSSRSplit(
|
|||
|
||||
return [...imports, ...contents, ...exports].join('\n');
|
||||
}
|
||||
return void 0;
|
||||
},
|
||||
async generateBundle(_opts, bundle) {
|
||||
// Add assets from this SSR chunk as well.
|
||||
|
|
|
@ -2,13 +2,16 @@ import fs from 'node:fs';
|
|||
import path, { extname } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { teardown } from '@astrojs/compiler';
|
||||
import * as eslexer from 'es-module-lexer';
|
||||
import glob from 'fast-glob';
|
||||
import { bgGreen, bgMagenta, black, green } from 'kleur/colors';
|
||||
import * as vite from 'vite';
|
||||
import type { RouteData } from '../../@types/astro.js';
|
||||
import { PROPAGATED_ASSET_FLAG } from '../../content/consts.js';
|
||||
import { hasAnyContentFlag } from '../../content/utils.js';
|
||||
import {
|
||||
getSymlinkedContentCollections,
|
||||
hasAnyContentFlag,
|
||||
reverseSymlink,
|
||||
} from '../../content/utils.js';
|
||||
import {
|
||||
type BuildInternals,
|
||||
createBuildInternals,
|
||||
|
@ -21,6 +24,7 @@ import { runHookBuildSetup } from '../../integrations/hooks.js';
|
|||
import { getOutputDirectory } from '../../prerender/utils.js';
|
||||
import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||
import type { Logger } from '../logger/core.js';
|
||||
import { routeIsRedirect } from '../redirects/index.js';
|
||||
import { getOutDirWithinCwd } from './common.js';
|
||||
import { CHUNKS_PATH } from './consts.js';
|
||||
|
@ -38,7 +42,7 @@ import type { StaticBuildOptions } from './types.js';
|
|||
import { encodeName, getTimeStat, viteBuildReturnToRollupOutputs } from './util.js';
|
||||
|
||||
export async function viteBuild(opts: StaticBuildOptions) {
|
||||
const { allPages, settings } = opts;
|
||||
const { allPages, settings, logger } = opts;
|
||||
// Make sure we have an adapter before building
|
||||
if (isModeServerWithNoAdapter(opts.settings)) {
|
||||
throw new AstroError(AstroErrorData.NoAdapterInstalled);
|
||||
|
@ -78,7 +82,7 @@ export async function viteBuild(opts: StaticBuildOptions) {
|
|||
// Build your project (SSR application code, assets, client JS, etc.)
|
||||
const ssrTime = performance.now();
|
||||
opts.logger.info('build', `Building ${settings.config.output} entrypoints...`);
|
||||
const ssrOutput = await ssrBuild(opts, internals, pageInput, container);
|
||||
const ssrOutput = await ssrBuild(opts, internals, pageInput, container, logger);
|
||||
opts.logger.info('build', green(`✓ Completed in ${getTimeStat(ssrTime, performance.now())}.`));
|
||||
|
||||
settings.timer.end('SSR build');
|
||||
|
@ -151,7 +155,7 @@ export async function staticBuild(
|
|||
case isServerLikeOutput(settings.config): {
|
||||
settings.timer.start('Server generate');
|
||||
await generatePages(opts, internals);
|
||||
await cleanStaticOutput(opts, internals, ssrOutputChunkNames);
|
||||
await cleanStaticOutput(opts, internals);
|
||||
opts.logger.info(null, `\n${bgMagenta(black(' finalizing server assets '))}\n`);
|
||||
await ssrMoveAssets(opts);
|
||||
settings.timer.end('Server generate');
|
||||
|
@ -166,7 +170,8 @@ async function ssrBuild(
|
|||
opts: StaticBuildOptions,
|
||||
internals: BuildInternals,
|
||||
input: Set<string>,
|
||||
container: AstroBuildPluginContainer
|
||||
container: AstroBuildPluginContainer,
|
||||
logger: Logger
|
||||
) {
|
||||
const buildID = Date.now().toString();
|
||||
const { allPages, settings, viteConfig } = opts;
|
||||
|
@ -175,7 +180,8 @@ async function ssrBuild(
|
|||
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 contentDir = new URL('./src/content', settings.config.root);
|
||||
const symlinks = await getSymlinkedContentCollections({ contentDir, logger, fs });
|
||||
const viteBuildConfig: vite.InlineConfig = {
|
||||
...viteConfig,
|
||||
mode: viteConfig.mode || 'production',
|
||||
|
@ -192,6 +198,8 @@ async function ssrBuild(
|
|||
copyPublicDir: !ssr,
|
||||
rollupOptions: {
|
||||
...viteConfig.build?.rollupOptions,
|
||||
// Setting as `exports-only` allows us to safely delete inputs that are only used during prerendering
|
||||
preserveEntrySignatures: 'exports-only',
|
||||
input: [],
|
||||
output: {
|
||||
hoistTransitiveImports: isContentCache,
|
||||
|
@ -251,7 +259,12 @@ async function ssrBuild(
|
|||
chunkInfo.facadeModuleId &&
|
||||
hasAnyContentFlag(chunkInfo.facadeModuleId)
|
||||
) {
|
||||
const [srcRelative, flag] = chunkInfo.facadeModuleId.split('/src/')[1].split('?');
|
||||
const moduleId = reverseSymlink({
|
||||
symlinks,
|
||||
entry: chunkInfo.facadeModuleId,
|
||||
contentDir,
|
||||
});
|
||||
const [srcRelative, flag] = moduleId.split('/src/')[1].split('?');
|
||||
if (flag === PROPAGATED_ASSET_FLAG) {
|
||||
return encodeName(`${removeFileExtension(srcRelative)}.entry.mjs`);
|
||||
}
|
||||
|
@ -369,65 +382,35 @@ 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.
|
||||
* If a component is shared between a statically rendered route and a SSR route, it will still be included in the SSR build.
|
||||
* Remove chunks that are used for prerendering only
|
||||
*/
|
||||
async function cleanStaticOutput(
|
||||
opts: StaticBuildOptions,
|
||||
internals: BuildInternals,
|
||||
ssrOutputChunkNames: string[]
|
||||
) {
|
||||
const prerenderedFiles = new Set();
|
||||
const onDemandsFiles = new Set();
|
||||
for (const pageData of internals.pagesByKeys.values()) {
|
||||
const { moduleSpecifier } = pageData;
|
||||
const bundleId =
|
||||
internals.pageToBundleMap.get(moduleSpecifier) ??
|
||||
internals.entrySpecifierToBundleMap.get(moduleSpecifier);
|
||||
if (pageData.route.prerender && !pageData.hasSharedModules && !onDemandsFiles.has(bundleId)) {
|
||||
prerenderedFiles.add(bundleId);
|
||||
} else {
|
||||
onDemandsFiles.add(bundleId);
|
||||
// Check if the component was not previously added to the static build by a statically rendered route
|
||||
if (prerenderedFiles.has(bundleId)) {
|
||||
prerenderedFiles.delete(bundleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
async function cleanStaticOutput(opts: StaticBuildOptions, internals: BuildInternals) {
|
||||
const ssr = isServerLikeOutput(opts.settings.config);
|
||||
const out = ssr
|
||||
? opts.settings.config.build.server
|
||||
: getOutDirWithinCwd(opts.settings.config.outDir);
|
||||
// The SSR output chunks for Astro are all .mjs files
|
||||
const files = ssrOutputChunkNames.filter((f) => f.endsWith('.mjs'));
|
||||
|
||||
if (files.length) {
|
||||
await eslexer.init;
|
||||
|
||||
// Cleanup prerendered chunks.
|
||||
// This has to happen AFTER the SSR build runs as a final step, because we need the code in order to generate the pages.
|
||||
// These chunks should only contain prerendering logic, so they are safe to modify.
|
||||
await Promise.all(
|
||||
files.map(async (filename) => {
|
||||
if (!prerenderedFiles.has(filename)) {
|
||||
return;
|
||||
await Promise.all(
|
||||
internals.prerenderOnlyChunks.map(async (chunk) => {
|
||||
const url = new URL(chunk.fileName, out);
|
||||
try {
|
||||
// Entry chunks may be referenced by non-deleted code, so we don't actually delete it
|
||||
// but only empty its content. These chunks should never be executed in practice, but
|
||||
// it should prevent broken import paths if adapters do a secondary bundle.
|
||||
if (chunk.isEntry || chunk.isDynamicEntry) {
|
||||
await fs.promises.writeFile(
|
||||
url,
|
||||
"// Contents removed by Astro as it's used for prerendering only",
|
||||
'utf-8'
|
||||
);
|
||||
} else {
|
||||
await fs.promises.unlink(url);
|
||||
}
|
||||
const url = new URL(filename, out);
|
||||
const text = await fs.promises.readFile(url, { encoding: 'utf8' });
|
||||
const [, exports] = eslexer.parse(text);
|
||||
// Replace exports (only prerendered pages) with a noop
|
||||
let value = 'const noop = () => {};';
|
||||
for (const e of exports) {
|
||||
if (e.n === 'default') value += `\n export default noop;`;
|
||||
else value += `\nexport const ${e.n} = noop;`;
|
||||
}
|
||||
await fs.promises.writeFile(url, value, { encoding: 'utf8' });
|
||||
})
|
||||
);
|
||||
|
||||
removeEmptyDirs(out);
|
||||
}
|
||||
} catch {
|
||||
// Best-effort only. Sometimes some chunks may be deleted by other plugins, like pure CSS chunks,
|
||||
// so they may already not exist.
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function cleanServerOutput(
|
||||
|
|
|
@ -29,7 +29,6 @@ export interface PageBuildData {
|
|||
moduleSpecifier: string;
|
||||
hoistedScript: HoistedScriptAsset | undefined;
|
||||
styles: Array<{ depth: number; order: number; sheet: StylesheetAsset }>;
|
||||
hasSharedModules: boolean;
|
||||
}
|
||||
|
||||
export type AllPagesData = Record<ComponentPath, PageBuildData>;
|
||||
|
|
|
@ -32,6 +32,11 @@ export const ROUTE_TYPE_HEADER = 'X-Astro-Route-Type';
|
|||
*/
|
||||
export const DEFAULT_404_COMPONENT = 'astro-default-404.astro';
|
||||
|
||||
/**
|
||||
* The value of the `component` field of the default 500 page, which is used when there is no user-provided 404.astro page.
|
||||
*/
|
||||
export const DEFAULT_500_COMPONENT = 'astro-default-500.astro';
|
||||
|
||||
/**
|
||||
* A response with one of these status codes will be rewritten
|
||||
* with the result of rendering the respective error page.
|
||||
|
|
|
@ -191,6 +191,19 @@ class AstroCookies implements AstroCookiesInterface {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges a new AstroCookies instance into the current instance. Any new cookies
|
||||
* will be added to the current instance, overwriting any existing cookies with the same name.
|
||||
*/
|
||||
merge(cookies: AstroCookies) {
|
||||
const outgoing = cookies.#outgoing;
|
||||
if (outgoing) {
|
||||
for (const [key, value] of outgoing) {
|
||||
this.#ensureOutgoingMap().set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Astro.cookies.header() returns an iterator for the cookies that have previously
|
||||
* been set by either Astro.cookies.set() or Astro.cookies.delete().
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue