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
This commit is contained in:
parent
61f438fdcb
commit
ba5e2b5e6c
67 changed files with 1684 additions and 627 deletions
29
.changeset/slow-islands-fix.md
Normal file
29
.changeset/slow-islands-fix.md
Normal file
|
@ -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.
|
0
comp.txt
Normal file
0
comp.txt
Normal file
|
@ -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"
|
||||
}
|
||||
|
|
12
examples/ssr/astro.config.mjs
Normal file
12
examples/ssr/astro.config.mjs
Normal file
|
@ -0,0 +1,12 @@
|
|||
// @ts-check
|
||||
|
||||
export default /** @type {import('astro').AstroUserConfig} */ ({
|
||||
renderers: ['@astrojs/renderer-svelte'],
|
||||
vite: {
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': 'http://localhost:8085'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
12
examples/ssr/build.mjs
Normal file
12
examples/ssr/build.mjs
Normal file
|
@ -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
examples/ssr/package.json
Normal file
21
examples/ssr/package.json
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
BIN
examples/ssr/public/images/products/cereal.jpg
Normal file
BIN
examples/ssr/public/images/products/cereal.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 487 KiB |
BIN
examples/ssr/public/images/products/muffins.jpg
Normal file
BIN
examples/ssr/public/images/products/muffins.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 139 KiB |
BIN
examples/ssr/public/images/products/oats.jpg
Normal file
BIN
examples/ssr/public/images/products/oats.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 101 KiB |
BIN
examples/ssr/public/images/products/yogurt.jpg
Normal file
BIN
examples/ssr/public/images/products/yogurt.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 78 KiB |
49
examples/ssr/server/api.mjs
Normal file
49
examples/ssr/server/api.mjs
Normal file
|
@ -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
examples/ssr/server/db.json
Normal file
28
examples/ssr/server/db.json
Normal file
|
@ -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
examples/ssr/server/dev-api.mjs
Normal file
17
examples/ssr/server/dev-api.mjs
Normal file
|
@ -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}`);
|
55
examples/ssr/server/server.mjs
Normal file
55
examples/ssr/server/server.mjs
Normal file
|
@ -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 = () => {};
|
35
examples/ssr/src/api.ts
Normal file
35
examples/ssr/src/api.ts
Normal file
|
@ -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;
|
||||
});
|
||||
}
|
47
examples/ssr/src/components/AddToCart.svelte
Normal file
47
examples/ssr/src/components/AddToCart.svelte
Normal file
|
@ -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
examples/ssr/src/components/Cart.svelte
Normal file
32
examples/ssr/src/components/Cart.svelte
Normal file
|
@ -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
examples/ssr/src/components/Container.astro
Normal file
12
examples/ssr/src/components/Container.astro
Normal file
|
@ -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>
|
31
examples/ssr/src/components/Header.astro
Normal file
31
examples/ssr/src/components/Header.astro
Normal file
|
@ -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>
|
61
examples/ssr/src/components/ProductListing.astro
Normal file
61
examples/ssr/src/components/ProductListing.astro
Normal file
|
@ -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>
|
15
examples/ssr/src/components/TextDecorationSkip.astro
Normal file
15
examples/ssr/src/components/TextDecorationSkip.astro
Normal file
|
@ -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> </Fragment>)}
|
||||
</Fragment>
|
||||
))}
|
36
examples/ssr/src/pages/index.astro
Normal file
36
examples/ssr/src/pages/index.astro
Normal file
|
@ -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>
|
55
examples/ssr/src/pages/products/[id].astro
Normal file
55
examples/ssr/src/pages/products/[id].astro
Normal file
|
@ -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>
|
3
examples/ssr/src/styles/common.css
Normal file
3
examples/ssr/src/styles/common.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
body {
|
||||
font-family: "GT America Standard", "Helvetica Neue", Helvetica,Arial,sans-serif;
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
"types": "./dist/types/@types/astro.d.ts",
|
||||
"exports": {
|
||||
".": "./astro.js",
|
||||
"./app/node": "./dist/core/app/node.js",
|
||||
"./client/*": "./dist/runtime/client/*",
|
||||
"./components": "./components/index.js",
|
||||
"./components/*": "./components/*",
|
||||
|
|
|
@ -28,6 +28,7 @@ export interface CLIFlags {
|
|||
port?: number;
|
||||
config?: string;
|
||||
experimentalStaticBuild?: boolean;
|
||||
experimentalSsr?: boolean;
|
||||
drafts?: boolean;
|
||||
}
|
||||
|
||||
|
@ -102,7 +103,7 @@ export interface AstroUserConfig {
|
|||
renderers?: string[];
|
||||
/** Options for rendering markdown content */
|
||||
markdownOptions?: {
|
||||
render?: [string | MarkdownParser, Record<string, any>];
|
||||
render?: MarkdownRenderOptions;
|
||||
};
|
||||
/** Options specific to `astro build` */
|
||||
buildOptions?: {
|
||||
|
@ -132,6 +133,10 @@ export interface AstroUserConfig {
|
|||
* Default: false
|
||||
*/
|
||||
experimentalStaticBuild?: boolean;
|
||||
/**
|
||||
* Enable a build for SSR support.
|
||||
*/
|
||||
experimentalSsr?: boolean;
|
||||
};
|
||||
/** Options for the development server run with `astro dev`. */
|
||||
devOptions?: {
|
||||
|
@ -224,6 +229,7 @@ export interface ManifestData {
|
|||
routes: RouteData[];
|
||||
}
|
||||
|
||||
export type MarkdownRenderOptions = [string | MarkdownParser, Record<string, any>];
|
||||
export type MarkdownParser = (contents: string, options?: Record<string, any>) => MarkdownParserResponse | PromiseLike<MarkdownParserResponse>;
|
||||
|
||||
export interface MarkdownParserResponse {
|
||||
|
@ -341,6 +347,11 @@ export interface RouteData {
|
|||
type: 'page';
|
||||
}
|
||||
|
||||
export type SerializedRouteData = Omit<RouteData, 'generate' | 'pattern'> & {
|
||||
generate: undefined;
|
||||
pattern: string;
|
||||
};
|
||||
|
||||
export type RuntimeMode = 'development' | 'production';
|
||||
|
||||
/**
|
||||
|
|
|
@ -31,6 +31,7 @@ function printHelp() {
|
|||
--project-root <path> Specify the path to the project root folder.
|
||||
--no-sitemap Disable sitemap generation (build only).
|
||||
--experimental-static-build A more performant build that expects assets to be define statically.
|
||||
--experimental-ssr Enable SSR compilation.
|
||||
--drafts Include markdown draft pages in the build.
|
||||
--verbose Enable verbose logging
|
||||
--silent Disable logging
|
||||
|
|
20
packages/astro/src/core/app/common.ts
Normal file
20
packages/astro/src/core/app/common.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import type { SSRManifest, SerializedSSRManifest, RouteInfo } from './types';
|
||||
import { deserializeRouteData } from '../routing/manifest/serialization.js';
|
||||
|
||||
export function deserializeManifest(serializedManifest: SerializedSSRManifest): SSRManifest {
|
||||
const routes: RouteInfo[] = [];
|
||||
for(const serializedRoute of serializedManifest.routes) {
|
||||
routes.push({
|
||||
...serializedRoute,
|
||||
routeData: deserializeRouteData(serializedRoute.routeData)
|
||||
});
|
||||
|
||||
const route = serializedRoute as unknown as RouteInfo;
|
||||
route.routeData = deserializeRouteData(serializedRoute.routeData);
|
||||
}
|
||||
|
||||
return {
|
||||
...serializedManifest,
|
||||
routes
|
||||
};
|
||||
}
|
100
packages/astro/src/core/app/index.ts
Normal file
100
packages/astro/src/core/app/index.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
import type { ComponentInstance, ManifestData, RouteData, Renderer } from '../../@types/astro';
|
||||
import type {
|
||||
SSRManifest as Manifest, RouteInfo
|
||||
} from './types';
|
||||
|
||||
import { defaultLogOptions } from '../logger.js';
|
||||
import { matchRoute } from '../routing/match.js';
|
||||
import { render } from '../render/core.js';
|
||||
import { RouteCache } from '../render/route-cache.js';
|
||||
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
|
||||
import { createRenderer } from '../render/renderer.js';
|
||||
import { prependForwardSlash } from '../path.js';
|
||||
|
||||
export class App {
|
||||
#manifest: Manifest;
|
||||
#manifestData: ManifestData;
|
||||
#rootFolder: URL;
|
||||
#routeDataToRouteInfo: Map<RouteData, RouteInfo>;
|
||||
#routeCache: RouteCache;
|
||||
#renderersPromise: Promise<Renderer[]>;
|
||||
|
||||
constructor(manifest: Manifest, rootFolder: URL) {
|
||||
this.#manifest = manifest;
|
||||
this.#manifestData = {
|
||||
routes: manifest.routes.map(route => route.routeData)
|
||||
};
|
||||
this.#rootFolder = rootFolder;
|
||||
this.#routeDataToRouteInfo = new Map(
|
||||
manifest.routes.map(route => [route.routeData, route])
|
||||
);
|
||||
this.#routeCache = new RouteCache(defaultLogOptions);
|
||||
this.#renderersPromise = this.#loadRenderers();
|
||||
}
|
||||
match({ pathname }: URL): RouteData | undefined {
|
||||
return matchRoute(pathname, this.#manifestData);
|
||||
}
|
||||
async render(url: URL, routeData?: RouteData): Promise<string> {
|
||||
if(!routeData) {
|
||||
routeData = this.match(url);
|
||||
if(!routeData) {
|
||||
return 'Not found';
|
||||
}
|
||||
}
|
||||
|
||||
const manifest = this.#manifest;
|
||||
const info = this.#routeDataToRouteInfo.get(routeData!)!;
|
||||
const [mod, renderers] = await Promise.all([
|
||||
this.#loadModule(info.file),
|
||||
this.#renderersPromise
|
||||
]);
|
||||
|
||||
const links = createLinkStylesheetElementSet(info.links, manifest.site);
|
||||
const scripts = createModuleScriptElementWithSrcSet(info.scripts, manifest.site);
|
||||
|
||||
return render({
|
||||
experimentalStaticBuild: true,
|
||||
links,
|
||||
logging: defaultLogOptions,
|
||||
markdownRender: manifest.markdown.render,
|
||||
mod,
|
||||
origin: url.origin,
|
||||
pathname: url.pathname,
|
||||
scripts,
|
||||
renderers,
|
||||
async resolve(specifier: string) {
|
||||
if(!(specifier in manifest.entryModules)) {
|
||||
throw new Error(`Unable to resolve [${specifier}]`);
|
||||
}
|
||||
const bundlePath = manifest.entryModules[specifier];
|
||||
return prependForwardSlash(bundlePath);
|
||||
},
|
||||
route: routeData,
|
||||
routeCache: this.#routeCache,
|
||||
site: this.#manifest.site
|
||||
})
|
||||
}
|
||||
async #loadRenderers(): Promise<Renderer[]> {
|
||||
const rendererNames = this.#manifest.renderers;
|
||||
return await Promise.all(rendererNames.map(async (rendererName) => {
|
||||
return createRenderer(rendererName, {
|
||||
renderer(name) {
|
||||
return import(name);
|
||||
},
|
||||
server(entry) {
|
||||
return import(entry);
|
||||
}
|
||||
})
|
||||
}));
|
||||
}
|
||||
async #loadModule(rootRelativePath: string): Promise<ComponentInstance> {
|
||||
let modUrl = new URL(rootRelativePath, this.#rootFolder).toString();
|
||||
let mod: ComponentInstance;
|
||||
try {
|
||||
mod = await import(modUrl);
|
||||
return mod;
|
||||
} catch(err) {
|
||||
throw new Error(`Unable to import ${modUrl}. Does this file exist?`);
|
||||
}
|
||||
}
|
||||
}
|
31
packages/astro/src/core/app/node.ts
Normal file
31
packages/astro/src/core/app/node.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import type { SSRManifest, SerializedSSRManifest } from './types';
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { App } from './index.js';
|
||||
import { deserializeManifest } from './common.js';
|
||||
import { IncomingMessage } from 'http';
|
||||
|
||||
function createURLFromRequest(req: IncomingMessage): URL {
|
||||
return new URL(`http://${req.headers.host}${req.url}`);
|
||||
}
|
||||
|
||||
class NodeApp extends App {
|
||||
match(req: IncomingMessage | URL) {
|
||||
return super.match(req instanceof URL ? req : createURLFromRequest(req));
|
||||
}
|
||||
render(req: IncomingMessage | URL) {
|
||||
return super.render(req instanceof URL ? req : createURLFromRequest(req));
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadManifest(rootFolder: URL): Promise<SSRManifest> {
|
||||
const manifestFile = new URL('./manifest.json', rootFolder);
|
||||
const rawManifest = await fs.promises.readFile(manifestFile, 'utf-8');
|
||||
const serializedManifest: SerializedSSRManifest = JSON.parse(rawManifest);
|
||||
return deserializeManifest(serializedManifest);
|
||||
}
|
||||
|
||||
export async function loadApp(rootFolder: URL): Promise<NodeApp> {
|
||||
const manifest = await loadManifest(rootFolder);
|
||||
return new NodeApp(manifest, rootFolder);
|
||||
}
|
26
packages/astro/src/core/app/types.ts
Normal file
26
packages/astro/src/core/app/types.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import type { RouteData, SerializedRouteData, MarkdownRenderOptions } from '../../@types/astro';
|
||||
|
||||
export interface RouteInfo {
|
||||
routeData: RouteData
|
||||
file: string;
|
||||
links: string[];
|
||||
scripts: string[];
|
||||
}
|
||||
|
||||
export type SerializedRouteInfo = Omit<RouteInfo, 'routeData'> & {
|
||||
routeData: SerializedRouteData;
|
||||
}
|
||||
|
||||
export interface SSRManifest {
|
||||
routes: RouteInfo[];
|
||||
site?: string;
|
||||
markdown: {
|
||||
render: MarkdownRenderOptions
|
||||
},
|
||||
renderers: string[];
|
||||
entryModules: Record<string, string>;
|
||||
}
|
||||
|
||||
export type SerializedSSRManifest = Omit<SSRManifest, 'routes'> & {
|
||||
routes: SerializedRouteInfo[];
|
||||
}
|
|
@ -8,12 +8,12 @@ import { performance } from 'perf_hooks';
|
|||
import vite, { ViteDevServer } from '../vite.js';
|
||||
import { createVite, ViteConfigWithSSR } from '../create-vite.js';
|
||||
import { debug, defaultLogOptions, info, levels, timerMessage, warn } from '../logger.js';
|
||||
import { createRouteManifest } from '../ssr/routing.js';
|
||||
import { generateSitemap } from '../ssr/sitemap.js';
|
||||
import { createRouteManifest } from '../routing/index.js';
|
||||
import { generateSitemap } from '../render/sitemap.js';
|
||||
import { collectPagesData } from './page-data.js';
|
||||
import { build as scanBasedBuild } from './scan-based-build.js';
|
||||
import { staticBuild } from './static-build.js';
|
||||
import { RouteCache } from '../ssr/route-cache.js';
|
||||
import { RouteCache } from '../render/route-cache.js';
|
||||
|
||||
export interface BuildOptions {
|
||||
mode?: string;
|
||||
|
@ -115,6 +115,7 @@ class AstroBuilder {
|
|||
allPages,
|
||||
astroConfig: this.config,
|
||||
logging: this.logging,
|
||||
manifest: this.manifest,
|
||||
origin: this.origin,
|
||||
pageNames,
|
||||
routeCache: this.routeCache,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { AstroConfig, ComponentInstance, ManifestData, RouteData, RSSResult } from '../../@types/astro';
|
||||
import type { AstroConfig, ComponentInstance, ManifestData, RouteData } from '../../@types/astro';
|
||||
import type { AllPagesData } from './types';
|
||||
import type { LogOptions } from '../logger';
|
||||
import type { ViteDevServer } from '../vite.js';
|
||||
|
@ -6,9 +6,9 @@ import type { ViteDevServer } from '../vite.js';
|
|||
import { fileURLToPath } from 'url';
|
||||
import * as colors from 'kleur/colors';
|
||||
import { debug } from '../logger.js';
|
||||
import { preload as ssrPreload } from '../ssr/index.js';
|
||||
import { generateRssFunction } from '../ssr/rss.js';
|
||||
import { callGetStaticPaths, RouteCache, RouteCacheEntry } from '../ssr/route-cache.js';
|
||||
import { preload as ssrPreload } from '../render/dev/index.js';
|
||||
import { generateRssFunction } from '../render/rss.js';
|
||||
import { callGetStaticPaths, RouteCache, RouteCacheEntry } from '../render/route-cache.js';
|
||||
|
||||
export interface CollectPagesDataOptions {
|
||||
astroConfig: AstroConfig;
|
||||
|
|
|
@ -9,7 +9,7 @@ import vite from '../vite.js';
|
|||
import { createBuildInternals } from '../../core/build/internal.js';
|
||||
import { rollupPluginAstroBuildHTML } from '../../vite-plugin-build-html/index.js';
|
||||
import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js';
|
||||
import { RouteCache } from '../ssr/route-cache.js';
|
||||
import { RouteCache } from '../render/route-cache.js';
|
||||
|
||||
export interface ScanBasedBuildOptions {
|
||||
allPages: AllPagesData;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import type { OutputChunk, OutputAsset, PreRenderedChunk, RollupOutput } from 'rollup';
|
||||
import type { Plugin as VitePlugin, UserConfig } from '../vite';
|
||||
import type { AstroConfig, Renderer, SSRElement } from '../../@types/astro';
|
||||
import type { OutputChunk, OutputAsset, RollupOutput } from 'rollup';
|
||||
import type { Plugin as VitePlugin, UserConfig, Manifest as ViteManifest } from '../vite';
|
||||
import type { AstroConfig, ComponentInstance, ManifestData, Renderer } from '../../@types/astro';
|
||||
import type { AllPagesData } from './types';
|
||||
import type { LogOptions } from '../logger';
|
||||
import type { ViteConfigWithSSR } from '../create-vite';
|
||||
import type { PageBuildData } from './types';
|
||||
import type { BuildInternals } from '../../core/build/internal.js';
|
||||
import type { AstroComponentFactory } from '../../runtime/server';
|
||||
import type { SerializedSSRManifest, SerializedRouteInfo } from '../app/types';
|
||||
|
||||
import fs from 'fs';
|
||||
import npath from 'path';
|
||||
|
@ -17,17 +17,18 @@ import { debug, error } from '../../core/logger.js';
|
|||
import { prependForwardSlash, appendForwardSlash } from '../../core/path.js';
|
||||
import { createBuildInternals } from '../../core/build/internal.js';
|
||||
import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js';
|
||||
import { getParamsAndProps } from '../ssr/index.js';
|
||||
import { createResult } from '../ssr/result.js';
|
||||
import { renderPage } from '../../runtime/server/index.js';
|
||||
import { prepareOutDir } from './fs.js';
|
||||
import { emptyDir, prepareOutDir } from './fs.js';
|
||||
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
|
||||
import { RouteCache } from '../ssr/route-cache.js';
|
||||
import { RouteCache } from '../render/route-cache.js';
|
||||
import { serializeRouteData } from '../routing/index.js';
|
||||
import { render } from '../render/core.js';
|
||||
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
|
||||
|
||||
export interface StaticBuildOptions {
|
||||
allPages: AllPagesData;
|
||||
astroConfig: AstroConfig;
|
||||
logging: LogOptions;
|
||||
manifest: ManifestData;
|
||||
origin: string;
|
||||
pageNames: string[];
|
||||
routeCache: RouteCache;
|
||||
|
@ -41,6 +42,12 @@ function addPageName(pathname: string, opts: StaticBuildOptions): void {
|
|||
opts.pageNames.push(pathname.replace(/\/?$/, pathrepl).replace(/^\//, ''));
|
||||
}
|
||||
|
||||
// Gives back a facadeId that is relative to the root.
|
||||
// ie, src/pages/index.astro instead of /Users/name..../src/pages/index.astro
|
||||
function rootRelativeFacadeId(facadeId: string, astroConfig: AstroConfig): string {
|
||||
return facadeId.slice(fileURLToPath(astroConfig.projectRoot).length);
|
||||
}
|
||||
|
||||
// Determines of a Rollup chunk is an entrypoint page.
|
||||
function chunkIsPage(astroConfig: AstroConfig, output: OutputAsset | OutputChunk, internals: BuildInternals) {
|
||||
if (output.type !== 'chunk') {
|
||||
|
@ -48,7 +55,7 @@ function chunkIsPage(astroConfig: AstroConfig, output: OutputAsset | OutputChunk
|
|||
}
|
||||
const chunk = output as OutputChunk;
|
||||
if (chunk.facadeModuleId) {
|
||||
const facadeToEntryId = prependForwardSlash(chunk.facadeModuleId.slice(fileURLToPath(astroConfig.projectRoot).length));
|
||||
const facadeToEntryId = prependForwardSlash(rootRelativeFacadeId(chunk.facadeModuleId, astroConfig));
|
||||
return internals.entrySpecifierToBundleMap.has(facadeToEntryId);
|
||||
}
|
||||
return false;
|
||||
|
@ -88,6 +95,9 @@ function getByFacadeId<T>(facadeId: string, map: Map<string, T>): T | undefined
|
|||
export async function staticBuild(opts: StaticBuildOptions) {
|
||||
const { allPages, astroConfig } = opts;
|
||||
|
||||
// Basic options
|
||||
const staticMode = !astroConfig.buildOptions.experimentalSsr;
|
||||
|
||||
// The pages to be built for rendering purposes.
|
||||
const pageInput = new Set<string>();
|
||||
|
||||
|
@ -148,26 +158,38 @@ export async function staticBuild(opts: StaticBuildOptions) {
|
|||
// Run the SSR build and client build in parallel
|
||||
const [ssrResult] = (await Promise.all([ssrBuild(opts, internals, pageInput), clientBuild(opts, internals, jsInput)])) as RollupOutput[];
|
||||
|
||||
// Generate each of the pages.
|
||||
await generatePages(ssrResult, opts, internals, facadeIdToPageDataMap);
|
||||
await cleanSsrOutput(opts);
|
||||
// SSG mode, generate pages.
|
||||
if(staticMode) {
|
||||
// Generate each of the pages.
|
||||
await generatePages(ssrResult, opts, internals, facadeIdToPageDataMap);
|
||||
await cleanSsrOutput(opts);
|
||||
} else {
|
||||
await generateManifest(ssrResult, opts, internals);
|
||||
await ssrMoveAssets(opts);
|
||||
}
|
||||
}
|
||||
|
||||
async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, input: Set<string>) {
|
||||
const { astroConfig, viteConfig } = opts;
|
||||
const ssr = astroConfig.buildOptions.experimentalSsr;
|
||||
const out = ssr ? getServerRoot(astroConfig) : getOutRoot(astroConfig);
|
||||
|
||||
return await vite.build({
|
||||
logLevel: 'error',
|
||||
mode: 'production',
|
||||
build: {
|
||||
emptyOutDir: false,
|
||||
manifest: ssr,
|
||||
minify: false,
|
||||
outDir: fileURLToPath(getOutRoot(astroConfig)),
|
||||
outDir: fileURLToPath(out),
|
||||
ssr: true,
|
||||
rollupOptions: {
|
||||
input: Array.from(input),
|
||||
output: {
|
||||
format: 'esm',
|
||||
entryFileNames: '[name].[hash].mjs',
|
||||
chunkFileNames: 'chunks/[name].[hash].mjs',
|
||||
assetFileNames: 'assets/[name].[hash][extname]'
|
||||
},
|
||||
},
|
||||
target: 'esnext', // must match an esbuild target
|
||||
|
@ -179,7 +201,7 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
|
|||
}),
|
||||
...(viteConfig.plugins || []),
|
||||
],
|
||||
publicDir: viteConfig.publicDir,
|
||||
publicDir: ssr ? false : viteConfig.publicDir,
|
||||
root: viteConfig.root,
|
||||
envPrefix: 'PUBLIC_',
|
||||
server: viteConfig.server,
|
||||
|
@ -196,17 +218,23 @@ async function clientBuild(opts: StaticBuildOptions, internals: BuildInternals,
|
|||
return null;
|
||||
}
|
||||
|
||||
const out = astroConfig.buildOptions.experimentalSsr ? getClientRoot(astroConfig) : getOutRoot(astroConfig);
|
||||
|
||||
return await vite.build({
|
||||
logLevel: 'error',
|
||||
mode: 'production',
|
||||
build: {
|
||||
emptyOutDir: false,
|
||||
minify: 'esbuild',
|
||||
outDir: fileURLToPath(getOutRoot(astroConfig)),
|
||||
outDir: fileURLToPath(out),
|
||||
rollupOptions: {
|
||||
input: Array.from(input),
|
||||
output: {
|
||||
format: 'esm',
|
||||
entryFileNames: '[name].[hash].js',
|
||||
chunkFileNames: 'chunks/[name].[hash].js',
|
||||
assetFileNames: 'assets/[name].[hash][extname]'
|
||||
|
||||
},
|
||||
preserveEntrySignatures: 'exports-only',
|
||||
},
|
||||
|
@ -285,14 +313,13 @@ async function generatePage(output: OutputChunk, opts: StaticBuildOptions, inter
|
|||
const hoistedId = getByFacadeId<string>(facadeId, internals.facadeIdToHoistedEntryMap) || null;
|
||||
|
||||
let compiledModule = await import(url.toString());
|
||||
let Component = compiledModule.default;
|
||||
|
||||
const generationOptions: Readonly<GeneratePathOptions> = {
|
||||
pageData,
|
||||
internals,
|
||||
linkIds,
|
||||
hoistedId,
|
||||
Component,
|
||||
mod: compiledModule,
|
||||
renderers,
|
||||
};
|
||||
|
||||
|
@ -314,65 +341,48 @@ interface GeneratePathOptions {
|
|||
internals: BuildInternals;
|
||||
linkIds: string[];
|
||||
hoistedId: string | null;
|
||||
Component: AstroComponentFactory;
|
||||
mod: ComponentInstance;
|
||||
renderers: Renderer[];
|
||||
}
|
||||
|
||||
async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) {
|
||||
const { astroConfig, logging, origin, routeCache } = opts;
|
||||
const { Component, internals, linkIds, hoistedId, pageData, renderers } = gopts;
|
||||
const { mod, internals, linkIds, hoistedId, pageData, renderers } = gopts;
|
||||
|
||||
// This adds the page name to the array so it can be shown as part of stats.
|
||||
addPageName(pathname, opts);
|
||||
|
||||
const [, mod] = pageData.preload;
|
||||
debug('build', `Generating: ${pathname}`);
|
||||
|
||||
const site = astroConfig.buildOptions.site;
|
||||
const links = createLinkStylesheetElementSet(linkIds, site);
|
||||
const scripts = createModuleScriptElementWithSrcSet(hoistedId ? [hoistedId] : [], site);
|
||||
|
||||
try {
|
||||
const [params, pageProps] = await getParamsAndProps({
|
||||
const html = await render({
|
||||
experimentalStaticBuild: true,
|
||||
links,
|
||||
logging,
|
||||
markdownRender: astroConfig.markdownOptions.render,
|
||||
mod,
|
||||
origin,
|
||||
pathname,
|
||||
scripts,
|
||||
renderers,
|
||||
async resolve(specifier: string) {
|
||||
const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier);
|
||||
if (typeof hashedFilePath !== 'string') {
|
||||
throw new Error(`Cannot find the built path for ${specifier}`);
|
||||
}
|
||||
const relPath = npath.posix.relative(pathname, '/' + hashedFilePath);
|
||||
const fullyRelativePath = relPath[0] === '.' ? relPath : './' + relPath;
|
||||
return fullyRelativePath;
|
||||
},
|
||||
route: pageData.route,
|
||||
routeCache,
|
||||
pathname,
|
||||
site: astroConfig.buildOptions.site,
|
||||
});
|
||||
|
||||
debug('build', `Generating: ${pathname}`);
|
||||
|
||||
const rootpath = appendForwardSlash(new URL(astroConfig.buildOptions.site || 'http://localhost/').pathname);
|
||||
const links = new Set<SSRElement>(
|
||||
linkIds.map((href) => ({
|
||||
props: {
|
||||
rel: 'stylesheet',
|
||||
href: npath.posix.join(rootpath, href),
|
||||
},
|
||||
children: '',
|
||||
}))
|
||||
);
|
||||
const scripts = hoistedId
|
||||
? new Set<SSRElement>([
|
||||
{
|
||||
props: {
|
||||
type: 'module',
|
||||
src: npath.posix.join(rootpath, hoistedId),
|
||||
},
|
||||
children: '',
|
||||
},
|
||||
])
|
||||
: new Set<SSRElement>();
|
||||
const result = createResult({ astroConfig, logging, origin, params, pathname, renderers, links, scripts });
|
||||
|
||||
// Override the `resolve` method so that hydrated components are given the
|
||||
// hashed filepath to the component.
|
||||
result.resolve = async (specifier: string) => {
|
||||
const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier);
|
||||
if (typeof hashedFilePath !== 'string') {
|
||||
throw new Error(`Cannot find the built path for ${specifier}`);
|
||||
}
|
||||
const relPath = npath.posix.relative(pathname, '/' + hashedFilePath);
|
||||
const fullyRelativePath = relPath[0] === '.' ? relPath : './' + relPath;
|
||||
return fullyRelativePath;
|
||||
};
|
||||
|
||||
let html = await renderPage(result, Component, pageProps, null);
|
||||
|
||||
const outFolder = getOutFolder(astroConfig, pathname);
|
||||
const outFile = getOutFile(astroConfig, outFolder, pathname);
|
||||
await fs.promises.mkdir(outFolder, { recursive: true });
|
||||
|
@ -382,11 +392,79 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G
|
|||
}
|
||||
}
|
||||
|
||||
async function generateManifest(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals) {
|
||||
const { astroConfig, manifest } = opts;
|
||||
const manifestFile = new URL('./manifest.json', getServerRoot(astroConfig));
|
||||
|
||||
const inputManifestJSON = await fs.promises.readFile(manifestFile, 'utf-8');
|
||||
const data: ViteManifest = JSON.parse(inputManifestJSON);
|
||||
|
||||
const rootRelativeIdToChunkMap = new Map<string, OutputChunk>();
|
||||
for(const output of result.output) {
|
||||
if(chunkIsPage(astroConfig, output, internals)) {
|
||||
const chunk = output as OutputChunk;
|
||||
if(chunk.facadeModuleId) {
|
||||
const id = rootRelativeFacadeId(chunk.facadeModuleId, astroConfig);
|
||||
rootRelativeIdToChunkMap.set(id, chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const routes: SerializedRouteInfo[] = [];
|
||||
|
||||
for(const routeData of manifest.routes) {
|
||||
const componentPath = routeData.component;
|
||||
const entry = data[componentPath];
|
||||
|
||||
if(!rootRelativeIdToChunkMap.has(componentPath)) {
|
||||
throw new Error('Unable to find chunk for ' + componentPath);
|
||||
}
|
||||
|
||||
const chunk = rootRelativeIdToChunkMap.get(componentPath)!;
|
||||
const facadeId = chunk.facadeModuleId!;
|
||||
const links = getByFacadeId<string[]>(facadeId, internals.facadeIdToAssetsMap) || [];
|
||||
const hoistedScript = getByFacadeId<string>(facadeId, internals.facadeIdToHoistedEntryMap);
|
||||
const scripts = hoistedScript ? [hoistedScript] : [];
|
||||
|
||||
routes.push({
|
||||
file: entry?.file,
|
||||
links,
|
||||
scripts,
|
||||
routeData: serializeRouteData(routeData)
|
||||
});
|
||||
}
|
||||
|
||||
const ssrManifest: SerializedSSRManifest = {
|
||||
routes,
|
||||
site: astroConfig.buildOptions.site,
|
||||
markdown: {
|
||||
render: astroConfig.markdownOptions.render
|
||||
},
|
||||
renderers: astroConfig.renderers,
|
||||
entryModules: Object.fromEntries(internals.entrySpecifierToBundleMap.entries())
|
||||
};
|
||||
|
||||
const outputManifestJSON = JSON.stringify(ssrManifest, null, ' ');
|
||||
await fs.promises.writeFile(manifestFile, outputManifestJSON, 'utf-8');
|
||||
}
|
||||
|
||||
function getOutRoot(astroConfig: AstroConfig): URL {
|
||||
const rootPathname = appendForwardSlash(astroConfig.buildOptions.site ? new URL(astroConfig.buildOptions.site).pathname : '/');
|
||||
return new URL('.' + rootPathname, astroConfig.dist);
|
||||
}
|
||||
|
||||
function getServerRoot(astroConfig: AstroConfig): URL {
|
||||
const rootFolder = getOutRoot(astroConfig);
|
||||
const serverFolder = new URL('./server/', rootFolder);
|
||||
return serverFolder;
|
||||
}
|
||||
|
||||
function getClientRoot(astroConfig: AstroConfig): URL {
|
||||
const rootFolder = getOutRoot(astroConfig);
|
||||
const serverFolder = new URL('./client/', rootFolder);
|
||||
return serverFolder;
|
||||
}
|
||||
|
||||
function getOutFolder(astroConfig: AstroConfig, pathname: string): URL {
|
||||
const outRoot = getOutRoot(astroConfig);
|
||||
|
||||
|
@ -421,6 +499,34 @@ async function cleanSsrOutput(opts: StaticBuildOptions) {
|
|||
);
|
||||
}
|
||||
|
||||
async function ssrMoveAssets(opts: StaticBuildOptions) {
|
||||
const { astroConfig } = opts;
|
||||
const serverRoot = getServerRoot(astroConfig);
|
||||
const clientRoot = getClientRoot(astroConfig);
|
||||
const serverAssets = new URL('./assets/', serverRoot);
|
||||
const clientAssets = new URL('./assets/', clientRoot);
|
||||
const files = await glob('assets/**/*', {
|
||||
cwd: fileURLToPath(serverRoot),
|
||||
});
|
||||
|
||||
// Make the directory
|
||||
await fs.promises.mkdir(clientAssets, { recursive: true });
|
||||
|
||||
await Promise.all(
|
||||
files.map(async (filename) => {
|
||||
const currentUrl = new URL(filename, serverRoot);
|
||||
const clientUrl = new URL(filename, clientRoot);
|
||||
return fs.promises.rename(currentUrl, clientUrl);
|
||||
})
|
||||
);
|
||||
|
||||
await emptyDir(fileURLToPath(serverAssets));
|
||||
|
||||
if(fs.existsSync(serverAssets)) {
|
||||
await fs.promises.rmdir(serverAssets);
|
||||
}
|
||||
}
|
||||
|
||||
export function vitePluginNewBuild(input: Set<string>, internals: BuildInternals, ext: 'js' | 'mjs'): VitePlugin {
|
||||
return {
|
||||
name: '@astro/rollup-plugin-new-build',
|
||||
|
@ -451,18 +557,6 @@ export function vitePluginNewBuild(input: Set<string>, internals: BuildInternals
|
|||
}
|
||||
},
|
||||
|
||||
outputOptions(outputOptions) {
|
||||
Object.assign(outputOptions, {
|
||||
entryFileNames(_chunk: PreRenderedChunk) {
|
||||
return 'assets/[name].[hash].' + ext;
|
||||
},
|
||||
chunkFileNames(_chunk: PreRenderedChunk) {
|
||||
return 'assets/[name].[hash].' + ext;
|
||||
},
|
||||
});
|
||||
return outputOptions;
|
||||
},
|
||||
|
||||
async generateBundle(_options, bundle) {
|
||||
const promises = [];
|
||||
const mapping = new Map<string, string>();
|
||||
|
|
2
packages/astro/src/core/build/types.d.ts
vendored
2
packages/astro/src/core/build/types.d.ts
vendored
|
@ -1,4 +1,4 @@
|
|||
import type { ComponentPreload } from '../ssr/index';
|
||||
import type { ComponentPreload } from '../render/dev/index';
|
||||
import type { RouteData } from '../../@types/astro';
|
||||
|
||||
export interface PageBuildData {
|
||||
|
|
|
@ -63,6 +63,7 @@ export const AstroConfigSchema = z.object({
|
|||
.optional()
|
||||
.default('directory'),
|
||||
experimentalStaticBuild: z.boolean().optional().default(false),
|
||||
experimentalSsr: z.boolean().optional().default(false),
|
||||
drafts: z.boolean().optional().default(false),
|
||||
})
|
||||
.optional()
|
||||
|
@ -130,6 +131,7 @@ function resolveFlags(flags: Partial<Flags>): CLIFlags {
|
|||
config: typeof flags.config === 'string' ? flags.config : undefined,
|
||||
hostname: typeof flags.hostname === 'string' ? flags.hostname : undefined,
|
||||
experimentalStaticBuild: typeof flags.experimentalStaticBuild === 'boolean' ? flags.experimentalStaticBuild : false,
|
||||
experimentalSsr: typeof flags.experimentalSsr === 'boolean' ? flags.experimentalSsr : false,
|
||||
drafts: typeof flags.drafts === 'boolean' ? flags.drafts : false,
|
||||
};
|
||||
}
|
||||
|
@ -143,6 +145,12 @@ function mergeCLIFlags(astroConfig: AstroUserConfig, flags: CLIFlags) {
|
|||
if (typeof flags.port === 'number') astroConfig.devOptions.port = flags.port;
|
||||
if (typeof flags.hostname === 'string') astroConfig.devOptions.hostname = flags.hostname;
|
||||
if (typeof flags.experimentalStaticBuild === 'boolean') astroConfig.buildOptions.experimentalStaticBuild = flags.experimentalStaticBuild;
|
||||
if (typeof flags.experimentalSsr === 'boolean') {
|
||||
astroConfig.buildOptions.experimentalSsr = flags.experimentalSsr;
|
||||
if(flags.experimentalSsr) {
|
||||
astroConfig.buildOptions.experimentalStaticBuild = true;
|
||||
}
|
||||
}
|
||||
if (typeof flags.drafts === 'boolean') astroConfig.buildOptions.drafts = flags.drafts;
|
||||
return astroConfig;
|
||||
}
|
||||
|
|
119
packages/astro/src/core/render/core.ts
Normal file
119
packages/astro/src/core/render/core.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import type { ComponentInstance, MarkdownRenderOptions, Params, Props, Renderer, RouteData, SSRElement } from '../../@types/astro';
|
||||
import type { LogOptions } from '../logger.js';
|
||||
|
||||
import { renderPage } from '../../runtime/server/index.js';
|
||||
import { getParams } from '../routing/index.js';
|
||||
import { createResult } from './result.js';
|
||||
import { findPathItemByKey, RouteCache, callGetStaticPaths } from './route-cache.js';
|
||||
import { warn } from '../logger.js';
|
||||
|
||||
interface GetParamsAndPropsOptions {
|
||||
mod: ComponentInstance;
|
||||
route: RouteData | undefined;
|
||||
routeCache: RouteCache;
|
||||
pathname: string;
|
||||
logging: LogOptions;
|
||||
}
|
||||
|
||||
async function getParamsAndProps(opts: GetParamsAndPropsOptions): Promise<[Params, Props]> {
|
||||
const { logging, mod, route, routeCache, pathname } = opts;
|
||||
// Handle dynamic routes
|
||||
let params: Params = {};
|
||||
let pageProps: Props;
|
||||
if (route && !route.pathname) {
|
||||
if (route.params.length) {
|
||||
const paramsMatch = route.pattern.exec(pathname);
|
||||
if (paramsMatch) {
|
||||
params = getParams(route.params)(paramsMatch);
|
||||
}
|
||||
}
|
||||
let routeCacheEntry = routeCache.get(route);
|
||||
if (!routeCacheEntry) {
|
||||
warn(logging, 'routeCache', `Internal Warning: getStaticPaths() called twice during the build. (${route.component})`);
|
||||
routeCacheEntry = await callGetStaticPaths(mod, route, true, logging);
|
||||
routeCache.set(route, routeCacheEntry);
|
||||
}
|
||||
const paramsKey = JSON.stringify(params);
|
||||
const matchedStaticPath = findPathItemByKey(routeCacheEntry.staticPaths, paramsKey);
|
||||
if (!matchedStaticPath) {
|
||||
throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`);
|
||||
}
|
||||
// This is written this way for performance; instead of spreading the props
|
||||
// which is O(n), create a new object that extends props.
|
||||
pageProps = Object.create(matchedStaticPath.props || Object.prototype);
|
||||
} else {
|
||||
pageProps = {};
|
||||
}
|
||||
return [params, pageProps];
|
||||
}
|
||||
|
||||
interface RenderOptions {
|
||||
experimentalStaticBuild: boolean;
|
||||
logging: LogOptions,
|
||||
links: Set<SSRElement>;
|
||||
markdownRender: MarkdownRenderOptions,
|
||||
mod: ComponentInstance;
|
||||
origin: string;
|
||||
pathname: string;
|
||||
scripts: Set<SSRElement>;
|
||||
resolve: (s: string) => Promise<string>;
|
||||
renderers: Renderer[];
|
||||
route?: RouteData;
|
||||
routeCache: RouteCache;
|
||||
site?: string;
|
||||
}
|
||||
|
||||
export async function render(opts: RenderOptions): Promise<string> {
|
||||
const {
|
||||
experimentalStaticBuild,
|
||||
links,
|
||||
logging,
|
||||
origin,
|
||||
markdownRender,
|
||||
mod,
|
||||
pathname,
|
||||
scripts,
|
||||
renderers,
|
||||
resolve,
|
||||
route,
|
||||
routeCache,
|
||||
site
|
||||
} = opts;
|
||||
|
||||
const [params, pageProps] = await getParamsAndProps({
|
||||
logging,
|
||||
mod,
|
||||
route,
|
||||
routeCache,
|
||||
pathname,
|
||||
});
|
||||
|
||||
// Validate the page component before rendering the page
|
||||
const Component = await mod.default;
|
||||
if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
|
||||
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
|
||||
|
||||
|
||||
const result = createResult({
|
||||
experimentalStaticBuild,
|
||||
links,
|
||||
logging,
|
||||
markdownRender,
|
||||
origin,
|
||||
params,
|
||||
pathname,
|
||||
resolve,
|
||||
renderers,
|
||||
site,
|
||||
scripts
|
||||
});
|
||||
|
||||
let html = await renderPage(result, Component, pageProps, null);
|
||||
|
||||
// inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?)
|
||||
if (experimentalStaticBuild && !/<!doctype html/i.test(html)) {
|
||||
html = '<!DOCTYPE html>\n' + html;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import type vite from '../vite';
|
||||
import type vite from '../../vite';
|
||||
|
||||
import path from 'path';
|
||||
import { viteID } from '../util.js';
|
||||
import { viteID } from '../../util.js';
|
||||
|
||||
// https://vitejs.dev/guide/features.html#css-pre-processors
|
||||
export const STYLE_EXTENSIONS = new Set(['.css', '.pcss', '.postcss', '.scss', '.sass', '.styl', '.stylus', '.less']);
|
44
packages/astro/src/core/render/dev/error.ts
Normal file
44
packages/astro/src/core/render/dev/error.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import type { BuildResult } from 'esbuild';
|
||||
import type vite from '../../vite';
|
||||
import type { SSRError } from '../../../@types/astro';
|
||||
|
||||
import eol from 'eol';
|
||||
import fs from 'fs';
|
||||
import { codeFrame } from '../../util.js';
|
||||
|
||||
interface ErrorHandlerOptions {
|
||||
filePath: URL;
|
||||
viteServer: vite.ViteDevServer;
|
||||
}
|
||||
|
||||
export async function errorHandler(e: unknown, { viteServer, filePath }: ErrorHandlerOptions) {
|
||||
// normalize error stack line-endings to \n
|
||||
if ((e as any).stack) {
|
||||
(e as any).stack = eol.lf((e as any).stack);
|
||||
}
|
||||
|
||||
// fix stack trace with Vite (this searches its module graph for matches)
|
||||
if (e instanceof Error) {
|
||||
viteServer.ssrFixStacktrace(e);
|
||||
}
|
||||
|
||||
// Astro error (thrown by esbuild so it needs to be formatted for Vite)
|
||||
if (Array.isArray((e as any).errors)) {
|
||||
const { location, pluginName, text } = (e as BuildResult).errors[0];
|
||||
const err = e as SSRError;
|
||||
if (location) err.loc = { file: location.file, line: location.line, column: location.column };
|
||||
let src = err.pluginCode;
|
||||
if (!src && err.id && fs.existsSync(err.id)) src = await fs.promises.readFile(err.id, 'utf8');
|
||||
if (!src) src = await fs.promises.readFile(filePath, 'utf8');
|
||||
err.frame = codeFrame(src, err.loc);
|
||||
err.id = location?.file;
|
||||
err.message = `${location?.file}: ${text}
|
||||
${err.frame}
|
||||
`;
|
||||
if (pluginName) err.plugin = pluginName;
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Generic error (probably from Vite, and already formatted)
|
||||
throw e;
|
||||
}
|
11
packages/astro/src/core/render/dev/hmr.ts
Normal file
11
packages/astro/src/core/render/dev/hmr.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
let hmrScript: string;
|
||||
export async function getHmrScript() {
|
||||
if (hmrScript) return hmrScript;
|
||||
const filePath = fileURLToPath(new URL('../../../runtime/client/hmr.js', import.meta.url));
|
||||
const content = await fs.promises.readFile(filePath);
|
||||
hmrScript = content.toString();
|
||||
return hmrScript;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import type vite from '../vite';
|
||||
import type vite from '../../vite';
|
||||
|
||||
import htmlparser2 from 'htmlparser2';
|
||||
|
158
packages/astro/src/core/render/dev/index.ts
Normal file
158
packages/astro/src/core/render/dev/index.ts
Normal file
|
@ -0,0 +1,158 @@
|
|||
import type vite from '../../vite';
|
||||
import type { AstroConfig, ComponentInstance, Renderer, RouteData, RuntimeMode } from '../../../@types/astro';
|
||||
import { LogOptions } from '../../logger.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { getStylesForURL } from './css.js';
|
||||
import { injectTags } from './html.js';
|
||||
import { RouteCache } from '../route-cache.js';
|
||||
import { resolveRenderers } from './renderers.js';
|
||||
import { errorHandler } from './error.js';
|
||||
import { getHmrScript } from './hmr.js';
|
||||
import { render as coreRender } from '../core.js';
|
||||
import { createModuleScriptElementWithSrcSet } from '../ssr-element.js';
|
||||
|
||||
interface SSROptions {
|
||||
/** an instance of the AstroConfig */
|
||||
astroConfig: AstroConfig;
|
||||
/** location of file on disk */
|
||||
filePath: URL;
|
||||
/** logging options */
|
||||
logging: LogOptions;
|
||||
/** "development" or "production" */
|
||||
mode: RuntimeMode;
|
||||
/** production website, needed for some RSS & Sitemap functions */
|
||||
origin: string;
|
||||
/** the web request (needed for dynamic routes) */
|
||||
pathname: string;
|
||||
/** optional, in case we need to render something outside of a dev server */
|
||||
route?: RouteData;
|
||||
/** pass in route cache because SSR can’t manage cache-busting */
|
||||
routeCache: RouteCache;
|
||||
/** Vite instance */
|
||||
viteServer: vite.ViteDevServer;
|
||||
}
|
||||
|
||||
export type ComponentPreload = [Renderer[], ComponentInstance];
|
||||
|
||||
const svelteStylesRE = /svelte\?svelte&type=style/;
|
||||
|
||||
export async function preload({ astroConfig, filePath, viteServer }: SSROptions): Promise<ComponentPreload> {
|
||||
// Important: This needs to happen first, in case a renderer provides polyfills.
|
||||
const renderers = await resolveRenderers(viteServer, astroConfig);
|
||||
// Load the module from the Vite SSR Runtime.
|
||||
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
|
||||
|
||||
return [renderers, mod];
|
||||
}
|
||||
|
||||
/** use Vite to SSR */
|
||||
export async function render(renderers: Renderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise<string> {
|
||||
const { astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer } = ssrOpts;
|
||||
|
||||
// Add hoisted script tags
|
||||
const scripts = createModuleScriptElementWithSrcSet(astroConfig.buildOptions.experimentalStaticBuild ?
|
||||
Array.from(mod.$$metadata.hoistedScriptPaths()) :
|
||||
[]
|
||||
);
|
||||
|
||||
// Inject HMR scripts
|
||||
if (mode === 'development' && astroConfig.buildOptions.experimentalStaticBuild) {
|
||||
scripts.add({
|
||||
props: { type: 'module', src: '/@vite/client' },
|
||||
children: '',
|
||||
});
|
||||
scripts.add({
|
||||
props: { type: 'module', src: new URL('../../runtime/client/hmr.js', import.meta.url).pathname },
|
||||
children: '',
|
||||
});
|
||||
}
|
||||
|
||||
let html = await coreRender({
|
||||
experimentalStaticBuild: astroConfig.buildOptions.experimentalStaticBuild,
|
||||
links: new Set(),
|
||||
logging,
|
||||
markdownRender: astroConfig.markdownOptions.render,
|
||||
mod,
|
||||
origin,
|
||||
pathname,
|
||||
scripts,
|
||||
// Resolves specifiers in the inline hydrated scripts, such as "@astrojs/renderer-preact/client.js"
|
||||
async resolve(s: string) {
|
||||
// The legacy build needs these to remain unresolved so that vite HTML
|
||||
// Can do the resolution. Without this condition the build output will be
|
||||
// broken in the legacy build. This can be removed once the legacy build is removed.
|
||||
if (astroConfig.buildOptions.experimentalStaticBuild) {
|
||||
const [, resolvedPath] = await viteServer.moduleGraph.resolveUrl(s);
|
||||
return resolvedPath;
|
||||
} else {
|
||||
return s;
|
||||
}
|
||||
},
|
||||
renderers,
|
||||
route,
|
||||
routeCache,
|
||||
site: astroConfig.buildOptions.site,
|
||||
});
|
||||
|
||||
// inject tags
|
||||
const tags: vite.HtmlTagDescriptor[] = [];
|
||||
|
||||
// dev only: inject Astro HMR client
|
||||
if (mode === 'development' && !astroConfig.buildOptions.experimentalStaticBuild) {
|
||||
tags.push({
|
||||
tag: 'script',
|
||||
attrs: { type: 'module' },
|
||||
// HACK: inject the direct contents of our `astro/runtime/client/hmr.js` to ensure
|
||||
// `import.meta.hot` is properly handled by Vite
|
||||
children: await getHmrScript(),
|
||||
injectTo: 'head',
|
||||
});
|
||||
}
|
||||
|
||||
// inject CSS
|
||||
[...getStylesForURL(filePath, viteServer)].forEach((href) => {
|
||||
if (mode === 'development' && svelteStylesRE.test(href)) {
|
||||
tags.push({
|
||||
tag: 'script',
|
||||
attrs: { type: 'module', src: href },
|
||||
injectTo: 'head',
|
||||
});
|
||||
} else {
|
||||
tags.push({
|
||||
tag: 'link',
|
||||
attrs: {
|
||||
rel: 'stylesheet',
|
||||
href,
|
||||
'data-astro-injected': true,
|
||||
},
|
||||
injectTo: 'head',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// add injected tags
|
||||
html = injectTags(html, tags);
|
||||
|
||||
// run transformIndexHtml() in dev to run Vite dev transformations
|
||||
if (mode === 'development' && !astroConfig.buildOptions.experimentalStaticBuild) {
|
||||
const relativeURL = filePath.href.replace(astroConfig.projectRoot.href, '/');
|
||||
html = await viteServer.transformIndexHtml(relativeURL, html, pathname);
|
||||
}
|
||||
|
||||
// inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?)
|
||||
if (!/<!doctype html/i.test(html)) {
|
||||
html = '<!DOCTYPE html>\n' + html;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
export async function ssr(ssrOpts: SSROptions): Promise<string> {
|
||||
try {
|
||||
const [renderers, mod] = await preload(ssrOpts);
|
||||
return await render(renderers, mod, ssrOpts); // note(drew): without "await", errors won’t get caught by errorHandler()
|
||||
} catch (e: unknown) {
|
||||
await errorHandler(e, { viteServer: ssrOpts.viteServer, filePath: ssrOpts.filePath });
|
||||
throw e;
|
||||
}
|
||||
}
|
36
packages/astro/src/core/render/dev/renderers.ts
Normal file
36
packages/astro/src/core/render/dev/renderers.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import type vite from '../../vite';
|
||||
import type { AstroConfig, Renderer } from '../../../@types/astro';
|
||||
|
||||
import { resolveDependency } from '../../util.js';
|
||||
import { createRenderer } from '../renderer.js';
|
||||
|
||||
const cache = new Map<string, Promise<Renderer>>();
|
||||
|
||||
async function resolveRenderer(viteServer: vite.ViteDevServer, renderer: string, astroConfig: AstroConfig): Promise<Renderer> {
|
||||
const resolvedRenderer: Renderer = await createRenderer(renderer, {
|
||||
renderer(name) {
|
||||
return import(resolveDependency(name, astroConfig));
|
||||
},
|
||||
async server(entry) {
|
||||
const { url } = await viteServer.moduleGraph.ensureEntryFromUrl(entry);
|
||||
const mod = await viteServer.ssrLoadModule(url);
|
||||
return mod;
|
||||
}
|
||||
});
|
||||
|
||||
return resolvedRenderer;
|
||||
}
|
||||
|
||||
export async function resolveRenderers(viteServer: vite.ViteDevServer, astroConfig: AstroConfig): Promise<Renderer[]> {
|
||||
const ids: string[] = astroConfig.renderers;
|
||||
const renderers = await Promise.all(
|
||||
ids.map((renderer) => {
|
||||
if (cache.has(renderer)) return cache.get(renderer)!;
|
||||
let promise = resolveRenderer(viteServer, renderer, astroConfig);
|
||||
cache.set(renderer, promise);
|
||||
return promise;
|
||||
})
|
||||
);
|
||||
|
||||
return renderers;
|
||||
}
|
30
packages/astro/src/core/render/renderer.ts
Normal file
30
packages/astro/src/core/render/renderer.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import type { Renderer } from '../../@types/astro';
|
||||
|
||||
import npath from 'path';
|
||||
|
||||
interface RendererResolverImplementation {
|
||||
renderer: (name: string) => Promise<any>;
|
||||
server: (entry: string) => Promise<any>;
|
||||
}
|
||||
|
||||
export async function createRenderer(renderer: string, impl: RendererResolverImplementation) {
|
||||
const resolvedRenderer: any = {};
|
||||
// We can dynamically import the renderer by itself because it shouldn't have
|
||||
// any non-standard imports, the index is just meta info.
|
||||
// The other entrypoints need to be loaded through Vite.
|
||||
const {
|
||||
default: { name, client, polyfills, hydrationPolyfills, server },
|
||||
} = await impl.renderer(renderer) //await import(resolveDependency(renderer, astroConfig));
|
||||
|
||||
resolvedRenderer.name = name;
|
||||
if (client) resolvedRenderer.source = npath.posix.join(renderer, client);
|
||||
resolvedRenderer.serverEntry = npath.posix.join(renderer, server);
|
||||
if (Array.isArray(hydrationPolyfills)) resolvedRenderer.hydrationPolyfills = hydrationPolyfills.map((src: string) => npath.posix.join(renderer, src));
|
||||
if (Array.isArray(polyfills)) resolvedRenderer.polyfills = polyfills.map((src: string) => npath.posix.join(renderer, src));
|
||||
|
||||
const { default: rendererSSR } = await impl.server(resolvedRenderer.serverEntry);
|
||||
resolvedRenderer.ssr = rendererSSR;
|
||||
|
||||
const completedRenderer: Renderer = resolvedRenderer;
|
||||
return completedRenderer;
|
||||
}
|
|
@ -1,25 +1,37 @@
|
|||
import type { AstroConfig, AstroGlobal, AstroGlobalPartial, Params, Renderer, SSRElement, SSRResult } from '../../@types/astro';
|
||||
import type { AstroGlobal, AstroGlobalPartial, MarkdownParser, MarkdownRenderOptions, Params, Renderer, SSRElement, SSRResult } from '../../@types/astro';
|
||||
|
||||
import { bold } from 'kleur/colors';
|
||||
import { canonicalURL as getCanonicalURL } from '../util.js';
|
||||
import { isCSSRequest } from './css.js';
|
||||
import { isCSSRequest } from './dev/css.js';
|
||||
import { isScriptRequest } from './script.js';
|
||||
import { renderSlot } from '../../runtime/server/index.js';
|
||||
import { warn, LogOptions } from '../logger.js';
|
||||
|
||||
export interface CreateResultArgs {
|
||||
astroConfig: AstroConfig;
|
||||
experimentalStaticBuild: boolean;
|
||||
logging: LogOptions;
|
||||
origin: string;
|
||||
markdownRender: MarkdownRenderOptions;
|
||||
params: Params;
|
||||
pathname: string;
|
||||
renderers: Renderer[];
|
||||
resolve: (s: string) => Promise<string>;
|
||||
site: string | undefined;
|
||||
links?: Set<SSRElement>;
|
||||
scripts?: Set<SSRElement>;
|
||||
}
|
||||
|
||||
export function createResult(args: CreateResultArgs): SSRResult {
|
||||
const { astroConfig, origin, params, pathname, renderers } = args;
|
||||
const {
|
||||
experimentalStaticBuild,
|
||||
origin,
|
||||
markdownRender,
|
||||
params,
|
||||
pathname,
|
||||
renderers,
|
||||
resolve,
|
||||
site: buildOptionsSite
|
||||
} = args;
|
||||
|
||||
// Create the result object that will be passed into the render function.
|
||||
// This object starts here as an empty shell (not yet the result) but then
|
||||
|
@ -32,7 +44,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
|||
createAstro(astroGlobal: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null) {
|
||||
const site = new URL(origin);
|
||||
const url = new URL('.' + pathname, site);
|
||||
const canonicalURL = getCanonicalURL('.' + pathname, astroConfig.buildOptions.site || origin);
|
||||
const canonicalURL = getCanonicalURL('.' + pathname, buildOptionsSite || origin);
|
||||
return {
|
||||
__proto__: astroGlobal,
|
||||
props,
|
||||
|
@ -42,7 +54,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
|||
url,
|
||||
},
|
||||
resolve(path: string) {
|
||||
if (astroConfig.buildOptions.experimentalStaticBuild) {
|
||||
if (experimentalStaticBuild) {
|
||||
let extra = `This can be replaced with a dynamic import like so: await import("${path}")`;
|
||||
if (isCSSRequest(path)) {
|
||||
extra = `It looks like you are resolving styles. If you are adding a link tag, replace with this:
|
||||
|
@ -83,33 +95,37 @@ ${extra}`
|
|||
},
|
||||
// <Markdown> also needs the same `astroConfig.markdownOptions.render` as `.md` pages
|
||||
async privateRenderMarkdownDoNotUse(content: string, opts: any) {
|
||||
let mdRender = astroConfig.markdownOptions.render;
|
||||
let renderOpts = {};
|
||||
let [mdRender, renderOpts] = markdownRender;
|
||||
let parser: MarkdownParser | null = null;
|
||||
//let renderOpts = {};
|
||||
if (Array.isArray(mdRender)) {
|
||||
renderOpts = mdRender[1];
|
||||
mdRender = mdRender[0];
|
||||
}
|
||||
// ['rehype-toc', opts]
|
||||
if (typeof mdRender === 'string') {
|
||||
({ default: mdRender } = await import(mdRender));
|
||||
const mod: { default: MarkdownParser } = await import(mdRender);
|
||||
parser = mod.default;
|
||||
}
|
||||
// [import('rehype-toc'), opts]
|
||||
else if (mdRender instanceof Promise) {
|
||||
({ default: mdRender } = await mdRender);
|
||||
const mod: { default: MarkdownParser } = await mdRender;
|
||||
parser = mod.default;
|
||||
} else if(typeof mdRender === 'function') {
|
||||
parser = mdRender;
|
||||
} else {
|
||||
throw new Error('No Markdown parser found.');
|
||||
}
|
||||
const { code } = await mdRender(content, { ...renderOpts, ...(opts ?? {}) });
|
||||
const { code } = await parser(content, { ...renderOpts, ...(opts ?? {}) });
|
||||
return code;
|
||||
},
|
||||
} as unknown as AstroGlobal;
|
||||
},
|
||||
// This is a stub and will be implemented by dev and build.
|
||||
async resolve(s: string): Promise<string> {
|
||||
return '';
|
||||
},
|
||||
resolve,
|
||||
_metadata: {
|
||||
renderers,
|
||||
pathname,
|
||||
experimentalStaticBuild: astroConfig.buildOptions.experimentalStaticBuild,
|
||||
experimentalStaticBuild,
|
||||
},
|
||||
};
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import type { ComponentInstance, GetStaticPathsItem, GetStaticPathsResult, GetStaticPathsResultKeyed, RouteData, RSS } from '../../@types/astro';
|
||||
import { LogOptions, warn, debug } from '../logger.js';
|
||||
|
||||
import { generatePaginateFunction } from '../ssr/paginate.js';
|
||||
import { validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
|
||||
import { generatePaginateFunction } from './paginate.js';
|
||||
import { validateGetStaticPathsModule, validateGetStaticPathsResult } from '../routing/index.js';
|
||||
|
||||
type RSSFn = (...args: any[]) => any;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { RSSFunction, RSS, RSSResult, FeedResult, RouteData } from '../../@types/astro';
|
||||
import type { RSSFunction, RSS, RSSResult, RouteData } from '../../@types/astro';
|
||||
|
||||
import { XMLValidator } from 'fast-xml-parser';
|
||||
import { canonicalURL, isValidURL, PRETTY_FEED_V3 } from '../util.js';
|
40
packages/astro/src/core/render/ssr-element.ts
Normal file
40
packages/astro/src/core/render/ssr-element.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import type { SSRElement } from '../../@types/astro';
|
||||
|
||||
import npath from 'path';
|
||||
import { appendForwardSlash } from '../../core/path.js';
|
||||
|
||||
function getRootPath(site?: string): string {
|
||||
return appendForwardSlash(new URL(site || 'http://localhost/').pathname)
|
||||
}
|
||||
|
||||
function joinToRoot(href: string, site?: string): string {
|
||||
return npath.posix.join(getRootPath(site), href);
|
||||
}
|
||||
|
||||
export function createLinkStylesheetElement(href: string, site?: string): SSRElement {
|
||||
return {
|
||||
props: {
|
||||
rel: 'stylesheet',
|
||||
href: joinToRoot(href, site)
|
||||
},
|
||||
children: '',
|
||||
};
|
||||
}
|
||||
|
||||
export function createLinkStylesheetElementSet(hrefs: string[], site?: string) {
|
||||
return new Set<SSRElement>(hrefs.map(href => createLinkStylesheetElement(href, site)));
|
||||
}
|
||||
|
||||
export function createModuleScriptElementWithSrc(src: string, site?: string): SSRElement {
|
||||
return {
|
||||
props: {
|
||||
type: 'module',
|
||||
src: joinToRoot(src, site),
|
||||
},
|
||||
children: '',
|
||||
}
|
||||
}
|
||||
|
||||
export function createModuleScriptElementWithSrcSet(srces: string[], site?: string): Set<SSRElement> {
|
||||
return new Set<SSRElement>(srces.map(src => createModuleScriptElementWithSrc(src, site)));
|
||||
}
|
11
packages/astro/src/core/routing/index.ts
Normal file
11
packages/astro/src/core/routing/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
export { createRouteManifest } from './manifest/create.js';
|
||||
export {
|
||||
serializeRouteData,
|
||||
deserializeRouteData
|
||||
} from './manifest/serialization.js';
|
||||
export { matchRoute } from './match.js';
|
||||
export { getParams } from './params.js';
|
||||
export {
|
||||
validateGetStaticPathsModule,
|
||||
validateGetStaticPathsResult
|
||||
} from './validation.js';
|
|
@ -1,69 +1,16 @@
|
|||
import type { AstroConfig, ComponentInstance, GetStaticPathsResult, ManifestData, Params, RouteData } from '../../@types/astro';
|
||||
import type { LogOptions } from '../logger';
|
||||
import type {
|
||||
AstroConfig,
|
||||
ManifestData,
|
||||
RouteData
|
||||
} from '../../../@types/astro';
|
||||
import type { LogOptions } from '../../logger';
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { compile } from 'path-to-regexp';
|
||||
import slash from 'slash';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { warn } from '../logger.js';
|
||||
|
||||
/**
|
||||
* given an array of params like `['x', 'y', 'z']` for
|
||||
* src/routes/[x]/[y]/[z]/svelte, create a function
|
||||
* that turns a RegExpExecArray into ({ x, y, z })
|
||||
*/
|
||||
export function getParams(array: string[]) {
|
||||
const fn = (match: RegExpExecArray) => {
|
||||
const params: Params = {};
|
||||
array.forEach((key, i) => {
|
||||
if (key.startsWith('...')) {
|
||||
params[key.slice(3)] = match[i + 1] ? decodeURIComponent(match[i + 1]) : undefined;
|
||||
} else {
|
||||
params[key] = decodeURIComponent(match[i + 1]);
|
||||
}
|
||||
});
|
||||
return params;
|
||||
};
|
||||
|
||||
return fn;
|
||||
}
|
||||
|
||||
/** Find matching route from pathname */
|
||||
export function matchRoute(pathname: string, manifest: ManifestData): RouteData | undefined {
|
||||
return manifest.routes.find((route) => route.pattern.test(pathname));
|
||||
}
|
||||
|
||||
/** Throw error for deprecated/malformed APIs */
|
||||
export function validateGetStaticPathsModule(mod: ComponentInstance) {
|
||||
if ((mod as any).createCollection) {
|
||||
throw new Error(`[createCollection] deprecated. Please use getStaticPaths() instead.`);
|
||||
}
|
||||
if (!mod.getStaticPaths) {
|
||||
throw new Error(`[getStaticPaths] getStaticPaths() function is required. Make sure that you \`export\` the function from your component.`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Throw error for malformed getStaticPaths() response */
|
||||
export function validateGetStaticPathsResult(result: GetStaticPathsResult, logging: LogOptions) {
|
||||
if (!Array.isArray(result)) {
|
||||
throw new Error(`[getStaticPaths] invalid return value. Expected an array of path objects, but got \`${JSON.stringify(result)}\`.`);
|
||||
}
|
||||
result.forEach((pathObject) => {
|
||||
if (!pathObject.params) {
|
||||
warn(logging, 'getStaticPaths', `invalid path object. Expected an object with key \`params\`, but got \`${JSON.stringify(pathObject)}\`. Skipped.`);
|
||||
return;
|
||||
}
|
||||
for (const [key, val] of Object.entries(pathObject.params)) {
|
||||
if (!(typeof val === 'undefined' || typeof val === 'string')) {
|
||||
warn(logging, 'getStaticPaths', `invalid path param: ${key}. A string value was expected, but got \`${JSON.stringify(val)}\`.`);
|
||||
}
|
||||
if (val === '') {
|
||||
warn(logging, 'getStaticPaths', `invalid path param: ${key}. \`undefined\` expected for an optional param, but got empty string.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
import { warn } from '../../logger.js';
|
||||
|
||||
interface Part {
|
||||
content: string;
|
||||
|
@ -82,6 +29,148 @@ interface Item {
|
|||
routeSuffix: string;
|
||||
}
|
||||
|
||||
function countOccurrences(needle: string, haystack: string) {
|
||||
let count = 0;
|
||||
for (let i = 0; i < haystack.length; i += 1) {
|
||||
if (haystack[i] === needle) count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function getParts(part: string, file: string) {
|
||||
const result: Part[] = [];
|
||||
part.split(/\[(.+?\(.+?\)|.+?)\]/).map((str, i) => {
|
||||
if (!str) return;
|
||||
const dynamic = i % 2 === 1;
|
||||
|
||||
const [, content] = dynamic ? /([^(]+)$/.exec(str) || [null, null] : [null, str];
|
||||
|
||||
if (!content || (dynamic && !/^(\.\.\.)?[a-zA-Z0-9_$]+$/.test(content))) {
|
||||
throw new Error(`Invalid route ${file} — parameter name must match /^[a-zA-Z0-9_$]+$/`);
|
||||
}
|
||||
|
||||
result.push({
|
||||
content,
|
||||
dynamic,
|
||||
spread: dynamic && /^\.{3}.+$/.test(content),
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getPattern(segments: Part[][], addTrailingSlash: AstroConfig['devOptions']['trailingSlash']) {
|
||||
const pathname = segments
|
||||
.map((segment) => {
|
||||
return segment[0].spread
|
||||
? '(?:\\/(.*?))?'
|
||||
: '\\/' +
|
||||
segment
|
||||
.map((part) => {
|
||||
if (part)
|
||||
return part.dynamic
|
||||
? '([^/]+?)'
|
||||
: part.content
|
||||
.normalize()
|
||||
.replace(/\?/g, '%3F')
|
||||
.replace(/#/g, '%23')
|
||||
.replace(/%5B/g, '[')
|
||||
.replace(/%5D/g, ']')
|
||||
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
})
|
||||
.join('');
|
||||
})
|
||||
.join('');
|
||||
|
||||
const trailing = addTrailingSlash && segments.length ? getTrailingSlashPattern(addTrailingSlash) : '$';
|
||||
return new RegExp(`^${pathname || '\\/'}${trailing}`);
|
||||
}
|
||||
|
||||
|
||||
function getTrailingSlashPattern(addTrailingSlash: AstroConfig['devOptions']['trailingSlash']): string {
|
||||
if (addTrailingSlash === 'always') {
|
||||
return '\\/$';
|
||||
}
|
||||
if (addTrailingSlash === 'never') {
|
||||
return '$';
|
||||
}
|
||||
return '\\/?$';
|
||||
}
|
||||
|
||||
function getGenerator(segments: Part[][], addTrailingSlash: AstroConfig['devOptions']['trailingSlash']) {
|
||||
const template = segments
|
||||
.map((segment) => {
|
||||
return segment[0].spread
|
||||
? `/:${segment[0].content.substr(3)}(.*)?`
|
||||
: '/' +
|
||||
segment
|
||||
.map((part) => {
|
||||
if (part)
|
||||
return part.dynamic
|
||||
? `:${part.content}`
|
||||
: part.content
|
||||
.normalize()
|
||||
.replace(/\?/g, '%3F')
|
||||
.replace(/#/g, '%23')
|
||||
.replace(/%5B/g, '[')
|
||||
.replace(/%5D/g, ']')
|
||||
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
})
|
||||
.join('');
|
||||
})
|
||||
.join('');
|
||||
|
||||
const trailing = addTrailingSlash !== 'never' && segments.length ? '/' : '';
|
||||
const toPath = compile(template + trailing);
|
||||
return toPath;
|
||||
}
|
||||
|
||||
function isSpread(str: string) {
|
||||
const spreadPattern = /\[\.{3}/g;
|
||||
return spreadPattern.test(str);
|
||||
}
|
||||
|
||||
function comparator(a: Item, b: Item) {
|
||||
if (a.isIndex !== b.isIndex) {
|
||||
if (a.isIndex) return isSpread(a.file) ? 1 : -1;
|
||||
|
||||
return isSpread(b.file) ? -1 : 1;
|
||||
}
|
||||
|
||||
const max = Math.max(a.parts.length, b.parts.length);
|
||||
|
||||
for (let i = 0; i < max; i += 1) {
|
||||
const aSubPart = a.parts[i];
|
||||
const bSubPart = b.parts[i];
|
||||
|
||||
if (!aSubPart) return 1; // b is more specific, so goes first
|
||||
if (!bSubPart) return -1;
|
||||
|
||||
// if spread && index, order later
|
||||
if (aSubPart.spread && bSubPart.spread) {
|
||||
return a.isIndex ? 1 : -1;
|
||||
}
|
||||
|
||||
// If one is ...spread order it later
|
||||
if (aSubPart.spread !== bSubPart.spread) return aSubPart.spread ? 1 : -1;
|
||||
|
||||
if (aSubPart.dynamic !== bSubPart.dynamic) {
|
||||
return aSubPart.dynamic ? 1 : -1;
|
||||
}
|
||||
|
||||
if (!aSubPart.dynamic && aSubPart.content !== bSubPart.content) {
|
||||
return bSubPart.content.length - aSubPart.content.length || (aSubPart.content < bSubPart.content ? -1 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (a.isPage !== b.isPage) {
|
||||
return a.isPage ? 1 : -1;
|
||||
}
|
||||
|
||||
// otherwise sort alphabetically
|
||||
return a.file < b.file ? -1 : 1;
|
||||
}
|
||||
|
||||
/** Create manifest of all static routes */
|
||||
export function createRouteManifest({ config, cwd }: { config: AstroConfig; cwd?: string }, logging: LogOptions): ManifestData {
|
||||
const components: string[] = [];
|
||||
|
@ -207,144 +296,3 @@ export function createRouteManifest({ config, cwd }: { config: AstroConfig; cwd?
|
|||
routes,
|
||||
};
|
||||
}
|
||||
|
||||
function countOccurrences(needle: string, haystack: string) {
|
||||
let count = 0;
|
||||
for (let i = 0; i < haystack.length; i += 1) {
|
||||
if (haystack[i] === needle) count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function isSpread(str: string) {
|
||||
const spreadPattern = /\[\.{3}/g;
|
||||
return spreadPattern.test(str);
|
||||
}
|
||||
|
||||
function comparator(a: Item, b: Item) {
|
||||
if (a.isIndex !== b.isIndex) {
|
||||
if (a.isIndex) return isSpread(a.file) ? 1 : -1;
|
||||
|
||||
return isSpread(b.file) ? -1 : 1;
|
||||
}
|
||||
|
||||
const max = Math.max(a.parts.length, b.parts.length);
|
||||
|
||||
for (let i = 0; i < max; i += 1) {
|
||||
const aSubPart = a.parts[i];
|
||||
const bSubPart = b.parts[i];
|
||||
|
||||
if (!aSubPart) return 1; // b is more specific, so goes first
|
||||
if (!bSubPart) return -1;
|
||||
|
||||
// if spread && index, order later
|
||||
if (aSubPart.spread && bSubPart.spread) {
|
||||
return a.isIndex ? 1 : -1;
|
||||
}
|
||||
|
||||
// If one is ...spread order it later
|
||||
if (aSubPart.spread !== bSubPart.spread) return aSubPart.spread ? 1 : -1;
|
||||
|
||||
if (aSubPart.dynamic !== bSubPart.dynamic) {
|
||||
return aSubPart.dynamic ? 1 : -1;
|
||||
}
|
||||
|
||||
if (!aSubPart.dynamic && aSubPart.content !== bSubPart.content) {
|
||||
return bSubPart.content.length - aSubPart.content.length || (aSubPart.content < bSubPart.content ? -1 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (a.isPage !== b.isPage) {
|
||||
return a.isPage ? 1 : -1;
|
||||
}
|
||||
|
||||
// otherwise sort alphabetically
|
||||
return a.file < b.file ? -1 : 1;
|
||||
}
|
||||
|
||||
function getParts(part: string, file: string) {
|
||||
const result: Part[] = [];
|
||||
part.split(/\[(.+?\(.+?\)|.+?)\]/).map((str, i) => {
|
||||
if (!str) return;
|
||||
const dynamic = i % 2 === 1;
|
||||
|
||||
const [, content] = dynamic ? /([^(]+)$/.exec(str) || [null, null] : [null, str];
|
||||
|
||||
if (!content || (dynamic && !/^(\.\.\.)?[a-zA-Z0-9_$]+$/.test(content))) {
|
||||
throw new Error(`Invalid route ${file} — parameter name must match /^[a-zA-Z0-9_$]+$/`);
|
||||
}
|
||||
|
||||
result.push({
|
||||
content,
|
||||
dynamic,
|
||||
spread: dynamic && /^\.{3}.+$/.test(content),
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getTrailingSlashPattern(addTrailingSlash: AstroConfig['devOptions']['trailingSlash']): string {
|
||||
if (addTrailingSlash === 'always') {
|
||||
return '\\/$';
|
||||
}
|
||||
if (addTrailingSlash === 'never') {
|
||||
return '$';
|
||||
}
|
||||
return '\\/?$';
|
||||
}
|
||||
|
||||
function getPattern(segments: Part[][], addTrailingSlash: AstroConfig['devOptions']['trailingSlash']) {
|
||||
const pathname = segments
|
||||
.map((segment) => {
|
||||
return segment[0].spread
|
||||
? '(?:\\/(.*?))?'
|
||||
: '\\/' +
|
||||
segment
|
||||
.map((part) => {
|
||||
if (part)
|
||||
return part.dynamic
|
||||
? '([^/]+?)'
|
||||
: part.content
|
||||
.normalize()
|
||||
.replace(/\?/g, '%3F')
|
||||
.replace(/#/g, '%23')
|
||||
.replace(/%5B/g, '[')
|
||||
.replace(/%5D/g, ']')
|
||||
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
})
|
||||
.join('');
|
||||
})
|
||||
.join('');
|
||||
|
||||
const trailing = addTrailingSlash && segments.length ? getTrailingSlashPattern(addTrailingSlash) : '$';
|
||||
return new RegExp(`^${pathname || '\\/'}${trailing}`);
|
||||
}
|
||||
|
||||
function getGenerator(segments: Part[][], addTrailingSlash: AstroConfig['devOptions']['trailingSlash']) {
|
||||
const template = segments
|
||||
.map((segment) => {
|
||||
return segment[0].spread
|
||||
? `/:${segment[0].content.substr(3)}(.*)?`
|
||||
: '/' +
|
||||
segment
|
||||
.map((part) => {
|
||||
if (part)
|
||||
return part.dynamic
|
||||
? `:${part.content}`
|
||||
: part.content
|
||||
.normalize()
|
||||
.replace(/\?/g, '%3F')
|
||||
.replace(/#/g, '%23')
|
||||
.replace(/%5B/g, '[')
|
||||
.replace(/%5D/g, ']')
|
||||
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
})
|
||||
.join('');
|
||||
})
|
||||
.join('');
|
||||
|
||||
const trailing = addTrailingSlash !== 'never' && segments.length ? '/' : '';
|
||||
const toPath = compile(template + trailing);
|
||||
return toPath;
|
||||
}
|
29
packages/astro/src/core/routing/manifest/serialization.ts
Normal file
29
packages/astro/src/core/routing/manifest/serialization.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import type {
|
||||
RouteData,
|
||||
SerializedRouteData
|
||||
} from '../../../@types/astro';
|
||||
|
||||
function createRouteData(pattern: RegExp, params: string[], component: string, pathname: string | undefined): RouteData {
|
||||
return {
|
||||
type: 'page',
|
||||
pattern,
|
||||
params,
|
||||
component,
|
||||
// TODO bring back
|
||||
generate: () => '',
|
||||
pathname: pathname || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeRouteData(routeData: RouteData): SerializedRouteData {
|
||||
// Is there a better way to do this in TypeScript?
|
||||
const outRouteData = routeData as unknown as SerializedRouteData;
|
||||
outRouteData.pattern = routeData.pattern.source;
|
||||
return outRouteData;
|
||||
}
|
||||
|
||||
export function deserializeRouteData(rawRouteData: SerializedRouteData) {
|
||||
const { component, params, pathname } = rawRouteData;
|
||||
const pattern = new RegExp(rawRouteData.pattern);
|
||||
return createRouteData(pattern, params, component, pathname);
|
||||
}
|
10
packages/astro/src/core/routing/match.ts
Normal file
10
packages/astro/src/core/routing/match.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import type {
|
||||
ManifestData,
|
||||
RouteData
|
||||
} from '../../@types/astro';
|
||||
|
||||
/** Find matching route from pathname */
|
||||
export function matchRoute(pathname: string, manifest: ManifestData): RouteData | undefined {
|
||||
return manifest.routes.find((route) => route.pattern.test(pathname));
|
||||
}
|
||||
|
23
packages/astro/src/core/routing/params.ts
Normal file
23
packages/astro/src/core/routing/params.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import type { Params } from '../../@types/astro';
|
||||
|
||||
/**
|
||||
* given an array of params like `['x', 'y', 'z']` for
|
||||
* src/routes/[x]/[y]/[z]/svelte, create a function
|
||||
* that turns a RegExpExecArray into ({ x, y, z })
|
||||
*/
|
||||
export function getParams(array: string[]) {
|
||||
const fn = (match: RegExpExecArray) => {
|
||||
const params: Params = {};
|
||||
array.forEach((key, i) => {
|
||||
if (key.startsWith('...')) {
|
||||
params[key.slice(3)] = match[i + 1] ? decodeURIComponent(match[i + 1]) : undefined;
|
||||
} else {
|
||||
params[key] = decodeURIComponent(match[i + 1]);
|
||||
}
|
||||
});
|
||||
return params;
|
||||
};
|
||||
|
||||
return fn;
|
||||
}
|
||||
|
37
packages/astro/src/core/routing/validation.ts
Normal file
37
packages/astro/src/core/routing/validation.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import type {
|
||||
ComponentInstance,
|
||||
GetStaticPathsResult
|
||||
} from '../../@types/astro';
|
||||
import type { LogOptions } from '../logger';
|
||||
import { warn } from '../logger.js';
|
||||
|
||||
/** Throw error for deprecated/malformed APIs */
|
||||
export function validateGetStaticPathsModule(mod: ComponentInstance) {
|
||||
if ((mod as any).createCollection) {
|
||||
throw new Error(`[createCollection] deprecated. Please use getStaticPaths() instead.`);
|
||||
}
|
||||
if (!mod.getStaticPaths) {
|
||||
throw new Error(`[getStaticPaths] getStaticPaths() function is required. Make sure that you \`export\` the function from your component.`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Throw error for malformed getStaticPaths() response */
|
||||
export function validateGetStaticPathsResult(result: GetStaticPathsResult, logging: LogOptions) {
|
||||
if (!Array.isArray(result)) {
|
||||
throw new Error(`[getStaticPaths] invalid return value. Expected an array of path objects, but got \`${JSON.stringify(result)}\`.`);
|
||||
}
|
||||
result.forEach((pathObject) => {
|
||||
if (!pathObject.params) {
|
||||
warn(logging, 'getStaticPaths', `invalid path object. Expected an object with key \`params\`, but got \`${JSON.stringify(pathObject)}\`. Skipped.`);
|
||||
return;
|
||||
}
|
||||
for (const [key, val] of Object.entries(pathObject.params)) {
|
||||
if (!(typeof val === 'undefined' || typeof val === 'string')) {
|
||||
warn(logging, 'getStaticPaths', `invalid path param: ${key}. A string value was expected, but got \`${JSON.stringify(val)}\`.`);
|
||||
}
|
||||
if (val === '') {
|
||||
warn(logging, 'getStaticPaths', `invalid path param: ${key}. \`undefined\` expected for an optional param, but got empty string.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,300 +0,0 @@
|
|||
import type { BuildResult } from 'esbuild';
|
||||
import type vite from '../vite';
|
||||
import type { AstroConfig, ComponentInstance, Params, Props, Renderer, RouteData, RuntimeMode, SSRElement, SSRError } from '../../@types/astro';
|
||||
import { LogOptions, warn } from '../logger.js';
|
||||
|
||||
import eol from 'eol';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { renderPage } from '../../runtime/server/index.js';
|
||||
import { codeFrame, resolveDependency } from '../util.js';
|
||||
import { getStylesForURL } from './css.js';
|
||||
import { injectTags } from './html.js';
|
||||
import { getParams, validateGetStaticPathsResult } from './routing.js';
|
||||
import { createResult } from './result.js';
|
||||
import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js';
|
||||
|
||||
const svelteStylesRE = /svelte\?svelte&type=style/;
|
||||
|
||||
interface SSROptions {
|
||||
/** an instance of the AstroConfig */
|
||||
astroConfig: AstroConfig;
|
||||
/** location of file on disk */
|
||||
filePath: URL;
|
||||
/** logging options */
|
||||
logging: LogOptions;
|
||||
/** "development" or "production" */
|
||||
mode: RuntimeMode;
|
||||
/** production website, needed for some RSS & Sitemap functions */
|
||||
origin: string;
|
||||
/** the web request (needed for dynamic routes) */
|
||||
pathname: string;
|
||||
/** optional, in case we need to render something outside of a dev server */
|
||||
route?: RouteData;
|
||||
/** pass in route cache because SSR can’t manage cache-busting */
|
||||
routeCache: RouteCache;
|
||||
/** Vite instance */
|
||||
viteServer: vite.ViteDevServer;
|
||||
}
|
||||
|
||||
const cache = new Map<string, Promise<Renderer>>();
|
||||
|
||||
// TODO: improve validation and error handling here.
|
||||
async function resolveRenderer(viteServer: vite.ViteDevServer, renderer: string, astroConfig: AstroConfig) {
|
||||
const resolvedRenderer: any = {};
|
||||
// We can dynamically import the renderer by itself because it shouldn't have
|
||||
// any non-standard imports, the index is just meta info.
|
||||
// The other entrypoints need to be loaded through Vite.
|
||||
const {
|
||||
default: { name, client, polyfills, hydrationPolyfills, server },
|
||||
} = await import(resolveDependency(renderer, astroConfig));
|
||||
|
||||
resolvedRenderer.name = name;
|
||||
if (client) resolvedRenderer.source = path.posix.join(renderer, client);
|
||||
resolvedRenderer.serverEntry = path.posix.join(renderer, server);
|
||||
if (Array.isArray(hydrationPolyfills)) resolvedRenderer.hydrationPolyfills = hydrationPolyfills.map((src: string) => path.posix.join(renderer, src));
|
||||
if (Array.isArray(polyfills)) resolvedRenderer.polyfills = polyfills.map((src: string) => path.posix.join(renderer, src));
|
||||
const { url } = await viteServer.moduleGraph.ensureEntryFromUrl(resolvedRenderer.serverEntry);
|
||||
const { default: rendererSSR } = await viteServer.ssrLoadModule(url);
|
||||
resolvedRenderer.ssr = rendererSSR;
|
||||
|
||||
const completedRenderer: Renderer = resolvedRenderer;
|
||||
return completedRenderer;
|
||||
}
|
||||
|
||||
async function resolveRenderers(viteServer: vite.ViteDevServer, astroConfig: AstroConfig): Promise<Renderer[]> {
|
||||
const ids: string[] = astroConfig.renderers;
|
||||
const renderers = await Promise.all(
|
||||
ids.map((renderer) => {
|
||||
if (cache.has(renderer)) return cache.get(renderer)!;
|
||||
let promise = resolveRenderer(viteServer, renderer, astroConfig);
|
||||
cache.set(renderer, promise);
|
||||
return promise;
|
||||
})
|
||||
);
|
||||
|
||||
return renderers;
|
||||
}
|
||||
|
||||
interface ErrorHandlerOptions {
|
||||
filePath: URL;
|
||||
viteServer: vite.ViteDevServer;
|
||||
}
|
||||
|
||||
async function errorHandler(e: unknown, { viteServer, filePath }: ErrorHandlerOptions) {
|
||||
// normalize error stack line-endings to \n
|
||||
if ((e as any).stack) {
|
||||
(e as any).stack = eol.lf((e as any).stack);
|
||||
}
|
||||
|
||||
// fix stack trace with Vite (this searches its module graph for matches)
|
||||
if (e instanceof Error) {
|
||||
viteServer.ssrFixStacktrace(e);
|
||||
}
|
||||
|
||||
// Astro error (thrown by esbuild so it needs to be formatted for Vite)
|
||||
if (Array.isArray((e as any).errors)) {
|
||||
const { location, pluginName, text } = (e as BuildResult).errors[0];
|
||||
const err = e as SSRError;
|
||||
if (location) err.loc = { file: location.file, line: location.line, column: location.column };
|
||||
let src = err.pluginCode;
|
||||
if (!src && err.id && fs.existsSync(err.id)) src = await fs.promises.readFile(err.id, 'utf8');
|
||||
if (!src) src = await fs.promises.readFile(filePath, 'utf8');
|
||||
err.frame = codeFrame(src, err.loc);
|
||||
err.id = location?.file;
|
||||
err.message = `${location?.file}: ${text}
|
||||
${err.frame}
|
||||
`;
|
||||
if (pluginName) err.plugin = pluginName;
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Generic error (probably from Vite, and already formatted)
|
||||
throw e;
|
||||
}
|
||||
|
||||
export type ComponentPreload = [Renderer[], ComponentInstance];
|
||||
|
||||
export async function preload({ astroConfig, filePath, viteServer }: SSROptions): Promise<ComponentPreload> {
|
||||
// Important: This needs to happen first, in case a renderer provides polyfills.
|
||||
const renderers = await resolveRenderers(viteServer, astroConfig);
|
||||
// Load the module from the Vite SSR Runtime.
|
||||
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
|
||||
|
||||
return [renderers, mod];
|
||||
}
|
||||
|
||||
export async function getParamsAndProps({ route, routeCache, pathname }: { route: RouteData | undefined; routeCache: RouteCache; pathname: string }): Promise<[Params, Props]> {
|
||||
// Handle dynamic routes
|
||||
let params: Params = {};
|
||||
let pageProps: Props;
|
||||
if (route && !route.pathname) {
|
||||
if (route.params.length) {
|
||||
const paramsMatch = route.pattern.exec(pathname);
|
||||
if (paramsMatch) {
|
||||
params = getParams(route.params)(paramsMatch);
|
||||
}
|
||||
}
|
||||
const routeCacheEntry = routeCache.get(route);
|
||||
if (!routeCacheEntry) {
|
||||
throw new Error(`[${route.component}] Internal error: route cache was empty, but expected to be full.`);
|
||||
}
|
||||
const paramsKey = JSON.stringify(params);
|
||||
const matchedStaticPath = findPathItemByKey(routeCacheEntry.staticPaths, paramsKey);
|
||||
if (!matchedStaticPath) {
|
||||
throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`);
|
||||
}
|
||||
// This is written this way for performance; instead of spreading the props
|
||||
// which is O(n), create a new object that extends props.
|
||||
pageProps = Object.create(matchedStaticPath.props || Object.prototype);
|
||||
} else {
|
||||
pageProps = {};
|
||||
}
|
||||
return [params, pageProps];
|
||||
}
|
||||
|
||||
/** use Vite to SSR */
|
||||
export async function render(renderers: Renderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise<string> {
|
||||
const { astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer } = ssrOpts;
|
||||
|
||||
// Handle dynamic routes
|
||||
let params: Params = {};
|
||||
let pageProps: Props = {};
|
||||
if (route && !route.pathname) {
|
||||
if (route.params.length) {
|
||||
const paramsMatch = route.pattern.exec(pathname);
|
||||
if (paramsMatch) {
|
||||
params = getParams(route.params)(paramsMatch);
|
||||
}
|
||||
}
|
||||
let routeCacheEntry = routeCache.get(route);
|
||||
// TODO(fks): All of our getStaticPaths logic should live in a single place,
|
||||
// to prevent duplicate runs during the build. This is not expected to run
|
||||
// anymore and we should change this check to thrown an internal error.
|
||||
if (!routeCacheEntry) {
|
||||
warn(logging, 'routeCache', `Internal Warning: getStaticPaths() called twice during the build. (${route.component})`);
|
||||
routeCacheEntry = await callGetStaticPaths(mod, route, true, logging);
|
||||
routeCache.set(route, routeCacheEntry);
|
||||
}
|
||||
const matchedStaticPath = routeCacheEntry.staticPaths.find(({ params: _params }) => JSON.stringify(_params) === JSON.stringify(params));
|
||||
if (!matchedStaticPath) {
|
||||
throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`);
|
||||
}
|
||||
pageProps = { ...matchedStaticPath.props } || {};
|
||||
}
|
||||
|
||||
// Validate the page component before rendering the page
|
||||
const Component = await mod.default;
|
||||
if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
|
||||
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
|
||||
|
||||
// Add hoisted script tags
|
||||
const scripts = astroConfig.buildOptions.experimentalStaticBuild
|
||||
? new Set<SSRElement>(
|
||||
Array.from(mod.$$metadata.hoistedScriptPaths()).map((src) => ({
|
||||
props: { type: 'module', src },
|
||||
children: '',
|
||||
}))
|
||||
)
|
||||
: new Set<SSRElement>();
|
||||
|
||||
// Inject HMR scripts
|
||||
if (mode === 'development' && astroConfig.buildOptions.experimentalStaticBuild) {
|
||||
scripts.add({
|
||||
props: { type: 'module', src: '/@vite/client' },
|
||||
children: '',
|
||||
});
|
||||
scripts.add({
|
||||
props: { type: 'module', src: new URL('../../runtime/client/hmr.js', import.meta.url).pathname },
|
||||
children: '',
|
||||
});
|
||||
}
|
||||
|
||||
const result = createResult({ astroConfig, logging, origin, params, pathname, renderers, scripts });
|
||||
// Resolves specifiers in the inline hydrated scripts, such as "@astrojs/renderer-preact/client.js"
|
||||
result.resolve = async (s: string) => {
|
||||
// The legacy build needs these to remain unresolved so that vite HTML
|
||||
// Can do the resolution. Without this condition the build output will be
|
||||
// broken in the legacy build. This can be removed once the legacy build is removed.
|
||||
if (astroConfig.buildOptions.experimentalStaticBuild) {
|
||||
const [, resolvedPath] = await viteServer.moduleGraph.resolveUrl(s);
|
||||
return resolvedPath;
|
||||
} else {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
|
||||
let html = await renderPage(result, Component, pageProps, null);
|
||||
|
||||
// inject tags
|
||||
const tags: vite.HtmlTagDescriptor[] = [];
|
||||
|
||||
// dev only: inject Astro HMR client
|
||||
if (mode === 'development' && !astroConfig.buildOptions.experimentalStaticBuild) {
|
||||
tags.push({
|
||||
tag: 'script',
|
||||
attrs: { type: 'module' },
|
||||
// HACK: inject the direct contents of our `astro/runtime/client/hmr.js` to ensure
|
||||
// `import.meta.hot` is properly handled by Vite
|
||||
children: await getHmrScript(),
|
||||
injectTo: 'head',
|
||||
});
|
||||
}
|
||||
|
||||
// inject CSS
|
||||
[...getStylesForURL(filePath, viteServer)].forEach((href) => {
|
||||
if (mode === 'development' && svelteStylesRE.test(href)) {
|
||||
tags.push({
|
||||
tag: 'script',
|
||||
attrs: { type: 'module', src: href },
|
||||
injectTo: 'head',
|
||||
});
|
||||
} else {
|
||||
tags.push({
|
||||
tag: 'link',
|
||||
attrs: {
|
||||
rel: 'stylesheet',
|
||||
href,
|
||||
'data-astro-injected': true,
|
||||
},
|
||||
injectTo: 'head',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// add injected tags
|
||||
html = injectTags(html, tags);
|
||||
|
||||
// run transformIndexHtml() in dev to run Vite dev transformations
|
||||
if (mode === 'development' && !astroConfig.buildOptions.experimentalStaticBuild) {
|
||||
const relativeURL = filePath.href.replace(astroConfig.projectRoot.href, '/');
|
||||
html = await viteServer.transformIndexHtml(relativeURL, html, pathname);
|
||||
}
|
||||
|
||||
// inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?)
|
||||
if (!/<!doctype html/i.test(html)) {
|
||||
html = '<!DOCTYPE html>\n' + html;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
let hmrScript: string;
|
||||
async function getHmrScript() {
|
||||
if (hmrScript) return hmrScript;
|
||||
const filePath = fileURLToPath(new URL('../../runtime/client/hmr.js', import.meta.url));
|
||||
const content = await fs.promises.readFile(filePath);
|
||||
hmrScript = content.toString();
|
||||
return hmrScript;
|
||||
}
|
||||
|
||||
export async function ssr(ssrOpts: SSROptions): Promise<string> {
|
||||
try {
|
||||
const [renderers, mod] = await preload(ssrOpts);
|
||||
return await render(renderers, mod, ssrOpts); // note(drew): without "await", errors won’t get caught by errorHandler()
|
||||
} catch (e: unknown) {
|
||||
await errorHandler(e, { viteServer: ssrOpts.viteServer, filePath: ssrOpts.filePath });
|
||||
throw e;
|
||||
}
|
||||
}
|
|
@ -157,7 +157,7 @@ export async function renderComponent(result: SSRResult, displayName: string, Co
|
|||
}
|
||||
const probableRendererNames = guessRenderers(metadata.componentUrl);
|
||||
|
||||
if (Array.isArray(renderers) && renderers.length === 0 && typeof Component !== 'string' && !HTMLElement.isPrototypeOf(Component as object)) {
|
||||
if (Array.isArray(renderers) && renderers.length === 0 && typeof Component !== 'string' && !componentIsHTMLElement(Component)) {
|
||||
const message = `Unable to render ${metadata.displayName}!
|
||||
|
||||
There are no \`renderers\` set in your \`astro.config.mjs\` file.
|
||||
|
@ -175,7 +175,7 @@ Did you mean to enable ${formatList(probableRendererNames.map((r) => '`' + r + '
|
|||
}
|
||||
}
|
||||
|
||||
if (!renderer && HTMLElement.isPrototypeOf(Component as object)) {
|
||||
if (!renderer && typeof HTMLElement === 'function' && componentIsHTMLElement(Component)) {
|
||||
const output = renderHTMLElement(result, Component as typeof HTMLElement, _props, slots);
|
||||
|
||||
return output;
|
||||
|
@ -465,6 +465,10 @@ export async function renderAstroComponent(component: InstanceType<typeof AstroC
|
|||
return unescapeHTML(await _render(template));
|
||||
}
|
||||
|
||||
function componentIsHTMLElement(Component: unknown) {
|
||||
return typeof HTMLElement !== 'undefined' && HTMLElement.isPrototypeOf(Component as object);
|
||||
}
|
||||
|
||||
export async function renderHTMLElement(result: SSRResult, constructor: typeof HTMLElement, props: any, slots: any) {
|
||||
const name = getHTMLElementName(constructor);
|
||||
|
||||
|
|
|
@ -2,17 +2,16 @@ import type vite from '../core/vite';
|
|||
import type http from 'http';
|
||||
import type { AstroConfig, ManifestData, RouteData } from '../@types/astro';
|
||||
import { info, LogOptions } from '../core/logger.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createRouteManifest, matchRoute } from '../core/ssr/routing.js';
|
||||
import { createRouteManifest, matchRoute } from '../core/routing/index.js';
|
||||
import mime from 'mime';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { createSafeError } from '../core/util.js';
|
||||
import { ssr } from '../core/ssr/index.js';
|
||||
import { ssr } from '../core/render/dev/index.js';
|
||||
import * as msg from '../core/messages.js';
|
||||
|
||||
import notFoundTemplate, { subpathNotUsedTemplate } from '../template/4xx.js';
|
||||
import serverErrorTemplate from '../template/5xx.js';
|
||||
import { RouteCache } from '../core/ssr/route-cache.js';
|
||||
import { RouteCache } from '../core/render/route-cache.js';
|
||||
|
||||
interface AstroPluginOptions {
|
||||
config: AstroConfig;
|
||||
|
@ -126,7 +125,6 @@ export default function createPlugin({ config, logging }: AstroPluginOptions): v
|
|||
return {
|
||||
name: 'astro:server',
|
||||
configureServer(viteServer) {
|
||||
const pagesDirectory = fileURLToPath(config.pages);
|
||||
let routeCache = new RouteCache(logging);
|
||||
let manifest: ManifestData = createRouteManifest({ config: config }, logging);
|
||||
/** rebuild the route cache + manifest, as needed. */
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type vite from '../core/vite';
|
||||
|
||||
import { STYLE_EXTENSIONS } from '../core/ssr/css.js';
|
||||
import { STYLE_EXTENSIONS } from '../core/render/dev/css.js';
|
||||
|
||||
export type TransformHook = (code: string, id: string, ssr?: boolean) => Promise<vite.TransformResult>;
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import type { RenderedChunk } from 'rollup';
|
||||
import type { BuildInternals } from '../core/build/internal';
|
||||
|
||||
import * as path from 'path';
|
||||
import esbuild from 'esbuild';
|
||||
import { Plugin as VitePlugin } from '../core/vite';
|
||||
import { isCSSRequest } from '../core/ssr/css.js';
|
||||
import { isCSSRequest } from '../core/render/dev/css.js';
|
||||
|
||||
const PLUGIN_NAME = '@astrojs/rollup-plugin-build-css';
|
||||
|
||||
|
|
|
@ -12,10 +12,10 @@ import { getAttribute, hasAttribute, insertBefore, remove, createScript, createE
|
|||
import { addRollupInput } from './add-rollup-input.js';
|
||||
import { findAssets, findExternalScripts, findInlineScripts, findInlineStyles, getTextContent, getAttributes } from './extract-assets.js';
|
||||
import { isBuildableImage, isBuildableLink, isHoistedScript, isInSrcDirectory, hasSrcSet } from './util.js';
|
||||
import { render as ssrRender } from '../core/ssr/index.js';
|
||||
import { render as ssrRender } from '../core/render/dev/index.js';
|
||||
import { getAstroStyleId, getAstroPageStyleId } from '../vite-plugin-build-css/index.js';
|
||||
import { prependDotSlash, removeEndingForwardSlash } from '../core/path.js';
|
||||
import { RouteCache } from '../core/ssr/route-cache.js';
|
||||
import { RouteCache } from '../core/render/route-cache.js';
|
||||
|
||||
// This package isn't real ESM, so have to coerce it
|
||||
const matchSrcset: typeof srcsetParse = (srcsetParse as any).default;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { expect } from 'chai';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createRouteManifest } from '../dist/core/ssr/routing.js';
|
||||
import { createRouteManifest } from '../dist/core/routing/index.js';
|
||||
|
||||
const cwd = new URL('./fixtures/route-manifest/', import.meta.url);
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import fs from 'fs';
|
||||
import { execa } from 'execa';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
|
||||
// NOTE: Only needed for Windows, due to a Turbo bug.
|
||||
// Once Turbo works on Windows, we can remove this script
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -160,6 +160,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@astropub/webapi/-/webapi-0.10.11.tgz#7cf3926bb24f474b344025fa437d08fa71d0a6b9"
|
||||
integrity sha512-i1Aw6Px3n+x0GGbZxoQc6bY2gxks//rPUwuX4ICuNai8GvK/6j/0OGMVkD3ZlVJ/zfUtds8BDx/k+TJYaAPlKQ==
|
||||
|
||||
"@astropub/webapi@^0.10.13":
|
||||
version "0.10.13"
|
||||
resolved "https://registry.yarnpkg.com/@astropub/webapi/-/webapi-0.10.13.tgz#28f95706d1e9041495347465fb347eb78035cdd7"
|
||||
integrity sha512-efUVnq9IWPHYl5nxSLkDZzp1RvNmKpYApcHhgQnN2A+4D8z6dnTYlXo5Ogl0aAJWMMBKN89Q2GJwDF0zy8Lonw==
|
||||
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789"
|
||||
|
@ -6572,16 +6577,11 @@ preact-render-to-string@^5.1.19:
|
|||
dependencies:
|
||||
pretty-format "^3.8.0"
|
||||
|
||||
preact@^10.6.5:
|
||||
preact@^10.6.5, preact@~10.6.5:
|
||||
version "10.6.5"
|
||||
resolved "https://registry.yarnpkg.com/preact/-/preact-10.6.5.tgz#726d8bd12903a0d51cdd17e2e1b90cc539403e0c"
|
||||
integrity sha512-i+LXM6JiVjQXSt2jG2vZZFapGpCuk1fl8o6ii3G84MA3xgj686FKjs4JFDkmUVhtxyq21+4ay74zqPykz9hU6w==
|
||||
|
||||
preact@~10.5.15:
|
||||
version "10.5.15"
|
||||
resolved "https://registry.yarnpkg.com/preact/-/preact-10.5.15.tgz#6df94d8afecf3f9e10a742fd8c362ddab464225f"
|
||||
integrity sha512-5chK29n6QcJc3m1lVrKQSQ+V7K1Gb8HeQY6FViQ5AxCAEGu3DaHffWNDkC9+miZgsLvbvU9rxbV1qinGHMHzqA==
|
||||
|
||||
prebuild-install@^7.0.0:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.0.1.tgz#c10075727c318efe72412f333e0ef625beaf3870"
|
||||
|
|
Loading…
Reference in a new issue