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;
|
||||
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": {
|
||||
"types": "./zod.d.ts",
|
||||
"default": "./zod.mjs"
|
||||
},
|
||||
"./middleware": {
|
||||
"types": "./dist/core/middleware/index.d.ts",
|
||||
"default": "./dist/core/middleware/index.js"
|
||||
}
|
||||
},
|
||||
"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;
|
||||
open?: boolean;
|
||||
experimentalAssets?: boolean;
|
||||
experimentalMiddleware?: boolean;
|
||||
}
|
||||
|
||||
export interface BuildConfig {
|
||||
|
@ -1034,6 +1035,26 @@ export interface AstroUserConfig {
|
|||
* }
|
||||
*/
|
||||
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
|
||||
|
@ -1431,6 +1452,11 @@ interface AstroSharedContext<Props extends Record<string, any> = Record<string,
|
|||
* Redirect to another page (**SSR Only**).
|
||||
*/
|
||||
redirect(path: string, status?: 301 | 302 | 303 | 307 | 308): Response;
|
||||
|
||||
/**
|
||||
* Object accessed via Astro middleware
|
||||
*/
|
||||
locals: App.Locals;
|
||||
}
|
||||
|
||||
export interface APIContext<Props extends Record<string, any> = Record<string, any>>
|
||||
|
@ -1464,7 +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'];
|
||||
/**
|
||||
|
@ -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)
|
||||
*/
|
||||
redirect: AstroSharedContext['redirect'];
|
||||
|
||||
/**
|
||||
* Object accessed via Astro middleware.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```ts
|
||||
* // src/middleware.ts
|
||||
* import {defineMiddleware} from "astro/middleware";
|
||||
*
|
||||
* export const onRequest = defineMiddleware((context, next) => {
|
||||
* context.locals.greeting = "Hello!";
|
||||
* next();
|
||||
* });
|
||||
* ```
|
||||
* Inside a `.astro` file:
|
||||
* ```astro
|
||||
* ---
|
||||
* // src/pages/index.astro
|
||||
* const greeting = Astro.locals.greeting;
|
||||
* ---
|
||||
* <h1>{greeting}</h1>
|
||||
* ```
|
||||
*/
|
||||
locals: App.Locals;
|
||||
}
|
||||
|
||||
export type Props = Record<string, unknown>;
|
||||
|
@ -1592,6 +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 {
|
||||
settings: AstroSettings;
|
||||
logging: LogOptions;
|
||||
|
|
|
@ -2,6 +2,7 @@ import type {
|
|||
ComponentInstance,
|
||||
EndpointHandler,
|
||||
ManifestData,
|
||||
MiddlewareResponseHandler,
|
||||
RouteData,
|
||||
SSRElement,
|
||||
} from '../../@types/astro';
|
||||
|
@ -9,9 +10,10 @@ import type { RouteInfo, SSRManifest as Manifest } from './types';
|
|||
|
||||
import mime from 'mime';
|
||||
import { attachToResponse, getSetCookiesFromResponse } from '../cookies/index.js';
|
||||
import { call as callEndpoint } from '../endpoint/index.js';
|
||||
import { call as callEndpoint, createAPIContext } from '../endpoint/index.js';
|
||||
import { consoleLogDestination } from '../logger/console.js';
|
||||
import { error, type LogOptions } from '../logger/core.js';
|
||||
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||
import { removeTrailingForwardSlash } from '../path.js';
|
||||
import {
|
||||
createEnvironment,
|
||||
|
@ -28,6 +30,8 @@ import {
|
|||
import { matchRoute } from '../routing/match.js';
|
||||
export { deserializeManifest } from './common.js';
|
||||
|
||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||
|
||||
export const pagesVirtualModuleId = '@astrojs-pages-virtual-entry';
|
||||
export const resolvedPagesVirtualModuleId = '\0' + pagesVirtualModuleId;
|
||||
const responseSentSymbol = Symbol.for('astro.responseSent');
|
||||
|
@ -127,6 +131,8 @@ export class App {
|
|||
}
|
||||
}
|
||||
|
||||
Reflect.set(request, clientLocalsSymbol, {});
|
||||
|
||||
// Use the 404 status code for 404.astro components
|
||||
if (routeData.route === '/404') {
|
||||
defaultStatus = 404;
|
||||
|
@ -191,7 +197,7 @@ export class App {
|
|||
}
|
||||
|
||||
try {
|
||||
const ctx = createRenderContext({
|
||||
const renderContext = await createRenderContext({
|
||||
request,
|
||||
origin: url.origin,
|
||||
pathname,
|
||||
|
@ -200,9 +206,35 @@ export class App {
|
|||
links,
|
||||
route: routeData,
|
||||
status,
|
||||
mod: mod as any,
|
||||
env: this.#env,
|
||||
});
|
||||
|
||||
const response = await renderPage(mod, ctx, this.#env);
|
||||
const apiContext = createAPIContext({
|
||||
request: renderContext.request,
|
||||
params: renderContext.params,
|
||||
props: renderContext.props,
|
||||
site: this.#env.site,
|
||||
adapterName: this.#env.adapterName,
|
||||
});
|
||||
const onRequest = this.#manifest.middleware?.onRequest;
|
||||
let response;
|
||||
if (onRequest) {
|
||||
response = await callMiddleware<Response>(
|
||||
onRequest as MiddlewareResponseHandler,
|
||||
apiContext,
|
||||
() => {
|
||||
return renderPage({ mod, renderContext, env: this.#env, apiContext });
|
||||
}
|
||||
);
|
||||
} else {
|
||||
response = await renderPage({
|
||||
mod,
|
||||
renderContext,
|
||||
env: this.#env,
|
||||
apiContext,
|
||||
});
|
||||
}
|
||||
Reflect.set(request, responseSentSymbol, true);
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
|
@ -224,15 +256,23 @@ export class App {
|
|||
const pathname = '/' + this.removeBase(url.pathname);
|
||||
const handler = mod as unknown as EndpointHandler;
|
||||
|
||||
const ctx = createRenderContext({
|
||||
const ctx = await createRenderContext({
|
||||
request,
|
||||
origin: url.origin,
|
||||
pathname,
|
||||
route: routeData,
|
||||
status,
|
||||
env: this.#env,
|
||||
mod: handler as any,
|
||||
});
|
||||
|
||||
const result = await callEndpoint(handler, this.#env, ctx, this.#logging);
|
||||
const result = await callEndpoint(
|
||||
handler,
|
||||
this.#env,
|
||||
ctx,
|
||||
this.#logging,
|
||||
this.#manifest.middleware
|
||||
);
|
||||
|
||||
if (result.type === 'response') {
|
||||
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark';
|
||||
import type {
|
||||
AstroMiddlewareInstance,
|
||||
ComponentInstance,
|
||||
RouteData,
|
||||
SerializedRouteData,
|
||||
|
@ -38,6 +39,7 @@ export interface SSRManifest {
|
|||
entryModules: Record<string, string>;
|
||||
assets: Set<string>;
|
||||
componentMetadata: SSRResult['componentMetadata'];
|
||||
middleware?: AstroMiddlewareInstance<unknown>;
|
||||
}
|
||||
|
||||
export type SerializedSSRManifest = Omit<SSRManifest, 'routes' | 'assets' | 'componentMetadata'> & {
|
||||
|
|
|
@ -5,10 +5,13 @@ import type { OutputAsset, OutputChunk } from 'rollup';
|
|||
import { fileURLToPath } from 'url';
|
||||
import type {
|
||||
AstroConfig,
|
||||
AstroMiddlewareInstance,
|
||||
AstroSettings,
|
||||
ComponentInstance,
|
||||
EndpointHandler,
|
||||
EndpointOutput,
|
||||
ImageTransform,
|
||||
MiddlewareResponseHandler,
|
||||
RouteType,
|
||||
SSRError,
|
||||
SSRLoadedRenderer,
|
||||
|
@ -25,9 +28,14 @@ import {
|
|||
} from '../../core/path.js';
|
||||
import { runHookBuildGenerated } from '../../integrations/index.js';
|
||||
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||
import { call as callEndpoint, throwIfRedirectNotAllowed } from '../endpoint/index.js';
|
||||
import {
|
||||
call as callEndpoint,
|
||||
createAPIContext,
|
||||
throwIfRedirectNotAllowed,
|
||||
} from '../endpoint/index.js';
|
||||
import { AstroError } from '../errors/index.js';
|
||||
import { debug, info } from '../logger/core.js';
|
||||
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||
import { createEnvironment, createRenderContext, renderPage } from '../render/index.js';
|
||||
import { callGetStaticPaths } from '../render/route-cache.js';
|
||||
import {
|
||||
|
@ -157,6 +165,7 @@ async function generatePage(
|
|||
const scripts = pageInfo?.hoistedScript ?? null;
|
||||
|
||||
const pageModule = ssrEntry.pageMap?.get(pageData.component);
|
||||
const middleware = ssrEntry.middleware;
|
||||
|
||||
if (!pageModule) {
|
||||
throw new Error(
|
||||
|
@ -186,7 +195,7 @@ async function generatePage(
|
|||
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
const path = paths[i];
|
||||
await generatePath(path, opts, generationOptions);
|
||||
await generatePath(path, opts, generationOptions, middleware);
|
||||
const timeEnd = performance.now();
|
||||
const timeChange = getTimeStat(timeStart, timeEnd);
|
||||
const timeIncrease = `(+${timeChange})`;
|
||||
|
@ -328,7 +337,8 @@ function getUrlForPath(
|
|||
async function generatePath(
|
||||
pathname: string,
|
||||
opts: StaticBuildOptions,
|
||||
gopts: GeneratePathOptions
|
||||
gopts: GeneratePathOptions,
|
||||
middleware?: AstroMiddlewareInstance<unknown>
|
||||
) {
|
||||
const { settings, logging, origin, routeCache } = opts;
|
||||
const { mod, internals, linkIds, scripts: hoistedScripts, pageData, renderers } = gopts;
|
||||
|
@ -414,7 +424,8 @@ async function generatePath(
|
|||
ssr,
|
||||
streaming: true,
|
||||
});
|
||||
const ctx = createRenderContext({
|
||||
|
||||
const renderContext = await createRenderContext({
|
||||
origin,
|
||||
pathname,
|
||||
request: createRequest({ url, headers: new Headers(), logging, ssr }),
|
||||
|
@ -422,13 +433,22 @@ async function generatePath(
|
|||
scripts,
|
||||
links,
|
||||
route: pageData.route,
|
||||
env,
|
||||
mod,
|
||||
});
|
||||
|
||||
let body: string | Uint8Array;
|
||||
let encoding: BufferEncoding | undefined;
|
||||
if (pageData.route.type === 'endpoint') {
|
||||
const endpointHandler = mod as unknown as EndpointHandler;
|
||||
const result = await callEndpoint(endpointHandler, env, ctx, logging);
|
||||
|
||||
const result = await callEndpoint(
|
||||
endpointHandler,
|
||||
env,
|
||||
renderContext,
|
||||
logging,
|
||||
middleware as AstroMiddlewareInstance<Response | EndpointOutput>
|
||||
);
|
||||
|
||||
if (result.type === 'response') {
|
||||
throwIfRedirectNotAllowed(result.response, opts.settings.config);
|
||||
|
@ -443,7 +463,26 @@ async function generatePath(
|
|||
} else {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await renderPage(mod, ctx, env);
|
||||
const apiContext = createAPIContext({
|
||||
request: renderContext.request,
|
||||
params: renderContext.params,
|
||||
props: renderContext.props,
|
||||
site: env.site,
|
||||
adapterName: env.adapterName,
|
||||
});
|
||||
|
||||
const onRequest = middleware?.onRequest;
|
||||
if (onRequest) {
|
||||
response = await callMiddleware<Response>(
|
||||
onRequest as MiddlewareResponseHandler,
|
||||
apiContext,
|
||||
() => {
|
||||
return renderPage({ mod, renderContext, env, apiContext });
|
||||
}
|
||||
);
|
||||
} else {
|
||||
response = await renderPage({ mod, renderContext, env, apiContext });
|
||||
}
|
||||
} catch (err) {
|
||||
if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') {
|
||||
(err as SSRError).id = pageData.component;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import type { Plugin as VitePlugin } from 'vite';
|
||||
import type { AstroBuildPlugin } from '../plugin';
|
||||
import type { StaticBuildOptions } from '../types';
|
||||
|
||||
import { pagesVirtualModuleId, resolvedPagesVirtualModuleId } from '../../app/index.js';
|
||||
import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../../constants.js';
|
||||
import { addRollupInput } from '../add-rollup-input.js';
|
||||
import { eachPageData, hasPrerenderedPages, type BuildInternals } from '../internal.js';
|
||||
import type { AstroBuildPlugin } from '../plugin';
|
||||
import type { StaticBuildOptions } from '../types';
|
||||
|
||||
export function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
|
||||
return {
|
||||
|
@ -22,8 +22,15 @@ export function vitePluginPages(opts: StaticBuildOptions, internals: BuildIntern
|
|||
}
|
||||
},
|
||||
|
||||
load(id) {
|
||||
async load(id) {
|
||||
if (id === resolvedPagesVirtualModuleId) {
|
||||
let middlewareId = null;
|
||||
if (opts.settings.config.experimental.middleware) {
|
||||
middlewareId = await this.resolve(
|
||||
`${opts.settings.config.srcDir.pathname}/${MIDDLEWARE_PATH_SEGMENT_NAME}`
|
||||
);
|
||||
}
|
||||
|
||||
let importMap = '';
|
||||
let imports = [];
|
||||
let i = 0;
|
||||
|
@ -47,8 +54,12 @@ export function vitePluginPages(opts: StaticBuildOptions, internals: BuildIntern
|
|||
|
||||
const def = `${imports.join('\n')}
|
||||
|
||||
${middlewareId ? `import * as _middleware from "${middlewareId.id}";` : ''}
|
||||
|
||||
export const pageMap = new Map([${importMap}]);
|
||||
export const renderers = [${rendererItems}];`;
|
||||
export const renderers = [${rendererItems}];
|
||||
${middlewareId ? `export const middleware = _middleware;` : ''}
|
||||
`;
|
||||
|
||||
return def;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Plugin as VitePlugin } from 'vite';
|
||||
import type { AstroAdapter } from '../../../@types/astro';
|
||||
import type { AstroAdapter, AstroConfig } from '../../../@types/astro';
|
||||
import type { SerializedRouteInfo, SerializedSSRManifest } from '../../app/types';
|
||||
import type { BuildInternals } from '../internal.js';
|
||||
import type { StaticBuildOptions } from '../types';
|
||||
|
@ -21,7 +21,11 @@ const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
|||
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
|
||||
const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g');
|
||||
|
||||
export function vitePluginSSR(internals: BuildInternals, adapter: AstroAdapter): VitePlugin {
|
||||
export function vitePluginSSR(
|
||||
internals: BuildInternals,
|
||||
adapter: AstroAdapter,
|
||||
config: AstroConfig
|
||||
): VitePlugin {
|
||||
return {
|
||||
name: '@astrojs/vite-plugin-astro-ssr',
|
||||
enforce: 'post',
|
||||
|
@ -35,13 +39,18 @@ export function vitePluginSSR(internals: BuildInternals, adapter: AstroAdapter):
|
|||
},
|
||||
load(id) {
|
||||
if (id === resolvedVirtualModuleId) {
|
||||
let middleware = '';
|
||||
if (config.experimental?.middleware === true) {
|
||||
middleware = 'middleware: _main.middleware';
|
||||
}
|
||||
return `import * as adapter from '${adapter.serverEntrypoint}';
|
||||
import * as _main from '${pagesVirtualModuleId}';
|
||||
import { deserializeManifest as _deserializeManifest } from 'astro/app';
|
||||
import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest';
|
||||
const _manifest = Object.assign(_deserializeManifest('${manifestReplace}'), {
|
||||
pageMap: _main.pageMap,
|
||||
renderers: _main.renderers
|
||||
renderers: _main.renderers,
|
||||
${middleware}
|
||||
});
|
||||
_privateSetManifestDontUseThis(_manifest);
|
||||
const _args = ${adapter.args ? JSON.stringify(adapter.args) : 'undefined'};
|
||||
|
@ -235,7 +244,9 @@ export function pluginSSR(
|
|||
build: 'ssr',
|
||||
hooks: {
|
||||
'build:before': () => {
|
||||
let vitePlugin = ssr ? vitePluginSSR(internals, options.settings.adapter!) : undefined;
|
||||
let vitePlugin = ssr
|
||||
? vitePluginSSR(internals, options.settings.adapter!, options.settings.config)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
enforce: 'after-user-plugins',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { default as vite, InlineConfig } from 'vite';
|
||||
import type {
|
||||
AstroConfig,
|
||||
AstroMiddlewareInstance,
|
||||
AstroSettings,
|
||||
BuildConfig,
|
||||
ComponentInstance,
|
||||
|
@ -44,6 +45,7 @@ export interface StaticBuildOptions {
|
|||
|
||||
export interface SingleFileBuiltModule {
|
||||
pageMap: Map<ComponentPath, ComponentInstance>;
|
||||
middleware: AstroMiddlewareInstance<unknown>;
|
||||
renderers: SSRLoadedRenderer[];
|
||||
}
|
||||
|
||||
|
|
|
@ -103,6 +103,8 @@ export function resolveFlags(flags: Partial<Flags>): CLIFlags {
|
|||
drafts: typeof flags.drafts === 'boolean' ? flags.drafts : undefined,
|
||||
experimentalAssets:
|
||||
typeof flags.experimentalAssets === 'boolean' ? flags.experimentalAssets : undefined,
|
||||
experimentalMiddleware:
|
||||
typeof flags.experimentalMiddleware === 'boolean' ? flags.experimentalMiddleware : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -136,6 +138,9 @@ function mergeCLIFlags(astroConfig: AstroUserConfig, flags: CLIFlags) {
|
|||
// TODO: Come back here and refactor to remove this expected error.
|
||||
astroConfig.server.open = flags.open;
|
||||
}
|
||||
if (typeof flags.experimentalMiddleware === 'boolean') {
|
||||
astroConfig.experimental.middleware = true;
|
||||
}
|
||||
return astroConfig;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
|
|||
legacy: {},
|
||||
experimental: {
|
||||
assets: false,
|
||||
middleware: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -187,6 +188,7 @@ export const AstroConfigSchema = z.object({
|
|||
experimental: z
|
||||
.object({
|
||||
assets: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.assets),
|
||||
middleware: z.oboolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.middleware),
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
|
|
|
@ -10,3 +10,6 @@ export const SUPPORTED_MARKDOWN_FILE_EXTENSIONS = [
|
|||
'.mdwn',
|
||||
'.md',
|
||||
] as const;
|
||||
|
||||
// The folder name where to find the middleware
|
||||
export const MIDDLEWARE_PATH_SEGMENT_NAME = 'middleware';
|
||||
|
|
|
@ -8,15 +8,18 @@ export async function call(options: SSROptions, logging: LogOptions) {
|
|||
const {
|
||||
env,
|
||||
preload: [, mod],
|
||||
middleware,
|
||||
} = options;
|
||||
const endpointHandler = mod as unknown as EndpointHandler;
|
||||
|
||||
const ctx = createRenderContext({
|
||||
const ctx = await createRenderContext({
|
||||
request: options.request,
|
||||
origin: options.origin,
|
||||
pathname: options.pathname,
|
||||
route: options.route,
|
||||
env,
|
||||
mod: endpointHandler as any,
|
||||
});
|
||||
|
||||
return await callEndpoint(endpointHandler, env, ctx, logging);
|
||||
return await callEndpoint(endpointHandler, env, ctx, logging, middleware);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import type { APIContext, AstroConfig, EndpointHandler, Params } from '../../@types/astro';
|
||||
import type {
|
||||
APIContext,
|
||||
AstroConfig,
|
||||
AstroMiddlewareInstance,
|
||||
EndpointHandler,
|
||||
EndpointOutput,
|
||||
MiddlewareEndpointHandler,
|
||||
Params,
|
||||
} from '../../@types/astro';
|
||||
import type { Environment, RenderContext } from '../render/index';
|
||||
|
||||
import { renderEndpoint } from '../../runtime/server/index.js';
|
||||
|
@ -6,9 +14,11 @@ import { ASTRO_VERSION } from '../constants.js';
|
|||
import { AstroCookies, attachToResponse } from '../cookies/index.js';
|
||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||
import { warn, type LogOptions } from '../logger/core.js';
|
||||
import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js';
|
||||
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||
import { isValueSerializable } from '../render/core.js';
|
||||
|
||||
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||
|
||||
type EndpointCallResult =
|
||||
| {
|
||||
|
@ -22,7 +32,7 @@ type EndpointCallResult =
|
|||
response: Response;
|
||||
};
|
||||
|
||||
function createAPIContext({
|
||||
export function createAPIContext({
|
||||
request,
|
||||
params,
|
||||
site,
|
||||
|
@ -35,7 +45,7 @@ function createAPIContext({
|
|||
props: Record<string, any>;
|
||||
adapterName?: string;
|
||||
}): APIContext {
|
||||
return {
|
||||
const context = {
|
||||
cookies: new AstroCookies(request),
|
||||
request,
|
||||
params,
|
||||
|
@ -51,7 +61,6 @@ function createAPIContext({
|
|||
});
|
||||
},
|
||||
url: new URL(request.url),
|
||||
// @ts-expect-error
|
||||
get clientAddress() {
|
||||
if (!(clientAddressSymbol in request)) {
|
||||
if (adapterName) {
|
||||
|
@ -66,44 +75,60 @@ function createAPIContext({
|
|||
|
||||
return Reflect.get(request, clientAddressSymbol);
|
||||
},
|
||||
};
|
||||
} as APIContext;
|
||||
|
||||
// We define a custom property, so we can check the value passed to locals
|
||||
Object.defineProperty(context, 'locals', {
|
||||
get() {
|
||||
return Reflect.get(request, clientLocalsSymbol);
|
||||
},
|
||||
set(val) {
|
||||
if (typeof val !== 'object') {
|
||||
throw new AstroError(AstroErrorData.LocalsNotAnObject);
|
||||
} else {
|
||||
Reflect.set(request, clientLocalsSymbol, val);
|
||||
}
|
||||
},
|
||||
});
|
||||
return context;
|
||||
}
|
||||
|
||||
export async function call(
|
||||
export async function call<MiddlewareResult = Response | EndpointOutput>(
|
||||
mod: EndpointHandler,
|
||||
env: Environment,
|
||||
ctx: RenderContext,
|
||||
logging: LogOptions
|
||||
logging: LogOptions,
|
||||
middleware?: AstroMiddlewareInstance<MiddlewareResult> | undefined
|
||||
): Promise<EndpointCallResult> {
|
||||
const paramsAndPropsResp = await getParamsAndProps({
|
||||
mod: mod as any,
|
||||
route: ctx.route,
|
||||
routeCache: env.routeCache,
|
||||
pathname: ctx.pathname,
|
||||
logging: env.logging,
|
||||
ssr: env.ssr,
|
||||
});
|
||||
|
||||
if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.NoMatchingStaticPathFound,
|
||||
message: AstroErrorData.NoMatchingStaticPathFound.message(ctx.pathname),
|
||||
hint: ctx.route?.component
|
||||
? AstroErrorData.NoMatchingStaticPathFound.hint([ctx.route?.component])
|
||||
: '',
|
||||
});
|
||||
}
|
||||
const [params, props] = paramsAndPropsResp;
|
||||
|
||||
const context = createAPIContext({
|
||||
request: ctx.request,
|
||||
params,
|
||||
props,
|
||||
params: ctx.params,
|
||||
props: ctx.props,
|
||||
site: env.site,
|
||||
adapterName: env.adapterName,
|
||||
});
|
||||
|
||||
const response = await renderEndpoint(mod, context, env.ssr);
|
||||
let response = await renderEndpoint(mod, context, env.ssr);
|
||||
if (middleware && middleware.onRequest) {
|
||||
if (response.body === null) {
|
||||
const onRequest = middleware.onRequest as MiddlewareEndpointHandler;
|
||||
response = await callMiddleware<Response | EndpointOutput>(onRequest, context, async () => {
|
||||
if (env.mode === 'development' && !isValueSerializable(context.locals)) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.LocalsNotSerializable,
|
||||
message: AstroErrorData.LocalsNotSerializable.message(ctx.pathname),
|
||||
});
|
||||
}
|
||||
return response;
|
||||
});
|
||||
} else {
|
||||
warn(
|
||||
env.logging,
|
||||
'middleware',
|
||||
"Middleware doesn't work for endpoints that return a simple body. The middleware will be disabled for this page."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (response instanceof Response) {
|
||||
attachToResponse(response, context.cookies);
|
||||
|
|
|
@ -628,6 +628,95 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati
|
|||
code: 3030,
|
||||
message: 'The response has already been sent to the browser and cannot be altered.',
|
||||
},
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @description
|
||||
* Thrown when the middleware does not return any data or call the `next` function.
|
||||
*
|
||||
* For example:
|
||||
* ```ts
|
||||
* import {defineMiddleware} from "astro/middleware";
|
||||
* export const onRequest = defineMiddleware((context, _) => {
|
||||
* // doesn't return anything or call `next`
|
||||
* context.locals.someData = false;
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
MiddlewareNoDataOrNextCalled: {
|
||||
title: "The middleware didn't return a response or call `next`",
|
||||
code: 3031,
|
||||
message:
|
||||
'The middleware needs to either return a `Response` object or call the `next` function.',
|
||||
},
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @description
|
||||
* Thrown in development mode when middleware returns something that is not a `Response` object.
|
||||
*
|
||||
* For example:
|
||||
* ```ts
|
||||
* import {defineMiddleware} from "astro/middleware";
|
||||
* export const onRequest = defineMiddleware(() => {
|
||||
* return "string"
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
MiddlewareNotAResponse: {
|
||||
title: 'The middleware returned something that is not a `Response` object',
|
||||
code: 3032,
|
||||
message: 'Any data returned from middleware must be a valid `Response` object.',
|
||||
},
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @description
|
||||
*
|
||||
* Thrown in development mode when `locals` is overwritten with something that is not an object
|
||||
*
|
||||
* For example:
|
||||
* ```ts
|
||||
* import {defineMiddleware} from "astro/middleware";
|
||||
* export const onRequest = defineMiddleware((context, next) => {
|
||||
* context.locals = 1541;
|
||||
* return next();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
LocalsNotAnObject: {
|
||||
title: 'Value assigned to `locals` is not accepted',
|
||||
code: 3033,
|
||||
message:
|
||||
'`locals` can only be assigned to an object. Other values like numbers, strings, etc. are not accepted.',
|
||||
hint: 'If you tried to remove some information from the `locals` object, try to use `delete` or set the property to `undefined`.',
|
||||
},
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @description
|
||||
* Thrown in development mode when a user attempts to store something that is not serializable in `locals`.
|
||||
*
|
||||
* For example:
|
||||
* ```ts
|
||||
* import {defineMiddleware} from "astro/middleware";
|
||||
* export const onRequest = defineMiddleware((context, next) => {
|
||||
* context.locals = {
|
||||
* foo() {
|
||||
* alert("Hello world!")
|
||||
* }
|
||||
* };
|
||||
* return next();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
LocalsNotSerializable: {
|
||||
title: '`Astro.locals` is not serializable',
|
||||
code: 3034,
|
||||
message: (href: string) => {
|
||||
return `The information stored in \`Astro.locals\` for the path "${href}" is not serializable.\nMake sure you store only serializable data.`;
|
||||
},
|
||||
},
|
||||
// No headings here, that way Vite errors are merged with Astro ones in the docs, which makes more sense to users.
|
||||
// Vite Errors - 4xxx
|
||||
/**
|
||||
|
|
99
packages/astro/src/core/middleware/callMiddleware.ts
Normal file
99
packages/astro/src/core/middleware/callMiddleware.ts
Normal file
|
@ -0,0 +1,99 @@
|
|||
import type { APIContext, MiddlewareHandler, MiddlewareNext } from '../../@types/astro';
|
||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||
|
||||
/**
|
||||
* Utility function that is in charge of calling the middleware.
|
||||
*
|
||||
* It accepts a `R` generic, which usually is the `Response` returned.
|
||||
* It is a generic because endpoints can return a different payload.
|
||||
*
|
||||
* When calling a middleware, we provide a `next` function, this function might or
|
||||
* might not be called.
|
||||
*
|
||||
* A middleware, to behave correctly, can:
|
||||
* - return a `Response`;
|
||||
* - call `next`;
|
||||
*
|
||||
* Failing doing so will result an error. A middleware can call `next` and do not return a
|
||||
* response. A middleware can not call `next` and return a new `Response` from scratch (maybe with a redirect).
|
||||
*
|
||||
* ```js
|
||||
* const onRequest = async (context, next) => {
|
||||
* const response = await next(context);
|
||||
* return response;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* const onRequest = async (context, next) => {
|
||||
* context.locals = "foo";
|
||||
* next();
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param onRequest The function called which accepts a `context` and a `resolve` function
|
||||
* @param apiContext The API context
|
||||
* @param responseFunction A callback function that should return a promise with the response
|
||||
*/
|
||||
export async function callMiddleware<R>(
|
||||
onRequest: MiddlewareHandler<R>,
|
||||
apiContext: APIContext,
|
||||
responseFunction: () => Promise<R>
|
||||
): Promise<Response | R> {
|
||||
let resolveResolve: any;
|
||||
new Promise((resolve) => {
|
||||
resolveResolve = resolve;
|
||||
});
|
||||
|
||||
let nextCalled = false;
|
||||
const next: MiddlewareNext<R> = async () => {
|
||||
nextCalled = true;
|
||||
return await responseFunction();
|
||||
};
|
||||
|
||||
let middlewarePromise = onRequest(apiContext, next);
|
||||
|
||||
return await Promise.resolve(middlewarePromise).then(async (value) => {
|
||||
// first we check if `next` was called
|
||||
if (nextCalled) {
|
||||
/**
|
||||
* Then we check if a value is returned. If so, we need to return the value returned by the
|
||||
* middleware.
|
||||
* e.g.
|
||||
* ```js
|
||||
* const response = await next();
|
||||
* const new Response(null, { status: 500, headers: response.headers });
|
||||
* ```
|
||||
*/
|
||||
if (typeof value !== 'undefined') {
|
||||
if (value instanceof Response === false) {
|
||||
throw new AstroError(AstroErrorData.MiddlewareNotAResponse);
|
||||
}
|
||||
return value as R;
|
||||
} else {
|
||||
/**
|
||||
* Here we handle the case where `next` was called and returned nothing.
|
||||
*/
|
||||
const responseResult = await responseFunction();
|
||||
return responseResult;
|
||||
}
|
||||
} else if (typeof value === 'undefined') {
|
||||
/**
|
||||
* There might be cases where `next` isn't called and the middleware **must** return
|
||||
* something.
|
||||
*
|
||||
* If not thing is returned, then we raise an Astro error.
|
||||
*/
|
||||
throw new AstroError(AstroErrorData.MiddlewareNoDataOrNextCalled);
|
||||
} else if (value instanceof Response === false) {
|
||||
throw new AstroError(AstroErrorData.MiddlewareNotAResponse);
|
||||
} else {
|
||||
// Middleware did not call resolve and returned a value
|
||||
return value as R;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isEndpointResult(response: any): boolean {
|
||||
return response && typeof response.body !== 'undefined';
|
||||
}
|
9
packages/astro/src/core/middleware/index.ts
Normal file
9
packages/astro/src/core/middleware/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import type { MiddlewareResponseHandler } from '../../@types/astro';
|
||||
import { sequence } from './sequence.js';
|
||||
|
||||
function defineMiddleware(fn: MiddlewareResponseHandler) {
|
||||
return fn;
|
||||
}
|
||||
|
||||
// NOTE: this export must export only the functions that will be exposed to user-land as officials APIs
|
||||
export { sequence, defineMiddleware };
|
22
packages/astro/src/core/middleware/loadMiddleware.ts
Normal file
22
packages/astro/src/core/middleware/loadMiddleware.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import type { AstroSettings } from '../../@types/astro';
|
||||
import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../constants.js';
|
||||
import type { ModuleLoader } from '../module-loader';
|
||||
|
||||
/**
|
||||
* It accepts a module loader and the astro settings, and it attempts to load the middlewares defined in the configuration.
|
||||
*
|
||||
* If not middlewares were not set, the function returns an empty array.
|
||||
*/
|
||||
export async function loadMiddleware(
|
||||
moduleLoader: ModuleLoader,
|
||||
srcDir: AstroSettings['config']['srcDir']
|
||||
) {
|
||||
// can't use node Node.js builtins
|
||||
let middlewarePath = srcDir.pathname + '/' + MIDDLEWARE_PATH_SEGMENT_NAME;
|
||||
try {
|
||||
const module = await moduleLoader.import(middlewarePath);
|
||||
return module;
|
||||
} catch {
|
||||
return void 0;
|
||||
}
|
||||
}
|
36
packages/astro/src/core/middleware/sequence.ts
Normal file
36
packages/astro/src/core/middleware/sequence.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import type { APIContext, MiddlewareResponseHandler } from '../../@types/astro';
|
||||
import { defineMiddleware } from './index.js';
|
||||
|
||||
// From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js
|
||||
/**
|
||||
*
|
||||
* It accepts one or more middleware handlers and makes sure that they are run in sequence.
|
||||
*/
|
||||
export function sequence(...handlers: MiddlewareResponseHandler[]): MiddlewareResponseHandler {
|
||||
const length = handlers.length;
|
||||
if (!length) {
|
||||
const handler: MiddlewareResponseHandler = defineMiddleware((context, next) => {
|
||||
return next();
|
||||
});
|
||||
return handler;
|
||||
}
|
||||
|
||||
return defineMiddleware((context, next) => {
|
||||
return applyHandle(0, context);
|
||||
|
||||
function applyHandle(i: number, handleContext: APIContext) {
|
||||
const handle = handlers[i];
|
||||
// @ts-expect-error
|
||||
// SAFETY: Usually `next` always returns something in user land, but in `sequence` we are actually
|
||||
// doing a loop over all the `next` functions, and eventually we call the last `next` that returns the `Response`.
|
||||
const result = handle(handleContext, async () => {
|
||||
if (i < length - 1) {
|
||||
return applyHandle(i + 1, handleContext);
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,4 +1,13 @@
|
|||
import type { RouteData, SSRElement, SSRResult } from '../../@types/astro';
|
||||
import type {
|
||||
ComponentInstance,
|
||||
Params,
|
||||
Props,
|
||||
RouteData,
|
||||
SSRElement,
|
||||
SSRResult,
|
||||
} from '../../@types/astro';
|
||||
import { getParamsAndPropsOrThrow } from './core.js';
|
||||
import type { Environment } from './environment';
|
||||
|
||||
/**
|
||||
* The RenderContext represents the parts of rendering that are specific to one request.
|
||||
|
@ -14,22 +23,38 @@ export interface RenderContext {
|
|||
componentMetadata?: SSRResult['componentMetadata'];
|
||||
route?: RouteData;
|
||||
status?: number;
|
||||
params: Params;
|
||||
props: Props;
|
||||
}
|
||||
|
||||
export type CreateRenderContextArgs = Partial<RenderContext> & {
|
||||
origin?: string;
|
||||
request: RenderContext['request'];
|
||||
mod: ComponentInstance;
|
||||
env: Environment;
|
||||
};
|
||||
|
||||
export function createRenderContext(options: CreateRenderContextArgs): RenderContext {
|
||||
export async function createRenderContext(
|
||||
options: CreateRenderContextArgs
|
||||
): Promise<RenderContext> {
|
||||
const request = options.request;
|
||||
const url = new URL(request.url);
|
||||
const origin = options.origin ?? url.origin;
|
||||
const pathname = options.pathname ?? url.pathname;
|
||||
const [params, props] = await getParamsAndPropsOrThrow({
|
||||
mod: options.mod as any,
|
||||
route: options.route,
|
||||
routeCache: options.env.routeCache,
|
||||
pathname: pathname,
|
||||
logging: options.env.logging,
|
||||
ssr: options.env.ssr,
|
||||
});
|
||||
return {
|
||||
...options,
|
||||
origin,
|
||||
pathname,
|
||||
url,
|
||||
params,
|
||||
props,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import type { ComponentInstance, Params, Props, RouteData } from '../../@types/astro';
|
||||
import type { LogOptions } from '../logger/core.js';
|
||||
import type { RenderContext } from './context.js';
|
||||
import type { Environment } from './environment.js';
|
||||
|
||||
import type { APIContext, ComponentInstance, Params, Props, RouteData } from '../../@types/astro';
|
||||
import { renderPage as runtimeRenderPage } from '../../runtime/server/index.js';
|
||||
import { attachToResponse } from '../cookies/index.js';
|
||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||
import type { LogOptions } from '../logger/core.js';
|
||||
import { getParams } from '../routing/params.js';
|
||||
import type { RenderContext } from './context.js';
|
||||
import type { Environment } from './environment.js';
|
||||
import { createResult } from './result.js';
|
||||
import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js';
|
||||
|
||||
|
@ -23,6 +22,26 @@ export const enum GetParamsAndPropsError {
|
|||
NoMatchingStaticPath,
|
||||
}
|
||||
|
||||
/**
|
||||
* It retrieves `Params` and `Props`, or throws an error
|
||||
* if they are not correctly retrieved.
|
||||
*/
|
||||
export async function getParamsAndPropsOrThrow(
|
||||
options: GetParamsAndPropsOptions
|
||||
): Promise<[Params, Props]> {
|
||||
let paramsAndPropsResp = await getParamsAndProps(options);
|
||||
if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.NoMatchingStaticPathFound,
|
||||
message: AstroErrorData.NoMatchingStaticPathFound.message(options.pathname),
|
||||
hint: options.route?.component
|
||||
? AstroErrorData.NoMatchingStaticPathFound.hint([options.route?.component])
|
||||
: '',
|
||||
});
|
||||
}
|
||||
return paramsAndPropsResp;
|
||||
}
|
||||
|
||||
export async function getParamsAndProps(
|
||||
opts: GetParamsAndPropsOptions
|
||||
): Promise<[Params, Props] | GetParamsAndPropsError> {
|
||||
|
@ -84,65 +103,63 @@ export async function getParamsAndProps(
|
|||
return [params, pageProps];
|
||||
}
|
||||
|
||||
export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env: Environment) {
|
||||
const paramsAndPropsRes = await getParamsAndProps({
|
||||
logging: env.logging,
|
||||
mod,
|
||||
route: ctx.route,
|
||||
routeCache: env.routeCache,
|
||||
pathname: ctx.pathname,
|
||||
ssr: env.ssr,
|
||||
});
|
||||
|
||||
if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.NoMatchingStaticPathFound,
|
||||
message: AstroErrorData.NoMatchingStaticPathFound.message(ctx.pathname),
|
||||
hint: ctx.route?.component
|
||||
? AstroErrorData.NoMatchingStaticPathFound.hint([ctx.route?.component])
|
||||
: '',
|
||||
});
|
||||
}
|
||||
const [params, pageProps] = paramsAndPropsRes;
|
||||
export type RenderPage = {
|
||||
mod: ComponentInstance;
|
||||
renderContext: RenderContext;
|
||||
env: Environment;
|
||||
apiContext?: APIContext;
|
||||
};
|
||||
|
||||
export async function renderPage({ mod, renderContext, env, apiContext }: RenderPage) {
|
||||
// Validate the page component before rendering the page
|
||||
const Component = mod.default;
|
||||
if (!Component)
|
||||
throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
|
||||
|
||||
let locals = {};
|
||||
if (apiContext) {
|
||||
if (env.mode === 'development' && !isValueSerializable(apiContext.locals)) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.LocalsNotSerializable,
|
||||
message: AstroErrorData.LocalsNotSerializable.message(renderContext.pathname),
|
||||
});
|
||||
}
|
||||
locals = apiContext.locals;
|
||||
}
|
||||
const result = createResult({
|
||||
adapterName: env.adapterName,
|
||||
links: ctx.links,
|
||||
styles: ctx.styles,
|
||||
links: renderContext.links,
|
||||
styles: renderContext.styles,
|
||||
logging: env.logging,
|
||||
markdown: env.markdown,
|
||||
mode: env.mode,
|
||||
origin: ctx.origin,
|
||||
params,
|
||||
props: pageProps,
|
||||
pathname: ctx.pathname,
|
||||
componentMetadata: ctx.componentMetadata,
|
||||
origin: renderContext.origin,
|
||||
params: renderContext.params,
|
||||
props: renderContext.props,
|
||||
pathname: renderContext.pathname,
|
||||
componentMetadata: renderContext.componentMetadata,
|
||||
resolve: env.resolve,
|
||||
renderers: env.renderers,
|
||||
request: ctx.request,
|
||||
request: renderContext.request,
|
||||
site: env.site,
|
||||
scripts: ctx.scripts,
|
||||
scripts: renderContext.scripts,
|
||||
ssr: env.ssr,
|
||||
status: ctx.status ?? 200,
|
||||
status: renderContext.status ?? 200,
|
||||
locals,
|
||||
});
|
||||
|
||||
// Support `export const components` for `MDX` pages
|
||||
if (typeof (mod as any).components === 'object') {
|
||||
Object.assign(pageProps, { components: (mod as any).components });
|
||||
Object.assign(renderContext.props, { components: (mod as any).components });
|
||||
}
|
||||
|
||||
const response = await runtimeRenderPage(
|
||||
let response = await runtimeRenderPage(
|
||||
result,
|
||||
Component,
|
||||
pageProps,
|
||||
renderContext.props,
|
||||
null,
|
||||
env.streaming,
|
||||
ctx.route
|
||||
renderContext.route
|
||||
);
|
||||
|
||||
// If there is an Astro.cookies instance, attach it to the response so that
|
||||
|
@ -153,3 +170,57 @@ export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env
|
|||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether any value can is serializable.
|
||||
*
|
||||
* A serializable value contains plain values. For example, `Proxy`, `Set`, `Map`, functions, etc.
|
||||
* are not serializable objects.
|
||||
*
|
||||
* @param object
|
||||
*/
|
||||
export function isValueSerializable(value: unknown): boolean {
|
||||
let type = typeof value;
|
||||
let plainObject = true;
|
||||
if (type === 'object' && isPlainObject(value)) {
|
||||
for (const [, nestedValue] of Object.entries(value)) {
|
||||
if (!isValueSerializable(nestedValue)) {
|
||||
plainObject = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
plainObject = false;
|
||||
}
|
||||
let result =
|
||||
value === null ||
|
||||
type === 'string' ||
|
||||
type === 'number' ||
|
||||
type === 'boolean' ||
|
||||
Array.isArray(value) ||
|
||||
plainObject;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* From [redux-toolkit](https://github.com/reduxjs/redux-toolkit/blob/master/packages/toolkit/src/isPlainObject.ts)
|
||||
*
|
||||
* Returns true if the passed value is "plain" object, i.e. an object whose
|
||||
* prototype is the root `Object.prototype`. This includes objects created
|
||||
* using object literals, but not for instance for class instances.
|
||||
*/
|
||||
function isPlainObject(value: unknown): value is object {
|
||||
if (typeof value !== 'object' || value === null) return false;
|
||||
|
||||
let proto = Object.getPrototypeOf(value);
|
||||
if (proto === null) return true;
|
||||
|
||||
let baseProto = proto;
|
||||
while (Object.getPrototypeOf(baseProto) !== null) {
|
||||
baseProto = Object.getPrototypeOf(baseProto);
|
||||
}
|
||||
|
||||
return proto === baseProto;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import { fileURLToPath } from 'url';
|
||||
import type {
|
||||
AstroMiddlewareInstance,
|
||||
AstroSettings,
|
||||
ComponentInstance,
|
||||
MiddlewareResponseHandler,
|
||||
RouteData,
|
||||
SSRElement,
|
||||
SSRLoadedRenderer,
|
||||
} from '../../../@types/astro';
|
||||
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
|
||||
import { createAPIContext } from '../../endpoint/index.js';
|
||||
import { enhanceViteSSRError } from '../../errors/dev/index.js';
|
||||
import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js';
|
||||
import { callMiddleware } from '../../middleware/callMiddleware.js';
|
||||
import type { ModuleLoader } from '../../module-loader/index';
|
||||
import { isPage, resolveIdToUrl, viteID } from '../../util.js';
|
||||
import { createRenderContext, renderPage as coreRenderPage } from '../index.js';
|
||||
|
@ -35,6 +39,10 @@ export interface SSROptions {
|
|||
request: Request;
|
||||
/** optional, in case we need to render something outside of a dev server */
|
||||
route?: RouteData;
|
||||
/**
|
||||
* Optional middlewares
|
||||
*/
|
||||
middleware?: AstroMiddlewareInstance<unknown>;
|
||||
}
|
||||
|
||||
export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance];
|
||||
|
@ -158,8 +166,9 @@ export async function renderPage(options: SSROptions): Promise<Response> {
|
|||
env: options.env,
|
||||
filePath: options.filePath,
|
||||
});
|
||||
const { env } = options;
|
||||
|
||||
const ctx = createRenderContext({
|
||||
const renderContext = await createRenderContext({
|
||||
request: options.request,
|
||||
origin: options.origin,
|
||||
pathname: options.pathname,
|
||||
|
@ -168,7 +177,25 @@ export async function renderPage(options: SSROptions): Promise<Response> {
|
|||
styles,
|
||||
componentMetadata: metadata,
|
||||
route: options.route,
|
||||
mod,
|
||||
env,
|
||||
});
|
||||
if (options.middleware) {
|
||||
if (options.middleware && options.middleware.onRequest) {
|
||||
const apiContext = createAPIContext({
|
||||
request: options.request,
|
||||
params: renderContext.params,
|
||||
props: renderContext.props,
|
||||
adapterName: options.env.adapterName,
|
||||
});
|
||||
|
||||
return await coreRenderPage(mod, ctx, options.env); // NOTE: without "await", errors won’t get caught below
|
||||
const onRequest = options.middleware.onRequest as MiddlewareResponseHandler;
|
||||
const response = await callMiddleware<Response>(onRequest, apiContext, () => {
|
||||
return coreRenderPage({ mod, renderContext, env: options.env, apiContext });
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
return await coreRenderPage({ mod, renderContext, env: options.env }); // NOTE: without "await", errors won’t get caught below
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
export { createRenderContext } from './context.js';
|
||||
export type { RenderContext } from './context.js';
|
||||
export { getParamsAndProps, GetParamsAndPropsError, renderPage } from './core.js';
|
||||
export {
|
||||
getParamsAndProps,
|
||||
GetParamsAndPropsError,
|
||||
getParamsAndPropsOrThrow,
|
||||
renderPage,
|
||||
} from './core.js';
|
||||
export type { Environment } from './environment';
|
||||
export { createBasicEnvironment, createEnvironment } from './environment.js';
|
||||
export { loadRenderer } from './renderer.js';
|
||||
|
|
|
@ -50,6 +50,7 @@ export interface CreateResultArgs {
|
|||
componentMetadata?: SSRResult['componentMetadata'];
|
||||
request: Request;
|
||||
status: number;
|
||||
locals: App.Locals;
|
||||
}
|
||||
|
||||
function getFunctionExpression(slot: any) {
|
||||
|
@ -131,7 +132,7 @@ class Slots {
|
|||
let renderMarkdown: any = null;
|
||||
|
||||
export function createResult(args: CreateResultArgs): SSRResult {
|
||||
const { markdown, params, pathname, renderers, request, resolve } = args;
|
||||
const { markdown, params, pathname, renderers, request, resolve, locals } = args;
|
||||
|
||||
const url = new URL(request.url);
|
||||
const headers = new Headers();
|
||||
|
@ -200,6 +201,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
|||
},
|
||||
params,
|
||||
props,
|
||||
locals,
|
||||
request,
|
||||
url,
|
||||
redirect: args.ssr
|
||||
|
|
|
@ -16,6 +16,7 @@ export interface CreateRequestOptions {
|
|||
}
|
||||
|
||||
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||
|
||||
export function createRequest({
|
||||
url,
|
||||
|
@ -65,5 +66,7 @@ export function createRequest({
|
|||
Reflect.set(request, clientAddressSymbol, clientAddress);
|
||||
}
|
||||
|
||||
Reflect.set(request, clientLocalsSymbol, {});
|
||||
|
||||
return request;
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ export async function runHookConfigSetup({
|
|||
|
||||
let updatedConfig: AstroConfig = { ...settings.config };
|
||||
let updatedSettings: AstroSettings = { ...settings, config: updatedConfig };
|
||||
|
||||
for (const integration of settings.config.integrations) {
|
||||
/**
|
||||
* By making integration hooks optional, Astro can now ignore null or undefined Integrations
|
||||
|
@ -68,7 +69,7 @@ export async function runHookConfigSetup({
|
|||
* ]
|
||||
* ```
|
||||
*/
|
||||
if (integration?.hooks?.['astro:config:setup']) {
|
||||
if (integration.hooks?.['astro:config:setup']) {
|
||||
const hooks: HookParameters<'astro:config:setup'> = {
|
||||
config: updatedConfig,
|
||||
command,
|
||||
|
|
|
@ -19,7 +19,7 @@ function getHandlerFromModule(mod: EndpointHandler, method: string) {
|
|||
|
||||
/** Renders an endpoint request to completion, returning the body. */
|
||||
export async function renderEndpoint(mod: EndpointHandler, context: APIContext, ssr: boolean) {
|
||||
const { request, params } = context;
|
||||
const { request, params, locals } = context;
|
||||
const chosenMethod = request.method?.toLowerCase();
|
||||
const handler = getHandlerFromModule(mod, chosenMethod);
|
||||
if (!ssr && ssr === false && chosenMethod && chosenMethod !== 'get') {
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import type http from 'http';
|
||||
import mime from 'mime';
|
||||
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 { call as callEndpoint } from '../core/endpoint/dev/index.js';
|
||||
import { throwIfRedirectNotAllowed } from '../core/endpoint/index.js';
|
||||
import { AstroErrorData } from '../core/errors/index.js';
|
||||
import { warn } from '../core/logger/core.js';
|
||||
import { loadMiddleware } from '../core/middleware/loadMiddleware.js';
|
||||
import type {
|
||||
ComponentPreload,
|
||||
DevelopmentEnvironment,
|
||||
SSROptions,
|
||||
} from '../core/render/dev/index';
|
||||
import { preload, renderPage } from '../core/render/dev/index.js';
|
||||
import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/index.js';
|
||||
import { createRequest } from '../core/request.js';
|
||||
|
@ -169,7 +169,12 @@ export async function handleRoute(
|
|||
request,
|
||||
route,
|
||||
};
|
||||
|
||||
if (env.settings.config.experimental.middleware) {
|
||||
const middleware = await loadMiddleware(env.loader, env.settings.config.srcDir);
|
||||
if (middleware) {
|
||||
options.middleware = middleware;
|
||||
}
|
||||
}
|
||||
// Route successfully matched! Render it.
|
||||
if (route.type === 'endpoint') {
|
||||
const result = await callEndpoint(options, logging);
|
||||
|
|
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);
|
||||
}
|
||||
|
||||
Reflect.set(request, Symbol.for('astro.locals'), {});
|
||||
${provideAddress ? `request[Symbol.for('astro.clientAddress')] = '0.0.0.0';` : ''}
|
||||
return super.render(request, routeData);
|
||||
}
|
||||
|
@ -51,6 +52,7 @@ export default function ({ provideAddress = true, extendAdapter } = { provideAdd
|
|||
return {
|
||||
manifest,
|
||||
createApp: (streaming) => new MyApp(manifest, streaming)
|
||||
|
||||
};
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -231,7 +231,7 @@ export async function loadFixture(inlineConfig) {
|
|||
},
|
||||
loadTestAdapterApp: async (streaming) => {
|
||||
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);
|
||||
app.manifest = manifest;
|
||||
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/'),
|
||||
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 $ = 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/'),
|
||||
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 $ = 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/'),
|
||||
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 $ = cheerio.load(html);
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { expect } from 'chai';
|
||||
|
||||
import {
|
||||
createComponent,
|
||||
render,
|
||||
|
@ -46,8 +45,18 @@ describe('core/render', () => {
|
|||
});
|
||||
});
|
||||
|
||||
const ctx = createRenderContext({ request: new Request('http://example.com/') });
|
||||
const response = await renderPage(createAstroModule(Page), ctx, env);
|
||||
const mod = createAstroModule(Page);
|
||||
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);
|
||||
|
||||
|
@ -85,8 +94,17 @@ describe('core/render', () => {
|
|||
});
|
||||
});
|
||||
|
||||
const ctx = createRenderContext({ request: new Request('http://example.com/') });
|
||||
const response = await renderPage(createAstroModule(Page), ctx, env);
|
||||
const mod = createAstroModule(Page);
|
||||
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);
|
||||
|
||||
|
@ -105,8 +123,18 @@ describe('core/render', () => {
|
|||
return render`<div>${renderComponent(result, 'Component', Component, {})}</div>`;
|
||||
});
|
||||
|
||||
const ctx = createRenderContext({ request: new Request('http://example.com/') });
|
||||
const response = await renderPage(createAstroModule(Page), ctx, env);
|
||||
const mod = createAstroModule(Page);
|
||||
const ctx = await createRenderContext({
|
||||
request: new Request('http://example.com/'),
|
||||
env,
|
||||
mod,
|
||||
});
|
||||
|
||||
const response = await renderPage({
|
||||
mod,
|
||||
renderContext: ctx,
|
||||
env,
|
||||
});
|
||||
|
||||
try {
|
||||
await response.text();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { polyfill } from '@astrojs/webapi';
|
||||
import type { SSRManifest } from 'astro';
|
||||
import { NodeApp } from 'astro/app/node';
|
||||
import middleware from './middleware.js';
|
||||
import middleware from './nodeMiddleware.js';
|
||||
import startServer from './standalone.js';
|
||||
import type { Options } from './types';
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import https from 'https';
|
|||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createServer } from './http-server.js';
|
||||
import middleware from './middleware.js';
|
||||
import middleware from './nodeMiddleware.js';
|
||||
import type { Options } from './types';
|
||||
|
||||
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