@jk2908/solas 0.1.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/LICENSE +21 -0
- package/README.md +333 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +219 -0
- package/dist/error-boundary.d.ts +1 -0
- package/dist/error-boundary.js +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +235 -0
- package/dist/internal/build.d.ts +104 -0
- package/dist/internal/build.js +633 -0
- package/dist/internal/codegen/config.d.ts +5 -0
- package/dist/internal/codegen/config.js +19 -0
- package/dist/internal/codegen/environments.d.ts +12 -0
- package/dist/internal/codegen/environments.js +42 -0
- package/dist/internal/codegen/manifest.d.ts +5 -0
- package/dist/internal/codegen/manifest.js +15 -0
- package/dist/internal/codegen/maps.d.ts +5 -0
- package/dist/internal/codegen/maps.js +75 -0
- package/dist/internal/codegen/utils.d.ts +1 -0
- package/dist/internal/codegen/utils.js +2 -0
- package/dist/internal/env/browser.d.ts +4 -0
- package/dist/internal/env/browser.js +58 -0
- package/dist/internal/env/request-context.d.ts +19 -0
- package/dist/internal/env/request-context.js +2 -0
- package/dist/internal/env/rsc.d.ts +39 -0
- package/dist/internal/env/rsc.js +368 -0
- package/dist/internal/env/ssr.d.ts +42 -0
- package/dist/internal/env/ssr.js +149 -0
- package/dist/internal/env/utils.d.ts +2 -0
- package/dist/internal/env/utils.js +28 -0
- package/dist/internal/metadata.d.ts +81 -0
- package/dist/internal/metadata.js +185 -0
- package/dist/internal/navigation/http-exception-boundary.d.ts +12 -0
- package/dist/internal/navigation/http-exception-boundary.js +48 -0
- package/dist/internal/navigation/http-exception.d.ts +33 -0
- package/dist/internal/navigation/http-exception.js +45 -0
- package/dist/internal/navigation/link.d.ts +13 -0
- package/dist/internal/navigation/link.js +63 -0
- package/dist/internal/navigation/redirect-boundary.d.ts +12 -0
- package/dist/internal/navigation/redirect-boundary.js +39 -0
- package/dist/internal/navigation/redirect.d.ts +21 -0
- package/dist/internal/navigation/redirect.js +63 -0
- package/dist/internal/navigation/use-search-params.d.ts +1 -0
- package/dist/internal/navigation/use-search-params.js +13 -0
- package/dist/internal/prerender.d.ts +151 -0
- package/dist/internal/prerender.js +422 -0
- package/dist/internal/render/head.d.ts +4 -0
- package/dist/internal/render/head.js +38 -0
- package/dist/internal/render/tree.d.ts +47 -0
- package/dist/internal/render/tree.js +108 -0
- package/dist/internal/router/create-router.d.ts +6 -0
- package/dist/internal/router/create-router.js +95 -0
- package/dist/internal/router/pattern.d.ts +8 -0
- package/dist/internal/router/pattern.js +31 -0
- package/dist/internal/router/prefetcher.d.ts +47 -0
- package/dist/internal/router/prefetcher.js +90 -0
- package/dist/internal/router/resolver.d.ts +174 -0
- package/dist/internal/router/resolver.js +356 -0
- package/dist/internal/router/router-context.d.ts +11 -0
- package/dist/internal/router/router-context.js +7 -0
- package/dist/internal/router/router-provider.d.ts +6 -0
- package/dist/internal/router/router-provider.js +131 -0
- package/dist/internal/router/router.d.ts +79 -0
- package/dist/internal/router/router.js +417 -0
- package/dist/internal/router/use-router.d.ts +5 -0
- package/dist/internal/router/use-router.js +5 -0
- package/dist/internal/server/cookies.d.ts +6 -0
- package/dist/internal/server/cookies.js +17 -0
- package/dist/internal/server/dynamic.d.ts +9 -0
- package/dist/internal/server/dynamic.js +22 -0
- package/dist/internal/server/headers.d.ts +5 -0
- package/dist/internal/server/headers.js +19 -0
- package/dist/internal/server/url.d.ts +5 -0
- package/dist/internal/server/url.js +16 -0
- package/dist/internal/ui/defaults/error.d.ts +4 -0
- package/dist/internal/ui/defaults/error.js +6 -0
- package/dist/internal/ui/error-boundary.d.ts +26 -0
- package/dist/internal/ui/error-boundary.js +41 -0
- package/dist/navigation.d.ts +6 -0
- package/dist/navigation.js +6 -0
- package/dist/prerender.d.ts +1 -0
- package/dist/prerender.js +1 -0
- package/dist/router.d.ts +4 -0
- package/dist/router.js +4 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +4 -0
- package/dist/solas.d.ts +32 -0
- package/dist/solas.js +125 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.js +1 -0
- package/dist/utils/compress.d.ts +11 -0
- package/dist/utils/compress.js +76 -0
- package/dist/utils/context.d.ts +6 -0
- package/dist/utils/context.js +25 -0
- package/dist/utils/cookies.d.ts +3 -0
- package/dist/utils/cookies.js +35 -0
- package/dist/utils/export-reader.d.ts +29 -0
- package/dist/utils/export-reader.js +117 -0
- package/dist/utils/format.d.ts +6 -0
- package/dist/utils/format.js +72 -0
- package/dist/utils/logger.d.ts +52 -0
- package/dist/utils/logger.js +105 -0
- package/dist/utils/time.d.ts +4 -0
- package/dist/utils/time.js +29 -0
- package/package.json +111 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Solas } from '../../solas';
|
|
2
|
+
import { AUTOGEN_MSG } from './utils';
|
|
3
|
+
/**
|
|
4
|
+
* Generates the RSC entry code
|
|
5
|
+
*/
|
|
6
|
+
export function writeRSCEntry() {
|
|
7
|
+
return `
|
|
8
|
+
${AUTOGEN_MSG}
|
|
9
|
+
|
|
10
|
+
import { createHandler } from '${Solas.Config.PKG_NAME}/env/rsc'
|
|
11
|
+
|
|
12
|
+
import { manifest } from './manifest'
|
|
13
|
+
import { importMap } from './maps'
|
|
14
|
+
import { config } from './config'
|
|
15
|
+
|
|
16
|
+
export default createHandler(config, manifest, importMap)
|
|
17
|
+
|
|
18
|
+
import.meta.hot?.accept()
|
|
19
|
+
`.trim();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generates the SSR entry code
|
|
23
|
+
*/
|
|
24
|
+
export function writeSSREntry() {
|
|
25
|
+
return `
|
|
26
|
+
${AUTOGEN_MSG}
|
|
27
|
+
|
|
28
|
+
export { prerender, resume, ssr } from '${Solas.Config.PKG_NAME}/env/ssr'
|
|
29
|
+
`.trim();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Generates the browser entry code
|
|
33
|
+
*/
|
|
34
|
+
export function writeBrowserEntry() {
|
|
35
|
+
return `
|
|
36
|
+
${AUTOGEN_MSG}
|
|
37
|
+
|
|
38
|
+
import { browser } from '${Solas.Config.PKG_NAME}/env/browser'
|
|
39
|
+
|
|
40
|
+
browser()
|
|
41
|
+
`.trim();
|
|
42
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Solas } from '../../solas';
|
|
2
|
+
import { AUTOGEN_MSG } from './utils';
|
|
3
|
+
/**
|
|
4
|
+
* Generates the code to create an exported manifest object
|
|
5
|
+
*/
|
|
6
|
+
export function writeManifest(manifest) {
|
|
7
|
+
return `
|
|
8
|
+
${AUTOGEN_MSG}
|
|
9
|
+
|
|
10
|
+
import type { Manifest } from '${Solas.Config.PKG_NAME}'
|
|
11
|
+
|
|
12
|
+
export const manifest =
|
|
13
|
+
${JSON.stringify(manifest, null, 2)} as const satisfies Manifest
|
|
14
|
+
`.trim();
|
|
15
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Solas } from '../../solas';
|
|
2
|
+
import { AUTOGEN_MSG } from './utils';
|
|
3
|
+
/**
|
|
4
|
+
* Generates the import map for all route components, endpoints, layouts, shells, and middlewares
|
|
5
|
+
*/
|
|
6
|
+
export function writeMaps(imports, modules) {
|
|
7
|
+
const statics = [
|
|
8
|
+
...imports.endpoints.static.entries().map(([k, v]) => {
|
|
9
|
+
const [, method] = k.split('_');
|
|
10
|
+
return `import { ${method.toUpperCase()} as ${k}} from ${JSON.stringify(v)}`.trim();
|
|
11
|
+
}),
|
|
12
|
+
...imports.components.static
|
|
13
|
+
.entries()
|
|
14
|
+
.map(([k, v]) => `import * as ${k} from ${JSON.stringify(v)}`.trim()),
|
|
15
|
+
...imports.middlewares.static
|
|
16
|
+
.entries()
|
|
17
|
+
.map(([k, v]) => `import { middleware as ${k} } from ${JSON.stringify(v)}`.trim()),
|
|
18
|
+
];
|
|
19
|
+
const dynamics = [
|
|
20
|
+
...imports.components.dynamic
|
|
21
|
+
.entries()
|
|
22
|
+
.map(([k, v]) => `export const ${k} = () => import(${JSON.stringify(v)})`.trim()),
|
|
23
|
+
];
|
|
24
|
+
const map = Object.entries(modules).map(([id, m]) => {
|
|
25
|
+
const parts = [];
|
|
26
|
+
if (m.shellId)
|
|
27
|
+
parts.push(`shell: ${m.shellId}`);
|
|
28
|
+
if (m.layoutIds?.length) {
|
|
29
|
+
const layouts = m.layoutIds.map(id => (id === null ? 'null' : id)).join(', ');
|
|
30
|
+
parts.push(`layouts: [${layouts}]`);
|
|
31
|
+
}
|
|
32
|
+
if (m.pageId)
|
|
33
|
+
parts.push(`page: ${m.pageId}`);
|
|
34
|
+
if (m.endpointId)
|
|
35
|
+
parts.push(`endpoint: ${m.endpointId}`);
|
|
36
|
+
if (m['401Ids']?.length) {
|
|
37
|
+
const unauthorized = m['401Ids'].map(id => (id === null ? 'null' : id)).join(', ');
|
|
38
|
+
parts.push(`'401s': [${unauthorized}]`);
|
|
39
|
+
}
|
|
40
|
+
if (m['403Ids']?.length) {
|
|
41
|
+
const forbidden = m['403Ids'].map(id => (id === null ? 'null' : id)).join(', ');
|
|
42
|
+
parts.push(`'403s': [${forbidden}]`);
|
|
43
|
+
}
|
|
44
|
+
if (m['404Ids']?.length) {
|
|
45
|
+
const notFounds = m['404Ids'].map(id => (id === null ? 'null' : id)).join(', ');
|
|
46
|
+
parts.push(`'404s': [${notFounds}]`);
|
|
47
|
+
}
|
|
48
|
+
if (m['500Ids']?.length) {
|
|
49
|
+
const serverErrors = m['500Ids'].map(id => (id === null ? 'null' : id)).join(', ');
|
|
50
|
+
parts.push(`'500s': [${serverErrors}]`);
|
|
51
|
+
}
|
|
52
|
+
if (m.loadingIds?.length) {
|
|
53
|
+
const loaders = m.loadingIds.map(id => (id === null ? 'null' : id)).join(', ');
|
|
54
|
+
parts.push(`loaders: [${loaders}]`);
|
|
55
|
+
}
|
|
56
|
+
if (m.middlewareIds?.length) {
|
|
57
|
+
const middleware = m.middlewareIds.map(id => (id === null ? 'null' : id)).join(', ');
|
|
58
|
+
parts.push(`middlewares: [${middleware}]`);
|
|
59
|
+
}
|
|
60
|
+
return `${JSON.stringify(id)}: { ${parts.join(', ')} }`;
|
|
61
|
+
});
|
|
62
|
+
return `
|
|
63
|
+
${AUTOGEN_MSG}
|
|
64
|
+
|
|
65
|
+
import type { ImportMap } from '${Solas.Config.PKG_NAME}'
|
|
66
|
+
|
|
67
|
+
${statics.join('\n')}
|
|
68
|
+
|
|
69
|
+
${dynamics.join('\n')}
|
|
70
|
+
|
|
71
|
+
export const importMap = {
|
|
72
|
+
${map.join(',\n')}
|
|
73
|
+
} as const satisfies ImportMap
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const AUTOGEN_MSG = "// auto-generated by Solas";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { StrictMode, Suspense, useCallback, useEffect, useState, useTransition, } from 'react';
|
|
3
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
4
|
+
import { createFromFetch, createFromReadableStream, createTemporaryReferenceSet, encodeReply, setServerCallback, } from '@vitejs/plugin-rsc/browser';
|
|
5
|
+
import { rscStream } from 'rsc-html-stream/client';
|
|
6
|
+
import { RedirectBoundary } from '../navigation/redirect-boundary';
|
|
7
|
+
import { Head } from '../render/head';
|
|
8
|
+
import { RouterProvider } from '../router/router-provider';
|
|
9
|
+
import { ErrorBoundary } from '../ui/error-boundary';
|
|
10
|
+
/**
|
|
11
|
+
* Browser RSC hydration entry point
|
|
12
|
+
*/
|
|
13
|
+
export async function browser() {
|
|
14
|
+
const payload = await createFromReadableStream(rscStream, {
|
|
15
|
+
unstable_allowPartialStream: true,
|
|
16
|
+
});
|
|
17
|
+
let setPayload = () => { };
|
|
18
|
+
function A() {
|
|
19
|
+
const [p, setP] = useState(payload);
|
|
20
|
+
const [isPending, startTransition] = useTransition();
|
|
21
|
+
const setPayloadInTransition = useCallback((payload) => {
|
|
22
|
+
startTransition(() => {
|
|
23
|
+
setP(payload);
|
|
24
|
+
});
|
|
25
|
+
}, []);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
// expose external setPayload - used inside
|
|
28
|
+
// server callback to update payload after
|
|
29
|
+
// action execution
|
|
30
|
+
setPayload = setPayloadInTransition;
|
|
31
|
+
}, [setPayloadInTransition]);
|
|
32
|
+
return (_jsx(RedirectBoundary, { children: _jsxs(RouterProvider, { setPayload: setPayloadInTransition, isNavigating: isPending, children: [
|
|
33
|
+
_jsx(ErrorBoundary, { fallback: null, children: _jsx(Suspense, { fallback: null, children: _jsx(Head, { metadata: p.metadata }) }) }), p.root] }) }));
|
|
34
|
+
}
|
|
35
|
+
setServerCallback(async (id, args) => {
|
|
36
|
+
const url = new URL(window.location.href);
|
|
37
|
+
const temporaryReferences = createTemporaryReferenceSet();
|
|
38
|
+
const payload = await createFromFetch(fetch(url, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
body: await encodeReply(args, { temporaryReferences }),
|
|
41
|
+
headers: {
|
|
42
|
+
'x-rsc-action-id': id,
|
|
43
|
+
},
|
|
44
|
+
}), { temporaryReferences });
|
|
45
|
+
setPayload(payload);
|
|
46
|
+
const { ok, data } = payload.returnValue ?? {};
|
|
47
|
+
if (!ok)
|
|
48
|
+
throw data;
|
|
49
|
+
return data;
|
|
50
|
+
});
|
|
51
|
+
hydrateRoot(document, _jsx(StrictMode, { children: _jsx(A, {}) }), {
|
|
52
|
+
formState: payload.formState,
|
|
53
|
+
});
|
|
54
|
+
import.meta.hot?.on?.('rsc:update', async () => {
|
|
55
|
+
const p = await createFromFetch(fetch(window.location.href, { headers: { Accept: 'text/x-component' } }));
|
|
56
|
+
setPayload(p);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SolasRequest } from '../../types';
|
|
2
|
+
import type { Cookies } from '../../utils/cookies';
|
|
3
|
+
export type RequestCache = {
|
|
4
|
+
cookies?: Readonly<ReturnType<typeof Cookies.parse>>;
|
|
5
|
+
headers?: ReadonlyMap<string, string>;
|
|
6
|
+
url?: URL;
|
|
7
|
+
};
|
|
8
|
+
export declare const RequestContext: {
|
|
9
|
+
use(): {
|
|
10
|
+
req: SolasRequest;
|
|
11
|
+
prerender: "full" | "ppr" | null;
|
|
12
|
+
cache: RequestCache;
|
|
13
|
+
};
|
|
14
|
+
write<R>(value: {
|
|
15
|
+
req: SolasRequest;
|
|
16
|
+
prerender: "full" | "ppr" | null;
|
|
17
|
+
cache: RequestCache;
|
|
18
|
+
}, fn: () => R | Promise<R>): R | Promise<R>;
|
|
19
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ReactFormState } from 'react-dom/client';
|
|
2
|
+
import type { ImportMap, Manifest, RuntimeConfig, SolasRequest } from '../../types';
|
|
3
|
+
import { Metadata } from '../metadata';
|
|
4
|
+
export type RSCPayload = {
|
|
5
|
+
returnValue?: {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
data: unknown;
|
|
8
|
+
};
|
|
9
|
+
formState?: ReactFormState;
|
|
10
|
+
root: React.ReactNode;
|
|
11
|
+
metadata?: Promise<Metadata.Item>;
|
|
12
|
+
};
|
|
13
|
+
export declare function action(req: SolasRequest): Promise<{
|
|
14
|
+
returnValue: {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
data: unknown;
|
|
17
|
+
} | undefined;
|
|
18
|
+
formState: ReactFormState | undefined;
|
|
19
|
+
temporaryReferences: unknown;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Check if a request is an action request and reuse parsed FormData
|
|
23
|
+
* when multipart action detection already had to inspect the body
|
|
24
|
+
*/
|
|
25
|
+
export declare function maybeActionWithParsedFormData(req: Request): Promise<{
|
|
26
|
+
action: boolean;
|
|
27
|
+
formData: null;
|
|
28
|
+
} | {
|
|
29
|
+
action: boolean;
|
|
30
|
+
formData: FormData;
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Create the object exported by the generated RSC entry. Uses the generated config,
|
|
34
|
+
* route manifest, and import map to build the router once, then returns an object
|
|
35
|
+
* with a fetch method that handles requests
|
|
36
|
+
*/
|
|
37
|
+
export declare function createHandler(config: RuntimeConfig, manifest: Manifest, importMap: ImportMap): {
|
|
38
|
+
fetch(req: Request): Promise<Response>;
|
|
39
|
+
};
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createTemporaryReferenceSet, decodeAction, decodeFormState, decodeReply, loadServerAction, renderToReadableStream, } from '@vitejs/plugin-rsc/rsc';
|
|
3
|
+
import { Solas } from '../../solas';
|
|
4
|
+
import { Logger } from '../../utils/logger';
|
|
5
|
+
import { getKnownDigest, isKnownError } from './utils';
|
|
6
|
+
import { Metadata } from '../metadata';
|
|
7
|
+
import { HttpException, isHttpException } from '../navigation/http-exception';
|
|
8
|
+
import { Prerender } from '../prerender';
|
|
9
|
+
import { Tree } from '../render/tree';
|
|
10
|
+
import { createRouter } from '../router/create-router';
|
|
11
|
+
import { Resolver } from '../router/resolver';
|
|
12
|
+
import { Router } from '../router/router';
|
|
13
|
+
import DefaultErr from '../ui/defaults/error';
|
|
14
|
+
import { RequestContext } from './request-context';
|
|
15
|
+
/**
|
|
16
|
+
* Get the streamed RSC payload and response metadata for a single request.
|
|
17
|
+
* Resolves the route match, collects metadata, and returns the stream,
|
|
18
|
+
* status code, and prerender mode needed by the response layer
|
|
19
|
+
*/
|
|
20
|
+
async function getPayload(req, manifest, importMap, baseMetadata, returnValue, formState, temporaryReferences) {
|
|
21
|
+
const resolver = new Resolver(manifest, importMap);
|
|
22
|
+
const logger = new Logger();
|
|
23
|
+
const prerender = req.headers.get(`x-${Solas.Config.SLUG}-prerender`) === '1';
|
|
24
|
+
const url = new URL(req.url);
|
|
25
|
+
const pathname = url.pathname.endsWith('/') && url.pathname !== '/'
|
|
26
|
+
? url.pathname.slice(0, -1)
|
|
27
|
+
: url.pathname;
|
|
28
|
+
const match = resolver.enhance(resolver.reconcile(pathname, req[Solas.Config.REQUEST_META].match, req[Solas.Config.REQUEST_META].error));
|
|
29
|
+
// if there's no match then no user supplied error boundary
|
|
30
|
+
// has been found, and we should server render a default
|
|
31
|
+
// error screen
|
|
32
|
+
if (!match) {
|
|
33
|
+
const error = req[Solas.Config.REQUEST_META].error ?? new HttpException(404, 'Not found');
|
|
34
|
+
const title = `${'status' in error ? `${error.status} -` : ''}${error.message}`;
|
|
35
|
+
const rscPayload = {
|
|
36
|
+
root: (_jsxs("html", { lang: "en", children: [
|
|
37
|
+
_jsxs("head", { children: [
|
|
38
|
+
_jsx("meta", { charSet: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("meta", { name: "robots", content: "noindex,nofollow" }), _jsx("title", { children: title })
|
|
39
|
+
] }), _jsx("body", { children: _jsx(DefaultErr, { error: error }) })
|
|
40
|
+
] })),
|
|
41
|
+
returnValue,
|
|
42
|
+
formState,
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
// this path is a safety fallback when a prerender request
|
|
46
|
+
// hits an unmatched route. In build prerender we force
|
|
47
|
+
// mode to 'full' so the 404/error shell resolves
|
|
48
|
+
// immediately. In normal request-time rendering
|
|
49
|
+
// we keep mode as null (obvi)
|
|
50
|
+
stream: RequestContext.write({
|
|
51
|
+
req,
|
|
52
|
+
prerender: prerender ? 'full' : null,
|
|
53
|
+
cache: {},
|
|
54
|
+
}, () => renderToReadableStream(rscPayload, {
|
|
55
|
+
temporaryReferences,
|
|
56
|
+
onError(err) {
|
|
57
|
+
if (err == null)
|
|
58
|
+
return;
|
|
59
|
+
const digest = getKnownDigest(err);
|
|
60
|
+
if (digest)
|
|
61
|
+
return digest;
|
|
62
|
+
if (isKnownError(err))
|
|
63
|
+
return;
|
|
64
|
+
logger.error('[rsc]', err);
|
|
65
|
+
},
|
|
66
|
+
})),
|
|
67
|
+
status: 404,
|
|
68
|
+
ppr: false,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// check if this route is a candidate for ppr
|
|
72
|
+
const ppr = match.prerender === 'ppr';
|
|
73
|
+
const collection = new Metadata.Collection(baseMetadata);
|
|
74
|
+
const metadata = collection
|
|
75
|
+
.add(...(match.metadata?.({ params: match.params, error: match.error }) ?? []))
|
|
76
|
+
.run();
|
|
77
|
+
const rscPayload = {
|
|
78
|
+
root: (_jsx(_Fragment, { children: _jsx(Tree, { depth: match.__depth, params: match.params, error: match.error, ui: match.ui }) })),
|
|
79
|
+
returnValue,
|
|
80
|
+
formState,
|
|
81
|
+
metadata,
|
|
82
|
+
};
|
|
83
|
+
// status code comes from route match error if any
|
|
84
|
+
const status = isHttpException(match.error) ? match.error.status : 200;
|
|
85
|
+
try {
|
|
86
|
+
// this is the main matched route render pass for page/layout
|
|
87
|
+
// tree output. Mode is null for normal ssr, 'full' for full
|
|
88
|
+
// prerender, and 'ppr' for ppr prerender. dynamic() only
|
|
89
|
+
// suspends when mode is 'ppr'
|
|
90
|
+
const stream = RequestContext.write({
|
|
91
|
+
req,
|
|
92
|
+
prerender: prerender ? (ppr ? 'ppr' : 'full') : null,
|
|
93
|
+
cache: {},
|
|
94
|
+
}, () => renderToReadableStream(rscPayload, {
|
|
95
|
+
temporaryReferences,
|
|
96
|
+
onError(err) {
|
|
97
|
+
if (err == null)
|
|
98
|
+
return;
|
|
99
|
+
const digest = getKnownDigest(err);
|
|
100
|
+
if (digest)
|
|
101
|
+
return digest;
|
|
102
|
+
if (isKnownError(err))
|
|
103
|
+
return;
|
|
104
|
+
logger.error('[rsc]', err);
|
|
105
|
+
},
|
|
106
|
+
}));
|
|
107
|
+
return { stream, status, ppr };
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
// shell failed to render - return minimal fallback
|
|
111
|
+
logger.error('rsc shell', err);
|
|
112
|
+
const title = err instanceof Error
|
|
113
|
+
? 'status' in err
|
|
114
|
+
? `${err.status} - ${err.message}`
|
|
115
|
+
: `500 - ${err.message}`
|
|
116
|
+
: '500 - Unknown server error';
|
|
117
|
+
const error = new Error(err instanceof Error ? err.message : 'Unknown server error', {
|
|
118
|
+
cause: err,
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
// this branch renders the minimal error shell after the
|
|
122
|
+
// main tree throws. We keep the same mode as the
|
|
123
|
+
// request so helpers see consistent state
|
|
124
|
+
// prevents mode drift on error paths
|
|
125
|
+
stream: RequestContext.write({
|
|
126
|
+
req,
|
|
127
|
+
prerender: prerender ? 'full' : null,
|
|
128
|
+
cache: {},
|
|
129
|
+
}, () => renderToReadableStream({
|
|
130
|
+
root: (_jsxs("html", { lang: "en", children: [
|
|
131
|
+
_jsxs("head", { children: [
|
|
132
|
+
_jsx("meta", { charSet: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("meta", { name: "robots", content: "noindex,nofollow" }), _jsx("title", { children: title })
|
|
133
|
+
] }), _jsx("body", { children: _jsx(DefaultErr, { error: error }) })
|
|
134
|
+
] })),
|
|
135
|
+
returnValue,
|
|
136
|
+
formState,
|
|
137
|
+
}, {
|
|
138
|
+
temporaryReferences,
|
|
139
|
+
})),
|
|
140
|
+
status: 500,
|
|
141
|
+
ppr: false,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export async function action(req) {
|
|
146
|
+
let returnValue;
|
|
147
|
+
let formState;
|
|
148
|
+
let temporaryReferences;
|
|
149
|
+
const id = req.headers.get('x-rsc-action-id');
|
|
150
|
+
if (id) {
|
|
151
|
+
// x-rsc-action-id header exists when action is
|
|
152
|
+
// called via ReactClient.setServerCallback
|
|
153
|
+
const body = req.headers.get('content-type')?.startsWith('multipart/form-data')
|
|
154
|
+
? await req.formData()
|
|
155
|
+
: await req.text();
|
|
156
|
+
temporaryReferences = createTemporaryReferenceSet();
|
|
157
|
+
const args = await decodeReply(body, {
|
|
158
|
+
temporaryReferences,
|
|
159
|
+
});
|
|
160
|
+
const action = await loadServerAction(id);
|
|
161
|
+
try {
|
|
162
|
+
const data = await action.apply(null, args);
|
|
163
|
+
returnValue = { ok: true, data };
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
returnValue = { ok: false, data: err };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// otherwise server function is called via
|
|
171
|
+
// <form action={...}>
|
|
172
|
+
// we might have already parsed FormData in the router for multipart action
|
|
173
|
+
// detection should be attached to the SolasRequest, so we can reuse that
|
|
174
|
+
// to avoid parsing twice
|
|
175
|
+
const parsedFormData = req[Solas.Config.REQUEST_META]?.parsedFormData;
|
|
176
|
+
const formData = parsedFormData ?? (await req.formData());
|
|
177
|
+
const decodedAction = await decodeAction(formData);
|
|
178
|
+
const result = await decodedAction();
|
|
179
|
+
formState = await decodeFormState(result, formData);
|
|
180
|
+
}
|
|
181
|
+
return { returnValue, formState, temporaryReferences };
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Check if a request is an action request and reuse parsed FormData
|
|
185
|
+
* when multipart action detection already had to inspect the body
|
|
186
|
+
*/
|
|
187
|
+
export async function maybeActionWithParsedFormData(req) {
|
|
188
|
+
if (req.method !== 'POST')
|
|
189
|
+
return { action: false, formData: null };
|
|
190
|
+
if (req.headers.has('x-rsc-action-id'))
|
|
191
|
+
return { action: true, formData: null };
|
|
192
|
+
const contentType = req.headers.get('content-type') ?? '';
|
|
193
|
+
if (!contentType.startsWith('multipart/form-data')) {
|
|
194
|
+
return { action: false, formData: null };
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const formData = await req.clone().formData();
|
|
198
|
+
for (const key of formData.keys()) {
|
|
199
|
+
if (key === '$ACTION_KEY' ||
|
|
200
|
+
key.startsWith('$ACTION_') ||
|
|
201
|
+
key.startsWith('$ACTION_REF_')) {
|
|
202
|
+
return { action: true, formData };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return { action: false, formData: null };
|
|
208
|
+
}
|
|
209
|
+
return { action: false, formData: null };
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Create the object exported by the generated RSC entry. Uses the generated config,
|
|
213
|
+
* route manifest, and import map to build the router once, then returns an object
|
|
214
|
+
* with a fetch method that handles requests
|
|
215
|
+
*/
|
|
216
|
+
export function createHandler(config, manifest, importMap) {
|
|
217
|
+
const fullyPrerenderedRoutes = new Set(Object.values(manifest)
|
|
218
|
+
.flat()
|
|
219
|
+
.filter(entry => 'prerender' in entry && String(entry.prerender) === 'full')
|
|
220
|
+
.map(entry => entry.__path));
|
|
221
|
+
/**
|
|
222
|
+
* Create the HTTP response for a single incoming request. Runs actions when needed,
|
|
223
|
+
* converts the payload into component, HTML, or prerender artifact responses, and
|
|
224
|
+
* applies the final status and headers
|
|
225
|
+
*/
|
|
226
|
+
async function createResponse(req) {
|
|
227
|
+
let opts = {
|
|
228
|
+
formState: undefined,
|
|
229
|
+
temporaryReferences: undefined,
|
|
230
|
+
returnValue: undefined,
|
|
231
|
+
};
|
|
232
|
+
if (req[Solas.Config.REQUEST_META].action)
|
|
233
|
+
opts = await action(req);
|
|
234
|
+
const { stream: rscStream, status, ppr, } = await getPayload(req, manifest, importMap, config.metadata, opts.returnValue, opts.formState, opts.temporaryReferences);
|
|
235
|
+
const stream = await rscStream;
|
|
236
|
+
if (!req.headers.get('accept')?.includes('text/html')) {
|
|
237
|
+
return new Response(stream, {
|
|
238
|
+
headers: {
|
|
239
|
+
'Cache-Control': 'private, no-store',
|
|
240
|
+
'Content-Type': 'text/x-component; charset=utf-8',
|
|
241
|
+
Vary: 'accept',
|
|
242
|
+
},
|
|
243
|
+
status,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const mod = await import.meta.viteRsc.loadModule('ssr', 'index');
|
|
247
|
+
const pathname = new URL(req.url).pathname;
|
|
248
|
+
const runtimePpr = !import.meta.env.DEV && ppr;
|
|
249
|
+
// prerender artifact requests bypass the normal document path so the cli
|
|
250
|
+
// gets structured JSON instead of a rendered html response
|
|
251
|
+
if (req.headers.get(`x-${Solas.Config.SLUG}-prerender`) === '1' &&
|
|
252
|
+
req.headers.get(`x-${Solas.Config.SLUG}-prerender-artifact`) === '1') {
|
|
253
|
+
const artifact = await mod.prerender(stream, {
|
|
254
|
+
formState: opts.formState,
|
|
255
|
+
ppr: runtimePpr,
|
|
256
|
+
route: pathname,
|
|
257
|
+
});
|
|
258
|
+
return new Response(JSON.stringify(artifact), {
|
|
259
|
+
headers: {
|
|
260
|
+
'Cache-Control': 'private, no-store',
|
|
261
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
262
|
+
Vary: 'accept',
|
|
263
|
+
},
|
|
264
|
+
status,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
const artifactManifest = runtimePpr
|
|
268
|
+
? await Prerender.Artifact.loadManifest(Solas.Config.OUT_DIR)
|
|
269
|
+
: null;
|
|
270
|
+
const artifactManifestEntry = artifactManifest?.routes[pathname] ?? null;
|
|
271
|
+
let tryPrelude = false;
|
|
272
|
+
if (artifactManifestEntry) {
|
|
273
|
+
tryPrelude = artifactManifestEntry.mode === 'ppr';
|
|
274
|
+
}
|
|
275
|
+
else if (runtimePpr) {
|
|
276
|
+
const artifactMetadata = await Prerender.Artifact.loadMetadata(Solas.Config.OUT_DIR, pathname);
|
|
277
|
+
tryPrelude =
|
|
278
|
+
!!artifactMetadata &&
|
|
279
|
+
Prerender.Artifact.isCompatible(artifactMetadata, pathname, 'ppr');
|
|
280
|
+
}
|
|
281
|
+
if (tryPrelude) {
|
|
282
|
+
const postponedState = await Prerender.Artifact.loadPostponedState(Solas.Config.OUT_DIR, pathname);
|
|
283
|
+
const prelude = await Prerender.Artifact.loadPrelude(Solas.Config.OUT_DIR, pathname);
|
|
284
|
+
// resumable ppr responses splice fresh streamed content into the cached
|
|
285
|
+
// prelude when postponed state is available for this route
|
|
286
|
+
if (postponedState) {
|
|
287
|
+
const resumeStream = await mod.resume(stream, postponedState, {
|
|
288
|
+
nonce: undefined,
|
|
289
|
+
injectPayload: true,
|
|
290
|
+
});
|
|
291
|
+
const body = prelude
|
|
292
|
+
? Prerender.Artifact.composePreludeAndResume(prelude, resumeStream)
|
|
293
|
+
: resumeStream;
|
|
294
|
+
return new Response(body, {
|
|
295
|
+
headers: {
|
|
296
|
+
'Cache-Control': 'private, no-store',
|
|
297
|
+
'Content-Type': 'text/html',
|
|
298
|
+
Vary: 'accept',
|
|
299
|
+
},
|
|
300
|
+
status,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const htmlStream = await mod.ssr(stream, {
|
|
305
|
+
formState: opts.formState,
|
|
306
|
+
ppr: runtimePpr,
|
|
307
|
+
});
|
|
308
|
+
return new Response(htmlStream, {
|
|
309
|
+
headers: {
|
|
310
|
+
'Cache-Control': 'private, no-store',
|
|
311
|
+
'Content-Type': 'text/html',
|
|
312
|
+
Vary: 'accept',
|
|
313
|
+
},
|
|
314
|
+
status,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
const router = createRouter(config, manifest, importMap, createResponse);
|
|
318
|
+
return {
|
|
319
|
+
async fetch(req) {
|
|
320
|
+
const url = new URL(req.url);
|
|
321
|
+
const accept = req.headers.get('accept') ?? '';
|
|
322
|
+
// fully prerendered html can be served straight from disk for normal
|
|
323
|
+
// document requests, but artifact generation must still hit the runtime path
|
|
324
|
+
if (!import.meta.env.DEV &&
|
|
325
|
+
accept.includes('text/html') &&
|
|
326
|
+
req.headers.get(`x-${Solas.Config.SLUG}-prerender-artifact`) !== '1') {
|
|
327
|
+
const pathname = url.pathname;
|
|
328
|
+
let prerenderPath = null;
|
|
329
|
+
const artifactManifest = await Prerender.Artifact.loadManifest(Solas.Config.OUT_DIR);
|
|
330
|
+
const artifactManifestEntry = artifactManifest?.routes[pathname] ?? null;
|
|
331
|
+
if (fullyPrerenderedRoutes.has(pathname)) {
|
|
332
|
+
prerenderPath =
|
|
333
|
+
pathname === '/'
|
|
334
|
+
? Solas.Config.OUT_DIR + '/index.html'
|
|
335
|
+
: Solas.Config.OUT_DIR + pathname + '/index.html';
|
|
336
|
+
}
|
|
337
|
+
else if (artifactManifestEntry) {
|
|
338
|
+
if (artifactManifestEntry.mode === 'full') {
|
|
339
|
+
prerenderPath =
|
|
340
|
+
pathname === '/'
|
|
341
|
+
? Solas.Config.OUT_DIR + '/index.html'
|
|
342
|
+
: Solas.Config.OUT_DIR + pathname + '/index.html';
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
const artifactMetadata = await Prerender.Artifact.loadMetadata(Solas.Config.OUT_DIR, pathname);
|
|
347
|
+
if (artifactMetadata &&
|
|
348
|
+
Prerender.Artifact.isCompatible(artifactMetadata, pathname, 'full')) {
|
|
349
|
+
prerenderPath =
|
|
350
|
+
pathname === '/'
|
|
351
|
+
? Solas.Config.OUT_DIR + '/index.html'
|
|
352
|
+
: Solas.Config.OUT_DIR + pathname + '/index.html';
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (prerenderPath) {
|
|
356
|
+
const res = await Router.serve(prerenderPath, req, config.precompress, {
|
|
357
|
+
// avoid shared or proxy caching unless users opt into public caching later
|
|
358
|
+
'Cache-Control': 'private, no-store',
|
|
359
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
360
|
+
});
|
|
361
|
+
if (res.status !== 404)
|
|
362
|
+
return res;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return router.fetch(req);
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
}
|