merge
This commit is contained in:
commit
a5369ae504
239 changed files with 5185 additions and 3143 deletions
5
.changeset/chatty-dolls-visit.md
Normal file
5
.changeset/chatty-dolls-visit.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@astrojs/sitemap': minor
|
||||
---
|
||||
|
||||
Adds support to SSR routes to sitemap generation.
|
5
.changeset/friendly-fishes-sing.md
Normal file
5
.changeset/friendly-fishes-sing.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': minor
|
||||
---
|
||||
|
||||
Implement Inline Stylesheets RFC as experimental
|
21
.changeset/green-cups-hammer.md
Normal file
21
.changeset/green-cups-hammer.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
'astro': minor
|
||||
---
|
||||
|
||||
Implements a new class-based scoping strategy
|
||||
|
||||
This implements the [Scoping RFC](https://github.com/withastro/roadmap/pull/543), providing a way to opt in to increased style specificity for Astro component styles.
|
||||
|
||||
This prevents bugs where global styles override Astro component styles due to CSS ordering and the use of element selectors.
|
||||
|
||||
To enable class-based scoping, you can set it in your config:
|
||||
|
||||
```js
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
scopedStyleStrategy: 'class'
|
||||
});
|
||||
```
|
||||
|
||||
Note that the 0-specificity `:where` pseudo-selector is still the default strategy. The intent is to change `'class'` to be the default in 3.0.
|
5
.changeset/nine-geckos-act.md
Normal file
5
.changeset/nine-geckos-act.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': minor
|
||||
---
|
||||
|
||||
Support `<Code inline />` to output inline code HTML (no `pre` tag)
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
'@astrojs/telemetry': patch
|
||||
'@astrojs/webapi': patch
|
||||
---
|
||||
|
||||
Upgrade undici to v5.22.0
|
5
.changeset/pretty-bears-deliver.md
Normal file
5
.changeset/pretty-bears-deliver.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': minor
|
||||
---
|
||||
|
||||
New middleware API
|
8
.changeset/smooth-cows-jog.md
Normal file
8
.changeset/smooth-cows-jog.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
'@astrojs/markdoc': patch
|
||||
'@astrojs/mdx': patch
|
||||
'@astrojs/markdown-remark': minor
|
||||
'astro': minor
|
||||
---
|
||||
|
||||
Upgrade shiki to v0.14.1. This updates the shiki theme colors and adds the theme name to the `pre` tag, e.g. `<pre class="astro-code github-dark">`.
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Update `experimental.assets`'s `image.service` configuration to allow for a config option in addition to an entrypoint
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"@astrojs/cloudflare": patch
|
||||
---
|
||||
|
||||
Fix missing code language in Cloudflare README
|
5
.changeset/twelve-feet-switch.md
Normal file
5
.changeset/twelve-feet-switch.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Ensure multiple cookies set in dev result in multiple set-cookie headers
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
'@astrojs/tailwind': patch
|
||||
'@astrojs/svelte': patch
|
||||
---
|
||||
|
||||
Update dependencies
|
4
.github/workflows/check-merge.yml
vendored
4
.github/workflows/check-merge.yml
vendored
|
@ -34,7 +34,7 @@ jobs:
|
|||
|
||||
- name: Get changed files in the .changeset folder
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v29
|
||||
uses: tj-actions/changed-files@v35
|
||||
if: steps.set-blocks.outputs.blocks == ''
|
||||
with:
|
||||
files: |
|
||||
|
@ -87,5 +87,5 @@ jobs:
|
|||
--url https://api.github.com/repos/${{github.repository}}/pulls/${{github.event.number}}/reviews \
|
||||
--header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \
|
||||
--header 'content-type: application/json' \
|
||||
-d '{"event":"REQUEST_CHANGES"}'
|
||||
-d '{"event":"REQUEST_CHANGES", body: ""}'
|
||||
|
||||
|
|
|
@ -21,12 +21,15 @@ import { MyCounter } from '../components/my-counter.js';
|
|||
<Lorem />
|
||||
|
||||
{
|
||||
/**
|
||||
* Our VS Code extension does not currently properly typecheck attributes on Lit components
|
||||
* As such, the following code will result in a TypeScript error inside the editor, nonetheless, it works in Astro!
|
||||
*/
|
||||
(
|
||||
/**
|
||||
* Our editor tooling does not currently properly typecheck attributes on imported Lit components. As such, without a
|
||||
* pragma directive telling TypeScript to ignore the error, the line below will result in an error in the editor.
|
||||
* Nonetheless, this code works in Astro itself!
|
||||
*/
|
||||
// @ts-expect-error
|
||||
<CalcAdd num={0} />
|
||||
)
|
||||
}
|
||||
{/** @ts-expect-error */}
|
||||
<CalcAdd num={0} />
|
||||
</body>
|
||||
</html>
|
||||
|
|
13
examples/middleware/astro.config.mjs
Normal file
13
examples/middleware/astro.config.mjs
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import node from '@astrojs/node';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
adapter: node({
|
||||
mode: 'standalone',
|
||||
}),
|
||||
experimental: {
|
||||
middleware: true,
|
||||
},
|
||||
});
|
23
examples/middleware/package.json
Normal file
23
examples/middleware/package.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "@example/middleware",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"server": "node dist/server/entry.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"svelte": "^3.48.0",
|
||||
"@astrojs/node": "workspace:*",
|
||||
"concurrently": "^7.2.1",
|
||||
"unocss": "^0.15.6",
|
||||
"vite-imagetools": "^4.0.4",
|
||||
"html-minifier": "^4.0.0"
|
||||
}
|
||||
}
|
63
examples/middleware/src/components/Card.astro
Normal file
63
examples/middleware/src/components/Card.astro
Normal file
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
export interface Props {
|
||||
title: string;
|
||||
body: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
const { href, title, body } = Astro.props;
|
||||
---
|
||||
|
||||
<li class="link-card">
|
||||
<a href={href}>
|
||||
<h2>
|
||||
{title}
|
||||
<span>→</span>
|
||||
</h2>
|
||||
<p>
|
||||
{body}
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
<style>
|
||||
.link-card {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
padding: 0.25rem;
|
||||
background-color: white;
|
||||
background-image: none;
|
||||
background-size: 400%;
|
||||
border-radius: 0.6rem;
|
||||
background-position: 100%;
|
||||
transition: background-position 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.link-card > a {
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
line-height: 1.4;
|
||||
padding: 1rem 1.3rem;
|
||||
border-radius: 0.35rem;
|
||||
color: #111;
|
||||
background-color: white;
|
||||
opacity: 0.8;
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
transition: color 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
p {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
color: #444;
|
||||
}
|
||||
.link-card:is(:hover, :focus-within) {
|
||||
background-position: 0;
|
||||
background-image: var(--accent-gradient);
|
||||
}
|
||||
.link-card:is(:hover, :focus-within) h2 {
|
||||
color: rgb(var(--accent));
|
||||
}
|
||||
</style>
|
13
examples/middleware/src/env.d.ts
vendored
Normal file
13
examples/middleware/src/env.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
/// <reference types="astro/client" />
|
||||
declare global {
|
||||
namespace AstroMiddleware {
|
||||
interface Locals {
|
||||
user: {
|
||||
name: string;
|
||||
surname: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
35
examples/middleware/src/layouts/Layout.astro
Normal file
35
examples/middleware/src/layouts/Layout.astro
Normal file
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
export interface Props {
|
||||
title: string;
|
||||
}
|
||||
|
||||
const { title } = Astro.props;
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
<style is:global>
|
||||
:root {
|
||||
--accent: 124, 58, 237;
|
||||
--accent-gradient: linear-gradient(45deg, rgb(var(--accent)), #da62c4 30%, white 60%);
|
||||
}
|
||||
html {
|
||||
font-family: system-ui, sans-serif;
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
code {
|
||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||
}
|
||||
</style>
|
71
examples/middleware/src/middleware.ts
Normal file
71
examples/middleware/src/middleware.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { defineMiddleware, sequence } from 'astro/middleware';
|
||||
import htmlMinifier from 'html-minifier';
|
||||
|
||||
const limit = 50;
|
||||
|
||||
const loginInfo = {
|
||||
token: undefined,
|
||||
currentTime: undefined,
|
||||
};
|
||||
|
||||
export const minifier = defineMiddleware(async (context, next) => {
|
||||
const response = await next();
|
||||
// check if the response is returning some HTML
|
||||
if (response.headers.get('content-type') === 'text/html') {
|
||||
let headers = response.headers;
|
||||
let html = await response.text();
|
||||
let newHtml = htmlMinifier.minify(html, {
|
||||
removeAttributeQuotes: true,
|
||||
collapseWhitespace: true,
|
||||
});
|
||||
return new Response(newHtml, {
|
||||
status: 200,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
return response;
|
||||
});
|
||||
|
||||
const validation = defineMiddleware(async (context, next) => {
|
||||
if (context.request.url.endsWith('/admin')) {
|
||||
if (loginInfo.currentTime) {
|
||||
const difference = new Date().getTime() - loginInfo.currentTime;
|
||||
if (difference > limit) {
|
||||
console.log('hit threshold');
|
||||
loginInfo.token = undefined;
|
||||
loginInfo.currentTime = undefined;
|
||||
return context.redirect('/login');
|
||||
}
|
||||
}
|
||||
// we naively check if we have a token
|
||||
if (loginInfo.token && loginInfo.token === 'loggedIn') {
|
||||
// we fill the locals with user-facing information
|
||||
context.locals.user = {
|
||||
name: 'AstroUser',
|
||||
surname: 'AstroSurname',
|
||||
};
|
||||
return await next();
|
||||
} else {
|
||||
loginInfo.token = undefined;
|
||||
loginInfo.currentTime = undefined;
|
||||
return context.redirect('/login');
|
||||
}
|
||||
} else if (context.request.url.endsWith('/api/login')) {
|
||||
const response = await next();
|
||||
// the login endpoint will return to us a JSON with username and password
|
||||
const data = await response.json();
|
||||
// we naively check if username and password are equals to some string
|
||||
if (data.username === 'astro' && data.password === 'astro') {
|
||||
// we store the token somewhere outside of locals because the `locals` object is attached to the request
|
||||
// and when doing a redirect, we lose that information
|
||||
loginInfo.token = 'loggedIn';
|
||||
loginInfo.currentTime = new Date().getTime();
|
||||
return context.redirect('/admin');
|
||||
}
|
||||
}
|
||||
// we don't really care about awaiting the response in this case
|
||||
next();
|
||||
return;
|
||||
});
|
||||
|
||||
export const onRequest = sequence(validation, minifier);
|
55
examples/middleware/src/pages/admin.astro
Normal file
55
examples/middleware/src/pages/admin.astro
Normal file
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
const user = Astro.locals.user;
|
||||
---
|
||||
|
||||
<Layout title="Welcome back!!">
|
||||
<main>
|
||||
<h1>Welcome back <span class="text-gradient">{user.name} {user.surname}</span></h1>
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
main {
|
||||
margin: auto;
|
||||
padding: 1.5rem;
|
||||
max-width: 60ch;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
}
|
||||
.text-gradient {
|
||||
background-image: var(--accent-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-size: 400%;
|
||||
background-position: 0%;
|
||||
}
|
||||
.instructions {
|
||||
line-height: 1.6;
|
||||
margin: 1rem 0;
|
||||
border: 1px solid rgba(var(--accent), 25%);
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
.instructions code {
|
||||
font-size: 0.875em;
|
||||
font-weight: bold;
|
||||
background: rgba(var(--accent), 12%);
|
||||
color: rgb(var(--accent));
|
||||
border-radius: 4px;
|
||||
padding: 0.3em 0.45em;
|
||||
}
|
||||
.instructions strong {
|
||||
color: rgb(var(--accent));
|
||||
}
|
||||
.link-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(24ch, 1fr));
|
||||
gap: 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
18
examples/middleware/src/pages/api/login.ts
Normal file
18
examples/middleware/src/pages/api/login.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { APIRoute } from 'astro';
|
||||
|
||||
export const post: APIRoute = async ({ request }) => {
|
||||
const data = await request.formData();
|
||||
const username = data.get('username');
|
||||
const password = data.get('password');
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
username,
|
||||
password,
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
63
examples/middleware/src/pages/index.astro
Normal file
63
examples/middleware/src/pages/index.astro
Normal file
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import Card from '../components/Card.astro';
|
||||
---
|
||||
|
||||
<Layout title="Welcome to Astro.">
|
||||
<main>
|
||||
<h1>Welcome to <span class="text-gradient">Astro</span></h1>
|
||||
<p class="instructions">
|
||||
To get started, open the directory <code>src/pages</code> in your project.<br />
|
||||
<strong>Code Challenge:</strong> Tweak the "Welcome to Astro" message above.
|
||||
</p>
|
||||
{}
|
||||
<ul role="list" class="link-card-grid">
|
||||
<Card href="/login" title="Login" body="Try the login" />
|
||||
</ul>
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
main {
|
||||
margin: auto;
|
||||
padding: 1.5rem;
|
||||
max-width: 60ch;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
}
|
||||
.text-gradient {
|
||||
background-image: var(--accent-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-size: 400%;
|
||||
background-position: 0%;
|
||||
}
|
||||
.instructions {
|
||||
line-height: 1.6;
|
||||
margin: 1rem 0;
|
||||
border: 1px solid rgba(var(--accent), 25%);
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
.instructions code {
|
||||
font-size: 0.875em;
|
||||
font-weight: bold;
|
||||
background: rgba(var(--accent), 12%);
|
||||
color: rgb(var(--accent));
|
||||
border-radius: 4px;
|
||||
padding: 0.3em 0.45em;
|
||||
}
|
||||
.instructions strong {
|
||||
color: rgb(var(--accent));
|
||||
}
|
||||
.link-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(24ch, 1fr));
|
||||
gap: 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
75
examples/middleware/src/pages/login.astro
Normal file
75
examples/middleware/src/pages/login.astro
Normal file
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
|
||||
const status = Astro.response.status;
|
||||
let redirectMessage;
|
||||
if (status === 301) {
|
||||
redirectMessage = 'Your session is finished, please login again';
|
||||
}
|
||||
---
|
||||
|
||||
<Layout title="Welcome to Astro.">
|
||||
<main>
|
||||
<h1>Welcome to <span class="text-gradient">Astro</span></h1>
|
||||
<p class="instructions">
|
||||
To get started, open the directory <code>src/pages</code> in your project.<br />
|
||||
<strong>Code Challenge:</strong> Tweak the "Welcome to Astro" message above.
|
||||
</p>
|
||||
{redirectMessage}
|
||||
<form action="/api/login" method="POST">
|
||||
<label>
|
||||
Username: <input type="text" minlength="1" id="username" name="username" />
|
||||
</label>
|
||||
<label>
|
||||
Password: <input type="password" minlength="1" id="password" name="password" />
|
||||
</label>
|
||||
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
main {
|
||||
margin: auto;
|
||||
padding: 1.5rem;
|
||||
max-width: 60ch;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
}
|
||||
.text-gradient {
|
||||
background-image: var(--accent-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-size: 400%;
|
||||
background-position: 0%;
|
||||
}
|
||||
.instructions {
|
||||
line-height: 1.6;
|
||||
margin: 1rem 0;
|
||||
border: 1px solid rgba(var(--accent), 25%);
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
.instructions code {
|
||||
font-size: 0.875em;
|
||||
font-weight: bold;
|
||||
background: rgba(var(--accent), 12%);
|
||||
color: rgb(var(--accent));
|
||||
border-radius: 4px;
|
||||
padding: 0.3em 0.45em;
|
||||
}
|
||||
.instructions strong {
|
||||
color: rgb(var(--accent));
|
||||
}
|
||||
.link-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(24ch, 1fr));
|
||||
gap: 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
3
examples/middleware/tsconfig.json
Normal file
3
examples/middleware/tsconfig.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/base"
|
||||
}
|
|
@ -88,7 +88,7 @@
|
|||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"only-allow": "^1.1.1",
|
||||
"organize-imports-cli": "^0.10.0",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-astro": "^0.8.0",
|
||||
"tiny-glob": "^0.2.9",
|
||||
"turbo": "^1.9.3",
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
# @astrojs/rss
|
||||
|
||||
## 2.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6970](https://github.com/withastro/astro/pull/6970) [`b5482cee2`](https://github.com/withastro/astro/commit/b5482cee2387149ff397447e546130ba3dea58db) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fix: remove accidental stripping of trailing `/1/` on canonical URLs
|
||||
|
||||
## 2.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@astrojs/rss",
|
||||
"description": "Add RSS feeds to your Astro projects",
|
||||
"version": "2.4.0",
|
||||
"version": "2.4.1",
|
||||
"type": "module",
|
||||
"types": "./dist/index.d.ts",
|
||||
"author": "withastro",
|
||||
|
|
|
@ -8,7 +8,6 @@ export function createCanonicalURL(
|
|||
base?: string
|
||||
): URL {
|
||||
let pathname = url.replace(/\/index.html$/, ''); // index.html is not canonical
|
||||
pathname = pathname.replace(/\/1\/?$/, ''); // neither is a trailing /1/ (impl. detail of collections)
|
||||
if (trailingSlash === false) {
|
||||
// remove the trailing slash
|
||||
pathname = pathname.replace(/(\/+)?$/, '');
|
||||
|
|
|
@ -1,5 +1,39 @@
|
|||
# astro
|
||||
|
||||
## 2.3.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6967](https://github.com/withastro/astro/pull/6967) [`a8a319aef`](https://github.com/withastro/astro/commit/a8a319aef744a64647ee16c7d558d74de6864c6c) Thanks [@bluwy](https://github.com/bluwy)! - Fix `astro-entry` error on build with multiple JSX frameworks
|
||||
|
||||
- [#6961](https://github.com/withastro/astro/pull/6961) [`a695e44ae`](https://github.com/withastro/astro/commit/a695e44aed6e2f5d32cb950d4237be6e5657ba98) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fix getImage type
|
||||
|
||||
- [#6956](https://github.com/withastro/astro/pull/6956) [`367e61776`](https://github.com/withastro/astro/commit/367e61776196a17d61c28daa4dfbabb6244e040c) Thanks [@lilnasy](https://github.com/lilnasy)! - Changed where various parts of the build pipeline look to decide if a page should be prerendered. They now exclusively consider PageBuildData, allowing integrations to participate in the decision.
|
||||
|
||||
- [#6969](https://github.com/withastro/astro/pull/6969) [`77270cc2c`](https://github.com/withastro/astro/commit/77270cc2cd06c942d7abf1d882e36d9163edafa5) Thanks [@bluwy](https://github.com/bluwy)! - Avoid removing leading slash for `build.assetsPrefix` value in the build output
|
||||
|
||||
- [#6910](https://github.com/withastro/astro/pull/6910) [`895fa07d8`](https://github.com/withastro/astro/commit/895fa07d8b4b8359984e048daca5437e40f44390) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Inline `process.env` boolean values (`0`, `1`, `true`, `false`) during the build. This helps with DCE and allows for better `export const prerender` detection.
|
||||
|
||||
- [#6958](https://github.com/withastro/astro/pull/6958) [`72c6bf01f`](https://github.com/withastro/astro/commit/72c6bf01fe49b331ca8ad9206a7506b15caf5b8d) Thanks [@bluwy](https://github.com/bluwy)! - Fix content render imports flow
|
||||
|
||||
- [#6952](https://github.com/withastro/astro/pull/6952) [`e5bd084c0`](https://github.com/withastro/astro/commit/e5bd084c01e4f60a157969b50c05ce002f7b63d2) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Update allowed Sharp versions to support 0.32.0
|
||||
|
||||
## 2.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6940](https://github.com/withastro/astro/pull/6940) [`a98df9374`](https://github.com/withastro/astro/commit/a98df9374dec65c678fa47319cb1481b1af123e2) Thanks [@delucis](https://github.com/delucis)! - Support custom 404s added via `injectRoute` or as `src/pages/404.html`
|
||||
|
||||
- [#6948](https://github.com/withastro/astro/pull/6948) [`50975f2ea`](https://github.com/withastro/astro/commit/50975f2ea3a59f9e023cc631a9372c0c7986eec9) Thanks [@imchell](https://github.com/imchell)! - Placeholders for slots are cleaned in HTML String that is rendered
|
||||
|
||||
- [#6848](https://github.com/withastro/astro/pull/6848) [`ebae1eaf8`](https://github.com/withastro/astro/commit/ebae1eaf87f49399036033c673b513338f7d9c42) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Update `experimental.assets`'s `image.service` configuration to allow for a config option in addition to an entrypoint
|
||||
|
||||
- [#6953](https://github.com/withastro/astro/pull/6953) [`dc062f669`](https://github.com/withastro/astro/commit/dc062f6695ce577dc569781fc0678c903012c336) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Update `astro check` to use version 1.0.0 of the Astro language server
|
||||
|
||||
- Updated dependencies [[`ac57b5549`](https://github.com/withastro/astro/commit/ac57b5549f828a17bdbebdaca7ace075307a3c9d)]:
|
||||
- @astrojs/telemetry@2.1.1
|
||||
- @astrojs/webapi@2.1.1
|
||||
|
||||
## 2.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
|
27
packages/astro/client-base.d.ts
vendored
27
packages/astro/client-base.d.ts
vendored
|
@ -3,7 +3,26 @@
|
|||
declare module 'astro:assets' {
|
||||
// Exporting things one by one is a bit cumbersome, not sure if there's a better way - erika, 2023-02-03
|
||||
type AstroAssets = {
|
||||
getImage: typeof import('./dist/assets/index.js').getImage;
|
||||
// getImage's type here is different from the internal function since the Vite module implicitly pass the service config
|
||||
/**
|
||||
* Get an optimized image and the necessary attributes to render it.
|
||||
*
|
||||
* **Example**
|
||||
* ```astro
|
||||
* ---
|
||||
* import { getImage } from 'astro:assets';
|
||||
* import originalImage from '../assets/image.png';
|
||||
*
|
||||
* const optimizedImage = await getImage({src: originalImage, width: 1280 });
|
||||
* ---
|
||||
* <img src={optimizedImage.src} {...optimizedImage.attributes} />
|
||||
* ```
|
||||
*
|
||||
* This is functionally equivalent to using the `<Image />` component, as the component calls this function internally.
|
||||
*/
|
||||
getImage: (
|
||||
options: import('./dist/assets/types.js').ImageTransform
|
||||
) => Promise<import('./dist/assets/types.js').GetImageResult>;
|
||||
getConfiguredImageService: typeof import('./dist/assets/index.js').getConfiguredImageService;
|
||||
Image: typeof import('./components/Image.astro').default;
|
||||
};
|
||||
|
@ -368,3 +387,9 @@ declare module '*?inline' {
|
|||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace App {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface Locals {}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
import type * as shiki from 'shiki';
|
||||
import { renderToHtml } from 'shiki';
|
||||
import { getHighlighter } from './Shiki.js';
|
||||
|
||||
export interface Props {
|
||||
|
@ -30,36 +31,60 @@ export interface Props {
|
|||
* @default false
|
||||
*/
|
||||
wrap?: boolean | null;
|
||||
/**
|
||||
* Generate inline code element only, without the pre element wrapper.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
inline?: boolean;
|
||||
}
|
||||
|
||||
const { code, lang = 'plaintext', theme = 'github-dark', wrap = false } = Astro.props;
|
||||
|
||||
/** Replace the shiki class name with a custom astro class name. */
|
||||
function repairShikiTheme(html: string): string {
|
||||
// Replace "shiki" class naming with "astro"
|
||||
html = html.replace(/<pre class="(.*?)shiki(.*?)"/, '<pre class="$1astro-code$2"');
|
||||
// Handle code wrapping
|
||||
// if wrap=null, do nothing.
|
||||
if (wrap === false) {
|
||||
html = html.replace(/style="(.*?)"/, 'style="$1; overflow-x: auto;"');
|
||||
} else if (wrap === true) {
|
||||
html = html.replace(
|
||||
/style="(.*?)"/,
|
||||
'style="$1; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;"'
|
||||
);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
const {
|
||||
code,
|
||||
lang = 'plaintext',
|
||||
theme = 'github-dark',
|
||||
wrap = false,
|
||||
inline = false,
|
||||
} = Astro.props;
|
||||
|
||||
// 1. Get the shiki syntax highlighter
|
||||
const highlighter = await getHighlighter({
|
||||
theme,
|
||||
// Load custom lang if passed an object, otherwise load the default
|
||||
langs: typeof lang !== 'string' ? [lang] : undefined,
|
||||
});
|
||||
const _html = highlighter.codeToHtml(code, {
|
||||
lang: typeof lang === 'string' ? lang : lang.id,
|
||||
|
||||
// 2. Turn code into shiki theme tokens
|
||||
const tokens = highlighter.codeToThemedTokens(code, typeof lang === 'string' ? lang : lang.id);
|
||||
|
||||
// 3. Get shiki theme object
|
||||
const _theme = highlighter.getTheme();
|
||||
|
||||
// 4. Render the theme tokens as html
|
||||
const html = renderToHtml(tokens, {
|
||||
themeName: _theme.name,
|
||||
fg: _theme.fg,
|
||||
bg: _theme.bg,
|
||||
elements: {
|
||||
pre({ className, style, children }) {
|
||||
// Swap to `code` tag if inline
|
||||
const tag = inline ? 'code' : 'pre';
|
||||
// Replace "shiki" class naming with "astro-code"
|
||||
className = className.replace(/shiki/g, 'astro-code');
|
||||
// Handle code wrapping
|
||||
// if wrap=null, do nothing.
|
||||
if (wrap === false) {
|
||||
style += '; overflow-x: auto;"';
|
||||
} else if (wrap === true) {
|
||||
style += '; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;"';
|
||||
}
|
||||
return `<${tag} class="${className}" style="${style}" tabindex="0">${children}</${tag}>`;
|
||||
},
|
||||
code({ children }) {
|
||||
return inline ? children : `<code>${children}</code>`;
|
||||
},
|
||||
},
|
||||
});
|
||||
const html = repairShikiTheme(_html);
|
||||
---
|
||||
|
||||
<Fragment set:html={html} />
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,33 +1,43 @@
|
|||
/**
|
||||
* This file is prebuilt from packages/astro/scripts/shiki-gen-themes.mjs
|
||||
* Do not edit this directly, but instead edit that file and rerun it to generate this file.
|
||||
*/
|
||||
|
||||
// prettier-ignore
|
||||
export const themes = {
|
||||
'css-variables': () => import('shiki/themes/css-variables.json').then((mod) => mod.default),
|
||||
'dark-plus': () => import('shiki/themes/dark-plus.json').then((mod) => mod.default),
|
||||
'dracula-soft': () => import('shiki/themes/dracula-soft.json').then((mod) => mod.default),
|
||||
dracula: () => import('shiki/themes/dracula.json').then((mod) => mod.default),
|
||||
'github-dark-dimmed': () =>
|
||||
import('shiki/themes/github-dark-dimmed.json').then((mod) => mod.default),
|
||||
'github-dark': () => import('shiki/themes/github-dark.json').then((mod) => mod.default),
|
||||
'github-light': () => import('shiki/themes/github-light.json').then((mod) => mod.default),
|
||||
hc_light: () => import('shiki/themes/hc_light.json').then((mod) => mod.default),
|
||||
'light-plus': () => import('shiki/themes/light-plus.json').then((mod) => mod.default),
|
||||
'material-darker': () => import('shiki/themes/material-darker.json').then((mod) => mod.default),
|
||||
'material-default': () => import('shiki/themes/material-default.json').then((mod) => mod.default),
|
||||
'material-lighter': () => import('shiki/themes/material-lighter.json').then((mod) => mod.default),
|
||||
'material-ocean': () => import('shiki/themes/material-ocean.json').then((mod) => mod.default),
|
||||
'material-palenight': () =>
|
||||
import('shiki/themes/material-palenight.json').then((mod) => mod.default),
|
||||
'min-dark': () => import('shiki/themes/min-dark.json').then((mod) => mod.default),
|
||||
'min-light': () => import('shiki/themes/min-light.json').then((mod) => mod.default),
|
||||
monokai: () => import('shiki/themes/monokai.json').then((mod) => mod.default),
|
||||
nord: () => import('shiki/themes/nord.json').then((mod) => mod.default),
|
||||
'one-dark-pro': () => import('shiki/themes/one-dark-pro.json').then((mod) => mod.default),
|
||||
poimandres: () => import('shiki/themes/poimandres.json').then((mod) => mod.default),
|
||||
'rose-pine-dawn': () => import('shiki/themes/rose-pine-dawn.json').then((mod) => mod.default),
|
||||
'rose-pine-moon': () => import('shiki/themes/rose-pine-moon.json').then((mod) => mod.default),
|
||||
'rose-pine': () => import('shiki/themes/rose-pine.json').then((mod) => mod.default),
|
||||
'slack-dark': () => import('shiki/themes/slack-dark.json').then((mod) => mod.default),
|
||||
'slack-ochin': () => import('shiki/themes/slack-ochin.json').then((mod) => mod.default),
|
||||
'solarized-dark': () => import('shiki/themes/solarized-dark.json').then((mod) => mod.default),
|
||||
'solarized-light': () => import('shiki/themes/solarized-light.json').then((mod) => mod.default),
|
||||
'vitesse-dark': () => import('shiki/themes/vitesse-dark.json').then((mod) => mod.default),
|
||||
'vitesse-light': () => import('shiki/themes/vitesse-light.json').then((mod) => mod.default),
|
||||
'css-variables': () => import('shiki/themes/css-variables.json').then(mod => mod.default),
|
||||
'dark-plus': () => import('shiki/themes/dark-plus.json').then(mod => mod.default),
|
||||
'dracula-soft': () => import('shiki/themes/dracula-soft.json').then(mod => mod.default),
|
||||
'dracula': () => import('shiki/themes/dracula.json').then(mod => mod.default),
|
||||
'github-dark-dimmed': () => import('shiki/themes/github-dark-dimmed.json').then(mod => mod.default),
|
||||
'github-dark': () => import('shiki/themes/github-dark.json').then(mod => mod.default),
|
||||
'github-light': () => import('shiki/themes/github-light.json').then(mod => mod.default),
|
||||
'hc_light': () => import('shiki/themes/hc_light.json').then(mod => mod.default),
|
||||
'light-plus': () => import('shiki/themes/light-plus.json').then(mod => mod.default),
|
||||
'material-theme-darker': () => import('shiki/themes/material-theme-darker.json').then(mod => mod.default),
|
||||
'material-theme-lighter': () => import('shiki/themes/material-theme-lighter.json').then(mod => mod.default),
|
||||
'material-theme-ocean': () => import('shiki/themes/material-theme-ocean.json').then(mod => mod.default),
|
||||
'material-theme-palenight': () => import('shiki/themes/material-theme-palenight.json').then(mod => mod.default),
|
||||
'material-theme': () => import('shiki/themes/material-theme.json').then(mod => mod.default),
|
||||
'min-dark': () => import('shiki/themes/min-dark.json').then(mod => mod.default),
|
||||
'min-light': () => import('shiki/themes/min-light.json').then(mod => mod.default),
|
||||
'monokai': () => import('shiki/themes/monokai.json').then(mod => mod.default),
|
||||
'nord': () => import('shiki/themes/nord.json').then(mod => mod.default),
|
||||
'one-dark-pro': () => import('shiki/themes/one-dark-pro.json').then(mod => mod.default),
|
||||
'poimandres': () => import('shiki/themes/poimandres.json').then(mod => mod.default),
|
||||
'rose-pine-dawn': () => import('shiki/themes/rose-pine-dawn.json').then(mod => mod.default),
|
||||
'rose-pine-moon': () => import('shiki/themes/rose-pine-moon.json').then(mod => mod.default),
|
||||
'rose-pine': () => import('shiki/themes/rose-pine.json').then(mod => mod.default),
|
||||
'slack-dark': () => import('shiki/themes/slack-dark.json').then(mod => mod.default),
|
||||
'slack-ochin': () => import('shiki/themes/slack-ochin.json').then(mod => mod.default),
|
||||
'solarized-dark': () => import('shiki/themes/solarized-dark.json').then(mod => mod.default),
|
||||
'solarized-light': () => import('shiki/themes/solarized-light.json').then(mod => mod.default),
|
||||
'vitesse-dark': () => import('shiki/themes/vitesse-dark.json').then(mod => mod.default),
|
||||
'vitesse-light': () => import('shiki/themes/vitesse-light.json').then(mod => mod.default),
|
||||
// old theme names for compat
|
||||
'material-darker': () => import('shiki/themes/material-theme-darker').then(mod => mod.default),
|
||||
'material-default': () => import('shiki/themes/material-theme').then(mod => mod.default),
|
||||
'material-lighter': () => import('shiki/themes/material-theme-lighter').then(mod => mod.default),
|
||||
'material-ocean': () => import('shiki/themes/material-theme-ocean').then(mod => mod.default),
|
||||
'material-palenight': () => import('shiki/themes/material-theme-palenight').then(mod => mod.default),
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "astro",
|
||||
"version": "2.3.2",
|
||||
"version": "2.3.4",
|
||||
"description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
|
||||
"type": "module",
|
||||
"author": "withastro",
|
||||
|
@ -64,6 +64,10 @@
|
|||
"./zod": {
|
||||
"types": "./zod.d.ts",
|
||||
"default": "./zod.mjs"
|
||||
},
|
||||
"./middleware": {
|
||||
"types": "./dist/core/middleware/index.d.ts",
|
||||
"default": "./dist/core/middleware/index.js"
|
||||
}
|
||||
},
|
||||
"imports": {
|
||||
|
@ -106,11 +110,11 @@
|
|||
"test:e2e:match": "playwright test -g"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/compiler": "^1.3.1",
|
||||
"@astrojs/language-server": "^0.28.3",
|
||||
"@astrojs/compiler": "^1.4.0",
|
||||
"@astrojs/language-server": "^1.0.0",
|
||||
"@astrojs/markdown-remark": "^2.1.4",
|
||||
"@astrojs/telemetry": "^2.1.0",
|
||||
"@astrojs/webapi": "^2.1.0",
|
||||
"@astrojs/telemetry": "^2.1.1",
|
||||
"@astrojs/webapi": "^2.1.1",
|
||||
"@babel/core": "^7.18.2",
|
||||
"@babel/generator": "^7.18.2",
|
||||
"@babel/parser": "^7.18.4",
|
||||
|
@ -119,7 +123,7 @@
|
|||
"@babel/types": "^7.18.4",
|
||||
"@types/babel__core": "^7.1.19",
|
||||
"@types/yargs-parser": "^21.0.0",
|
||||
"acorn": "^8.8.1",
|
||||
"acorn": "^8.8.2",
|
||||
"boxen": "^6.2.1",
|
||||
"chokidar": "^3.5.3",
|
||||
"ci-info": "^3.3.1",
|
||||
|
@ -130,7 +134,7 @@
|
|||
"devalue": "^4.2.0",
|
||||
"diff": "^5.1.0",
|
||||
"es-module-lexer": "^1.1.0",
|
||||
"estree-walker": "^3.0.1",
|
||||
"estree-walker": "3.0.0",
|
||||
"execa": "^6.1.0",
|
||||
"fast-glob": "^3.2.11",
|
||||
"github-slugger": "^2.0.0",
|
||||
|
@ -146,7 +150,7 @@
|
|||
"rehype": "^12.0.1",
|
||||
"semver": "^7.3.8",
|
||||
"server-destroy": "^1.0.1",
|
||||
"shiki": "^0.11.1",
|
||||
"shiki": "^0.14.1",
|
||||
"slash": "^4.0.0",
|
||||
"string-width": "^5.1.2",
|
||||
"strip-ansi": "^7.0.1",
|
||||
|
@ -181,7 +185,6 @@
|
|||
"@types/rimraf": "^3.0.2",
|
||||
"@types/send": "^0.17.1",
|
||||
"@types/server-destroy": "^1.0.1",
|
||||
"@types/sharp": "^0.31.1",
|
||||
"@types/unist": "^2.0.6",
|
||||
"astro-scripts": "workspace:*",
|
||||
"chai": "^4.3.6",
|
||||
|
@ -196,13 +199,13 @@
|
|||
"remark-code-titles": "^0.1.2",
|
||||
"rollup": "^3.9.0",
|
||||
"sass": "^1.52.2",
|
||||
"sharp": "^0.31.3",
|
||||
"sharp": "^0.32.1",
|
||||
"srcset-parse": "^1.1.0",
|
||||
"undici": "^5.22.0",
|
||||
"unified": "^10.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"sharp": "^0.31.3"
|
||||
"sharp": ">=0.31.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"sharp": {
|
||||
|
|
|
@ -4,31 +4,34 @@ const dir = await fs.promises.readdir('packages/astro/node_modules/shiki/languag
|
|||
|
||||
const langImports = dir.map((f) => {
|
||||
const key = f.slice(0, f.indexOf('.tmLanguage.json'));
|
||||
return [
|
||||
key,
|
||||
`import('shiki/languages/${f}').then(mod => mod.default).then(grammar => {
|
||||
const lang = BUNDLED_LANGUAGES.find(l => l.id === '${key}');
|
||||
if(lang) {
|
||||
return [key, `import('shiki/languages/${f}').then((mod) => handleLang(mod.default, '${key}'))`];
|
||||
});
|
||||
|
||||
let code = `\
|
||||
/**
|
||||
* This file is prebuilt from packages/astro/scripts/shiki-gen-languages.mjs
|
||||
* Do not edit this directly, but instead edit that file and rerun it to generate this file.
|
||||
*/
|
||||
|
||||
import { BUNDLED_LANGUAGES } from 'shiki';
|
||||
|
||||
function handleLang(grammar, language) {
|
||||
const lang = BUNDLED_LANGUAGES.find((l) => l.id === language);
|
||||
if (lang) {
|
||||
return {
|
||||
...lang,
|
||||
grammar
|
||||
grammar,
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
})`,
|
||||
];
|
||||
});
|
||||
let code = `import { BUNDLED_LANGUAGES } from 'shiki';
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
export const languages = {`;
|
||||
let i = 0;
|
||||
|
||||
for (const [key, imp] of langImports) {
|
||||
if (i > 0) {
|
||||
code += ',';
|
||||
}
|
||||
code += `\n\t'${key}': () => ${imp}`;
|
||||
i++;
|
||||
code += `\n\t'${key}': () => ${imp},`;
|
||||
}
|
||||
code += '\n};';
|
||||
|
||||
|
|
|
@ -2,18 +2,36 @@ import fs from 'fs';
|
|||
|
||||
const dir = await fs.promises.readdir('packages/astro/node_modules/shiki/themes/');
|
||||
|
||||
const toThemeImport = (theme) => `import('shiki/themes/${theme}').then(mod => mod.default)`;
|
||||
|
||||
const themeImports = dir.map((f) => {
|
||||
return [f.slice(0, f.indexOf('.json')), `import('shiki/themes/${f}').then(mod => mod.default)`];
|
||||
return [f.slice(0, f.indexOf('.json')), toThemeImport(f)];
|
||||
});
|
||||
|
||||
let code = `export const themes = {`;
|
||||
let i = 0;
|
||||
// Map of old theme names to new names to preserve compatibility when we upgrade shiki
|
||||
const compatThemes = {
|
||||
'material-darker': 'material-theme-darker',
|
||||
'material-default': 'material-theme',
|
||||
'material-lighter': 'material-theme-lighter',
|
||||
'material-ocean': 'material-theme-ocean',
|
||||
'material-palenight': 'material-theme-palenight',
|
||||
};
|
||||
|
||||
let code = `\
|
||||
/**
|
||||
* This file is prebuilt from packages/astro/scripts/shiki-gen-themes.mjs
|
||||
* Do not edit this directly, but instead edit that file and rerun it to generate this file.
|
||||
*/
|
||||
|
||||
// prettier-ignore
|
||||
export const themes = {`;
|
||||
|
||||
for (const [key, imp] of themeImports) {
|
||||
if (i > 0) {
|
||||
code += ',';
|
||||
}
|
||||
code += `\n\t'${key}': () => ${imp}`;
|
||||
i++;
|
||||
code += `\n\t'${key}': () => ${imp},`;
|
||||
}
|
||||
code += `\n\t// old theme names for compat`;
|
||||
for (const oldName in compatThemes) {
|
||||
code += `\n\t'${oldName}': () => ${toThemeImport(compatThemes[oldName])},`;
|
||||
}
|
||||
code += '\n};';
|
||||
|
||||
|
|
9
packages/astro/src/@types/app.d.ts
vendored
Normal file
9
packages/astro/src/@types/app.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* Shared interfaces throughout the application that can be overridden by the user.
|
||||
*/
|
||||
declare namespace App {
|
||||
/**
|
||||
* Used by middlewares to store information, that can be read by the user via the global `Astro.locals`
|
||||
*/
|
||||
interface Locals {}
|
||||
}
|
|
@ -103,6 +103,7 @@ export interface CLIFlags {
|
|||
drafts?: boolean;
|
||||
open?: boolean;
|
||||
experimentalAssets?: boolean;
|
||||
experimentalMiddleware?: boolean;
|
||||
}
|
||||
|
||||
export interface BuildConfig {
|
||||
|
@ -511,6 +512,23 @@ export interface AstroUserConfig {
|
|||
*/
|
||||
trailingSlash?: 'always' | 'never' | 'ignore';
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @name scopedStyleStrategy
|
||||
* @type {('where' | 'class')}
|
||||
* @default `'where'`
|
||||
* @description
|
||||
* @version 2.4
|
||||
*
|
||||
* Specify the strategy used for scoping styles within Astro components. Choose from:
|
||||
* - `'where'` - Use `:where` selectors, causing no specifity increase.
|
||||
* - `'class'` - Use class-based selectors, causing a +1 specifity increase.
|
||||
*
|
||||
* Using `'class'` is helpful when you want to ensure that element selectors within an Astro component override global style defaults (e.g. from a global stylesheet).
|
||||
* Using `'where'` gives you more control over specifity, but requires that you use higher-specifity selectors, layers, and other tools to control which selectors are applied.
|
||||
*/
|
||||
scopedStyleStrategy?: 'where' | 'class';
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @name adapter
|
||||
|
@ -1034,6 +1052,46 @@ export interface AstroUserConfig {
|
|||
* }
|
||||
*/
|
||||
assets?: boolean;
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.inlineStylesheets
|
||||
* @type {('always' | 'auto' | 'never')}
|
||||
* @default `never`
|
||||
* @description
|
||||
* Control whether styles are sent to the browser in a separate css file or inlined into <style> tags. Choose from the following options:
|
||||
* - `'always'` - all styles are inlined into <style> tags
|
||||
* - `'auto'` - only stylesheets smaller than `ViteConfig.build.assetsInlineLimit` (default: 4kb) are inlined. Otherwise, styles are sent in external stylesheets.
|
||||
* - `'never'` - all styles are sent in external stylesheets
|
||||
*
|
||||
* ```js
|
||||
* {
|
||||
* experimental: {
|
||||
* inlineStylesheets: `auto`,
|
||||
* },
|
||||
* }
|
||||
*/
|
||||
inlineStylesheets?: 'always' | 'auto' | 'never';
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.middleware
|
||||
* @type {boolean}
|
||||
* @default `false`
|
||||
* @version 2.4.0
|
||||
* @description
|
||||
* Enable experimental support for Astro middleware.
|
||||
*
|
||||
* To enable this feature, set `experimental.middleware` to `true` in your Astro config:
|
||||
*
|
||||
* ```js
|
||||
* {
|
||||
* experimental: {
|
||||
* middleware: true,
|
||||
* },
|
||||
* }
|
||||
*/
|
||||
middleware?: boolean;
|
||||
};
|
||||
|
||||
// Legacy options to be removed
|
||||
|
@ -1431,6 +1489,11 @@ interface AstroSharedContext<Props extends Record<string, any> = Record<string,
|
|||
* Redirect to another page (**SSR Only**).
|
||||
*/
|
||||
redirect(path: string, status?: 301 | 302 | 303 | 307 | 308): Response;
|
||||
|
||||
/**
|
||||
* Object accessed via Astro middleware
|
||||
*/
|
||||
locals: App.Locals;
|
||||
}
|
||||
|
||||
export interface APIContext<Props extends Record<string, any> = Record<string, any>>
|
||||
|
@ -1464,7 +1527,7 @@ export interface APIContext<Props extends Record<string, any> = Record<string, a
|
|||
* }
|
||||
* ```
|
||||
*
|
||||
* [context reference](https://docs.astro.build/en/guides/api-reference/#contextparams)
|
||||
* [context reference](https://docs.astro.build/en/reference/api-reference/#contextparams)
|
||||
*/
|
||||
params: AstroSharedContext['params'];
|
||||
/**
|
||||
|
@ -1504,6 +1567,31 @@ export interface APIContext<Props extends Record<string, any> = Record<string, a
|
|||
* [context reference](https://docs.astro.build/en/guides/api-reference/#contextredirect)
|
||||
*/
|
||||
redirect: AstroSharedContext['redirect'];
|
||||
|
||||
/**
|
||||
* Object accessed via Astro middleware.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```ts
|
||||
* // src/middleware.ts
|
||||
* import {defineMiddleware} from "astro/middleware";
|
||||
*
|
||||
* export const onRequest = defineMiddleware((context, next) => {
|
||||
* context.locals.greeting = "Hello!";
|
||||
* next();
|
||||
* });
|
||||
* ```
|
||||
* Inside a `.astro` file:
|
||||
* ```astro
|
||||
* ---
|
||||
* // src/pages/index.astro
|
||||
* const greeting = Astro.locals.greeting;
|
||||
* ---
|
||||
* <h1>{greeting}</h1>
|
||||
* ```
|
||||
*/
|
||||
locals: App.Locals;
|
||||
}
|
||||
|
||||
export type Props = Record<string, unknown>;
|
||||
|
@ -1592,6 +1680,22 @@ export interface AstroIntegration {
|
|||
};
|
||||
}
|
||||
|
||||
export type MiddlewareNext<R> = () => Promise<R>;
|
||||
export type MiddlewareHandler<R> = (
|
||||
context: APIContext,
|
||||
next: MiddlewareNext<R>
|
||||
) => Promise<R> | Promise<void> | void;
|
||||
|
||||
export type MiddlewareResponseHandler = MiddlewareHandler<Response>;
|
||||
export type MiddlewareEndpointHandler = MiddlewareHandler<Response | EndpointOutput>;
|
||||
export type MiddlewareNextResponse = MiddlewareNext<Response>;
|
||||
|
||||
// NOTE: when updating this file with other functions,
|
||||
// remember to update `plugin-page.ts` too, to add that function as a no-op function.
|
||||
export type AstroMiddlewareInstance<R> = {
|
||||
onRequest?: MiddlewareHandler<R>;
|
||||
};
|
||||
|
||||
export interface AstroPluginOptions {
|
||||
settings: AstroSettings;
|
||||
logging: LogOptions;
|
||||
|
|
|
@ -29,22 +29,6 @@ export async function getConfiguredImageService(): Promise<ImageService> {
|
|||
return globalThis.astroAsset.imageService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an optimized image and the necessary attributes to render it.
|
||||
*
|
||||
* **Example**
|
||||
* ```astro
|
||||
* ---
|
||||
* import { getImage } from 'astro:assets';
|
||||
* import originalImage from '../assets/image.png';
|
||||
*
|
||||
* const optimizedImage = await getImage({src: originalImage, width: 1280 });
|
||||
* ---
|
||||
* <img src={optimizedImage.src} {...optimizedImage.attributes} />
|
||||
* ```
|
||||
*
|
||||
* This is functionally equivalent to using the `<Image />` component, as the component calls this function internally.
|
||||
*/
|
||||
export async function getImage(
|
||||
options: ImageTransform,
|
||||
serviceConfig: Record<string, any>
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
createHeadAndContent,
|
||||
renderComponent,
|
||||
renderScriptElement,
|
||||
renderStyleElement,
|
||||
renderTemplate,
|
||||
renderUniqueStylesheet,
|
||||
unescapeHTML,
|
||||
|
@ -152,13 +151,21 @@ async function render({
|
|||
links = '',
|
||||
scripts = '';
|
||||
if (Array.isArray(collectedStyles)) {
|
||||
styles = collectedStyles.map((style: any) => renderStyleElement(style)).join('');
|
||||
styles = collectedStyles
|
||||
.map((style: any) => {
|
||||
return renderUniqueStylesheet(result, {
|
||||
type: 'inline',
|
||||
content: style,
|
||||
});
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
if (Array.isArray(collectedLinks)) {
|
||||
links = collectedLinks
|
||||
.map((link: any) => {
|
||||
return renderUniqueStylesheet(result, {
|
||||
href: prependForwardSlash(link),
|
||||
type: 'external',
|
||||
src: prependForwardSlash(link),
|
||||
});
|
||||
})
|
||||
.join('');
|
||||
|
|
|
@ -123,7 +123,8 @@ export function astroConfigBuildPlugin(
|
|||
chunk.type === 'chunk' &&
|
||||
(chunk.code.includes(LINKS_PLACEHOLDER) || chunk.code.includes(SCRIPTS_PLACEHOLDER))
|
||||
) {
|
||||
let entryCSS = new Set<string>();
|
||||
let entryStyles = new Set<string>();
|
||||
let entryLinks = new Set<string>();
|
||||
let entryScripts = new Set<string>();
|
||||
|
||||
for (const id of Object.keys(chunk.modules)) {
|
||||
|
@ -137,7 +138,8 @@ export function astroConfigBuildPlugin(
|
|||
const _entryScripts = pageData.propagatedScripts?.get(id);
|
||||
if (_entryCss) {
|
||||
for (const value of _entryCss) {
|
||||
entryCSS.add(value);
|
||||
if (value.type === 'inline') entryStyles.add(value.content);
|
||||
if (value.type === 'external') entryLinks.add(value.src);
|
||||
}
|
||||
}
|
||||
if (_entryScripts) {
|
||||
|
@ -150,10 +152,16 @@ export function astroConfigBuildPlugin(
|
|||
}
|
||||
|
||||
let newCode = chunk.code;
|
||||
if (entryCSS.size) {
|
||||
if (entryStyles.size) {
|
||||
newCode = newCode.replace(
|
||||
JSON.stringify(STYLES_PLACEHOLDER),
|
||||
JSON.stringify(Array.from(entryStyles))
|
||||
);
|
||||
}
|
||||
if (entryLinks.size) {
|
||||
newCode = newCode.replace(
|
||||
JSON.stringify(LINKS_PLACEHOLDER),
|
||||
JSON.stringify(Array.from(entryCSS).map(prependBase))
|
||||
JSON.stringify(Array.from(entryLinks).map(prependBase))
|
||||
);
|
||||
}
|
||||
if (entryScripts.size) {
|
||||
|
|
|
@ -119,7 +119,7 @@ export function astroContentImportPlugin({
|
|||
if (settings.contentEntryTypes.some((t) => t.getRenderModule)) {
|
||||
plugins.push({
|
||||
name: 'astro:content-render-imports',
|
||||
async load(viteId) {
|
||||
async transform(_, viteId) {
|
||||
const contentRenderer = getContentRendererByViteId(viteId, settings);
|
||||
if (!contentRenderer) return;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import type {
|
|||
ComponentInstance,
|
||||
EndpointHandler,
|
||||
ManifestData,
|
||||
MiddlewareResponseHandler,
|
||||
RouteData,
|
||||
SSRElement,
|
||||
} from '../../@types/astro';
|
||||
|
@ -9,9 +10,10 @@ import type { RouteInfo, SSRManifest as Manifest } from './types';
|
|||
|
||||
import mime from 'mime';
|
||||
import { attachToResponse, getSetCookiesFromResponse } from '../cookies/index.js';
|
||||
import { call as callEndpoint } from '../endpoint/index.js';
|
||||
import { call as callEndpoint, createAPIContext } from '../endpoint/index.js';
|
||||
import { consoleLogDestination } from '../logger/console.js';
|
||||
import { error, type LogOptions } from '../logger/core.js';
|
||||
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||
import { removeTrailingForwardSlash } from '../path.js';
|
||||
import {
|
||||
createEnvironment,
|
||||
|
@ -22,12 +24,14 @@ import {
|
|||
import { RouteCache } from '../render/route-cache.js';
|
||||
import {
|
||||
createAssetLink,
|
||||
createLinkStylesheetElementSet,
|
||||
createModuleScriptElement,
|
||||
createStylesheetElementSet,
|
||||
} from '../render/ssr-element.js';
|
||||
import { matchRoute } from '../routing/match.js';
|
||||
export { deserializeManifest } from './common.js';
|
||||
|
||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||
|
||||
export const pagesVirtualModuleId = '@astrojs-pages-virtual-entry';
|
||||
export const resolvedPagesVirtualModuleId = '\0' + pagesVirtualModuleId;
|
||||
const responseSentSymbol = Symbol.for('astro.responseSent');
|
||||
|
@ -127,6 +131,8 @@ export class App {
|
|||
}
|
||||
}
|
||||
|
||||
Reflect.set(request, clientLocalsSymbol, {});
|
||||
|
||||
// Use the 404 status code for 404.astro components
|
||||
if (routeData.route === '/404') {
|
||||
defaultStatus = 404;
|
||||
|
@ -174,7 +180,9 @@ export class App {
|
|||
const url = new URL(request.url);
|
||||
const pathname = '/' + this.removeBase(url.pathname);
|
||||
const info = this.#routeDataToRouteInfo.get(routeData!)!;
|
||||
const links = createLinkStylesheetElementSet(info.links);
|
||||
// may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
|
||||
const links = new Set<never>();
|
||||
const styles = createStylesheetElementSet(info.styles);
|
||||
|
||||
let scripts = new Set<SSRElement>();
|
||||
for (const script of info.scripts) {
|
||||
|
@ -191,18 +199,45 @@ export class App {
|
|||
}
|
||||
|
||||
try {
|
||||
const ctx = createRenderContext({
|
||||
const renderContext = await createRenderContext({
|
||||
request,
|
||||
origin: url.origin,
|
||||
pathname,
|
||||
componentMetadata: this.#manifest.componentMetadata,
|
||||
scripts,
|
||||
styles,
|
||||
links,
|
||||
route: routeData,
|
||||
status,
|
||||
mod: mod as any,
|
||||
env: this.#env,
|
||||
});
|
||||
|
||||
const response = await renderPage(mod, ctx, this.#env);
|
||||
const apiContext = createAPIContext({
|
||||
request: renderContext.request,
|
||||
params: renderContext.params,
|
||||
props: renderContext.props,
|
||||
site: this.#env.site,
|
||||
adapterName: this.#env.adapterName,
|
||||
});
|
||||
const onRequest = this.#manifest.middleware?.onRequest;
|
||||
let response;
|
||||
if (onRequest) {
|
||||
response = await callMiddleware<Response>(
|
||||
onRequest as MiddlewareResponseHandler,
|
||||
apiContext,
|
||||
() => {
|
||||
return renderPage({ mod, renderContext, env: this.#env, apiContext });
|
||||
}
|
||||
);
|
||||
} else {
|
||||
response = await renderPage({
|
||||
mod,
|
||||
renderContext,
|
||||
env: this.#env,
|
||||
apiContext,
|
||||
});
|
||||
}
|
||||
Reflect.set(request, responseSentSymbol, true);
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
|
@ -224,15 +259,23 @@ export class App {
|
|||
const pathname = '/' + this.removeBase(url.pathname);
|
||||
const handler = mod as unknown as EndpointHandler;
|
||||
|
||||
const ctx = createRenderContext({
|
||||
const ctx = await createRenderContext({
|
||||
request,
|
||||
origin: url.origin,
|
||||
pathname,
|
||||
route: routeData,
|
||||
status,
|
||||
env: this.#env,
|
||||
mod: handler as any,
|
||||
});
|
||||
|
||||
const result = await callEndpoint(handler, this.#env, ctx, this.#logging);
|
||||
const result = await callEndpoint(
|
||||
handler,
|
||||
this.#env,
|
||||
ctx,
|
||||
this.#logging,
|
||||
this.#manifest.middleware
|
||||
);
|
||||
|
||||
if (result.type === 'response') {
|
||||
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark';
|
||||
import type {
|
||||
AstroMiddlewareInstance,
|
||||
ComponentInstance,
|
||||
RouteData,
|
||||
SerializedRouteData,
|
||||
|
@ -10,6 +11,10 @@ import type {
|
|||
|
||||
export type ComponentPath = string;
|
||||
|
||||
export type StylesheetAsset =
|
||||
| { type: 'inline'; content: string }
|
||||
| { type: 'external'; src: string };
|
||||
|
||||
export interface RouteInfo {
|
||||
routeData: RouteData;
|
||||
file: string;
|
||||
|
@ -20,6 +25,7 @@ export interface RouteInfo {
|
|||
// Hoisted
|
||||
| { type: 'inline' | 'external'; value: string }
|
||||
)[];
|
||||
styles: StylesheetAsset[];
|
||||
}
|
||||
|
||||
export type SerializedRouteInfo = Omit<RouteInfo, 'routeData'> & {
|
||||
|
@ -38,6 +44,7 @@ export interface SSRManifest {
|
|||
entryModules: Record<string, string>;
|
||||
assets: Set<string>;
|
||||
componentMetadata: SSRResult['componentMetadata'];
|
||||
middleware?: AstroMiddlewareInstance<unknown>;
|
||||
}
|
||||
|
||||
export type SerializedSSRManifest = Omit<SSRManifest, 'routes' | 'assets' | 'componentMetadata'> & {
|
||||
|
|
|
@ -5,10 +5,13 @@ import type { OutputAsset, OutputChunk } from 'rollup';
|
|||
import { fileURLToPath } from 'url';
|
||||
import type {
|
||||
AstroConfig,
|
||||
AstroMiddlewareInstance,
|
||||
AstroSettings,
|
||||
ComponentInstance,
|
||||
EndpointHandler,
|
||||
EndpointOutput,
|
||||
ImageTransform,
|
||||
MiddlewareResponseHandler,
|
||||
RouteType,
|
||||
SSRError,
|
||||
SSRLoadedRenderer,
|
||||
|
@ -25,27 +28,32 @@ import {
|
|||
} from '../../core/path.js';
|
||||
import { runHookBuildGenerated } from '../../integrations/index.js';
|
||||
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||
import { call as callEndpoint, throwIfRedirectNotAllowed } from '../endpoint/index.js';
|
||||
import {
|
||||
call as callEndpoint,
|
||||
createAPIContext,
|
||||
throwIfRedirectNotAllowed,
|
||||
} from '../endpoint/index.js';
|
||||
import { AstroError } from '../errors/index.js';
|
||||
import { debug, info } from '../logger/core.js';
|
||||
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||
import { createEnvironment, createRenderContext, renderPage } from '../render/index.js';
|
||||
import { callGetStaticPaths } from '../render/route-cache.js';
|
||||
import {
|
||||
createAssetLink,
|
||||
createLinkStylesheetElementSet,
|
||||
createModuleScriptsSet,
|
||||
createStylesheetElementSet,
|
||||
} from '../render/ssr-element.js';
|
||||
import { createRequest } from '../request.js';
|
||||
import { matchRoute } from '../routing/match.js';
|
||||
import { getOutputFilename } from '../util.js';
|
||||
import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js';
|
||||
import {
|
||||
eachPageData,
|
||||
eachPrerenderedPageData,
|
||||
getPageDataByComponent,
|
||||
sortedCSS,
|
||||
} from './internal.js';
|
||||
import type { PageBuildData, SingleFileBuiltModule, StaticBuildOptions } from './types';
|
||||
import { cssOrder, eachPageData, getPageDataByComponent, mergeInlineCss } from './internal.js';
|
||||
import type {
|
||||
PageBuildData,
|
||||
SingleFileBuiltModule,
|
||||
StaticBuildOptions,
|
||||
StylesheetAsset,
|
||||
} from './types';
|
||||
import { getTimeStat } from './util.js';
|
||||
|
||||
function shouldSkipDraft(pageModule: ComponentInstance, settings: AstroSettings): boolean {
|
||||
|
@ -99,8 +107,9 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
|
|||
const builtPaths = new Set<string>();
|
||||
|
||||
if (ssr) {
|
||||
for (const pageData of eachPrerenderedPageData(internals)) {
|
||||
await generatePage(opts, internals, pageData, ssrEntry, builtPaths);
|
||||
for (const pageData of eachPageData(internals)) {
|
||||
if (pageData.route.prerender)
|
||||
await generatePage(opts, internals, pageData, ssrEntry, builtPaths);
|
||||
}
|
||||
} else {
|
||||
for (const pageData of eachPageData(internals)) {
|
||||
|
@ -157,10 +166,17 @@ async function generatePage(
|
|||
const renderers = ssrEntry.renderers;
|
||||
|
||||
const pageInfo = getPageDataByComponent(internals, pageData.route.component);
|
||||
const linkIds: string[] = sortedCSS(pageData);
|
||||
|
||||
// may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
|
||||
const linkIds: [] = [];
|
||||
const scripts = pageInfo?.hoistedScript ?? null;
|
||||
const styles = pageData.styles
|
||||
.sort(cssOrder)
|
||||
.map(({ sheet }) => sheet)
|
||||
.reduce(mergeInlineCss, []);
|
||||
|
||||
const pageModule = ssrEntry.pageMap?.get(pageData.component);
|
||||
const middleware = ssrEntry.middleware;
|
||||
|
||||
if (!pageModule) {
|
||||
throw new Error(
|
||||
|
@ -178,6 +194,7 @@ async function generatePage(
|
|||
internals,
|
||||
linkIds,
|
||||
scripts,
|
||||
styles,
|
||||
mod: pageModule,
|
||||
renderers,
|
||||
};
|
||||
|
@ -190,7 +207,7 @@ async function generatePage(
|
|||
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
const path = paths[i];
|
||||
await generatePath(path, opts, generationOptions);
|
||||
await generatePath(path, opts, generationOptions, middleware);
|
||||
const timeEnd = performance.now();
|
||||
const timeChange = getTimeStat(timeStart, timeEnd);
|
||||
const timeIncrease = `(+${timeChange})`;
|
||||
|
@ -268,6 +285,7 @@ interface GeneratePathOptions {
|
|||
internals: BuildInternals;
|
||||
linkIds: string[];
|
||||
scripts: { type: 'inline' | 'external'; value: string } | null;
|
||||
styles: StylesheetAsset[];
|
||||
mod: ComponentInstance;
|
||||
renderers: SSRLoadedRenderer[];
|
||||
}
|
||||
|
@ -332,10 +350,19 @@ function getUrlForPath(
|
|||
async function generatePath(
|
||||
pathname: string,
|
||||
opts: StaticBuildOptions,
|
||||
gopts: GeneratePathOptions
|
||||
gopts: GeneratePathOptions,
|
||||
middleware?: AstroMiddlewareInstance<unknown>
|
||||
) {
|
||||
const { settings, logging, origin, routeCache } = opts;
|
||||
const { mod, internals, linkIds, scripts: hoistedScripts, pageData, renderers } = gopts;
|
||||
const {
|
||||
mod,
|
||||
internals,
|
||||
linkIds,
|
||||
scripts: hoistedScripts,
|
||||
styles: _styles,
|
||||
pageData,
|
||||
renderers,
|
||||
} = gopts;
|
||||
|
||||
// This adds the page name to the array so it can be shown as part of stats.
|
||||
if (pageData.route.type === 'page') {
|
||||
|
@ -344,13 +371,15 @@ async function generatePath(
|
|||
|
||||
debug('build', `Generating: ${pathname}`);
|
||||
|
||||
const links = createLinkStylesheetElementSet(
|
||||
linkIds,
|
||||
// may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
|
||||
const links = new Set<never>();
|
||||
const scripts = createModuleScriptsSet(
|
||||
hoistedScripts ? [hoistedScripts] : [],
|
||||
settings.config.base,
|
||||
settings.config.build.assetsPrefix
|
||||
);
|
||||
const scripts = createModuleScriptsSet(
|
||||
hoistedScripts ? [hoistedScripts] : [],
|
||||
const styles = createStylesheetElementSet(
|
||||
_styles,
|
||||
settings.config.base,
|
||||
settings.config.build.assetsPrefix
|
||||
);
|
||||
|
@ -418,21 +447,32 @@ async function generatePath(
|
|||
ssr,
|
||||
streaming: true,
|
||||
});
|
||||
const ctx = createRenderContext({
|
||||
|
||||
const renderContext = await createRenderContext({
|
||||
origin,
|
||||
pathname,
|
||||
request: createRequest({ url, headers: new Headers(), logging, ssr }),
|
||||
componentMetadata: internals.componentMetadata,
|
||||
scripts,
|
||||
styles,
|
||||
links,
|
||||
route: pageData.route,
|
||||
env,
|
||||
mod,
|
||||
});
|
||||
|
||||
let body: string | Uint8Array;
|
||||
let encoding: BufferEncoding | undefined;
|
||||
if (pageData.route.type === 'endpoint') {
|
||||
const endpointHandler = mod as unknown as EndpointHandler;
|
||||
const result = await callEndpoint(endpointHandler, env, ctx, logging);
|
||||
|
||||
const result = await callEndpoint(
|
||||
endpointHandler,
|
||||
env,
|
||||
renderContext,
|
||||
logging,
|
||||
middleware as AstroMiddlewareInstance<Response | EndpointOutput>
|
||||
);
|
||||
|
||||
if (result.type === 'response') {
|
||||
throwIfRedirectNotAllowed(result.response, opts.settings.config);
|
||||
|
@ -447,7 +487,26 @@ async function generatePath(
|
|||
} else {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await renderPage(mod, ctx, env);
|
||||
const apiContext = createAPIContext({
|
||||
request: renderContext.request,
|
||||
params: renderContext.params,
|
||||
props: renderContext.props,
|
||||
site: env.site,
|
||||
adapterName: env.adapterName,
|
||||
});
|
||||
|
||||
const onRequest = middleware?.onRequest;
|
||||
if (onRequest) {
|
||||
response = await callMiddleware<Response>(
|
||||
onRequest as MiddlewareResponseHandler,
|
||||
apiContext,
|
||||
() => {
|
||||
return renderPage({ mod, renderContext, env, apiContext });
|
||||
}
|
||||
);
|
||||
} else {
|
||||
response = await renderPage({ mod, renderContext, env, apiContext });
|
||||
}
|
||||
} catch (err) {
|
||||
if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') {
|
||||
(err as SSRError).id = pageData.component;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Rollup } from 'vite';
|
||||
import type { PageBuildData, ViteID } from './types';
|
||||
import type { PageBuildData, StylesheetAsset, ViteID } from './types';
|
||||
|
||||
import type { SSRResult } from '../../@types/astro';
|
||||
import type { PageOptions } from '../../vite-plugin-astro/types';
|
||||
|
@ -216,63 +216,64 @@ export function* eachPageData(internals: BuildInternals) {
|
|||
}
|
||||
|
||||
export function hasPrerenderedPages(internals: BuildInternals) {
|
||||
for (const id of internals.pagesByViteID.keys()) {
|
||||
if (internals.pageOptionsByPage.get(id)?.prerender) {
|
||||
for (const pageData of eachPageData(internals)) {
|
||||
if (pageData.route.prerender) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function* eachPrerenderedPageData(internals: BuildInternals) {
|
||||
for (const [id, pageData] of internals.pagesByViteID.entries()) {
|
||||
if (internals.pageOptionsByPage.get(id)?.prerender) {
|
||||
yield pageData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* eachServerPageData(internals: BuildInternals) {
|
||||
for (const [id, pageData] of internals.pagesByViteID.entries()) {
|
||||
if (!internals.pageOptionsByPage.get(id)?.prerender) {
|
||||
yield pageData;
|
||||
}
|
||||
}
|
||||
interface OrderInfo {
|
||||
depth: number;
|
||||
order: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort a page's CSS by depth. A higher depth means that the CSS comes from shared subcomponents.
|
||||
* A lower depth means it comes directly from the top-level page.
|
||||
* The return of this function is an array of CSS paths, with shared CSS on top
|
||||
* and page-level CSS on bottom.
|
||||
* Can be used to sort stylesheets so that shared rules come first
|
||||
* and page-specific rules come after.
|
||||
*/
|
||||
export function sortedCSS(pageData: PageBuildData) {
|
||||
return Array.from(pageData.css)
|
||||
.sort((a, b) => {
|
||||
let depthA = a[1].depth,
|
||||
depthB = b[1].depth,
|
||||
orderA = a[1].order,
|
||||
orderB = b[1].order;
|
||||
export function cssOrder(a: OrderInfo, b: OrderInfo) {
|
||||
let depthA = a.depth,
|
||||
depthB = b.depth,
|
||||
orderA = a.order,
|
||||
orderB = b.order;
|
||||
|
||||
if (orderA === -1 && orderB >= 0) {
|
||||
return 1;
|
||||
} else if (orderB === -1 && orderA >= 0) {
|
||||
return -1;
|
||||
} else if (orderA > orderB) {
|
||||
return 1;
|
||||
} else if (orderA < orderB) {
|
||||
return -1;
|
||||
} else {
|
||||
if (depthA === -1) {
|
||||
return -1;
|
||||
} else if (depthB === -1) {
|
||||
return 1;
|
||||
} else {
|
||||
return depthA > depthB ? -1 : 1;
|
||||
}
|
||||
}
|
||||
})
|
||||
.map(([id]) => id);
|
||||
if (orderA === -1 && orderB >= 0) {
|
||||
return 1;
|
||||
} else if (orderB === -1 && orderA >= 0) {
|
||||
return -1;
|
||||
} else if (orderA > orderB) {
|
||||
return 1;
|
||||
} else if (orderA < orderB) {
|
||||
return -1;
|
||||
} else {
|
||||
if (depthA === -1) {
|
||||
return -1;
|
||||
} else if (depthB === -1) {
|
||||
return 1;
|
||||
} else {
|
||||
return depthA > depthB ? -1 : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeInlineCss(
|
||||
acc: Array<StylesheetAsset>,
|
||||
current: StylesheetAsset
|
||||
): Array<StylesheetAsset> {
|
||||
const lastAdded = acc.at(acc.length - 1);
|
||||
const lastWasInline = lastAdded?.type === 'inline';
|
||||
const currentIsInline = current?.type === 'inline';
|
||||
if (lastWasInline && currentIsInline) {
|
||||
const merged = { type: 'inline' as const, content: lastAdded.content + current.content };
|
||||
acc[acc.length - 1] = merged;
|
||||
return acc;
|
||||
}
|
||||
acc.push(current);
|
||||
return acc;
|
||||
}
|
||||
|
||||
export function isHoistedScript(internals: BuildInternals, id: string): boolean {
|
||||
|
|
|
@ -53,7 +53,7 @@ export async function collectPagesData(
|
|||
component: route.component,
|
||||
route,
|
||||
moduleSpecifier: '',
|
||||
css: new Map(),
|
||||
styles: [],
|
||||
propagatedStyles: new Map(),
|
||||
propagatedScripts: new Map(),
|
||||
hoistedScript: undefined,
|
||||
|
@ -76,7 +76,7 @@ export async function collectPagesData(
|
|||
component: route.component,
|
||||
route,
|
||||
moduleSpecifier: '',
|
||||
css: new Map(),
|
||||
styles: [],
|
||||
propagatedStyles: new Map(),
|
||||
propagatedScripts: new Map(),
|
||||
hoistedScript: undefined,
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { Plugin as VitePlugin } from 'vite';
|
|||
import type { BuildInternals } from '../internal.js';
|
||||
import type { AstroBuildPlugin } from '../plugin.js';
|
||||
|
||||
const astroEntryPrefix = '\0astro-entry:';
|
||||
export const astroEntryPrefix = '\0astro-entry:';
|
||||
|
||||
/**
|
||||
* When adding hydrated or client:only components as Rollup inputs, sometimes we're not using all
|
||||
|
|
|
@ -5,7 +5,7 @@ import { type Plugin as VitePlugin, type ResolvedConfig } from 'vite';
|
|||
import { isBuildableCSSRequest } from '../../render/dev/util.js';
|
||||
import type { BuildInternals } from '../internal';
|
||||
import type { AstroBuildPlugin } from '../plugin';
|
||||
import type { PageBuildData, StaticBuildOptions } from '../types';
|
||||
import type { PageBuildData, StaticBuildOptions, StylesheetAsset } from '../types';
|
||||
|
||||
import { PROPAGATED_ASSET_FLAG } from '../../../content/consts.js';
|
||||
import * as assetName from '../css-asset-name.js';
|
||||
|
@ -25,217 +25,7 @@ interface PluginOptions {
|
|||
target: 'client' | 'server';
|
||||
}
|
||||
|
||||
export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
|
||||
const { internals, buildOptions } = options;
|
||||
const { settings } = buildOptions;
|
||||
|
||||
let resolvedConfig: ResolvedConfig;
|
||||
|
||||
function createNameHash(baseId: string, hashIds: string[]): string {
|
||||
const baseName = baseId ? npath.parse(baseId).name : 'index';
|
||||
const hash = crypto.createHash('sha256');
|
||||
for (const id of hashIds) {
|
||||
hash.update(id, 'utf-8');
|
||||
}
|
||||
const h = hash.digest('hex').slice(0, 8);
|
||||
const proposedName = baseName + '.' + h;
|
||||
return proposedName;
|
||||
}
|
||||
|
||||
function* getParentClientOnlys(
|
||||
id: string,
|
||||
ctx: { getModuleInfo: GetModuleInfo }
|
||||
): Generator<PageBuildData, void, unknown> {
|
||||
for (const [info] of walkParentInfos(id, ctx)) {
|
||||
yield* getPageDatasByClientOnlyID(internals, info.id);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'astro:rollup-plugin-build-css',
|
||||
|
||||
transform(_, id) {
|
||||
// In the SSR build, styles that are bundled are tracked in `internals.cssChunkModuleIds`.
|
||||
// In the client build, if we're also bundling the same style, return an empty string to
|
||||
// deduplicate the final CSS output.
|
||||
if (options.target === 'client' && internals.cssChunkModuleIds.has(id)) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
outputOptions(outputOptions) {
|
||||
// Skip in client builds as its module graph doesn't have reference to Astro pages
|
||||
// to be able to chunk based on it's related top-level pages.
|
||||
if (options.target === 'client') return;
|
||||
|
||||
const assetFileNames = outputOptions.assetFileNames;
|
||||
const namingIncludesHash = assetFileNames?.toString().includes('[hash]');
|
||||
const createNameForParentPages = namingIncludesHash
|
||||
? assetName.shortHashedName
|
||||
: assetName.createSlugger(settings);
|
||||
|
||||
extendManualChunks(outputOptions, {
|
||||
after(id, meta) {
|
||||
// For CSS, create a hash of all of the pages that use it.
|
||||
// This causes CSS to be built into shared chunks when used by multiple pages.
|
||||
if (isBuildableCSSRequest(id)) {
|
||||
for (const [pageInfo] of walkParentInfos(id, {
|
||||
getModuleInfo: meta.getModuleInfo,
|
||||
})) {
|
||||
if (new URL(pageInfo.id, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG)) {
|
||||
// Split delayed assets to separate modules
|
||||
// so they can be injected where needed
|
||||
return createNameHash(id, [id]);
|
||||
}
|
||||
}
|
||||
return createNameForParentPages(id, meta);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async generateBundle(_outputOptions, bundle) {
|
||||
type ViteMetadata = {
|
||||
importedAssets: Set<string>;
|
||||
importedCss: Set<string>;
|
||||
};
|
||||
|
||||
const appendCSSToPage = (
|
||||
pageData: PageBuildData,
|
||||
meta: ViteMetadata,
|
||||
depth: number,
|
||||
order: number
|
||||
) => {
|
||||
for (const importedCssImport of meta.importedCss) {
|
||||
// CSS is prioritized based on depth. Shared CSS has a higher depth due to being imported by multiple pages.
|
||||
// Depth info is used when sorting the links on the page.
|
||||
if (pageData?.css.has(importedCssImport)) {
|
||||
// eslint-disable-next-line
|
||||
const cssInfo = pageData?.css.get(importedCssImport)!;
|
||||
if (depth < cssInfo.depth) {
|
||||
cssInfo.depth = depth;
|
||||
}
|
||||
|
||||
// Update the order, preferring the lowest order we have.
|
||||
if (cssInfo.order === -1) {
|
||||
cssInfo.order = order;
|
||||
} else if (order < cssInfo.order && order > -1) {
|
||||
cssInfo.order = order;
|
||||
}
|
||||
} else {
|
||||
pageData?.css.set(importedCssImport, { depth, order });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const [_, chunk] of Object.entries(bundle)) {
|
||||
if (chunk.type === 'chunk') {
|
||||
const c = chunk;
|
||||
|
||||
if ('viteMetadata' in chunk) {
|
||||
const meta = chunk['viteMetadata'] as ViteMetadata;
|
||||
|
||||
// Chunks that have the viteMetadata.importedCss are CSS chunks
|
||||
if (meta.importedCss.size) {
|
||||
// In the SSR build, keep track of all CSS chunks' modules as the client build may
|
||||
// duplicate them, e.g. for `client:load` components that render in SSR and client
|
||||
// for hydation.
|
||||
if (options.target === 'server') {
|
||||
for (const id of Object.keys(c.modules)) {
|
||||
internals.cssChunkModuleIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
// For the client build, client:only styles need to be mapped
|
||||
// over to their page. For this chunk, determine if it's a child of a
|
||||
// client:only component and if so, add its CSS to the page it belongs to.
|
||||
if (options.target === 'client') {
|
||||
for (const id of Object.keys(c.modules)) {
|
||||
for (const pageData of getParentClientOnlys(id, this)) {
|
||||
for (const importedCssImport of meta.importedCss) {
|
||||
pageData.css.set(importedCssImport, { depth: -1, order: -1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For this CSS chunk, walk parents until you find a page. Add the CSS to that page.
|
||||
for (const id of Object.keys(c.modules)) {
|
||||
for (const [pageInfo, depth, order] of walkParentInfos(
|
||||
id,
|
||||
this,
|
||||
function until(importer) {
|
||||
return new URL(importer, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG);
|
||||
}
|
||||
)) {
|
||||
if (new URL(pageInfo.id, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG)) {
|
||||
for (const parent of walkParentInfos(id, this)) {
|
||||
const parentInfo = parent[0];
|
||||
if (moduleIsTopLevelPage(parentInfo)) {
|
||||
const pageViteID = parentInfo.id;
|
||||
const pageData = getPageDataByViteID(internals, pageViteID);
|
||||
if (pageData) {
|
||||
for (const css of meta.importedCss) {
|
||||
const existingCss =
|
||||
pageData.propagatedStyles.get(pageInfo.id) ?? new Set();
|
||||
pageData.propagatedStyles.set(
|
||||
pageInfo.id,
|
||||
new Set([...existingCss, css])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (moduleIsTopLevelPage(pageInfo)) {
|
||||
const pageViteID = pageInfo.id;
|
||||
const pageData = getPageDataByViteID(internals, pageViteID);
|
||||
if (pageData) {
|
||||
appendCSSToPage(pageData, meta, depth, order);
|
||||
}
|
||||
} else if (
|
||||
options.target === 'client' &&
|
||||
isHoistedScript(internals, pageInfo.id)
|
||||
) {
|
||||
for (const pageData of getPageDatasByHoistedScriptId(
|
||||
internals,
|
||||
pageInfo.id
|
||||
)) {
|
||||
appendCSSToPage(pageData, meta, -1, order);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'astro:rollup-plugin-single-css',
|
||||
enforce: 'post',
|
||||
configResolved(config) {
|
||||
resolvedConfig = config;
|
||||
},
|
||||
generateBundle(_, bundle) {
|
||||
// If user disable css code-splitting, search for Vite's hardcoded
|
||||
// `style.css` and add it as css for each page.
|
||||
// Ref: https://github.com/vitejs/vite/blob/b2c0ee04d4db4a0ef5a084c50f49782c5f88587c/packages/vite/src/node/plugins/html.ts#L690-L705
|
||||
if (!resolvedConfig.build.cssCodeSplit) {
|
||||
const cssChunk = Object.values(bundle).find(
|
||||
(chunk) => chunk.type === 'asset' && chunk.name === 'style.css'
|
||||
);
|
||||
if (cssChunk) {
|
||||
for (const pageData of eachPageData(internals)) {
|
||||
pageData.css.set(cssChunk.fileName, { depth: -1, order: -1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
/***** ASTRO PLUGIN *****/
|
||||
|
||||
export function pluginCSS(
|
||||
options: StaticBuildOptions,
|
||||
|
@ -258,3 +48,272 @@ export function pluginCSS(
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
/***** ROLLUP SUB-PLUGINS *****/
|
||||
|
||||
function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
|
||||
const { internals, buildOptions } = options;
|
||||
const { settings } = buildOptions;
|
||||
|
||||
let resolvedConfig: ResolvedConfig;
|
||||
|
||||
// stylesheet filenames are kept in here until "post", when they are rendered and ready to be inlined
|
||||
const pagesToCss: Record<string, Record<string, { order: number; depth: number }>> = {};
|
||||
const pagesToPropagatedCss: Record<string, Record<string, Set<string>>> = {};
|
||||
|
||||
const cssBuildPlugin: VitePlugin = {
|
||||
name: 'astro:rollup-plugin-build-css',
|
||||
|
||||
transform(_, id) {
|
||||
// In the SSR build, styles that are bundled are tracked in `internals.cssChunkModuleIds`.
|
||||
// In the client build, if we're also bundling the same style, return an empty string to
|
||||
// deduplicate the final CSS output.
|
||||
if (options.target === 'client' && internals.cssChunkModuleIds.has(id)) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
outputOptions(outputOptions) {
|
||||
// Skip in client builds as its module graph doesn't have reference to Astro pages
|
||||
// to be able to chunk based on it's related top-level pages.
|
||||
if (options.target === 'client') return;
|
||||
|
||||
const assetFileNames = outputOptions.assetFileNames;
|
||||
const namingIncludesHash = assetFileNames?.toString().includes('[hash]');
|
||||
const createNameForParentPages = namingIncludesHash
|
||||
? assetName.shortHashedName
|
||||
: assetName.createSlugger(settings);
|
||||
|
||||
extendManualChunks(outputOptions, {
|
||||
after(id, meta) {
|
||||
// For CSS, create a hash of all of the pages that use it.
|
||||
// This causes CSS to be built into shared chunks when used by multiple pages.
|
||||
if (isBuildableCSSRequest(id)) {
|
||||
for (const [pageInfo] of walkParentInfos(id, {
|
||||
getModuleInfo: meta.getModuleInfo,
|
||||
})) {
|
||||
if (new URL(pageInfo.id, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG)) {
|
||||
// Split delayed assets to separate modules
|
||||
// so they can be injected where needed
|
||||
return createNameHash(id, [id]);
|
||||
}
|
||||
}
|
||||
return createNameForParentPages(id, meta);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async generateBundle(_outputOptions, bundle) {
|
||||
for (const [_, chunk] of Object.entries(bundle)) {
|
||||
if (chunk.type !== 'chunk') continue;
|
||||
if ('viteMetadata' in chunk === false) continue;
|
||||
const meta = chunk.viteMetadata as ViteMetadata;
|
||||
|
||||
// Skip if the chunk has no CSS, we want to handle CSS chunks only
|
||||
if (meta.importedCss.size < 1) continue;
|
||||
|
||||
// In the SSR build, keep track of all CSS chunks' modules as the client build may
|
||||
// duplicate them, e.g. for `client:load` components that render in SSR and client
|
||||
// for hydation.
|
||||
if (options.target === 'server') {
|
||||
for (const id of Object.keys(chunk.modules)) {
|
||||
internals.cssChunkModuleIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
// For the client build, client:only styles need to be mapped
|
||||
// over to their page. For this chunk, determine if it's a child of a
|
||||
// client:only component and if so, add its CSS to the page it belongs to.
|
||||
if (options.target === 'client') {
|
||||
for (const id of Object.keys(chunk.modules)) {
|
||||
for (const pageData of getParentClientOnlys(id, this, internals)) {
|
||||
for (const importedCssImport of meta.importedCss) {
|
||||
const cssToInfoRecord = (pagesToCss[pageData.moduleSpecifier] ??= {});
|
||||
cssToInfoRecord[importedCssImport] = { depth: -1, order: -1 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For this CSS chunk, walk parents until you find a page. Add the CSS to that page.
|
||||
for (const id of Object.keys(chunk.modules)) {
|
||||
for (const [pageInfo, depth, order] of walkParentInfos(
|
||||
id,
|
||||
this,
|
||||
function until(importer) {
|
||||
return new URL(importer, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG);
|
||||
}
|
||||
)) {
|
||||
if (new URL(pageInfo.id, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG)) {
|
||||
for (const parent of walkParentInfos(id, this)) {
|
||||
const parentInfo = parent[0];
|
||||
if (moduleIsTopLevelPage(parentInfo) === false) continue;
|
||||
|
||||
const pageViteID = parentInfo.id;
|
||||
const pageData = getPageDataByViteID(internals, pageViteID);
|
||||
if (pageData === undefined) continue;
|
||||
|
||||
for (const css of meta.importedCss) {
|
||||
const propagatedStyles = (pagesToPropagatedCss[pageData.moduleSpecifier] ??= {});
|
||||
const existingCss = (propagatedStyles[pageInfo.id] ??= new Set());
|
||||
|
||||
existingCss.add(css);
|
||||
}
|
||||
}
|
||||
} else if (moduleIsTopLevelPage(pageInfo)) {
|
||||
const pageViteID = pageInfo.id;
|
||||
const pageData = getPageDataByViteID(internals, pageViteID);
|
||||
if (pageData) {
|
||||
appendCSSToPage(pageData, meta, pagesToCss, depth, order);
|
||||
}
|
||||
} else if (options.target === 'client' && isHoistedScript(internals, pageInfo.id)) {
|
||||
for (const pageData of getPageDatasByHoistedScriptId(internals, pageInfo.id)) {
|
||||
appendCSSToPage(pageData, meta, pagesToCss, -1, order);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const singleCssPlugin: VitePlugin = {
|
||||
name: 'astro:rollup-plugin-single-css',
|
||||
enforce: 'post',
|
||||
configResolved(config) {
|
||||
resolvedConfig = config;
|
||||
},
|
||||
generateBundle(_, bundle) {
|
||||
// If user disable css code-splitting, search for Vite's hardcoded
|
||||
// `style.css` and add it as css for each page.
|
||||
// Ref: https://github.com/vitejs/vite/blob/b2c0ee04d4db4a0ef5a084c50f49782c5f88587c/packages/vite/src/node/plugins/html.ts#L690-L705
|
||||
if (resolvedConfig.build.cssCodeSplit) return;
|
||||
const cssChunk = Object.values(bundle).find(
|
||||
(chunk) => chunk.type === 'asset' && chunk.name === 'style.css'
|
||||
);
|
||||
if (cssChunk === undefined) return;
|
||||
for (const pageData of eachPageData(internals)) {
|
||||
const cssToInfoMap = (pagesToCss[pageData.moduleSpecifier] ??= {});
|
||||
cssToInfoMap[cssChunk.fileName] = { depth: -1, order: -1 };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const inlineStylesheetsPlugin: VitePlugin = {
|
||||
name: 'astro:rollup-plugin-inline-stylesheets',
|
||||
enforce: 'post',
|
||||
async generateBundle(_outputOptions, bundle) {
|
||||
const inlineConfig = settings.config.experimental.inlineStylesheets;
|
||||
const { assetsInlineLimit = 4096 } = settings.config.vite?.build ?? {};
|
||||
|
||||
Object.entries(bundle).forEach(([id, stylesheet]) => {
|
||||
if (
|
||||
stylesheet.type !== 'asset' ||
|
||||
stylesheet.name?.endsWith('.css') !== true ||
|
||||
typeof stylesheet.source !== 'string'
|
||||
)
|
||||
return;
|
||||
|
||||
const assetSize = new TextEncoder().encode(stylesheet.source).byteLength;
|
||||
|
||||
const toBeInlined =
|
||||
inlineConfig === 'always'
|
||||
? true
|
||||
: inlineConfig === 'never'
|
||||
? false
|
||||
: assetSize <= assetsInlineLimit;
|
||||
|
||||
if (toBeInlined) delete bundle[id];
|
||||
|
||||
// there should be a single js object for each stylesheet,
|
||||
// allowing the single reference to be shared and checked for duplicates
|
||||
const sheet: StylesheetAsset = toBeInlined
|
||||
? { type: 'inline', content: stylesheet.source }
|
||||
: { type: 'external', src: stylesheet.fileName };
|
||||
|
||||
const pages = Array.from(eachPageData(internals));
|
||||
|
||||
pages.forEach((pageData) => {
|
||||
const orderingInfo = pagesToCss[pageData.moduleSpecifier]?.[stylesheet.fileName];
|
||||
if (orderingInfo !== undefined) return pageData.styles.push({ ...orderingInfo, sheet });
|
||||
|
||||
const propagatedPaths = pagesToPropagatedCss[pageData.moduleSpecifier];
|
||||
if (propagatedPaths === undefined) return;
|
||||
Object.entries(propagatedPaths).forEach(([pageInfoId, css]) => {
|
||||
// return early if sheet does not need to be propagated
|
||||
if (css.has(stylesheet.fileName) !== true) return;
|
||||
|
||||
// return early if the stylesheet needing propagation has already been included
|
||||
if (pageData.styles.some((s) => s.sheet === sheet)) return;
|
||||
|
||||
const propagatedStyles =
|
||||
pageData.propagatedStyles.get(pageInfoId) ??
|
||||
pageData.propagatedStyles.set(pageInfoId, new Set()).get(pageInfoId)!;
|
||||
|
||||
propagatedStyles.add(sheet);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return [cssBuildPlugin, singleCssPlugin, inlineStylesheetsPlugin];
|
||||
}
|
||||
|
||||
/***** UTILITY FUNCTIONS *****/
|
||||
|
||||
function createNameHash(baseId: string, hashIds: string[]): string {
|
||||
const baseName = baseId ? npath.parse(baseId).name : 'index';
|
||||
const hash = crypto.createHash('sha256');
|
||||
for (const id of hashIds) {
|
||||
hash.update(id, 'utf-8');
|
||||
}
|
||||
const h = hash.digest('hex').slice(0, 8);
|
||||
const proposedName = baseName + '.' + h;
|
||||
return proposedName;
|
||||
}
|
||||
|
||||
function* getParentClientOnlys(
|
||||
id: string,
|
||||
ctx: { getModuleInfo: GetModuleInfo },
|
||||
internals: BuildInternals
|
||||
): Generator<PageBuildData, void, unknown> {
|
||||
for (const [info] of walkParentInfos(id, ctx)) {
|
||||
yield* getPageDatasByClientOnlyID(internals, info.id);
|
||||
}
|
||||
}
|
||||
|
||||
type ViteMetadata = {
|
||||
importedAssets: Set<string>;
|
||||
importedCss: Set<string>;
|
||||
};
|
||||
|
||||
function appendCSSToPage(
|
||||
pageData: PageBuildData,
|
||||
meta: ViteMetadata,
|
||||
pagesToCss: Record<string, Record<string, { order: number; depth: number }>>,
|
||||
depth: number,
|
||||
order: number
|
||||
) {
|
||||
for (const importedCssImport of meta.importedCss) {
|
||||
// CSS is prioritized based on depth. Shared CSS has a higher depth due to being imported by multiple pages.
|
||||
// Depth info is used when sorting the links on the page.
|
||||
const cssInfo = pagesToCss[pageData.moduleSpecifier]?.[importedCssImport];
|
||||
if (cssInfo !== undefined) {
|
||||
if (depth < cssInfo.depth) {
|
||||
cssInfo.depth = depth;
|
||||
}
|
||||
|
||||
// Update the order, preferring the lowest order we have.
|
||||
if (cssInfo.order === -1) {
|
||||
cssInfo.order = order;
|
||||
} else if (order < cssInfo.order && order > -1) {
|
||||
cssInfo.order = order;
|
||||
}
|
||||
} else {
|
||||
const cssToInfoRecord = (pagesToCss[pageData.moduleSpecifier] ??= {});
|
||||
cssToInfoRecord[importedCssImport] = { depth, order };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import type { Plugin as VitePlugin } from 'vite';
|
||||
import type { AstroBuildPlugin } from '../plugin';
|
||||
import type { StaticBuildOptions } from '../types';
|
||||
|
||||
import { pagesVirtualModuleId, resolvedPagesVirtualModuleId } from '../../app/index.js';
|
||||
import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../../constants.js';
|
||||
import { addRollupInput } from '../add-rollup-input.js';
|
||||
import { eachPageData, hasPrerenderedPages, type BuildInternals } from '../internal.js';
|
||||
import type { AstroBuildPlugin } from '../plugin';
|
||||
import type { StaticBuildOptions } from '../types';
|
||||
|
||||
export function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
|
||||
return {
|
||||
|
@ -22,8 +22,15 @@ export function vitePluginPages(opts: StaticBuildOptions, internals: BuildIntern
|
|||
}
|
||||
},
|
||||
|
||||
load(id) {
|
||||
async load(id) {
|
||||
if (id === resolvedPagesVirtualModuleId) {
|
||||
let middlewareId = null;
|
||||
if (opts.settings.config.experimental.middleware) {
|
||||
middlewareId = await this.resolve(
|
||||
`${opts.settings.config.srcDir.pathname}/${MIDDLEWARE_PATH_SEGMENT_NAME}`
|
||||
);
|
||||
}
|
||||
|
||||
let importMap = '';
|
||||
let imports = [];
|
||||
let i = 0;
|
||||
|
@ -47,8 +54,12 @@ export function vitePluginPages(opts: StaticBuildOptions, internals: BuildIntern
|
|||
|
||||
const def = `${imports.join('\n')}
|
||||
|
||||
${middlewareId ? `import * as _middleware from "${middlewareId.id}";` : ''}
|
||||
|
||||
export const pageMap = new Map([${importMap}]);
|
||||
export const renderers = [${rendererItems}];`;
|
||||
export const renderers = [${rendererItems}];
|
||||
${middlewareId ? `export const middleware = _middleware;` : ''}
|
||||
`;
|
||||
|
||||
return def;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Plugin as VitePlugin } from 'vite';
|
||||
import type { AstroAdapter } from '../../../@types/astro';
|
||||
import type { AstroAdapter, AstroConfig } from '../../../@types/astro';
|
||||
import type { SerializedRouteInfo, SerializedSSRManifest } from '../../app/types';
|
||||
import type { BuildInternals } from '../internal.js';
|
||||
import type { StaticBuildOptions } from '../types';
|
||||
|
@ -13,7 +13,7 @@ import { joinPaths, prependForwardSlash } from '../../path.js';
|
|||
import { serializeRouteData } from '../../routing/index.js';
|
||||
import { addRollupInput } from '../add-rollup-input.js';
|
||||
import { getOutFile, getOutFolder } from '../common.js';
|
||||
import { eachPrerenderedPageData, eachServerPageData, sortedCSS } from '../internal.js';
|
||||
import { cssOrder, eachPageData, mergeInlineCss } from '../internal.js';
|
||||
import type { AstroBuildPlugin } from '../plugin';
|
||||
|
||||
export const virtualModuleId = '@astrojs-ssr-virtual-entry';
|
||||
|
@ -21,7 +21,11 @@ const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
|||
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
|
||||
const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g');
|
||||
|
||||
export function vitePluginSSR(internals: BuildInternals, adapter: AstroAdapter): VitePlugin {
|
||||
export function vitePluginSSR(
|
||||
internals: BuildInternals,
|
||||
adapter: AstroAdapter,
|
||||
config: AstroConfig
|
||||
): VitePlugin {
|
||||
return {
|
||||
name: '@astrojs/vite-plugin-astro-ssr',
|
||||
enforce: 'post',
|
||||
|
@ -35,13 +39,18 @@ export function vitePluginSSR(internals: BuildInternals, adapter: AstroAdapter):
|
|||
},
|
||||
load(id) {
|
||||
if (id === resolvedVirtualModuleId) {
|
||||
let middleware = '';
|
||||
if (config.experimental?.middleware === true) {
|
||||
middleware = 'middleware: _main.middleware';
|
||||
}
|
||||
return `import * as adapter from '${adapter.serverEntrypoint}';
|
||||
import * as _main from '${pagesVirtualModuleId}';
|
||||
import { deserializeManifest as _deserializeManifest } from 'astro/app';
|
||||
import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest';
|
||||
const _manifest = Object.assign(_deserializeManifest('${manifestReplace}'), {
|
||||
pageMap: _main.pageMap,
|
||||
renderers: _main.renderers
|
||||
renderers: _main.renderers,
|
||||
${middleware}
|
||||
});
|
||||
_privateSetManifestDontUseThis(_manifest);
|
||||
const _args = ${adapter.args ? JSON.stringify(adapter.args) : 'undefined'};
|
||||
|
@ -142,7 +151,8 @@ function buildManifest(
|
|||
}
|
||||
};
|
||||
|
||||
for (const pageData of eachPrerenderedPageData(internals)) {
|
||||
for (const pageData of eachPageData(internals)) {
|
||||
if (!pageData.route.prerender) continue;
|
||||
if (!pageData.route.pathname) continue;
|
||||
|
||||
const outFolder = getOutFolder(
|
||||
|
@ -161,12 +171,14 @@ function buildManifest(
|
|||
file,
|
||||
links: [],
|
||||
scripts: [],
|
||||
styles: [],
|
||||
routeData: serializeRouteData(pageData.route, settings.config.trailingSlash),
|
||||
});
|
||||
staticFiles.push(file);
|
||||
}
|
||||
|
||||
for (const pageData of eachServerPageData(internals)) {
|
||||
for (const pageData of eachPageData(internals)) {
|
||||
if (pageData.route.prerender) continue;
|
||||
const scripts: SerializedRouteInfo['scripts'] = [];
|
||||
if (pageData.hoistedScript) {
|
||||
const hoistedValue = pageData.hoistedScript.value;
|
||||
|
@ -186,7 +198,14 @@ function buildManifest(
|
|||
});
|
||||
}
|
||||
|
||||
const links = sortedCSS(pageData).map((pth) => prefixAssetPath(pth));
|
||||
// may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
|
||||
const links: [] = [];
|
||||
|
||||
const styles = pageData.styles
|
||||
.sort(cssOrder)
|
||||
.map(({ sheet }) => sheet)
|
||||
.map((s) => (s.type === 'external' ? { ...s, src: prefixAssetPath(s.src) } : s))
|
||||
.reduce(mergeInlineCss, []);
|
||||
|
||||
routes.push({
|
||||
file: '',
|
||||
|
@ -197,6 +216,7 @@ function buildManifest(
|
|||
.filter((script) => script.stage === 'head-inline')
|
||||
.map(({ stage, content }) => ({ stage, children: content })),
|
||||
],
|
||||
styles,
|
||||
routeData: serializeRouteData(pageData.route, settings.config.trailingSlash),
|
||||
});
|
||||
}
|
||||
|
@ -233,7 +253,9 @@ export function pluginSSR(
|
|||
build: 'ssr',
|
||||
hooks: {
|
||||
'build:before': () => {
|
||||
let vitePlugin = ssr ? vitePluginSSR(internals, options.settings.adapter!) : undefined;
|
||||
let vitePlugin = ssr
|
||||
? vitePluginSSR(internals, options.settings.adapter!, options.settings.config)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
enforce: 'after-user-plugins',
|
||||
|
|
|
@ -8,7 +8,7 @@ import { fileURLToPath } from 'url';
|
|||
import * as vite from 'vite';
|
||||
import {
|
||||
createBuildInternals,
|
||||
eachPrerenderedPageData,
|
||||
eachPageData,
|
||||
type BuildInternals,
|
||||
} from '../../core/build/internal.js';
|
||||
import { emptyDir, removeEmptyDirs } from '../../core/fs/index.js';
|
||||
|
@ -290,8 +290,9 @@ async function runPostBuildHooks(
|
|||
*/
|
||||
async function cleanStaticOutput(opts: StaticBuildOptions, internals: BuildInternals) {
|
||||
const allStaticFiles = new Set();
|
||||
for (const pageData of eachPrerenderedPageData(internals)) {
|
||||
allStaticFiles.add(internals.pageToBundleMap.get(pageData.moduleSpecifier));
|
||||
for (const pageData of eachPageData(internals)) {
|
||||
if (pageData.route.prerender)
|
||||
allStaticFiles.add(internals.pageToBundleMap.get(pageData.moduleSpecifier));
|
||||
}
|
||||
const ssr = opts.settings.config.output === 'server';
|
||||
const out = ssr ? opts.buildConfig.server : getOutDirWithinCwd(opts.settings.config.outDir);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { default as vite, InlineConfig } from 'vite';
|
||||
import type {
|
||||
AstroConfig,
|
||||
AstroMiddlewareInstance,
|
||||
AstroSettings,
|
||||
BuildConfig,
|
||||
ComponentInstance,
|
||||
|
@ -16,14 +17,18 @@ export type ComponentPath = string;
|
|||
export type ViteID = string;
|
||||
export type PageOutput = AstroConfig['output'];
|
||||
|
||||
export type StylesheetAsset =
|
||||
| { type: 'inline'; content: string }
|
||||
| { type: 'external'; src: string };
|
||||
|
||||
export interface PageBuildData {
|
||||
component: ComponentPath;
|
||||
route: RouteData;
|
||||
moduleSpecifier: string;
|
||||
css: Map<string, { depth: number; order: number }>;
|
||||
propagatedStyles: Map<string, Set<string>>;
|
||||
propagatedStyles: Map<string, Set<StylesheetAsset>>;
|
||||
propagatedScripts: Map<string, Set<string>>;
|
||||
hoistedScript: { type: 'inline' | 'external'; value: string } | undefined;
|
||||
styles: Array<{ depth: number; order: number; sheet: StylesheetAsset }>;
|
||||
}
|
||||
export type AllPagesData = Record<ComponentPath, PageBuildData>;
|
||||
|
||||
|
@ -44,6 +49,7 @@ export interface StaticBuildOptions {
|
|||
|
||||
export interface SingleFileBuiltModule {
|
||||
pageMap: Map<ComponentPath, ComponentInstance>;
|
||||
middleware: AstroMiddlewareInstance<unknown>;
|
||||
renderers: SSRLoadedRenderer[];
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ export async function compile({
|
|||
sourcemap: 'both',
|
||||
internalURL: 'astro/server/index.js',
|
||||
astroGlobalArgs: JSON.stringify(astroConfig.site),
|
||||
scopedStyleStrategy: astroConfig.scopedStyleStrategy,
|
||||
resultScopedSlot: true,
|
||||
preprocessStyle: createStylePreprocessor({
|
||||
filename,
|
||||
|
|
|
@ -103,6 +103,8 @@ export function resolveFlags(flags: Partial<Flags>): CLIFlags {
|
|||
drafts: typeof flags.drafts === 'boolean' ? flags.drafts : undefined,
|
||||
experimentalAssets:
|
||||
typeof flags.experimentalAssets === 'boolean' ? flags.experimentalAssets : undefined,
|
||||
experimentalMiddleware:
|
||||
typeof flags.experimentalMiddleware === 'boolean' ? flags.experimentalMiddleware : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -136,6 +138,9 @@ function mergeCLIFlags(astroConfig: AstroUserConfig, flags: CLIFlags) {
|
|||
// TODO: Come back here and refactor to remove this expected error.
|
||||
astroConfig.server.open = flags.open;
|
||||
}
|
||||
if (typeof flags.experimentalMiddleware === 'boolean') {
|
||||
astroConfig.experimental.middleware = true;
|
||||
}
|
||||
return astroConfig;
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,8 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
|
|||
legacy: {},
|
||||
experimental: {
|
||||
assets: false,
|
||||
inlineStylesheets: 'never',
|
||||
middleware: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -73,6 +75,10 @@ export const AstroConfigSchema = z.object({
|
|||
.union([z.literal('static'), z.literal('server')])
|
||||
.optional()
|
||||
.default('static'),
|
||||
scopedStyleStrategy: z
|
||||
.union([z.literal('where'), z.literal('class')])
|
||||
.optional()
|
||||
.default('where'),
|
||||
adapter: z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}) }).optional(),
|
||||
integrations: z.preprocess(
|
||||
// preprocess
|
||||
|
@ -185,6 +191,11 @@ export const AstroConfigSchema = z.object({
|
|||
experimental: z
|
||||
.object({
|
||||
assets: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.assets),
|
||||
inlineStylesheets: z
|
||||
.enum(['always', 'auto', 'never'])
|
||||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.experimental.inlineStylesheets),
|
||||
middleware: z.oboolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.middleware),
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
|
|
|
@ -10,3 +10,6 @@ export const SUPPORTED_MARKDOWN_FILE_EXTENSIONS = [
|
|||
'.mdwn',
|
||||
'.md',
|
||||
] as const;
|
||||
|
||||
// The folder name where to find the middleware
|
||||
export const MIDDLEWARE_PATH_SEGMENT_NAME = 'middleware';
|
||||
|
|
|
@ -8,15 +8,18 @@ export async function call(options: SSROptions, logging: LogOptions) {
|
|||
const {
|
||||
env,
|
||||
preload: [, mod],
|
||||
middleware,
|
||||
} = options;
|
||||
const endpointHandler = mod as unknown as EndpointHandler;
|
||||
|
||||
const ctx = createRenderContext({
|
||||
const ctx = await createRenderContext({
|
||||
request: options.request,
|
||||
origin: options.origin,
|
||||
pathname: options.pathname,
|
||||
route: options.route,
|
||||
env,
|
||||
mod: endpointHandler as any,
|
||||
});
|
||||
|
||||
return await callEndpoint(endpointHandler, env, ctx, logging);
|
||||
return await callEndpoint(endpointHandler, env, ctx, logging, middleware);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import type { APIContext, AstroConfig, EndpointHandler, Params } from '../../@types/astro';
|
||||
import type {
|
||||
APIContext,
|
||||
AstroConfig,
|
||||
AstroMiddlewareInstance,
|
||||
EndpointHandler,
|
||||
EndpointOutput,
|
||||
MiddlewareEndpointHandler,
|
||||
Params,
|
||||
} from '../../@types/astro';
|
||||
import type { Environment, RenderContext } from '../render/index';
|
||||
|
||||
import { renderEndpoint } from '../../runtime/server/index.js';
|
||||
|
@ -6,9 +14,11 @@ import { ASTRO_VERSION } from '../constants.js';
|
|||
import { AstroCookies, attachToResponse } from '../cookies/index.js';
|
||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||
import { warn, type LogOptions } from '../logger/core.js';
|
||||
import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js';
|
||||
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||
import { isValueSerializable } from '../render/core.js';
|
||||
|
||||
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||
|
||||
type EndpointCallResult =
|
||||
| {
|
||||
|
@ -22,7 +32,7 @@ type EndpointCallResult =
|
|||
response: Response;
|
||||
};
|
||||
|
||||
function createAPIContext({
|
||||
export function createAPIContext({
|
||||
request,
|
||||
params,
|
||||
site,
|
||||
|
@ -35,7 +45,7 @@ function createAPIContext({
|
|||
props: Record<string, any>;
|
||||
adapterName?: string;
|
||||
}): APIContext {
|
||||
return {
|
||||
const context = {
|
||||
cookies: new AstroCookies(request),
|
||||
request,
|
||||
params,
|
||||
|
@ -51,7 +61,6 @@ function createAPIContext({
|
|||
});
|
||||
},
|
||||
url: new URL(request.url),
|
||||
// @ts-expect-error
|
||||
get clientAddress() {
|
||||
if (!(clientAddressSymbol in request)) {
|
||||
if (adapterName) {
|
||||
|
@ -66,44 +75,60 @@ function createAPIContext({
|
|||
|
||||
return Reflect.get(request, clientAddressSymbol);
|
||||
},
|
||||
};
|
||||
} as APIContext;
|
||||
|
||||
// We define a custom property, so we can check the value passed to locals
|
||||
Object.defineProperty(context, 'locals', {
|
||||
get() {
|
||||
return Reflect.get(request, clientLocalsSymbol);
|
||||
},
|
||||
set(val) {
|
||||
if (typeof val !== 'object') {
|
||||
throw new AstroError(AstroErrorData.LocalsNotAnObject);
|
||||
} else {
|
||||
Reflect.set(request, clientLocalsSymbol, val);
|
||||
}
|
||||
},
|
||||
});
|
||||
return context;
|
||||
}
|
||||
|
||||
export async function call(
|
||||
export async function call<MiddlewareResult = Response | EndpointOutput>(
|
||||
mod: EndpointHandler,
|
||||
env: Environment,
|
||||
ctx: RenderContext,
|
||||
logging: LogOptions
|
||||
logging: LogOptions,
|
||||
middleware?: AstroMiddlewareInstance<MiddlewareResult> | undefined
|
||||
): Promise<EndpointCallResult> {
|
||||
const paramsAndPropsResp = await getParamsAndProps({
|
||||
mod: mod as any,
|
||||
route: ctx.route,
|
||||
routeCache: env.routeCache,
|
||||
pathname: ctx.pathname,
|
||||
logging: env.logging,
|
||||
ssr: env.ssr,
|
||||
});
|
||||
|
||||
if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.NoMatchingStaticPathFound,
|
||||
message: AstroErrorData.NoMatchingStaticPathFound.message(ctx.pathname),
|
||||
hint: ctx.route?.component
|
||||
? AstroErrorData.NoMatchingStaticPathFound.hint([ctx.route?.component])
|
||||
: '',
|
||||
});
|
||||
}
|
||||
const [params, props] = paramsAndPropsResp;
|
||||
|
||||
const context = createAPIContext({
|
||||
request: ctx.request,
|
||||
params,
|
||||
props,
|
||||
params: ctx.params,
|
||||
props: ctx.props,
|
||||
site: env.site,
|
||||
adapterName: env.adapterName,
|
||||
});
|
||||
|
||||
const response = await renderEndpoint(mod, context, env.ssr);
|
||||
let response = await renderEndpoint(mod, context, env.ssr);
|
||||
if (middleware && middleware.onRequest) {
|
||||
if (response.body === null) {
|
||||
const onRequest = middleware.onRequest as MiddlewareEndpointHandler;
|
||||
response = await callMiddleware<Response | EndpointOutput>(onRequest, context, async () => {
|
||||
if (env.mode === 'development' && !isValueSerializable(context.locals)) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.LocalsNotSerializable,
|
||||
message: AstroErrorData.LocalsNotSerializable.message(ctx.pathname),
|
||||
});
|
||||
}
|
||||
return response;
|
||||
});
|
||||
} else {
|
||||
warn(
|
||||
env.logging,
|
||||
'middleware',
|
||||
"Middleware doesn't work for endpoints that return a simple body. The middleware will be disabled for this page."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (response instanceof Response) {
|
||||
attachToResponse(response, context.cookies);
|
||||
|
|
|
@ -628,6 +628,95 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati
|
|||
code: 3030,
|
||||
message: 'The response has already been sent to the browser and cannot be altered.',
|
||||
},
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @description
|
||||
* Thrown when the middleware does not return any data or call the `next` function.
|
||||
*
|
||||
* For example:
|
||||
* ```ts
|
||||
* import {defineMiddleware} from "astro/middleware";
|
||||
* export const onRequest = defineMiddleware((context, _) => {
|
||||
* // doesn't return anything or call `next`
|
||||
* context.locals.someData = false;
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
MiddlewareNoDataOrNextCalled: {
|
||||
title: "The middleware didn't return a response or call `next`",
|
||||
code: 3031,
|
||||
message:
|
||||
'The middleware needs to either return a `Response` object or call the `next` function.',
|
||||
},
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @description
|
||||
* Thrown in development mode when middleware returns something that is not a `Response` object.
|
||||
*
|
||||
* For example:
|
||||
* ```ts
|
||||
* import {defineMiddleware} from "astro/middleware";
|
||||
* export const onRequest = defineMiddleware(() => {
|
||||
* return "string"
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
MiddlewareNotAResponse: {
|
||||
title: 'The middleware returned something that is not a `Response` object',
|
||||
code: 3032,
|
||||
message: 'Any data returned from middleware must be a valid `Response` object.',
|
||||
},
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @description
|
||||
*
|
||||
* Thrown in development mode when `locals` is overwritten with something that is not an object
|
||||
*
|
||||
* For example:
|
||||
* ```ts
|
||||
* import {defineMiddleware} from "astro/middleware";
|
||||
* export const onRequest = defineMiddleware((context, next) => {
|
||||
* context.locals = 1541;
|
||||
* return next();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
LocalsNotAnObject: {
|
||||
title: 'Value assigned to `locals` is not accepted',
|
||||
code: 3033,
|
||||
message:
|
||||
'`locals` can only be assigned to an object. Other values like numbers, strings, etc. are not accepted.',
|
||||
hint: 'If you tried to remove some information from the `locals` object, try to use `delete` or set the property to `undefined`.',
|
||||
},
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @description
|
||||
* Thrown in development mode when a user attempts to store something that is not serializable in `locals`.
|
||||
*
|
||||
* For example:
|
||||
* ```ts
|
||||
* import {defineMiddleware} from "astro/middleware";
|
||||
* export const onRequest = defineMiddleware((context, next) => {
|
||||
* context.locals = {
|
||||
* foo() {
|
||||
* alert("Hello world!")
|
||||
* }
|
||||
* };
|
||||
* return next();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
LocalsNotSerializable: {
|
||||
title: '`Astro.locals` is not serializable',
|
||||
code: 3034,
|
||||
message: (href: string) => {
|
||||
return `The information stored in \`Astro.locals\` for the path "${href}" is not serializable.\nMake sure you store only serializable data.`;
|
||||
},
|
||||
},
|
||||
// No headings here, that way Vite errors are merged with Astro ones in the docs, which makes more sense to users.
|
||||
// Vite Errors - 4xxx
|
||||
/**
|
||||
|
|
99
packages/astro/src/core/middleware/callMiddleware.ts
Normal file
99
packages/astro/src/core/middleware/callMiddleware.ts
Normal file
|
@ -0,0 +1,99 @@
|
|||
import type { APIContext, MiddlewareHandler, MiddlewareNext } from '../../@types/astro';
|
||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||
|
||||
/**
|
||||
* Utility function that is in charge of calling the middleware.
|
||||
*
|
||||
* It accepts a `R` generic, which usually is the `Response` returned.
|
||||
* It is a generic because endpoints can return a different payload.
|
||||
*
|
||||
* When calling a middleware, we provide a `next` function, this function might or
|
||||
* might not be called.
|
||||
*
|
||||
* A middleware, to behave correctly, can:
|
||||
* - return a `Response`;
|
||||
* - call `next`;
|
||||
*
|
||||
* Failing doing so will result an error. A middleware can call `next` and do not return a
|
||||
* response. A middleware can not call `next` and return a new `Response` from scratch (maybe with a redirect).
|
||||
*
|
||||
* ```js
|
||||
* const onRequest = async (context, next) => {
|
||||
* const response = await next(context);
|
||||
* return response;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* const onRequest = async (context, next) => {
|
||||
* context.locals = "foo";
|
||||
* next();
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param onRequest The function called which accepts a `context` and a `resolve` function
|
||||
* @param apiContext The API context
|
||||
* @param responseFunction A callback function that should return a promise with the response
|
||||
*/
|
||||
export async function callMiddleware<R>(
|
||||
onRequest: MiddlewareHandler<R>,
|
||||
apiContext: APIContext,
|
||||
responseFunction: () => Promise<R>
|
||||
): Promise<Response | R> {
|
||||
let resolveResolve: any;
|
||||
new Promise((resolve) => {
|
||||
resolveResolve = resolve;
|
||||
});
|
||||
|
||||
let nextCalled = false;
|
||||
const next: MiddlewareNext<R> = async () => {
|
||||
nextCalled = true;
|
||||
return await responseFunction();
|
||||
};
|
||||
|
||||
let middlewarePromise = onRequest(apiContext, next);
|
||||
|
||||
return await Promise.resolve(middlewarePromise).then(async (value) => {
|
||||
// first we check if `next` was called
|
||||
if (nextCalled) {
|
||||
/**
|
||||
* Then we check if a value is returned. If so, we need to return the value returned by the
|
||||
* middleware.
|
||||
* e.g.
|
||||
* ```js
|
||||
* const response = await next();
|
||||
* const new Response(null, { status: 500, headers: response.headers });
|
||||
* ```
|
||||
*/
|
||||
if (typeof value !== 'undefined') {
|
||||
if (value instanceof Response === false) {
|
||||
throw new AstroError(AstroErrorData.MiddlewareNotAResponse);
|
||||
}
|
||||
return value as R;
|
||||
} else {
|
||||
/**
|
||||
* Here we handle the case where `next` was called and returned nothing.
|
||||
*/
|
||||
const responseResult = await responseFunction();
|
||||
return responseResult;
|
||||
}
|
||||
} else if (typeof value === 'undefined') {
|
||||
/**
|
||||
* There might be cases where `next` isn't called and the middleware **must** return
|
||||
* something.
|
||||
*
|
||||
* If not thing is returned, then we raise an Astro error.
|
||||
*/
|
||||
throw new AstroError(AstroErrorData.MiddlewareNoDataOrNextCalled);
|
||||
} else if (value instanceof Response === false) {
|
||||
throw new AstroError(AstroErrorData.MiddlewareNotAResponse);
|
||||
} else {
|
||||
// Middleware did not call resolve and returned a value
|
||||
return value as R;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isEndpointResult(response: any): boolean {
|
||||
return response && typeof response.body !== 'undefined';
|
||||
}
|
9
packages/astro/src/core/middleware/index.ts
Normal file
9
packages/astro/src/core/middleware/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import type { MiddlewareResponseHandler } from '../../@types/astro';
|
||||
import { sequence } from './sequence.js';
|
||||
|
||||
function defineMiddleware(fn: MiddlewareResponseHandler) {
|
||||
return fn;
|
||||
}
|
||||
|
||||
// NOTE: this export must export only the functions that will be exposed to user-land as officials APIs
|
||||
export { sequence, defineMiddleware };
|
22
packages/astro/src/core/middleware/loadMiddleware.ts
Normal file
22
packages/astro/src/core/middleware/loadMiddleware.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import type { AstroSettings } from '../../@types/astro';
|
||||
import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../constants.js';
|
||||
import type { ModuleLoader } from '../module-loader';
|
||||
|
||||
/**
|
||||
* It accepts a module loader and the astro settings, and it attempts to load the middlewares defined in the configuration.
|
||||
*
|
||||
* If not middlewares were not set, the function returns an empty array.
|
||||
*/
|
||||
export async function loadMiddleware(
|
||||
moduleLoader: ModuleLoader,
|
||||
srcDir: AstroSettings['config']['srcDir']
|
||||
) {
|
||||
// can't use node Node.js builtins
|
||||
let middlewarePath = srcDir.pathname + '/' + MIDDLEWARE_PATH_SEGMENT_NAME;
|
||||
try {
|
||||
const module = await moduleLoader.import(middlewarePath);
|
||||
return module;
|
||||
} catch {
|
||||
return void 0;
|
||||
}
|
||||
}
|
36
packages/astro/src/core/middleware/sequence.ts
Normal file
36
packages/astro/src/core/middleware/sequence.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import type { APIContext, MiddlewareResponseHandler } from '../../@types/astro';
|
||||
import { defineMiddleware } from './index.js';
|
||||
|
||||
// From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js
|
||||
/**
|
||||
*
|
||||
* It accepts one or more middleware handlers and makes sure that they are run in sequence.
|
||||
*/
|
||||
export function sequence(...handlers: MiddlewareResponseHandler[]): MiddlewareResponseHandler {
|
||||
const length = handlers.length;
|
||||
if (!length) {
|
||||
const handler: MiddlewareResponseHandler = defineMiddleware((context, next) => {
|
||||
return next();
|
||||
});
|
||||
return handler;
|
||||
}
|
||||
|
||||
return defineMiddleware((context, next) => {
|
||||
return applyHandle(0, context);
|
||||
|
||||
function applyHandle(i: number, handleContext: APIContext) {
|
||||
const handle = handlers[i];
|
||||
// @ts-expect-error
|
||||
// SAFETY: Usually `next` always returns something in user land, but in `sequence` we are actually
|
||||
// doing a loop over all the `next` functions, and eventually we call the last `next` that returns the `Response`.
|
||||
const result = handle(handleContext, async () => {
|
||||
if (i < length - 1) {
|
||||
return applyHandle(i + 1, handleContext);
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -52,7 +52,18 @@ function isString(path: unknown): path is string {
|
|||
}
|
||||
|
||||
export function joinPaths(...paths: (string | undefined)[]) {
|
||||
return paths.filter(isString).map(trimSlashes).join('/');
|
||||
return paths
|
||||
.filter(isString)
|
||||
.map((path, i) => {
|
||||
if (i === 0) {
|
||||
return removeTrailingForwardSlash(path);
|
||||
} else if (i === paths.length - 1) {
|
||||
return removeLeadingForwardSlash(path);
|
||||
} else {
|
||||
return trimSlashes(path);
|
||||
}
|
||||
})
|
||||
.join('/');
|
||||
}
|
||||
|
||||
export function removeFileExtension(path: string) {
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
import type { RouteData, SSRElement, SSRResult } from '../../@types/astro';
|
||||
import type {
|
||||
ComponentInstance,
|
||||
Params,
|
||||
Props,
|
||||
RouteData,
|
||||
SSRElement,
|
||||
SSRResult,
|
||||
} from '../../@types/astro';
|
||||
import { getParamsAndPropsOrThrow } from './core.js';
|
||||
import type { Environment } from './environment';
|
||||
|
||||
/**
|
||||
* The RenderContext represents the parts of rendering that are specific to one request.
|
||||
|
@ -14,22 +23,38 @@ export interface RenderContext {
|
|||
componentMetadata?: SSRResult['componentMetadata'];
|
||||
route?: RouteData;
|
||||
status?: number;
|
||||
params: Params;
|
||||
props: Props;
|
||||
}
|
||||
|
||||
export type CreateRenderContextArgs = Partial<RenderContext> & {
|
||||
origin?: string;
|
||||
request: RenderContext['request'];
|
||||
mod: ComponentInstance;
|
||||
env: Environment;
|
||||
};
|
||||
|
||||
export function createRenderContext(options: CreateRenderContextArgs): RenderContext {
|
||||
export async function createRenderContext(
|
||||
options: CreateRenderContextArgs
|
||||
): Promise<RenderContext> {
|
||||
const request = options.request;
|
||||
const url = new URL(request.url);
|
||||
const origin = options.origin ?? url.origin;
|
||||
const pathname = options.pathname ?? url.pathname;
|
||||
const [params, props] = await getParamsAndPropsOrThrow({
|
||||
mod: options.mod as any,
|
||||
route: options.route,
|
||||
routeCache: options.env.routeCache,
|
||||
pathname: pathname,
|
||||
logging: options.env.logging,
|
||||
ssr: options.env.ssr,
|
||||
});
|
||||
return {
|
||||
...options,
|
||||
origin,
|
||||
pathname,
|
||||
url,
|
||||
params,
|
||||
props,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import type { ComponentInstance, Params, Props, RouteData } from '../../@types/astro';
|
||||
import type { LogOptions } from '../logger/core.js';
|
||||
import type { RenderContext } from './context.js';
|
||||
import type { Environment } from './environment.js';
|
||||
|
||||
import type { APIContext, ComponentInstance, Params, Props, RouteData } from '../../@types/astro';
|
||||
import { renderPage as runtimeRenderPage } from '../../runtime/server/index.js';
|
||||
import { attachToResponse } from '../cookies/index.js';
|
||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||
import type { LogOptions } from '../logger/core.js';
|
||||
import { getParams } from '../routing/params.js';
|
||||
import type { RenderContext } from './context.js';
|
||||
import type { Environment } from './environment.js';
|
||||
import { createResult } from './result.js';
|
||||
import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js';
|
||||
|
||||
|
@ -23,6 +22,26 @@ export const enum GetParamsAndPropsError {
|
|||
NoMatchingStaticPath,
|
||||
}
|
||||
|
||||
/**
|
||||
* It retrieves `Params` and `Props`, or throws an error
|
||||
* if they are not correctly retrieved.
|
||||
*/
|
||||
export async function getParamsAndPropsOrThrow(
|
||||
options: GetParamsAndPropsOptions
|
||||
): Promise<[Params, Props]> {
|
||||
let paramsAndPropsResp = await getParamsAndProps(options);
|
||||
if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.NoMatchingStaticPathFound,
|
||||
message: AstroErrorData.NoMatchingStaticPathFound.message(options.pathname),
|
||||
hint: options.route?.component
|
||||
? AstroErrorData.NoMatchingStaticPathFound.hint([options.route?.component])
|
||||
: '',
|
||||
});
|
||||
}
|
||||
return paramsAndPropsResp;
|
||||
}
|
||||
|
||||
export async function getParamsAndProps(
|
||||
opts: GetParamsAndPropsOptions
|
||||
): Promise<[Params, Props] | GetParamsAndPropsError> {
|
||||
|
@ -84,65 +103,63 @@ export async function getParamsAndProps(
|
|||
return [params, pageProps];
|
||||
}
|
||||
|
||||
export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env: Environment) {
|
||||
const paramsAndPropsRes = await getParamsAndProps({
|
||||
logging: env.logging,
|
||||
mod,
|
||||
route: ctx.route,
|
||||
routeCache: env.routeCache,
|
||||
pathname: ctx.pathname,
|
||||
ssr: env.ssr,
|
||||
});
|
||||
|
||||
if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.NoMatchingStaticPathFound,
|
||||
message: AstroErrorData.NoMatchingStaticPathFound.message(ctx.pathname),
|
||||
hint: ctx.route?.component
|
||||
? AstroErrorData.NoMatchingStaticPathFound.hint([ctx.route?.component])
|
||||
: '',
|
||||
});
|
||||
}
|
||||
const [params, pageProps] = paramsAndPropsRes;
|
||||
export type RenderPage = {
|
||||
mod: ComponentInstance;
|
||||
renderContext: RenderContext;
|
||||
env: Environment;
|
||||
apiContext?: APIContext;
|
||||
};
|
||||
|
||||
export async function renderPage({ mod, renderContext, env, apiContext }: RenderPage) {
|
||||
// Validate the page component before rendering the page
|
||||
const Component = mod.default;
|
||||
if (!Component)
|
||||
throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
|
||||
|
||||
let locals = {};
|
||||
if (apiContext) {
|
||||
if (env.mode === 'development' && !isValueSerializable(apiContext.locals)) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.LocalsNotSerializable,
|
||||
message: AstroErrorData.LocalsNotSerializable.message(renderContext.pathname),
|
||||
});
|
||||
}
|
||||
locals = apiContext.locals;
|
||||
}
|
||||
const result = createResult({
|
||||
adapterName: env.adapterName,
|
||||
links: ctx.links,
|
||||
styles: ctx.styles,
|
||||
links: renderContext.links,
|
||||
styles: renderContext.styles,
|
||||
logging: env.logging,
|
||||
markdown: env.markdown,
|
||||
mode: env.mode,
|
||||
origin: ctx.origin,
|
||||
params,
|
||||
props: pageProps,
|
||||
pathname: ctx.pathname,
|
||||
componentMetadata: ctx.componentMetadata,
|
||||
origin: renderContext.origin,
|
||||
params: renderContext.params,
|
||||
props: renderContext.props,
|
||||
pathname: renderContext.pathname,
|
||||
componentMetadata: renderContext.componentMetadata,
|
||||
resolve: env.resolve,
|
||||
renderers: env.renderers,
|
||||
request: ctx.request,
|
||||
request: renderContext.request,
|
||||
site: env.site,
|
||||
scripts: ctx.scripts,
|
||||
scripts: renderContext.scripts,
|
||||
ssr: env.ssr,
|
||||
status: ctx.status ?? 200,
|
||||
status: renderContext.status ?? 200,
|
||||
locals,
|
||||
});
|
||||
|
||||
// Support `export const components` for `MDX` pages
|
||||
if (typeof (mod as any).components === 'object') {
|
||||
Object.assign(pageProps, { components: (mod as any).components });
|
||||
Object.assign(renderContext.props, { components: (mod as any).components });
|
||||
}
|
||||
|
||||
const response = await runtimeRenderPage(
|
||||
let response = await runtimeRenderPage(
|
||||
result,
|
||||
Component,
|
||||
pageProps,
|
||||
renderContext.props,
|
||||
null,
|
||||
env.streaming,
|
||||
ctx.route
|
||||
renderContext.route
|
||||
);
|
||||
|
||||
// If there is an Astro.cookies instance, attach it to the response so that
|
||||
|
@ -153,3 +170,57 @@ export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env
|
|||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether any value can is serializable.
|
||||
*
|
||||
* A serializable value contains plain values. For example, `Proxy`, `Set`, `Map`, functions, etc.
|
||||
* are not serializable objects.
|
||||
*
|
||||
* @param object
|
||||
*/
|
||||
export function isValueSerializable(value: unknown): boolean {
|
||||
let type = typeof value;
|
||||
let plainObject = true;
|
||||
if (type === 'object' && isPlainObject(value)) {
|
||||
for (const [, nestedValue] of Object.entries(value)) {
|
||||
if (!isValueSerializable(nestedValue)) {
|
||||
plainObject = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
plainObject = false;
|
||||
}
|
||||
let result =
|
||||
value === null ||
|
||||
type === 'string' ||
|
||||
type === 'number' ||
|
||||
type === 'boolean' ||
|
||||
Array.isArray(value) ||
|
||||
plainObject;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* From [redux-toolkit](https://github.com/reduxjs/redux-toolkit/blob/master/packages/toolkit/src/isPlainObject.ts)
|
||||
*
|
||||
* Returns true if the passed value is "plain" object, i.e. an object whose
|
||||
* prototype is the root `Object.prototype`. This includes objects created
|
||||
* using object literals, but not for instance for class instances.
|
||||
*/
|
||||
function isPlainObject(value: unknown): value is object {
|
||||
if (typeof value !== 'object' || value === null) return false;
|
||||
|
||||
let proto = Object.getPrototypeOf(value);
|
||||
if (proto === null) return true;
|
||||
|
||||
let baseProto = proto;
|
||||
while (Object.getPrototypeOf(baseProto) !== null) {
|
||||
baseProto = Object.getPrototypeOf(baseProto);
|
||||
}
|
||||
|
||||
return proto === baseProto;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import { fileURLToPath } from 'url';
|
||||
import type {
|
||||
AstroMiddlewareInstance,
|
||||
AstroSettings,
|
||||
ComponentInstance,
|
||||
MiddlewareResponseHandler,
|
||||
RouteData,
|
||||
SSRElement,
|
||||
SSRLoadedRenderer,
|
||||
} from '../../../@types/astro';
|
||||
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
|
||||
import { createAPIContext } from '../../endpoint/index.js';
|
||||
import { enhanceViteSSRError } from '../../errors/dev/index.js';
|
||||
import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js';
|
||||
import { callMiddleware } from '../../middleware/callMiddleware.js';
|
||||
import type { ModuleLoader } from '../../module-loader/index';
|
||||
import { isPage, resolveIdToUrl, viteID } from '../../util.js';
|
||||
import { createRenderContext, renderPage as coreRenderPage } from '../index.js';
|
||||
|
@ -35,6 +39,10 @@ export interface SSROptions {
|
|||
request: Request;
|
||||
/** optional, in case we need to render something outside of a dev server */
|
||||
route?: RouteData;
|
||||
/**
|
||||
* Optional middlewares
|
||||
*/
|
||||
middleware?: AstroMiddlewareInstance<unknown>;
|
||||
}
|
||||
|
||||
export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance];
|
||||
|
@ -158,8 +166,9 @@ export async function renderPage(options: SSROptions): Promise<Response> {
|
|||
env: options.env,
|
||||
filePath: options.filePath,
|
||||
});
|
||||
const { env } = options;
|
||||
|
||||
const ctx = createRenderContext({
|
||||
const renderContext = await createRenderContext({
|
||||
request: options.request,
|
||||
origin: options.origin,
|
||||
pathname: options.pathname,
|
||||
|
@ -168,7 +177,25 @@ export async function renderPage(options: SSROptions): Promise<Response> {
|
|||
styles,
|
||||
componentMetadata: metadata,
|
||||
route: options.route,
|
||||
mod,
|
||||
env,
|
||||
});
|
||||
if (options.middleware) {
|
||||
if (options.middleware && options.middleware.onRequest) {
|
||||
const apiContext = createAPIContext({
|
||||
request: options.request,
|
||||
params: renderContext.params,
|
||||
props: renderContext.props,
|
||||
adapterName: options.env.adapterName,
|
||||
});
|
||||
|
||||
return await coreRenderPage(mod, ctx, options.env); // NOTE: without "await", errors won’t get caught below
|
||||
const onRequest = options.middleware.onRequest as MiddlewareResponseHandler;
|
||||
const response = await callMiddleware<Response>(onRequest, apiContext, () => {
|
||||
return coreRenderPage({ mod, renderContext, env: options.env, apiContext });
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
return await coreRenderPage({ mod, renderContext, env: options.env }); // NOTE: without "await", errors won’t get caught below
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
export { createRenderContext } from './context.js';
|
||||
export type { RenderContext } from './context.js';
|
||||
export { getParamsAndProps, GetParamsAndPropsError, renderPage } from './core.js';
|
||||
export {
|
||||
getParamsAndProps,
|
||||
GetParamsAndPropsError,
|
||||
getParamsAndPropsOrThrow,
|
||||
renderPage,
|
||||
} from './core.js';
|
||||
export type { Environment } from './environment';
|
||||
export { createBasicEnvironment, createEnvironment } from './environment.js';
|
||||
export { loadRenderer } from './renderer.js';
|
||||
|
|
|
@ -50,6 +50,7 @@ export interface CreateResultArgs {
|
|||
componentMetadata?: SSRResult['componentMetadata'];
|
||||
request: Request;
|
||||
status: number;
|
||||
locals: App.Locals;
|
||||
}
|
||||
|
||||
function getFunctionExpression(slot: any) {
|
||||
|
@ -131,7 +132,7 @@ class Slots {
|
|||
let renderMarkdown: any = null;
|
||||
|
||||
export function createResult(args: CreateResultArgs): SSRResult {
|
||||
const { markdown, params, pathname, renderers, request, resolve } = args;
|
||||
const { markdown, params, pathname, renderers, request, resolve, locals } = args;
|
||||
|
||||
const url = new URL(request.url);
|
||||
const headers = new Headers();
|
||||
|
@ -200,6 +201,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
|||
},
|
||||
params,
|
||||
props,
|
||||
locals,
|
||||
request,
|
||||
url,
|
||||
redirect: args.ssr
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import slashify from 'slash';
|
||||
import type { SSRElement } from '../../@types/astro';
|
||||
import { joinPaths, prependForwardSlash } from '../../core/path.js';
|
||||
import type { StylesheetAsset } from '../app/types';
|
||||
|
||||
export function createAssetLink(href: string, base?: string, assetsPrefix?: string): string {
|
||||
if (assetsPrefix) {
|
||||
|
@ -12,28 +13,35 @@ export function createAssetLink(href: string, base?: string, assetsPrefix?: stri
|
|||
}
|
||||
}
|
||||
|
||||
export function createLinkStylesheetElement(
|
||||
href: string,
|
||||
export function createStylesheetElement(
|
||||
stylesheet: StylesheetAsset,
|
||||
base?: string,
|
||||
assetsPrefix?: string
|
||||
): SSRElement {
|
||||
return {
|
||||
props: {
|
||||
rel: 'stylesheet',
|
||||
href: createAssetLink(href, base, assetsPrefix),
|
||||
},
|
||||
children: '',
|
||||
};
|
||||
if (stylesheet.type === 'inline') {
|
||||
return {
|
||||
props: {
|
||||
type: 'text/css',
|
||||
},
|
||||
children: stylesheet.content,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
props: {
|
||||
rel: 'stylesheet',
|
||||
href: createAssetLink(stylesheet.src, base, assetsPrefix),
|
||||
},
|
||||
children: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function createLinkStylesheetElementSet(
|
||||
hrefs: string[],
|
||||
export function createStylesheetElementSet(
|
||||
stylesheets: StylesheetAsset[],
|
||||
base?: string,
|
||||
assetsPrefix?: string
|
||||
) {
|
||||
return new Set<SSRElement>(
|
||||
hrefs.map((href) => createLinkStylesheetElement(href, base, assetsPrefix))
|
||||
);
|
||||
): Set<SSRElement> {
|
||||
return new Set(stylesheets.map((s) => createStylesheetElement(s, base, assetsPrefix)));
|
||||
}
|
||||
|
||||
export function createModuleScriptElement(
|
||||
|
|
|
@ -16,6 +16,7 @@ export interface CreateRequestOptions {
|
|||
}
|
||||
|
||||
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||
|
||||
export function createRequest({
|
||||
url,
|
||||
|
@ -65,5 +66,7 @@ export function createRequest({
|
|||
Reflect.set(request, clientAddressSymbol, clientAddress);
|
||||
}
|
||||
|
||||
Reflect.set(request, clientLocalsSymbol, {});
|
||||
|
||||
return request;
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ export async function runHookConfigSetup({
|
|||
|
||||
let updatedConfig: AstroConfig = { ...settings.config };
|
||||
let updatedSettings: AstroSettings = { ...settings, config: updatedConfig };
|
||||
|
||||
for (const integration of settings.config.integrations) {
|
||||
/**
|
||||
* By making integration hooks optional, Astro can now ignore null or undefined Integrations
|
||||
|
@ -68,7 +69,7 @@ export async function runHookConfigSetup({
|
|||
* ]
|
||||
* ```
|
||||
*/
|
||||
if (integration?.hooks?.['astro:config:setup']) {
|
||||
if (integration.hooks?.['astro:config:setup']) {
|
||||
const hooks: HookParameters<'astro:config:setup'> = {
|
||||
config: updatedConfig,
|
||||
command,
|
||||
|
|
|
@ -19,7 +19,7 @@ function getHandlerFromModule(mod: EndpointHandler, method: string) {
|
|||
|
||||
/** Renders an endpoint request to completion, returning the body. */
|
||||
export async function renderEndpoint(mod: EndpointHandler, context: APIContext, ssr: boolean) {
|
||||
const { request, params } = context;
|
||||
const { request, params, locals } = context;
|
||||
const chosenMethod = request.method?.toLowerCase();
|
||||
const handler = getHandlerFromModule(mod, chosenMethod);
|
||||
if (!ssr && ssr === false && chosenMethod && chosenMethod !== 'get') {
|
||||
|
|
|
@ -19,7 +19,6 @@ export {
|
|||
renderScriptElement,
|
||||
renderSlot,
|
||||
renderSlotToString,
|
||||
renderStyleElement,
|
||||
renderTemplate as render,
|
||||
renderTemplate,
|
||||
renderToString,
|
||||
|
|
|
@ -263,7 +263,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
|||
if (isPage || renderer?.name === 'astro:jsx') {
|
||||
yield html;
|
||||
} else if (html && html.length > 0) {
|
||||
yield markHTMLString(html.replace(/\<\/?astro-slot\>/g, ''));
|
||||
yield markHTMLString(html.replace(/\<\/?astro-slot\b[^>]*>/g, ''));
|
||||
} else {
|
||||
yield '';
|
||||
}
|
||||
|
|
|
@ -16,7 +16,11 @@ export function renderAllHeadContent(result: SSRResult) {
|
|||
result._metadata.hasRenderedHead = true;
|
||||
const styles = Array.from(result.styles)
|
||||
.filter(uniqueElements)
|
||||
.map((style) => renderElement('style', style));
|
||||
.map((style) =>
|
||||
style.props.rel === 'stylesheet'
|
||||
? renderElement('link', style)
|
||||
: renderElement('style', style)
|
||||
);
|
||||
// Clear result.styles so that any new styles added will be inlined.
|
||||
result.styles.clear();
|
||||
const scripts = Array.from(result.scripts)
|
||||
|
|
|
@ -11,6 +11,6 @@ export { renderHTMLElement } from './dom.js';
|
|||
export { maybeRenderHead, renderHead } from './head.js';
|
||||
export { renderPage } from './page.js';
|
||||
export { renderSlot, renderSlotToString, type ComponentSlots } from './slot.js';
|
||||
export { renderScriptElement, renderStyleElement, renderUniqueStylesheet } from './tags.js';
|
||||
export { renderScriptElement, renderUniqueStylesheet } from './tags.js';
|
||||
export type { RenderInstruction } from './types';
|
||||
export { addAttribute, defineScriptVars, voidElementNames } from './util.js';
|
||||
|
|
|
@ -1,15 +1,7 @@
|
|||
import type { SSRElement, SSRResult } from '../../../@types/astro';
|
||||
import type { StylesheetAsset } from '../../../core/app/types';
|
||||
import { renderElement } from './util.js';
|
||||
|
||||
const stylesheetRel = 'stylesheet';
|
||||
|
||||
export function renderStyleElement(children: string) {
|
||||
return renderElement('style', {
|
||||
props: {},
|
||||
children,
|
||||
});
|
||||
}
|
||||
|
||||
export function renderScriptElement({ props, children }: SSRElement) {
|
||||
return renderElement('script', {
|
||||
props,
|
||||
|
@ -17,26 +9,14 @@ export function renderScriptElement({ props, children }: SSRElement) {
|
|||
});
|
||||
}
|
||||
|
||||
export function renderStylesheet({ href }: { href: string }) {
|
||||
return renderElement(
|
||||
'link',
|
||||
{
|
||||
props: {
|
||||
rel: stylesheetRel,
|
||||
href,
|
||||
},
|
||||
children: '',
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
export function renderUniqueStylesheet(result: SSRResult, link: { href: string }) {
|
||||
for (const existingLink of result.links) {
|
||||
if (existingLink.props.rel === stylesheetRel && existingLink.props.href === link.href) {
|
||||
return '';
|
||||
}
|
||||
export function renderUniqueStylesheet(result: SSRResult, sheet: StylesheetAsset) {
|
||||
if (sheet.type === 'external') {
|
||||
if (Array.from(result.styles).some((s) => s.props.href === sheet.src)) return '';
|
||||
return renderElement('link', { props: { rel: 'stylesheet', href: sheet.src }, children: '' });
|
||||
}
|
||||
|
||||
return renderStylesheet(link);
|
||||
if (sheet.type === 'inline') {
|
||||
if (Array.from(result.styles).some((s) => s.children.includes(sheet.content))) return '';
|
||||
return renderElement('style', { props: { type: 'text/css' }, children: sheet.content });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,9 +57,10 @@ export async function writeWebResponse(res: http.ServerResponse, webResponse: Re
|
|||
|
||||
// Attach any set-cookie headers added via Astro.cookies.set()
|
||||
const setCookieHeaders = Array.from(getSetCookiesFromResponse(webResponse));
|
||||
setCookieHeaders.forEach((cookie) => {
|
||||
headers.append('set-cookie', cookie);
|
||||
});
|
||||
if (setCookieHeaders.length) {
|
||||
// Always use `res.setHeader` because headers.append causes them to be concatenated.
|
||||
res.setHeader('set-cookie', setCookieHeaders);
|
||||
}
|
||||
|
||||
const _headers = Object.fromEntries(headers.entries());
|
||||
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
import type http from 'http';
|
||||
import mime from 'mime';
|
||||
import type { AstroSettings, ComponentInstance, ManifestData, RouteData } from '../@types/astro';
|
||||
import type {
|
||||
ComponentPreload,
|
||||
DevelopmentEnvironment,
|
||||
SSROptions,
|
||||
} from '../core/render/dev/index';
|
||||
|
||||
import type { ComponentInstance, ManifestData, RouteData } from '../@types/astro';
|
||||
import { attachToResponse } from '../core/cookies/index.js';
|
||||
import { call as callEndpoint } from '../core/endpoint/dev/index.js';
|
||||
import { throwIfRedirectNotAllowed } from '../core/endpoint/index.js';
|
||||
import { AstroErrorData } from '../core/errors/index.js';
|
||||
import { warn } from '../core/logger/core.js';
|
||||
import { appendForwardSlash } from '../core/path.js';
|
||||
import { loadMiddleware } from '../core/middleware/loadMiddleware.js';
|
||||
import type {
|
||||
ComponentPreload,
|
||||
DevelopmentEnvironment,
|
||||
SSROptions,
|
||||
} from '../core/render/dev/index';
|
||||
import { preload, renderPage } from '../core/render/dev/index.js';
|
||||
import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/index.js';
|
||||
import { createRequest } from '../core/request.js';
|
||||
import { matchAllRoutes } from '../core/routing/index.js';
|
||||
import { resolvePages } from '../core/util.js';
|
||||
import { log404 } from './common.js';
|
||||
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
|
||||
|
||||
|
@ -35,11 +33,9 @@ interface MatchedRoute {
|
|||
mod: ComponentInstance;
|
||||
}
|
||||
|
||||
function getCustom404Route({ config }: AstroSettings, manifest: ManifestData) {
|
||||
// For Windows compat, use relative page paths to match the 404 route
|
||||
const relPages = resolvePages(config).href.replace(config.root.href, '');
|
||||
const pattern = new RegExp(`${appendForwardSlash(relPages)}404.(astro|md)`);
|
||||
return manifest.routes.find((r) => r.component.match(pattern));
|
||||
function getCustom404Route(manifest: ManifestData): RouteData | undefined {
|
||||
const route404 = /^\/404\/?$/;
|
||||
return manifest.routes.find((r) => route404.test(r.route));
|
||||
}
|
||||
|
||||
export async function matchRoute(
|
||||
|
@ -97,7 +93,7 @@ export async function matchRoute(
|
|||
}
|
||||
|
||||
log404(logging, pathname);
|
||||
const custom404 = getCustom404Route(settings, manifest);
|
||||
const custom404 = getCustom404Route(manifest);
|
||||
|
||||
if (custom404) {
|
||||
const filePath = new URL(`./${custom404.component}`, settings.config.root);
|
||||
|
@ -173,7 +169,12 @@ export async function handleRoute(
|
|||
request,
|
||||
route,
|
||||
};
|
||||
|
||||
if (env.settings.config.experimental.middleware) {
|
||||
const middleware = await loadMiddleware(env.loader, env.settings.config.srcDir);
|
||||
if (middleware) {
|
||||
options.middleware = middleware;
|
||||
}
|
||||
}
|
||||
// Route successfully matched! Render it.
|
||||
if (route.type === 'endpoint') {
|
||||
const result = await callEndpoint(options, logging);
|
||||
|
|
|
@ -31,7 +31,14 @@ function getPrivateEnv(
|
|||
// Ignore public env var
|
||||
if (envPrefixes.every((prefix) => !key.startsWith(prefix))) {
|
||||
if (typeof process.env[key] !== 'undefined') {
|
||||
privateEnv[key] = `process.env.${key}`;
|
||||
const value = process.env[key];
|
||||
// Boolean values should be inlined to support `export const prerender`
|
||||
// We already know that these are NOT sensitive values, so inlining is safe
|
||||
if (value === '0' || value === '1' || value === 'true' || value === 'false') {
|
||||
privateEnv[key] = value;
|
||||
} else {
|
||||
privateEnv[key] = `process.env.${key}`;
|
||||
}
|
||||
} else {
|
||||
privateEnv[key] = JSON.stringify(fullEnv[key]);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import babel from '@babel/core';
|
|||
import * as colors from 'kleur/colors';
|
||||
import path from 'path';
|
||||
import { CONTENT_FLAG, PROPAGATED_ASSET_FLAG } from '../content/index.js';
|
||||
import { astroEntryPrefix } from '../core/build/plugins/plugin-component-entry.js';
|
||||
import { error } from '../core/logger/core.js';
|
||||
import { removeQueryString } from '../core/path.js';
|
||||
import { detectImportSource } from './import-source.js';
|
||||
|
@ -139,7 +140,9 @@ export default function jsx({ settings, logging }: AstroPluginJSXOptions): Plugi
|
|||
},
|
||||
async transform(code, id, opts) {
|
||||
const ssr = Boolean(opts?.ssr);
|
||||
if (SPECIAL_QUERY_REGEX.test(id)) {
|
||||
// Skip special queries and astro entries. We skip astro entries here as we know it doesn't contain
|
||||
// JSX code, and also because we can't detect the import source to apply JSX transforms.
|
||||
if (SPECIAL_QUERY_REGEX.test(id) || id.startsWith(astroEntryPrefix)) {
|
||||
return null;
|
||||
}
|
||||
id = removeQueryString(id);
|
||||
|
|
|
@ -63,6 +63,29 @@ describe('Assets Prefix - Static', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Assets Prefix - Static with path prefix', () => {
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/astro-assets-prefix/',
|
||||
build: {
|
||||
assetsPrefix: '/starting-slash',
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('all stylesheets should start with assetPrefix', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
const stylesheets = $('link[rel="stylesheet"]');
|
||||
stylesheets.each((i, el) => {
|
||||
expect(el.attribs.href).to.match(/^\/starting-slash\/.*/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Assets Prefix - Server', () => {
|
||||
let app;
|
||||
|
||||
|
@ -119,3 +142,32 @@ describe('Assets Prefix - Server', () => {
|
|||
expect(imgAsset.attr('src')).to.match(assetsPrefixRegex);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Assets Prefix - Server with path prefix', () => {
|
||||
let app;
|
||||
|
||||
before(async () => {
|
||||
const fixture = await loadFixture({
|
||||
root: './fixtures/astro-assets-prefix/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
build: {
|
||||
assetsPrefix: '/starting-slash',
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
it('all stylesheets should start with assetPrefix', async () => {
|
||||
const request = new Request('http://example.com/custom-base/');
|
||||
const response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
const stylesheets = $('link[rel="stylesheet"]');
|
||||
stylesheets.each((i, el) => {
|
||||
expect(el.attribs.href).to.match(/^\/starting-slash\/.*/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@ describe('<Code>', () => {
|
|||
const $ = cheerio.load(html);
|
||||
expect($('pre')).to.have.lengthOf(1);
|
||||
expect($('pre').attr('style')).to.equal(
|
||||
'background-color: #0d1117; overflow-x: auto;',
|
||||
'background-color: #24292e; overflow-x: auto;',
|
||||
'applies default and overflow'
|
||||
);
|
||||
expect($('pre > code')).to.have.lengthOf(1);
|
||||
|
@ -28,7 +28,7 @@ describe('<Code>', () => {
|
|||
let html = await fixture.readFile('/basic/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
expect($('pre')).to.have.lengthOf(1);
|
||||
expect($('pre').attr('class'), 'astro-code');
|
||||
expect($('pre').attr('class'), 'astro-code nord');
|
||||
expect($('pre > code')).to.have.lengthOf(1);
|
||||
// test: contains many generated spans
|
||||
expect($('pre > code span').length).to.be.greaterThanOrEqual(6);
|
||||
|
@ -38,7 +38,7 @@ describe('<Code>', () => {
|
|||
let html = await fixture.readFile('/custom-theme/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
expect($('pre')).to.have.lengthOf(1);
|
||||
expect($('pre').attr('class')).to.equal('astro-code');
|
||||
expect($('pre').attr('class')).to.equal('astro-code nord');
|
||||
expect($('pre').attr('style')).to.equal(
|
||||
'background-color: #2e3440ff; overflow-x: auto;',
|
||||
'applies custom theme'
|
||||
|
@ -52,7 +52,7 @@ describe('<Code>', () => {
|
|||
expect($('pre')).to.have.lengthOf(1);
|
||||
// test: applies wrap overflow
|
||||
expect($('pre').attr('style')).to.equal(
|
||||
'background-color: #0d1117; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;'
|
||||
'background-color: #24292e; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;'
|
||||
);
|
||||
}
|
||||
{
|
||||
|
@ -60,14 +60,14 @@ describe('<Code>', () => {
|
|||
const $ = cheerio.load(html);
|
||||
expect($('pre')).to.have.lengthOf(1);
|
||||
// test: applies wrap overflow
|
||||
expect($('pre').attr('style')).to.equal('background-color: #0d1117; overflow-x: auto;');
|
||||
expect($('pre').attr('style')).to.equal('background-color: #24292e; overflow-x: auto;');
|
||||
}
|
||||
{
|
||||
let html = await fixture.readFile('/wrap-null/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
expect($('pre')).to.have.lengthOf(1);
|
||||
// test: applies wrap overflow
|
||||
expect($('pre').attr('style')).to.equal('background-color: #0d1117');
|
||||
expect($('pre').attr('style')).to.equal('background-color: #24292e');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -75,7 +75,7 @@ describe('<Code>', () => {
|
|||
let html = await fixture.readFile('/css-theme/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
expect($('pre')).to.have.lengthOf(1);
|
||||
expect($('pre').attr('class')).to.equal('astro-code');
|
||||
expect($('pre').attr('class')).to.equal('astro-code css-variables');
|
||||
expect(
|
||||
$('pre, pre span')
|
||||
.map((i, f) => (f.attribs ? f.attribs.style : 'no style found'))
|
||||
|
@ -100,4 +100,14 @@ describe('<Code>', () => {
|
|||
expect($('#lang > pre')).to.have.lengthOf(1);
|
||||
expect($('#lang > pre > code span').length).to.equal(3);
|
||||
});
|
||||
|
||||
it('<Code inline> has no pre tag', async () => {
|
||||
let html = await fixture.readFile('/inline/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
const codeEl = $('.astro-code');
|
||||
|
||||
expect(codeEl.prop('tagName')).to.eq('CODE');
|
||||
expect(codeEl.attr('style')).to.include('background-color:');
|
||||
expect($('pre')).to.have.lengthOf(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,11 +3,11 @@ import { loadFixture } from './test-utils.js';
|
|||
import * as cheerio from 'cheerio';
|
||||
|
||||
describe('getStaticPaths - build calls', () => {
|
||||
before(async () => {
|
||||
// reset the flag used by [...calledTwiceTest].astro between each test
|
||||
globalThis.isCalledOnce = false;
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
const fixture = await loadFixture({
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/astro-get-static-paths/',
|
||||
site: 'https://mysite.dev/',
|
||||
base: '/blog',
|
||||
|
@ -15,10 +15,22 @@ describe('getStaticPaths - build calls', () => {
|
|||
await fixture.build();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// reset the flag used by [...calledTwiceTest].astro between each test
|
||||
globalThis.isCalledOnce = false;
|
||||
});
|
||||
|
||||
it('is only called once during build', () => {
|
||||
// useless expect; if build() throws in setup then this test fails
|
||||
expect(true).to.equal(true);
|
||||
});
|
||||
|
||||
it('Astro.url sets the current pathname', async () => {
|
||||
const html = await fixture.readFile('/food/tacos/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#url').text()).to.equal('/blog/food/tacos/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStaticPaths - dev calls', () => {
|
||||
|
@ -26,11 +38,16 @@ describe('getStaticPaths - dev calls', () => {
|
|||
let devServer;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/astro-get-static-paths/',
|
||||
site: 'https://mysite.dev/',
|
||||
});
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// reset the flag used by [...calledTwiceTest].astro between each test
|
||||
globalThis.isCalledOnce = false;
|
||||
|
||||
fixture = await loadFixture({ root: './fixtures/astro-get-static-paths/' });
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
@ -47,95 +64,46 @@ describe('getStaticPaths - dev calls', () => {
|
|||
res = await fixture.fetch('/c');
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStaticPaths - 404 behavior', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
before(async () => {
|
||||
// reset the flag used by [...calledTwiceTest].astro between each test
|
||||
globalThis.isCalledOnce = false;
|
||||
|
||||
fixture = await loadFixture({ root: './fixtures/astro-get-static-paths/' });
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
devServer.stop();
|
||||
});
|
||||
|
||||
it('resolves 200 on matching static path - named params', async () => {
|
||||
const res = await fixture.fetch('/pizza/provolone-sausage');
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
|
||||
it('resolves 404 on pattern match without static path - named params', async () => {
|
||||
const res = await fixture.fetch('/pizza/provolone-pineapple');
|
||||
expect(res.status).to.equal(404);
|
||||
});
|
||||
|
||||
it('resolves 200 on matching static path - rest params', async () => {
|
||||
const res = await fixture.fetch('/pizza/grimaldis/new-york');
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
|
||||
it('resolves 404 on pattern match without static path - rest params', async () => {
|
||||
const res = await fixture.fetch('/pizza/pizza-hut');
|
||||
expect(res.status).to.equal(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStaticPaths - route params type validation', () => {
|
||||
let fixture, devServer;
|
||||
|
||||
before(async () => {
|
||||
// reset the flag used by [...calledTwiceTest].astro between each test
|
||||
globalThis.isCalledOnce = false;
|
||||
|
||||
fixture = await loadFixture({ root: './fixtures/astro-get-static-paths/' });
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
it('resolves 200 on nested array parameters', async () => {
|
||||
const res = await fixture.fetch('/nested-arrays/slug1');
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
|
||||
it('resolves 200 on matching static path - string params', async () => {
|
||||
// route provided with { params: { year: "2022", slug: "post-2" }}
|
||||
const res = await fixture.fetch('/blog/2022/post-1');
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
|
||||
it('resolves 200 on matching static path - numeric params', async () => {
|
||||
// route provided with { params: { year: 2022, slug: "post-2" }}
|
||||
const res = await fixture.fetch('/blog/2022/post-2');
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStaticPaths - numeric route params', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
before(async () => {
|
||||
// reset the flag used by [...calledTwiceTest].astro between each test
|
||||
globalThis.isCalledOnce = false;
|
||||
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/astro-get-static-paths/',
|
||||
site: 'https://mysite.dev/',
|
||||
describe('404 behavior', () => {
|
||||
it('resolves 200 on matching static path - named params', async () => {
|
||||
const res = await fixture.fetch('/pizza/provolone-sausage');
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
|
||||
it('resolves 404 on pattern match without static path - named params', async () => {
|
||||
const res = await fixture.fetch('/pizza/provolone-pineapple');
|
||||
expect(res.status).to.equal(404);
|
||||
});
|
||||
|
||||
it('resolves 200 on matching static path - rest params', async () => {
|
||||
const res = await fixture.fetch('/pizza/grimaldis/new-york');
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
|
||||
it('resolves 404 on pattern match without static path - rest params', async () => {
|
||||
const res = await fixture.fetch('/pizza/pizza-hut');
|
||||
expect(res.status).to.equal(404);
|
||||
});
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
describe('route params type validation', () => {
|
||||
it('resolves 200 on nested array parameters', async () => {
|
||||
const res = await fixture.fetch('/nested-arrays/slug1');
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
|
||||
it('resolves 200 on matching static path - string params', async () => {
|
||||
// route provided with { params: { year: "2022", slug: "post-2" }}
|
||||
const res = await fixture.fetch('/blog/2022/post-1');
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
|
||||
it('resolves 200 on matching static path - numeric params', async () => {
|
||||
// route provided with { params: { year: 2022, slug: "post-2" }}
|
||||
const res = await fixture.fetch('/blog/2022/post-2');
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves 200 on matching static paths', async () => {
|
||||
|
@ -155,25 +123,3 @@ describe('getStaticPaths - numeric route params', () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStaticPaths - Astro.url', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
before(async () => {
|
||||
// reset the flag used by [...calledTwiceTest].astro between each test
|
||||
globalThis.isCalledOnce = false;
|
||||
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/astro-get-static-paths/',
|
||||
site: 'https://mysite.dev/',
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('Sets the current pathname', async () => {
|
||||
const html = await fixture.readFile('/food/tacos/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#url').text()).to.equal('/food/tacos/');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ describe('Astro Markdown Shiki', () => {
|
|||
|
||||
expect($('pre')).to.have.lengthOf(2);
|
||||
expect($('pre').hasClass('astro-code')).to.equal(true);
|
||||
expect($('pre').attr().style).to.equal('background-color: #0d1117; overflow-x: auto;');
|
||||
expect($('pre').attr().style).to.equal('background-color: #24292e; overflow-x: auto;');
|
||||
});
|
||||
|
||||
it('Can render diff syntax with "user-select: none"', async () => {
|
||||
|
@ -47,7 +47,7 @@ describe('Astro Markdown Shiki', () => {
|
|||
|
||||
expect($('pre')).to.have.lengthOf(1);
|
||||
expect($('pre').hasClass('astro-code')).to.equal(true);
|
||||
expect($('pre').attr().style).to.equal('background-color: #ffffff; overflow-x: auto;');
|
||||
expect($('pre').attr().style).to.equal('background-color: #fff; overflow-x: auto;');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -84,13 +84,13 @@ describe('Astro Markdown Shiki', () => {
|
|||
|
||||
const segments = $('.line').get(6).children;
|
||||
expect(segments).to.have.lengthOf(3);
|
||||
expect(segments[0].attribs.style).to.be.equal('color: #C9D1D9');
|
||||
expect(segments[1].attribs.style).to.be.equal('color: #79C0FF');
|
||||
expect(segments[2].attribs.style).to.be.equal('color: #C9D1D9');
|
||||
expect(segments[0].attribs.style).to.be.equal('color: #E1E4E8');
|
||||
expect(segments[1].attribs.style).to.be.equal('color: #79B8FF');
|
||||
expect(segments[2].attribs.style).to.be.equal('color: #E1E4E8');
|
||||
|
||||
const unknownLang = $('.line').last().html();
|
||||
expect(unknownLang).to.be.equal(
|
||||
'<span style="color: #c9d1d9">This language does not exist</span>'
|
||||
'<span style="color: #e1e4e8">This language does not exist</span>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -98,7 +98,7 @@ describe('Astro Markdown Shiki', () => {
|
|||
describe('Wrap', () => {
|
||||
describe('wrap = true', () => {
|
||||
const style =
|
||||
'background-color: #0d1117; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;';
|
||||
'background-color: #24292e; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;';
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
|
@ -117,7 +117,7 @@ describe('Astro Markdown Shiki', () => {
|
|||
});
|
||||
|
||||
describe('wrap = false', () => {
|
||||
const style = 'background-color: #0d1117; overflow-x: auto;';
|
||||
const style = 'background-color: #24292e; overflow-x: auto;';
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
|
@ -135,7 +135,7 @@ describe('Astro Markdown Shiki', () => {
|
|||
});
|
||||
|
||||
describe('wrap = null', () => {
|
||||
const style = 'background-color: #0d1117';
|
||||
const style = 'background-color: #24292e';
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
|
|
|
@ -16,4 +16,10 @@ describe('Slots with client: directives', () => {
|
|||
const $ = cheerio.load(html);
|
||||
expect($('script')).to.have.a.lengthOf(1);
|
||||
});
|
||||
|
||||
it('Astro slot tags are cleaned', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
expect($('astro-slot')).to.have.a.lengthOf(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -36,6 +36,25 @@ describe('Content Collections - render()', () => {
|
|||
expect($('link[rel=stylesheet]')).to.have.a.lengthOf(0);
|
||||
});
|
||||
|
||||
it('De-duplicates CSS used both in layout and directly in target page', async () => {
|
||||
const html = await fixture.readFile('/with-layout-prop/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const set = new Set();
|
||||
|
||||
$('link[rel=stylesheet]').each((_, linkEl) => {
|
||||
const href = linkEl.attribs.href;
|
||||
expect(set).to.not.contain(href);
|
||||
set.add(href);
|
||||
});
|
||||
|
||||
$('style').each((_, styleEl) => {
|
||||
const textContent = styleEl.children[0].data;
|
||||
expect(set).to.not.contain(textContent);
|
||||
set.add(textContent);
|
||||
});
|
||||
});
|
||||
|
||||
it('Includes component scripts for rendered entry', async () => {
|
||||
const html = await fixture.readFile('/launch-week-component-scripts/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
@ -116,6 +135,28 @@ describe('Content Collections - render()', () => {
|
|||
expect($('link[rel=stylesheet]')).to.have.a.lengthOf(0);
|
||||
});
|
||||
|
||||
it('De-duplicates CSS used both in layout and directly in target page', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
const request = new Request('http://example.com/with-layout-prop/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const set = new Set();
|
||||
|
||||
$('link[rel=stylesheet]').each((_, linkEl) => {
|
||||
const href = linkEl.attribs.href;
|
||||
expect(set).to.not.contain(href);
|
||||
set.add(href);
|
||||
});
|
||||
|
||||
$('style').each((_, styleEl) => {
|
||||
const textContent = styleEl.children[0].data;
|
||||
expect(set).to.not.contain(textContent);
|
||||
set.add(textContent);
|
||||
});
|
||||
});
|
||||
|
||||
it('Applies MDX components export', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
const request = new Request('http://example.com/launch-week-components-export');
|
||||
|
|
|
@ -6,7 +6,7 @@ describe('Importing raw/inlined CSS', () => {
|
|||
let fixture;
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/css-inline/',
|
||||
root: './fixtures/css-import-as-inline/',
|
||||
});
|
||||
});
|
||||
describe('Build', () => {
|
285
packages/astro/test/css-inline-stylesheets.test.js
Normal file
285
packages/astro/test/css-inline-stylesheets.test.js
Normal file
|
@ -0,0 +1,285 @@
|
|||
import { expect } from 'chai';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
import testAdapter from './test-adapter.js';
|
||||
|
||||
describe('Setting inlineStylesheets to never in static output', () => {
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/css-inline-stylesheets/never/',
|
||||
output: 'static',
|
||||
experimental: {
|
||||
inlineStylesheets: 'never',
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('Does not render any <style> tags', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('style').toArray()).to.be.empty;
|
||||
});
|
||||
|
||||
describe('Inspect linked stylesheets', () => {
|
||||
// object, so it can be passed by reference
|
||||
const allStyles = {};
|
||||
|
||||
before(async () => {
|
||||
allStyles.value = await stylesFromStaticOutput(fixture);
|
||||
});
|
||||
|
||||
commonExpectations(allStyles);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Setting inlineStylesheets to never in server output', () => {
|
||||
let app;
|
||||
|
||||
before(async () => {
|
||||
const fixture = await loadFixture({
|
||||
root: './fixtures/css-inline-stylesheets/never/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
experimental: {
|
||||
inlineStylesheets: 'never',
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
it('Does not render any <style> tags', async () => {
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('style').toArray()).to.be.empty;
|
||||
});
|
||||
|
||||
describe('Inspect linked stylesheets', () => {
|
||||
const allStyles = {};
|
||||
|
||||
before(async () => {
|
||||
allStyles.value = await stylesFromServer(app);
|
||||
});
|
||||
|
||||
commonExpectations(allStyles);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Setting inlineStylesheets to auto in static output', () => {
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/css-inline-stylesheets/auto/',
|
||||
output: 'static',
|
||||
experimental: {
|
||||
inlineStylesheets: 'auto',
|
||||
},
|
||||
vite: {
|
||||
build: {
|
||||
assetsInlineLimit: 512,
|
||||
},
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('Renders some <style> and some <link> tags', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// the count of style/link tags depends on our css chunking logic
|
||||
// this test should be updated if it changes
|
||||
expect($('style')).to.have.lengthOf(3);
|
||||
expect($('link[rel=stylesheet]')).to.have.lengthOf(1);
|
||||
});
|
||||
|
||||
describe('Inspect linked and inlined stylesheets', () => {
|
||||
const allStyles = {};
|
||||
|
||||
before(async () => {
|
||||
allStyles.value = await stylesFromStaticOutput(fixture);
|
||||
});
|
||||
|
||||
commonExpectations(allStyles);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Setting inlineStylesheets to auto in server output', () => {
|
||||
let app;
|
||||
|
||||
before(async () => {
|
||||
const fixture = await loadFixture({
|
||||
root: './fixtures/css-inline-stylesheets/auto/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
experimental: {
|
||||
inlineStylesheets: 'auto',
|
||||
},
|
||||
vite: {
|
||||
build: {
|
||||
assetsInlineLimit: 512,
|
||||
},
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
it('Renders some <style> and some <link> tags', async () => {
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// the count of style/link tags depends on our css chunking logic
|
||||
// this test should be updated if it changes
|
||||
expect($('style')).to.have.lengthOf(3);
|
||||
expect($('link[rel=stylesheet]')).to.have.lengthOf(1);
|
||||
});
|
||||
|
||||
describe('Inspect linked and inlined stylesheets', () => {
|
||||
const allStyles = {};
|
||||
|
||||
before(async () => {
|
||||
allStyles.value = await stylesFromServer(app);
|
||||
});
|
||||
|
||||
commonExpectations(allStyles);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Setting inlineStylesheets to always in static output', () => {
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/css-inline-stylesheets/always/',
|
||||
output: 'static',
|
||||
experimental: {
|
||||
inlineStylesheets: 'always',
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('Does not render any <link> tags', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('link[rel=stylesheet]').toArray()).to.be.empty;
|
||||
});
|
||||
|
||||
describe('Inspect inlined stylesheets', () => {
|
||||
const allStyles = {};
|
||||
|
||||
before(async () => {
|
||||
allStyles.value = await stylesFromStaticOutput(fixture);
|
||||
});
|
||||
|
||||
commonExpectations(allStyles);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Setting inlineStylesheets to always in server output', () => {
|
||||
let app;
|
||||
|
||||
before(async () => {
|
||||
const fixture = await loadFixture({
|
||||
root: './fixtures/css-inline-stylesheets/always/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
experimental: {
|
||||
inlineStylesheets: 'always',
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
it('Does not render any <link> tags', async () => {
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('link[rel=stylesheet]').toArray()).to.be.empty;
|
||||
});
|
||||
|
||||
describe('Inspect inlined stylesheets', () => {
|
||||
const allStyles = {};
|
||||
|
||||
before(async () => {
|
||||
allStyles.value = await stylesFromServer(app);
|
||||
});
|
||||
|
||||
commonExpectations(allStyles);
|
||||
});
|
||||
});
|
||||
|
||||
async function stylesFromStaticOutput(fixture) {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const links = $('link[rel=stylesheet]');
|
||||
const hrefs = links.map((_, linkEl) => linkEl.attribs.href).toArray();
|
||||
const allLinkedStylesheets = await Promise.all(hrefs.map((href) => fixture.readFile(href)));
|
||||
const allLinkedStyles = allLinkedStylesheets.join('');
|
||||
|
||||
const styles = $('style');
|
||||
const allInlinedStylesheets = styles.map((_, styleEl) => styleEl.children[0].data).toArray();
|
||||
const allInlinedStyles = allInlinedStylesheets.join('');
|
||||
|
||||
return allLinkedStyles + allInlinedStyles;
|
||||
}
|
||||
|
||||
async function stylesFromServer(app) {
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const links = $('link[rel=stylesheet]');
|
||||
const hrefs = links.map((_, linkEl) => linkEl.attribs.href).toArray();
|
||||
const allLinkedStylesheets = await Promise.all(
|
||||
hrefs.map(async (href) => {
|
||||
const cssRequest = new Request(`http://example.com${href}`);
|
||||
const cssResponse = await app.render(cssRequest);
|
||||
return await cssResponse.text();
|
||||
})
|
||||
);
|
||||
const allLinkedStyles = allLinkedStylesheets.join('');
|
||||
|
||||
const styles = $('style');
|
||||
const allInlinedStylesheets = styles.map((_, styleEl) => styleEl.children[0].data).toArray();
|
||||
const allInlinedStyles = allInlinedStylesheets.join('');
|
||||
return allLinkedStyles + allInlinedStyles;
|
||||
}
|
||||
|
||||
function commonExpectations(allStyles) {
|
||||
it('Includes all authored css', () => {
|
||||
// authored in imported.css
|
||||
expect(allStyles.value).to.include('.bg-lightcoral');
|
||||
|
||||
// authored in index.astro
|
||||
expect(allStyles.value).to.include('#welcome');
|
||||
|
||||
// authored in components/Button.astro
|
||||
expect(allStyles.value).to.include('.variant-outline');
|
||||
|
||||
// authored in layouts/Layout.astro
|
||||
expect(allStyles.value).to.include('Menlo');
|
||||
});
|
||||
|
||||
it('Styles used both in content layout and directly in page are included only once', () => {
|
||||
// authored in components/Button.astro
|
||||
expect(allStyles.value.match(/cubic-bezier/g)).to.have.lengthOf(1);
|
||||
});
|
||||
}
|
42
packages/astro/test/custom-404-html.test.js
Normal file
42
packages/astro/test/custom-404-html.test.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { expect } from 'chai';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
describe('Custom 404.html', () => {
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/custom-404-html/',
|
||||
site: 'http://example.com',
|
||||
});
|
||||
});
|
||||
|
||||
describe('dev', () => {
|
||||
let devServer;
|
||||
let $;
|
||||
|
||||
before(async () => {
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
it('renders /', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
|
||||
expect($('h1').text()).to.equal('Home');
|
||||
});
|
||||
|
||||
it('renders 404 for /a', async () => {
|
||||
const html = await fixture.fetch('/a').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
|
||||
expect($('h1').text()).to.equal('Page not found');
|
||||
expect($('p').text()).to.equal('This 404 is a static HTML file.');
|
||||
});
|
||||
});
|
||||
});
|
42
packages/astro/test/custom-404-injected.test.js
Normal file
42
packages/astro/test/custom-404-injected.test.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { expect } from 'chai';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
describe('Custom 404 with injectRoute', () => {
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/custom-404-injected/',
|
||||
site: 'http://example.com',
|
||||
});
|
||||
});
|
||||
|
||||
describe('dev', () => {
|
||||
let devServer;
|
||||
let $;
|
||||
|
||||
before(async () => {
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
it('renders /', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
|
||||
expect($('h1').text()).to.equal('Home');
|
||||
});
|
||||
|
||||
it('renders 404 for /a', async () => {
|
||||
const html = await fixture.fetch('/a').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
|
||||
expect($('h1').text()).to.equal('Page not found');
|
||||
expect($('p').text()).to.equal('/a');
|
||||
});
|
||||
});
|
||||
});
|
10
packages/astro/test/fixtures/astro-component-code/src/pages/inline.astro
vendored
Normal file
10
packages/astro/test/fixtures/astro-component-code/src/pages/inline.astro
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
import {Code} from 'astro/components';
|
||||
---
|
||||
<html>
|
||||
<head><title>Code component</title></head>
|
||||
<body>
|
||||
Simple:
|
||||
<Code code="console.log('inline code');" lang="js" inline />
|
||||
</body>
|
||||
</html>
|
2
packages/astro/test/fixtures/content/src/components/H3.astro
vendored
Normal file
2
packages/astro/test/fixtures/content/src/components/H3.astro
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
<style>h3 { margin: 1rem }</style>
|
||||
<h3><slot /></h3>
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
import { CollectionEntry, getCollection } from 'astro:content';
|
||||
|
||||
import H3 from './H3.astro'
|
||||
// Test for recursive `getCollection()` calls
|
||||
const blog = await getCollection('blog');
|
||||
|
||||
|
@ -23,6 +23,7 @@ const {
|
|||
</head>
|
||||
<body data-layout-prop="true">
|
||||
<h1>{title}</h1>
|
||||
<H3>H3 inserted in the layout</H3>
|
||||
<nav>
|
||||
<ul>
|
||||
{blog.map((post) => (
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
---
|
||||
import { getEntryBySlug } from 'astro:content';
|
||||
import H3 from '../components/H3.astro';
|
||||
|
||||
const entry = await getEntryBySlug('blog', 'with-layout-prop');
|
||||
const { Content } = await entry.render();
|
||||
---
|
||||
<Content />
|
||||
<H3>H3 directly inserted to the page</H3>
|
||||
<Content />
|
8
packages/astro/test/fixtures/css-import-as-inline/package.json
vendored
Normal file
8
packages/astro/test/fixtures/css-import-as-inline/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/css-import-as-inline",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue