From ecbcc8c42cab4b043821bbc31574397fdbf9d481 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 29 Mar 2022 08:18:11 -0400 Subject: [PATCH] Make it deployable to Netlify (#2931) --- examples/ssr/astro.config.mjs | 13 --- examples/ssr/package.json | 4 +- examples/ssr/server/api.mjs | 100 ------------------ examples/ssr/server/dev-api.mjs | 17 --- examples/ssr/server/server.mjs | 33 +++--- examples/ssr/src/api.ts | 2 +- examples/ssr/{server => src/models}/db.json | 0 examples/ssr/src/models/db.ts | 9 ++ examples/ssr/src/models/session.ts | 3 + examples/ssr/src/pages/api/cart.ts | 47 ++++++++ examples/ssr/src/pages/api/products.ts | 7 ++ examples/ssr/src/pages/api/products/[id].ts | 17 +++ examples/ssr/tsconfig.json | 1 + .../astro/src/core/build/vite-plugin-ssr.ts | 1 + packages/astro/src/core/request.ts | 5 +- .../src/vite-plugin-astro-server/index.ts | 12 +++ .../fixtures/ssr-dynamic/src/pages/[id].astro | 1 + packages/astro/test/ssr-dynamic.test.js | 15 ++- 18 files changed, 133 insertions(+), 154 deletions(-) delete mode 100644 examples/ssr/server/api.mjs delete mode 100644 examples/ssr/server/dev-api.mjs rename examples/ssr/{server => src/models}/db.json (100%) create mode 100644 examples/ssr/src/models/db.ts create mode 100644 examples/ssr/src/models/session.ts create mode 100644 examples/ssr/src/pages/api/cart.ts create mode 100644 examples/ssr/src/pages/api/products.ts create mode 100644 examples/ssr/src/pages/api/products/[id].ts diff --git a/examples/ssr/astro.config.mjs b/examples/ssr/astro.config.mjs index f6aba20cef77..448d5829d161 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 da28565704db..401f7e06a6dd 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 589766ee9f24..000000000000 --- 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 305ac609b719..000000000000 --- 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 bed49b749f0e..9838d7ada5eb 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 40058360b61b..64dfe17d31d9 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 000000000000..d9caa8b030f9 --- /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 000000000000..60ca8f1da485 --- /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 000000000000..5dbe5acbdb3e --- /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 000000000000..533bdef23e99 --- /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 000000000000..6a3a83722bb3 --- /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 e0065a323c00..ee0432bb3be4 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/build/vite-plugin-ssr.ts b/packages/astro/src/core/build/vite-plugin-ssr.ts index efa54cc0131f..f057dc2ceb42 100644 --- a/packages/astro/src/core/build/vite-plugin-ssr.ts +++ b/packages/astro/src/core/build/vite-plugin-ssr.ts @@ -18,6 +18,7 @@ const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@'; export function vitePluginSSR(buildOpts: StaticBuildOptions, internals: BuildInternals, adapter: AstroAdapter): VitePlugin { return { name: '@astrojs/vite-plugin-astro-ssr', + enforce: 'post', options(opts) { return addRollupInput(opts, [virtualModuleId]); }, diff --git a/packages/astro/src/core/request.ts b/packages/astro/src/core/request.ts index 4b9766b5bb68..6594621a110d 100644 --- a/packages/astro/src/core/request.ts +++ b/packages/astro/src/core/request.ts @@ -3,20 +3,23 @@ import type { LogOptions } from './logger'; import { warn } from './logger.js'; type HeaderType = Headers | Record | IncomingHttpHeaders; +type RequestBody = ArrayBuffer | Blob | ReadableStream | URLSearchParams | FormData; export interface CreateRequestOptions { url: URL | string; headers: HeaderType; method?: string; + body?: RequestBody | undefined; logging: LogOptions; } -export function createRequest({ url, headers, method = 'GET', logging }: CreateRequestOptions): Request { +export function createRequest({ url, headers, method = 'GET', body = undefined, logging }: CreateRequestOptions): Request { let headersObj = headers instanceof Headers ? headers : new Headers(Object.entries(headers as Record)); const request = new Request(url.toString(), { method: method, headers: headersObj, + body }); Object.defineProperties(request, { diff --git a/packages/astro/src/vite-plugin-astro-server/index.ts b/packages/astro/src/vite-plugin-astro-server/index.ts index 747765fec1b6..250122d7bcf5 100644 --- a/packages/astro/src/vite-plugin-astro-server/index.ts +++ b/packages/astro/src/vite-plugin-astro-server/index.ts @@ -128,11 +128,23 @@ 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, headers: buildingToSSR ? req.headers : new Headers(), method: req.method, + body, logging, }); diff --git a/packages/astro/test/fixtures/ssr-dynamic/src/pages/[id].astro b/packages/astro/test/fixtures/ssr-dynamic/src/pages/[id].astro index e64626172973..8ba5cc82ebcd 100644 --- a/packages/astro/test/fixtures/ssr-dynamic/src/pages/[id].astro +++ b/packages/astro/test/fixtures/ssr-dynamic/src/pages/[id].astro @@ -4,6 +4,7 @@ const val = Number(Astro.params.id); Test app +

Item { val }

diff --git a/packages/astro/test/ssr-dynamic.test.js b/packages/astro/test/ssr-dynamic.test.js index 843243425ded..d938e5c95aef 100644 --- a/packages/astro/test/ssr-dynamic.test.js +++ b/packages/astro/test/ssr-dynamic.test.js @@ -19,12 +19,23 @@ describe('Dynamic pages in SSR', () => { await fixture.build(); }); - it('Do not have to implement getStaticPaths', async () => { + async function fetchHTML(path) { const app = await fixture.loadTestAdapterApp(); - const request = new Request('http://example.com/123'); + const request = new Request('http://example.com' + path); const response = await app.render(request); const html = await response.text(); + return html; + } + + it('Do not have to implement getStaticPaths', async () => { + const html = await fetchHTML('/123'); const $ = cheerioLoad(html); expect($('h1').text()).to.equal('Item 123'); }); + + it('Includes page styles', async () => { + const html = await fetchHTML('/123'); + const $ = cheerioLoad(html); + expect($('link').length).to.equal(1); + }); });