npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jk2908/solas

v0.5.4

Published

A Vite + React meta-framework exploring streaming, Server Components, and partial prerendering. Designed for simplicity and lightness

Readme

Solas

Solas is a minimal React meta-framework powered by Vite, created for experimenting with routing, streaming, and prerendering with React Server Components.

Solas is experimental and currently has no automated test suite, so expect rough edges.

Install

npm install @jk2908/solas react react-dom react-server-dom-webpack vite
npm install -D @vitejs/plugin-react typescript vite-tsconfig-paths

Use

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.tsx

Use these filename conventions:

  • +layout.tsx: shared layout for a route branch.
  • +page.tsx: page component for a route.
  • +endpoint.ts: request handler. Can be placed in any folder and responds to all HTTP methods for its route path.
  • +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.

If a route has both +page.tsx and +endpoint.ts, Solas selects the GET handler by Accept header:

  • Accept: text/html or text/x-component: render +page.tsx
  • other GET requests (for example application/json): run +endpoint.ts GET

Non-GET methods (POST, PUT, PATCH, DELETE) always run +endpoint.ts.

Client Routing

Import client navigation helpers from @jk2908/solas/router.

import { Link, useRouter } from '@jk2908/solas/router'

Solas generates route types for your app. Link and router.go(...) use those generated route types for autocomplete and type checking:

<Link href="/posts" />
<Link href="/p/:id" params={{ id: 'post-1' }} />

await router.go('/posts')
await router.go('/p/:id', { params: { id: 'post-1' } })

That gives you:

  • autocomplete for known route paths
  • required params for dynamic routes like /p/:id
  • rejected params for static routes that do not accept them
  • typed query and navigation options on router.go(...)

If you already have a concrete path, you can pass that directly instead of using route params:

;<Link href={`/p/${post.id}`} />

await router.go(`/p/${post.id}`)

In that form the path is already resolved, so params are not used. Typed Link and router.go(...) only allow params when you pass a route pattern like /p/:id.

If the target already contains a query string and you also pass query, Solas merges them. Existing query entries are kept unless you override the same key in query, and explicit query values win:

<Link href={`/p/${post.id}?tab=summary`} query={{ draft: true, tab: 'full' }} />

That resolves to /p/${post.id}?tab=full&draft=true.

Use Link for same-origin app navigation. Prefetching is opt-in:

<Link href="/posts">Posts</Link>
<Link href="/posts" prefetch="intent">Prefetch on focus or touch</Link>
<Link href="/posts" prefetch="hover">Prefetch on hover</Link>
<Link href="/p/:id" params={{ id: 'post-1' }}>Typed params</Link>

prefetch="none" is the default. Solas does not automatically prefetch routes unless you opt in with Link or call router.prefetch(...) yourself.

Use useRouter() inside client components for programmatic navigation, prefetching, and refreshing the current route:

'use client'

import { useRouter } from '@jk2908/solas/router'

export function Controls() {
	const router = useRouter()

	return (
		<>
			<button type="button" onClick={() => router.go('/posts')}>
				Go to posts
			</button>

			<button type="button" onMouseEnter={() => router.prefetch('/posts')}>
				Prefetch posts
			</button>

			<button type="button" onClick={() => router.refresh()}>
				Refresh current route
			</button>
		</>
	)
}

router.go(...) and router.refresh() both return promises, so you can await either of them when you need to sequence work after navigation completes:

const finalPath = await router.go('/posts')
await router.refresh()

router.refresh() always refreshes the current browser location at the moment you call it. If you call it after await router.go('/posts'), it refreshes /posts (or the final redirected path).

router.go(...) accepts route params and query values using the same typed route rules as Link:

await router.go('/p/:id', {
	params: { id: 'post-2' },
	query: { draft: true },
})

Those same generated route types can also be reused outside navigation helpers when you want route params to stay typed in page exports:

import type { Route, Solas } from '@jk2908/solas'

export const metadata: Route.Metadata<Solas.Routes['/writing/:slug']> = ({ params }) => {
	const post = allPosts?.find(p => p.__mdsrc.slug === params?.slug)

	return {
		title: post?.title ?? 'Post not found',
	}
}

export const params: Route.StaticParams<Solas.Routes['/writing/:slug']> = () =>
	allPosts?.map(p => ({ slug: p.__mdsrc.slug })) ?? []

That keeps your route params aligned across links, imperative navigation, metadata, and static params.

