summaryrefslogtreecommitdiff
path: root/examples/ssr
diff options
context:
space:
mode:
authorGravatar Matthew Phillips <matthew@skypack.dev> 2022-02-14 12:48:52 -0500
committerGravatar GitHub <noreply@github.com> 2022-02-14 12:48:52 -0500
commitba5e2b5e6c20207955991775dc4aa8879331542c (patch)
tree28e68347035a534f8b56991ede570dfcf830cb01 /examples/ssr
parent61f438fdcbab7163bc3399e623a80d283e018371 (diff)
downloadastro-ba5e2b5e6c20207955991775dc4aa8879331542c.tar.gz
astro-ba5e2b5e6c20207955991775dc4aa8879331542c.tar.zst
astro-ba5e2b5e6c20207955991775dc4aa8879331542c.zip
Flagged SSR support (#2548)
* Checkpoint, basics are working * Add the `--experimental-ssr` flag * Adds the changeset * Fixes population of getStaticPaths results * Pass through the imported module * Route manifest test * Fix remaining tests * Fix remaining tests * Copy server assets over * Fix types * Allowing passing in the request to the Node version of App * Improve the example app * Gets CI to pass
Diffstat (limited to 'examples/ssr')
-rw-r--r--examples/ssr/astro.config.mjs12
-rw-r--r--examples/ssr/build.mjs12
-rw-r--r--examples/ssr/package.json21
-rw-r--r--examples/ssr/public/images/products/cereal.jpgbin0 -> 499130 bytes
-rw-r--r--examples/ssr/public/images/products/muffins.jpgbin0 -> 142260 bytes
-rw-r--r--examples/ssr/public/images/products/oats.jpgbin0 -> 103289 bytes
-rw-r--r--examples/ssr/public/images/products/yogurt.jpgbin0 -> 80237 bytes
-rw-r--r--examples/ssr/server/api.mjs49
-rw-r--r--examples/ssr/server/db.json28
-rw-r--r--examples/ssr/server/dev-api.mjs17
-rw-r--r--examples/ssr/server/server.mjs55
-rw-r--r--examples/ssr/src/api.ts35
-rw-r--r--examples/ssr/src/components/AddToCart.svelte47
-rw-r--r--examples/ssr/src/components/Cart.svelte32
-rw-r--r--examples/ssr/src/components/Container.astro12
-rw-r--r--examples/ssr/src/components/Header.astro31
-rw-r--r--examples/ssr/src/components/ProductListing.astro61
-rw-r--r--examples/ssr/src/components/TextDecorationSkip.astro15
-rw-r--r--examples/ssr/src/pages/index.astro36
-rw-r--r--examples/ssr/src/pages/products/[id].astro55
-rw-r--r--examples/ssr/src/styles/common.css3
21 files changed, 521 insertions, 0 deletions
diff --git a/examples/ssr/astro.config.mjs b/examples/ssr/astro.config.mjs
new file mode 100644
index 000000000..7c986b97d
--- /dev/null
+++ b/examples/ssr/astro.config.mjs
@@ -0,0 +1,12 @@
+// @ts-check
+
+export default /** @type {import('astro').AstroUserConfig} */ ({
+ renderers: ['@astrojs/renderer-svelte'],
+ vite: {
+ server: {
+ proxy: {
+ '/api': 'http://localhost:8085'
+ }
+ }
+ }
+});
diff --git a/examples/ssr/build.mjs b/examples/ssr/build.mjs
new file mode 100644
index 000000000..5d2e4a3aa
--- /dev/null
+++ b/examples/ssr/build.mjs
@@ -0,0 +1,12 @@
+import {execa} from 'execa';
+
+const api = execa('npm', ['run', 'dev-api']);
+api.stdout.pipe(process.stdout);
+api.stderr.pipe(process.stderr);
+
+const build = execa('yarn', ['astro', 'build', '--experimental-ssr']);
+build.stdout.pipe(process.stdout);
+build.stderr.pipe(process.stderr);
+await build;
+
+api.kill();
diff --git a/examples/ssr/package.json b/examples/ssr/package.json
new file mode 100644
index 000000000..c783a5416
--- /dev/null
+++ b/examples/ssr/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@example/ssr",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev-api": "node server/dev-api.mjs",
+ "dev": "npm run dev-api & astro dev --experimental-ssr",
+ "start": "astro dev",
+ "build": "echo 'Run yarn build-ssr instead'",
+ "build-ssr": "node build.mjs",
+ "server": "node server/server.mjs"
+ },
+ "devDependencies": {
+ "astro": "^0.23.0-next.0",
+ "unocss": "^0.15.5",
+ "vite-imagetools": "^4.0.1"
+ },
+ "dependencies": {
+ "@astropub/webapi": "^0.10.13"
+ }
+}
diff --git a/examples/ssr/public/images/products/cereal.jpg b/examples/ssr/public/images/products/cereal.jpg
new file mode 100644
index 000000000..c1f4cce4a
--- /dev/null
+++ b/examples/ssr/public/images/products/cereal.jpg
Binary files differ
diff --git a/examples/ssr/public/images/products/muffins.jpg b/examples/ssr/public/images/products/muffins.jpg
new file mode 100644
index 000000000..897733ee8
--- /dev/null
+++ b/examples/ssr/public/images/products/muffins.jpg
Binary files differ
diff --git a/examples/ssr/public/images/products/oats.jpg b/examples/ssr/public/images/products/oats.jpg
new file mode 100644
index 000000000..b8db72ae0
--- /dev/null
+++ b/examples/ssr/public/images/products/oats.jpg
Binary files differ
diff --git a/examples/ssr/public/images/products/yogurt.jpg b/examples/ssr/public/images/products/yogurt.jpg
new file mode 100644
index 000000000..9cd39666d
--- /dev/null
+++ b/examples/ssr/public/images/products/yogurt.jpg
Binary files differ
diff --git a/examples/ssr/server/api.mjs b/examples/ssr/server/api.mjs
new file mode 100644
index 000000000..3928d0507
--- /dev/null
+++ b/examples/ssr/server/api.mjs
@@ -0,0 +1,49 @@
+import fs from 'fs';
+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]));
+
+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));
+ }
+ }
+
+]
+
+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/db.json b/examples/ssr/server/db.json
new file mode 100644
index 000000000..76f9e4da3
--- /dev/null
+++ b/examples/ssr/server/db.json
@@ -0,0 +1,28 @@
+{
+ "products": [
+ {
+ "id": 1,
+ "name": "Cereal",
+ "price": 3.99,
+ "image": "/images/products/cereal.jpg"
+ },
+ {
+ "id": 2,
+ "name": "Yogurt",
+ "price": 3.97,
+ "image": "/images/products/yogurt.jpg"
+ },
+ {
+ "id": 3,
+ "name": "Rolled Oats",
+ "price": 2.89,
+ "image": "/images/products/oats.jpg"
+ },
+ {
+ "id": 4,
+ "name": "Muffins",
+ "price": 4.39,
+ "image": "/images/products/muffins.jpg"
+ }
+ ]
+}
diff --git a/examples/ssr/server/dev-api.mjs b/examples/ssr/server/dev-api.mjs
new file mode 100644
index 000000000..74e0ef83b
--- /dev/null
+++ b/examples/ssr/server/dev-api.mjs
@@ -0,0 +1,17 @@
+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
new file mode 100644
index 000000000..6f0a0dea6
--- /dev/null
+++ b/examples/ssr/server/server.mjs
@@ -0,0 +1,55 @@
+import { createServer } from 'http';
+import fs from 'fs';
+import mime from 'mime';
+import { loadApp } from 'astro/app/node';
+import { polyfill } from '@astropub/webapi'
+import { apiHandler } from './api.mjs';
+
+polyfill(globalThis);
+
+const clientRoot = new URL('../dist/client/', import.meta.url);
+const serverRoot = new URL('../dist/server/', import.meta.url);
+const app = await loadApp(serverRoot);
+
+async function handle(req, res) {
+ const route = app.match(req);
+
+ if(route) {
+ const html = await app.render(req, route);
+
+ res.writeHead(200, {
+ 'Content-Type': 'text/html'
+ });
+ res.end(html)
+ } else 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();
+ }
+ }
+}
+
+const server = createServer((req, res) => {
+ handle(req, res).catch(err => {
+ console.error(err);
+ res.writeHead(500, {
+ 'Content-Type': 'text/plain'
+ });
+ res.end(err.toString());
+ })
+});
+
+server.listen(8085);
+console.log('Serving at http://localhost:8085');
+
+// Silence weird <time> warning
+console.error = () => {};
diff --git a/examples/ssr/src/api.ts b/examples/ssr/src/api.ts
new file mode 100644
index 000000000..9fd7d0683
--- /dev/null
+++ b/examples/ssr/src/api.ts
@@ -0,0 +1,35 @@
+interface Product {
+ id: number;
+ name: string;
+ price: number;
+ image: string;
+}
+
+//let origin: string;
+const { mode } = import.meta.env;
+const origin = mode === 'develeopment' ?
+ `http://localhost:3000` :
+ `http://localhost:8085`;
+
+async function get<T>(endpoint: string, cb: (response: Response) => Promise<T>): Promise<T> {
+ const response = await fetch(`${origin}${endpoint}`);
+ if(!response.ok) {
+ // TODO make this better...
+ return null;
+ }
+ return cb(response);
+}
+
+export async function getProducts(): Promise<Product[]> {
+ return get<Product[]>('/api/products', async response => {
+ const products: Product[] = await response.json();
+ return products;
+ });
+}
+
+export async function getProduct(id: number): Promise<Product> {
+ return get<Product>(`/api/products/${id}`, async response => {
+ const product: Product = await response.json();
+ return product;
+ });
+}
diff --git a/examples/ssr/src/components/AddToCart.svelte b/examples/ssr/src/components/AddToCart.svelte
new file mode 100644
index 000000000..b03b8180a
--- /dev/null
+++ b/examples/ssr/src/components/AddToCart.svelte
@@ -0,0 +1,47 @@
+<script>
+ export let id = 0;
+
+ function addToCart() {
+ window.dispatchEvent(new CustomEvent('add-to-cart', {
+ detail: id
+ }));
+ }
+</script>
+<style>
+ button {
+ display:block;
+ padding:0.5em 1em 0.5em 1em;
+ border-radius:100px;
+ border:none;
+ font-size: 1.4em;
+ position:relative;
+ background:#0652DD;
+ cursor:pointer;
+ height:2em;
+ width:10em;
+ overflow:hidden;
+ transition:transform 0.1s;
+ z-index:1;
+}
+button:hover {
+ transform:scale(1.1);
+}
+
+.pretext {
+ color:#fff;
+ background:#0652DD;
+ position:absolute;
+ top:0;
+ left:0;
+ height:100%;
+ width:100%;
+ display:flex;
+ justify-content:center;
+ align-items:center;
+ font-family: 'Quicksand', sans-serif;
+ text-transform: uppercase;
+}
+</style>
+<button on:click={addToCart}>
+ <span class="pretext">Add to cart</span>
+</button>
diff --git a/examples/ssr/src/components/Cart.svelte b/examples/ssr/src/components/Cart.svelte
new file mode 100644
index 000000000..63dd1b5a5
--- /dev/null
+++ b/examples/ssr/src/components/Cart.svelte
@@ -0,0 +1,32 @@
+<script>
+ export let count = 0;
+ let items = new Set();
+
+ function onAddToCart(ev) {
+ const id = ev.detail;
+ items.add(id);
+ count++;
+ }
+</script>
+<style>
+ .cart {
+ display: flex;
+ align-items: center;
+ }
+ .cart :first-child {
+ margin-right: 5px;
+ }
+
+ .cart-icon {
+ font-size: 36px;
+ }
+
+ .count {
+ font-size: 24px;
+ }
+</style>
+<svelte:window on:add-to-cart={onAddToCart}/>
+<div class="cart">
+ <span class="material-icons cart-icon">shopping_cart</span>
+ <span class="count">{count}</span>
+</div>
diff --git a/examples/ssr/src/components/Container.astro b/examples/ssr/src/components/Container.astro
new file mode 100644
index 000000000..f982522b8
--- /dev/null
+++ b/examples/ssr/src/components/Container.astro
@@ -0,0 +1,12 @@
+---
+const { tag = 'div' } = Astro.props;
+const Tag = tag;
+---
+<style>
+ .container {
+ width: 1248px; /** TODO: responsive */
+ margin-left: auto;
+ margin-right: auto;
+ }
+</style>
+<Tag class="container"><slot /></Tag>
diff --git a/examples/ssr/src/components/Header.astro b/examples/ssr/src/components/Header.astro
new file mode 100644
index 000000000..2839c70d3
--- /dev/null
+++ b/examples/ssr/src/components/Header.astro
@@ -0,0 +1,31 @@
+---
+import TextDecorationSkip from './TextDecorationSkip.astro';
+import Cart from './Cart.svelte';
+---
+<style>
+ @import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap');
+
+ header {
+ margin: 1rem 2rem;
+ display: flex;
+ justify-content: space-between;
+ }
+
+ h1 {
+ margin: 0;
+ font-family: 'Lobster', cursive;
+ color: black;
+ }
+
+ a, a:visited {
+ color: inherit;
+ text-decoration: none;
+ }
+</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 />
+ </div>
+</header>
diff --git a/examples/ssr/src/components/ProductListing.astro b/examples/ssr/src/components/ProductListing.astro
new file mode 100644
index 000000000..c0af5a34c
--- /dev/null
+++ b/examples/ssr/src/components/ProductListing.astro
@@ -0,0 +1,61 @@
+---
+const { products } = Astro.props;
+---
+<style>
+ ul {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ }
+
+ figure {
+ width: 200px;
+ padding: 7px;
+ border: 1px solid black;
+ display: flex;
+ flex-direction: column;
+ }
+
+ figure figcaption {
+ text-align: center;
+ line-height: 1.6;
+ }
+
+ figure img {
+ width: 100%;
+ height: 250px;
+ object-fit: cover;
+ }
+
+ .product a {
+ display: block;
+ text-decoration: none;
+ color: inherit;
+ }
+
+ .name {
+ font-weight: 500;
+ }
+
+ .price {
+ font-size: 90%;
+ color: #787878;
+ }
+</style>
+<slot name="title"></slot>
+<ul>
+{products.map(product => (
+ <li class="product">
+ <a href={`/products/${product.id}`}>
+ <figure>
+ <img src={product.image} />
+ <figcaption>
+ <div class="name">{product.name}</div>
+ <div class="price">${product.price}</div>
+ </figcaption>
+ </figure>
+ </a>
+ </li>
+))}
+</ul>
diff --git a/examples/ssr/src/components/TextDecorationSkip.astro b/examples/ssr/src/components/TextDecorationSkip.astro
new file mode 100644
index 000000000..b35179ea8
--- /dev/null
+++ b/examples/ssr/src/components/TextDecorationSkip.astro
@@ -0,0 +1,15 @@
+---
+const { text } = Astro.props;
+const words = text.split(' ');
+const last = words.length - 1;
+---
+<style>
+ span {
+ text-decoration: underline;
+ }
+</style>
+{words.map((word, i) => (
+ <Fragment>
+ <span>{word}</span>{i !== last && (<Fragment>&#32;</Fragment>)}
+ </Fragment>
+))}
diff --git a/examples/ssr/src/pages/index.astro b/examples/ssr/src/pages/index.astro
new file mode 100644
index 000000000..ea2c6c2f6
--- /dev/null
+++ b/examples/ssr/src/pages/index.astro
@@ -0,0 +1,36 @@
+---
+import Header from '../components/Header.astro';
+import Container from '../components/Container.astro';
+import ProductListing from '../components/ProductListing.astro';
+import { getProducts } from '../api';
+import '../styles/common.css';
+
+const products = await getProducts();
+---
+<html>
+<head>
+ <title>Online Store</title>
+ <style>
+ h1 {
+ font-size: 36px;
+ }
+
+ .product-listing-title {
+ text-align: center;
+ }
+
+ .product-listing {
+
+ }
+ </style>
+</head>
+<body>
+ <Header />
+
+ <Container tag="main">
+ <ProductListing products={products} class="product-listing">
+ <h2 class="product-listing-title" slot="title">Product Listing</h2>
+ </ProductListing>
+ </Container>
+</body>
+</html>
diff --git a/examples/ssr/src/pages/products/[id].astro b/examples/ssr/src/pages/products/[id].astro
new file mode 100644
index 000000000..943f2ab84
--- /dev/null
+++ b/examples/ssr/src/pages/products/[id].astro
@@ -0,0 +1,55 @@
+---
+import Header from '../../components/Header.astro';
+import Container from '../../components/Container.astro';
+import AddToCart from '../../components/AddToCart.svelte';
+import { getProducts, getProduct } from '../../api';
+import '../../styles/common.css';
+
+export async function getStaticPaths() {
+ const products = await getProducts();
+ return products.map(product => {
+ return {
+ params: { id: product.id.toString() }
+ }
+ });
+}
+
+const id = Number(Astro.request.params.id);
+const product = await getProduct(id);
+---
+
+<html lang="en">
+<head>
+ <title>{product.name} | Online Store</title>
+ <style>
+ h2 {
+ text-align: center;
+ font-size: 3.5rem;
+ }
+
+ figure {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ }
+
+ img {
+ width: 400px;
+ }
+ </style>
+</head>
+<body>
+ <Header />
+
+ <Container tag="article">
+ <h2>{product.name}</h2>
+ <figure>
+ <img src={product.image} />
+ <figcaption>
+ <AddToCart id={id} client:idle />
+ <p>Description here...</p>
+ </figcaption>
+ </figure>
+
+ </Container>
+</body>
+</html>
diff --git a/examples/ssr/src/styles/common.css b/examples/ssr/src/styles/common.css
new file mode 100644
index 000000000..7879df33c
--- /dev/null
+++ b/examples/ssr/src/styles/common.css
@@ -0,0 +1,3 @@
+body {
+ font-family: "GT America Standard", "Helvetica Neue", Helvetica,Arial,sans-serif;
+}