Implements redirects, headers for SSR (#2798)
* Implements redirects, headers for SSR * Move away from an explicit Request * Properly handle endpoint routes in the build * chore(lint): ESLint fix * Update based on review comments Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
8f13b3d406
commit
4c25a1c2ea
33 changed files with 698 additions and 109 deletions
5
.changeset/fuzzy-lies-nail.md
Normal file
5
.changeset/fuzzy-lies-nail.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Implement APIs for headers for SSR flag
|
|
@ -4,8 +4,14 @@ export default defineConfig({
|
||||||
renderers: ['@astrojs/renderer-svelte'],
|
renderers: ['@astrojs/renderer-svelte'],
|
||||||
vite: {
|
vite: {
|
||||||
server: {
|
server: {
|
||||||
|
cors: {
|
||||||
|
credentials: true
|
||||||
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': 'http://localhost:8085',
|
'/api': {
|
||||||
|
target: 'http://127.0.0.1:8085',
|
||||||
|
changeOrigin: true,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev-api": "node server/dev-api.mjs",
|
"dev-api": "node server/dev-api.mjs",
|
||||||
"dev": "npm run dev-api & astro dev --experimental-ssr",
|
"dev-server": "astro dev --experimental-ssr",
|
||||||
|
"dev": "concurrently \"npm run dev-api\" \"astro dev --experimental-ssr\"",
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
"build": "echo 'Run pnpm run build-ssr instead'",
|
"build": "echo 'Run pnpm run build-ssr instead'",
|
||||||
"build-ssr": "node build.mjs",
|
"build-ssr": "node build.mjs",
|
||||||
|
@ -13,6 +14,8 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/renderer-svelte": "^0.5.2",
|
"@astrojs/renderer-svelte": "^0.5.2",
|
||||||
"astro": "^0.24.3",
|
"astro": "^0.24.3",
|
||||||
|
"concurrently": "^7.0.0",
|
||||||
|
"lightcookie": "^1.0.25",
|
||||||
"unocss": "^0.15.6",
|
"unocss": "^0.15.6",
|
||||||
"vite-imagetools": "^4.0.3"
|
"vite-imagetools": "^4.0.3"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import lightcookie from 'lightcookie';
|
||||||
|
|
||||||
const dbJSON = fs.readFileSync(new URL('./db.json', import.meta.url));
|
const dbJSON = fs.readFileSync(new URL('./db.json', import.meta.url));
|
||||||
const db = JSON.parse(dbJSON);
|
const db = JSON.parse(dbJSON);
|
||||||
const products = db.products;
|
const products = db.products;
|
||||||
const productMap = new Map(products.map((product) => [product.id, product]));
|
const productMap = new Map(products.map((product) => [product.id, product]));
|
||||||
|
|
||||||
|
// Normally this would be in a database.
|
||||||
|
const userCartItems = new Map();
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
match: /\/api\/products\/([0-9])+/,
|
match: /\/api\/products\/([0-9])+/,
|
||||||
|
@ -32,6 +37,53 @@ const routes = [
|
||||||
res.end(JSON.stringify(products));
|
res.end(JSON.stringify(products));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
match: /\/api\/cart/,
|
||||||
|
async handle(req, res) {
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
let cookie = req.headers.cookie;
|
||||||
|
let userId = cookie ? lightcookie.parse(cookie)['user-id'] : '1'; // default for testing
|
||||||
|
if(!userId || !userCartItems.has(userId)) {
|
||||||
|
res.end(JSON.stringify({ items: [] }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let items = userCartItems.get(userId);
|
||||||
|
let array = Array.from(items.values());
|
||||||
|
res.end(JSON.stringify({ items: array }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /\/api\/add-to-cart/,
|
||||||
|
async handle(req, res) {
|
||||||
|
let body = '';
|
||||||
|
req.on('data', chunk => body += chunk);
|
||||||
|
return new Promise(resolve => {
|
||||||
|
req.on('end', () => {
|
||||||
|
let cookie = req.headers.cookie;
|
||||||
|
let userId = lightcookie.parse(cookie)['user-id'];
|
||||||
|
let msg = JSON.parse(body);
|
||||||
|
|
||||||
|
if(!userCartItems.has(userId)) {
|
||||||
|
userCartItems.set(userId, new Map());
|
||||||
|
}
|
||||||
|
|
||||||
|
let cart = userCartItems.get(userId);
|
||||||
|
if(cart.has(msg.id)) {
|
||||||
|
cart.get(msg.id).count++;
|
||||||
|
} else {
|
||||||
|
cart.set(msg.id, { id: msg.id, name: msg.name, count: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
});
|
||||||
|
res.end(JSON.stringify({ ok: true }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function apiHandler(req, res) {
|
export async function apiHandler(req, res) {
|
||||||
|
|
|
@ -15,9 +15,10 @@ async function handle(req, res) {
|
||||||
const route = app.match(req);
|
const route = app.match(req);
|
||||||
|
|
||||||
if (route) {
|
if (route) {
|
||||||
const html = await app.render(req, route);
|
/** @type {Response} */
|
||||||
|
const response = await app.render(req, route);
|
||||||
res.writeHead(200, {
|
const html = await response.text();
|
||||||
|
res.writeHead(response.status, {
|
||||||
'Content-Type': 'text/html; charset=utf-8',
|
'Content-Type': 'text/html; charset=utf-8',
|
||||||
'Content-Length': Buffer.byteLength(html, 'utf-8'),
|
'Content-Length': Buffer.byteLength(html, 'utf-8'),
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,12 +5,25 @@ interface Product {
|
||||||
image: string;
|
image: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
//let origin: string;
|
interface User {
|
||||||
const { mode } = import.meta.env;
|
id: number;
|
||||||
const origin = mode === 'develepment' ? `http://localhost:3000` : `http://localhost:8085`;
|
}
|
||||||
|
|
||||||
|
interface Cart {
|
||||||
|
items: Array<{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { MODE } = import.meta.env;
|
||||||
|
const origin = MODE === 'development' ? `http://127.0.0.1:3000` : `http://127.0.0.1:8085`;
|
||||||
|
|
||||||
async function get<T>(endpoint: string, cb: (response: Response) => Promise<T>): Promise<T> {
|
async function get<T>(endpoint: string, cb: (response: Response) => Promise<T>): Promise<T> {
|
||||||
const response = await fetch(`${origin}${endpoint}`);
|
const response = await fetch(`${origin}${endpoint}`, {
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// TODO make this better...
|
// TODO make this better...
|
||||||
return null;
|
return null;
|
||||||
|
@ -31,3 +44,33 @@ export async function getProduct(id: number): Promise<Product> {
|
||||||
return product;
|
return product;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUser(): Promise<User> {
|
||||||
|
return get<User>(`/api/user`, async response => {
|
||||||
|
const user: User = await response.json();
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCart(): Promise<Cart> {
|
||||||
|
return get<Cart>(`/api/cart`, async response => {
|
||||||
|
const cart: Cart = await response.json();
|
||||||
|
return cart;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addToUserCart(id: number | string, name: string): Promise<void> {
|
||||||
|
await fetch(`${origin}/api/add-to-cart`, {
|
||||||
|
credentials: 'same-origin',
|
||||||
|
method: 'POST',
|
||||||
|
mode: 'no-cors',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache': 'no-cache'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { addToUserCart } from '../api';
|
||||||
export let id = 0;
|
export let id = 0;
|
||||||
|
export let name = '';
|
||||||
|
|
||||||
function addToCart() {
|
function notifyCartItem(id) {
|
||||||
window.dispatchEvent(new CustomEvent('add-to-cart', {
|
window.dispatchEvent(new CustomEvent('add-to-cart', {
|
||||||
detail: id
|
detail: id
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addToCart() {
|
||||||
|
await addToUserCart(id, name);
|
||||||
|
notifyCartItem(id);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
button {
|
button {
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
.cart {
|
.cart {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
.cart :first-child {
|
.cart :first-child {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
@ -26,7 +28,7 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<svelte:window on:add-to-cart={onAddToCart}/>
|
<svelte:window on:add-to-cart={onAddToCart}/>
|
||||||
<div class="cart">
|
<a href="/cart" class="cart">
|
||||||
<span class="material-icons cart-icon">shopping_cart</span>
|
<span class="material-icons cart-icon">shopping_cart</span>
|
||||||
<span class="count">{count}</span>
|
<span class="count">{count}</span>
|
||||||
</div>
|
</a>
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
---
|
---
|
||||||
import TextDecorationSkip from './TextDecorationSkip.astro';
|
import TextDecorationSkip from './TextDecorationSkip.astro';
|
||||||
import Cart from './Cart.svelte';
|
import Cart from './Cart.svelte';
|
||||||
|
import { getCart } from '../api';
|
||||||
|
|
||||||
|
const cart = await getCart();
|
||||||
|
const cartCount = cart.items.reduce((sum, item) => sum + item.count, 0);
|
||||||
---
|
---
|
||||||
<style>
|
<style>
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap');
|
||||||
|
@ -21,11 +25,25 @@ import Cart from './Cart.svelte';
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.right-pane {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-icons {
|
||||||
|
font-size: 36px;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
<header>
|
<header>
|
||||||
<h1><a href="/"><TextDecorationSkip text="Online Store" /></a></h1>
|
<h1><a href="/"><TextDecorationSkip text="Online Store" /></a></h1>
|
||||||
<div class="right-pane">
|
<div class="right-pane">
|
||||||
<Cart client:idle />
|
<a href="/login">
|
||||||
|
<span class="material-icons">
|
||||||
|
login
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<Cart client:idle count={cartCount} />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
8
examples/ssr/src/models/user.ts
Normal file
8
examples/ssr/src/models/user.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import lightcookie from 'lightcookie';
|
||||||
|
|
||||||
|
|
||||||
|
export function isLoggedIn(request: Request): boolean {
|
||||||
|
const cookie = request.headers.get('cookie');
|
||||||
|
const parsed = lightcookie.parse(cookie);
|
||||||
|
return 'user-id' in parsed;
|
||||||
|
}
|
47
examples/ssr/src/pages/cart.astro
Normal file
47
examples/ssr/src/pages/cart.astro
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
---
|
||||||
|
import Header from '../components/Header.astro';
|
||||||
|
import Container from '../components/Container.astro';
|
||||||
|
import { getCart } from '../api';
|
||||||
|
import { isLoggedIn } from '../models/user';
|
||||||
|
|
||||||
|
if(!isLoggedIn(Astro.request)) {
|
||||||
|
return Astro.redirect('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// They must be logged in.
|
||||||
|
|
||||||
|
const user = { name: 'test'}; // getUser?
|
||||||
|
const cart = await getCart();
|
||||||
|
---
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Cart | Online Store</title>
|
||||||
|
<style>
|
||||||
|
h1 {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<Container tag="main">
|
||||||
|
<h1>Cart</h1>
|
||||||
|
<p>Hi { user.name }! Here are your cart items:</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Item</th>
|
||||||
|
<th>Count</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{cart.items.map(item => <tr>
|
||||||
|
<td>{item.name}</td>
|
||||||
|
<td>{item.count}</td>
|
||||||
|
</tr>)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Container>
|
||||||
|
</body>
|
||||||
|
</html>
|
30
examples/ssr/src/pages/login.astro
Normal file
30
examples/ssr/src/pages/login.astro
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
import Header from '../components/Header.astro';
|
||||||
|
import Container from '../components/Container.astro';
|
||||||
|
---
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Online Store</title>
|
||||||
|
<style>
|
||||||
|
h1 {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<Container tag="main">
|
||||||
|
<h1>Login</h1>
|
||||||
|
<form action="/login.form" method="POST">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input type="text" name="name">
|
||||||
|
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" name="password">
|
||||||
|
|
||||||
|
<input type="submit" value="Submit">
|
||||||
|
</form>
|
||||||
|
</Container>
|
||||||
|
</body>
|
||||||
|
</html>
|
10
examples/ssr/src/pages/login.form.js
Normal file
10
examples/ssr/src/pages/login.form.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
export function post(params, request) {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 301,
|
||||||
|
headers: {
|
||||||
|
'Location': '/',
|
||||||
|
'Set-Cookie': 'user-id=1; Path=/; Max-Age=2592000'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -45,7 +45,7 @@ const product = await getProduct(id);
|
||||||
<figure>
|
<figure>
|
||||||
<img src={product.image} />
|
<img src={product.image} />
|
||||||
<figcaption>
|
<figcaption>
|
||||||
<AddToCart id={id} client:idle />
|
<AddToCart client:idle id={id} name={product.name} />
|
||||||
<p>Description here...</p>
|
<p>Description here...</p>
|
||||||
</figcaption>
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
8
examples/ssr/tsconfig.json
Normal file
8
examples/ssr/tsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ES2015", "DOM"],
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"types": ["astro/env"]
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import type * as babel from '@babel/core';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
import type { AstroConfigSchema } from '../core/config';
|
import type { AstroConfigSchema } from '../core/config';
|
||||||
import type { AstroComponentFactory, Metadata } from '../runtime/server';
|
import type { AstroComponentFactory, Metadata } from '../runtime/server';
|
||||||
|
import type { AstroRequest } from '../core/render/request';
|
||||||
import type * as vite from 'vite';
|
import type * as vite from 'vite';
|
||||||
|
|
||||||
export interface AstroBuiltinProps {
|
export interface AstroBuiltinProps {
|
||||||
|
@ -43,14 +44,7 @@ export interface AstroGlobal extends AstroGlobalPartial {
|
||||||
/** set props for this astro component (along with default values) */
|
/** set props for this astro component (along with default values) */
|
||||||
props: Record<string, number | string | any>;
|
props: Record<string, number | string | any>;
|
||||||
/** get information about this page */
|
/** get information about this page */
|
||||||
request: {
|
request: AstroRequest;
|
||||||
/** get the current page URL */
|
|
||||||
url: URL;
|
|
||||||
/** get the current canonical URL */
|
|
||||||
canonicalURL: URL;
|
|
||||||
/** get page params (dynamic pages only) */
|
|
||||||
params: Params;
|
|
||||||
};
|
|
||||||
/** see if slots are used */
|
/** see if slots are used */
|
||||||
slots: Record<string, true | undefined> & { has(slotName: string): boolean; render(slotName: string): Promise<string> };
|
slots: Record<string, true | undefined> & { has(slotName: string): boolean; render(slotName: string): Promise<string> };
|
||||||
}
|
}
|
||||||
|
@ -563,7 +557,7 @@ export interface EndpointOutput<Output extends Body = Body> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EndpointHandler {
|
export interface EndpointHandler {
|
||||||
[method: string]: (params: any) => EndpointOutput;
|
[method: string]: (params: any, request: AstroRequest) => EndpointOutput | Response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -27,14 +27,18 @@ export class App {
|
||||||
this.#routeCache = new RouteCache(defaultLogOptions);
|
this.#routeCache = new RouteCache(defaultLogOptions);
|
||||||
this.#renderersPromise = this.#loadRenderers();
|
this.#renderersPromise = this.#loadRenderers();
|
||||||
}
|
}
|
||||||
match({ pathname }: URL): RouteData | undefined {
|
match(request: Request): RouteData | undefined {
|
||||||
return matchRoute(pathname, this.#manifestData);
|
const url = new URL(request.url);
|
||||||
|
return matchRoute(url.pathname, this.#manifestData);
|
||||||
}
|
}
|
||||||
async render(url: URL, routeData?: RouteData): Promise<string> {
|
async render(request: Request, routeData?: RouteData): Promise<Response> {
|
||||||
if (!routeData) {
|
if (!routeData) {
|
||||||
routeData = this.match(url);
|
routeData = this.match(request);
|
||||||
if (!routeData) {
|
if (!routeData) {
|
||||||
return 'Not found';
|
return new Response(null, {
|
||||||
|
status: 404,
|
||||||
|
statusText: 'Not found'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,10 +46,11 @@ export class App {
|
||||||
const info = this.#routeDataToRouteInfo.get(routeData!)!;
|
const info = this.#routeDataToRouteInfo.get(routeData!)!;
|
||||||
const [mod, renderers] = await Promise.all([this.#loadModule(info.file), this.#renderersPromise]);
|
const [mod, renderers] = await Promise.all([this.#loadModule(info.file), this.#renderersPromise]);
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
const links = createLinkStylesheetElementSet(info.links, manifest.site);
|
const links = createLinkStylesheetElementSet(info.links, manifest.site);
|
||||||
const scripts = createModuleScriptElementWithSrcSet(info.scripts, manifest.site);
|
const scripts = createModuleScriptElementWithSrcSet(info.scripts, manifest.site);
|
||||||
|
|
||||||
return render({
|
const result = await render({
|
||||||
legacyBuild: false,
|
legacyBuild: false,
|
||||||
links,
|
links,
|
||||||
logging: defaultLogOptions,
|
logging: defaultLogOptions,
|
||||||
|
@ -65,6 +70,18 @@ export class App {
|
||||||
route: routeData,
|
route: routeData,
|
||||||
routeCache: this.#routeCache,
|
routeCache: this.#routeCache,
|
||||||
site: this.#manifest.site,
|
site: this.#manifest.site,
|
||||||
|
ssr: true,
|
||||||
|
method: info.routeData.type === 'endpoint' ? '' : 'GET',
|
||||||
|
headers: request.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if(result.type === 'response') {
|
||||||
|
return result.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = result.html;
|
||||||
|
return new Response(html, {
|
||||||
|
status: 200
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async #loadRenderers(): Promise<Renderer[]> {
|
async #loadRenderers(): Promise<Renderer[]> {
|
||||||
|
|
|
@ -1,20 +1,27 @@
|
||||||
import type { SSRManifest, SerializedSSRManifest } from './types';
|
import type { SSRManifest, SerializedSSRManifest } from './types';
|
||||||
|
import type { IncomingHttpHeaders } from 'http';
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { App } from './index.js';
|
import { App } from './index.js';
|
||||||
import { deserializeManifest } from './common.js';
|
import { deserializeManifest } from './common.js';
|
||||||
import { IncomingMessage } from 'http';
|
import { IncomingMessage } from 'http';
|
||||||
|
|
||||||
function createURLFromRequest(req: IncomingMessage): URL {
|
function createRequestFromNodeRequest(req: IncomingMessage): Request {
|
||||||
return new URL(`http://${req.headers.host}${req.url}`);
|
let url = `http://${req.headers.host}${req.url}`;
|
||||||
|
const entries = Object.entries(req.headers as Record<string, any>);
|
||||||
|
let request = new Request(url, {
|
||||||
|
method: req.method || 'GET',
|
||||||
|
headers: new Headers(entries)
|
||||||
|
});
|
||||||
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
class NodeApp extends App {
|
class NodeApp extends App {
|
||||||
match(req: IncomingMessage | URL) {
|
match(req: IncomingMessage | Request) {
|
||||||
return super.match(req instanceof URL ? req : createURLFromRequest(req));
|
return super.match(req instanceof Request ? req : createRequestFromNodeRequest(req));
|
||||||
}
|
}
|
||||||
render(req: IncomingMessage | URL) {
|
render(req: IncomingMessage | Request) {
|
||||||
return super.render(req instanceof URL ? req : createURLFromRequest(req));
|
return super.render(req instanceof Request ? req : createRequestFromNodeRequest(req));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { LogOptions } from '../logger';
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import * as colors from 'kleur/colors';
|
import * as colors from 'kleur/colors';
|
||||||
import { polyfill } from '@astrojs/webapi';
|
import { apply as applyPolyfill } from '../polyfill.js';
|
||||||
import { performance } from 'perf_hooks';
|
import { performance } from 'perf_hooks';
|
||||||
import * as vite from 'vite';
|
import * as vite from 'vite';
|
||||||
import { createVite, ViteConfigWithSSR } from '../create-vite.js';
|
import { createVite, ViteConfigWithSSR } from '../create-vite.js';
|
||||||
|
@ -22,11 +22,6 @@ export interface BuildOptions {
|
||||||
|
|
||||||
/** `astro build` */
|
/** `astro build` */
|
||||||
export default async function build(config: AstroConfig, options: BuildOptions = { logging: defaultLogOptions }): Promise<void> {
|
export default async function build(config: AstroConfig, options: BuildOptions = { logging: defaultLogOptions }): Promise<void> {
|
||||||
// polyfill WebAPIs to globalThis for Node v12, Node v14, and Node v16
|
|
||||||
polyfill(globalThis, {
|
|
||||||
exclude: 'window document',
|
|
||||||
});
|
|
||||||
|
|
||||||
const builder = new AstroBuilder(config, options);
|
const builder = new AstroBuilder(config, options);
|
||||||
await builder.build();
|
await builder.build();
|
||||||
}
|
}
|
||||||
|
@ -42,6 +37,8 @@ class AstroBuilder {
|
||||||
private viteConfig?: ViteConfigWithSSR;
|
private viteConfig?: ViteConfigWithSSR;
|
||||||
|
|
||||||
constructor(config: AstroConfig, options: BuildOptions) {
|
constructor(config: AstroConfig, options: BuildOptions) {
|
||||||
|
applyPolyfill();
|
||||||
|
|
||||||
if (!config.buildOptions.site && config.buildOptions.sitemap !== false) {
|
if (!config.buildOptions.site && config.buildOptions.sitemap !== false) {
|
||||||
warn(options.logging, 'config', `Set "buildOptions.site" to generate correct canonical URLs and sitemap`);
|
warn(options.logging, 'config', `Set "buildOptions.site" to generate correct canonical URLs and sitemap`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import type { OutputChunk, OutputAsset, RollupOutput } from 'rollup';
|
import type { OutputChunk, OutputAsset, RollupOutput } from 'rollup';
|
||||||
import type { Plugin as VitePlugin, UserConfig, Manifest as ViteManifest } from 'vite';
|
import type { Plugin as VitePlugin, UserConfig, Manifest as ViteManifest } from 'vite';
|
||||||
import type { AstroConfig, ComponentInstance, ManifestData, Renderer, RouteType } from '../../@types/astro';
|
import type { AstroConfig, ComponentInstance, EndpointHandler, ManifestData, Renderer, RouteType } from '../../@types/astro';
|
||||||
import type { AllPagesData } from './types';
|
import type { AllPagesData } from './types';
|
||||||
import type { LogOptions } from '../logger';
|
import type { LogOptions } from '../logger';
|
||||||
import type { ViteConfigWithSSR } from '../create-vite';
|
import type { ViteConfigWithSSR } from '../create-vite';
|
||||||
import type { PageBuildData } from './types';
|
import type { PageBuildData } from './types';
|
||||||
import type { BuildInternals } from '../../core/build/internal.js';
|
import type { BuildInternals } from '../../core/build/internal.js';
|
||||||
|
import type { RenderOptions } from '../../core/render/core';
|
||||||
import type { SerializedSSRManifest, SerializedRouteInfo } from '../app/types';
|
import type { SerializedSSRManifest, SerializedRouteInfo } from '../app/types';
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
@ -20,9 +21,11 @@ import { createBuildInternals } from '../../core/build/internal.js';
|
||||||
import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js';
|
import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js';
|
||||||
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
|
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
|
||||||
import { RouteCache } from '../render/route-cache.js';
|
import { RouteCache } from '../render/route-cache.js';
|
||||||
|
import { call as callEndpoint } from '../endpoint/index.js';
|
||||||
import { serializeRouteData } from '../routing/index.js';
|
import { serializeRouteData } from '../routing/index.js';
|
||||||
import { render } from '../render/core.js';
|
import { render } from '../render/core.js';
|
||||||
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
|
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
|
||||||
|
import { createRequest } from '../render/request.js';
|
||||||
|
|
||||||
export interface StaticBuildOptions {
|
export interface StaticBuildOptions {
|
||||||
allPages: AllPagesData;
|
allPages: AllPagesData;
|
||||||
|
@ -134,6 +137,8 @@ export async function staticBuild(opts: StaticBuildOptions) {
|
||||||
|
|
||||||
const topLevelImports = new Set([
|
const topLevelImports = new Set([
|
||||||
// Any component that gets hydrated
|
// Any component that gets hydrated
|
||||||
|
// 'components/Counter.jsx'
|
||||||
|
// { 'components/Counter.jsx': 'counter.hash.js' }
|
||||||
...metadata.hydratedComponentPaths(),
|
...metadata.hydratedComponentPaths(),
|
||||||
// Client-only components
|
// Client-only components
|
||||||
...metadata.clientOnlyComponentPaths(),
|
...metadata.clientOnlyComponentPaths(),
|
||||||
|
@ -373,7 +378,7 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G
|
||||||
const scripts = createModuleScriptElementWithSrcSet(hoistedId ? [hoistedId] : [], site);
|
const scripts = createModuleScriptElementWithSrcSet(hoistedId ? [hoistedId] : [], site);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const html = await render({
|
const options: RenderOptions = {
|
||||||
legacyBuild: false,
|
legacyBuild: false,
|
||||||
links,
|
links,
|
||||||
logging,
|
logging,
|
||||||
|
@ -392,15 +397,37 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G
|
||||||
const fullyRelativePath = relPath[0] === '.' ? relPath : './' + relPath;
|
const fullyRelativePath = relPath[0] === '.' ? relPath : './' + relPath;
|
||||||
return fullyRelativePath;
|
return fullyRelativePath;
|
||||||
},
|
},
|
||||||
|
method: 'GET',
|
||||||
|
headers: new Headers(),
|
||||||
route: pageData.route,
|
route: pageData.route,
|
||||||
routeCache,
|
routeCache,
|
||||||
site: astroConfig.buildOptions.site,
|
site: astroConfig.buildOptions.site,
|
||||||
});
|
ssr: opts.astroConfig.buildOptions.experimentalSsr
|
||||||
|
}
|
||||||
|
|
||||||
|
let body: string;
|
||||||
|
if(pageData.route.type === 'endpoint') {
|
||||||
|
|
||||||
|
const result = await callEndpoint(mod as unknown as EndpointHandler, options);
|
||||||
|
|
||||||
|
if(result.type === 'response') {
|
||||||
|
throw new Error(`Returning a Response from an endpoint is not supported in SSG mode.`)
|
||||||
|
}
|
||||||
|
body = result.body;
|
||||||
|
} else {
|
||||||
|
const result = await render(options);
|
||||||
|
|
||||||
|
// If there's a redirect or something, just do nothing.
|
||||||
|
if(result.type !== 'html') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
body = result.html;
|
||||||
|
}
|
||||||
|
|
||||||
const outFolder = getOutFolder(astroConfig, pathname, pageData.route.type);
|
const outFolder = getOutFolder(astroConfig, pathname, pageData.route.type);
|
||||||
const outFile = getOutFile(astroConfig, outFolder, pathname, pageData.route.type);
|
const outFile = getOutFile(astroConfig, outFolder, pathname, pageData.route.type);
|
||||||
await fs.promises.mkdir(outFolder, { recursive: true });
|
await fs.promises.mkdir(outFolder, { recursive: true });
|
||||||
await fs.promises.writeFile(outFile, html, 'utf-8');
|
await fs.promises.writeFile(outFile, body, 'utf-8');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error(opts.logging, 'build', `Error rendering:`, err);
|
error(opts.logging, 'build', `Error rendering:`, err);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { polyfill } from '@astrojs/webapi';
|
|
||||||
import type { AddressInfo } from 'net';
|
|
||||||
import { performance } from 'perf_hooks';
|
|
||||||
import type { AstroConfig } from '../../@types/astro';
|
import type { AstroConfig } from '../../@types/astro';
|
||||||
|
import type { AddressInfo } from 'net';
|
||||||
|
|
||||||
|
import { performance } from 'perf_hooks';
|
||||||
|
import { apply as applyPolyfill } from '../polyfill.js';
|
||||||
import { createVite } from '../create-vite.js';
|
import { createVite } from '../create-vite.js';
|
||||||
import { defaultLogOptions, info, warn, LogOptions } from '../logger.js';
|
import { defaultLogOptions, info, warn, LogOptions } from '../logger.js';
|
||||||
import * as vite from 'vite';
|
import * as vite from 'vite';
|
||||||
|
@ -20,10 +21,7 @@ export interface DevServer {
|
||||||
/** `astro dev` */
|
/** `astro dev` */
|
||||||
export default async function dev(config: AstroConfig, options: DevOptions = { logging: defaultLogOptions }): Promise<DevServer> {
|
export default async function dev(config: AstroConfig, options: DevOptions = { logging: defaultLogOptions }): Promise<DevServer> {
|
||||||
const devStart = performance.now();
|
const devStart = performance.now();
|
||||||
// polyfill WebAPIs for Node.js runtime
|
applyPolyfill();
|
||||||
polyfill(globalThis, {
|
|
||||||
exclude: 'window document',
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: remove call once --hostname is baselined
|
// TODO: remove call once --hostname is baselined
|
||||||
const host = getResolvedHostForVite(config);
|
const host = getResolvedHostForVite(config);
|
||||||
|
|
23
packages/astro/src/core/endpoint/dev/index.ts
Normal file
23
packages/astro/src/core/endpoint/dev/index.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import type { EndpointHandler } from '../../../@types/astro';
|
||||||
|
import type { SSROptions } from '../../render/dev';
|
||||||
|
|
||||||
|
import { preload } from '../../render/dev/index.js';
|
||||||
|
import { errorHandler } from '../../render/dev/error.js';
|
||||||
|
import { call as callEndpoint } from '../index.js';
|
||||||
|
import { getParamsAndProps, GetParamsAndPropsError } from '../../render/core.js';
|
||||||
|
import { createRequest } from '../../render/request.js';
|
||||||
|
|
||||||
|
|
||||||
|
export async function call(ssrOpts: SSROptions) {
|
||||||
|
try {
|
||||||
|
const [, mod] = await preload(ssrOpts);
|
||||||
|
return await callEndpoint(mod as unknown as EndpointHandler, {
|
||||||
|
...ssrOpts,
|
||||||
|
ssr: ssrOpts.astroConfig.buildOptions.experimentalSsr
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e: unknown) {
|
||||||
|
await errorHandler(e, { viteServer: ssrOpts.viteServer, filePath: ssrOpts.filePath });
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
51
packages/astro/src/core/endpoint/index.ts
Normal file
51
packages/astro/src/core/endpoint/index.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import type { EndpointHandler } from '../../@types/astro';
|
||||||
|
import type { RenderOptions } from '../render/core';
|
||||||
|
import { renderEndpoint } from '../../runtime/server/index.js';
|
||||||
|
import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js';
|
||||||
|
import { createRequest } from '../render/request.js';
|
||||||
|
|
||||||
|
export type EndpointOptions = Pick<RenderOptions,
|
||||||
|
'logging' |
|
||||||
|
'headers' |
|
||||||
|
'method' |
|
||||||
|
'origin' |
|
||||||
|
'route' |
|
||||||
|
'routeCache' |
|
||||||
|
'pathname' |
|
||||||
|
'route' |
|
||||||
|
'site' |
|
||||||
|
'ssr'
|
||||||
|
>;
|
||||||
|
|
||||||
|
type EndpointCallResult = {
|
||||||
|
type: 'simple',
|
||||||
|
body: string
|
||||||
|
} | {
|
||||||
|
type: 'response',
|
||||||
|
response: Response
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function call(mod: EndpointHandler, opts: EndpointOptions): Promise<EndpointCallResult> {
|
||||||
|
const paramsAndPropsResp = await getParamsAndProps({ ...opts, mod: (mod as any) });
|
||||||
|
|
||||||
|
if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||||
|
throw new Error(`[getStaticPath] route pattern matched, but no matching static path found. (${opts.pathname})`);
|
||||||
|
}
|
||||||
|
const [params] = paramsAndPropsResp;
|
||||||
|
const request = createRequest(opts.method, opts.pathname, opts.headers, opts.origin,
|
||||||
|
opts.site, opts.ssr);
|
||||||
|
|
||||||
|
const response = await renderEndpoint(mod, request, params);
|
||||||
|
|
||||||
|
if(response instanceof Response) {
|
||||||
|
return {
|
||||||
|
type: 'response',
|
||||||
|
response
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'simple',
|
||||||
|
body: response.body
|
||||||
|
};
|
||||||
|
}
|
8
packages/astro/src/core/polyfill.ts
Normal file
8
packages/astro/src/core/polyfill.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { polyfill } from '@astrojs/webapi';
|
||||||
|
|
||||||
|
export function apply() {
|
||||||
|
// polyfill WebAPIs for Node.js runtime
|
||||||
|
polyfill(globalThis, {
|
||||||
|
exclude: 'window document',
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,15 +1,15 @@
|
||||||
import type { ComponentInstance, EndpointHandler, MarkdownRenderOptions, Params, Props, Renderer, RouteData, SSRElement } from '../../@types/astro';
|
import type { ComponentInstance, EndpointHandler, MarkdownRenderOptions, Params, Props, Renderer, RouteData, SSRElement } from '../../@types/astro';
|
||||||
import type { LogOptions } from '../logger.js';
|
import type { LogOptions } from '../logger.js';
|
||||||
|
import type { AstroRequest } from './request';
|
||||||
|
|
||||||
import { renderEndpoint, renderHead, renderToString } from '../../runtime/server/index.js';
|
import { renderHead, renderPage } from '../../runtime/server/index.js';
|
||||||
import { getParams } from '../routing/index.js';
|
import { getParams } from '../routing/index.js';
|
||||||
import { createResult } from './result.js';
|
import { createResult } from './result.js';
|
||||||
import { findPathItemByKey, RouteCache, callGetStaticPaths } from './route-cache.js';
|
import { findPathItemByKey, RouteCache, callGetStaticPaths } from './route-cache.js';
|
||||||
import { warn } from '../logger.js';
|
|
||||||
|
|
||||||
interface GetParamsAndPropsOptions {
|
interface GetParamsAndPropsOptions {
|
||||||
mod: ComponentInstance;
|
mod: ComponentInstance;
|
||||||
route: RouteData | undefined;
|
route?: RouteData | undefined;
|
||||||
routeCache: RouteCache;
|
routeCache: RouteCache;
|
||||||
pathname: string;
|
pathname: string;
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
|
@ -55,7 +55,7 @@ export async function getParamsAndProps(opts: GetParamsAndPropsOptions): Promise
|
||||||
return [params, pageProps];
|
return [params, pageProps];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RenderOptions {
|
export interface RenderOptions {
|
||||||
legacyBuild: boolean;
|
legacyBuild: boolean;
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
links: Set<SSRElement>;
|
links: Set<SSRElement>;
|
||||||
|
@ -69,10 +69,13 @@ interface RenderOptions {
|
||||||
route?: RouteData;
|
route?: RouteData;
|
||||||
routeCache: RouteCache;
|
routeCache: RouteCache;
|
||||||
site?: string;
|
site?: string;
|
||||||
|
ssr: boolean;
|
||||||
|
method: string;
|
||||||
|
headers: Headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function render(opts: RenderOptions): Promise<string> {
|
export async function render(opts: RenderOptions): Promise<{ type: 'html', html: string } | { type: 'response', response: Response }> {
|
||||||
const { legacyBuild, links, logging, origin, markdownRender, mod, pathname, scripts, renderers, resolve, route, routeCache, site } = opts;
|
const { headers, legacyBuild, links, logging, origin, markdownRender, method, mod, pathname, scripts, renderers, resolve, route, routeCache, site, ssr } = opts;
|
||||||
|
|
||||||
const paramsAndPropsRes = await getParamsAndProps({
|
const paramsAndPropsRes = await getParamsAndProps({
|
||||||
logging,
|
logging,
|
||||||
|
@ -87,11 +90,6 @@ export async function render(opts: RenderOptions): Promise<string> {
|
||||||
}
|
}
|
||||||
const [params, pageProps] = paramsAndPropsRes;
|
const [params, pageProps] = paramsAndPropsRes;
|
||||||
|
|
||||||
// For endpoints, render the content immediately without injecting scripts or styles
|
|
||||||
if (route?.type === 'endpoint') {
|
|
||||||
return renderEndpoint(mod as any as EndpointHandler, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the page component before rendering the page
|
// Validate the page component before rendering the page
|
||||||
const Component = await mod.default;
|
const Component = await mod.default;
|
||||||
if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
|
if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
|
||||||
|
@ -109,10 +107,18 @@ export async function render(opts: RenderOptions): Promise<string> {
|
||||||
renderers,
|
renderers,
|
||||||
site,
|
site,
|
||||||
scripts,
|
scripts,
|
||||||
|
ssr,
|
||||||
|
method,
|
||||||
|
headers
|
||||||
});
|
});
|
||||||
|
|
||||||
let html = await renderToString(result, Component, pageProps, null);
|
let page = await renderPage(result, Component, pageProps, null);
|
||||||
|
|
||||||
|
if(page.type === 'response') {
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = page.html;
|
||||||
// handle final head injection if it hasn't happened already
|
// handle final head injection if it hasn't happened already
|
||||||
if (html.indexOf('<!--astro:head:injected-->') == -1) {
|
if (html.indexOf('<!--astro:head:injected-->') == -1) {
|
||||||
html = (await renderHead(result)) + html;
|
html = (await renderHead(result)) + html;
|
||||||
|
@ -124,6 +130,9 @@ export async function render(opts: RenderOptions): Promise<string> {
|
||||||
if (!legacyBuild && !/<!doctype html/i.test(html)) {
|
if (!legacyBuild && !/<!doctype html/i.test(html)) {
|
||||||
html = '<!DOCTYPE html>\n' + html;
|
html = '<!DOCTYPE html>\n' + html;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html;
|
return {
|
||||||
|
type: 'html',
|
||||||
|
html
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import type * as vite from 'vite';
|
import type * as vite from 'vite';
|
||||||
import type { AstroConfig, ComponentInstance, Renderer, RouteData, RuntimeMode, SSRElement } from '../../../@types/astro';
|
import type { AstroConfig, ComponentInstance, Renderer, RouteData, RuntimeMode, SSRElement } from '../../../@types/astro';
|
||||||
|
import type { AstroRequest } from '../request';
|
||||||
|
|
||||||
import { LogOptions } from '../../logger.js';
|
import { LogOptions } from '../../logger.js';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { getStylesForURL } from './css.js';
|
import { getStylesForURL } from './css.js';
|
||||||
|
@ -12,7 +14,7 @@ import { prependForwardSlash } from '../../path.js';
|
||||||
import { render as coreRender } from '../core.js';
|
import { render as coreRender } from '../core.js';
|
||||||
import { createModuleScriptElementWithSrcSet } from '../ssr-element.js';
|
import { createModuleScriptElementWithSrcSet } from '../ssr-element.js';
|
||||||
|
|
||||||
interface SSROptions {
|
export interface SSROptions {
|
||||||
/** an instance of the AstroConfig */
|
/** an instance of the AstroConfig */
|
||||||
astroConfig: AstroConfig;
|
astroConfig: AstroConfig;
|
||||||
/** location of file on disk */
|
/** location of file on disk */
|
||||||
|
@ -31,10 +33,18 @@ interface SSROptions {
|
||||||
routeCache: RouteCache;
|
routeCache: RouteCache;
|
||||||
/** Vite instance */
|
/** Vite instance */
|
||||||
viteServer: vite.ViteDevServer;
|
viteServer: vite.ViteDevServer;
|
||||||
|
/** Method */
|
||||||
|
method: string;
|
||||||
|
/** Headers */
|
||||||
|
headers: Headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComponentPreload = [Renderer[], ComponentInstance];
|
export type ComponentPreload = [Renderer[], ComponentInstance];
|
||||||
|
|
||||||
|
export type RenderResponse =
|
||||||
|
{ type: 'html', html: string } |
|
||||||
|
{ type: 'response', response: Response };
|
||||||
|
|
||||||
const svelteStylesRE = /svelte\?svelte&type=style/;
|
const svelteStylesRE = /svelte\?svelte&type=style/;
|
||||||
|
|
||||||
export async function preload({ astroConfig, filePath, viteServer }: Pick<SSROptions, 'astroConfig' | 'filePath' | 'viteServer'>): Promise<ComponentPreload> {
|
export async function preload({ astroConfig, filePath, viteServer }: Pick<SSROptions, 'astroConfig' | 'filePath' | 'viteServer'>): Promise<ComponentPreload> {
|
||||||
|
@ -47,8 +57,8 @@ export async function preload({ astroConfig, filePath, viteServer }: Pick<SSROpt
|
||||||
}
|
}
|
||||||
|
|
||||||
/** use Vite to SSR */
|
/** use Vite to SSR */
|
||||||
export async function render(renderers: Renderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise<string> {
|
export async function render(renderers: Renderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise<RenderResponse> {
|
||||||
const { astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer } = ssrOpts;
|
const { astroConfig, filePath, logging, mode, origin, pathname, method, headers, route, routeCache, viteServer } = ssrOpts;
|
||||||
const legacy = astroConfig.buildOptions.legacyBuild;
|
const legacy = astroConfig.buildOptions.legacyBuild;
|
||||||
|
|
||||||
// Add hoisted script tags
|
// Add hoisted script tags
|
||||||
|
@ -113,9 +123,12 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
|
||||||
route,
|
route,
|
||||||
routeCache,
|
routeCache,
|
||||||
site: astroConfig.buildOptions.site,
|
site: astroConfig.buildOptions.site,
|
||||||
|
ssr: astroConfig.buildOptions.experimentalSsr,
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (route?.type === 'endpoint') {
|
if (route?.type === 'endpoint' || content.type === 'response') {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,23 +171,26 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
|
||||||
}
|
}
|
||||||
|
|
||||||
// add injected tags
|
// add injected tags
|
||||||
content = injectTags(content, tags);
|
let html = injectTags(content.html, tags);
|
||||||
|
|
||||||
// run transformIndexHtml() in dev to run Vite dev transformations
|
// run transformIndexHtml() in dev to run Vite dev transformations
|
||||||
if (mode === 'development' && astroConfig.buildOptions.legacyBuild) {
|
if (mode === 'development' && astroConfig.buildOptions.legacyBuild) {
|
||||||
const relativeURL = filePath.href.replace(astroConfig.projectRoot.href, '/');
|
const relativeURL = filePath.href.replace(astroConfig.projectRoot.href, '/');
|
||||||
content = await viteServer.transformIndexHtml(relativeURL, content, pathname);
|
html = await viteServer.transformIndexHtml(relativeURL, html, pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
// inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?)
|
// inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?)
|
||||||
if (!/<!doctype html/i.test(content)) {
|
if (!/<!doctype html/i.test(html)) {
|
||||||
content = '<!DOCTYPE html>\n' + content;
|
html = '<!DOCTYPE html>\n' + content;
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return {
|
||||||
|
type: 'html',
|
||||||
|
html
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ssr(preloadedComponent: ComponentPreload, ssrOpts: SSROptions): Promise<string> {
|
export async function ssr(preloadedComponent: ComponentPreload, ssrOpts: SSROptions): Promise<RenderResponse> {
|
||||||
try {
|
try {
|
||||||
const [renderers, mod] = preloadedComponent;
|
const [renderers, mod] = preloadedComponent;
|
||||||
return await render(renderers, mod, ssrOpts); // note(drew): without "await", errors won’t get caught by errorHandler()
|
return await render(renderers, mod, ssrOpts); // note(drew): without "await", errors won’t get caught by errorHandler()
|
||||||
|
|
51
packages/astro/src/core/render/request.ts
Normal file
51
packages/astro/src/core/render/request.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import type { Params } from '../../@types/astro';
|
||||||
|
import { canonicalURL as utilCanonicalURL } from '../util.js';
|
||||||
|
|
||||||
|
type Site = string | undefined;
|
||||||
|
|
||||||
|
export interface AstroRequest {
|
||||||
|
/** get the current page URL */
|
||||||
|
url: URL;
|
||||||
|
|
||||||
|
/** get the current canonical URL */
|
||||||
|
canonicalURL: URL;
|
||||||
|
|
||||||
|
/** get page params (dynamic pages only) */
|
||||||
|
params: Params;
|
||||||
|
|
||||||
|
headers: Headers;
|
||||||
|
|
||||||
|
method: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AstroRequestSSR = AstroRequest
|
||||||
|
|
||||||
|
export function createRequest(method: string, pathname: string, headers: Headers,
|
||||||
|
origin: string, site: Site, ssr: boolean): AstroRequest {
|
||||||
|
const url = new URL('.' + pathname, new URL(origin));
|
||||||
|
|
||||||
|
const canonicalURL = utilCanonicalURL('.' + pathname, site ?? url.origin);
|
||||||
|
|
||||||
|
const request: AstroRequest = {
|
||||||
|
url,
|
||||||
|
canonicalURL,
|
||||||
|
params: {},
|
||||||
|
headers,
|
||||||
|
method
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!ssr) {
|
||||||
|
// Headers are only readable if using SSR-mode. If not, make it an empty headers
|
||||||
|
// object, so you can't do something bad.
|
||||||
|
request.headers = new Headers();
|
||||||
|
|
||||||
|
// Disallow using query params.
|
||||||
|
request.url = new URL(request.url);
|
||||||
|
|
||||||
|
for(const [key] of request.url.searchParams) {
|
||||||
|
request.url.searchParams.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
|
@ -1,13 +1,22 @@
|
||||||
import type { AstroGlobal, AstroGlobalPartial, MarkdownParser, MarkdownRenderOptions, Params, Renderer, SSRElement, SSRResult } from '../../@types/astro';
|
import type { AstroGlobal, AstroGlobalPartial, MarkdownParser, MarkdownRenderOptions, Params, Renderer, SSRElement, SSRResult } from '../../@types/astro';
|
||||||
|
import type { AstroRequest } from './request';
|
||||||
|
|
||||||
import { bold } from 'kleur/colors';
|
import { bold } from 'kleur/colors';
|
||||||
import { canonicalURL as getCanonicalURL } from '../util.js';
|
import { createRequest } from './request.js';
|
||||||
import { isCSSRequest } from './dev/css.js';
|
import { isCSSRequest } from './dev/css.js';
|
||||||
import { isScriptRequest } from './script.js';
|
import { isScriptRequest } from './script.js';
|
||||||
import { renderSlot } from '../../runtime/server/index.js';
|
import { renderSlot } from '../../runtime/server/index.js';
|
||||||
import { warn, LogOptions } from '../logger.js';
|
import { warn, LogOptions } from '../logger.js';
|
||||||
|
|
||||||
|
function onlyAvailableInSSR(name: string) {
|
||||||
|
return function() {
|
||||||
|
// TODO add more guidance when we have docs and adapters.
|
||||||
|
throw new Error(`Oops, you are trying to use ${name}, which is only available with SSR.`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateResultArgs {
|
export interface CreateResultArgs {
|
||||||
|
ssr: boolean;
|
||||||
legacyBuild: boolean;
|
legacyBuild: boolean;
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
origin: string;
|
origin: string;
|
||||||
|
@ -19,6 +28,8 @@ export interface CreateResultArgs {
|
||||||
site: string | undefined;
|
site: string | undefined;
|
||||||
links?: Set<SSRElement>;
|
links?: Set<SSRElement>;
|
||||||
scripts?: Set<SSRElement>;
|
scripts?: Set<SSRElement>;
|
||||||
|
headers: Headers;
|
||||||
|
method: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Slots {
|
class Slots {
|
||||||
|
@ -63,7 +74,10 @@ class Slots {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createResult(args: CreateResultArgs): SSRResult {
|
export function createResult(args: CreateResultArgs): SSRResult {
|
||||||
const { legacyBuild, origin, markdownRender, params, pathname, renderers, resolve, site: buildOptionsSite } = args;
|
const { legacyBuild, markdownRender, method, origin, headers, params, pathname, renderers, resolve, site } = args;
|
||||||
|
|
||||||
|
const request = createRequest(method, pathname, headers, origin, site, args.ssr);
|
||||||
|
request.params = params;
|
||||||
|
|
||||||
// Create the result object that will be passed into the render function.
|
// Create the result object that will be passed into the render function.
|
||||||
// This object starts here as an empty shell (not yet the result) but then
|
// This object starts here as an empty shell (not yet the result) but then
|
||||||
|
@ -74,19 +88,20 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
||||||
links: args.links ?? new Set<SSRElement>(),
|
links: args.links ?? new Set<SSRElement>(),
|
||||||
/** This function returns the `Astro` faux-global */
|
/** This function returns the `Astro` faux-global */
|
||||||
createAstro(astroGlobal: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null) {
|
createAstro(astroGlobal: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null) {
|
||||||
const site = new URL(origin);
|
|
||||||
const url = new URL('.' + pathname, site);
|
|
||||||
const canonicalURL = getCanonicalURL('.' + pathname, buildOptionsSite || origin);
|
|
||||||
const astroSlots = new Slots(result, slots);
|
const astroSlots = new Slots(result, slots);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
__proto__: astroGlobal,
|
__proto__: astroGlobal,
|
||||||
props,
|
props,
|
||||||
request: {
|
request,
|
||||||
canonicalURL,
|
redirect: args.ssr ? (path: string) => {
|
||||||
params,
|
return new Response(null, {
|
||||||
url,
|
status: 301,
|
||||||
},
|
headers: {
|
||||||
|
Location: path
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} : onlyAvailableInSSR('Astro.redirect'),
|
||||||
resolve(path: string) {
|
resolve(path: string) {
|
||||||
if (!legacyBuild) {
|
if (!legacyBuild) {
|
||||||
let extra = `This can be replaced with a dynamic import like so: await import("${path}")`;
|
let extra = `This can be replaced with a dynamic import like so: await import("${path}")`;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { AstroComponentMetadata, EndpointHandler, Renderer } from '../../@types/astro';
|
import type { AstroComponentMetadata, EndpointHandler, Renderer, Params } from '../../@types/astro';
|
||||||
import type { AstroGlobalPartial, SSRResult, SSRElement } from '../../@types/astro';
|
import type { AstroGlobalPartial, SSRResult, SSRElement } from '../../@types/astro';
|
||||||
|
import type { AstroRequest } from '../../core/render/request';
|
||||||
|
|
||||||
import shorthash from 'shorthash';
|
import shorthash from 'shorthash';
|
||||||
import { extractDirectives, generateHydrateScript } from './hydration.js';
|
import { extractDirectives, generateHydrateScript } from './hydration.js';
|
||||||
|
@ -80,13 +81,17 @@ export class AstroComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAstroComponent(obj: any): obj is AstroComponent {
|
||||||
|
return typeof obj === 'object' && Object.prototype.toString.call(obj) === '[object AstroComponent]';
|
||||||
|
}
|
||||||
|
|
||||||
export async function render(htmlParts: TemplateStringsArray, ...expressions: any[]) {
|
export async function render(htmlParts: TemplateStringsArray, ...expressions: any[]) {
|
||||||
return new AstroComponent(htmlParts, expressions);
|
return new AstroComponent(htmlParts, expressions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The callback passed to to $$createComponent
|
// The callback passed to to $$createComponent
|
||||||
export interface AstroComponentFactory {
|
export interface AstroComponentFactory {
|
||||||
(result: any, props: any, slots: any): ReturnType<typeof render>;
|
(result: any, props: any, slots: any): ReturnType<typeof render> | Response;
|
||||||
isAstroComponentFactory?: boolean;
|
isAstroComponentFactory?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -407,24 +412,19 @@ export function defineScriptVars(vars: Record<any, any>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renders an endpoint request to completion, returning the body.
|
// Renders an endpoint request to completion, returning the body.
|
||||||
export async function renderEndpoint(mod: EndpointHandler, params: any) {
|
export async function renderEndpoint(mod: EndpointHandler, request: AstroRequest, params: Params) {
|
||||||
const method = 'get';
|
const chosenMethod = request.method?.toLowerCase() ?? 'get';
|
||||||
const handler = mod[method];
|
const handler = mod[chosenMethod];
|
||||||
|
|
||||||
if (!handler || typeof handler !== 'function') {
|
if (!handler || typeof handler !== 'function') {
|
||||||
throw new Error(`Endpoint handler not found! Expected an exported function for "${method}"`);
|
throw new Error(`Endpoint handler not found! Expected an exported function for "${chosenMethod}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { body } = await mod.get(params);
|
return await handler.call(mod, params, request);
|
||||||
|
|
||||||
return body;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calls a component and renders it into a string of HTML
|
async function replaceHeadInjection(result: SSRResult, html: string): Promise<string> {
|
||||||
export async function renderToString(result: SSRResult, componentFactory: AstroComponentFactory, props: any, children: any): Promise<string> {
|
let template = html;
|
||||||
const Component = await componentFactory(result, props, children);
|
|
||||||
let template = await renderAstroComponent(Component);
|
|
||||||
|
|
||||||
// <!--astro:head--> injected by compiler
|
// <!--astro:head--> injected by compiler
|
||||||
// Must be handled at the end of the rendering process
|
// Must be handled at the end of the rendering process
|
||||||
if (template.indexOf('<!--astro:head-->') > -1) {
|
if (template.indexOf('<!--astro:head-->') > -1) {
|
||||||
|
@ -433,6 +433,41 @@ export async function renderToString(result: SSRResult, componentFactory: AstroC
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calls a component and renders it into a string of HTML
|
||||||
|
export async function renderToString(result: SSRResult, componentFactory: AstroComponentFactory,
|
||||||
|
props: any, children: any): Promise<string> {
|
||||||
|
const Component = await componentFactory(result, props, children);
|
||||||
|
if(!isAstroComponent(Component)) {
|
||||||
|
throw new Error('Cannot return a Response from a nested component.');
|
||||||
|
}
|
||||||
|
|
||||||
|
let template = await renderAstroComponent(Component);
|
||||||
|
return replaceHeadInjection(result, template);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderPage(
|
||||||
|
result: SSRResult,
|
||||||
|
componentFactory: AstroComponentFactory,
|
||||||
|
props: any,
|
||||||
|
children: any
|
||||||
|
): Promise<{ type: 'html', html: string } | { type: 'response', response: Response }> {
|
||||||
|
const response = await componentFactory(result, props, children);
|
||||||
|
|
||||||
|
if(isAstroComponent(response)) {
|
||||||
|
let template = await renderAstroComponent(response);
|
||||||
|
const html = await replaceHeadInjection(result, template);
|
||||||
|
return {
|
||||||
|
type: 'html',
|
||||||
|
html
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: 'response',
|
||||||
|
response
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Filter out duplicate elements in our set
|
// Filter out duplicate elements in our set
|
||||||
const uniqueElements = (item: any, index: number, all: any[]) => {
|
const uniqueElements = (item: any, index: number, all: any[]) => {
|
||||||
const props = JSON.stringify(item.props);
|
const props = JSON.stringify(item.props);
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
import type * as vite from 'vite';
|
import type * as vite from 'vite';
|
||||||
import type http from 'http';
|
import type http from 'http';
|
||||||
import type { AstroConfig, ManifestData } from '../@types/astro';
|
import type { AstroConfig, ManifestData } from '../@types/astro';
|
||||||
|
import type { RenderResponse, SSROptions } from '../core/render/dev/index';
|
||||||
import { info, warn, error, LogOptions } from '../core/logger.js';
|
import { info, warn, error, LogOptions } from '../core/logger.js';
|
||||||
import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/core.js';
|
import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/core.js';
|
||||||
import { createRouteManifest, matchRoute } from '../core/routing/index.js';
|
import { createRouteManifest, matchRoute } from '../core/routing/index.js';
|
||||||
import stripAnsi from 'strip-ansi';
|
import stripAnsi from 'strip-ansi';
|
||||||
import { createSafeError } from '../core/util.js';
|
import { createSafeError } from '../core/util.js';
|
||||||
import { ssr, preload } from '../core/render/dev/index.js';
|
import { ssr, preload } from '../core/render/dev/index.js';
|
||||||
|
import { call as callEndpoint } from '../core/endpoint/dev/index.js';
|
||||||
import * as msg from '../core/messages.js';
|
import * as msg from '../core/messages.js';
|
||||||
|
|
||||||
import notFoundTemplate, { subpathNotUsedTemplate } from '../template/4xx.js';
|
import notFoundTemplate, { subpathNotUsedTemplate } from '../template/4xx.js';
|
||||||
import serverErrorTemplate from '../template/5xx.js';
|
import serverErrorTemplate from '../template/5xx.js';
|
||||||
import { RouteCache } from '../core/render/route-cache.js';
|
import { RouteCache } from '../core/render/route-cache.js';
|
||||||
|
import { AstroRequest } from '../core/render/request.js';
|
||||||
|
|
||||||
interface AstroPluginOptions {
|
interface AstroPluginOptions {
|
||||||
config: AstroConfig;
|
config: AstroConfig;
|
||||||
|
@ -37,6 +40,33 @@ function writeHtmlResponse(res: http.ServerResponse, statusCode: number, html: s
|
||||||
res.end();
|
res.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function writeWebResponse(res: http.ServerResponse, webResponse: Response) {
|
||||||
|
const { status, headers, body } = webResponse;
|
||||||
|
res.writeHead(status, Object.fromEntries(headers.entries()));
|
||||||
|
if(body) {
|
||||||
|
const reader = body.getReader();
|
||||||
|
while(true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if(done) break;
|
||||||
|
if(value) {
|
||||||
|
res.write(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeSSRResult(result: RenderResponse, res: http.ServerResponse, statusCode: 200 | 404) {
|
||||||
|
if(result.type === 'response') {
|
||||||
|
const { response } = result;
|
||||||
|
await writeWebResponse(res, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { html } = result;
|
||||||
|
writeHtmlResponse(res, statusCode, html);
|
||||||
|
}
|
||||||
|
|
||||||
async function handle404Response(origin: string, config: AstroConfig, req: http.IncomingMessage, res: http.ServerResponse) {
|
async function handle404Response(origin: string, config: AstroConfig, req: http.IncomingMessage, res: http.ServerResponse) {
|
||||||
const site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined;
|
const site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined;
|
||||||
const devRoot = site ? site.pathname : '/';
|
const devRoot = site ? site.pathname : '/';
|
||||||
|
@ -87,7 +117,8 @@ async function handleRequest(
|
||||||
const site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined;
|
const site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined;
|
||||||
const devRoot = site ? site.pathname : '/';
|
const devRoot = site ? site.pathname : '/';
|
||||||
const origin = `${viteServer.config.server.https ? 'https' : 'http'}://${req.headers.host}`;
|
const origin = `${viteServer.config.server.https ? 'https' : 'http'}://${req.headers.host}`;
|
||||||
const pathname = decodeURI(new URL(origin + req.url).pathname);
|
const url = new URL(origin + req.url);
|
||||||
|
const pathname = decodeURI(url.pathname);
|
||||||
const rootRelativeUrl = pathname.substring(devRoot.length - 1);
|
const rootRelativeUrl = pathname.substring(devRoot.length - 1);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -129,24 +160,26 @@ async function handleRequest(
|
||||||
if (routeCustom404) {
|
if (routeCustom404) {
|
||||||
const filePathCustom404 = new URL(`./${routeCustom404.component}`, config.projectRoot);
|
const filePathCustom404 = new URL(`./${routeCustom404.component}`, config.projectRoot);
|
||||||
const preloadedCompCustom404 = await preload({ astroConfig: config, filePath: filePathCustom404, viteServer });
|
const preloadedCompCustom404 = await preload({ astroConfig: config, filePath: filePathCustom404, viteServer });
|
||||||
const html = await ssr(preloadedCompCustom404, {
|
const result = await ssr(preloadedCompCustom404, {
|
||||||
astroConfig: config,
|
astroConfig: config,
|
||||||
filePath: filePathCustom404,
|
filePath: filePathCustom404,
|
||||||
logging,
|
logging,
|
||||||
mode: 'development',
|
mode: 'development',
|
||||||
|
method: 'GET',
|
||||||
|
headers: new Headers(Object.entries(req.headers as Record<string, any>)),
|
||||||
origin,
|
origin,
|
||||||
pathname: rootRelativeUrl,
|
pathname: rootRelativeUrl,
|
||||||
route: routeCustom404,
|
route: routeCustom404,
|
||||||
routeCache,
|
routeCache,
|
||||||
viteServer,
|
viteServer,
|
||||||
});
|
});
|
||||||
return writeHtmlResponse(res, statusCode, html);
|
return await writeSSRResult(result, res, statusCode);
|
||||||
} else {
|
} else {
|
||||||
return handle404Response(origin, config, req, res);
|
return handle404Response(origin, config, req, res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const html = await ssr(preloadedComponent, {
|
const options: SSROptions = {
|
||||||
astroConfig: config,
|
astroConfig: config,
|
||||||
filePath,
|
filePath,
|
||||||
logging,
|
logging,
|
||||||
|
@ -156,9 +189,25 @@ async function handleRequest(
|
||||||
route,
|
route,
|
||||||
routeCache,
|
routeCache,
|
||||||
viteServer,
|
viteServer,
|
||||||
});
|
method: req.method || 'GET',
|
||||||
writeHtmlResponse(res, statusCode, html);
|
headers: new Headers(Object.entries(req.headers as Record<string, any>)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Route successfully matched! Render it.
|
||||||
|
if(route.type === 'endpoint') {
|
||||||
|
const result = await callEndpoint(options);
|
||||||
|
if(result.type === 'response') {
|
||||||
|
await writeWebResponse(res, result.response);
|
||||||
|
} else {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(result.body);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const result = await ssr(preloadedComponent, options);
|
||||||
|
return await writeSSRResult(result, res, statusCode);
|
||||||
|
}
|
||||||
} catch (_err: any) {
|
} catch (_err: any) {
|
||||||
|
debugger;
|
||||||
info(logging, 'serve', msg.req({ url: pathname, statusCode: 500 }));
|
info(logging, 'serve', msg.req({ url: pathname, statusCode: 500 }));
|
||||||
const err = createSafeError(_err);
|
const err = createSafeError(_err);
|
||||||
error(logging, 'error', msg.err(err));
|
error(logging, 'error', msg.err(err));
|
||||||
|
|
|
@ -83,10 +83,12 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin {
|
||||||
for (const pathname of pageData.paths) {
|
for (const pathname of pageData.paths) {
|
||||||
pageNames.push(pathname.replace(/\/?$/, '/').replace(/^\//, ''));
|
pageNames.push(pathname.replace(/\/?$/, '/').replace(/^\//, ''));
|
||||||
const id = ASTRO_PAGE_PREFIX + pathname;
|
const id = ASTRO_PAGE_PREFIX + pathname;
|
||||||
const html = await ssrRender(renderers, mod, {
|
const response = await ssrRender(renderers, mod, {
|
||||||
astroConfig,
|
astroConfig,
|
||||||
filePath: new URL(`./${component}`, astroConfig.projectRoot),
|
filePath: new URL(`./${component}`, astroConfig.projectRoot),
|
||||||
logging,
|
logging,
|
||||||
|
headers: new Headers(),
|
||||||
|
method: 'GET',
|
||||||
mode: 'production',
|
mode: 'production',
|
||||||
origin,
|
origin,
|
||||||
pathname,
|
pathname,
|
||||||
|
@ -94,6 +96,12 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin {
|
||||||
routeCache,
|
routeCache,
|
||||||
viteServer,
|
viteServer,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if(response.type !== 'html') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = response.html;
|
||||||
renderedPageMap.set(id, html);
|
renderedPageMap.set(id, html);
|
||||||
|
|
||||||
const document = parse5.parse(html, {
|
const document = parse5.parse(html, {
|
||||||
|
|
|
@ -80,7 +80,7 @@ export async function loadFixture(inlineConfig) {
|
||||||
return devResult;
|
return devResult;
|
||||||
},
|
},
|
||||||
config,
|
config,
|
||||||
fetch: (url, init) => fetch(`http://${config.devOptions.hostname}:${config.devOptions.port}${url.replace(/^\/?/, '/')}`, init),
|
fetch: (url, init) => fetch(`http://${'127.0.0.1'}:${config.devOptions.port}${url.replace(/^\/?/, '/')}`, init),
|
||||||
preview: async (opts = {}) => {
|
preview: async (opts = {}) => {
|
||||||
const previewServer = await preview(config, { logging: 'error', ...opts });
|
const previewServer = await preview(config, { logging: 'error', ...opts });
|
||||||
inlineConfig.devOptions.port = previewServer.port; // update port for fetch
|
inlineConfig.devOptions.port = previewServer.port; // update port for fetch
|
||||||
|
|
|
@ -203,11 +203,15 @@ importers:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@astrojs/renderer-svelte': ^0.5.2
|
'@astrojs/renderer-svelte': ^0.5.2
|
||||||
astro: ^0.24.3
|
astro: ^0.24.3
|
||||||
|
concurrently: ^7.0.0
|
||||||
|
lightcookie: ^1.0.25
|
||||||
unocss: ^0.15.6
|
unocss: ^0.15.6
|
||||||
vite-imagetools: ^4.0.3
|
vite-imagetools: ^4.0.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@astrojs/renderer-svelte': link:../../packages/renderers/renderer-svelte
|
'@astrojs/renderer-svelte': link:../../packages/renderers/renderer-svelte
|
||||||
astro: link:../../packages/astro
|
astro: link:../../packages/astro
|
||||||
|
concurrently: 7.0.0
|
||||||
|
lightcookie: 1.0.25
|
||||||
unocss: 0.15.6
|
unocss: 0.15.6
|
||||||
vite-imagetools: 4.0.3
|
vite-imagetools: 4.0.3
|
||||||
|
|
||||||
|
@ -4636,6 +4640,21 @@ packages:
|
||||||
/concat-map/0.0.1:
|
/concat-map/0.0.1:
|
||||||
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
|
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
|
||||||
|
|
||||||
|
/concurrently/7.0.0:
|
||||||
|
resolution: {integrity: sha512-WKM7PUsI8wyXpF80H+zjHP32fsgsHNQfPLw/e70Z5dYkV7hF+rf8q3D+ScWJIEr57CpkO3OWBko6hwhQLPR8Pw==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.13.0 || >=16.0.0}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
chalk: 4.1.2
|
||||||
|
date-fns: 2.28.0
|
||||||
|
lodash: 4.17.21
|
||||||
|
rxjs: 6.6.7
|
||||||
|
spawn-command: 0.0.2-1
|
||||||
|
supports-color: 8.1.1
|
||||||
|
tree-kill: 1.2.2
|
||||||
|
yargs: 16.2.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/consola/2.15.3:
|
/consola/2.15.3:
|
||||||
resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==}
|
resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -4760,6 +4779,11 @@ packages:
|
||||||
resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==}
|
resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/date-fns/2.28.0:
|
||||||
|
resolution: {integrity: sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==}
|
||||||
|
engines: {node: '>=0.11'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/debug/4.3.3:
|
/debug/4.3.3:
|
||||||
resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==}
|
resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
|
@ -6837,6 +6861,10 @@ packages:
|
||||||
type-check: 0.4.0
|
type-check: 0.4.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lightcookie/1.0.25:
|
||||||
|
resolution: {integrity: sha512-SrY/+eBPaKAMnsn7mCsoOMZzoQyCyHHHZlFCu2fjo28XxSyCLjlooKiTxyrXTg8NPaHp1YzWi0lcGG1gDi6KHw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/lilconfig/2.0.4:
|
/lilconfig/2.0.4:
|
||||||
resolution: {integrity: sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==}
|
resolution: {integrity: sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -8572,6 +8600,13 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask: 1.2.3
|
queue-microtask: 1.2.3
|
||||||
|
|
||||||
|
/rxjs/6.6.7:
|
||||||
|
resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==}
|
||||||
|
engines: {npm: '>=2.0.0'}
|
||||||
|
dependencies:
|
||||||
|
tslib: 1.14.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/sade/1.8.1:
|
/sade/1.8.1:
|
||||||
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
|
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
@ -8867,6 +8902,10 @@ packages:
|
||||||
/space-separated-tokens/2.0.1:
|
/space-separated-tokens/2.0.1:
|
||||||
resolution: {integrity: sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw==}
|
resolution: {integrity: sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw==}
|
||||||
|
|
||||||
|
/spawn-command/0.0.2-1:
|
||||||
|
resolution: {integrity: sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/spawndamnit/2.0.0:
|
/spawndamnit/2.0.0:
|
||||||
resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==}
|
resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -9318,6 +9357,11 @@ packages:
|
||||||
punycode: 2.1.1
|
punycode: 2.1.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/tree-kill/1.2.2:
|
||||||
|
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||||
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/trim-newlines/3.0.1:
|
/trim-newlines/3.0.1:
|
||||||
resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
|
resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -10077,7 +10121,7 @@ packages:
|
||||||
/wide-align/1.1.5:
|
/wide-align/1.1.5:
|
||||||
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
|
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
string-width: 1.0.2
|
string-width: 4.2.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/word-wrap/1.2.3:
|
/word-wrap/1.2.3:
|
||||||
|
|
Loading…
Reference in a new issue