@jk2908/solas 0.3.2 → 0.3.3
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 +4 -0
- package/dist/internal/env/flight.js +8 -5
- package/dist/internal/env/rsc.js +7 -6
- package/dist/internal/navigation/http-exception.d.ts +6 -1
- package/dist/internal/navigation/http-exception.js +18 -1
- package/dist/internal/render/tree.d.ts +2 -1
- package/dist/internal/render/tree.js +1 -1
- package/dist/internal/ui/defaults/error.d.ts +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.3 - 2026-04-23
|
|
4
|
+
|
|
5
|
+
- Fixed HTML missing-route rendering when Solas is installed from npm by serialising `HttpException` and `Error` values into transport-safe objects before they cross the RSC payload boundary, preserving the expected 404 flow instead of crashing during SSR.
|
|
6
|
+
|
|
3
7
|
## 0.3.2 - 2026-04-21
|
|
4
8
|
|
|
5
9
|
- Fixed PPR flight transport and closed-connection handling by replacing `rsc-html-stream` with the local runtime transport.
|
|
@@ -91,17 +91,20 @@ export function injectPayload(payload, opts = {}) {
|
|
|
91
91
|
let html = decoder.decode(chunk, { stream: true });
|
|
92
92
|
// hold the final closing tags back so payload scripts land inside the document,
|
|
93
93
|
// not after it
|
|
94
|
-
if (html.endsWith(HTML_TRAIL))
|
|
94
|
+
if (html.endsWith(HTML_TRAIL))
|
|
95
95
|
html = html.slice(0, -HTML_TRAIL.length);
|
|
96
|
-
|
|
96
|
+
// write the buffered html before the payload scripts, so they are guaranteed to be
|
|
97
|
+
// parsed in the right place
|
|
97
98
|
if (html)
|
|
98
99
|
controller.enqueue(encoder.encode(html));
|
|
99
100
|
}
|
|
100
101
|
// flush any decoder state left over from split utf-8/html chunks
|
|
101
102
|
let remaining = decoder.decode();
|
|
102
|
-
if
|
|
103
|
+
// if the remaining buffered html ends with the closing tags, remove them so they
|
|
104
|
+
// can be re-appended after the payload
|
|
105
|
+
if (remaining.endsWith(HTML_TRAIL))
|
|
103
106
|
remaining = remaining.slice(0, -HTML_TRAIL.length);
|
|
104
|
-
|
|
107
|
+
// if there is any html left after removing the closing tags, write it before the payload
|
|
105
108
|
if (remaining)
|
|
106
109
|
controller.enqueue(encoder.encode(remaining));
|
|
107
110
|
buffered = [];
|
|
@@ -159,7 +162,7 @@ async function writePayload(payload, controller, nonce) {
|
|
|
159
162
|
catch {
|
|
160
163
|
// most rows are text, but keep binary chunks intact when a payload
|
|
161
164
|
// row cannot be decoded as utf-8
|
|
162
|
-
const base64 = JSON.stringify(btoa(String.fromCodePoint(...chunk)));
|
|
165
|
+
const base64 = JSON.stringify(window.btoa(String.fromCodePoint(...chunk)));
|
|
163
166
|
writePayloadScript(`Uint8Array.from(atob(${base64}), value => value.codePointAt(0))`, controller, nonce);
|
|
164
167
|
}
|
|
165
168
|
}
|
package/dist/internal/env/rsc.js
CHANGED
|
@@ -6,7 +6,7 @@ import { createHttpRouter } from '../http-router/create-http-router.js';
|
|
|
6
6
|
import { HttpRouter } from '../http-router/router.js';
|
|
7
7
|
import { normalisePathname } from '../http-router/utils.js';
|
|
8
8
|
import { Metadata } from '../metadata.js';
|
|
9
|
-
import { HttpException, isHttpException } from '../navigation/http-exception.js';
|
|
9
|
+
import { HttpException, isHttpException, toHttpExceptionLike, } from '../navigation/http-exception.js';
|
|
10
10
|
import { Prerender } from '../prerender.js';
|
|
11
11
|
import { Tree } from '../render/tree.js';
|
|
12
12
|
import { Resolver } from '../resolver.js';
|
|
@@ -32,8 +32,8 @@ async function createPayload(req, manifest, importMap, baseMetadata, returnValue
|
|
|
32
32
|
// has been found, and we should server render a default
|
|
33
33
|
// error screen
|
|
34
34
|
if (!match) {
|
|
35
|
-
const error = req[Solas.Config.REQUEST_META_KEY].error ?? new HttpException(404, 'Not found');
|
|
36
|
-
const title = `${
|
|
35
|
+
const error = toHttpExceptionLike(req[Solas.Config.REQUEST_META_KEY].error ?? new HttpException(404, 'Not found'));
|
|
36
|
+
const title = `${error.status ? `${error.status} -` : ''}${error.message}`;
|
|
37
37
|
const rscPayload = {
|
|
38
38
|
root: (_jsxs("html", { lang: "en", children: [
|
|
39
39
|
_jsxs("head", { children: [
|
|
@@ -80,8 +80,9 @@ async function createPayload(req, manifest, importMap, baseMetadata, returnValue
|
|
|
80
80
|
const metadata = collection
|
|
81
81
|
.add(...(match.metadata?.({ params: match.params, error: match.error }) ?? []))
|
|
82
82
|
.run();
|
|
83
|
+
const error = match.error ? toHttpExceptionLike(match.error) : undefined;
|
|
83
84
|
const rscPayload = {
|
|
84
|
-
root: (_jsx(_Fragment, { children: _jsx(Tree, { depth: match.__depth, params: match.params, error:
|
|
85
|
+
root: (_jsx(_Fragment, { children: _jsx(Tree, { depth: match.__depth, params: match.params, error: error, ui: match.ui }) })),
|
|
85
86
|
returnValue,
|
|
86
87
|
formState,
|
|
87
88
|
metadata,
|
|
@@ -124,9 +125,9 @@ async function createPayload(req, manifest, importMap, baseMetadata, returnValue
|
|
|
124
125
|
? `${err.status} - ${err.message}`
|
|
125
126
|
: `500 - ${err.message}`
|
|
126
127
|
: '500 - Unknown server error';
|
|
127
|
-
const error = new Error(err instanceof Error ? err.message : 'Unknown server error', {
|
|
128
|
+
const error = toHttpExceptionLike(new Error(err instanceof Error ? err.message : 'Unknown server error', {
|
|
128
129
|
cause: err,
|
|
129
|
-
});
|
|
130
|
+
}));
|
|
130
131
|
return {
|
|
131
132
|
// this branch renders the minimal error shell after the
|
|
132
133
|
// main tree throws. We keep the same mode as the
|
|
@@ -7,6 +7,7 @@ export declare namespace HttpException {
|
|
|
7
7
|
};
|
|
8
8
|
}
|
|
9
9
|
export declare const HTTP_EXCEPTION_NAME_MAP: Record<HttpException.StatusCode, string>;
|
|
10
|
+
export type HttpExceptionLike = Pick<Error, 'name' | 'message' | 'stack'> & Partial<Pick<HttpException, 'digest' | 'payload' | 'status'>>;
|
|
10
11
|
/**
|
|
11
12
|
* An exception representing an HTTP error, with an optional payload
|
|
12
13
|
* and cause
|
|
@@ -21,9 +22,13 @@ export declare class HttpException extends Error {
|
|
|
21
22
|
export declare const HTTP_EXCEPTION_DIGEST_PREFIX = "HTTP_EXCEPTION";
|
|
22
23
|
/**
|
|
23
24
|
* Check if an error is an HTTPException
|
|
24
|
-
* @description uses the digest property to work across server/client boundaries
|
|
25
25
|
*/
|
|
26
26
|
export declare function isHttpException(err: unknown): err is HttpException;
|
|
27
|
+
/**
|
|
28
|
+
* Convert an HttpException or any Error into a plain object that can be
|
|
29
|
+
* safely serialised
|
|
30
|
+
*/
|
|
31
|
+
export declare function toHttpExceptionLike(error: HttpException | Error): HttpExceptionLike;
|
|
27
32
|
/**
|
|
28
33
|
* Throw an HTTPException
|
|
29
34
|
*/
|
|
@@ -25,7 +25,6 @@ export class HttpException extends Error {
|
|
|
25
25
|
export const HTTP_EXCEPTION_DIGEST_PREFIX = 'HTTP_EXCEPTION';
|
|
26
26
|
/**
|
|
27
27
|
* Check if an error is an HTTPException
|
|
28
|
-
* @description uses the digest property to work across server/client boundaries
|
|
29
28
|
*/
|
|
30
29
|
export function isHttpException(err) {
|
|
31
30
|
return (typeof err === 'object' &&
|
|
@@ -34,6 +33,24 @@ export function isHttpException(err) {
|
|
|
34
33
|
typeof err.digest === 'string' &&
|
|
35
34
|
err.digest.startsWith(HTTP_EXCEPTION_DIGEST_PREFIX));
|
|
36
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Convert an HttpException or any Error into a plain object that can be
|
|
38
|
+
* safely serialised
|
|
39
|
+
*/
|
|
40
|
+
export function toHttpExceptionLike(error) {
|
|
41
|
+
return {
|
|
42
|
+
name: error.name,
|
|
43
|
+
message: error.message,
|
|
44
|
+
stack: error.stack,
|
|
45
|
+
...('digest' in error && typeof error.digest === 'string'
|
|
46
|
+
? { digest: error.digest }
|
|
47
|
+
: {}),
|
|
48
|
+
...('payload' in error && error.payload !== undefined
|
|
49
|
+
? { payload: error.payload }
|
|
50
|
+
: {}),
|
|
51
|
+
...('status' in error ? { status: error.status } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
37
54
|
/**
|
|
38
55
|
* Throw an HTTPException
|
|
39
56
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Resolver } from '../resolver.js';
|
|
2
|
+
import { type HttpExceptionLike } from '../navigation/http-exception.js';
|
|
2
3
|
type Match = NonNullable<Resolver.EnhancedMatch>;
|
|
3
4
|
/**
|
|
4
5
|
* Render the resolved route tree for a matched page
|
|
@@ -41,7 +42,7 @@ type Match = NonNullable<Resolver.EnhancedMatch>;
|
|
|
41
42
|
export declare function Tree({ depth, params, error, ui }: {
|
|
42
43
|
depth: Match['__depth'];
|
|
43
44
|
params: Match['params'];
|
|
44
|
-
error
|
|
45
|
+
error?: HttpExceptionLike;
|
|
45
46
|
ui: Match['ui'];
|
|
46
47
|
}): import("react/jsx-runtime").JSX.Element;
|
|
47
48
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Suspense } from 'react';
|
|
3
3
|
import { HttpExceptionBoundary } from '../navigation/http-exception-boundary.js';
|
|
4
|
-
import { HttpException, isHttpException } from '../navigation/http-exception.js';
|
|
4
|
+
import { HttpException, isHttpException, } from '../navigation/http-exception.js';
|
|
5
5
|
import DefaultErr from '../ui/defaults/error.js';
|
|
6
6
|
const UNAUTHORISED_ERROR = new HttpException(401, 'Unauthorised');
|
|
7
7
|
const FORBIDDEN_ERROR = new HttpException(403, 'Forbidden');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HttpExceptionLike } from '../../navigation/http-exception.js';
|
|
2
2
|
export default function Err({ error }: {
|
|
3
|
-
error:
|
|
3
|
+
error: HttpExceptionLike;
|
|
4
4
|
}): import("react/jsx-runtime").JSX.Element;
|