@jk2908/solas
v0.2.3
Published
A React Server Components meta-framework powered by Vite
Downloads
629
Readme
Solas
Solas is a minimal React meta-framework powered by Vite, created for experimenting with routing, streaming, and prerendering with React Server Components.
It has not been rigorously tested yet (there are currently no automated tests) ... and broken behaviour should be expected.
Install
npm install @jk2908/solas react react-dom react-server-dom-webpack vite
npm install -D @vitejs/plugin-react typescript vite-tsconfig-pathsUse
Create a Vite config that registers Solas.
import { defineConfig } from 'vite'
import solas from '@jk2908/solas'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [solas(), react()],
})Structure
Put your routes in app/.
app/
+layout.tsx
+page.tsx
+middleware.ts
+loading.tsx
+401.tsx
+403.tsx
+404.tsx
+500.tsx
about/
+layout.tsx
+page.tsx
api/
+endpoint.ts
posts/
[id]/
+page.tsxUse these filename conventions:
+layout.tsx: shared layout for a route branch.+page.tsx: page component for a route.+endpoint.ts: request handler for non-page routes.+middleware.ts: middleware that runs for the current route branch and is inherited by child routes. Parent and child middleware stack together.+loading.tsx: loading fallback inherited by child routes.+401.tsx: boundary for unauthorised responses in the current route branch and its children.+403.tsx: boundary for forbidden responses in the current route branch and its children.+404.tsx: boundary for not found responses in the current route branch and its children.+500.tsx: boundary for server errors in the current route branch and its children.
Nested folders create nested routes. Dynamic segments use [param], and catch-all segments use [...param].
Status boundaries follow the same override pattern as layouts: a child route uses the nearest matching boundary file above it, and a more specific boundary replaces a parent one.
Config
All Solas options are passed to solas() inside defineConfig.
url
url is optional. If you set it, Solas treats it as the public origin for your app.
Solas resolves it in this order:
- the
urloption passed tosolas() VITE_APP_URL
Current behaviour:
- Solas reads that value during plugin configuration.
- Solas exposes the resolved value as
import.meta.env.VITE_APP_URL. - If
urlis set, prerender uses it as the request origin for build-time renders. - The runtime router does not otherwise require
config.urlfor routing to work.
In practice, you only need url if your app code wants to read the public origin from import.meta.env.VITE_APP_URL, or if your prerendered output needs a real public origin.
If you do want to set it explicitly, this is the shape:
export default defineConfig(({ mode }) => ({
plugins: [
solas({
url: mode === 'production' ? 'https://example.com' : 'http://localhost:8787',
}),
],
}))If you prefer an environment variable, set this instead:
VITE_APP_URL=https://example.comport
Use port to change the development server port.
Default: 8787
export default defineConfig({
plugins: [
solas({
port: 4000,
}),
],
})precompress
Use precompress to control whether Solas writes compressed build assets.
Default: true
export default defineConfig({
plugins: [
solas({
precompress: false,
}),
],
})prerender
Use prerender to set the default prerender mode for the app. Valid values are full, ppr, and false.
Default: false
false: do not prerender the route.full: render the full route to HTML at build time.ppr: prerender a static shell and defer dynamic regions to request time.
export default defineConfig({
plugins: [
solas({
prerender: 'ppr',
}),
],
})This value is only the default. Route files can override it with export const prerender = ..., and the nearest explicit export wins.
// vite.config.ts
export default defineConfig({
plugins: [solas({ prerender: 'full' })],
})// app/about/+layout.tsx
export const prerender = 'ppr'// app/about/team/+page.tsx
export const prerender = falseIn that example, the app default is full, the about layout overrides it to ppr, and the page overrides it again to false.
For dynamic routes, prerendering uses the params you export from the page:
export const params = () => [{ id: 'post-1' }, { id: 'post-2' }]
export const prerender = 'full'In ppr mode, Solas prerenders the shell and lets you defer parts of the tree to request time.
Use dynamic() inside a Suspense boundary to mark a subtree as request-time only:
import { Suspense } from 'react'
import { dynamic } from '@jk2908/solas/server'
export const prerender = 'ppr'
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Ts />
</Suspense>
)
}
async function Ts() {
dynamic()
return <div>{Date.now()}</div>
}During prerender, dynamic() suspends so the nearest Suspense fallback is written into the static shell. At request time, the deferred content resolves normally.
If you call dynamic() outside ppr mode, Solas does not defer that subtree. In full mode it logs a warning and the component still renders at build time.
headers(), cookies(), and url() also mark the current render path as dynamic, so they should be treated the same way when you are building a ppr shell.
metadata
Use metadata to set default document metadata.
export default defineConfig({
plugins: [
solas({
metadata: {
title: '%s - Solas',
meta: [
{
name: 'description',
content: 'My Solas app',
},
],
},
}),
],
})This is also only the default. Route metadata is merged in order, so config metadata can be extended or overridden by the shell, layouts, page, and status boundaries. The later, more specific route metadata wins for titles and duplicate tags.
// vite.config.ts
solas({
metadata: {
title: '%s - Solas',
},
})
// app/+layout.tsx
export const metadata = {
title: 'Docs',
}
// app/guides/+page.tsx
export const metadata = {
title: 'Routing',
}In that example, the final page title becomes Routing - Solas.
trailingSlash
Use trailingSlash to set the app-wide URL policy.
Default: never
never:/about/redirects to/aboutalways:/aboutredirects to/about/ignore: both forms resolve without a canonical redirect
This is a global setting in solas(). Solas does not read trailingSlash from route files.
Prerendered output follows the same policy. always writes route HTML as about/index.html, while never and ignore write it as about.html.
export default defineConfig({
plugins: [
solas({
trailingSlash: 'always',
}),
],
})sitemap
Use sitemap to generate a sitemap.xml at build time.
Default: false
When enabled, Solas writes a sitemap containing all routes with deterministic URLs: static routes, prerendered routes, and dynamic routes resolved via params. The origin for each URL comes from config.url.
export default defineConfig(({ mode }) => ({
plugins: [
solas({
url: mode === 'production' ? 'https://example.com' : 'http://localhost:8787',
sitemap: true,
}),
],
}))Routes with dynamic segments ([id]) or catch-all segments ([...param]) are only included if they export params and prerender.
To add routes that Solas cannot discover automatically (for example, catch-all routes backed by a CMS), pass an object with a routes function. The function receives the auto-discovered routes and returns the final list:
export default defineConfig(({ mode }) => ({
plugins: [
solas({
url: mode === 'production' ? 'https://example.com' : 'http://localhost:8787',
sitemap: {
async routes(existing) {
const posts = await getPosts()
return [...existing, ...posts.map(p => `/blog/${p.slug}`)]
},
},
}),
],
}))The routes function can be async. The callback also lets you filter routes:
sitemap: {
routes: (r) => r.filter(route => !route.startsWith('/admin')),
}The sitemap is written to the build output directory as sitemap.xml after prerendering and before precompression.
logger.level
Use logger.level to control internal Solas logging.
Default: info
Valid values are debug, info, warn, error, and fatal.
debug: show everythinginfo: the defaultwarn: only warnings and errorserror: only errorsfatal: only fatal errors
This is mainly useful when debugging framework behaviour such as routing and prerendering. It is for Solas internals, not your app's general-purpose logging, and it does not control top-level CLI status output such as build and preview progress messages.
export default defineConfig({
plugins: [
solas({
logger: {
level: process.env.NODE_ENV === 'production' ? 'fatal' : 'info',
},
}),
],
})Scripts
Add scripts to your app:
{
"scripts": {
"dev": "solas dev",
"build": "solas build",
"preview": "solas preview"
}
}Commands
solas devstarts the development server.solas buildcreates a production build, prerenders configured routes, and writes compressed assets when enabled.solas previewserves the built app for local verification.
