Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flagged SSR support #2548

Merged
merged 14 commits into from
Feb 14, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.4",
"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': '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 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 https://localhost:8085');

// Silence weird <time> warning
console.error = () => {};
matthewp marked this conversation as resolved.
Show resolved Hide resolved
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' ?
`https://localhost:3000` :
`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