summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/fuzzy-lies-nail.md5
-rw-r--r--examples/ssr/astro.config.mjs8
-rw-r--r--examples/ssr/package.json5
-rw-r--r--examples/ssr/server/api.mjs52
-rw-r--r--examples/ssr/server/server.mjs7
-rw-r--r--examples/ssr/src/api.ts51
-rw-r--r--examples/ssr/src/components/AddToCart.svelte9
-rw-r--r--examples/ssr/src/components/Cart.svelte6
-rw-r--r--examples/ssr/src/components/Header.astro20
-rw-r--r--examples/ssr/src/models/user.ts8
-rw-r--r--examples/ssr/src/pages/cart.astro47
-rw-r--r--examples/ssr/src/pages/login.astro30
-rw-r--r--examples/ssr/src/pages/login.form.js10
-rw-r--r--examples/ssr/src/pages/products/[id].astro2
-rw-r--r--examples/ssr/tsconfig.json8
-rw-r--r--packages/astro/src/@types/astro.ts12
-rw-r--r--packages/astro/src/core/app/index.ts29
-rw-r--r--packages/astro/src/core/app/node.ts19
-rw-r--r--packages/astro/src/core/build/index.ts9
-rw-r--r--packages/astro/src/core/build/static-build.ts35
-rw-r--r--packages/astro/src/core/dev/index.ts10
-rw-r--r--packages/astro/src/core/endpoint/dev/index.ts23
-rw-r--r--packages/astro/src/core/endpoint/index.ts51
-rw-r--r--packages/astro/src/core/polyfill.ts8
-rw-r--r--packages/astro/src/core/render/core.ts37
-rw-r--r--packages/astro/src/core/render/dev/index.ts36
-rw-r--r--packages/astro/src/core/render/request.ts51
-rw-r--r--packages/astro/src/core/render/result.ts35
-rw-r--r--packages/astro/src/runtime/server/index.ts63
-rw-r--r--packages/astro/src/vite-plugin-astro-server/index.ts61
-rw-r--r--packages/astro/src/vite-plugin-build-html/index.ts10
-rw-r--r--packages/astro/test/test-utils.js2
-rw-r--r--pnpm-lock.yaml46
33 files changed, 697 insertions, 108 deletions
diff --git a/.changeset/fuzzy-lies-nail.md b/.changeset/fuzzy-lies-nail.md
new file mode 100644
index 000000000..07368b458
--- /dev/null
+++ b/.changeset/fuzzy-lies-nail.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Implement APIs for headers for SSR flag
diff --git a/examples/ssr/astro.config.mjs b/examples/ssr/astro.config.mjs
index d54ab5929..4b01264ec 100644
--- a/examples/ssr/astro.config.mjs
+++ b/examples/ssr/astro.config.mjs
@@ -4,8 +4,14 @@ export default defineConfig({
renderers: ['@astrojs/renderer-svelte'],
vite: {
server: {
+ cors: {
+ credentials: true
+ },
proxy: {
- '/api': 'http://localhost:8085',
+ '/api': {
+ target: 'http://127.0.0.1:8085',
+ changeOrigin: true,
+ }
},
},
},
diff --git a/examples/ssr/package.json b/examples/ssr/package.json
index 5180ae7f9..fe135b832 100644
--- a/examples/ssr/package.json
+++ b/examples/ssr/package.json
@@ -4,7 +4,8 @@
"private": true,
"scripts": {
"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",
"build": "echo 'Run pnpm run build-ssr instead'",
"build-ssr": "node build.mjs",
@@ -13,6 +14,8 @@
"devDependencies": {
"@astrojs/renderer-svelte": "^0.5.2",
"astro": "^0.24.3",
+ "concurrently": "^7.0.0",
+ "lightcookie": "^1.0.25",
"unocss": "^0.15.6",
"vite-imagetools": "^4.0.3"
}
diff --git a/examples/ssr/server/api.mjs b/examples/ssr/server/api.mjs
index 3d2656815..9bb0be72a 100644
--- a/examples/ssr/server/api.mjs
+++ b/examples/ssr/server/api.mjs
@@ -1,9 +1,14 @@
import fs from 'fs';
+import lightcookie from 'lightcookie';
+
const dbJSON = fs.readFileSync(new URL('./db.json', import.meta.url));
const db = JSON.parse(dbJSON);
const products = db.products;
const productMap = new Map(products.map((product) => [product.id, product]));
+// Normally this would be in a database.
+const userCartItems = new Map();
+
const routes = [
{
match: /\/api\/products\/([0-9])+/,
@@ -32,6 +37,53 @@ const routes = [
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) {
diff --git a/examples/ssr/server/server.mjs b/examples/ssr/server/server.mjs
index e760ac2f8..c6f35685e 100644
--- a/examples/ssr/server/server.mjs
+++ b/examples/ssr/server/server.mjs
@@ -15,9 +15,10 @@ async function handle(req, res) {
const route = app.match(req);
if (route) {
- const html = await app.render(req, route);
-
- res.writeHead(200, {
+ /** @type {Response} */
+ const response = await app.render(req, route);
+ const html = await response.text();
+ res.writeHead(response.status, {
'Content-Type': 'text/html; charset=utf-8',
'Content-Length': Buffer.byteLength(html, 'utf-8'),
});
diff --git a/examples/ssr/src/api.ts b/examples/ssr/src/api.ts
index 59619ade6..b71990f3f 100644
--- a/examples/ssr/src/api.ts
+++ b/examples/ssr/src/api.ts
@@ -5,12 +5,25 @@ interface Product {
image: string;
}
-//let origin: string;
-const { mode } = import.meta.env;
-const origin = mode === 'develepment' ? `http://localhost:3000` : `http://localhost:8085`;
+interface User {
+ id: number;
+}
+
+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> {
- const response = await fetch(`${origin}${endpoint}`);
+ const response = await fetch(`${origin}${endpoint}`, {
+ credentials: 'same-origin'
+ });
if (!response.ok) {
// TODO make this better...
return null;
@@ -31,3 +44,33 @@ export async function getProduct(id: number): Promise<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
+ })
+ });
+}
diff --git a/examples/ssr/src/components/AddToCart.svelte b/examples/ssr/src/components/AddToCart.svelte
index b03b8180a..0f7a97a93 100644
--- a/examples/ssr/src/components/AddToCart.svelte
+++ b/examples/ssr/src/components/AddToCart.svelte
@@ -1,11 +1,18 @@
<script>
+ import { addToUserCart } from '../api';
export let id = 0;
+ export let name = '';
- function addToCart() {
+ function notifyCartItem(id) {
window.dispatchEvent(new CustomEvent('add-to-cart', {
detail: id
}));
}
+
+ async function addToCart() {
+ await addToUserCart(id, name);
+ notifyCartItem(id);
+ }
</script>
<style>
button {
diff --git a/examples/ssr/src/components/Cart.svelte b/examples/ssr/src/components/Cart.svelte
index 63dd1b5a5..74db0bc79 100644
--- a/examples/ssr/src/components/Cart.svelte
+++ b/examples/ssr/src/components/Cart.svelte
@@ -12,6 +12,8 @@
.cart {
display: flex;
align-items: center;
+ text-decoration: none;
+ color: inherit;
}
.cart :first-child {
margin-right: 5px;
@@ -26,7 +28,7 @@
}
</style>
<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="count">{count}</span>
-</div>
+</a>
diff --git a/examples/ssr/src/components/Header.astro b/examples/ssr/src/components/Header.astro
index 2839c70d3..c4d925a5f 100644
--- a/examples/ssr/src/components/Header.astro
+++ b/examples/ssr/src/components/Header.astro
@@ -1,6 +1,10 @@
---
import TextDecorationSkip from './TextDecorationSkip.astro';
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>
@import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap');
@@ -21,11 +25,25 @@ import Cart from './Cart.svelte';
color: inherit;
text-decoration: none;
}
+
+ .right-pane {
+ display: flex;
+ }
+
+ .material-icons {
+ font-size: 36px;
+ margin-right: 1rem;
+ }
</style>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<header>
<h1><a href="/"><TextDecorationSkip text="Online Store" /></a></h1>
<div class="right-pane">
- <Cart client:idle />
+ <a href="/login">
+ <span class="material-icons">
+ login
+ </span>
+ </a>
+ <Cart client:idle count={cartCount} />
</div>
</header>
diff --git a/examples/ssr/src/models/user.ts b/examples/ssr/src/models/user.ts
new file mode 100644
index 000000000..ecd839d46
--- /dev/null
+++ b/examples/ssr/src/models/user.ts
@@ -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;
+}
diff --git a/examples/ssr/src/pages/cart.astro b/examples/ssr/src/pages/cart.astro
new file mode 100644
index 000000000..e4a00183e
--- /dev/null
+++ b/examples/ssr/src/pages/cart.astro
@@ -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>
diff --git a/examples/ssr/src/pages/login.astro b/examples/ssr/src/pages/login.astro
new file mode 100644
index 000000000..b12a82a5e
--- /dev/null
+++ b/examples/ssr/src/pages/login.astro
@@ -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>
diff --git a/examples/ssr/src/pages/login.form.js b/examples/ssr/src/pages/login.form.js
new file mode 100644
index 000000000..9875ae160
--- /dev/null
+++ b/examples/ssr/src/pages/login.form.js
@@ -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'
+ }
+ });
+}
diff --git a/examples/ssr/src/pages/products/[id].astro b/examples/ssr/src/pages/products/[id].astro
index 943f2ab84..9c400c2f1 100644
--- a/examples/ssr/src/pages/products/[id].astro
+++ b/examples/ssr/src/pages/products/[id].astro
@@ -45,7 +45,7 @@ const product = await getProduct(id);
<figure>
<img src={product.image} />
<figcaption>
- <AddToCart id={id} client:idle />
+ <AddToCart client:idle id={id} name={product.name} />
<p>Description here...</p>
</figcaption>
</figure>
diff --git a/examples/ssr/tsconfig.json b/examples/ssr/tsconfig.json
new file mode 100644
index 000000000..e0065a323
--- /dev/null
+++ b/examples/ssr/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "lib": ["ES2015", "DOM"],
+ "module": "ES2022",
+ "moduleResolution": "node",
+ "types": ["astro/env"]
+ }
+}
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 400f5070b..ce089b7e6 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -2,6 +2,7 @@ import type * as babel from '@babel/core';
import type { z } from 'zod';
import type { AstroConfigSchema } from '../core/config';
import type { AstroComponentFactory, Metadata } from '../runtime/server';
+import type { AstroRequest } from '../core/render/request';
import type * as vite from 'vite';
export interface AstroBuiltinProps {
@@ -43,14 +44,7 @@ export interface AstroGlobal extends AstroGlobalPartial {
/** set props for this astro component (along with default values) */
props: Record<string, number | string | any>;
/** get information about this page */
- request: {
- /** get the current page URL */
- url: URL;
- /** get the current canonical URL */
- canonicalURL: URL;
- /** get page params (dynamic pages only) */
- params: Params;
- };
+ request: AstroRequest;
/** see if slots are used */
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 {
- [method: string]: (params: any) => EndpointOutput;
+ [method: string]: (params: any, request: AstroRequest) => EndpointOutput | Response;
}
/**
diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts
index c31c37f31..fc5ffcced 100644
--- a/packages/astro/src/core/app/index.ts
+++ b/packages/astro/src/core/app/index.ts
@@ -27,14 +27,18 @@ export class App {
this.#routeCache = new RouteCache(defaultLogOptions);
this.#renderersPromise = this.#loadRenderers();
}
- match({ pathname }: URL): RouteData | undefined {
- return matchRoute(pathname, this.#manifestData);
+ match(request: Request): RouteData | undefined {
+ 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) {
- routeData = this.match(url);
+ routeData = this.match(request);
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 [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 scripts = createModuleScriptElementWithSrcSet(info.scripts, manifest.site);
- return render({
+ const result = await render({
legacyBuild: false,
links,
logging: defaultLogOptions,
@@ -65,6 +70,18 @@ export class App {
route: routeData,
routeCache: this.#routeCache,
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[]> {
diff --git a/packages/astro/src/core/app/node.ts b/packages/astro/src/core/app/node.ts
index d1bcbf46b..46ba710c2 100644
--- a/packages/astro/src/core/app/node.ts
+++ b/packages/astro/src/core/app/node.ts
@@ -1,20 +1,27 @@
import type { SSRManifest, SerializedSSRManifest } from './types';
+import type { IncomingHttpHeaders } from 'http';
import * as fs from 'fs';
import { App } from './index.js';
import { deserializeManifest } from './common.js';
import { IncomingMessage } from 'http';
-function createURLFromRequest(req: IncomingMessage): URL {
- return new URL(`http://${req.headers.host}${req.url}`);
+function createRequestFromNodeRequest(req: IncomingMessage): Request {
+ 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 {
- match(req: IncomingMessage | URL) {
- return super.match(req instanceof URL ? req : createURLFromRequest(req));
+ match(req: IncomingMessage | Request) {
+ return super.match(req instanceof Request ? req : createRequestFromNodeRequest(req));
}
- render(req: IncomingMessage | URL) {
- return super.render(req instanceof URL ? req : createURLFromRequest(req));
+ render(req: IncomingMessage | Request) {
+ return super.render(req instanceof Request ? req : createRequestFromNodeRequest(req));
}
}
diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts
index 5f5be3ca7..ece445aa0 100644
--- a/packages/astro/src/core/build/index.ts
+++ b/packages/astro/src/core/build/index.ts
@@ -3,7 +3,7 @@ import type { LogOptions } from '../logger';
import fs from 'fs';
import * as colors from 'kleur/colors';
-import { polyfill } from '@astrojs/webapi';
+import { apply as applyPolyfill } from '../polyfill.js';
import { performance } from 'perf_hooks';
import * as vite from 'vite';
import { createVite, ViteConfigWithSSR } from '../create-vite.js';
@@ -22,11 +22,6 @@ export interface BuildOptions {
/** `astro build` */
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);
await builder.build();
}
@@ -42,6 +37,8 @@ class AstroBuilder {
private viteConfig?: ViteConfigWithSSR;
constructor(config: AstroConfig, options: BuildOptions) {
+ applyPolyfill();
+
if (!config.buildOptions.site && config.buildOptions.sitemap !== false) {
warn(options.logging, 'config', `Set "buildOptions.site" to generate correct canonical URLs and sitemap`);
}
diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts
index 393e61ed2..ea4c89011 100644
--- a/packages/astro/src/core/build/static-build.ts
+++ b/packages/astro/src/core/build/static-build.ts
@@ -1,11 +1,12 @@
import type { OutputChunk, OutputAsset, RollupOutput } from 'rollup';
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 { LogOptions } from '../logger';
import type { ViteConfigWithSSR } from '../create-vite';
import type { PageBuildData } from './types';
import type { BuildInternals } from '../../core/build/internal.js';
+import type { RenderOptions } from '../../core/render/core';
import type { SerializedSSRManifest, SerializedRouteInfo } from '../app/types';
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 { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
import { RouteCache } from '../render/route-cache.js';
+import { call as callEndpoint } from '../endpoint/index.js';
import { serializeRouteData } from '../routing/index.js';
import { render } from '../render/core.js';
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
+import { createRequest } from '../render/request.js';
export interface StaticBuildOptions {
allPages: AllPagesData;
@@ -134,6 +137,8 @@ export async function staticBuild(opts: StaticBuildOptions) {
const topLevelImports = new Set([
// Any component that gets hydrated
+ // 'components/Counter.jsx'
+ // { 'components/Counter.jsx': 'counter.hash.js' }
...metadata.hydratedComponentPaths(),
// Client-only components
...metadata.clientOnlyComponentPaths(),
@@ -373,7 +378,7 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G
const scripts = createModuleScriptElementWithSrcSet(hoistedId ? [hoistedId] : [], site);
try {
- const html = await render({
+ const options: RenderOptions = {
legacyBuild: false,
links,
logging,
@@ -392,15 +397,37 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G
const fullyRelativePath = relPath[0] === '.' ? relPath : './' + relPath;
return fullyRelativePath;
},
+ method: 'GET',
+ headers: new Headers(),
route: pageData.route,
routeCache,
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 outFile = getOutFile(astroConfig, outFolder, pathname, pageData.route.type);
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) {
error(opts.logging, 'build', `Error rendering:`, err);
}
diff --git a/packages/astro/src/core/dev/index.ts b/packages/astro/src/core/dev/index.ts
index a1b48e1d9..be3a7ef7f 100644
--- a/packages/astro/src/core/dev/index.ts
+++ b/packages/astro/src/core/dev/index.ts
@@ -1,7 +1,8 @@
-import { polyfill } from '@astrojs/webapi';
+import type { AstroConfig } from '../../@types/astro';
import type { AddressInfo } from 'net';
+
import { performance } from 'perf_hooks';
-import type { AstroConfig } from '../../@types/astro';
+import { apply as applyPolyfill } from '../polyfill.js';
import { createVite } from '../create-vite.js';
import { defaultLogOptions, info, warn, LogOptions } from '../logger.js';
import * as vite from 'vite';
@@ -20,10 +21,7 @@ export interface DevServer {
/** `astro dev` */
export default async function dev(config: AstroConfig, options: DevOptions = { logging: defaultLogOptions }): Promise<DevServer> {
const devStart = performance.now();
- // polyfill WebAPIs for Node.js runtime
- polyfill(globalThis, {
- exclude: 'window document',
- });
+ applyPolyfill();
// TODO: remove call once --hostname is baselined
const host = getResolvedHostForVite(config);
diff --git a/packages/astro/src/core/endpoint/dev/index.ts b/packages/astro/src/core/endpoint/dev/index.ts
new file mode 100644
index 000000000..d05ae3f5f
--- /dev/null
+++ b/packages/astro/src/core/endpoint/dev/index.ts
@@ -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;
+ }
+}
diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts
new file mode 100644
index 000000000..f10efcc9f
--- /dev/null
+++ b/packages/astro/src/core/endpoint/index.ts
@@ -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
+ };
+}
diff --git a/packages/astro/src/core/polyfill.ts b/packages/astro/src/core/polyfill.ts
new file mode 100644
index 000000000..99e0d5cc5
--- /dev/null
+++ b/packages/astro/src/core/polyfill.ts
@@ -0,0 +1,8 @@
+import { polyfill } from '@astrojs/webapi';
+
+export function apply() {
+ // polyfill WebAPIs for Node.js runtime
+ polyfill(globalThis, {
+ exclude: 'window document',
+ });
+}
diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts
index ccea0c743..71b4d03e7 100644
--- a/packages/astro/src/core/render/core.ts
+++ b/packages/astro/src/core/render/core.ts
@@ -1,15 +1,15 @@
import type { ComponentInstance, EndpointHandler, MarkdownRenderOptions, Params, Props, Renderer, RouteData, SSRElement } from '../../@types/astro';
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 { createResult } from './result.js';
import { findPathItemByKey, RouteCache, callGetStaticPaths } from './route-cache.js';
-import { warn } from '../logger.js';
interface GetParamsAndPropsOptions {
mod: ComponentInstance;
- route: RouteData | undefined;
+ route?: RouteData | undefined;
routeCache: RouteCache;
pathname: string;
logging: LogOptions;
@@ -55,7 +55,7 @@ export async function getParamsAndProps(opts: GetParamsAndPropsOptions): Promise
return [params, pageProps];
}
-interface RenderOptions {
+export interface RenderOptions {
legacyBuild: boolean;
logging: LogOptions;
links: Set<SSRElement>;
@@ -69,10 +69,13 @@ interface RenderOptions {
route?: RouteData;
routeCache: RouteCache;
site?: string;
+ ssr: boolean;
+ method: string;
+ headers: Headers;
}
-export async function render(opts: RenderOptions): Promise<string> {
- const { legacyBuild, links, logging, origin, markdownRender, mod, pathname, scripts, renderers, resolve, route, routeCache, site } = opts;
+export async function render(opts: RenderOptions): Promise<{ type: 'html', html: string } | { type: 'response', response: Response }> {
+ const { headers, legacyBuild, links, logging, origin, markdownRender, method, mod, pathname, scripts, renderers, resolve, route, routeCache, site, ssr } = opts;
const paramsAndPropsRes = await getParamsAndProps({
logging,
@@ -87,11 +90,6 @@ export async function render(opts: RenderOptions): Promise<string> {
}
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
const Component = await mod.default;
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,
site,
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
if (html.indexOf('<!--astro:head:injected-->') == -1) {
html = (await renderHead(result)) + html;
@@ -124,6 +130,9 @@ export async function render(opts: RenderOptions): Promise<string> {
if (!legacyBuild && !/<!doctype html/i.test(html)) {
html = '<!DOCTYPE html>\n' + html;
}
-
- return html;
+
+ return {
+ type: 'html',
+ html
+ };
}
diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts
index e44ff3072..3d92bf945 100644
--- a/packages/astro/src/core/render/dev/index.ts
+++ b/packages/astro/src/core/render/dev/index.ts
@@ -1,5 +1,7 @@
import type * as vite from 'vite';
import type { AstroConfig, ComponentInstance, Renderer, RouteData, RuntimeMode, SSRElement } from '../../../@types/astro';
+import type { AstroRequest } from '../request';
+
import { LogOptions } from '../../logger.js';
import { fileURLToPath } from 'url';
import { getStylesForURL } from './css.js';
@@ -12,7 +14,7 @@ import { prependForwardSlash } from '../../path.js';
import { render as coreRender } from '../core.js';
import { createModuleScriptElementWithSrcSet } from '../ssr-element.js';
-interface SSROptions {
+export interface SSROptions {
/** an instance of the AstroConfig */
astroConfig: AstroConfig;
/** location of file on disk */
@@ -31,10 +33,18 @@ interface SSROptions {
routeCache: RouteCache;
/** Vite instance */
viteServer: vite.ViteDevServer;
+ /** Method */
+ method: string;
+ /** Headers */
+ headers: Headers;
}
export type ComponentPreload = [Renderer[], ComponentInstance];
+export type RenderResponse =
+ { type: 'html', html: string } |
+ { type: 'response', response: Response };
+
const svelteStylesRE = /svelte\?svelte&type=style/;
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 */
-export async function render(renderers: Renderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise<string> {
- const { astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer } = ssrOpts;
+export async function render(renderers: Renderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise<RenderResponse> {
+ const { astroConfig, filePath, logging, mode, origin, pathname, method, headers, route, routeCache, viteServer } = ssrOpts;
const legacy = astroConfig.buildOptions.legacyBuild;
// Add hoisted script tags
@@ -113,9 +123,12 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
route,
routeCache,
site: astroConfig.buildOptions.site,
+ ssr: astroConfig.buildOptions.experimentalSsr,
+ method,
+ headers,
});
- if (route?.type === 'endpoint') {
+ if (route?.type === 'endpoint' || content.type === 'response') {
return content;
}
@@ -158,23 +171,26 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
}
// add injected tags
- content = injectTags(content, tags);
+ let html = injectTags(content.html, tags);
// run transformIndexHtml() in dev to run Vite dev transformations
if (mode === 'development' && astroConfig.buildOptions.legacyBuild) {
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.?)
- if (!/<!doctype html/i.test(content)) {
- content = '<!DOCTYPE html>\n' + content;
+ if (!/<!doctype html/i.test(html)) {
+ 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 {
const [renderers, mod] = preloadedComponent;
return await render(renderers, mod, ssrOpts); // note(drew): without "await", errors won’t get caught by errorHandler()
diff --git a/packages/astro/src/core/render/request.ts b/packages/astro/src/core/render/request.ts
new file mode 100644
index 000000000..b969ec4d7
--- /dev/null
+++ b/packages/astro/src/core/render/request.ts
@@ -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;
+}
diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts
index 2175e112e..1086686cc 100644
--- a/packages/astro/src/core/render/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -1,13 +1,22 @@
import type { AstroGlobal, AstroGlobalPartial, MarkdownParser, MarkdownRenderOptions, Params, Renderer, SSRElement, SSRResult } from '../../@types/astro';
+import type { AstroRequest } from './request';
import { bold } from 'kleur/colors';
-import { canonicalURL as getCanonicalURL } from '../util.js';
+import { createRequest } from './request.js';
import { isCSSRequest } from './dev/css.js';
import { isScriptRequest } from './script.js';
import { renderSlot } from '../../runtime/server/index.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 {
+ ssr: boolean;
legacyBuild: boolean;
logging: LogOptions;
origin: string;
@@ -19,6 +28,8 @@ export interface CreateResultArgs {
site: string | undefined;
links?: Set<SSRElement>;
scripts?: Set<SSRElement>;
+ headers: Headers;
+ method: string;
}
class Slots {
@@ -63,7 +74,10 @@ class Slots {
}
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.
// 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>(),
/** This function returns the `Astro` faux-global */
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);
return {
__proto__: astroGlobal,
props,
- request: {
- canonicalURL,
- params,
- url,
- },
+ request,
+ redirect: args.ssr ? (path: string) => {
+ return new Response(null, {
+ status: 301,
+ headers: {
+ Location: path
+ }
+ });
+ } : onlyAvailableInSSR('Astro.redirect'),
resolve(path: string) {
if (!legacyBuild) {
let extra = `This can be replaced with a dynamic import like so: await import("${path}")`;
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index e4307feff..14d9ce256 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -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 { AstroRequest } from '../../core/render/request';
import shorthash from 'shorthash';
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[]) {
return new AstroComponent(htmlParts, expressions);
}
// The callback passed to to $$createComponent
export interface AstroComponentFactory {
- (result: any, props: any, slots: any): ReturnType<typeof render>;
+ (result: any, props: any, slots: any): ReturnType<typeof render> | Response;
isAstroComponentFactory?: boolean;
}
@@ -407,24 +412,19 @@ export function defineScriptVars(vars: Record<any, any>) {
}
// Renders an endpoint request to completion, returning the body.
-export async function renderEndpoint(mod: EndpointHandler, params: any) {
- const method = 'get';
- const handler = mod[method];
+export async function renderEndpoint(mod: EndpointHandler, request: AstroRequest, params: Params) {
+ const chosenMethod = request.method?.toLowerCase() ?? 'get';
+ const handler = mod[chosenMethod];
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 body;
+ return await handler.call(mod, params, request);
}
-// 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);
- let template = await renderAstroComponent(Component);
-
+async function replaceHeadInjection(result: SSRResult, html: string): Promise<string> {
+ let template = html;
// <!--astro:head--> injected by compiler
// Must be handled at the end of the rendering process
if (template.indexOf('<!--astro:head-->') > -1) {
@@ -433,6 +433,41 @@ export async function renderToString(result: SSRResult, componentFactory: AstroC
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
const uniqueElements = (item: any, index: number, all: any[]) => {
const props = JSON.stringify(item.props);
diff --git a/packages/astro/src/vite-plugin-astro-server/index.ts b/packages/astro/src/vite-plugin-astro-server/index.ts
index 33baeed71..32e6adf03 100644
--- a/packages/astro/src/vite-plugin-astro-server/index.ts
+++ b/packages/astro/src/vite-plugin-astro-server/index.ts
@@ -1,17 +1,20 @@
import type * as vite from 'vite';
import type http from 'http';
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 { getParamsAndProps, GetParamsAndPropsError } from '../core/render/core.js';
import { createRouteManifest, matchRoute } from '../core/routing/index.js';
import stripAnsi from 'strip-ansi';
import { createSafeError } from '../core/util.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 notFoundTemplate, { subpathNotUsedTemplate } from '../template/4xx.js';
import serverErrorTemplate from '../template/5xx.js';
import { RouteCache } from '../core/render/route-cache.js';
+import { AstroRequest } from '../core/render/request.js';
interface AstroPluginOptions {
config: AstroConfig;
@@ -37,6 +40,33 @@ function writeHtmlResponse(res: http.ServerResponse, statusCode: number, html: s
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) {
const site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined;
const devRoot = site ? site.pathname : '/';
@@ -87,7 +117,8 @@ async function handleRequest(
const site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined;
const devRoot = site ? site.pathname : '/';
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);
try {
@@ -129,24 +160,26 @@ async function handleRequest(
if (routeCustom404) {
const filePathCustom404 = new URL(`./${routeCustom404.component}`, config.projectRoot);
const preloadedCompCustom404 = await preload({ astroConfig: config, filePath: filePathCustom404, viteServer });
- const html = await ssr(preloadedCompCustom404, {
+ const result = await ssr(preloadedCompCustom404, {
astroConfig: config,
filePath: filePathCustom404,
logging,
mode: 'development',
+ method: 'GET',
+ headers: new Headers(Object.entries(req.headers as Record<string, any>)),
origin,
pathname: rootRelativeUrl,
route: routeCustom404,
routeCache,
viteServer,
});
- return writeHtmlResponse(res, statusCode, html);
+ return await writeSSRResult(result, res, statusCode);
} else {
return handle404Response(origin, config, req, res);
}
}
- const html = await ssr(preloadedComponent, {
+ const options: SSROptions = {
astroConfig: config,
filePath,
logging,
@@ -156,9 +189,25 @@ async function handleRequest(
route,
routeCache,
viteServer,
- });
- writeHtmlResponse(res, statusCode, html);
+ method: req.method || 'GET',
+ 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) {
+ debugger;
info(logging, 'serve', msg.req({ url: pathname, statusCode: 500 }));
const err = createSafeError(_err);
error(logging, 'error', msg.err(err));
diff --git a/packages/astro/src/vite-plugin-build-html/index.ts b/packages/astro/src/vite-plugin-build-html/index.ts
index 8319970cc..3f67d9375 100644
--- a/packages/astro/src/vite-plugin-build-html/index.ts
+++ b/packages/astro/src/vite-plugin-build-html/index.ts
@@ -83,10 +83,12 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin {
for (const pathname of pageData.paths) {
pageNames.push(pathname.replace(/\/?$/, '/').replace(/^\//, ''));
const id = ASTRO_PAGE_PREFIX + pathname;
- const html = await ssrRender(renderers, mod, {
+ const response = await ssrRender(renderers, mod, {
astroConfig,
filePath: new URL(`./${component}`, astroConfig.projectRoot),
logging,
+ headers: new Headers(),
+ method: 'GET',
mode: 'production',
origin,
pathname,
@@ -94,6 +96,12 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin {
routeCache,
viteServer,
});
+
+ if(response.type !== 'html') {
+ continue;
+ }
+
+ const html = response.html;
renderedPageMap.set(id, html);
const document = parse5.parse(html, {
diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js
index 340653325..c7060dcc7 100644
--- a/packages/astro/test/test-utils.js
+++ b/packages/astro/test/test-utils.js
@@ -80,7 +80,7 @@ export async function loadFixture(inlineConfig) {
return devResult;
},
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 = {}) => {
const previewServer = await preview(config, { logging: 'error', ...opts });
inlineConfig.devOptions.port = previewServer.port; // update port for fetch
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b1e4f0029..512efc508 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -203,11 +203,15 @@ importers:
specifiers:
'@astrojs/renderer-svelte': ^0.5.2
astro: ^0.24.3
+ concurrently: ^7.0.0
+ lightcookie: ^1.0.25
unocss: ^0.15.6
vite-imagetools: ^4.0.3
devDependencies:
'@astrojs/renderer-svelte': link:../../packages/renderers/renderer-svelte
astro: link:../../packages/astro
+ concurrently: 7.0.0
+ lightcookie: 1.0.25
unocss: 0.15.6
vite-imagetools: 4.0.3
@@ -4636,6 +4640,21 @@ packages:
/concat-map/0.0.1:
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:
resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==}
dev: true
@@ -4760,6 +4779,11 @@ packages:
resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==}
dev: true
+ /date-fns/2.28.0:
+ resolution: {integrity: sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==}
+ engines: {node: '>=0.11'}
+ dev: true
+
/debug/4.3.3:
resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==}
engines: {node: '>=6.0'}
@@ -6837,6 +6861,10 @@ packages:
type-check: 0.4.0
dev: true
+ /lightcookie/1.0.25:
+ resolution: {integrity: sha512-SrY/+eBPaKAMnsn7mCsoOMZzoQyCyHHHZlFCu2fjo28XxSyCLjlooKiTxyrXTg8NPaHp1YzWi0lcGG1gDi6KHw==}
+ dev: true
+
/lilconfig/2.0.4:
resolution: {integrity: sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==}
engines: {node: '>=10'}
@@ -8572,6 +8600,13 @@ packages:
dependencies:
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:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'}
@@ -8867,6 +8902,10 @@ packages:
/space-separated-tokens/2.0.1:
resolution: {integrity: sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw==}
+ /spawn-command/0.0.2-1:
+ resolution: {integrity: sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=}
+ dev: true
+
/spawndamnit/2.0.0:
resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==}
dependencies:
@@ -9318,6 +9357,11 @@ packages:
punycode: 2.1.1
dev: true
+ /tree-kill/1.2.2:
+ resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
+ hasBin: true
+ dev: true
+
/trim-newlines/3.0.1:
resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
engines: {node: '>=8'}
@@ -10077,7 +10121,7 @@ packages:
/wide-align/1.1.5:
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
dependencies:
- string-width: 1.0.2
+ string-width: 4.2.3
dev: true
/word-wrap/1.2.3: