0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-17 23:11:29 -05:00

Merge branch 'next' into astro-dot-session

This commit is contained in:
Matt Kane 2024-11-22 09:23:36 +00:00
commit cc28da35df
149 changed files with 1537 additions and 472 deletions

View file

@ -0,0 +1,50 @@
---
'astro': minor
---
Adds a new `astro:routes:resolved` hook to the Integration API. Also update the `astro:build:done` hook by deprecating `routes` and adding a new `assets` map.
When building an integration, you can now get access to routes inside the `astro:routes:resolved` hook:
```js
const integration = () => {
return {
name: 'my-integration',
hooks: {
'astro:routes:resolved': ({ routes }) => {
console.log(routes)
}
}
}
}
```
This hook runs before `astro:config:done`, and whenever a route changes in development.
The `routes` array from `astro:build:done` is now deprecated, and exposed properties are now available on `astro:routes:resolved`, except for `distURL`. For this, you can use the newly exposed `assets` map:
```diff
const integration = () => {
+ let routes
return {
name: 'my-integration',
hooks: {
+ 'astro:routes:resolved': (params) => {
+ routes = params.routes
+ },
'astro:build:done': ({
- routes
+ assets
}) => {
+ for (const route of routes) {
+ const distURL = assets.get(route.pattern)
+ if (distURL) {
+ Object.assign(route, { distURL })
+ }
+ }
console.log(routes)
}
}
}
}
```

View file

@ -34,12 +34,15 @@
"afraid-apricots-buy",
"blue-boats-relax",
"blue-sloths-stare",
"blue-socks-doubt",
"brave-elephants-fly",
"breezy-colts-promise",
"brown-bulldogs-share",
"chatty-teachers-sit",
"chilly-terms-know",
"clean-camels-drive",
"clean-donuts-walk",
"clean-moles-rest",
"cold-bananas-hear",
"cool-mangos-shop",
"cuddly-shoes-press",
@ -61,6 +64,7 @@
"funny-wolves-dream",
"fuzzy-pugs-live",
"gentle-scissors-bow",
"giant-ravens-look",
"giant-rocks-thank",
"gorgeous-foxes-divide",
"healthy-ads-scream",
@ -95,8 +99,10 @@
"poor-seals-clap",
"pretty-walls-camp",
"proud-games-repair",
"proud-terms-swim",
"quick-ads-exercise",
"quick-onions-leave",
"rotten-dodos-judge",
"rotten-phones-scream",
"selfish-cats-crash",
"selfish-impalas-grin",
@ -116,6 +122,7 @@
"tame-rats-cross",
"ten-students-repair",
"ten-walls-tap",
"thirty-clocks-jump",
"three-days-cough",
"three-olives-reflect",
"tough-planets-dress",

View file

@ -1,5 +1,90 @@
---
'astro': patch
'astro': minor
---
Adds experimental reponsive image support
Adds experimental support for automatic responsive images
This feature is experimental and may change in future versions. To enable it, set `experimental.responsiveImages` to `true` in your `astro.config.mjs` file.
```js title=astro.config.mjs
{
experimental: {
responsiveImages: true,
},
}
```
When this flag is enabled, you can pass a `layout` prop to any `<Image />` or `<Picture />` component to create a responsive image. When a layout is set, images have automatically generated `srcset` and `sizes` attributes based on the image's dimensions and the layout type. Images with `responsive` and `full-width` layouts will have styles applied to ensure they resize according to their container.
```astro
---
import { Image, Picture } from 'astro:assets';
import myImage from '../assets/my_image.png';
---
<Image src={myImage} alt="A description of my image." layout='responsive' width={800} height={600} />
<Picture src={myImage} alt="A description of my image." layout='full-width' formats={['avif', 'webp', 'jpeg']} />
```
This `<Image />` component will generate the following HTML output:
```html title=Output
<img
src="/_astro/my_image.hash3.webp"
srcset="/_astro/my_image.hash1.webp 640w,
/_astro/my_image.hash2.webp 750w,
/_astro/my_image.hash3.webp 800w,
/_astro/my_image.hash4.webp 828w,
/_astro/my_image.hash5.webp 1080w,
/_astro/my_image.hash6.webp 1280w,
/_astro/my_image.hash7.webp 1600w"
alt="A description of my image"
sizes="(min-width: 800px) 800px, 100vw"
loading="lazy"
decoding="async"
fetchpriority="auto"
width="800"
height="600"
style="--w: 800; --h: 600; --fit: cover; --pos: center;"
data-astro-image="responsive"
>
```
#### Responsive image properties
These are additional properties available to the `<Image />` and `<Picture />` components when responsive images are enabled:
- `layout`: The layout type for the image. Can be `responsive`, `fixed`, `full-width` or `none`. Defaults to value of `image.experimentalLayout`.
- `fit`: Defines how the image should be cropped if the aspect ratio is changed. Values match those of CSS `object-fit`. Defaults to `cover`, or the value of `image.experimentalObjectFit` if set.
- `position`: Defines the position of the image crop if the aspect ratio is changed. Values match those of CSS `object-position`. Defaults to `center`, or the value of `image.experimentalObjectPosition` if set.
- `priority`: If set, eagerly loads the image. Otherwise images will be lazy-loaded. Use this for your largest above-the-fold image. Defaults to `false`.
#### Default responsive image settings
You can enable responsive images for all `<Image />` and `<Picture />` components by setting `image.experimentalLayout` with a default value. This can be overridden by the `layout` prop on each component.
**Example:**
```js title=astro.config.mjs
{
image: {
// Used for all `<Image />` and `<Picture />` components unless overridden
experimentalLayout: 'responsive',
},
experimental: {
responsiveImages: true,
},
}
```
```astro
---
import { Image } from 'astro:assets';
import myImage from '../assets/my_image.png';
---
<Image src={myImage} alt="This will use responsive layout" width={800} height={600} />
<Image src={myImage} alt="This will use full-width layout" layout="full-width" />
<Image src={myImage} alt="This will disable responsive images" layout="none" />
```
For a complete overview, and to give feedback on this experimental API, see the [Responsive Images RFC](https://github.com/withastro/roadmap/blob/responsive-images/proposals/0053-responsive-images.md).

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Call server island early so it can set headers

View file

@ -0,0 +1,7 @@
---
'astro': minor
---
Changes the default content config location from `src/content/config.*` to `src/content.config.*`.
The previous location is still supported, and is required if the `legacy.collections` flag is enabled.

View file

@ -1,6 +1,6 @@
import { globby as glob } from 'globby';
import { fileURLToPath } from 'node:url';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { globby as glob } from 'globby';
import { setOutput } from './utils.mjs';
const { GITHUB_REF = 'main' } = process.env;
@ -18,34 +18,34 @@ const descriptors = [
'updates',
];
const verbs = [
"just went out!",
"just launched!",
"now available!",
"in the wild!",
"now live!",
"hit the registry!",
"to share!",
"for you!",
"for yall! 🤠",
"comin your way!",
"comin atcha!",
"comin in hot!",
"freshly minted on the blockchain! (jk)",
"[is] out (now with 100% more reticulated splines!)",
"(as seen on TV!)",
"just dropped!",
" artisanally hand-crafted just for you.",
" oh happy day!",
" enjoy!",
"now out. Be the first on your block to download!",
"made with love 💕",
"[is] out! Our best [version] yet!",
"[is] here. DOWNLOAD! DOWNLOAD! DOWNLOAD!",
"... HUZZAH!",
"[has] landed!",
"landed! The internet just got a little more fun.",
" from our family to yours.",
" go forth and build!"
'just went out!',
'just launched!',
'now available!',
'in the wild!',
'now live!',
'hit the registry!',
'to share!',
'for you!',
'for yall! 🤠',
'comin your way!',
'comin atcha!',
'comin in hot!',
'freshly minted on the blockchain! (jk)',
'[is] out (now with 100% more reticulated splines!)',
'(as seen on TV!)',
'just dropped!',
' artisanally hand-crafted just for you.',
' oh happy day!',
' enjoy!',
'now out. Be the first on your block to download!',
'made with love 💕',
'[is] out! Our best [version] yet!',
'[is] here. DOWNLOAD! DOWNLOAD! DOWNLOAD!',
'... HUZZAH!',
'[has] landed!',
'landed! The internet just got a little more fun.',
' from our family to yours.',
' go forth and build!',
];
const extraVerbs = [
'new',
@ -72,7 +72,7 @@ const plurals = new Map([
function pluralize(text) {
return text.replace(/(\[([^\]]+)\])/gm, (_, _full, match) =>
plurals.has(match) ? plurals.get(match) : `${match}s`
plurals.has(match) ? plurals.get(match) : `${match}s`,
);
}
@ -91,7 +91,7 @@ async function generatePackageMap() {
const pkgFile = fileURLToPath(new URL(pkg, packageRoot));
const content = await readFile(pkgFile).then((res) => JSON.parse(res.toString()));
packageMap.set(content.name, `./packages/${pkg.replace('/package.json', '')}`);
})
}),
);
}
@ -110,7 +110,7 @@ async function generateMessage() {
version,
url: new URL(`${p}/CHANGELOG.md#${version.replace(/\./g, '')}`, baseUrl).toString(),
};
})
}),
);
const emoji = item(emojis);
@ -122,7 +122,7 @@ async function generateMessage() {
if (packages.length === 1) {
const { name, version, url } = packages[0];
message += `${emoji} \`${name}@${version}\` ${singularlize(
verb
verb,
)}\nRead the [release notes ](<${url}>)\n`;
} else {
message += `${emoji} Some ${descriptor} ${pluralize(verb)}\n\n`;

View file

@ -1,18 +1,18 @@
import { build } from 'esbuild';
import { existsSync } from 'node:fs';
import { build } from 'esbuild';
const CLIENT_RUNTIME_PATH = 'packages/astro/src/runtime/client/';
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 B';
if (bytes === 0) return '0 B';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
export default async function checkBundleSize({ github, context }) {
@ -24,7 +24,7 @@ export default async function checkBundleSize({ github, context }) {
pull_number: PR_NUM,
});
const clientRuntimeFiles = files.filter((file) => {
return file.filename.startsWith(CLIENT_RUNTIME_PATH) && file.status !== 'removed'
return file.filename.startsWith(CLIENT_RUNTIME_PATH) && file.status !== 'removed';
});
if (clientRuntimeFiles.length === 0) return;
@ -35,17 +35,24 @@ export default async function checkBundleSize({ github, context }) {
const output = await bundle(clientRuntimeFiles);
for (let [filename, { oldSize, newSize, sourceFile }] of Object.entries(output)) {
filename = ['idle', 'load', 'media', 'only', 'visible'].includes(filename) ? `client:${filename}` : filename;
const prefix = (newSize - oldSize) === 0 ? '' : (newSize - oldSize) > 0 ? '+ ' : '- ';
filename = ['idle', 'load', 'media', 'only', 'visible'].includes(filename)
? `client:${filename}`
: filename;
const prefix = newSize - oldSize === 0 ? '' : newSize - oldSize > 0 ? '+ ' : '- ';
const change = `${prefix}${formatBytes(newSize - oldSize)}`;
table.push(`| [\`${filename}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/tree/${context.payload.pull_request.head.ref}/${sourceFile}) | ${formatBytes(oldSize)} | ${formatBytes(newSize)} | ${change} |`);
table.push(
`| [\`${filename}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/tree/${context.payload.pull_request.head.ref}/${sourceFile}) | ${formatBytes(oldSize)} | ${formatBytes(newSize)} | ${change} |`,
);
}
const { data: comments } = await github.rest.issues.listComments({
...context.repo,
issue_number: PR_NUM
})
const comment = comments.find(comment => comment.user.login === 'github-actions[bot]' && comment.body.includes('Bundle Size Check'));
issue_number: PR_NUM,
});
const comment = comments.find(
(comment) =>
comment.user.login === 'github-actions[bot]' && comment.body.includes('Bundle Size Check'),
);
const method = comment ? 'updateComment' : 'createComment';
const payload = comment ? { comment_id: comment.id } : { issue_number: PR_NUM };
await github.rest.issues[method]({
@ -60,9 +67,11 @@ ${table.join('\n')}`,
}
async function bundle(files) {
const { metafile } = await build({
entryPoints: [...files.map(({ filename }) => filename), ...files.map(({ filename }) => `main/${filename}`).filter(f => existsSync(f))],
entryPoints: [
...files.map(({ filename }) => filename),
...files.map(({ filename }) => `main/${filename}`).filter((f) => existsSync(f)),
],
bundle: true,
minify: true,
sourcemap: false,
@ -70,17 +79,24 @@ async function bundle(files) {
outdir: 'out',
external: ['astro:*', 'aria-query', 'axobject-query'],
metafile: true,
})
});
return Object.entries(metafile.outputs).reduce((acc, [filename, info]) => {
filename = filename.slice('out/'.length);
if (filename.startsWith('main/')) {
filename = filename.slice('main/'.length).replace(CLIENT_RUNTIME_PATH, '').replace('.js', '');
const oldSize = info.bytes;
return Object.assign(acc, { [filename]: Object.assign(acc[filename] ?? { oldSize: 0, newSize: 0 }, { oldSize }) });
return Object.assign(acc, {
[filename]: Object.assign(acc[filename] ?? { oldSize: 0, newSize: 0 }, { oldSize }),
});
}
filename = filename.replace(CLIENT_RUNTIME_PATH, '').replace('.js', '');
const newSize = info.bytes;
return Object.assign(acc, { [filename]: Object.assign(acc[filename] ?? { oldSize: 0, newSize: 0 }, { newSize, sourceFile: Object.keys(info.inputs).find(src => src.endsWith('.ts')) }) });
return Object.assign(acc, {
[filename]: Object.assign(acc[filename] ?? { oldSize: 0, newSize: 0 }, {
newSize,
sourceFile: Object.keys(info.inputs).find((src) => src.endsWith('.ts')),
}),
});
}, {});
}

View file

@ -1,59 +1,53 @@
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as crypto from 'node:crypto'
import * as crypto from 'node:crypto';
import * as fs from 'node:fs';
import * as os from 'node:os';
/** Based on https://github.com/actions/toolkit/blob/4e3b068ce116d28cb840033c02f912100b4592b0/packages/core/src/file-command.ts */
export function setOutput(key, value) {
const filePath = process.env['GITHUB_OUTPUT'] || ''
if (filePath) {
return issueFileCommand('OUTPUT', prepareKeyValueMessage(key, value))
}
process.stdout.write(os.EOL)
const filePath = process.env['GITHUB_OUTPUT'] || '';
if (filePath) {
return issueFileCommand('OUTPUT', prepareKeyValueMessage(key, value));
}
process.stdout.write(os.EOL);
}
function issueFileCommand(command, message) {
const filePath = process.env[`GITHUB_${command}`]
if (!filePath) {
throw new Error(
`Unable to find environment variable for file command ${command}`
)
}
if (!fs.existsSync(filePath)) {
throw new Error(`Missing file at path: ${filePath}`)
}
const filePath = process.env[`GITHUB_${command}`];
if (!filePath) {
throw new Error(`Unable to find environment variable for file command ${command}`);
}
if (!fs.existsSync(filePath)) {
throw new Error(`Missing file at path: ${filePath}`);
}
fs.appendFileSync(filePath, `${toCommandValue(message)}${os.EOL}`, {
encoding: 'utf8'
})
fs.appendFileSync(filePath, `${toCommandValue(message)}${os.EOL}`, {
encoding: 'utf8',
});
}
function prepareKeyValueMessage(key, value) {
const delimiter = `gh-delimiter-${crypto.randomUUID()}`
const convertedValue = toCommandValue(value)
const delimiter = `gh-delimiter-${crypto.randomUUID()}`;
const convertedValue = toCommandValue(value);
// These should realistically never happen, but just in case someone finds a
// way to exploit uuid generation let's not allow keys or values that contain
// the delimiter.
if (key.includes(delimiter)) {
throw new Error(
`Unexpected input: name should not contain the delimiter "${delimiter}"`
)
}
// These should realistically never happen, but just in case someone finds a
// way to exploit uuid generation let's not allow keys or values that contain
// the delimiter.
if (key.includes(delimiter)) {
throw new Error(`Unexpected input: name should not contain the delimiter "${delimiter}"`);
}
if (convertedValue.includes(delimiter)) {
throw new Error(
`Unexpected input: value should not contain the delimiter "${delimiter}"`
)
}
if (convertedValue.includes(delimiter)) {
throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`);
}
return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`
return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`;
}
function toCommandValue(input) {
if (input === null || input === undefined) {
return ''
} else if (typeof input === 'string' || input instanceof String) {
return input
}
return JSON.stringify(input)
if (input === null || input === undefined) {
return '';
} else if (typeof input === 'string' || input instanceof String) {
return input;
}
return JSON.stringify(input);
}

