@jk2908/solas 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0
4
+
5
+ ### Features
6
+
7
+ - The static adapter supports client-side navigation. The build emits each fully prerendered route's RSC payload as `<route>.rsc.txt` next to its HTML, and the browser router fetches it, so a static host needs no server to answer navigations.
8
+ - The active adapter's name is exposed as `import.meta.env.SOLAS_ADAPTER`, so apps can branch on the platform.
9
+
3
10
  ## 0.11.1
4
11
 
5
12
  ### Fixes
package/README.md CHANGED
@@ -229,6 +229,8 @@ Solas resolves the origin in this order:
229
229
 
230
230
  The resolved origin is exposed to your app as `import.meta.env.VITE_APP_URL` and is used for build-time prerender requests and `sitemap` URLs. When `url` is set, it is also used as a trusted origin for CSRF.
231
231
 
232
+ The active adapter's name is exposed as `import.meta.env.SOLAS_ADAPTER`, so your app can branch on the platform it is deployed to.
233
+
232
234
  You usually only need `url` in production, where the Vite dev server origin is meaningless. For example, to generate a `sitemap.xml` with real URLs:
233
235
 
234
236
  ```ts
@@ -707,7 +709,9 @@ export default defineConfig({
707
709
  })
708
710
  ```
709
711
 
710
- `vite build` writes the site to `build/` (override with `outDir`): the client assets, each route as `<route>.html` (or `<route>/index.html` under `trailingSlash: 'always'`), plus `sitemap.xml` and `robots.txt`. Serve that directory with anything — `npx serve build`, GitHub Pages, Netlify, an S3 bucket, nginx.
712
+ `vite build` writes the site to `build/` (override with `outDir`): the client assets, each route as `<route>.html` (or `<route>/index.html` under `trailingSlash: 'always'`), its RSC payload as `<route>.rsc.txt`, plus `sitemap.xml` and `robots.txt`. Serve that directory with anything — `npx serve build`, GitHub Pages, Netlify, an S3 bucket, nginx.
713
+
714
+ Client-side navigation works with no server: the browser router fetches each route's `<route>.rsc.txt` payload instead of asking a server for `text/x-component`.
711
715
 
712
716
  Because it forces full prerendering, routes that need a server are not a fit: per-route PPR or SSR opt-ins and dynamic routes without `params()`.
713
717
 
