@jk2908/solas 0.2.2 → 0.3.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 +13 -0
- package/README.md +5 -3
- package/dist/cli.js +77 -83
- package/dist/error-boundary.d.ts +1 -1
- package/dist/error-boundary.js +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +10 -14
- package/dist/internal/build.d.ts +1 -1
- package/dist/internal/build.js +4 -4
- package/dist/internal/codegen/config.d.ts +1 -1
- package/dist/internal/codegen/config.js +10 -10
- package/dist/internal/codegen/environments.js +22 -18
- package/dist/internal/codegen/manifest.d.ts +1 -1
- package/dist/internal/codegen/manifest.js +6 -7
- package/dist/internal/codegen/maps.d.ts +1 -1
- package/dist/internal/codegen/maps.js +38 -27
- package/dist/internal/codegen/utils.d.ts +20 -0
- package/dist/internal/codegen/utils.js +140 -1
- package/dist/internal/env/browser.js +20 -16
- package/dist/internal/env/request-context.d.ts +2 -2
- package/dist/internal/env/request-context.js +1 -1
- package/dist/internal/env/rsc.d.ts +8 -22
- package/dist/internal/env/rsc.js +38 -117
- package/dist/internal/env/ssr.js +9 -9
- package/dist/internal/env/utils.js +2 -2
- package/dist/internal/metadata.d.ts +2 -2
- package/dist/internal/metadata.js +18 -6
- package/dist/internal/navigation/http-exception-boundary.d.ts +2 -2
- package/dist/internal/navigation/http-exception-boundary.js +1 -1
- package/dist/internal/navigation/link.js +1 -1
- package/dist/internal/navigation/redirect-boundary.d.ts +1 -1
- package/dist/internal/navigation/redirect-boundary.js +1 -1
- package/dist/internal/navigation/redirect.js +1 -1
- package/dist/internal/navigation/use-search-params.js +4 -2
- package/dist/internal/prerender.d.ts +10 -1
- package/dist/internal/prerender.js +55 -5
- package/dist/internal/render/head.d.ts +4 -1
- package/dist/internal/render/head.js +37 -18
- package/dist/internal/render/tree.d.ts +1 -1
- package/dist/internal/render/tree.js +3 -3
- package/dist/internal/router/create-router.d.ts +2 -2
- package/dist/internal/router/create-router.js +1 -1
- package/dist/internal/router/prefetcher.d.ts +1 -1
- package/dist/internal/router/prefetcher.js +8 -3
- package/dist/internal/router/resolver.d.ts +29 -29
- package/dist/internal/router/resolver.js +4 -4
- package/dist/internal/router/router-context.d.ts +4 -0
- package/dist/internal/router/router-context.js +1 -0
- package/dist/internal/router/router-provider.d.ts +6 -2
- package/dist/internal/router/router-provider.js +38 -22
- package/dist/internal/router/router.d.ts +1 -1
- package/dist/internal/router/router.js +4 -4
- package/dist/internal/router/use-router.d.ts +5 -1
- package/dist/internal/router/use-router.js +1 -1
- package/dist/internal/router/utils.d.ts +1 -1
- package/dist/internal/server/actions.d.ts +30 -0
- package/dist/internal/server/actions.js +107 -0
- package/dist/internal/server/cookies.d.ts +1 -1
- package/dist/internal/server/cookies.js +3 -3
- package/dist/internal/server/dynamic.js +2 -2
- package/dist/internal/server/headers.js +2 -2
- package/dist/internal/server/url.js +14 -3
- package/dist/internal/ui/defaults/error.d.ts +1 -1
- package/dist/internal/ui/error-boundary.d.ts +1 -1
- package/dist/internal/ui/error-boundary.js +1 -1
- package/dist/navigation.d.ts +6 -6
- package/dist/navigation.js +6 -6
- package/dist/prerender.d.ts +1 -1
- package/dist/prerender.js +1 -1
- package/dist/router.d.ts +4 -4
- package/dist/router.js +4 -4
- package/dist/server.d.ts +4 -4
- package/dist/server.js +4 -4
- package/dist/solas.d.ts +1 -1
- package/dist/solas.js +1 -0
- package/dist/types.d.ts +6 -6
- package/dist/types.js +1 -1
- package/dist/utils/context.js +1 -1
- package/dist/utils/logger.js +2 -2
- package/package.json +3 -1
- package/dist/utils/format.d.ts +0 -6
- package/dist/utils/format.js +0 -72
|
@@ -1,2 +1,141 @@
|
|
|
1
|
-
import { Solas } from '../../solas';
|
|
1
|
+
import { Solas } from '../../solas.js';
|
|
2
2
|
export const AUTOGEN_MSG = `// auto-generated by ${Solas.Config.NAME}`;
|
|
3
|
+
const INDENT = '\t';
|
|
4
|
+
const IDENTIFIER = /^[$A-Z_a-z][$\w]*$/;
|
|
5
|
+
/**
|
|
6
|
+
* Check whether a string contains control characters that should never appear in source
|
|
7
|
+
*/
|
|
8
|
+
function hasControlChar(value) {
|
|
9
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
10
|
+
const code = value.charCodeAt(index);
|
|
11
|
+
if ((code >= 0 && code <= 31) || code === 127) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Validate an identifier before writing it into generated source unquoted
|
|
19
|
+
*/
|
|
20
|
+
export function toIdentifier(value, label) {
|
|
21
|
+
if (!IDENTIFIER.test(value)) {
|
|
22
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validate and quote a relative specifier before embedding it in generated imports
|
|
28
|
+
*/
|
|
29
|
+
export function toRelativeModuleSpecifier(value, label) {
|
|
30
|
+
if (value.length === 0) {
|
|
31
|
+
throw new Error(`Invalid ${label}: module specifier cannot be empty`);
|
|
32
|
+
}
|
|
33
|
+
if (value.includes('\\')) {
|
|
34
|
+
throw new Error(`Invalid ${label}: module specifier must use forward slashes`);
|
|
35
|
+
}
|
|
36
|
+
if (hasControlChar(value)) {
|
|
37
|
+
throw new Error(`Invalid ${label}: module specifier contains control characters`);
|
|
38
|
+
}
|
|
39
|
+
if (!value.startsWith('./') && !value.startsWith('../')) {
|
|
40
|
+
throw new Error(`Invalid ${label}: module specifier must be relative`);
|
|
41
|
+
}
|
|
42
|
+
return toStringLiteral(value);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Validate a nullable identifier list whilst preserving explicit null holes
|
|
46
|
+
*/
|
|
47
|
+
export function toIdentifierList(values, label) {
|
|
48
|
+
return values
|
|
49
|
+
.map((value, index) => value === null ? 'null' : toIdentifier(value, `${label}[${index}] identifier`))
|
|
50
|
+
.join(', ');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Escape text into a safe string literal for generated source
|
|
54
|
+
*/
|
|
55
|
+
export function toStringLiteral(value, quoteStyle = "'") {
|
|
56
|
+
return `${quoteStyle}${value
|
|
57
|
+
.replace(/\\/g, '\\\\')
|
|
58
|
+
.replace(new RegExp(quoteStyle, 'g'), `\\${quoteStyle}`)
|
|
59
|
+
.replace(/\n/g, '\\n')
|
|
60
|
+
.replace(/\r/g, '\\r')
|
|
61
|
+
.replace(/\t/g, '\\t')}${quoteStyle}`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Convert a string into a valid unquoted property key if possible, otherwise quote it
|
|
65
|
+
* as a string literal for generated source
|
|
66
|
+
*/
|
|
67
|
+
function toPropertyKey(value) {
|
|
68
|
+
return IDENTIFIER.test(value) ? value : toStringLiteral(value);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check whether a value is a simple literal that can be safely inlined in generated
|
|
72
|
+
* source without risking syntax errors or readability issues, or whether it should
|
|
73
|
+
* be printed on multiple lines for clarity
|
|
74
|
+
*/
|
|
75
|
+
function isInlineValue(value) {
|
|
76
|
+
return (value === null ||
|
|
77
|
+
typeof value === 'boolean' ||
|
|
78
|
+
typeof value === 'number' ||
|
|
79
|
+
typeof value === 'string');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Emit readable ts source for generated config and manifest data
|
|
83
|
+
*/
|
|
84
|
+
export function toSourceLiteral(value, level = 0) {
|
|
85
|
+
if (value === null)
|
|
86
|
+
return 'null';
|
|
87
|
+
if (typeof value === 'string')
|
|
88
|
+
return toStringLiteral(value);
|
|
89
|
+
if (typeof value === 'boolean' || typeof value === 'number') {
|
|
90
|
+
return String(value);
|
|
91
|
+
}
|
|
92
|
+
if (typeof value === 'function')
|
|
93
|
+
return value.toString();
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
if (value.length === 0)
|
|
96
|
+
return '[]';
|
|
97
|
+
const items = value.map(item => toSourceLiteral(item, level + 1));
|
|
98
|
+
const inline = value.every(isInlineValue) && items.join(', ').length <= 60;
|
|
99
|
+
if (inline)
|
|
100
|
+
return `[${items.join(', ')}]`;
|
|
101
|
+
return [
|
|
102
|
+
'[',
|
|
103
|
+
items.map(item => indent(item, level + 1)).join(',\n'),
|
|
104
|
+
`${INDENT.repeat(level)}]`,
|
|
105
|
+
].join('\n');
|
|
106
|
+
}
|
|
107
|
+
if (typeof value === 'object') {
|
|
108
|
+
const entries = Object.entries(value);
|
|
109
|
+
if (entries.length === 0)
|
|
110
|
+
return '{}';
|
|
111
|
+
return [
|
|
112
|
+
'{',
|
|
113
|
+
entries
|
|
114
|
+
.map(([key, entryValue]) => {
|
|
115
|
+
const prefix = `${INDENT.repeat(level + 1)}${toPropertyKey(key)}: `;
|
|
116
|
+
if (typeof entryValue === 'function') {
|
|
117
|
+
const source = entryValue.toString();
|
|
118
|
+
if (source.startsWith(`${key}(`) || source.startsWith(`async ${key}(`)) {
|
|
119
|
+
return indent(source, level + 1);
|
|
120
|
+
}
|
|
121
|
+
return `${prefix}${source.replace(/\n/g, `\n${INDENT.repeat(level + 1)}`)}`;
|
|
122
|
+
}
|
|
123
|
+
return `${prefix}${toSourceLiteral(entryValue, level + 1).replace(/\n/g, `\n${INDENT.repeat(level + 1)}`)}`;
|
|
124
|
+
})
|
|
125
|
+
.join(',\n'),
|
|
126
|
+
`${INDENT.repeat(level)}}`,
|
|
127
|
+
].join('\n');
|
|
128
|
+
}
|
|
129
|
+
throw new Error(`Unsupported generated value type: ${typeof value}`);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Indent each line of a block of source code by the specified level for embedding in
|
|
133
|
+
* generated output
|
|
134
|
+
*/
|
|
135
|
+
function indent(value, level = 1) {
|
|
136
|
+
const prefix = INDENT.repeat(level);
|
|
137
|
+
return value
|
|
138
|
+
.split('\n')
|
|
139
|
+
.map(line => (line.length > 0 ? `${prefix}${line}` : line))
|
|
140
|
+
.join('\n');
|
|
141
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { StrictMode, Suspense, useCallback,
|
|
2
|
+
import { StrictMode, Suspense, useCallback, useState, useTransition } from 'react';
|
|
3
3
|
import { hydrateRoot } from 'react-dom/client';
|
|
4
4
|
import { createFromFetch, createFromReadableStream, createTemporaryReferenceSet, encodeReply, setServerCallback, } from '@vitejs/plugin-rsc/browser';
|
|
5
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';
|
|
6
|
+
import { RedirectBoundary } from '../navigation/redirect-boundary.js';
|
|
7
|
+
import { Head } from '../render/head.js';
|
|
8
|
+
import { RouterProvider } from '../router/router-provider.js';
|
|
9
|
+
import { ErrorBoundary } from '../ui/error-boundary.js';
|
|
10
10
|
/**
|
|
11
11
|
* Browser RSC hydration entry point
|
|
12
12
|
*/
|
|
@@ -14,7 +14,9 @@ export async function browser() {
|
|
|
14
14
|
const payload = await createFromReadableStream(rscStream, {
|
|
15
15
|
unstable_allowPartialStream: true,
|
|
16
16
|
});
|
|
17
|
-
|
|
17
|
+
const payloadSetter = {
|
|
18
|
+
current: () => { },
|
|
19
|
+
};
|
|
18
20
|
function A() {
|
|
19
21
|
const [p, setP] = useState(payload);
|
|
20
22
|
const [isPending, startTransition] = useTransition();
|
|
@@ -23,13 +25,10 @@ export async function browser() {
|
|
|
23
25
|
setP(payload);
|
|
24
26
|
});
|
|
25
27
|
}, []);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
setPayload = setPayloadInTransition;
|
|
31
|
-
}, [setPayloadInTransition]);
|
|
32
|
-
return (_jsx(RedirectBoundary, { children: _jsxs(RouterProvider, { setPayload: setPayloadInTransition, isNavigating: isPending, children: [
|
|
28
|
+
// make the latest payload updater available to action/hmr callbacks
|
|
29
|
+
// immediately during render, without waiting for an effect to run
|
|
30
|
+
payloadSetter.current = setPayloadInTransition;
|
|
31
|
+
return (_jsx(RedirectBoundary, { children: _jsxs(RouterProvider, { setPayload: setPayloadInTransition, isNavigating: isPending, url: p.url, children: [
|
|
33
32
|
_jsx(ErrorBoundary, { fallback: null, children: _jsx(Suspense, { fallback: null, children: _jsx(Head, { metadata: p.metadata }) }) }), p.root] }) }));
|
|
34
33
|
}
|
|
35
34
|
setServerCallback(async (id, args) => {
|
|
@@ -42,7 +41,7 @@ export async function browser() {
|
|
|
42
41
|
'x-rsc-action-id': id,
|
|
43
42
|
},
|
|
44
43
|
}), { temporaryReferences });
|
|
45
|
-
|
|
44
|
+
payloadSetter.current(payload);
|
|
46
45
|
const { ok, data } = payload.returnValue ?? {};
|
|
47
46
|
if (!ok)
|
|
48
47
|
throw data;
|
|
@@ -52,7 +51,12 @@ export async function browser() {
|
|
|
52
51
|
formState: payload.formState,
|
|
53
52
|
});
|
|
54
53
|
import.meta.hot?.on?.('rsc:update', async () => {
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
try {
|
|
55
|
+
const p = await createFromFetch(fetch(window.location.href, { headers: { Accept: 'text/x-component' } }));
|
|
56
|
+
payloadSetter.current(p);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
console.error('[hmr] failed to refresh rsc payload', err);
|
|
60
|
+
}
|
|
57
61
|
});
|
|
58
62
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { SolasRequest } from '../../types';
|
|
2
|
-
import type { Cookies } from '../../utils/cookies';
|
|
1
|
+
import type { SolasRequest } from '../../types.js';
|
|
2
|
+
import type { Cookies } from '../../utils/cookies.js';
|
|
3
3
|
export type RequestCache = {
|
|
4
4
|
cookies?: Readonly<ReturnType<typeof Cookies.parse>>;
|
|
5
5
|
headers?: ReadonlyMap<string, string>;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Context } from '../../utils/context';
|
|
1
|
+
import { Context } from '../../utils/context.js';
|
|
2
2
|
export const RequestContext = Context.create('request');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReactFormState } from 'react-dom/client';
|
|
2
|
-
import type { ImportMap, Manifest, RuntimeConfig
|
|
3
|
-
import { Metadata } from '../metadata';
|
|
2
|
+
import type { ImportMap, Manifest, RuntimeConfig } from '../../types.js';
|
|
3
|
+
import { Metadata } from '../metadata.js';
|
|
4
|
+
import { Prerender } from '../prerender.js';
|
|
4
5
|
export type RSCPayload = {
|
|
5
6
|
returnValue?: {
|
|
6
7
|
ok: boolean;
|
|
@@ -9,31 +10,16 @@ export type RSCPayload = {
|
|
|
9
10
|
formState?: ReactFormState;
|
|
10
11
|
root: React.ReactNode;
|
|
11
12
|
metadata?: Promise<Metadata.Item>;
|
|
13
|
+
url?: {
|
|
14
|
+
pathname?: string;
|
|
15
|
+
search?: string;
|
|
16
|
+
};
|
|
12
17
|
};
|
|
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 maybeAction(req: Request): Promise<{
|
|
26
|
-
action: boolean;
|
|
27
|
-
formData: null;
|
|
28
|
-
} | {
|
|
29
|
-
action: boolean;
|
|
30
|
-
formData: FormData;
|
|
31
|
-
}>;
|
|
32
18
|
/**
|
|
33
19
|
* Create the object exported by the generated RSC entry. Uses the generated config,
|
|
34
20
|
* route manifest, and import map to build the router once, then returns an object
|
|
35
21
|
* with a fetch method that handles requests
|
|
36
22
|
*/
|
|
37
|
-
export declare function createHandler(config: RuntimeConfig, manifest: Manifest, importMap: ImportMap): {
|
|
23
|
+
export declare function createHandler(config: RuntimeConfig, manifest: Manifest, importMap: ImportMap, artifactManifest?: Prerender.Artifact.Manifest | null): {
|
|
38
24
|
fetch(req: Request): Promise<Response>;
|
|
39
25
|
};
|
package/dist/internal/env/rsc.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { Solas } from '../../solas';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
2
|
+
import { renderToReadableStream } from '@vitejs/plugin-rsc/rsc';
|
|
3
|
+
import { Logger } from '../../utils/logger.js';
|
|
4
|
+
import { Solas } from '../../solas.js';
|
|
5
|
+
import { Metadata } from '../metadata.js';
|
|
6
|
+
import { HttpException, isHttpException } from '../navigation/http-exception.js';
|
|
7
|
+
import { Prerender } from '../prerender.js';
|
|
8
|
+
import { Tree } from '../render/tree.js';
|
|
9
|
+
import { createRouter } from '../router/create-router.js';
|
|
10
|
+
import { Resolver } from '../router/resolver.js';
|
|
11
|
+
import { Router } from '../router/router.js';
|
|
12
|
+
import { normalisePathname } from '../router/utils.js';
|
|
13
|
+
import { processActionRequest } from '../server/actions.js';
|
|
14
|
+
import DefaultErr from '../ui/defaults/error.js';
|
|
15
|
+
import { RequestContext } from './request-context.js';
|
|
16
|
+
import { getKnownDigest, isKnownError } from './utils.js';
|
|
17
17
|
/**
|
|
18
18
|
* Get the streamed RSC payload and response metadata for a single request.
|
|
19
19
|
* Resolves the route match, collects metadata, and returns the stream,
|
|
@@ -42,6 +42,10 @@ async function getPayload(req, manifest, importMap, baseMetadata, returnValue, f
|
|
|
42
42
|
] })),
|
|
43
43
|
returnValue,
|
|
44
44
|
formState,
|
|
45
|
+
url: {
|
|
46
|
+
pathname: url.pathname,
|
|
47
|
+
search: url.search,
|
|
48
|
+
},
|
|
45
49
|
};
|
|
46
50
|
return {
|
|
47
51
|
// this path is a safety fallback when a prerender request
|
|
@@ -81,6 +85,10 @@ async function getPayload(req, manifest, importMap, baseMetadata, returnValue, f
|
|
|
81
85
|
returnValue,
|
|
82
86
|
formState,
|
|
83
87
|
metadata,
|
|
88
|
+
url: {
|
|
89
|
+
pathname: url.pathname,
|
|
90
|
+
search: url.search,
|
|
91
|
+
},
|
|
84
92
|
};
|
|
85
93
|
// status code comes from route match error if any
|
|
86
94
|
const status = isHttpException(match.error) ? match.error.status : 200;
|
|
@@ -136,6 +144,10 @@ async function getPayload(req, manifest, importMap, baseMetadata, returnValue, f
|
|
|
136
144
|
] })),
|
|
137
145
|
returnValue,
|
|
138
146
|
formState,
|
|
147
|
+
url: {
|
|
148
|
+
pathname: url.pathname,
|
|
149
|
+
search: url.search,
|
|
150
|
+
},
|
|
139
151
|
}, {
|
|
140
152
|
temporaryReferences,
|
|
141
153
|
})),
|
|
@@ -144,83 +156,13 @@ async function getPayload(req, manifest, importMap, baseMetadata, returnValue, f
|
|
|
144
156
|
};
|
|
145
157
|
}
|
|
146
158
|
}
|
|
147
|
-
export async function action(req) {
|
|
148
|
-
let returnValue;
|
|
149
|
-
let formState;
|
|
150
|
-
let temporaryReferences;
|
|
151
|
-
const id = req.headers.get('x-rsc-action-id');
|
|
152
|
-
if (id) {
|
|
153
|
-
// x-rsc-action-id header exists when action is
|
|
154
|
-
// called via ReactClient.setServerCallback
|
|
155
|
-
const body = req.headers.get('content-type')?.startsWith('multipart/form-data')
|
|
156
|
-
? await req.formData()
|
|
157
|
-
: await req.text();
|
|
158
|
-
temporaryReferences = createTemporaryReferenceSet();
|
|
159
|
-
const args = await decodeReply(body, {
|
|
160
|
-
temporaryReferences,
|
|
161
|
-
});
|
|
162
|
-
const action = await loadServerAction(id);
|
|
163
|
-
try {
|
|
164
|
-
const data = await action.apply(null, args);
|
|
165
|
-
returnValue = { ok: true, data };
|
|
166
|
-
}
|
|
167
|
-
catch (err) {
|
|
168
|
-
returnValue = { ok: false, data: err };
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
// otherwise server function is called via
|
|
173
|
-
// <form action={...}>
|
|
174
|
-
// we might have already parsed FormData in the router for multipart action
|
|
175
|
-
// detection should be attached to the SolasRequest, so we can reuse that
|
|
176
|
-
// to avoid parsing twice
|
|
177
|
-
const parsedFormData = req[Solas.Config.REQUEST_META]?.parsedFormData;
|
|
178
|
-
const formData = parsedFormData ?? (await req.formData());
|
|
179
|
-
const decodedAction = await decodeAction(formData);
|
|
180
|
-
const result = await decodedAction();
|
|
181
|
-
formState = await decodeFormState(result, formData);
|
|
182
|
-
}
|
|
183
|
-
return { returnValue, formState, temporaryReferences };
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Check if a request is an action request and reuse parsed FormData
|
|
187
|
-
* when multipart action detection already had to inspect the body
|
|
188
|
-
*/
|
|
189
|
-
export async function maybeAction(req) {
|
|
190
|
-
if (req.method !== 'POST')
|
|
191
|
-
return { action: false, formData: null };
|
|
192
|
-
if (req.headers.has('x-rsc-action-id'))
|
|
193
|
-
return { action: true, formData: null };
|
|
194
|
-
const contentType = req.headers.get('content-type') ?? '';
|
|
195
|
-
if (!contentType.startsWith('multipart/form-data')) {
|
|
196
|
-
return { action: false, formData: null };
|
|
197
|
-
}
|
|
198
|
-
try {
|
|
199
|
-
const formData = await req.clone().formData();
|
|
200
|
-
for (const key of formData.keys()) {
|
|
201
|
-
if (key === '$ACTION_KEY' ||
|
|
202
|
-
key.startsWith('$ACTION_') ||
|
|
203
|
-
key.startsWith('$ACTION_REF_')) {
|
|
204
|
-
return { action: true, formData };
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
catch {
|
|
209
|
-
return { action: false, formData: null };
|
|
210
|
-
}
|
|
211
|
-
return { action: false, formData: null };
|
|
212
|
-
}
|
|
213
159
|
/**
|
|
214
160
|
* Create the object exported by the generated RSC entry. Uses the generated config,
|
|
215
161
|
* route manifest, and import map to build the router once, then returns an object
|
|
216
162
|
* with a fetch method that handles requests
|
|
217
163
|
*/
|
|
218
|
-
export function createHandler(config, manifest, importMap) {
|
|
164
|
+
export function createHandler(config, manifest, importMap, artifactManifest = null) {
|
|
219
165
|
const prerenderPathMode = config.trailingSlash === 'always' ? 'always' : 'never';
|
|
220
|
-
const fullyPrerenderedRoutes = new Set(Object.values(manifest)
|
|
221
|
-
.flat()
|
|
222
|
-
.filter(entry => 'prerender' in entry && String(entry.prerender) === 'full')
|
|
223
|
-
.map(entry => normalisePathname(entry.__path, prerenderPathMode)));
|
|
224
166
|
/**
|
|
225
167
|
* Create the HTTP response for a single incoming request. Runs actions when needed,
|
|
226
168
|
* converts the payload into component, HTML, or prerender artifact responses, and
|
|
@@ -233,7 +175,7 @@ export function createHandler(config, manifest, importMap) {
|
|
|
233
175
|
returnValue: undefined,
|
|
234
176
|
};
|
|
235
177
|
if (req[Solas.Config.REQUEST_META].action)
|
|
236
|
-
opts = await
|
|
178
|
+
opts = await processActionRequest(req);
|
|
237
179
|
const { stream: rscStream, status, ppr, } = await getPayload(req, manifest, importMap, config.metadata, opts.returnValue, opts.formState, opts.temporaryReferences);
|
|
238
180
|
const stream = await rscStream;
|
|
239
181
|
if (!req.headers.get('accept')?.includes('text/html')) {
|
|
@@ -268,10 +210,9 @@ export function createHandler(config, manifest, importMap) {
|
|
|
268
210
|
status,
|
|
269
211
|
});
|
|
270
212
|
}
|
|
271
|
-
const
|
|
272
|
-
?
|
|
213
|
+
const artifactManifestEntry = runtimePpr
|
|
214
|
+
? (artifactManifest?.routes[lookupPath] ?? null)
|
|
273
215
|
: null;
|
|
274
|
-
const artifactManifestEntry = artifactManifest?.routes[lookupPath] ?? null;
|
|
275
216
|
let tryPrelude = false;
|
|
276
217
|
if (artifactManifestEntry) {
|
|
277
218
|
tryPrelude = artifactManifestEntry.mode === 'ppr';
|
|
@@ -334,36 +275,16 @@ export function createHandler(config, manifest, importMap) {
|
|
|
334
275
|
return Response.redirect(url.toString(), 308);
|
|
335
276
|
}
|
|
336
277
|
// fully prerendered html can be served straight from disk for normal
|
|
337
|
-
// document requests, but artifact
|
|
278
|
+
// document requests, but build-time artifact requests must bypass
|
|
279
|
+
// this shortcut so they still render fresh output
|
|
338
280
|
if (!import.meta.env.DEV &&
|
|
339
281
|
accept.includes('text/html') &&
|
|
340
282
|
req.headers.get(`x-${Solas.Config.SLUG}-prerender-artifact`) !== '1') {
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const artifactManifestEntry = artifactManifest?.routes[lookupPath] ?? null;
|
|
347
|
-
const fullHtmlPath = lookupPath === '/'
|
|
348
|
-
? path.join(Solas.Config.OUT_DIR, 'index.html')
|
|
349
|
-
: config.trailingSlash === 'always'
|
|
350
|
-
? path.join(Solas.Config.OUT_DIR, routePath, 'index.html')
|
|
351
|
-
: path.join(Solas.Config.OUT_DIR, `${routePath}.html`);
|
|
352
|
-
if (fullyPrerenderedRoutes.has(lookupPath)) {
|
|
353
|
-
prerenderPath = fullHtmlPath;
|
|
354
|
-
}
|
|
355
|
-
else if (artifactManifestEntry) {
|
|
356
|
-
if (artifactManifestEntry.mode === 'full') {
|
|
357
|
-
prerenderPath = fullHtmlPath;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
else {
|
|
361
|
-
const artifactMetadata = await Prerender.Artifact.loadMetadata(Solas.Config.OUT_DIR, lookupPath);
|
|
362
|
-
if (artifactMetadata &&
|
|
363
|
-
Prerender.Artifact.isCompatible(artifactMetadata, lookupPath, 'full')) {
|
|
364
|
-
prerenderPath = fullHtmlPath;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
283
|
+
const lookupPath = normalisePathname(canonicalPath, prerenderPathMode);
|
|
284
|
+
const fullPrerenderFilename = artifactManifest?.routes[lookupPath]?.fullPrerenderFilename;
|
|
285
|
+
const prerenderPath = fullPrerenderFilename
|
|
286
|
+
? Prerender.Artifact.getFilePath(Solas.Config.OUT_DIR, lookupPath, fullPrerenderFilename)
|
|
287
|
+
: null;
|
|
367
288
|
if (prerenderPath) {
|
|
368
289
|
const res = await Router.serve(prerenderPath, req, config.precompress, {
|
|
369
290
|
// avoid shared or proxy caching unless users opt into public caching later
|
package/dist/internal/env/ssr.js
CHANGED
|
@@ -4,18 +4,18 @@ import { resume as reactResume, renderToReadableStream } from 'react-dom/server.
|
|
|
4
4
|
import { prerender as reactPrerender } from 'react-dom/static.edge';
|
|
5
5
|
import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr';
|
|
6
6
|
import { injectRSCPayload } from 'rsc-html-stream/server';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
7
|
+
import { Logger } from '../../utils/logger.js';
|
|
8
|
+
import { Solas } from '../../solas.js';
|
|
9
|
+
import { RedirectBoundary } from '../navigation/redirect-boundary.js';
|
|
10
|
+
import { Prerender } from '../prerender.js';
|
|
11
|
+
import { Head } from '../render/head.js';
|
|
12
|
+
import { RouterProvider } from '../router/router-provider.js';
|
|
13
|
+
import { ErrorBoundary } from '../ui/error-boundary.js';
|
|
14
|
+
import { getKnownDigest } from './utils.js';
|
|
15
15
|
const logger = new Logger();
|
|
16
16
|
function A({ payloadPromise }) {
|
|
17
17
|
const payload = use(payloadPromise);
|
|
18
|
-
return (_jsx(RedirectBoundary, { children: _jsxs(RouterProvider, { children: [
|
|
18
|
+
return (_jsx(RedirectBoundary, { children: _jsxs(RouterProvider, { url: payload.url, children: [
|
|
19
19
|
_jsx(ErrorBoundary, { fallback: null, children: _jsx(Suspense, { fallback: null, children: _jsx(Head, { metadata: payload.metadata }) }) }), payload.root] }) }));
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { HTTP_EXCEPTION_DIGEST_PREFIX } from '../navigation/http-exception';
|
|
2
|
-
import { REDIRECT_DIGEST_PREFIX } from '../navigation/redirect';
|
|
1
|
+
import { HTTP_EXCEPTION_DIGEST_PREFIX } from '../navigation/http-exception.js';
|
|
2
|
+
import { REDIRECT_DIGEST_PREFIX } from '../navigation/redirect.js';
|
|
3
3
|
const possibilities = [HTTP_EXCEPTION_DIGEST_PREFIX, REDIRECT_DIGEST_PREFIX];
|
|
4
4
|
export function getKnownDigest(err) {
|
|
5
5
|
if (typeof err === 'object' &&
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { HttpException } from './navigation/http-exception';
|
|
2
|
-
import { Build } from './build';
|
|
1
|
+
import type { HttpException } from './navigation/http-exception.js';
|
|
2
|
+
import { Build } from './build.js';
|
|
3
3
|
type EntryKind = typeof Build.EntryKind;
|
|
4
4
|
export declare namespace Metadata {
|
|
5
5
|
type EntrySource = Exclude<EntryKind[keyof EntryKind], typeof Build.EntryKind.ENDPOINT | typeof Build.EntryKind.MIDDLEWARE>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Build } from './build';
|
|
2
|
-
import { isHttpException } from './navigation/http-exception';
|
|
1
|
+
import { Build } from './build.js';
|
|
2
|
+
import { isHttpException } from './navigation/http-exception.js';
|
|
3
3
|
const TITLE_TEMPLATE_STR = '%s';
|
|
4
4
|
export { Metadata };
|
|
5
5
|
var Metadata;
|
|
@@ -14,6 +14,18 @@ var Metadata;
|
|
|
14
14
|
[Build.EntryKind['500']]: 40,
|
|
15
15
|
[Build.EntryKind.LOADING]: 50,
|
|
16
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* Check whether a value is a supported metadata primitive
|
|
19
|
+
*/
|
|
20
|
+
function isTagValue(value) {
|
|
21
|
+
return (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Convert supported metadata primitives to string for title handling
|
|
25
|
+
*/
|
|
26
|
+
function toTitleString(value) {
|
|
27
|
+
return isTagValue(value) ? String(value) : undefined;
|
|
28
|
+
}
|
|
17
29
|
class Collection {
|
|
18
30
|
/**
|
|
19
31
|
* The base metadata object
|
|
@@ -40,8 +52,8 @@ var Metadata;
|
|
|
40
52
|
const metaMap = new Map();
|
|
41
53
|
const linkMap = new Map();
|
|
42
54
|
for (const item of items) {
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
const titleStr = toTitleString(item.title);
|
|
56
|
+
if (titleStr !== undefined) {
|
|
45
57
|
if (titleStr.includes(TITLE_TEMPLATE_STR)) {
|
|
46
58
|
titleTemplate = titleStr;
|
|
47
59
|
}
|
|
@@ -119,10 +131,10 @@ var Metadata;
|
|
|
119
131
|
if (items.length === 0)
|
|
120
132
|
return Collection.#clone(this.#base);
|
|
121
133
|
let merged = Collection.#clone(this.#base);
|
|
122
|
-
const res = await Promise.allSettled(items.map(
|
|
134
|
+
const res = await Promise.allSettled(items.map(item => item.task));
|
|
123
135
|
const ok = res
|
|
124
136
|
.filter((result) => result.status === 'fulfilled')
|
|
125
|
-
.map(result => result.value);
|
|
137
|
+
.map((result) => result.value);
|
|
126
138
|
if (ok.length)
|
|
127
139
|
merged = Collection.#merge(merged, ...ok);
|
|
128
140
|
return merged;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { BoundaryError } from '../../types';
|
|
2
|
-
import { type HttpException } from './http-exception';
|
|
1
|
+
import type { BoundaryError } from '../../types.js';
|
|
2
|
+
import { type HttpException } from './http-exception.js';
|
|
3
3
|
type ComponentsMap = Partial<Record<HttpException.StatusCode, React.ReactElement | null>>;
|
|
4
4
|
export type Props = {
|
|
5
5
|
fallback: ((error: BoundaryError) => React.ReactNode) | React.ReactNode;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Component } from 'react';
|
|
4
|
-
import { HTTP_EXCEPTION_DIGEST_PREFIX, isHttpException, } from './http-exception';
|
|
4
|
+
import { HTTP_EXCEPTION_DIGEST_PREFIX, isHttpException, } from './http-exception.js';
|
|
5
5
|
function isSupportedStatusCode(value) {
|
|
6
6
|
return value === 401 || value === 403 || value === 404 || value === 500;
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef } from 'react';
|
|
4
|
-
import { useRouter } from '../router/use-router';
|
|
4
|
+
import { useRouter } from '../router/use-router.js';
|
|
5
5
|
function guard(path, prefetcher) {
|
|
6
6
|
const connection = window.navigator.connection;
|
|
7
7
|
if (document.visibilityState === 'hidden')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { Component } from 'react';
|
|
4
|
-
import { isRedirect, REDIRECT_DIGEST_PREFIX } from './redirect';
|
|
4
|
+
import { isRedirect, REDIRECT_DIGEST_PREFIX } from './redirect.js';
|
|
5
5
|
class Boundary extends Component {
|
|
6
6
|
constructor(props) {
|
|
7
7
|
super(props);
|