View file

@ -95,9 +95,12 @@ jobs:
- name: Build Packages
run: pnpm run build
- name: Lint
- name: Lint source code
run: pnpm run lint:ci
- name: Lint publish code
run: pnpm run publint
test:
name: "Test: ${{ matrix.os }} (node@${{ matrix.NODE_VERSION }})"
runs-on: ${{ matrix.os }}

View file

@ -5,20 +5,24 @@ on:
pull_request:
branches:
- main
paths:
- 'packages/astro/src/**/*.ts'
- 'benchmark/**'
push:
branches:
- main
paths:
- 'packages/astro/src/**/*.ts'
- 'benchmark/**'
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
FORCE_COLOR: true
CODSPEED_TOKEN: ${{ secrets.CODSPEED_TOKEN }}
CODSPEED: true
jobs:
codspeed:
if: ${{ github.repository_owner == 'withastro' }}
runs-on: ubuntu-latest
permissions:
contents: read
@ -47,4 +51,5 @@ jobs:
timeout-minutes: 30
with:
run: pnpm benchmark codspeed
token: ${{ secrets.CODSPEED_TOKEN }}

View file

@ -16,8 +16,8 @@ Anything enforced by linting and formatting is considered a **style rule.** It i
These style rules are maintained in configuration files, and therefore not documented in this document. Read any of the following configuration files to learn more about the style rules that we strictly enforced across the codebase:
- [ESLint](https://github.com/withastro/astro/blob/main/.eslintrc.cjs) (Linting)
- [Prettier](https://github.com/withastro/astro/blob/main/.prettierrc.json) (Formatting)
- [ESLint](https://github.com/withastro/astro/blob/main/eslint.config.js) (Linting)
- [Prettier](https://github.com/withastro/astro/blob/main/prettier.config.js) (Formatting)
Alternatively, don't worry too much about style rules and trust that our tools will catch these issues for you and offer inline suggestions as you work.

View file

@ -1,19 +1,13 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.3/schema.json",
"files": {
"ignore": [
"vendor",
"**/dist/**",
"**/smoke/**",
"**/fixtures/**",
"**/_temp-fixtures/**",
"**/vendor/**",
"**/.vercel/**",
"benchmark/projects/",
"benchmark/results/",
"benchmark/bench/_template.js",
],
"include": ["test/**", "e2e/**", "packages/**", "/scripts/**", "benchmark/bench"],
"ignore": ["**/smoke/**", "**/fixtures/**", "**/_temp-fixtures/**", "**/vendor/**"],
"include": ["test/**", "e2e/**", "packages/**", "scripts/**", "benchmark/bench"],
},
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true,
},
"formatter": {
"indentStyle": "tab",

View file

@ -10,6 +10,6 @@
"astro": "astro"
},
"dependencies": {
"astro": "^5.0.0-beta.8"
"astro": "^5.0.0-beta.10"
}
}

View file

@ -13,6 +13,6 @@
"@astrojs/mdx": "^4.0.0-beta.3",
"@astrojs/rss": "^4.0.9",
"@astrojs/sitemap": "^3.2.1",
"astro": "^5.0.0-beta.8"
"astro": "^5.0.0-beta.10"
}
}

View file

@ -15,7 +15,7 @@
],
"scripts": {},
"devDependencies": {
"astro": "^5.0.0-beta.8"
"astro": "^5.0.0-beta.10"
},
"peerDependencies": {
"astro": "^4.0.0 || ^5.0.0"

View file

@ -11,8 +11,8 @@
"test": "vitest run"
},
"dependencies": {
"astro": "^5.0.0-beta.8",
"@astrojs/react": "^3.6.2",
"astro": "^5.0.0-beta.10",
"@astrojs/react": "^3.6.3-beta.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"vitest": "^2.1.4"

View file

@ -13,6 +13,6 @@
"@astrojs/alpinejs": "^0.4.0",
"@types/alpinejs": "^3.13.10",
"alpinejs": "^3.14.3",
"astro": "^5.0.0-beta.8"
"astro": "^5.0.0-beta.10"
}
}

View file

@ -10,14 +10,14 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/preact": "^3.5.3",
"@astrojs/react": "^3.6.2",
"@astrojs/solid-js": "^4.4.3",
"@astrojs/svelte": "^6.0.0-beta.2",
"@astrojs/vue": "^5.0.0-beta.1",
"@astrojs/preact": "^3.5.4-beta.0",
"@astrojs/react": "^3.6.3-beta.0",
"@astrojs/solid-js": "^4.4.4-beta.0",
"@astrojs/svelte": "^6.0.2-beta.0",
"@astrojs/vue": "^5.0.0-beta.2",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"astro": "^5.0.0-beta.8",
"astro": "^5.0.0-beta.10",
"preact": "^10.24.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",

View file

@ -10,9 +10,9 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/preact": "^3.5.3",
"@astrojs/preact": "^3.5.4-beta.0",
"@preact/signals": "^1.3.0",
"astro": "^5.0.0-beta.8",
"astro": "^5.0.0-beta.10",
"preact": "^10.24.3"
}
}

View file

@ -10,10 +10,10 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/react": "^3.6.2",
"@astrojs/react": "^3.6.3-beta.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"astro": "^5.0.0-beta.8",
"astro": "^5.0.0-beta.10",
"react": "^18.3.1",
"react-dom": "^18.3.1"
}

