diff --git a/examples/ssr/astro.config.mjs b/examples/ssr/astro.config.mjs index f6aba20cef..448d5829d1 100644 --- a/examples/ssr/astro.config.mjs +++ b/examples/ssr/astro.config.mjs @@ -6,17 +6,4 @@ import nodejs from '@astrojs/node'; export default defineConfig({ adapter: nodejs(), integrations: [svelte()], - vite: { - server: { - cors: { - credentials: true, - }, - proxy: { - '/api': { - target: 'http://127.0.0.1:8085', - changeOrigin: true, - }, - }, - }, - }, }); diff --git a/examples/ssr/package.json b/examples/ssr/package.json index 44ffc3bfae..07d61d4596 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -3,9 +3,7 @@ "version": "0.0.1", "private": true, "scripts": { - "dev-api": "node server/dev-api.mjs", - "dev-server": "astro dev --experimental-ssr", - "dev": "concurrently \"npm run dev-api\" \"astro dev --experimental-ssr\"", + "dev": "astro dev --experimental-ssr", "start": "astro dev", "build": "astro build --experimental-ssr", "server": "node server/server.mjs" diff --git a/examples/ssr/server/api.mjs b/examples/ssr/server/api.mjs deleted file mode 100644 index 589766ee9f..0000000000 --- a/examples/ssr/server/api.mjs +++ /dev/null @@ -1,100 +0,0 @@ -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])+/, - async handle(_req, res, [, idStr]) { - const id = Number(idStr); - if (productMap.has(id)) { - const product = productMap.get(id); - res.writeHead(200, { - 'Content-Type': 'application/json', - }); - res.end(JSON.stringify(product)); - } else { - res.writeHead(404, { - 'Content-Type': 'text/plain', - }); - res.end('Not found'); - } - }, - }, - { - match: /\/api\/products/, - async handle(_req, res) { - res.writeHead(200, { - 'Content-Type': 'application/json', - }); - 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) { - for (const route of routes) { - const match = route.match.exec(req.url); - if (match) { - return route.handle(req, res, match); - } - } - res.writeHead(404, { - 'Content-Type': 'text/plain', - }); - res.end('Not found'); -} diff --git a/examples/ssr/server/dev-api.mjs b/examples/ssr/server/dev-api.mjs deleted file mode 100644 index 305ac609b7..0000000000 --- a/examples/ssr/server/dev-api.mjs +++ /dev/null @@ -1,17 +0,0 @@ -import { createServer } from 'http'; -import { apiHandler } from './api.mjs'; - -const PORT = process.env.PORT || 8085; - -const server = createServer((req, res) => { - apiHandler(req, res).catch((err) => { - console.error(err); - res.writeHead(500, { - 'Content-Type': 'text/plain', - }); - res.end(err.toString()); - }); -}); - -server.listen(PORT); -console.log(`API running at http://localhost:${PORT}`); diff --git a/examples/ssr/server/server.mjs b/examples/ssr/server/server.mjs index bed49b749f..9838d7ada5 100644 --- a/examples/ssr/server/server.mjs +++ b/examples/ssr/server/server.mjs @@ -1,29 +1,28 @@ import { createServer } from 'http'; import fs from 'fs'; import mime from 'mime'; -import { apiHandler } from './api.mjs'; import { handler as ssrHandler } from '../dist/server/entry.mjs'; const clientRoot = new URL('../dist/client/', import.meta.url); async function handle(req, res) { - ssrHandler(req, res, async () => { - // Did not match an SSR route + ssrHandler(req, res, async (err) => { + if(err) { + res.writeHead(500); + res.end(err.stack) + return; + } - if (/^\/api\//.test(req.url)) { - return apiHandler(req, res); - } else { - let local = new URL('.' + req.url, clientRoot); - try { - const data = await fs.promises.readFile(local); - res.writeHead(200, { - 'Content-Type': mime.getType(req.url), - }); - res.end(data); - } catch { - res.writeHead(404); - res.end(); - } + let local = new URL('.' + req.url, clientRoot); + try { + const data = await fs.promises.readFile(local); + res.writeHead(200, { + 'Content-Type': mime.getType(req.url), + }); + res.end(data); + } catch { + res.writeHead(404); + res.end(); } }); } diff --git a/examples/ssr/src/api.ts b/examples/ssr/src/api.ts index 40058360b6..64dfe17d31 100644 --- a/examples/ssr/src/api.ts +++ b/examples/ssr/src/api.ts @@ -60,7 +60,7 @@ export async function getCart(): Promise { } export async function addToUserCart(id: number | string, name: string): Promise { - await fetch(`${origin}/api/add-to-cart`, { + await fetch(`${origin}/api/cart`, { credentials: 'same-origin', method: 'POST', mode: 'no-cors', diff --git a/examples/ssr/server/db.json b/examples/ssr/src/models/db.json similarity index 100% rename from examples/ssr/server/db.json rename to examples/ssr/src/models/db.json diff --git a/examples/ssr/src/models/db.ts b/examples/ssr/src/models/db.ts new file mode 100644 index 0000000000..d9caa8b030 --- /dev/null +++ b/examples/ssr/src/models/db.ts @@ -0,0 +1,9 @@ +import db from './db.json'; + +const products = db.products; +const productMap = new Map(products.map((product) => [product.id, product])); + +export { + products, + productMap +}; diff --git a/examples/ssr/src/models/session.ts b/examples/ssr/src/models/session.ts new file mode 100644 index 0000000000..60ca8f1da4 --- /dev/null +++ b/examples/ssr/src/models/session.ts @@ -0,0 +1,3 @@ + +// Normally this would be in a database. +export const userCartItems = new Map(); diff --git a/examples/ssr/src/pages/api/cart.ts b/examples/ssr/src/pages/api/cart.ts new file mode 100644 index 0000000000..5dbe5acbdb --- /dev/null +++ b/examples/ssr/src/pages/api/cart.ts @@ -0,0 +1,47 @@ +import lightcookie from 'lightcookie'; +import { userCartItems } from '../../models/session'; + +export function get(_params: any, request: Request) { + let cookie = request.headers.get('cookie'); + let userId = cookie ? lightcookie.parse(cookie)['user-id'] : '1'; // default for testing + if (!userId || !userCartItems.has(userId)) { + return { + body: JSON.stringify({ items: [] }) + }; + } + let items = userCartItems.get(userId); + let array = Array.from(items.values()); + + return { + body: JSON.stringify({ items: array }) + } +} + +interface AddToCartItem { + id: number; + name: string; +} + +export async function post(_params: any, request: Request) { + const item: AddToCartItem = await request.json(); + + let cookie = request.headers.get('cookie'); + let userId = lightcookie.parse(cookie)['user-id']; + + if (!userCartItems.has(userId)) { + userCartItems.set(userId, new Map()); + } + + let cart = userCartItems.get(userId); + if (cart.has(item.id)) { + cart.get(item.id).count++; + } else { + cart.set(item.id, { id: item.id, name: item.name, count: 1 }); + } + + return { + body: JSON.stringify({ + ok: true + }) + }; +} diff --git a/examples/ssr/src/pages/api/products.ts b/examples/ssr/src/pages/api/products.ts new file mode 100644 index 0000000000..533bdef23e --- /dev/null +++ b/examples/ssr/src/pages/api/products.ts @@ -0,0 +1,7 @@ +import { products } from '../../models/db'; + +export function get() { + return { + body: JSON.stringify(products) + }; +} diff --git a/examples/ssr/src/pages/api/products/[id].ts b/examples/ssr/src/pages/api/products/[id].ts new file mode 100644 index 0000000000..6a3a83722b --- /dev/null +++ b/examples/ssr/src/pages/api/products/[id].ts @@ -0,0 +1,17 @@ +import { productMap } from '../../../models/db'; + +export function get({ id: idStr }) { + const id = Number(idStr); + if (productMap.has(id)) { + const product = productMap.get(id); + + return { + body: JSON.stringify(product) + }; + } else { + return new Response(null, { + status: 400, + statusText: 'Not found' + }); + } +} diff --git a/examples/ssr/tsconfig.json b/examples/ssr/tsconfig.json index e0065a323c..ee0432bb3b 100644 --- a/examples/ssr/tsconfig.json +++ b/examples/ssr/tsconfig.json @@ -3,6 +3,7 @@ "lib": ["ES2015", "DOM"], "module": "ES2022", "moduleResolution": "node", + "resolveJsonModule": true, "types": ["astro/env"] } } diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index b14fad5047..dba0a99307 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -71,8 +71,7 @@ export class App { routeCache: this.#routeCache, site: this.#manifest.site, ssr: true, - method: info.routeData.type === 'endpoint' ? '' : 'GET', - headers: request.headers, + request, }); if (result.type === 'response') { diff --git a/packages/astro/src/core/request.ts b/packages/astro/src/core/request.ts index 74ce72eec3..1330d9b883 100644 --- a/packages/astro/src/core/request.ts +++ b/packages/astro/src/core/request.ts @@ -1,14 +1,17 @@ import type { IncomingHttpHeaders } from 'http'; type HeaderType = Headers | Record | IncomingHttpHeaders; +type RequestBody = ArrayBuffer | Blob | ReadableStream | URLSearchParams | FormData; -export function createRequest(url: URL | string, headers: HeaderType, method: string = 'GET'): Request { +export function createRequest(url: URL | string, headers: HeaderType, + method: string = 'GET', body: RequestBody | undefined = undefined): Request { let headersObj = headers instanceof Headers ? headers : new Headers(Object.entries(headers as Record)); const request = new Request(url.toString(), { method: method, - headers: headersObj + headers: headersObj, + body }); Object.defineProperties(request, { @@ -26,7 +29,5 @@ export function createRequest(url: URL | string, headers: HeaderType, method: st } }); - // TODO warn - return request; } diff --git a/packages/astro/src/vite-plugin-astro-server/index.ts b/packages/astro/src/vite-plugin-astro-server/index.ts index ec45ba8d51..53ee5644cf 100644 --- a/packages/astro/src/vite-plugin-astro-server/index.ts +++ b/packages/astro/src/vite-plugin-astro-server/index.ts @@ -128,8 +128,19 @@ async function handleRequest( } } + let body: ArrayBuffer | undefined = undefined; + if(!(req.method === 'GET' || req.method === 'HEAD')) { + let bytes: string[] = []; + await new Promise(resolve => { + req.setEncoding('utf-8'); + req.on('data', bts => bytes.push(bts)); + req.on('close', resolve); + }); + body = new TextEncoder().encode(bytes.join('')).buffer; + } + // Headers are only available when using SSR. - const request = createRequest(url, buildingToSSR ? req.headers : new Headers(), req.method); + const request = createRequest(url, buildingToSSR ? req.headers : new Headers(), req.method, body); try { if (!pathname.startsWith(devRoot)) {