@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 +7 -0
- package/README.md +5 -1
- package/dist/index.js +2 -0
- package/dist/internal/browser-router/router.js +26 -6
- package/dist/internal/postbuild.js +8 -0
- package/dist/internal/prerender.d.ts +11 -0
- package/dist/internal/prerender.js +13 -0
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
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 =
|
|
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
|
-
|
|
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,
|
|
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
package/package.json
CHANGED