View file

@ -10,8 +10,8 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/solid-js": "^4.4.3",
"astro": "^5.0.0-beta.8",
"solid-js": "^1.9.2"
"@astrojs/solid-js": "^4.4.4-beta.0",
"astro": "^5.0.0-beta.10",
"solid-js": "^1.9.3"
}
}

View file

@ -10,8 +10,8 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/svelte": "^6.0.0",
"astro": "^5.0.0-beta.8",
"@astrojs/svelte": "^6.0.2-beta.0",
"astro": "^5.0.0-beta.10",
"svelte": "^5.1.16"
}
}

View file

@ -10,8 +10,8 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/vue": "^5.0.0-beta.1",
"astro": "^5.0.0-beta.8",
"@astrojs/vue": "^5.0.0-beta.2",
"astro": "^5.0.0-beta.10",
"vue": "^3.5.12"
}
}

View file

@ -11,6 +11,6 @@
},
"dependencies": {
"@astrojs/node": "^9.0.0-alpha.1",
"astro": "^5.0.0-beta.8"
"astro": "^5.0.0-beta.10"
}
}

View file

@ -15,7 +15,7 @@
],
"scripts": {},
"devDependencies": {
"astro": "^5.0.0-beta.8"
"astro": "^5.0.0-beta.10"
},
"peerDependencies": {
"astro": "^4.0.0"

View file

@ -10,6 +10,6 @@
"astro": "astro"
},
"dependencies": {
"astro": "^5.0.0-beta.8"
"astro": "^5.0.0-beta.10"
}
}

View file

@ -10,6 +10,6 @@
"astro": "astro"
},
"dependencies": {
"astro": "^5.0.0-beta.8"
"astro": "^5.0.0-beta.10"
}
}

View file

@ -12,8 +12,8 @@
},
"dependencies": {
"@astrojs/node": "^9.0.0-alpha.1",
"@astrojs/svelte": "^6.0.0",
"astro": "^5.0.0-beta.8",
"@astrojs/svelte": "^6.0.2-beta.0",
"astro": "^5.0.0-beta.10",
"svelte": "^5.1.16"
}
}

View file

@ -9,7 +9,7 @@
"astro": "astro"
},
"dependencies": {
"astro": "^5.0.0-beta.8",
"astro": "^5.0.0-beta.10",
"sass": "^1.80.6",
"sharp": "^0.33.3"
}

View file

@ -15,6 +15,6 @@
"./app": "./dist/app.js"
},
"devDependencies": {
"astro": "^5.0.0-beta.8"
"astro": "^5.0.0-beta.10"
}
}

View file

@ -11,6 +11,6 @@
},
"dependencies": {
"@astrojs/markdoc": "^0.12.0-beta.0",
"astro": "^5.0.0-beta.8"
"astro": "^5.0.0-beta.10"
}
}

View file

@ -11,8 +11,8 @@
},
"dependencies": {
"@astrojs/mdx": "^4.0.0-beta.3",
"@astrojs/preact": "^3.5.3",
"astro": "^5.0.0-beta.8",
"@astrojs/preact": "^3.5.4-beta.0",
"astro": "^5.0.0-beta.10",
"preact": "^10.24.3"
}
}

View file

@ -10,9 +10,9 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/preact": "^3.5.3",
"@astrojs/preact": "^3.5.4-beta.0",
"@nanostores/preact": "^0.5.2",
"astro": "^5.0.0-beta.8",
"astro": "^5.0.0-beta.10",
"nanostores": "^0.11.3",
"preact": "^10.24.3"
}

View file

@ -13,7 +13,7 @@
"@astrojs/mdx": "^4.0.0-beta.3",
"@astrojs/tailwind": "^5.1.2",
"@types/canvas-confetti": "^1.6.4",
"astro": "^5.0.0-beta.8",
"astro": "^5.0.0-beta.10",
"autoprefixer": "^10.4.20",
"canvas-confetti": "^1.9.3",
"postcss": "^8.4.47",

View file

@ -11,7 +11,7 @@
"test": "vitest"
},
"dependencies": {
"astro": "^5.0.0-beta.8",
"astro": "^5.0.0-beta.10",
"vitest": "^2.1.4"
}
}

View file

@ -5,7 +5,7 @@
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/withastro/astro.git"
"url": "git+https://github.com/withastro/astro.git"
},
"scripts": {
"release": "pnpm run build && changeset publish",
@ -37,6 +37,7 @@
"lint": "biome lint && eslint . --report-unused-disable-directives",
"lint:ci": "biome ci --formatter-enabled=false --organize-imports-enabled=false --reporter=github && eslint . --report-unused-disable-directives",
"lint:fix": "biome lint --write --unsafe",
"publint": "pnpm -r --filter=astro --filter=create-astro --filter=\"@astrojs/*\" --no-bail exec publint",
"version": "changeset version && node ./scripts/deps/update-example-versions.js && pnpm install --no-frozen-lockfile && pnpm run format",
"preinstall": "npx only-allow pnpm"
},
@ -65,6 +66,7 @@
"only-allow": "^1.2.1",
"prettier": "^3.3.3",
"prettier-plugin-astro": "^0.14.1",
"publint": "^0.2.12",
"turbo": "^2.2.3",
"typescript": "~5.6.3",
"typescript-eslint": "^8.13.0"

View file

@ -8,7 +8,7 @@
"bugs": "https://github.com/withastro/astro/issues",
"repository": {
"type": "git",
"url": "https://github.com/withastro/astro.git",
"url": "git+https://github.com/withastro/astro.git",
"directory": "packages/astro-prism"
},
"homepage": "https://docs.astro.build/en/reference/api-reference/#prism-",

View file

@ -8,7 +8,7 @@
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/withastro/astro.git",
"url": "git+https://github.com/withastro/astro.git",
"directory": "packages/astro-rss"
},
"bugs": "https://github.com/withastro/astro/issues",

View file

