@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.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +219 -0
  5. package/dist/error-boundary.d.ts +1 -0
  6. package/dist/error-boundary.js +1 -0
  7. package/dist/index.d.ts +7 -0
  8. package/dist/index.js +235 -0
  9. package/dist/internal/build.d.ts +104 -0
  10. package/dist/internal/build.js +633 -0
  11. package/dist/internal/codegen/config.d.ts +5 -0
  12. package/dist/internal/codegen/config.js +19 -0
  13. package/dist/internal/codegen/environments.d.ts +12 -0
  14. package/dist/internal/codegen/environments.js +42 -0
  15. package/dist/internal/codegen/manifest.d.ts +5 -0
  16. package/dist/internal/codegen/manifest.js +15 -0
  17. package/dist/internal/codegen/maps.d.ts +5 -0
  18. package/dist/internal/codegen/maps.js +75 -0
  19. package/dist/internal/codegen/utils.d.ts +1 -0
  20. package/dist/internal/codegen/utils.js +2 -0
  21. package/dist/internal/env/browser.d.ts +4 -0
  22. package/dist/internal/env/browser.js +58 -0
  23. package/dist/internal/env/request-context.d.ts +19 -0
  24. package/dist/internal/env/request-context.js +2 -0
  25. package/dist/internal/env/rsc.d.ts +39 -0
  26. package/dist/internal/env/rsc.js +368 -0
  27. package/dist/internal/env/ssr.d.ts +42 -0
  28. package/dist/internal/env/ssr.js +149 -0
  29. package/dist/internal/env/utils.d.ts +2 -0
  30. package/dist/internal/env/utils.js +28 -0
  31. package/dist/internal/metadata.d.ts +81 -0
  32. package/dist/internal/metadata.js +185 -0
  33. package/dist/internal/navigation/http-exception-boundary.d.ts +12 -0
  34. package/dist/internal/navigation/http-exception-boundary.js +48 -0
  35. package/dist/internal/navigation/http-exception.d.ts +33 -0
  36. package/dist/internal/navigation/http-exception.js +45 -0
  37. package/dist/internal/navigation/link.d.ts +13 -0
  38. package/dist/internal/navigation/link.js +63 -0
  39. package/dist/internal/navigation/redirect-boundary.d.ts +12 -0
  40. package/dist/internal/navigation/redirect-boundary.js +39 -0
  41. package/dist/internal/navigation/redirect.d.ts +21 -0
  42. package/dist/internal/navigation/redirect.js +63 -0
  43. package/dist/internal/navigation/use-search-params.d.ts +1 -0
  44. package/dist/internal/navigation/use-search-params.js +13 -0
  45. package/dist/internal/prerender.d.ts +151 -0
  46. package/dist/internal/prerender.js +422 -0
  47. package/dist/internal/render/head.d.ts +4 -0
  48. package/dist/internal/render/head.js +38 -0
  49. package/dist/internal/render/tree.d.ts +47 -0
  50. package/dist/internal/render/tree.js +108 -0
  51. package/dist/internal/router/create-router.d.ts +6 -0
  52. package/dist/internal/router/create-router.js +95 -0
  53. package/dist/internal/router/pattern.d.ts +8 -0
  54. package/dist/internal/router/pattern.js +31 -0
  55. package/dist/internal/router/prefetcher.d.ts +47 -0
  56. package/dist/internal/router/prefetcher.js +90 -0
  57. package/dist/internal/router/resolver.d.ts +174 -0
  58. package/dist/internal/router/resolver.js +356 -0
  59. package/dist/internal/router/router-context.d.ts +11 -0
  60. package/dist/internal/router/router-context.js +7 -0
  61. package/dist/internal/router/router-provider.d.ts +6 -0
  62. package/dist/internal/router/router-provider.js +131 -0
  63. package/dist/internal/router/router.d.ts +79 -0
  64. package/dist/internal/router/router.js +417 -0
  65. package/dist/internal/router/use-router.d.ts +5 -0
  66. package/dist/internal/router/use-router.js +5 -0
  67. package/dist/internal/server/cookies.d.ts +6 -0
  68. package/dist/internal/server/cookies.js +17 -0
  69. package/dist/internal/server/dynamic.d.ts +9 -0
  70. package/dist/internal/server/dynamic.js +22 -0
  71. package/dist/internal/server/headers.d.ts +5 -0
  72. package/dist/internal/server/headers.js +19 -0
  73. package/dist/internal/server/url.d.ts +5 -0
  74. package/dist/internal/server/url.js +16 -0
  75. package/dist/internal/ui/defaults/error.d.ts +4 -0
  76. package/dist/internal/ui/defaults/error.js +6 -0
  77. package/dist/internal/ui/error-boundary.d.ts +26 -0
  78. package/dist/internal/ui/error-boundary.js +41 -0
  79. package/dist/navigation.d.ts +6 -0
  80. package/dist/navigation.js +6 -0
  81. package/dist/prerender.d.ts +1 -0
  82. package/dist/prerender.js +1 -0
  83. package/dist/router.d.ts +4 -0
  84. package/dist/router.js +4 -0
  85. package/dist/server.d.ts +4 -0
  86. package/dist/server.js +4 -0
  87. package/dist/solas.d.ts +32 -0
  88. package/dist/solas.js +125 -0
  89. package/dist/types.d.ts +93 -0
  90. package/dist/types.js +1 -0
  91. package/dist/utils/compress.d.ts +11 -0
  92. package/dist/utils/compress.js +76 -0
  93. package/dist/utils/context.d.ts +6 -0
  94. package/dist/utils/context.js +25 -0
  95. package/dist/utils/cookies.d.ts +3 -0
  96. package/dist/utils/cookies.js +35 -0
  97. package/dist/utils/export-reader.d.ts +29 -0
  98. package/dist/utils/export-reader.js +117 -0
  99. package/dist/utils/format.d.ts +6 -0
  100. package/dist/utils/format.js +72 -0
  101. package/dist/utils/logger.d.ts +52 -0
  102. package/dist/utils/logger.js +105 -0
  103. package/dist/utils/time.d.ts +4 -0
  104. package/dist/utils/time.js +29 -0
  105. 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,5 @@
1
+ import type { Manifest } from '../../types';
2
+ /**
3
+ * Generates the code to create an exported manifest object
4
+ */
5
+ export declare function writeManifest(manifest: Manifest): string;
@@ -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,5 @@
1
+ import type { Build } from '../build';
2
+ /**
3
+ * Generates the import map for all route components, endpoints, layouts, shells, and middlewares
4
+ */
5
+ export declare function writeMaps(imports: Build.Imports, modules: Build.Modules): string;
@@ -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,2 @@
1
+ import { Solas } from '../../solas';
2
+ export const AUTOGEN_MSG = `// auto-generated by ${Solas.Config.NAME}`;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Browser RSC hydration entry point
3
+ */
4
+ export declare function browser(): Promise<void>;
@@ -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,2 @@
1
+ import { Context } from '../../utils/context';
2
+ export const RequestContext = Context.create('request');
@@ -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
+ }