router.refresh() clears the cached RSC response for the current path and fetches a fresh payload, so it is most useful for routes that render request-time data. router.isNavigating exposes pending client-side navigation state.

Config

All Solas options are passed to solas() inside defineConfig.

Runtime

Solas uses a runtime for filesystem access, mime lookup, and hashing.

This is not a deployment or packaging adapter. Platform adapters are not available yet.

Use the runtime config key to select the runtime used by Solas server code.

Supported values are 'auto', 'node', and 'bun'.

solas() defaults to runtime: 'auto'. In a Bun process, that selects Bun. Otherwise it falls back to Node.

If you already run Vite through Bun, runtime: 'auto' is usually enough and Solas will detect Bun automatically.

Node runtime

If you want to pin Node explicitly, set runtime: 'node':

import { defineConfig } from 'vite'

import solas from '@jk2908/solas'
import react from '@vitejs/plugin-react'

export default defineConfig({
	plugins: [solas({ runtime: 'node' }), react()],
})

Use the normal Vite commands with the Node runtime:

{
	"scripts": {
		"dev": "vite dev",
		"build": "vite build",
		"preview": "vite preview"
	}
}

Bun runtime

If you want Solas runtime code to execute in Bun, set runtime: 'bun':

import { defineConfig } from 'vite'

import solas from '@jk2908/solas'
import react from '@vitejs/plugin-react'

export default defineConfig({
	plugins: [solas({ runtime: 'bun' }), react()],
})

When you use the Bun runtime, run Vite through Bun so the server/runtime code executes in a Bun process:

{
	"scripts": {
		"dev": "bunx --bun vite dev",
		"build": "bunx --bun vite build",
		"preview": "bunx --bun vite preview"
	}
}

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 url option passed to solas()
  • VITE_APP_URL

Solas exposes the resolved value as import.meta.env.VITE_APP_URL. If url is set, prerender also uses it as the request origin for build-time renders.

In practice, you only need url if your app reads import.meta.env.VITE_APP_URL or 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.com

port

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 browser-served build assets (like .js, .css, etc.).

Default: false

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 = false

In 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() {
	await 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 /about
  • always: /about redirects 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',
		}),
	],
})

trustedOrigins

Use trustedOrigins to allow specific origins to make cross-origin browser submissions to your app.

Default: []

By default, only same-origin browser requests are allowed. Add a trusted origin when a third-party service needs to submit through the user's browser, such as a payment gateway or identity provider.

This setting controls which cross-origin browser sources are allowed for unsafe requests. See Security > CSRF for enforcement details.

Each value must be a complete origin including protocol:

export default defineConfig({
	plugins: [
		solas({
			trustedOrigins: ['https://payments.example.com', 'https://login.example.com'],
		}),
	],
})

Only add origins you completely trust.

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 everything
  • info: the default
  • warn: only warnings and errors
  • error: only errors
  • fatal: 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": "vite dev",
		"build": "vite build",
		"preview": "vite preview"
	}
}

These defaults assume either runtime: 'node' or runtime: 'auto' while running Vite under Node. If you set runtime: 'bun', or let runtime: 'auto' resolve to Bun by running Vite through Bun, use the Bun-backed commands shown in the runtime section above.

Commands

  • vite dev starts the development server.
  • vite build creates a production build. Solas finalizes that build by prerendering configured routes, writing the runtime manifest, generating sitemap.xml when enabled, and precompressing output when enabled.
  • vite preview serves the built app for local verification.

Security

CSRF

Solas protects server actions and +endpoint handlers against CSRF.

Server actions are always POST requests. +endpoint handlers are protected on browser-initiated POST, PUT, PATCH, and DELETE requests.

By default, browser requests must be same-origin.

When available, Solas checks browser provenance using:

  • Sec-Fetch-Site
  • Origin
  • Referer

Solas also considers the effective request origin when your app is behind a proxy by using X-Forwarded-Host, X-Forwarded-Proto, and config.url when present.

If you need to allow a trusted third-party browser POST source, configure it explicitly:

export default defineConfig({
	plugins: [
		solas({
			trustedOrigins: ['https://payments.example.com'],
		}),
	],
})

Requests from non-browser callers that do not send browser provenance headers are allowed by default, so typical server-to-server integrations and webhooks continue to work.

Cookie-backed app mutations should keep the default same-origin protection and only use trustedOrigins for narrowly scoped integrations that genuinely need cross-origin browser submissions.