@ -1,5 +1,201 @@
# astro
## 5.0.0-beta.10
### Patch Changes
- [#12486](https://github.com/withastro/astro/pull/12486) [`dc3d842`](https://github.com/withastro/astro/commit/dc3d842e4c6f3b7e59da8a13447a1450013e10dc) Thanks [@matthewp](https://github.com/matthewp)! - Call server island early so it can set headers
## 5.0.0-beta.9
### Minor Changes
- [#12067](https://github.com/withastro/astro/pull/12067) [`c48916c`](https://github.com/withastro/astro/commit/c48916cc4e6f7c31e3563d04b68a8698d8775b65) Thanks [@stramel](https://github.com/stramel)! - Adds experimental support for built-in SVG components.
This feature allows you to import SVG files directly into your Astro project as components. By default, Astro will inline the SVG content into your HTML output.
To enable this feature, set `experimental.svg` to `true` in your Astro config:
```js
{
experimental: {
svg: true,
},
}
```
To use this feature, import an SVG file in your Astro project, passing any common SVG attributes to the imported component. Astro also provides a `size` attribute to set equal `height` and `width` properties:
```astro
---
import Logo from './path/to/svg/file.svg';
---
<Logo size={24} />
```
For a complete overview, and to give feedback on this experimental API, see the [Feature RFC](https://github.com/withastro/roadmap/pull/1035).
- [#12329](https://github.com/withastro/astro/pull/12329) [`8309c61`](https://github.com/withastro/astro/commit/8309c61f0dfa5991d3f6c5c5fca4403794d6fda2) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Adds a new `astro:routes:resolved` hook to the Integration API. Also update the `astro:build:done` hook by deprecating `routes` and adding a new `assets` map.
When building an integration, you can now get access to routes inside the `astro:routes:resolved` hook:
```js
const integration = () => {
return {
name: 'my-integration',
hooks: {
'astro:routes:resolved': ({ routes }) => {
console.log(routes);
},
},
};
};
```
This hook runs before `astro:config:done`, and whenever a route changes in development.
The `routes` array from `astro:build:done` is now deprecated, and exposed properties are now available on `astro:routes:resolved`, except for `distURL`. For this, you can use the newly exposed `assets` map:
```diff
const integration = () => {
+ let routes
return {
name: 'my-integration',
hooks: {
+ 'astro:routes:resolved': (params) => {
+ routes = params.routes
+ },
'astro:build:done': ({
- routes
+ assets
}) => {
+ for (const route of routes) {
+ const distURL = assets.get(route.pattern)
+ if (distURL) {
+ Object.assign(route, { distURL })
+ }
+ }
console.log(routes)
}
}
}
}
```
- [#12377](https://github.com/withastro/astro/pull/12377) [`af867f3`](https://github.com/withastro/astro/commit/af867f3910ecd8fc04a5337f591d84f03192e3fa) Thanks [@ascorbic](https://github.com/ascorbic)! - Adds experimental support for automatic responsive images
This feature is experimental and may change in future versions. To enable it, set `experimental.responsiveImages` to `true` in your `astro.config.mjs` file.
```js title=astro.config.mjs
{
experimental: {
responsiveImages: true,
},
}
```
When this flag is enabled, you can pass a `layout` prop to any `<Image />` or `<Picture />` component to create a responsive image. When a layout is set, images have automatically generated `srcset` and `sizes` attributes based on the image's dimensions and the layout type. Images with `responsive` and `full-width` layouts will have styles applied to ensure they resize according to their container.
```astro
---
import { Image, Picture } from 'astro:assets';
import myImage from '../assets/my_image.png';
---
<Image
src={myImage}
alt="A description of my image."
layout="responsive"
width={800}
height={600}
/>
<Picture
src={myImage}
alt="A description of my image."
layout="full-width"
formats={['avif', 'webp', 'jpeg']}
/>
```
This `<Image />` component will generate the following HTML output:
```html title=Output
<img
src="/_astro/my_image.hash3.webp"
srcset="
/_astro/my_image.hash1.webp 640w,
/_astro/my_image.hash2.webp 750w,
/_astro/my_image.hash3.webp 800w,
/_astro/my_image.hash4.webp 828w,
/_astro/my_image.hash5.webp 1080w,
/_astro/my_image.hash6.webp 1280w,
/_astro/my_image.hash7.webp 1600w
"
alt="A description of my image"
sizes="(min-width: 800px) 800px, 100vw"
loading="lazy"
decoding="async"
fetchpriority="auto"
width="800"
height="600"
style="--w: 800; --h: 600; --fit: cover; --pos: center;"
data-astro-image="responsive"
/>
```
#### Responsive image properties
These are additional properties available to the `<Image />` and `<Picture />` components when responsive images are enabled:
- `layout`: The layout type for the image. Can be `responsive`, `fixed`, `full-width` or `none`. Defaults to value of `image.experimentalLayout`.
- `fit`: Defines how the image should be cropped if the aspect ratio is changed. Values match those of CSS `object-fit`. Defaults to `cover`, or the value of `image.experimentalObjectFit` if set.
- `position`: Defines the position of the image crop if the aspect ratio is changed. Values match those of CSS `object-position`. Defaults to `center`, or the value of `image.experimentalObjectPosition` if set.
- `priority`: If set, eagerly loads the image. Otherwise images will be lazy-loaded. Use this for your largest above-the-fold image. Defaults to `false`.
#### Default responsive image settings
You can enable responsive images for all `<Image />` and `<Picture />` components by setting `image.experimentalLayout` with a default value. This can be overridden by the `layout` prop on each component.
**Example:**
```js title=astro.config.mjs
{
image: {
// Used for all `<Image />` and `<Picture />` components unless overridden
experimentalLayout: 'responsive',
},
experimental: {
responsiveImages: true,
},
}
```
```astro
---
import { Image } from 'astro:assets';
import myImage from '../assets/my_image.png';
---
<Image src={myImage} alt="This will use responsive layout" width={800} height={600} />
<Image src={myImage} alt="This will use full-width layout" layout="full-width" />
<Image src={myImage} alt="This will disable responsive images" layout="none" />
```
For a complete overview, and to give feedback on this experimental API, see the [Responsive Images RFC](https://github.com/withastro/roadmap/blob/responsive-images/proposals/0053-responsive-images.md).
- [#12475](https://github.com/withastro/astro/pull/12475) [`3f02d5f`](https://github.com/withastro/astro/commit/3f02d5f12b167514fff6eb9693b4e25c668e7a31) Thanks [@ascorbic](https://github.com/ascorbic)! - Changes the default content config location from `src/content/config.*` to `src/content.config.*`.
The previous location is still supported, and is required if the `legacy.collections` flag is enabled.
### Patch Changes
- [#12424](https://github.com/withastro/astro/pull/12424) [`4364bff`](https://github.com/withastro/astro/commit/4364bff27332e52f92da72392620a36110daee42) Thanks [@ematipico](https://github.com/ematipico)! - Fixes an issue where an incorrect usage of Astro actions was lost when porting the fix from v4 to v5
- [#12438](https://github.com/withastro/astro/pull/12438) [`c8f877c`](https://github.com/withastro/astro/commit/c8f877cad2d8f1780f70045413872d5b9d32ebed) Thanks [@ascorbic](https://github.com/ascorbic)! - Fixes a bug where legacy content types were generated for content layer collections if they were in the content directory
## 5.0.0-beta.8
### Minor Changes
@ -1195,6 +1391,22 @@
- Updated dependencies [[`83a2a64`](https://github.com/withastro/astro/commit/83a2a648418ad30f4eb781d1c1b5f2d8a8ac846e)]:
- @astrojs/markdown-remark@6.0.0-alpha.0
## 4.16.14
### Patch Changes
- [#12480](https://github.com/withastro/astro/pull/12480) [`c3b7e7c`](https://github.com/withastro/astro/commit/c3b7e7cfa13603c08eb923703f31a92d514e82db) Thanks [@matthewp](https://github.com/matthewp)! - Removes the default throw behavior in `astro:env`
- [#12444](https://github.com/withastro/astro/pull/12444) [`28dd3ce`](https://github.com/withastro/astro/commit/28dd3ce5222a667fe113238254edf59318b3fa14) Thanks [@ematipico](https://github.com/ematipico)! - Fixes an issue where a server island hydration script might fail case the island ID misses from the DOM.
- [#12476](https://github.com/withastro/astro/pull/12476) [`80a9a52`](https://github.com/withastro/astro/commit/80a9a5299a9d51f2b09900d3200976d687feae8f) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes a case where the Content Layer `glob()` loader would not update when renaming or deleting an entry
- [#12418](https://github.com/withastro/astro/pull/12418) [`25baa4e`](https://github.com/withastro/astro/commit/25baa4ed0c5f55fa85c2c7e2c15848937ed1dc9b) Thanks [@oliverlynch](https://github.com/oliverlynch)! - Fix cached image redownloading if it is the first asset
- [#12477](https://github.com/withastro/astro/pull/12477) [`46f6b38`](https://github.com/withastro/astro/commit/46f6b386b3db6332f286d79958ef10261958cceb) Thanks [@ematipico](https://github.com/ematipico)! - Fixes an issue where the SSR build was emitting the `dist/server/entry.mjs` file with an incorrect import at the top of the file/
- [#12365](https://github.com/withastro/astro/pull/12365) [`a23985b`](https://github.com/withastro/astro/commit/a23985b02165c2ddce56d511b3f97b6815c452c9) Thanks [@apatel369](https://github.com/apatel369)! - Fixes an issue where `Astro.currentLocale` was not correctly returning the locale for 404 and 500 pages.
## 4.16.13
### Patch Changes

View file

@ -111,7 +111,7 @@ declare module '*.svg' {
type Props = {
/**
* Accesible, short-text description
*
*
* {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title|MDN Reference}
*/
title?: string;
@ -122,9 +122,9 @@ declare module '*.svg' {
/**
* Override the default rendering mode for SVGs
*/
mode?: import('./dist/assets/utils/svg.js').SvgRenderMode
} & astroHTML.JSX.SVGAttributes
mode?: import('./dist/assets/utils/svg.js').SvgRenderMode;
} & astroHTML.JSX.SVGAttributes;
const Component: ((_props: Props) => any) & ImageMetadata;
export default Component;
}

View file

@ -1,13 +1,13 @@
{
"name": "astro",
"version": "5.0.0-beta.8",
"version": "5.0.0-beta.10",
"description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
"type": "module",
"author": "withastro",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/withastro/astro.git",
"url": "git+https://github.com/withastro/astro.git",
"directory": "packages/astro"
},
"bugs": "https://github.com/withastro/astro/issues",

View file

@ -18,10 +18,11 @@ export default function astroIntegrationActionsRouteHandler({
name: VIRTUAL_MODULE_ID,
hooks: {
async 'astro:config:setup'(params) {
params.injectRoute({
settings.injectedRoutes.push({
pattern: ACTION_RPC_ROUTE_PATTERN,
entrypoint: 'astro/actions/runtime/route.js',
prerender: false,
origin: 'internal',
});
params.addMiddleware({

View file

@ -153,6 +153,9 @@ export async function generateImagesForPath(
const isLocalImage = isESMImportedImage(options.src);
const finalFileURL = new URL('.' + filepath, env.clientRoot);
const finalFolderURL = new URL('./', finalFileURL);
await fs.promises.mkdir(finalFolderURL, { recursive: true });
// For remote images, instead of saving the image directly, we save a JSON file with the image data and expiration date from the server
const cacheFile = basename(filepath) + (isLocalImage ? '' : '.json');
const cachedFileURL = new URL(cacheFile, env.assetsCacheDir);
@ -194,9 +197,6 @@ export async function generateImagesForPath(
// If the cache file doesn't exist, just move on, and we'll generate it
}
const finalFolderURL = new URL('./', finalFileURL);
await fs.promises.mkdir(finalFolderURL, { recursive: true });
// The original filepath or URL from the image transform
const originalImagePath = isLocalImage
? (options.src as ImageMetadata).src

View file

@ -63,5 +63,6 @@ function getImageEndpointData(
pathname: settings.config.image.endpoint.route,
prerender: false,
fallbackRoutes: [],
origin: 'internal',
};
}

View file

@ -150,6 +150,7 @@ export async function getImage(
resolvedOptions.fetchpriority ??= 'auto';
}
delete resolvedOptions.priority;
delete resolvedOptions.densities;
}
const validatedOptions = service.validateOptions

View file

@ -13,4 +13,4 @@ export {
} from './remotePattern.js';
export { hashTransform, propsToFilename } from './transformToPath.js';
export { inferRemoteSize } from './remoteProbe.js';
export { makeSvgComponent } from './svg.js'
export { makeSvgComponent } from './svg.js';

View file

@ -1,7 +1,7 @@
import { parse, renderSync } from 'ultrahtml';
import type { ImageMetadata } from '../types.js';
import type { SvgComponentProps } from '../runtime.js';
import { dropAttributes } from '../runtime.js';
import type { ImageMetadata } from '../types.js';
function parseSvg(contents: string) {
const root = parse(contents);
@ -13,7 +13,11 @@ function parseSvg(contents: string) {
export type SvgRenderMode = 'inline' | 'sprite';
export function makeSvgComponent(meta: ImageMetadata, contents: Buffer | string, options?: { mode?: SvgRenderMode }) {
export function makeSvgComponent(
meta: ImageMetadata,
contents: Buffer | string,
options?: { mode?: SvgRenderMode },
) {
const file = typeof contents === 'string' ? contents : contents.toString('utf-8');
const { attributes, body: children } = parseSvg(file);
const props: SvgComponentProps = {

View file

@ -17,8 +17,8 @@ import { getAssetsPrefix } from './utils/getAssetsPrefix.js';
import { isESMImportedImage } from './utils/imageKind.js';
import { emitESMImage } from './utils/node/emitAsset.js';
import { getProxyCode } from './utils/proxy.js';
import { hashTransform, propsToFilename } from './utils/transformToPath.js';
import { makeSvgComponent } from './utils/svg.js';
import { hashTransform, propsToFilename } from './utils/transformToPath.js';
const resolvedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID;
@ -217,7 +217,9 @@ export default function assets({ settings }: { settings: AstroSettings }): vite.
if (settings.config.experimental.svg && /\.svg$/.test(id)) {
const { contents, ...metadata } = imageMetadata;
// We know that the contents are present, as we only emit this property for SVG files
return makeSvgComponent(metadata, contents!, { mode: settings.config.experimental.svg.mode });
return makeSvgComponent(metadata, contents!, {
mode: settings.config.experimental.svg.mode,
});
}
// We can only reliably determine if an image is used on the server, as we need to track its usage throughout the entire build.

View file

@ -544,6 +544,7 @@ export class experimental_AstroContainer {
type,
fallbackRoutes: [],
isIndex: false,
origin: 'internal',
};
}

View file

@ -130,6 +130,7 @@ export function glob(globOptions: GlobOptions): Loader {
const existingEntry = store.get(id);
const digest = generateDigest(contents);
const filePath = fileURLToPath(fileUrl);
if (existingEntry && existingEntry.digest === digest && existingEntry.filePath) {
if (existingEntry.deferredRender) {
@ -141,11 +142,10 @@ export function glob(globOptions: GlobOptions): Loader {
store.addAssetImports(existingEntry.assetImports, existingEntry.filePath);
}
fileToIdMap.set(filePath, id);
return;
}
const filePath = fileURLToPath(fileUrl);
const relativePath = posixRelative(fileURLToPath(config.root), filePath);
const parsedData = await parseData({

View file

@ -26,12 +26,22 @@ type GlobResult = Record<string, LazyImport>;
type CollectionToEntryMap = Record<string, GlobResult>;
type GetEntryImport = (collection: string, lookupId: string) => Promise<LazyImport>;
export function getImporterFilename() {
// The 4th line in the stack trace should be the importer filename
const stackLine = new Error().stack?.split('\n')?.[3];
if (!stackLine) {
return null;
}
// Extract the relative path from the stack line
const match = /\/(src\/.*?):\d+:\d+/.exec(stackLine);
return match?.[1] ?? null;
}
export function defineCollection(config: any) {
if ('loader' in config) {
if (config.type && config.type !== CONTENT_LAYER_TYPE) {
throw new AstroUserError(
'Collections that use the Content Layer API must have a `loader` defined and no `type` set.',
"Check your collection definitions in `src/content/config.*`.'",
`Collections that use the Content Layer API must have a \`loader\` defined and no \`type\` set. Check your collection definitions in ${getImporterFilename() ?? 'your content config file'}.`,
);
}
config.type = CONTENT_LAYER_TYPE;

View file

@ -24,8 +24,16 @@ export async function attachContentServerListeners({
settings,
}: ContentServerListenerParams) {
const contentPaths = getContentPaths(settings.config, fs);
if (fs.existsSync(contentPaths.contentDir)) {
if (!settings.config.legacy?.collections) {
const contentGenerator = await createContentTypesGenerator({
fs,
settings,
logger,
viteServer,
contentConfigObserver: globalContentConfigObserver,
});
await contentGenerator.init();
} else if (fs.existsSync(contentPaths.contentDir)) {
logger.debug(
'content',
`Watching ${cyan(

View file

@ -86,13 +86,12 @@ export async function createContentTypesGenerator({
async function init(): Promise<
{ typesGenerated: true } | { typesGenerated: false; reason: 'no-content-dir' }
> {
if (!fs.existsSync(contentPaths.contentDir)) {
return { typesGenerated: false, reason: 'no-content-dir' };
}
events.push({ name: 'add', entry: contentPaths.config.url });
if (settings.config.legacy.collections) {
if (!fs.existsSync(contentPaths.contentDir)) {
return { typesGenerated: false, reason: 'no-content-dir' };
}
const globResult = await glob('**', {
cwd: fileURLToPath(contentPaths.contentDir),
fs: {

View file

@ -597,7 +597,7 @@ export async function autogenerateCollections({
}) as any,
};
}
if (!usesContentLayer) {
if (!usesContentLayer && fs.existsSync(contentDir)) {
// If the user hasn't defined any collections using the content layer, we'll try and help out by checking for
// any orphaned folders in the content directory and creating collections for them.
const orphanedCollections = [];
@ -623,7 +623,7 @@ export async function autogenerateCollections({
console.warn(
`
Auto-generating collections for folders in "src/content/" that are not defined as collections.
This is deprecated, so you should define these collections yourself in "src/content/config.ts".
This is deprecated, so you should define these collections yourself in "src/content.config.ts".
The following collections have been auto-generated: ${orphanedCollections
.map((name) => green(name))
.join(', ')}\n`,
@ -715,10 +715,10 @@ export type ContentPaths = {
};
export function getContentPaths(
{ srcDir }: Pick<AstroConfig, 'root' | 'srcDir'>,
{ srcDir, legacy }: Pick<AstroConfig, 'root' | 'srcDir' | 'legacy'>,
fs: typeof fsMod = fsMod,
): ContentPaths {
const configStats = search(fs, srcDir);
const configStats = search(fs, srcDir, legacy?.collections);
const pkgBase = new URL('../../', import.meta.url);
return {
contentDir: new URL('./content/', srcDir),
@ -728,10 +728,16 @@ export function getContentPaths(
config: configStats,
};
}
function search(fs: typeof fsMod, srcDir: URL) {
const paths = ['config.mjs', 'config.js', 'config.mts', 'config.ts'].map(
(p) => new URL(`./content/${p}`, srcDir),
);
function search(fs: typeof fsMod, srcDir: URL, legacy?: boolean) {
const paths = [
...(legacy
? []
: ['content.config.mjs', 'content.config.js', 'content.config.mts', 'content.config.ts']),
'content/config.mjs',
'content/config.js',
'content/config.mts',
'content/config.ts',
].map((p) => new URL(`./${p}`, srcDir));
for (const file of paths) {
if (fs.existsSync(file)) {
return { exists: true, url: file };

View file

@ -49,7 +49,6 @@ export class AppPipeline extends Pipeline {
undefined,
undefined,
undefined,
false,
defaultRoutes,
);
pipeline.#manifestData = manifestData;

View file

@ -56,7 +56,6 @@ export abstract class Pipeline {
* Used for `Astro.site`.
*/
readonly site = manifest.site ? new URL(manifest.site) : undefined,
readonly callSetGetEnv = true,
/**
* Array of built-in, internal, routes.
* Used to find the route module
@ -70,13 +69,6 @@ export abstract class Pipeline {
createI18nMiddleware(i18n, manifest.base, manifest.trailingSlash, manifest.buildFormat),
);
}
// In SSR, getSecret should fail by default. Setting it here will run before the
// adapter override.
if (callSetGetEnv && manifest.envGetSecretEnabled) {
setGetEnv(() => {
throw new AstroError(AstroErrorData.EnvUnsupportedGetSecret);
}, true);
}
}
abstract headElements(routeData: RouteData): Promise<HeadElements> | HeadElements;

View file

@ -30,6 +30,7 @@ import type {
import type { SSRManifest, SSRManifestI18n } from '../app/types.js';
import { NoPrerenderedRoutesWithDomains } from '../errors/errors-data.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import { NOOP_MIDDLEWARE_FN } from '../middleware/noop-middleware.js';
import { getRedirectLocationOrThrow, routeIsRedirect } from '../redirects/index.js';
import { RenderContext } from '../render-context.js';
import { callGetStaticPaths } from '../render/route-cache.js';
@ -59,14 +60,9 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil
const baseDirectory = getOutputDirectory(options.settings);
const renderersEntryUrl = new URL('renderers.mjs', baseDirectory);
const renderers = await import(renderersEntryUrl.toString());
let middleware: MiddlewareHandler = (_, next) => next();
try {
// middleware.mjs is not emitted if there is no user middleware
// in which case the import fails with ERR_MODULE_NOT_FOUND, and we fall back to a no-op middleware
middleware = await import(new URL('middleware.mjs', baseDirectory).toString()).then(
(mod) => mod.onRequest,
);
} catch {}
const middleware: MiddlewareHandler = internals.middlewareEntryPoint
? await import(internals.middlewareEntryPoint.toString()).then((mod) => mod.onRequest)
: NOOP_MIDDLEWARE_FN;
manifest = createBuildManifest(
options.settings,
internals,

View file

@ -130,15 +130,13 @@ export class BuildPipeline extends Pipeline {
const renderersEntryUrl = new URL(`renderers.mjs?time=${Date.now()}`, baseDirectory);
const renderers = await import(renderersEntryUrl.toString());
const middleware = await import(new URL('middleware.mjs', baseDirectory).toString())
.then((mod) => {
return function () {
return { onRequest: mod.onRequest };
};
})
// middleware.mjs is not emitted if there is no user middleware
// in which case the import fails with ERR_MODULE_NOT_FOUND, and we fall back to a no-op middleware
.catch(() => manifest.middleware);
const middleware = internals.middlewareEntryPoint
? await import(internals.middlewareEntryPoint.toString()).then((mod) => {
return function () {
return { onRequest: mod.onRequest };
};
})
: manifest.middleware;
if (!renderers) {
throw new Error(

View file

@ -170,7 +170,6 @@ function generateSSRCode(adapter: AstroAdapter, middlewareId: string) {
`import { renderers } from '${RENDERERS_MODULE_ID}';`,
`import * as serverEntrypointModule from '${ADAPTER_VIRTUAL_MODULE_ID}';`,
`import { manifest as defaultManifest } from '${SSR_MANIFEST_VIRTUAL_MODULE_ID}';`,
edgeMiddleware ? `` : `import { onRequest as middleware } from '${middlewareId}';`,
`import { serverIslandMap } from '${VIRTUAL_ISLAND_MAP_ID}';`,
];

View file

@ -562,16 +562,16 @@ export const AstroConfigSchema = z.object({
.optional(),
})
.optional(),
svg: z.union([
z.boolean(),
z
.object({
mode: z
.union([z.literal('inline'), z.literal('sprite')])
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.svg.mode),
})
])
svg: z
.union([
z.boolean(),
z.object({
mode: z
.union([z.literal('inline'), z.literal('sprite')])
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.svg.mode),
}),
])
.optional()
.transform((svgConfig) => {
// Handle normalization of `experimental.svg` config boolean values

View file

@ -7,6 +7,7 @@ import * as vite from 'vite';
import {
runHookConfigDone,
runHookConfigSetup,
runHookRoutesResolved,
runHookServerDone,
runHookServerStart,
} from '../../integrations/hooks.js';
@ -83,10 +84,11 @@ export async function createContainer({
.filter(Boolean) as string[];
// Create the route manifest already outside of Vite so that `runHookConfigDone` can use it to inform integrations of the build output
let manifest = await createRouteManifest({ settings, fsMod: fs }, logger);
let manifest = await createRouteManifest({ settings, fsMod: fs }, logger, { dev: true });
const devSSRManifest = createDevelopmentManifest(settings);
manifest = injectDefaultDevRoutes(settings, devSSRManifest, manifest);
await runHookRoutesResolved({ settings, logger, routes: manifest.routes });
await runHookConfigDone({ settings, logger, command: 'dev' });

View file

@ -1245,17 +1245,6 @@ export const EnvInvalidVariables = {
`The following environment variables defined in \`env.schema\` are invalid:\n\n${errors.map((err) => `- ${err}`).join('\n')}\n`,
} satisfies ErrorData;
/**
* @docs
* @description
* The `astro:env/server` exported function `getSecret()` is not supported by your adapter.
*/
export const EnvUnsupportedGetSecret = {
name: 'EnvUnsupportedGetSecret',
title: 'Unsupported astro:env getSecret',
message: '`astro:env/server` exported function `getSecret` is not supported by your adapter.',
} satisfies ErrorData;
/**
* @docs
* @description
@ -1476,7 +1465,8 @@ export const GenerateContentTypesError = {
title: 'Failed to generate content types.',
message: (errorMessage: string) =>
`\`astro sync\` command failed to generate content collection types: ${errorMessage}`,
hint: 'This error is often caused by a syntax error inside your content, or your content configuration file. Check your `src/content/config.*` file for typos.',
hint: (fileName?: string) =>
`This error is often caused by a syntax error inside your content, or your content configuration file. Check your ${fileName ?? 'content config'} file for typos.`,
} satisfies ErrorData;
/**
* @docs
@ -1488,7 +1478,7 @@ export const GenerateContentTypesError = {
* @docs
* @description
* Astro encountered an unknown error loading your content collections.
* This can be caused by certain errors inside your `src/content/config.ts` file or some internal errors.
* This can be caused by certain errors inside your `src/content.config.ts` file or some internal errors.
*
* If you can reliably cause this error to happen, we'd appreciate if you could [open an issue](https://astro.build/issues/)
*/
@ -1531,7 +1521,7 @@ export const GetEntryDeprecationError = {
* @description
* A Markdown or MDX entry does not match its collection schema.
* Make sure that all required fields are present, and that all fields are of the correct type.
* You can check against the collection schema in your `src/content/config.*` file.
* You can check against the collection schema in your `src/content.config.*` file.
* See the [Content collections documentation](https://docs.astro.build/en/guides/content-collections/) for more information.
*/
export const InvalidContentEntryFrontmatterError = {
@ -1558,7 +1548,7 @@ export const InvalidContentEntryFrontmatterError = {
* @description
* A content entry does not match its collection schema.
* Make sure that all required fields are present, and that all fields are of the correct type.
* You can check against the collection schema in your `src/content/config.*` file.
* You can check against the collection schema in your `src/content.config.*` file.
* See the [Content collections documentation](https://docs.astro.build/en/guides/content-collections/) for more information.
*/
export const InvalidContentEntryDataError = {
@ -1583,7 +1573,7 @@ export const InvalidContentEntryDataError = {
* @description
* A content entry does not match its collection schema.
* Make sure that all required fields are present, and that all fields are of the correct type.
* You can check against the collection schema in your `src/content/config.*` file.
* You can check against the collection schema in your `src/content.config.*` file.
* See the [Content collections documentation](https://docs.astro.build/en/guides/content-collections/) for more information.
*/
export const ContentEntryDataError = {

View file

@ -13,7 +13,6 @@ export const MIDDLEWARE_MODULE_ID = '\0astro-internal:middleware';
const NOOP_MIDDLEWARE = '\0noop-middleware';
export function vitePluginMiddleware({ settings }: { settings: AstroSettings }): VitePlugin {
let isCommandBuild = false;
let resolvedMiddlewareId: string | undefined = undefined;
const hasIntegrationMiddleware =
settings.middlewares.pre.length > 0 || settings.middlewares.post.length > 0;
@ -21,9 +20,6 @@ export function vitePluginMiddleware({ settings }: { settings: AstroSettings }):
return {
name: '@astro/plugin-middleware',
config(_, { command }) {
isCommandBuild = command === 'build';
},
async resolveId(id) {
if (id === MIDDLEWARE_MODULE_ID) {
const middlewareId = await this.resolve(
@ -53,15 +49,6 @@ export function vitePluginMiddleware({ settings }: { settings: AstroSettings }):
if (!userMiddlewareIsPresent && settings.config.i18n?.routing === 'manual') {
throw new AstroError(MissingMiddlewareForInternationalization);
}
// In the build, tell Vite to emit this file
if (isCommandBuild) {
this.emitFile({
type: 'chunk',
preserveSignature: 'strict',
fileName: 'middleware.mjs',
id,
});
}
const preMiddleware = createMiddlewareImports(settings.middlewares.pre, 'pre');
const postMiddleware = createMiddlewareImports(settings.middlewares.post, 'post');
@ -124,7 +111,7 @@ export function vitePluginMiddlewareBuild(
writeBundle(_, bundle) {
for (const [chunkName, chunk] of Object.entries(bundle)) {
if (chunk.type !== 'asset' && chunk.fileName === 'middleware.mjs') {
if (chunk.type !== 'asset' && chunk.facadeModuleId === MIDDLEWARE_MODULE_ID) {
const outputDirectory = getOutputDirectory(opts.settings);
internals.middlewareEntryPoint = new URL(chunkName, outputDirectory);
}

View file

@ -29,6 +29,7 @@ import { callMiddleware } from './middleware/callMiddleware.js';
import { sequence } from './middleware/index.js';
import { renderRedirect } from './redirects/render.js';
import { type Pipeline, Slots, getParams, getProps } from './render/index.js';
import { isRoute404or500 } from './routing/match.js';
import { copyRequest, getOriginPathname, setOriginPathname } from './routing/rewrite.js';
import { SERVER_ISLAND_COMPONENT } from './server-islands/endpoint.js';
import { AstroSession } from './session.js';
@ -579,11 +580,9 @@ export class RenderContext {
computedLocale = computeCurrentLocale(referer, locales, defaultLocale);
}
} else {
if (routeData.pathname) {
computedLocale = computeCurrentLocale(routeData.pathname, locales, defaultLocale);
} else {
computedLocale = computeCurrentLocale(url.pathname, locales, defaultLocale);
}
const pathname =
routeData.pathname && !isRoute404or500(routeData) ? routeData.pathname : url.pathname;
computedLocale = computeCurrentLocale(pathname, locales, defaultLocale);
}
this.#currentLocale = computedLocale ?? fallbackTo;

View file

@ -15,6 +15,7 @@ export const DEFAULT_404_ROUTE: RouteData = {
route: '/404',
fallbackRoutes: [],
isIndex: false,
origin: 'internal',
};
export const DEFAULT_500_ROUTE: RouteData = {
@ -29,6 +30,7 @@ export const DEFAULT_500_ROUTE: RouteData = {
route: '/500',
fallbackRoutes: [],
isIndex: false,
origin: 'internal',
};
export function ensure404Route(manifest: ManifestData) {

View file

@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url';
import { bold } from 'kleur/colors';
import pLimit from 'p-limit';
import { toRoutingStrategy } from '../../../i18n/utils.js';
import { runHookRoutesResolved } from '../../../integrations/hooks.js';
import { getPrerenderDefault } from '../../../prerender/utils.js';
import type { AstroConfig } from '../../../types/public/config.js';
import type { RouteData, RoutePart } from '../../../types/public/internal.js';
@ -255,6 +256,7 @@ function createFileBasedRoutes(
prerender,
fallbackRoutes: [],
distURL: [],
origin: 'project',
});
}
}
@ -280,7 +282,7 @@ function createInjectedRoutes({ settings, cwd }: CreateRouteManifestParams): Rou
const routes: RouteData[] = [];
for (const injectedRoute of settings.injectedRoutes) {
const { pattern: name, entrypoint, prerender: prerenderInjected } = injectedRoute;
const { pattern: name, entrypoint, prerender: prerenderInjected, origin } = injectedRoute;
const { resolved, component } = resolveInjectedRoute(entrypoint.toString(), config.root, cwd);
const segments = removeLeadingForwardSlash(name)
@ -320,6 +322,7 @@ function createInjectedRoutes({ settings, cwd }: CreateRouteManifestParams): Rou
prerender: prerenderInjected ?? prerender,
fallbackRoutes: [],
distURL: [],
origin,
});
}
@ -389,6 +392,7 @@ function createRedirectRoutes(
redirectRoute: routeMap.get(destination),
fallbackRoutes: [],
distURL: [],
origin: 'project',
});
}
@ -480,6 +484,7 @@ function detectRouteCollision(a: RouteData, b: RouteData, _config: AstroConfig,
export async function createRouteManifest(
params: CreateRouteManifestParams,
logger: Logger,
{ dev = false }: { dev?: boolean } = {},
): Promise<ManifestData> {
const { settings } = params;
const { config } = settings;
@ -727,6 +732,10 @@ export async function createRouteManifest(
}
}
if (!dev) {
await runHookRoutesResolved({ routes, settings, logger });
}
return {
routes,
};

View file

@ -41,5 +41,6 @@ export function deserializeRouteData(rawRouteData: SerializedRouteData): RouteDa
return deserializeRouteData(fallback);
}),
isIndex: rawRouteData.isIndex,
origin: rawRouteData.origin,
};
}

View file

@ -16,3 +16,13 @@ export function matchRoute(pathname: string, manifest: ManifestData): RouteData
export function matchAllRoutes(pathname: string, manifest: ManifestData): RouteData[] {
return manifest.routes.filter((route) => route.pattern.test(decodeURI(pathname)));
}
/**
* Determines if the given route matches a 404 or 500 error page.
*
* @param {RouteData} route - The route data to check.
* @returns {boolean} `true` if the route matches a 404 or 500 error page, otherwise `false`.
*/
export function isRoute404or500(route: RouteData): boolean {
return route.pattern.test('/404') || route.pattern.test('/500');
}

View file

@ -4,6 +4,7 @@ import {
renderComponent,
renderTemplate,
} from '../../runtime/server/index.js';
import { isAstroComponentFactory } from '../../runtime/server/render/astro/factory.js';
import { createSlotValueFromString } from '../../runtime/server/render/slot.js';
import type { ComponentInstance, ManifestData } from '../../types/astro.js';
import type { RouteData, SSRManifest } from '../../types/public/internal.js';
@ -31,6 +32,7 @@ export function getServerIslandRouteData(config: ConfigFields) {
isIndex: false,
fallbackRoutes: [],
route: SERVER_ISLAND_ROUTE,
origin: 'internal',
};
return route;
}
@ -120,17 +122,31 @@ export function createEndpoint(manifest: SSRManifest) {
const key = await manifest.key;
const encryptedProps = data.encryptedProps;
const propString = await decryptString(key, encryptedProps);
const props = JSON.parse(propString);
const componentModule = await imp();
const Component = (componentModule as any)[data.componentExport];
let Component = (componentModule as any)[data.componentExport];
const slots: ComponentSlots = {};
for (const prop in data.slots) {
slots[prop] = createSlotValueFromString(data.slots[prop]);
}
// Wrap Astro components so we can set propagation to
// `self` which is needed to force the runtime to wait
// on the component before sending out the response headers.
// This allows the island to set headers (cookies).
if (isAstroComponentFactory(Component)) {
const ServerIsland = Component;
Component = function (this: typeof ServerIsland, ...args: Parameters<typeof ServerIsland>) {
return ServerIsland.apply(this, args);
};
Object.assign(Component, ServerIsland);
Component.propagation = 'self';
}
return renderTemplate`${renderComponent(result, 'Component', Component, props, slots)}`;
};

View file

@ -21,7 +21,6 @@ import { resolveConfig } from '../config/config.js';
import { createNodeLogger } from '../config/logging.js';
import { createSettings } from '../config/settings.js';
import { createVite } from '../create-vite.js';
import { collectErrorMetadata } from '../errors/dev/utils.js';
import {
AstroError,
AstroErrorData,
@ -31,7 +30,6 @@ import {
isAstroError,
} from '../errors/index.js';
import type { Logger } from '../logger/core.js';
import { formatErrorMessage } from '../messages.js';
import { createRouteManifest } from '../routing/index.js';
import { ensureProcessNodeEnv } from '../util.js';
@ -255,7 +253,22 @@ async function syncContentCollections(
if (isAstroError(e)) {
throw e;
}
const hint = AstroUserError.is(e) ? e.hint : AstroErrorData.GenerateContentTypesError.hint;
let configFile;
try {
const contentPaths = getContentPaths(settings.config, fs);
if (contentPaths.config.exists) {
const matches = /\/(src\/.+)/.exec(contentPaths.config.url.href);
if (matches) {
configFile = matches[1];
}
}
} catch {
// ignore
}
const hint = AstroUserError.is(e)
? e.hint
: AstroErrorData.GenerateContentTypesError.hint(configFile);
throw new AstroError(
{
...AstroErrorData.GenerateContentTypesError,

View file

@ -24,7 +24,9 @@ import type {
import type {
AstroIntegration,
AstroRenderer,
BaseIntegrationHooks,
HookParameters,
IntegrationResolvedRoute,
IntegrationRouteData,
RouteOptions,
} from '../types/public/integrations.js';
@ -39,7 +41,7 @@ async function withTakingALongTimeMsg<T>({
logger,
}: {
name: string;
hookName: string;
hookName: keyof BaseIntegrationHooks;
hookResult: T | Promise<T>;
timeoutMs?: number;
logger: Logger;
@ -204,7 +206,7 @@ export async function runHookConfigSetup({
);
injectRoute.entrypoint = injectRoute.entryPoint as string;
}
updatedSettings.injectedRoutes.push(injectRoute);
updatedSettings.injectedRoutes.push({ ...injectRoute, origin: 'external' });
},
addWatchFile: (path) => {
updatedSettings.watchFiles.push(path instanceof URL ? fileURLToPath(path) : path);
@ -599,6 +601,9 @@ export async function runHookBuildDone({ settings, pages, routes, logging }: Run
pages: pages.map((p) => ({ pathname: p })),
dir,
routes: integrationRoutes,
assets: new Map(
routes.filter((r) => r.distURL !== undefined).map((r) => [r.route, r.distURL!]),
),
logger,
}),
logger: logging,
@ -648,6 +653,47 @@ export async function runHookRouteSetup({
}
}
export async function runHookRoutesResolved({
routes,
settings,
logger,
}: { routes: Array<RouteData>; settings: AstroSettings; logger: Logger }) {
for (const integration of settings.config.integrations) {
if (integration?.hooks?.['astro:routes:resolved']) {
const integrationLogger = getLogger(integration, logger);
await withTakingALongTimeMsg({
name: integration.name,
hookName: 'astro:routes:resolved',
hookResult: integration.hooks['astro:routes:resolved']({
routes: routes.map((route) => toIntegrationResolvedRoute(route)),
logger: integrationLogger,
}),
logger,
});
}
}
}
function toIntegrationResolvedRoute(route: RouteData): IntegrationResolvedRoute {
return {
isPrerendered: route.prerender,
entrypoint: route.component,
pattern: route.route,
params: route.params,
origin: route.origin,
generate: route.generate,
patternRegex: route.pattern,
segments: route.segments,
type: route.type,
pathname: route.pathname,
redirect: route.redirect,
redirectRoute: route.redirectRoute
? toIntegrationResolvedRoute(route.redirectRoute)
: undefined,
};
}
function toIntegrationRouteData(route: RouteData): IntegrationRouteData {
return {
route: route.route,

View file

@ -119,22 +119,23 @@ let response = await fetch('${serverIslandUrl}', {
});
`
}
if(response.status === 200 && response.headers.get('content-type') === 'text/html') {
let html = await response.text();
// Swap!
while(script.previousSibling &&
script.previousSibling.nodeType !== 8 &&
script.previousSibling.data !== '[if astro]>server-island-start<![endif]') {
script.previousSibling.remove();
if (script) {
if(response.status === 200 && response.headers.get('content-type') === 'text/html') {
let html = await response.text();
// Swap!
while(script.previousSibling &&
script.previousSibling.nodeType !== 8 &&
script.previousSibling.data !== '[if astro]>server-island-start<![endif]') {
script.previousSibling.remove();
}
script.previousSibling?.remove();
let frag = document.createRange().createContextualFragment(html);
script.before(frag);
}
script.previousSibling?.remove();
let frag = document.createRange().createContextualFragment(html);
script.before(frag);
script.remove();
}
script.remove();
</script>`);
},
};

View file

@ -10,12 +10,10 @@ import type { ContentEntryType, DataEntryType } from './public/content.js';
import type {
AstroAdapter,
AstroRenderer,
InjectedRoute,
InjectedScriptStage,
InjectedType,
ResolvedInjectedRoute,
} from './public/integrations.js';
import type { RouteData } from './public/internal.js';
import type { InternalInjectedRoute, ResolvedInjectedRoute, RouteData } from './public/internal.js';
import type { DevToolbarAppEntry } from './public/toolbar.js';
export type SerializedRouteData = Omit<
@ -35,7 +33,7 @@ export interface AstroSettings {
config: AstroConfig;
adapter: AstroAdapter | undefined;
preferences: AstroPreferences;
injectedRoutes: InjectedRoute[];
injectedRoutes: InternalInjectedRoute[];
resolvedInjectedRoutes: ResolvedInjectedRoute[];
pageExtensions: string[];
contentEntryTypes: ContentEntryType[];

View file

@ -1749,7 +1749,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
* When you are ready to remove this flag and migrate to the new Content Layer API for your legacy collections, you must define a collection for any directories in `src/content/` that you want to continue to use as a collection. It is sufficient to declare an empty collection, and Astro will implicitly generate an appropriate definition for your legacy collections:
*
* ```js
* // src/content/config.ts
* // src/content.config.ts
* import { defineCollection, z } from 'astro:content';
*
* const blog = defineCollection({ })
@ -1941,11 +1941,10 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
* - `position`: Defines the position of the image crop if the aspect ratio is changed. Values match those of CSS `object-position`. Defaults to `center`, or the value of `image.experimentalObjectPosition` if set.
* - `priority`: If set, eagerly loads the image. Otherwise images will be lazy-loaded. Use this for your largest above-the-fold image. Defaults to `false`.
*
* The following `<Image />` component properties should not be used with responsive images as these are automatically generated:
* The `widths` and `sizes` attributes are automatically generated based on the image's dimensions and the layout type, and in most cases should not be set manually. The generated `sizes` attribute for `responsive` and `full-width` images
* is based on the assumption that the image is displayed at close to the full width of the screen when the viewport is smaller than the image's width. If it is significantly different (e.g. if it's in a multi-column layout on small screens) you may need to adjust the `sizes` attribute manually for best results.
*
* - `densities`
* - `widths`
* - `sizes`
* The `densities` attribute is not compatible with responsive images and will be ignored if set.
*/
responsiveImages?: boolean;

View file

@ -8,7 +8,7 @@ import type { getToolbarServerCommunicationHelpers } from '../../integrations/ho
import type { DeepPartial } from '../../type-utils.js';
import type { AstroConfig } from './config.js';
import type { RefreshContentOptions } from './content.js';
import type { RouteData } from './internal.js';
import type { InternalInjectedRoute, RouteData } from './internal.js';
import type { DevToolbarAppEntry } from './toolbar.js';
export interface RouteOptions {
@ -138,15 +138,7 @@ export type AstroAdapterFeatureMap = {
*/
export type InjectedScriptStage = 'before-hydration' | 'head-inline' | 'page' | 'page-ssr';
export interface InjectedRoute {
pattern: string;
entrypoint: string | URL;
prerender?: boolean;
}
export interface ResolvedInjectedRoute extends InjectedRoute {
resolvedEntryPoint?: URL;
}
export type InjectedRoute = Omit<InternalInjectedRoute, 'origin'>;
export interface InjectedType {
filename: string;
@ -225,13 +217,19 @@ export interface BaseIntegrationHooks {
'astro:build:done': (options: {
pages: { pathname: string }[];
dir: URL;
/** @deprecated Use the `assets` map and the new `astro:routes:resolved` hook */
routes: IntegrationRouteData[];
assets: Map<string, URL[]>;
logger: AstroIntegrationLogger;
}) => void | Promise<void>;
'astro:route:setup': (options: {
route: RouteOptions;
logger: AstroIntegrationLogger;
}) => void | Promise<void>;
'astro:routes:resolved': (options: {
routes: IntegrationResolvedRoute[];
logger: AstroIntegrationLogger;
}) => void | Promise<void>;
}
export interface AstroIntegration {
@ -245,13 +243,45 @@ export interface AstroIntegration {
/**
* A smaller version of the {@link RouteData} that is used in the integrations.
* @deprecated Use {@link IntegrationResolvedRoute}
*/
export type IntegrationRouteData = Omit<
RouteData,
'isIndex' | 'fallbackRoutes' | 'redirectRoute'
'isIndex' | 'fallbackRoutes' | 'redirectRoute' | 'origin'
> & {
/**
* {@link RouteData.redirectRoute}
*/
redirectRoute?: IntegrationRouteData;
};
export interface IntegrationResolvedRoute
extends Pick<
RouteData,
'generate' | 'params' | 'pathname' | 'segments' | 'type' | 'redirect' | 'origin'
> {
/**
* {@link RouteData.route}
*/
pattern: RouteData['route'];
/**
* {@link RouteData.pattern}
*/
patternRegex: RouteData['pattern'];
/**
* {@link RouteData.component}
*/
entrypoint: RouteData['component'];
/**
* {@link RouteData.prerender}
*/
isPrerendered: RouteData['prerender'];
/**
* {@link RouteData.redirectRoute}
*/
redirectRoute?: IntegrationResolvedRoute;
}

View file

@ -136,6 +136,11 @@ export interface RouteData {
* - src/pages/blog/index.astro
*/
isIndex: boolean;
/**
* Whether the route comes from Astro core, an integration or the user's project
*/
origin: 'internal' | 'external' | 'project';
}
/**
@ -284,3 +289,16 @@ export interface SSRMetadata {
}
export type SSRError = Error & ViteErrorPayload['err'];
// `origin` is set within the hook, but the user doesn't have access to this property. That's why
// we need an intermediary interface
export interface InternalInjectedRoute {
pattern: string;
entrypoint: string | URL;
prerender?: boolean;
origin: RouteData['origin'];
}
export interface ResolvedInjectedRoute extends InternalInjectedRoute {
resolvedEntryPoint?: URL;
}

View file

@ -1,10 +1,12 @@
import { AsyncLocalStorage } from 'node:async_hooks';
import type fs from 'node:fs';
import { IncomingMessage } from 'node:http';
import { fileURLToPath } from 'node:url';
import type * as vite from 'vite';
import { normalizePath } from 'vite';
import type { SSRManifest, SSRManifestI18n } from '../core/app/types.js';
import { warnMissingAdapter } from '../core/dev/adapter-validation.js';
import { createKey } from '../core/encryption.js';
import { createKey, getEnvironmentKey, hasEnvironmentKey } from '../core/encryption.js';
import { getViteErrorPayload } from '../core/errors/dev/index.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { patchOverlay } from '../core/errors/overlay.js';
@ -13,7 +15,9 @@ import { NOOP_MIDDLEWARE_FN } from '../core/middleware/noop-middleware.js';
import { createViteLoader } from '../core/module-loader/index.js';
import { injectDefaultDevRoutes } from '../core/routing/dev-default.js';
import { createRouteManifest } from '../core/routing/index.js';
import { getRoutePrerenderOption } from '../core/routing/manifest/prerender.js';
import { toFallbackType, toRoutingStrategy } from '../i18n/utils.js';
import { runHookRoutesResolved } from '../integrations/hooks.js';
import type { AstroSettings, ManifestData } from '../types/astro.js';
import { baseMiddleware } from './base.js';
import { createController } from './controller.js';
@ -50,26 +54,46 @@ export default function createVitePluginAstroServer({
const controller = createController({ loader });
const localStorage = new AsyncLocalStorage();
/** rebuild the route cache + manifest, as needed. */
async function rebuildManifest(needsManifestRebuild: boolean) {
/** rebuild the route cache + manifest */
async function rebuildManifest(path: string | null = null) {
pipeline.clearRouteCache();
if (needsManifestRebuild) {
// If a route changes, we check if it's part of the manifest and check for its prerender value
if (path !== null) {
const route = routeManifest.routes.find(
(r) =>
normalizePath(path) ===
normalizePath(fileURLToPath(new URL(r.component, settings.config.root))),
);
if (!route) {
return;
}
if (route.type !== 'page' && route.type !== 'endpoint') return;
const routePath = fileURLToPath(new URL(route.component, settings.config.root));
try {
const content = await fsMod.promises.readFile(routePath, 'utf-8');
await getRoutePrerenderOption(content, route, settings, logger);
} catch (_) {}
} else {
routeManifest = injectDefaultDevRoutes(
settings,
devSSRManifest,
await createRouteManifest({ settings, fsMod }, logger), // TODO: Handle partial updates to the manifest
await createRouteManifest({ settings, fsMod }, logger, { dev: true }),
);
warnMissingAdapter(logger, settings);
pipeline.manifest.checkOrigin =
settings.config.security.checkOrigin && settings.buildOutput === 'server';
pipeline.setManifestData(routeManifest);
}
await runHookRoutesResolved({ routes: routeManifest.routes, settings, logger });
warnMissingAdapter(logger, settings);
pipeline.manifest.checkOrigin =
settings.config.security.checkOrigin && settings.buildOutput === 'server';
pipeline.setManifestData(routeManifest);
}
// Rebuild route manifest on file change, if needed.
viteServer.watcher.on('add', rebuildManifest.bind(null, true));
viteServer.watcher.on('unlink', rebuildManifest.bind(null, true));
viteServer.watcher.on('change', rebuildManifest.bind(null, false));
// Rebuild route manifest on file change
viteServer.watcher.on('add', rebuildManifest.bind(null, null));
viteServer.watcher.on('unlink', rebuildManifest.bind(null, null));
viteServer.watcher.on('change', rebuildManifest);
function handleUnhandledRejection(rejection: any) {
const error = new AstroError({
@ -168,7 +192,7 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
checkOrigin:
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
envGetSecretEnabled: false,
key: createKey(),
key: hasEnvironmentKey() ? getEnvironmentKey() : createKey(),
middleware() {
return {
onRequest: NOOP_MIDDLEWARE_FN,

View file

@ -5,7 +5,7 @@ import type { AstroSettings } from '../types/astro.js';
import { normalizePath } from 'vite';
import { runHookServerSetup } from '../integrations/hooks.js';
import type { InjectedRoute, ResolvedInjectedRoute } from '../types/public/integrations.js';
import type { InternalInjectedRoute, ResolvedInjectedRoute } from '../types/public/internal.js';
/** Connect Astro integrations into Vite, as needed. */
export default function astroIntegrationsContainerPlugin({
@ -33,7 +33,7 @@ export default function astroIntegrationsContainerPlugin({
async function resolveEntryPoint(
this: PluginContext,
route: InjectedRoute,
route: InternalInjectedRoute,
): Promise<ResolvedInjectedRoute> {
const resolvedId = await this.resolve(route.entrypoint.toString())
.then((res) => res?.id)

View file

@ -209,7 +209,7 @@ describe('astro sync', () => {
assert.fail();
}
});
it('Does not throw if a virtual module is imported in content/config.ts', async () => {
it('Does not throw if a virtual module is imported in content.config.ts', async () => {
try {
await fixture.load('./fixtures/astro-env-content-collections/');
fixture.clean();

View file

@ -285,7 +285,7 @@ describe('Content Layer', () => {
it('clears the store on new build if the config has changed', async () => {
let newJson = devalue.parse(await fixture.readFile('/collections.json'));
assert.equal(newJson.increment.data.lastValue, 1);
await fixture.editFile('src/content/config.ts', (prev) => {
await fixture.editFile('src/content.config.ts', (prev) => {
return `${prev}\nexport const foo = 'bar';`;
});
await fixture.build();
@ -424,5 +424,26 @@ describe('Content Layer', () => {
assert.equal(res.status, 500);
assert.ok(text.includes('RenderUndefinedEntryError'));
});
it('update the store when a file is renamed', async () => {
const rawJsonResponse = await fixture.fetch('/collections.json');
const initialJson = devalue.parse(await rawJsonResponse.text());
assert.equal(initialJson.numbers.map((e) => e.id).includes('src/data/glob-data/three'), true);
const oldPath = new URL('./data/glob-data/three.json', fixture.config.srcDir);
const newPath = new URL('./data/glob-data/four.json', fixture.config.srcDir);
await fs.rename(oldPath, newPath);
await fixture.onNextDataStoreChange();
try {
const updatedJsonResponse = await fixture.fetch('/collections.json');
const updated = devalue.parse(await updatedJsonResponse.text());
assert.equal(updated.numbers.map((e) => e.id).includes('src/data/glob-data/three'), false);
assert.equal(updated.numbers.map((e) => e.id).includes('src/data/glob-data/four'), true);
} finally {
await fs.rename(newPath, oldPath);
}
});
});
});

View file

@ -53,7 +53,7 @@ describe('astro:assets - SVG Components', () => {
assert.equal(!!$(this).attr('mode'), false);
const $use = $(this).children('use');
assert.equal($use.length, 0);
})
});
});
it('Adds the <svg> tag with the definition', () => {
@ -64,7 +64,7 @@ describe('astro:assets - SVG Components', () => {
const $symbol = $svg.children('symbol');
assert.equal($symbol.length, 1);
assert.equal($symbol.attr('id').startsWith('a:'), true);
const $use = $svg.children('use');
assert.equal($use.length, 1);
assert.equal($use.attr('href').startsWith('#a:'), true);
@ -78,7 +78,7 @@ describe('astro:assets - SVG Components', () => {
const $symbol = $svg.children('symbol');
assert.equal($symbol.length, 0);
const definitionId = $('#definition svg symbol').attr('id')
const definitionId = $('#definition svg symbol').attr('id');
const $use = $svg.children('use');
assert.equal($use.length, 1);
assert.equal($use.attr('href').startsWith('#a:'), true);
@ -167,13 +167,13 @@ describe('astro:assets - SVG Components', () => {
assert.equal($svg.attr('role'), 'img');
assert.equal(!!$svg.attr('mode'), false);
const $symbol = $svg.children('symbol')
const $symbol = $svg.children('symbol');
assert.equal($symbol.length, 0);
const $use = $svg.children('use')
const $use = $svg.children('use');
assert.equal($use.length, 0);
const $path = $svg.children('path');
assert.equal($path.length, 1);
})
});
it('adds the svg into the document directly', () => {
let $svg = $('#inline svg');
assert.equal($svg.length, 1);
@ -183,9 +183,9 @@ describe('astro:assets - SVG Components', () => {
assert.equal($svg.attr('role'), 'img');
assert.equal(!!$svg.attr('mode'), false);
const $symbol = $svg.children('symbol')
const $symbol = $svg.children('symbol');
assert.equal($symbol.length, 0);
const $use = $svg.children('use')
const $use = $svg.children('use');
assert.equal($use.length, 0);
const $path = $svg.children('path');
assert.equal($path.length, 1);
@ -199,13 +199,13 @@ describe('astro:assets - SVG Components', () => {
assert.equal($svg.attr('role'), 'img');
assert.equal(!!$svg.attr('mode'), false);
const $symbol = $svg.children('symbol')
const $symbol = $svg.children('symbol');
assert.equal($symbol.length, 0);
const $use = $svg.children('use')
const $use = $svg.children('use');
assert.equal($use.length, 0);
const $path = $svg.children('path');
assert.equal($path.length, 1);
})
});
it('adds the svg into the document as a sprite, overridding the default', () => {
let $svg = $('#definition svg');
assert.equal($svg.length, 1);
@ -215,10 +215,10 @@ describe('astro:assets - SVG Components', () => {
assert.equal($svg.attr('role'), 'img');
assert.equal(!!$svg.attr('mode'), false);
let $symbol = $svg.children('symbol')
let $symbol = $svg.children('symbol');
assert.equal($symbol.length, 1);
assert.equal(!!$symbol.attr('viewBox'), true);
let $use = $svg.children('use')
let $use = $svg.children('use');
assert.equal($use.length, 1);
let $path = $svg.children('path');
assert.equal($path.length, 0);
@ -231,14 +231,14 @@ describe('astro:assets - SVG Components', () => {
assert.equal($svg.attr('role'), 'img');
assert.equal(!!$svg.attr('mode'), false);
$symbol = $svg.children('symbol')
$symbol = $svg.children('symbol');
assert.equal($symbol.length, 0);
assert.equal(!!$symbol.attr('viewBox'), false);
$use = $svg.children('use')
$use = $svg.children('use');
assert.equal($use.length, 1);
$path = $svg.children('path');
assert.equal($path.length, 0);
})
});
});
describe('title', () => {
let $;
@ -255,7 +255,7 @@ describe('astro:assets - SVG Components', () => {
const $title = $('#base svg > title');
assert.equal($title.length, 1);
assert.equal($title.text(), 'GitHub Logo')
assert.equal($title.text(), 'GitHub Logo');
});
});
describe('strip', () => {
@ -291,11 +291,11 @@ describe('astro:assets - SVG Components', () => {
assert.equal($svg.attr('class'), 'foobar');
assert.equal($svg.attr('data-state'), 'open');
const $symbol = $svg.children('symbol')
const $symbol = $svg.children('symbol');
assert.equal($symbol.length, 0);
const $use = $svg.children('use')
const $use = $svg.children('use');
assert.equal($use.length, 0);
const $path = $svg.children('path')
const $path = $svg.children('path');
assert.equal($path.length, 1);
});
it('allows overriding the role attribute', () => {
@ -337,7 +337,6 @@ describe('astro:assets - SVG Components', () => {
useId = $('.two.use svg > use').attr('id');
assert.equal(defId, useId);
// Third SVG
$svg = $('.three svg');
assert.equal($svg.length, 1);
@ -372,9 +371,11 @@ describe('astro:assets - SVG Components', () => {
const $svg = $('svg');
assert.equal($svg.length, 2);
$svg.each(function() { assert.equal($(this).attr('role'), 'img') });
$svg.each(function () {
assert.equal($(this).attr('role'), 'img');
});
const definitionId = $($svg[0]).children('symbol').attr('id')
const definitionId = $($svg[0]).children('symbol').attr('id');
const $reuse = $($svg[1]);
const $symbol = $reuse.children('symbol');

View file

@ -1,8 +1,8 @@
// @ts-check
import assert from 'node:assert/strict';
import { before, describe, it } from 'node:test';
import { loadFixture } from './test-utils.js';
import { removeDir } from '@astrojs/internal-helpers/fs';
import { loadFixture } from './test-utils.js';
describe('Content Collections - data collections', () => {
let fixture;

View file

@ -4,7 +4,7 @@ import { glob } from 'astro/loaders';
const reptiles = defineCollection({
loader: glob({
pattern: '*.mdx',
base: new URL('../../content-outside-src-mdx', import.meta.url),
base: new URL('../content-outside-src-mdx', import.meta.url),
}),
schema: () =>
z.object({

Some files were not shown because too many files have changed in this diff Show more