package/dist/index.js CHANGED
@@ -261,6 +261,7 @@ function solas(c) {
261
261
  viteConfig.define ??= {};
262
262
  viteConfig.define['import.meta.env.VITE_APP_URL'] = JSON.stringify(resolvedUrl);
263
263
  viteConfig.define['import.meta.env.SOLAS_VERSION'] = JSON.stringify(pkg.version);
264
+ viteConfig.define['import.meta.env.SOLAS_ADAPTER'] = JSON.stringify(config.adapter?.name ?? '');
264
265
  viteConfig.optimizeDeps ??= {};
265
266
  viteConfig.optimizeDeps.exclude = [
266
267
  ...new Set([
@@ -335,6 +336,7 @@ function solas(c) {
335
336
  precompress: config.precompress,
336
337
  trailingSlash: config.trailingSlash,
337
338
  url: resolvedUrl,
339
+ adapter: config.adapter?.name ?? '',
338
340
  }));
339
341
  },
340
342
  buildApp: {
@@ -28,6 +28,25 @@ const DEFAULT_GO_CONFIG = {
28
28
  };
29
29
  const logger = new Logger();
30
30
  const responseCache = new ResponseCache();
31
+ const STATIC = import.meta.env.SOLAS_ADAPTER === 'static';
32
+ /**
33
+ * The URL to fetch the response for a navigation. On a static host that is the
34
+ * pre-generated RSC payload; otherwise the path itself carries the RSC response
35
+ * for an `Accept: text/x-component` request.
36
+ */
37
+ function getPayloadUrl(path) {
38
+ const url = new URL(path, window.location.origin);
39
+ url.pathname = url.pathname.endsWith('/')
40
+ ? `${url.pathname}index.rsc.txt`
41
+ : `${url.pathname}.rsc.txt`;
42
+ url.search = '';
43
+ return url;
44
+ }
45
+ function performNavigationFetch(path, init) {
46
+ return STATIC
47
+ ? fetch(getPayloadUrl(path), init)
48
+ : fetch(path, { ...init, headers: { ...init?.headers, accept: 'text/x-component' } });
49
+ }
31
50
  export function BrowserRouterProvider({ children, setPayload, isNavigating = false, url, }) {
32
51
  const id = useRef(0);
33
52
  const controller = useRef(null);
@@ -79,10 +98,7 @@ export function BrowserRouterProvider({ children, setPayload, isNavigating = fal
79
98
  if (!promise) {
80
99
  const ctrl = new AbortController();
81
100
  controller.current = ctrl;
82
- promise = fetch(path, {
83
- headers: { accept: 'text/x-component' },
84
- signal: ctrl.signal,
85
- });
101
+ promise = performNavigationFetch(path, { signal: ctrl.signal });
86
102
  responseCache.set(path, promise);
87
103
  }
88
104
  if (navigationId !== id.current)
@@ -91,7 +107,11 @@ export function BrowserRouterProvider({ children, setPayload, isNavigating = fal
91
107
  promise,
92
108
  createFromFetch(promise),
93
109
  ]);
94
- const resolvedPath = ResponseCache.toCacheKey(res.url, window.location.origin) ?? path;
110
+ // on a static host the payload comes from `<path>.rsc.txt`, so the
111
+ // response URL is not the page path
112
+ const resolvedPath = STATIC
113
+ ? path
114
+ : (ResponseCache.toCacheKey(res.url, window.location.origin) ?? path);
95
115
  if (navigationId !== id.current)
96
116
  return resolvedPath;
97
117
  if (resolvedPath !== path) {
@@ -136,7 +156,7 @@ export function BrowserRouterProvider({ children, setPayload, isNavigating = fal
136
156
  return;
137
157
  if (responseCache.has(key))
138
158
  return;
139
- responseCache.set(key, fetch(key, { headers: { Accept: 'text/x-component' } }));
159
+ responseCache.set(key, performNavigationFetch(key));
140
160
  }, []);
141
161
  /**
142
162
  * Refreshes the current page by re-fetching the RSC response for the current path and updating the
@@ -114,6 +114,14 @@ export async function postbuild(cwd = process.cwd()) {
114
114
  const staticPath = path.join(staticRoot, Prerender.staticRoutePath(route, staticMode));
115
115
  await fs.mkdir(path.dirname(staticPath), { recursive: true });
116
116
  await fs.writeFile(staticPath, artifact.html);
117
+ // a static host has no server to answer an RSC navigation, so emit
118
+ // the payload next to the HTML for the client router to fetch
119
+ if (manifest.adapter === 'static') {
120
+ await fs.writeFile(staticPath.replace(/\.html$/, '.rsc.txt'), await Prerender.getRscPayload(app, route, {
121
+ origin: manifest.url,
122
+ base: manifest.base,
123
+ }));
124
+ }
117
125
  artfifactManifest[route] = {
118
126
  mode: artifact.mode,
119
127
  files: ['html'],
@@ -122,6 +122,17 @@ export declare function get(app: {
122
122
  route: string;
123
123
  artifact: any;
124
124
  }>;
125
+ /**
126
+ * Fetch the RSC payload for a route, the response a client would get for an RSC
127
+ * navigation. Written alongside a fully prerendered route's HTML so a static
128
+ * host can serve client-side navigation without a server.
129
+ */
130
+ export declare function getRscPayload(app: {
131
+ fetch: (req: Request) => Promise<Response>;
132
+ }, route: string, opts: {
133
+ origin?: string;
134
+ base?: string;
135
+ }): Promise<string>;
125
136
  /**
126
137
  * Run the prerendering process for a list of routes with a specified concurrency limit,
127
138
  * by calling the 'get' function for each route and yielding the results as they become available.
@@ -331,6 +331,19 @@ export async function get(app, route, opts) {
331
331
  return { route, status: res.status };
332
332
  return { route, artifact: await res.json() };
333
333
  }
334
+ /**
335
+ * Fetch the RSC payload for a route, the response a client would get for an RSC
336
+ * navigation. Written alongside a fully prerendered route's HTML so a static
337
+ * host can serve client-side navigation without a server.
338
+ */
339
+ export async function getRscPayload(app, route, opts) {
340
+ const url = `${opts.origin ?? `http://${Config.SLUG}.local`}${applyBasePath(route, opts.base)}`;
341
+ const res = await app.fetch(new Request(url, { headers: { Accept: 'text/x-component' } }));
342
+ if (!(res instanceof Response) || !res.ok) {
343
+ throw new Error(`[prerender] RSC payload for ${route} was not served`);
344
+ }
345
+ return res.text();
346
+ }
334
347
  /**
335
348
  * Run the prerendering process for a list of routes with a specified concurrency limit,
336
349
  * by calling the 'get' function for each route and yielding the results as they become available.
package/dist/types.d.ts CHANGED
@@ -130,6 +130,7 @@ export type BuildManifest = {
130
130
  precompress: boolean;
131
131
  trailingSlash: (typeof Config.TRAILING_SLASH_MODES)[number];
132
132
  url?: string;
133
+ adapter?: string;
133
134
  };
134
135
  export declare namespace Route {
135
136
  type ParamsOf<TRoute> = TRoute extends {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jk2908/solas",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "A Vite + React meta-framework exploring streaming, Server Components, and partial prerendering. Designed for simplicity and lightness",
5
5
  "keywords": [
6
6
  "framework",