Skip to content

Commit

Permalink
Flagged SSR support (withastro#2548)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
matthewp committed Feb 14, 2022
1 parent 61f438f commit ba5e2b5
Show file tree
Hide file tree
Showing 67 changed files with 1,684 additions and 627 deletions.
29 changes: 29 additions & 0 deletions .changeset/slow-islands-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'astro': patch
---

Experimental SSR Support

> ⚠️ If you are a user of Astro and see this PR and think that you can start deploying your app to a server and get SSR, slow down a second! This is only the initial flag and **very basic support**. Styles are not loading correctly at this point, for example. Like we did with the `--experimental-static-build` flag, this feature will be refined over the next few weeks/months and we'll let you know when its ready for community testing.
## Changes

- This adds a new `--experimental-ssr` flag to `astro build` which will result in `dist/server/` and `dist/client/` directories.
- SSR can be used through this API:
```js
import { createServer } from 'http';
import { loadApp } from 'astro/app/node';

const app = await loadApp(new URL('./dist/server/', import.meta.url));

createServer((req, res) => {
const route = app.match(req);
if(route) {
let html = await app.render(req, route);
}

}).listen(8080);
```
- This API will be refined over time.
- This only works in Node.js at the moment.
- Many features will likely not work correctly, but rendering HTML at least should.
Empty file added comp.txt
Empty file.
2 changes: 1 addition & 1 deletion examples/fast-build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"devDependencies": {
"astro": "^0.23.0-next.6",
"preact": "~10.5.15",
"preact": "~10.6.5",
"unocss": "^0.15.5",
"vite-imagetools": "^4.0.1"
}
Expand Down
12 changes: 12 additions & 0 deletions examples/ssr/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// @ts-check

export default /** @type {import('astro').AstroUserConfig} */ ({
renderers: ['@astrojs/renderer-svelte'],
vite: {
server: {
proxy: {
'/api': 'http:https://localhost:8085'
}
}
}
});
12 changes: 12 additions & 0 deletions examples/ssr/build.mjs
Original file line number Diff line number Diff line change
@@ -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();
21 changes: 21 additions & 0 deletions examples/ssr/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Binary file added examples/ssr/public/images/products/cereal.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/ssr/public/images/products/muffins.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/ssr/public/images/products/oats.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/ssr/public/images/products/yogurt.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions examples/ssr/server/api.mjs
Original file line number Diff line number Diff line change
@@ -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');
}
28 changes: 28 additions & 0 deletions examples/ssr/server/db.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
17 changes: 17 additions & 0 deletions examples/ssr/server/dev-api.mjs
Original file line number Diff line number Diff line change
@@ -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:https://localhost:${PORT}`);
55 changes: 55 additions & 0 deletions examples/ssr/server/server.mjs
Original file line number Diff line number Diff line change
@@ -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:https://localhost:8085');

// Silence weird <time> warning
console.error = () => {};
35 changes: 35 additions & 0 deletions examples/ssr/src/api.ts
Original file line number Diff line number Diff line change
@@ -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:https://localhost:3000` :
`http:https://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;
});
}
47 changes: 47 additions & 0 deletions examples/ssr/src/components/AddToCart.svelte
Original file line number Diff line number Diff line change
@@ -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>
32 changes: 32 additions & 0 deletions examples/ssr/src/components/Cart.svelte
Original file line number Diff line number Diff line change
@@ -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>
12 changes: 12 additions & 0 deletions examples/ssr/src/components/Container.astro
Original file line number Diff line number Diff line change
@@ -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>
Loading

0 comments on commit ba5e2b5

Please sign in to comment.