feat(astro): experimental middleware (#6721)
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
ad907196cb
commit
831b67cdb8
67 changed files with 3727 additions and 2168 deletions
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
|
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"
|
||||||
|
}
|
6
packages/astro/client-base.d.ts
vendored
6
packages/astro/client-base.d.ts
vendored
|
@ -387,3 +387,9 @@ declare module '*?inline' {
|
||||||
const src: string;
|
const src: string;
|
||||||
export default src;
|
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 {}
|
||||||
|
}
|
||||||
|
|
|
@ -64,6 +64,10 @@
|
||||||
"./zod": {
|
"./zod": {
|
||||||
"types": "./zod.d.ts",
|
"types": "./zod.d.ts",
|
||||||
"default": "./zod.mjs"
|
"default": "./zod.mjs"
|
||||||
|
},
|
||||||
|
"./middleware": {
|
||||||
|
"types": "./dist/core/middleware/index.d.ts",
|
||||||
|
"default": "./dist/core/middleware/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|
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;
|
drafts?: boolean;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
experimentalAssets?: boolean;
|
experimentalAssets?: boolean;
|
||||||
|
experimentalMiddleware?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BuildConfig {
|
export interface BuildConfig {
|
||||||
|
@ -1034,6 +1035,26 @@ export interface AstroUserConfig {
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
assets?: boolean;
|
assets?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
// Legacy options to be removed
|
||||||
|
@ -1431,6 +1452,11 @@ interface AstroSharedContext<Props extends Record<string, any> = Record<string,
|
||||||
* Redirect to another page (**SSR Only**).
|
* Redirect to another page (**SSR Only**).
|
||||||
*/
|
*/
|
||||||
redirect(path: string, status?: 301 | 302 | 303 | 307 | 308): Response;
|
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>>
|
export interface APIContext<Props extends Record<string, any> = Record<string, any>>
|
||||||
|
@ -1464,7 +1490,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'];
|
params: AstroSharedContext['params'];
|
||||||
/**
|
/**
|
||||||
|
@ -1504,6 +1530,31 @@ export interface APIContext<Props extends Record<string, any> = Record<string, a
|
||||||
* [context reference](https://docs.astro.build/en/guides/api-reference/#contextredirect)
|
* [context reference](https://docs.astro.build/en/guides/api-reference/#contextredirect)
|
||||||
*/
|
*/
|
||||||
redirect: AstroSharedContext['redirect'];
|
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>;
|
export type Props = Record<string, unknown>;
|
||||||
|
@ -1592,6 +1643,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 {
|
export interface AstroPluginOptions {
|
||||||
settings: AstroSettings;
|
settings: AstroSettings;
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type {
|
||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
EndpointHandler,
|
EndpointHandler,
|
||||||
ManifestData,
|
ManifestData,
|
||||||
|
MiddlewareResponseHandler,
|
||||||
RouteData,
|
RouteData,
|
||||||
SSRElement,
|
SSRElement,
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
|
@ -9,9 +10,10 @@ import type { RouteInfo, SSRManifest as Manifest } from './types';
|
||||||
|
|
||||||
import mime from 'mime';
|
import mime from 'mime';
|
||||||
import { attachToResponse, getSetCookiesFromResponse } from '../cookies/index.js';
|
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 { consoleLogDestination } from '../logger/console.js';
|
||||||
import { error, type LogOptions } from '../logger/core.js';
|
import { error, type LogOptions } from '../logger/core.js';
|
||||||
|
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||||
import { removeTrailingForwardSlash } from '../path.js';
|
import { removeTrailingForwardSlash } from '../path.js';
|
||||||
import {
|
import {
|
||||||
createEnvironment,
|
createEnvironment,
|
||||||
|
@ -28,6 +30,8 @@ import {
|
||||||
import { matchRoute } from '../routing/match.js';
|
import { matchRoute } from '../routing/match.js';
|
||||||
export { deserializeManifest } from './common.js';
|
export { deserializeManifest } from './common.js';
|
||||||
|
|
||||||
|
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||||
|
|
||||||
export const pagesVirtualModuleId = '@astrojs-pages-virtual-entry';
|
export const pagesVirtualModuleId = '@astrojs-pages-virtual-entry';
|
||||||
export const resolvedPagesVirtualModuleId = '\0' + pagesVirtualModuleId;
|
export const resolvedPagesVirtualModuleId = '\0' + pagesVirtualModuleId;
|
||||||
const responseSentSymbol = Symbol.for('astro.responseSent');
|
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
|
// Use the 404 status code for 404.astro components
|
||||||
if (routeData.route === '/404') {
|
if (routeData.route === '/404') {
|
||||||
defaultStatus = 404;
|
defaultStatus = 404;
|
||||||
|
@ -191,7 +197,7 @@ export class App {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ctx = createRenderContext({
|
const renderContext = await createRenderContext({
|
||||||
request,
|
request,
|
||||||
origin: url.origin,
|
origin: url.origin,
|
||||||
pathname,
|
pathname,
|
||||||
|
@ -200,9 +206,35 @@ export class App {
|
||||||
links,
|
links,
|
||||||
route: routeData,
|
route: routeData,
|
||||||
status,
|
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);
|
Reflect.set(request, responseSentSymbol, true);
|
||||||
return response;
|
return response;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
@ -224,15 +256,23 @@ export class App {
|
||||||
const pathname = '/' + this.removeBase(url.pathname);
|
const pathname = '/' + this.removeBase(url.pathname);
|
||||||
const handler = mod as unknown as EndpointHandler;
|
const handler = mod as unknown as EndpointHandler;
|
||||||
|
|
||||||
const ctx = createRenderContext({
|
const ctx = await createRenderContext({
|
||||||
request,
|
request,
|
||||||
origin: url.origin,
|
origin: url.origin,
|
||||||
pathname,
|
pathname,
|
||||||
route: routeData,
|
route: routeData,
|
||||||
status,
|
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.type === 'response') {
|
||||||
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
|
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark';
|
import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark';
|
||||||
import type {
|
import type {
|
||||||
|
AstroMiddlewareInstance,
|
||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
RouteData,
|
RouteData,
|
||||||
SerializedRouteData,
|
SerializedRouteData,
|
||||||
|
@ -38,6 +39,7 @@ export interface SSRManifest {
|
||||||
entryModules: Record<string, string>;
|
entryModules: Record<string, string>;
|
||||||
assets: Set<string>;
|
assets: Set<string>;
|
||||||
componentMetadata: SSRResult['componentMetadata'];
|
componentMetadata: SSRResult['componentMetadata'];
|
||||||
|
middleware?: AstroMiddlewareInstance<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SerializedSSRManifest = Omit<SSRManifest, 'routes' | 'assets' | 'componentMetadata'> & {
|
export type SerializedSSRManifest = Omit<SSRManifest, 'routes' | 'assets' | 'componentMetadata'> & {
|
||||||
|
|
|
@ -5,10 +5,13 @@ import type { OutputAsset, OutputChunk } from 'rollup';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import type {
|
import type {
|
||||||
AstroConfig,
|
AstroConfig,
|
||||||
|
AstroMiddlewareInstance,
|
||||||
AstroSettings,
|
AstroSettings,
|
||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
EndpointHandler,
|
EndpointHandler,
|
||||||
|
EndpointOutput,
|
||||||
ImageTransform,
|
ImageTransform,
|
||||||
|
MiddlewareResponseHandler,
|
||||||
RouteType,
|
RouteType,
|
||||||
SSRError,
|
SSRError,
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
|
@ -25,9 +28,14 @@ import {
|
||||||
} from '../../core/path.js';
|
} from '../../core/path.js';
|
||||||
import { runHookBuildGenerated } from '../../integrations/index.js';
|
import { runHookBuildGenerated } from '../../integrations/index.js';
|
||||||
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/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 { AstroError } from '../errors/index.js';
|
||||||
import { debug, info } from '../logger/core.js';
|
import { debug, info } from '../logger/core.js';
|
||||||
|
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||||
import { createEnvironment, createRenderContext, renderPage } from '../render/index.js';
|
import { createEnvironment, createRenderContext, renderPage } from '../render/index.js';
|
||||||
import { callGetStaticPaths } from '../render/route-cache.js';
|
import { callGetStaticPaths } from '../render/route-cache.js';
|
||||||
import {
|
import {
|
||||||
|
@ -157,6 +165,7 @@ async function generatePage(
|
||||||
const scripts = pageInfo?.hoistedScript ?? null;
|
const scripts = pageInfo?.hoistedScript ?? null;
|
||||||
|
|
||||||
const pageModule = ssrEntry.pageMap?.get(pageData.component);
|
const pageModule = ssrEntry.pageMap?.get(pageData.component);
|
||||||
|
const middleware = ssrEntry.middleware;
|
||||||
|
|
||||||
if (!pageModule) {
|
if (!pageModule) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -186,7 +195,7 @@ async function generatePage(
|
||||||
|
|
||||||
for (let i = 0; i < paths.length; i++) {
|
for (let i = 0; i < paths.length; i++) {
|
||||||
const path = paths[i];
|
const path = paths[i];
|
||||||
await generatePath(path, opts, generationOptions);
|
await generatePath(path, opts, generationOptions, middleware);
|
||||||
const timeEnd = performance.now();
|
const timeEnd = performance.now();
|
||||||
const timeChange = getTimeStat(timeStart, timeEnd);
|
const timeChange = getTimeStat(timeStart, timeEnd);
|
||||||
const timeIncrease = `(+${timeChange})`;
|
const timeIncrease = `(+${timeChange})`;
|
||||||
|
@ -328,7 +337,8 @@ function getUrlForPath(
|
||||||
async function generatePath(
|
async function generatePath(
|
||||||
pathname: string,
|
pathname: string,
|
||||||
opts: StaticBuildOptions,
|
opts: StaticBuildOptions,
|
||||||
gopts: GeneratePathOptions
|
gopts: GeneratePathOptions,
|
||||||
|
middleware?: AstroMiddlewareInstance<unknown>
|
||||||
) {
|
) {
|
||||||
const { settings, logging, origin, routeCache } = opts;
|
const { settings, logging, origin, routeCache } = opts;
|
||||||
const { mod, internals, linkIds, scripts: hoistedScripts, pageData, renderers } = gopts;
|
const { mod, internals, linkIds, scripts: hoistedScripts, pageData, renderers } = gopts;
|
||||||
|
@ -414,7 +424,8 @@ async function generatePath(
|
||||||
ssr,
|
ssr,
|
||||||
streaming: true,
|
streaming: true,
|
||||||
});
|
});
|
||||||
const ctx = createRenderContext({
|
|
||||||
|
const renderContext = await createRenderContext({
|
||||||
origin,
|
origin,
|
||||||
pathname,
|
pathname,
|
||||||
request: createRequest({ url, headers: new Headers(), logging, ssr }),
|
request: createRequest({ url, headers: new Headers(), logging, ssr }),
|
||||||
|
@ -422,13 +433,22 @@ async function generatePath(
|
||||||
scripts,
|
scripts,
|
||||||
links,
|
links,
|
||||||
route: pageData.route,
|
route: pageData.route,
|
||||||
|
env,
|
||||||
|
mod,
|
||||||
});
|
});
|
||||||
|
|
||||||
let body: string | Uint8Array;
|
let body: string | Uint8Array;
|
||||||
let encoding: BufferEncoding | undefined;
|
let encoding: BufferEncoding | undefined;
|
||||||
if (pageData.route.type === 'endpoint') {
|
if (pageData.route.type === 'endpoint') {
|
||||||
const endpointHandler = mod as unknown as EndpointHandler;
|
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') {
|
if (result.type === 'response') {
|
||||||
throwIfRedirectNotAllowed(result.response, opts.settings.config);
|
throwIfRedirectNotAllowed(result.response, opts.settings.config);
|
||||||
|
@ -443,7 +463,26 @@ async function generatePath(
|
||||||
} else {
|
} else {
|
||||||
let response: Response;
|
let response: Response;
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') {
|
if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') {
|
||||||
(err as SSRError).id = pageData.component;
|
(err as SSRError).id = pageData.component;
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import type { Plugin as VitePlugin } from 'vite';
|
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 { pagesVirtualModuleId, resolvedPagesVirtualModuleId } from '../../app/index.js';
|
||||||
|
import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../../constants.js';
|
||||||
import { addRollupInput } from '../add-rollup-input.js';
|
import { addRollupInput } from '../add-rollup-input.js';
|
||||||
import { eachPageData, hasPrerenderedPages, type BuildInternals } from '../internal.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 {
|
export function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
|
||||||
return {
|
return {
|
||||||
|
@ -22,8 +22,15 @@ export function vitePluginPages(opts: StaticBuildOptions, internals: BuildIntern
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
load(id) {
|
async load(id) {
|
||||||
if (id === resolvedPagesVirtualModuleId) {
|
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 importMap = '';
|
||||||
let imports = [];
|
let imports = [];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
@ -47,8 +54,12 @@ export function vitePluginPages(opts: StaticBuildOptions, internals: BuildIntern
|
||||||
|
|
||||||
const def = `${imports.join('\n')}
|
const def = `${imports.join('\n')}
|
||||||
|
|
||||||
|
${middlewareId ? `import * as _middleware from "${middlewareId.id}";` : ''}
|
||||||
|
|
||||||
export const pageMap = new Map([${importMap}]);
|
export const pageMap = new Map([${importMap}]);
|
||||||
export const renderers = [${rendererItems}];`;
|
export const renderers = [${rendererItems}];
|
||||||
|
${middlewareId ? `export const middleware = _middleware;` : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Plugin as VitePlugin } from 'vite';
|
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 { SerializedRouteInfo, SerializedSSRManifest } from '../../app/types';
|
||||||
import type { BuildInternals } from '../internal.js';
|
import type { BuildInternals } from '../internal.js';
|
||||||
import type { StaticBuildOptions } from '../types';
|
import type { StaticBuildOptions } from '../types';
|
||||||
|
@ -21,7 +21,11 @@ const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||||
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
|
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
|
||||||
const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g');
|
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 {
|
return {
|
||||||
name: '@astrojs/vite-plugin-astro-ssr',
|
name: '@astrojs/vite-plugin-astro-ssr',
|
||||||
enforce: 'post',
|
enforce: 'post',
|
||||||
|
@ -35,13 +39,18 @@ export function vitePluginSSR(internals: BuildInternals, adapter: AstroAdapter):
|
||||||
},
|
},
|
||||||
load(id) {
|
load(id) {
|
||||||
if (id === resolvedVirtualModuleId) {
|
if (id === resolvedVirtualModuleId) {
|
||||||
|
let middleware = '';
|
||||||
|
if (config.experimental?.middleware === true) {
|
||||||
|
middleware = 'middleware: _main.middleware';
|
||||||
|
}
|
||||||
return `import * as adapter from '${adapter.serverEntrypoint}';
|
return `import * as adapter from '${adapter.serverEntrypoint}';
|
||||||
import * as _main from '${pagesVirtualModuleId}';
|
import * as _main from '${pagesVirtualModuleId}';
|
||||||
import { deserializeManifest as _deserializeManifest } from 'astro/app';
|
import { deserializeManifest as _deserializeManifest } from 'astro/app';
|
||||||
import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest';
|
import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest';
|
||||||
const _manifest = Object.assign(_deserializeManifest('${manifestReplace}'), {
|
const _manifest = Object.assign(_deserializeManifest('${manifestReplace}'), {
|
||||||
pageMap: _main.pageMap,
|
pageMap: _main.pageMap,
|
||||||
renderers: _main.renderers
|
renderers: _main.renderers,
|
||||||
|
${middleware}
|
||||||
});
|
});
|
||||||
_privateSetManifestDontUseThis(_manifest);
|
_privateSetManifestDontUseThis(_manifest);
|
||||||
const _args = ${adapter.args ? JSON.stringify(adapter.args) : 'undefined'};
|
const _args = ${adapter.args ? JSON.stringify(adapter.args) : 'undefined'};
|
||||||
|
@ -235,7 +244,9 @@ export function pluginSSR(
|
||||||
build: 'ssr',
|
build: 'ssr',
|
||||||
hooks: {
|
hooks: {
|
||||||
'build:before': () => {
|
'build:before': () => {
|
||||||
let vitePlugin = ssr ? vitePluginSSR(internals, options.settings.adapter!) : undefined;
|
let vitePlugin = ssr
|
||||||
|
? vitePluginSSR(internals, options.settings.adapter!, options.settings.config)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enforce: 'after-user-plugins',
|
enforce: 'after-user-plugins',
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { default as vite, InlineConfig } from 'vite';
|
import type { default as vite, InlineConfig } from 'vite';
|
||||||
import type {
|
import type {
|
||||||
AstroConfig,
|
AstroConfig,
|
||||||
|
AstroMiddlewareInstance,
|
||||||
AstroSettings,
|
AstroSettings,
|
||||||
BuildConfig,
|
BuildConfig,
|
||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
|
@ -44,6 +45,7 @@ export interface StaticBuildOptions {
|
||||||
|
|
||||||
export interface SingleFileBuiltModule {
|
export interface SingleFileBuiltModule {
|
||||||
pageMap: Map<ComponentPath, ComponentInstance>;
|
pageMap: Map<ComponentPath, ComponentInstance>;
|
||||||
|
middleware: AstroMiddlewareInstance<unknown>;
|
||||||
renderers: SSRLoadedRenderer[];
|
renderers: SSRLoadedRenderer[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -103,6 +103,8 @@ export function resolveFlags(flags: Partial<Flags>): CLIFlags {
|
||||||
drafts: typeof flags.drafts === 'boolean' ? flags.drafts : undefined,
|
drafts: typeof flags.drafts === 'boolean' ? flags.drafts : undefined,
|
||||||
experimentalAssets:
|
experimentalAssets:
|
||||||
typeof flags.experimentalAssets === 'boolean' ? flags.experimentalAssets : undefined,
|
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.
|
// TODO: Come back here and refactor to remove this expected error.
|
||||||
astroConfig.server.open = flags.open;
|
astroConfig.server.open = flags.open;
|
||||||
}
|
}
|
||||||
|
if (typeof flags.experimentalMiddleware === 'boolean') {
|
||||||
|
astroConfig.experimental.middleware = true;
|
||||||
|
}
|
||||||
return astroConfig;
|
return astroConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
|
||||||
legacy: {},
|
legacy: {},
|
||||||
experimental: {
|
experimental: {
|
||||||
assets: false,
|
assets: false,
|
||||||
|
middleware: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -187,6 +188,7 @@ export const AstroConfigSchema = z.object({
|
||||||
experimental: z
|
experimental: z
|
||||||
.object({
|
.object({
|
||||||
assets: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.assets),
|
assets: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.assets),
|
||||||
|
middleware: z.oboolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.middleware),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({}),
|
.default({}),
|
||||||
|
|
|
@ -10,3 +10,6 @@ export const SUPPORTED_MARKDOWN_FILE_EXTENSIONS = [
|
||||||
'.mdwn',
|
'.mdwn',
|
||||||
'.md',
|
'.md',
|
||||||
] as const;
|
] 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 {
|
const {
|
||||||
env,
|
env,
|
||||||
preload: [, mod],
|
preload: [, mod],
|
||||||
|
middleware,
|
||||||
} = options;
|
} = options;
|
||||||
const endpointHandler = mod as unknown as EndpointHandler;
|
const endpointHandler = mod as unknown as EndpointHandler;
|
||||||
|
|
||||||
const ctx = createRenderContext({
|
const ctx = await createRenderContext({
|
||||||
request: options.request,
|
request: options.request,
|
||||||
origin: options.origin,
|
origin: options.origin,
|
||||||
pathname: options.pathname,
|
pathname: options.pathname,
|
||||||
route: options.route,
|
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 type { Environment, RenderContext } from '../render/index';
|
||||||
|
|
||||||
import { renderEndpoint } from '../../runtime/server/index.js';
|
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 { AstroCookies, attachToResponse } from '../cookies/index.js';
|
||||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||||
import { warn, type LogOptions } from '../logger/core.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 clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||||
|
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||||
|
|
||||||
type EndpointCallResult =
|
type EndpointCallResult =
|
||||||
| {
|
| {
|
||||||
|
@ -22,7 +32,7 @@ type EndpointCallResult =
|
||||||
response: Response;
|
response: Response;
|
||||||
};
|
};
|
||||||
|
|
||||||
function createAPIContext({
|
export function createAPIContext({
|
||||||
request,
|
request,
|
||||||
params,
|
params,
|
||||||
site,
|
site,
|
||||||
|
@ -35,7 +45,7 @@ function createAPIContext({
|
||||||
props: Record<string, any>;
|
props: Record<string, any>;
|
||||||
adapterName?: string;
|
adapterName?: string;
|
||||||
}): APIContext {
|
}): APIContext {
|
||||||
return {
|
const context = {
|
||||||
cookies: new AstroCookies(request),
|
cookies: new AstroCookies(request),
|
||||||
request,
|
request,
|
||||||
params,
|
params,
|
||||||
|
@ -51,7 +61,6 @@ function createAPIContext({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
url: new URL(request.url),
|
url: new URL(request.url),
|
||||||
// @ts-expect-error
|
|
||||||
get clientAddress() {
|
get clientAddress() {
|
||||||
if (!(clientAddressSymbol in request)) {
|
if (!(clientAddressSymbol in request)) {
|
||||||
if (adapterName) {
|
if (adapterName) {
|
||||||
|
@ -66,44 +75,60 @@ function createAPIContext({
|
||||||
|
|
||||||
return Reflect.get(request, clientAddressSymbol);
|
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,
|
mod: EndpointHandler,
|
||||||
env: Environment,
|
env: Environment,
|
||||||
ctx: RenderContext,
|
ctx: RenderContext,
|
||||||
logging: LogOptions
|
logging: LogOptions,
|
||||||
|
middleware?: AstroMiddlewareInstance<MiddlewareResult> | undefined
|
||||||
): Promise<EndpointCallResult> {
|
): 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({
|
const context = createAPIContext({
|
||||||
request: ctx.request,
|
request: ctx.request,
|
||||||
params,
|
params: ctx.params,
|
||||||
props,
|
props: ctx.props,
|
||||||
site: env.site,
|
site: env.site,
|
||||||
adapterName: env.adapterName,
|
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) {
|
if (response instanceof Response) {
|
||||||
attachToResponse(response, context.cookies);
|
attachToResponse(response, context.cookies);
|
||||||
|
|
|
@ -628,6 +628,95 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati
|
||||||
code: 3030,
|
code: 3030,
|
||||||
message: 'The response has already been sent to the browser and cannot be altered.',
|
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.
|
// No headings here, that way Vite errors are merged with Astro ones in the docs, which makes more sense to users.
|
||||||
// Vite Errors - 4xxx
|
// 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -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.
|
* The RenderContext represents the parts of rendering that are specific to one request.
|
||||||
|
@ -14,22 +23,38 @@ export interface RenderContext {
|
||||||
componentMetadata?: SSRResult['componentMetadata'];
|
componentMetadata?: SSRResult['componentMetadata'];
|
||||||
route?: RouteData;
|
route?: RouteData;
|
||||||
status?: number;
|
status?: number;
|
||||||
|
params: Params;
|
||||||
|
props: Props;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateRenderContextArgs = Partial<RenderContext> & {
|
export type CreateRenderContextArgs = Partial<RenderContext> & {
|
||||||
origin?: string;
|
origin?: string;
|
||||||
request: RenderContext['request'];
|
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 request = options.request;
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const origin = options.origin ?? url.origin;
|
const origin = options.origin ?? url.origin;
|
||||||
const pathname = options.pathname ?? url.pathname;
|
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 {
|
return {
|
||||||
...options,
|
...options,
|
||||||
origin,
|
origin,
|
||||||
pathname,
|
pathname,
|
||||||
url,
|
url,
|
||||||
|
params,
|
||||||
|
props,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import type { ComponentInstance, Params, Props, RouteData } from '../../@types/astro';
|
import type { APIContext, 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 { renderPage as runtimeRenderPage } from '../../runtime/server/index.js';
|
import { renderPage as runtimeRenderPage } from '../../runtime/server/index.js';
|
||||||
import { attachToResponse } from '../cookies/index.js';
|
import { attachToResponse } from '../cookies/index.js';
|
||||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||||
|
import type { LogOptions } from '../logger/core.js';
|
||||||
import { getParams } from '../routing/params.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 { createResult } from './result.js';
|
||||||
import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js';
|
import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js';
|
||||||
|
|
||||||
|
@ -23,6 +22,26 @@ export const enum GetParamsAndPropsError {
|
||||||
NoMatchingStaticPath,
|
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(
|
export async function getParamsAndProps(
|
||||||
opts: GetParamsAndPropsOptions
|
opts: GetParamsAndPropsOptions
|
||||||
): Promise<[Params, Props] | GetParamsAndPropsError> {
|
): Promise<[Params, Props] | GetParamsAndPropsError> {
|
||||||
|
@ -84,65 +103,63 @@ export async function getParamsAndProps(
|
||||||
return [params, pageProps];
|
return [params, pageProps];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env: Environment) {
|
export type RenderPage = {
|
||||||
const paramsAndPropsRes = await getParamsAndProps({
|
mod: ComponentInstance;
|
||||||
logging: env.logging,
|
renderContext: RenderContext;
|
||||||
mod,
|
env: Environment;
|
||||||
route: ctx.route,
|
apiContext?: APIContext;
|
||||||
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 async function renderPage({ mod, renderContext, env, apiContext }: RenderPage) {
|
||||||
// Validate the page component before rendering the page
|
// Validate the page component before rendering the page
|
||||||
const Component = mod.default;
|
const Component = mod.default;
|
||||||
if (!Component)
|
if (!Component)
|
||||||
throw new Error(`Expected an exported Astro component but received typeof ${typeof 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({
|
const result = createResult({
|
||||||
adapterName: env.adapterName,
|
adapterName: env.adapterName,
|
||||||
links: ctx.links,
|
links: renderContext.links,
|
||||||
styles: ctx.styles,
|
styles: renderContext.styles,
|
||||||
logging: env.logging,
|
logging: env.logging,
|
||||||
markdown: env.markdown,
|
markdown: env.markdown,
|
||||||
mode: env.mode,
|
mode: env.mode,
|
||||||
origin: ctx.origin,
|
origin: renderContext.origin,
|
||||||
params,
|
params: renderContext.params,
|
||||||
props: pageProps,
|
props: renderContext.props,
|
||||||
pathname: ctx.pathname,
|
pathname: renderContext.pathname,
|
||||||
componentMetadata: ctx.componentMetadata,
|
componentMetadata: renderContext.componentMetadata,
|
||||||
resolve: env.resolve,
|
resolve: env.resolve,
|
||||||
renderers: env.renderers,
|
renderers: env.renderers,
|
||||||
request: ctx.request,
|
request: renderContext.request,
|
||||||
site: env.site,
|
site: env.site,
|
||||||
scripts: ctx.scripts,
|
scripts: renderContext.scripts,
|
||||||
ssr: env.ssr,
|
ssr: env.ssr,
|
||||||
status: ctx.status ?? 200,
|
status: renderContext.status ?? 200,
|
||||||
|
locals,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Support `export const components` for `MDX` pages
|
// Support `export const components` for `MDX` pages
|
||||||
if (typeof (mod as any).components === 'object') {
|
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,
|
result,
|
||||||
Component,
|
Component,
|
||||||
pageProps,
|
renderContext.props,
|
||||||
null,
|
null,
|
||||||
env.streaming,
|
env.streaming,
|
||||||
ctx.route
|
renderContext.route
|
||||||
);
|
);
|
||||||
|
|
||||||
// If there is an Astro.cookies instance, attach it to the response so that
|
// 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;
|
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 { fileURLToPath } from 'url';
|
||||||
import type {
|
import type {
|
||||||
|
AstroMiddlewareInstance,
|
||||||
AstroSettings,
|
AstroSettings,
|
||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
|
MiddlewareResponseHandler,
|
||||||
RouteData,
|
RouteData,
|
||||||
SSRElement,
|
SSRElement,
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
} from '../../../@types/astro';
|
} from '../../../@types/astro';
|
||||||
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
|
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
|
||||||
|
import { createAPIContext } from '../../endpoint/index.js';
|
||||||
import { enhanceViteSSRError } from '../../errors/dev/index.js';
|
import { enhanceViteSSRError } from '../../errors/dev/index.js';
|
||||||
import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js';
|
import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js';
|
||||||
|
import { callMiddleware } from '../../middleware/callMiddleware.js';
|
||||||
import type { ModuleLoader } from '../../module-loader/index';
|
import type { ModuleLoader } from '../../module-loader/index';
|
||||||
import { isPage, resolveIdToUrl, viteID } from '../../util.js';
|
import { isPage, resolveIdToUrl, viteID } from '../../util.js';
|
||||||
import { createRenderContext, renderPage as coreRenderPage } from '../index.js';
|
import { createRenderContext, renderPage as coreRenderPage } from '../index.js';
|
||||||
|
@ -35,6 +39,10 @@ export interface SSROptions {
|
||||||
request: Request;
|
request: Request;
|
||||||
/** optional, in case we need to render something outside of a dev server */
|
/** optional, in case we need to render something outside of a dev server */
|
||||||
route?: RouteData;
|
route?: RouteData;
|
||||||
|
/**
|
||||||
|
* Optional middlewares
|
||||||
|
*/
|
||||||
|
middleware?: AstroMiddlewareInstance<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance];
|
export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance];
|
||||||
|
@ -158,8 +166,9 @@ export async function renderPage(options: SSROptions): Promise<Response> {
|
||||||
env: options.env,
|
env: options.env,
|
||||||
filePath: options.filePath,
|
filePath: options.filePath,
|
||||||
});
|
});
|
||||||
|
const { env } = options;
|
||||||
|
|
||||||
const ctx = createRenderContext({
|
const renderContext = await createRenderContext({
|
||||||
request: options.request,
|
request: options.request,
|
||||||
origin: options.origin,
|
origin: options.origin,
|
||||||
pathname: options.pathname,
|
pathname: options.pathname,
|
||||||
|
@ -168,7 +177,25 @@ export async function renderPage(options: SSROptions): Promise<Response> {
|
||||||
styles,
|
styles,
|
||||||
componentMetadata: metadata,
|
componentMetadata: metadata,
|
||||||
route: options.route,
|
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 { createRenderContext } from './context.js';
|
||||||
export type { RenderContext } 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 type { Environment } from './environment';
|
||||||
export { createBasicEnvironment, createEnvironment } from './environment.js';
|
export { createBasicEnvironment, createEnvironment } from './environment.js';
|
||||||
export { loadRenderer } from './renderer.js';
|
export { loadRenderer } from './renderer.js';
|
||||||
|
|
|
@ -50,6 +50,7 @@ export interface CreateResultArgs {
|
||||||
componentMetadata?: SSRResult['componentMetadata'];
|
componentMetadata?: SSRResult['componentMetadata'];
|
||||||
request: Request;
|
request: Request;
|
||||||
status: number;
|
status: number;
|
||||||
|
locals: App.Locals;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFunctionExpression(slot: any) {
|
function getFunctionExpression(slot: any) {
|
||||||
|
@ -131,7 +132,7 @@ class Slots {
|
||||||
let renderMarkdown: any = null;
|
let renderMarkdown: any = null;
|
||||||
|
|
||||||
export function createResult(args: CreateResultArgs): SSRResult {
|
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 url = new URL(request.url);
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
|
@ -200,6 +201,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
||||||
},
|
},
|
||||||
params,
|
params,
|
||||||
props,
|
props,
|
||||||
|
locals,
|
||||||
request,
|
request,
|
||||||
url,
|
url,
|
||||||
redirect: args.ssr
|
redirect: args.ssr
|
||||||
|
|
|
@ -16,6 +16,7 @@ export interface CreateRequestOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||||
|
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||||
|
|
||||||
export function createRequest({
|
export function createRequest({
|
||||||
url,
|
url,
|
||||||
|
@ -65,5 +66,7 @@ export function createRequest({
|
||||||
Reflect.set(request, clientAddressSymbol, clientAddress);
|
Reflect.set(request, clientAddressSymbol, clientAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reflect.set(request, clientLocalsSymbol, {});
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ export async function runHookConfigSetup({
|
||||||
|
|
||||||
let updatedConfig: AstroConfig = { ...settings.config };
|
let updatedConfig: AstroConfig = { ...settings.config };
|
||||||
let updatedSettings: AstroSettings = { ...settings, config: updatedConfig };
|
let updatedSettings: AstroSettings = { ...settings, config: updatedConfig };
|
||||||
|
|
||||||
for (const integration of settings.config.integrations) {
|
for (const integration of settings.config.integrations) {
|
||||||
/**
|
/**
|
||||||
* By making integration hooks optional, Astro can now ignore null or undefined 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'> = {
|
const hooks: HookParameters<'astro:config:setup'> = {
|
||||||
config: updatedConfig,
|
config: updatedConfig,
|
||||||
command,
|
command,
|
||||||
|
|
|
@ -19,7 +19,7 @@ function getHandlerFromModule(mod: EndpointHandler, method: string) {
|
||||||
|
|
||||||
/** Renders an endpoint request to completion, returning the body. */
|
/** Renders an endpoint request to completion, returning the body. */
|
||||||
export async function renderEndpoint(mod: EndpointHandler, context: APIContext, ssr: boolean) {
|
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 chosenMethod = request.method?.toLowerCase();
|
||||||
const handler = getHandlerFromModule(mod, chosenMethod);
|
const handler = getHandlerFromModule(mod, chosenMethod);
|
||||||
if (!ssr && ssr === false && chosenMethod && chosenMethod !== 'get') {
|
if (!ssr && ssr === false && chosenMethod && chosenMethod !== 'get') {
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import type http from 'http';
|
import type http from 'http';
|
||||||
import mime from 'mime';
|
import mime from 'mime';
|
||||||
import type { ComponentInstance, ManifestData, RouteData } from '../@types/astro';
|
import type { ComponentInstance, ManifestData, RouteData } from '../@types/astro';
|
||||||
import type {
|
|
||||||
ComponentPreload,
|
|
||||||
DevelopmentEnvironment,
|
|
||||||
SSROptions,
|
|
||||||
} from '../core/render/dev/index';
|
|
||||||
|
|
||||||
import { attachToResponse } from '../core/cookies/index.js';
|
import { attachToResponse } from '../core/cookies/index.js';
|
||||||
import { call as callEndpoint } from '../core/endpoint/dev/index.js';
|
import { call as callEndpoint } from '../core/endpoint/dev/index.js';
|
||||||
import { throwIfRedirectNotAllowed } from '../core/endpoint/index.js';
|
import { throwIfRedirectNotAllowed } from '../core/endpoint/index.js';
|
||||||
import { AstroErrorData } from '../core/errors/index.js';
|
import { AstroErrorData } from '../core/errors/index.js';
|
||||||
import { warn } from '../core/logger/core.js';
|
import { warn } from '../core/logger/core.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 { preload, renderPage } from '../core/render/dev/index.js';
|
||||||
import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/index.js';
|
import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/index.js';
|
||||||
import { createRequest } from '../core/request.js';
|
import { createRequest } from '../core/request.js';
|
||||||
|
@ -169,7 +169,12 @@ export async function handleRoute(
|
||||||
request,
|
request,
|
||||||
route,
|
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.
|
// Route successfully matched! Render it.
|
||||||
if (route.type === 'endpoint') {
|
if (route.type === 'endpoint') {
|
||||||
const result = await callEndpoint(options, logging);
|
const result = await callEndpoint(options, logging);
|
||||||
|
|
7
packages/astro/test/fixtures/middleware-dev/astro.config.mjs
vendored
Normal file
7
packages/astro/test/fixtures/middleware-dev/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
experimental: {
|
||||||
|
middleware: true
|
||||||
|
}
|
||||||
|
});
|
8
packages/astro/test/fixtures/middleware-dev/package.json
vendored
Normal file
8
packages/astro/test/fixtures/middleware-dev/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "@test/middleware-dev",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
40
packages/astro/test/fixtures/middleware-dev/src/middleware.js
vendored
Normal file
40
packages/astro/test/fixtures/middleware-dev/src/middleware.js
vendored
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { sequence, defineMiddleware } from 'astro/middleware';
|
||||||
|
|
||||||
|
const first = defineMiddleware(async (context, next) => {
|
||||||
|
if (context.request.url.includes('/lorem')) {
|
||||||
|
context.locals.name = 'ipsum';
|
||||||
|
} else if (context.request.url.includes('/rewrite')) {
|
||||||
|
return new Response('<span>New content!!</span>', {
|
||||||
|
status: 200,
|
||||||
|
});
|
||||||
|
} else if (context.request.url.includes('/broken-500')) {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
context.locals.name = 'bar';
|
||||||
|
}
|
||||||
|
return await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
const second = defineMiddleware(async (context, next) => {
|
||||||
|
if (context.request.url.includes('/second')) {
|
||||||
|
context.locals.name = 'second';
|
||||||
|
} else if (context.request.url.includes('/redirect')) {
|
||||||
|
return context.redirect('/', 302);
|
||||||
|
}
|
||||||
|
return await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
const third = defineMiddleware(async (context, next) => {
|
||||||
|
if (context.request.url.includes('/broken-locals')) {
|
||||||
|
context.locals = {
|
||||||
|
fn() {},
|
||||||
|
};
|
||||||
|
} else if (context.request.url.includes('/does-nothing')) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
export const onRequest = sequence(first, second, third);
|
0
packages/astro/test/fixtures/middleware-dev/src/pages/broken-500.astro
vendored
Normal file
0
packages/astro/test/fixtures/middleware-dev/src/pages/broken-500.astro
vendored
Normal file
0
packages/astro/test/fixtures/middleware-dev/src/pages/broken-locals.astro
vendored
Normal file
0
packages/astro/test/fixtures/middleware-dev/src/pages/broken-locals.astro
vendored
Normal file
9
packages/astro/test/fixtures/middleware-dev/src/pages/does-nothing.astro
vendored
Normal file
9
packages/astro/test/fixtures/middleware-dev/src/pages/does-nothing.astro
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Testing</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<p>Not interested</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
14
packages/astro/test/fixtures/middleware-dev/src/pages/index.astro
vendored
Normal file
14
packages/astro/test/fixtures/middleware-dev/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
const data = Astro.locals;
|
||||||
|
---
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Testing</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<span>Index</span>
|
||||||
|
<p>{data?.name}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
13
packages/astro/test/fixtures/middleware-dev/src/pages/lorem.astro
vendored
Normal file
13
packages/astro/test/fixtures/middleware-dev/src/pages/lorem.astro
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
const data = Astro.locals;
|
||||||
|
---
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Testing</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<p>{data?.name}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
9
packages/astro/test/fixtures/middleware-dev/src/pages/not-interested.astro
vendored
Normal file
9
packages/astro/test/fixtures/middleware-dev/src/pages/not-interested.astro
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Testing</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<p>Not interested</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
0
packages/astro/test/fixtures/middleware-dev/src/pages/redirect.astro
vendored
Normal file
0
packages/astro/test/fixtures/middleware-dev/src/pages/redirect.astro
vendored
Normal file
9
packages/astro/test/fixtures/middleware-dev/src/pages/rewrite.astro
vendored
Normal file
9
packages/astro/test/fixtures/middleware-dev/src/pages/rewrite.astro
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Testing</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<p>Rewrite</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
13
packages/astro/test/fixtures/middleware-dev/src/pages/second.astro
vendored
Normal file
13
packages/astro/test/fixtures/middleware-dev/src/pages/second.astro
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
const data = Astro.locals;
|
||||||
|
---
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Testing</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<p>{data?.name}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
8
packages/astro/test/fixtures/middleware-ssg/astro.config.mjs
vendored
Normal file
8
packages/astro/test/fixtures/middleware-ssg/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
output: "static",
|
||||||
|
experimental: {
|
||||||
|
middleware: true
|
||||||
|
}
|
||||||
|
});
|
8
packages/astro/test/fixtures/middleware-ssg/package.json
vendored
Normal file
8
packages/astro/test/fixtures/middleware-ssg/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "@test/middleware-ssg",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
12
packages/astro/test/fixtures/middleware-ssg/src/middleware.js
vendored
Normal file
12
packages/astro/test/fixtures/middleware-ssg/src/middleware.js
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { sequence, defineMiddleware } from 'astro/middleware';
|
||||||
|
|
||||||
|
const first = defineMiddleware(async (context, next) => {
|
||||||
|
if (context.request.url.includes('/second')) {
|
||||||
|
context.locals.name = 'second';
|
||||||
|
} else {
|
||||||
|
context.locals.name = 'bar';
|
||||||
|
}
|
||||||
|
return await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
export const onRequest = sequence(first);
|
14
packages/astro/test/fixtures/middleware-ssg/src/pages/index.astro
vendored
Normal file
14
packages/astro/test/fixtures/middleware-ssg/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
const data = Astro.locals;
|
||||||
|
---
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Testing</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<span>Index</span>
|
||||||
|
<p>{data?.name}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
13
packages/astro/test/fixtures/middleware-ssg/src/pages/second.astro
vendored
Normal file
13
packages/astro/test/fixtures/middleware-ssg/src/pages/second.astro
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
const data = Astro.locals;
|
||||||
|
---
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Testing</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<p>{data?.name}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
202
packages/astro/test/middleware.test.js
Normal file
202
packages/astro/test/middleware.test.js
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
import { loadFixture } from './test-utils.js';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
import testAdapter from './test-adapter.js';
|
||||||
|
|
||||||
|
describe('Middleware in DEV mode', () => {
|
||||||
|
/** @type {import('./test-utils').Fixture} */
|
||||||
|
let fixture;
|
||||||
|
let devServer;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/middleware-dev/',
|
||||||
|
});
|
||||||
|
devServer = await fixture.startDevServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await devServer.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render locals data', async () => {
|
||||||
|
const html = await fixture.fetch('/').then((res) => res.text());
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
expect($('p').html()).to.equal('bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change locals data based on URL', async () => {
|
||||||
|
let html = await fixture.fetch('/').then((res) => res.text());
|
||||||
|
let $ = cheerio.load(html);
|
||||||
|
expect($('p').html()).to.equal('bar');
|
||||||
|
|
||||||
|
html = await fixture.fetch('/lorem').then((res) => res.text());
|
||||||
|
$ = cheerio.load(html);
|
||||||
|
expect($('p').html()).to.equal('ipsum');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call a second middleware', async () => {
|
||||||
|
let html = await fixture.fetch('/second').then((res) => res.text());
|
||||||
|
let $ = cheerio.load(html);
|
||||||
|
expect($('p').html()).to.equal('second');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully create a new response', async () => {
|
||||||
|
let html = await fixture.fetch('/rewrite').then((res) => res.text());
|
||||||
|
let $ = cheerio.load(html);
|
||||||
|
expect($('p').html()).to.be.null;
|
||||||
|
expect($('span').html()).to.equal('New content!!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a new response that is a 500', async () => {
|
||||||
|
await fixture.fetch('/broken-500').then((res) => {
|
||||||
|
expect(res.status).to.equal(500);
|
||||||
|
return res.text();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully render a page if the middleware calls only next() and returns nothing', async () => {
|
||||||
|
let html = await fixture.fetch('/not-interested').then((res) => res.text());
|
||||||
|
let $ = cheerio.load(html);
|
||||||
|
expect($('p').html()).to.equal('Not interested');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when locals are not serializable', async () => {
|
||||||
|
let html = await fixture.fetch('/broken-locals').then((res) => res.text());
|
||||||
|
let $ = cheerio.load(html);
|
||||||
|
expect($('title').html()).to.equal('LocalsNotSerializable');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error when the middleware doesn't call next or doesn't return a response", async () => {
|
||||||
|
let html = await fixture.fetch('/does-nothing').then((res) => res.text());
|
||||||
|
let $ = cheerio.load(html);
|
||||||
|
expect($('title').html()).to.equal('MiddlewareNoDataOrNextCalled');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Middleware in PROD mode, SSG', () => {
|
||||||
|
/** @type {import('./test-utils').Fixture} */
|
||||||
|
let fixture;
|
||||||
|
/** @type {import('./test-utils').PreviewServer} */
|
||||||
|
let previewServer;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/middleware-ssg/',
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render locals data', async () => {
|
||||||
|
const html = await fixture.readFile('/index.html');
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
expect($('p').html()).to.equal('bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change locals data based on URL', async () => {
|
||||||
|
let html = await fixture.readFile('/index.html');
|
||||||
|
let $ = cheerio.load(html);
|
||||||
|
expect($('p').html()).to.equal('bar');
|
||||||
|
|
||||||
|
html = await fixture.readFile('/second/index.html');
|
||||||
|
$ = cheerio.load(html);
|
||||||
|
expect($('p').html()).to.equal('second');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Middleware API in PROD mode, SSR', () => {
|
||||||
|
/** @type {import('./test-utils').Fixture} */
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/middleware-dev/',
|
||||||
|
output: 'server',
|
||||||
|
adapter: testAdapter({
|
||||||
|
// exports: ['manifest', 'createApp', 'middleware'],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render locals data', async () => {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const request = new Request('http://example.com/');
|
||||||
|
const response = await app.render(request);
|
||||||
|
const html = await response.text();
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
expect($('p').html()).to.equal('bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change locals data based on URL', async () => {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
let response = await app.render(new Request('http://example.com/'));
|
||||||
|
let html = await response.text();
|
||||||
|
let $ = cheerio.load(html);
|
||||||
|
expect($('p').html()).to.equal('bar');
|
||||||
|
|
||||||
|
response = await app.render(new Request('http://example.com/lorem'));
|
||||||
|
html = await response.text();
|
||||||
|
$ = cheerio.load(html);
|
||||||
|
expect($('p').html()).to.equal('ipsum');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully redirect to another page', async () => {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const request = new Request('http://example.com/redirect');
|
||||||
|
const response = await app.render(request);
|
||||||
|
expect(response.status).to.equal(302);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call a second middleware', async () => {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const response = await app.render(new Request('http://example.com/second'));
|
||||||
|
const html = await response.text();
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
expect($('p').html()).to.equal('second');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully create a new response', async () => {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const request = new Request('http://example.com/rewrite');
|
||||||
|
const response = await app.render(request);
|
||||||
|
const html = await response.text();
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
expect($('p').html()).to.be.null;
|
||||||
|
expect($('span').html()).to.equal('New content!!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a new response that is a 500', async () => {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const request = new Request('http://example.com/broken-500');
|
||||||
|
const response = await app.render(request);
|
||||||
|
expect(response.status).to.equal(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully render a page if the middleware calls only next() and returns nothing', async () => {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const request = new Request('http://example.com/not-interested');
|
||||||
|
const response = await app.render(request);
|
||||||
|
const html = await response.text();
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
expect($('p').html()).to.equal('Not interested');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT throw an error when locals are not serializable', async () => {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const request = new Request('http://example.com/broken-locals');
|
||||||
|
const response = await app.render(request);
|
||||||
|
const html = await response.text();
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
expect($('title').html()).to.not.equal('LocalsNotSerializable');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throws an error when the middleware doesn't call next or doesn't return a response", async () => {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const request = new Request('http://example.com/does-nothing');
|
||||||
|
const response = await app.render(request);
|
||||||
|
const html = await response.text();
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
expect($('title').html()).to.not.equal('MiddlewareNoDataReturned');
|
||||||
|
});
|
||||||
|
});
|
|
@ -42,6 +42,7 @@ export default function ({ provideAddress = true, extendAdapter } = { provideAdd
|
||||||
return new Response(data);
|
return new Response(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reflect.set(request, Symbol.for('astro.locals'), {});
|
||||||
${provideAddress ? `request[Symbol.for('astro.clientAddress')] = '0.0.0.0';` : ''}
|
${provideAddress ? `request[Symbol.for('astro.clientAddress')] = '0.0.0.0';` : ''}
|
||||||
return super.render(request, routeData);
|
return super.render(request, routeData);
|
||||||
}
|
}
|
||||||
|
@ -51,6 +52,7 @@ export default function ({ provideAddress = true, extendAdapter } = { provideAdd
|
||||||
return {
|
return {
|
||||||
manifest,
|
manifest,
|
||||||
createApp: (streaming) => new MyApp(manifest, streaming)
|
createApp: (streaming) => new MyApp(manifest, streaming)
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -231,7 +231,7 @@ export async function loadFixture(inlineConfig) {
|
||||||
},
|
},
|
||||||
loadTestAdapterApp: async (streaming) => {
|
loadTestAdapterApp: async (streaming) => {
|
||||||
const url = new URL(`./server/entry.mjs?id=${fixtureId}`, config.outDir);
|
const url = new URL(`./server/entry.mjs?id=${fixtureId}`, config.outDir);
|
||||||
const { createApp, manifest } = await import(url);
|
const { createApp, manifest, middleware } = await import(url);
|
||||||
const app = createApp(streaming);
|
const app = createApp(streaming);
|
||||||
app.manifest = manifest;
|
app.manifest = manifest;
|
||||||
return app;
|
return app;
|
||||||
|
|
|
@ -95,13 +95,21 @@ describe('core/render', () => {
|
||||||
)}`;
|
)}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const ctx = createRenderContext({
|
const PageModule = createAstroModule(Page);
|
||||||
|
const ctx = await createRenderContext({
|
||||||
request: new Request('http://example.com/'),
|
request: new Request('http://example.com/'),
|
||||||
links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
|
links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
|
||||||
|
mod: PageModule,
|
||||||
|
env,
|
||||||
});
|
});
|
||||||
const PageModule = createAstroModule(Page);
|
|
||||||
|
|
||||||
const response = await renderPage(PageModule, ctx, env);
|
const response = await renderPage({
|
||||||
|
mod: PageModule,
|
||||||
|
renderContext: ctx,
|
||||||
|
env,
|
||||||
|
params: ctx.params,
|
||||||
|
props: ctx.props,
|
||||||
|
});
|
||||||
|
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
@ -173,14 +181,21 @@ describe('core/render', () => {
|
||||||
)}`;
|
)}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const ctx = createRenderContext({
|
const PageModule = createAstroModule(Page);
|
||||||
|
const ctx = await createRenderContext({
|
||||||
request: new Request('http://example.com/'),
|
request: new Request('http://example.com/'),
|
||||||
links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
|
links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
|
||||||
|
env,
|
||||||
|
mod: PageModule,
|
||||||
});
|
});
|
||||||
const PageModule = createAstroModule(Page);
|
|
||||||
|
|
||||||
const response = await renderPage(PageModule, ctx, env);
|
|
||||||
|
|
||||||
|
const response = await renderPage({
|
||||||
|
mod: PageModule,
|
||||||
|
renderContext: ctx,
|
||||||
|
env,
|
||||||
|
params: ctx.params,
|
||||||
|
props: ctx.props,
|
||||||
|
});
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
@ -218,14 +233,21 @@ describe('core/render', () => {
|
||||||
)}`;
|
)}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const ctx = createRenderContext({
|
const PageModule = createAstroModule(Page);
|
||||||
|
const ctx = await createRenderContext({
|
||||||
request: new Request('http://example.com/'),
|
request: new Request('http://example.com/'),
|
||||||
links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
|
links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
|
||||||
|
env,
|
||||||
|
mod: PageModule,
|
||||||
});
|
});
|
||||||
const PageModule = createAstroModule(Page);
|
|
||||||
|
|
||||||
const response = await renderPage(PageModule, ctx, env);
|
|
||||||
|
|
||||||
|
const response = await renderPage({
|
||||||
|
mod: PageModule,
|
||||||
|
renderContext: ctx,
|
||||||
|
env,
|
||||||
|
params: ctx.params,
|
||||||
|
props: ctx.props,
|
||||||
|
});
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createComponent,
|
createComponent,
|
||||||
render,
|
render,
|
||||||
|
@ -46,8 +45,18 @@ describe('core/render', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const ctx = createRenderContext({ request: new Request('http://example.com/') });
|
const mod = createAstroModule(Page);
|
||||||
const response = await renderPage(createAstroModule(Page), ctx, env);
|
const ctx = await createRenderContext({
|
||||||
|
request: new Request('http://example.com/'),
|
||||||
|
env,
|
||||||
|
mod,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await renderPage({
|
||||||
|
mod,
|
||||||
|
renderContext: ctx,
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
expect(response.status).to.equal(200);
|
expect(response.status).to.equal(200);
|
||||||
|
|
||||||
|
@ -85,8 +94,17 @@ describe('core/render', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const ctx = createRenderContext({ request: new Request('http://example.com/') });
|
const mod = createAstroModule(Page);
|
||||||
const response = await renderPage(createAstroModule(Page), ctx, env);
|
const ctx = await createRenderContext({
|
||||||
|
request: new Request('http://example.com/'),
|
||||||
|
env,
|
||||||
|
mod,
|
||||||
|
});
|
||||||
|
const response = await renderPage({
|
||||||
|
mod,
|
||||||
|
renderContext: ctx,
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
expect(response.status).to.equal(200);
|
expect(response.status).to.equal(200);
|
||||||
|
|
||||||
|
@ -105,8 +123,18 @@ describe('core/render', () => {
|
||||||
return render`<div>${renderComponent(result, 'Component', Component, {})}</div>`;
|
return render`<div>${renderComponent(result, 'Component', Component, {})}</div>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const ctx = createRenderContext({ request: new Request('http://example.com/') });
|
const mod = createAstroModule(Page);
|
||||||
const response = await renderPage(createAstroModule(Page), ctx, env);
|
const ctx = await createRenderContext({
|
||||||
|
request: new Request('http://example.com/'),
|
||||||
|
env,
|
||||||
|
mod,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await renderPage({
|
||||||
|
mod,
|
||||||
|
renderContext: ctx,
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await response.text();
|
await response.text();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { polyfill } from '@astrojs/webapi';
|
import { polyfill } from '@astrojs/webapi';
|
||||||
import type { SSRManifest } from 'astro';
|
import type { SSRManifest } from 'astro';
|
||||||
import { NodeApp } from 'astro/app/node';
|
import { NodeApp } from 'astro/app/node';
|
||||||
import middleware from './middleware.js';
|
import middleware from './nodeMiddleware.js';
|
||||||
import startServer from './standalone.js';
|
import startServer from './standalone.js';
|
||||||
import type { Options } from './types';
|
import type { Options } from './types';
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import https from 'https';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { createServer } from './http-server.js';
|
import { createServer } from './http-server.js';
|
||||||
import middleware from './middleware.js';
|
import middleware from './nodeMiddleware.js';
|
||||||
import type { Options } from './types';
|
import type { Options } from './types';
|
||||||
|
|
||||||
function resolvePaths(options: Options) {
|
function resolvePaths(options: Options) {
|
||||||
|
|
4146
pnpm-lock.yaml
4146
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue