@solidjs/start 0.4.0-alpha.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/client/StartClient.tsx +17 -0
- package/client/index.tsx +3 -0
- package/client/islands.tsx +5 -0
- package/client/mount.ts +98 -0
- package/client/spa/index.tsx +8 -0
- package/config/fs-router.js +137 -0
- package/config/index.js +178 -0
- package/config/server-components.js +106 -0
- package/config/server-runtime.jsx +44 -0
- package/package.json +52 -0
- package/server/StartServer.tsx +42 -0
- package/server/handler.tsx +72 -0
- package/server/index.tsx +4 -0
- package/server/islands/index.tsx +102 -0
- package/server/islands/utils.tsx +87 -0
- package/server/middleware.ts +129 -0
- package/server/renderAsset.tsx +16 -0
- package/server/spa/StartServer.tsx +30 -0
- package/server/spa/handler.tsx +24 -0
- package/server/spa/index.tsx +4 -0
- package/server/types.ts +58 -0
- package/shared/ErrorBoundary.tsx +48 -0
- package/shared/FileRoutes.tsx +28 -0
- package/shared/HttpHeader.tsx +30 -0
- package/shared/HttpStatusCode.tsx +13 -0
- package/shared/clientOnly.tsx +28 -0
- package/shared/index.tsx +6 -0
- package/shared/lazyRoute.tsx +59 -0
- package/shared/routes.ts +44 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import App from "#start/app";
|
|
3
|
+
import { Hydration, HydrationScript, NoHydration, ssr } from "solid-js/web";
|
|
4
|
+
import { renderAsset } from "./renderAsset";
|
|
5
|
+
|
|
6
|
+
const docType = ssr("<!DOCTYPE html>");
|
|
7
|
+
|
|
8
|
+
export function StartServer(props) {
|
|
9
|
+
const context = props.context;
|
|
10
|
+
const parsed = new URL(context.request.url);
|
|
11
|
+
const path = parsed.pathname + parsed.search;
|
|
12
|
+
return (
|
|
13
|
+
<NoHydration>
|
|
14
|
+
{docType as unknown as any}
|
|
15
|
+
<props.document
|
|
16
|
+
assets={<>{context.assets.map(m => renderAsset(m))}</>}
|
|
17
|
+
scripts={
|
|
18
|
+
<>
|
|
19
|
+
<HydrationScript />
|
|
20
|
+
<script innerHTML={`window.manifest = ${JSON.stringify(context.manifest)}`} />
|
|
21
|
+
<script
|
|
22
|
+
type="module"
|
|
23
|
+
src={
|
|
24
|
+
import.meta.env.MANIFEST["client"].inputs[
|
|
25
|
+
import.meta.env.MANIFEST["client"].handler
|
|
26
|
+
].output.path
|
|
27
|
+
}
|
|
28
|
+
/>
|
|
29
|
+
</>
|
|
30
|
+
}
|
|
31
|
+
>
|
|
32
|
+
{!import.meta.env.START_ISLANDS ? (
|
|
33
|
+
<Hydration>
|
|
34
|
+
<App />
|
|
35
|
+
</Hydration>
|
|
36
|
+
) : (
|
|
37
|
+
<App />
|
|
38
|
+
)}
|
|
39
|
+
</props.document>
|
|
40
|
+
</NoHydration>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { renderToStream } from "solid-js/web";
|
|
2
|
+
import { provideRequestEvent } from "solid-js/web/storage";
|
|
3
|
+
import { eventHandler, EventHandlerObject, EventHandlerRequest, H3Event } from "vinxi/server";
|
|
4
|
+
import { createRoutes } from "../shared/FileRoutes";
|
|
5
|
+
import { apiRoutes } from "../shared/routes";
|
|
6
|
+
import { getFetchEvent } from "./middleware";
|
|
7
|
+
import { FetchEvent, FETCH_EVENT, PageEvent } from "./types";
|
|
8
|
+
|
|
9
|
+
export function createHandler(
|
|
10
|
+
fn: (context: PageEvent) => unknown,
|
|
11
|
+
options: {
|
|
12
|
+
nonce?: string;
|
|
13
|
+
renderId?: string;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
createPageEvent?: (event: FetchEvent) => Promise<PageEvent>;
|
|
16
|
+
onRequest?: EventHandlerObject["onRequest"];
|
|
17
|
+
onBeforeResponse?: EventHandlerObject["onBeforeResponse"];
|
|
18
|
+
} = {}
|
|
19
|
+
) {
|
|
20
|
+
return eventHandler({
|
|
21
|
+
onRequest: options.onRequest,
|
|
22
|
+
onBeforeResponse: options.onBeforeResponse,
|
|
23
|
+
handler: (e: H3Event<EventHandlerRequest> & { startEvent: FetchEvent }) => {
|
|
24
|
+
const event = getFetchEvent(e);
|
|
25
|
+
|
|
26
|
+
return provideRequestEvent(event, async () => {
|
|
27
|
+
// api
|
|
28
|
+
const match = apiRoutes.find(
|
|
29
|
+
route =>
|
|
30
|
+
route[`$${event.request.method}`] && new URL(event.request.url).pathname === route.path
|
|
31
|
+
);
|
|
32
|
+
if (match) {
|
|
33
|
+
const mod = await match[`$${event.request.method}`].import();
|
|
34
|
+
const fn = mod[event.request.method];
|
|
35
|
+
const result = await fn(event);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// render stream
|
|
40
|
+
const context = await createPageEvent(event);
|
|
41
|
+
const stream = renderToStream(() => fn(context), options);
|
|
42
|
+
if (context.response && context.response.url) {
|
|
43
|
+
return event.redirect(context.response.url);
|
|
44
|
+
}
|
|
45
|
+
return { pipeTo: stream.pipeTo };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function createPageEvent(ctx: FetchEvent) {
|
|
52
|
+
const clientManifest = import.meta.env.MANIFEST["client"];
|
|
53
|
+
const serverManifest = import.meta.env.MANIFEST["ssr"];
|
|
54
|
+
const prevPath = ctx.request.headers.get("x-solid-referrer");
|
|
55
|
+
const mutation = ctx.request.headers.get("x-solid-mutation") === "true";
|
|
56
|
+
const pageEvent: PageEvent = Object.assign(ctx, {
|
|
57
|
+
manifest: await clientManifest.json(),
|
|
58
|
+
assets: [
|
|
59
|
+
...(await clientManifest.inputs[clientManifest.handler].assets()),
|
|
60
|
+
...(import.meta.env.START_ISLANDS
|
|
61
|
+
? await serverManifest.inputs[serverManifest.handler].assets()
|
|
62
|
+
: [])
|
|
63
|
+
],
|
|
64
|
+
routes: createRoutes(),
|
|
65
|
+
prevUrl: prevPath || "",
|
|
66
|
+
mutation: mutation,
|
|
67
|
+
$type: FETCH_EVENT,
|
|
68
|
+
$islands: new Set<string>()
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return pageEvent;
|
|
72
|
+
}
|
package/server/index.tsx
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Component, ComponentProps, lazy, sharedConfig } from "solid-js";
|
|
2
|
+
import { getRequestEvent, Hydration, NoHydration } from "solid-js/web";
|
|
3
|
+
// import { IslandManifest } from "./types";
|
|
4
|
+
import { splitProps } from "./utils";
|
|
5
|
+
|
|
6
|
+
declare module "solid-js" {
|
|
7
|
+
namespace JSX {
|
|
8
|
+
interface IntrinsicElements {
|
|
9
|
+
"solid-island": {
|
|
10
|
+
"data-props": string;
|
|
11
|
+
"data-id": string;
|
|
12
|
+
"data-when": "idle" | "load";
|
|
13
|
+
children: JSX.Element;
|
|
14
|
+
};
|
|
15
|
+
"solid-children": {
|
|
16
|
+
children: JSX.Element;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createIslandReference<T extends Component<any>>(
|
|
23
|
+
Comp:
|
|
24
|
+
| T
|
|
25
|
+
| (() => Promise<{
|
|
26
|
+
default: T;
|
|
27
|
+
}>),
|
|
28
|
+
id: string,
|
|
29
|
+
name: string
|
|
30
|
+
): T {
|
|
31
|
+
let Component = Comp as T;
|
|
32
|
+
|
|
33
|
+
if (!import.meta.env.START_ISLANDS) {
|
|
34
|
+
// TODO: have some sane semantics for islands used in non-island mode
|
|
35
|
+
return lazy(Comp as () => Promise<{ default: T }>);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function IslandComponent(props: ComponentProps<T>) {
|
|
39
|
+
return (
|
|
40
|
+
<Component {...props}>
|
|
41
|
+
<NoHydration>{props.children}</NoHydration>
|
|
42
|
+
</Component>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return ((compProps: ComponentProps<T>) => {
|
|
47
|
+
if (import.meta.env.SSR) {
|
|
48
|
+
const context = getRequestEvent();
|
|
49
|
+
const [, props] = splitProps(compProps, ["children"] as any);
|
|
50
|
+
const [, spreadProps] = splitProps(compProps, [] as any);
|
|
51
|
+
|
|
52
|
+
let fpath: string;
|
|
53
|
+
let styles: string[] = [];
|
|
54
|
+
// if (import.meta.env.PROD) {
|
|
55
|
+
// let x = context.env.manifest?.[path] as IslandManifest;
|
|
56
|
+
// context.$islands.add(path);
|
|
57
|
+
// if (x) {
|
|
58
|
+
// fpath = x.script.href;
|
|
59
|
+
// styles = x.assets.filter(v => v.type == "style").map(v => v.href);
|
|
60
|
+
// }
|
|
61
|
+
// } else {
|
|
62
|
+
fpath = id + "#" + name;
|
|
63
|
+
// }
|
|
64
|
+
|
|
65
|
+
const serialize = (props: ComponentProps<T>) => {
|
|
66
|
+
let offset = 0;
|
|
67
|
+
let el = JSON.stringify(props, (key, value) => {
|
|
68
|
+
if (value && value.t) {
|
|
69
|
+
offset++;
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
"data-props": el,
|
|
77
|
+
"data-offset": offset
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// @ts-expect-error
|
|
82
|
+
if (!sharedConfig.context?.noHydrate) {
|
|
83
|
+
return <Component {...compProps} />;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Hydration>
|
|
88
|
+
<solid-island
|
|
89
|
+
data-id={fpath!}
|
|
90
|
+
data-when={(props as any)["client:idle"] ? "idle" : "load"}
|
|
91
|
+
data-css={JSON.stringify(styles)}
|
|
92
|
+
{...serialize(props)}
|
|
93
|
+
>
|
|
94
|
+
<IslandComponent {...spreadProps} />
|
|
95
|
+
</solid-island>
|
|
96
|
+
</Hydration>
|
|
97
|
+
);
|
|
98
|
+
} else {
|
|
99
|
+
return <Component {...compProps} />;
|
|
100
|
+
}
|
|
101
|
+
}) as T;
|
|
102
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { $PROXY } from "solid-js";
|
|
3
|
+
|
|
4
|
+
function trueFn() {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
const propTraps = {
|
|
8
|
+
get(_, property, receiver) {
|
|
9
|
+
if (property === $PROXY) return receiver;
|
|
10
|
+
return _.get(property);
|
|
11
|
+
},
|
|
12
|
+
has(_, property) {
|
|
13
|
+
return _.has(property);
|
|
14
|
+
},
|
|
15
|
+
set: trueFn,
|
|
16
|
+
deleteProperty: trueFn,
|
|
17
|
+
getOwnPropertyDescriptor(_, property) {
|
|
18
|
+
return {
|
|
19
|
+
configurable: true,
|
|
20
|
+
enumerable: true,
|
|
21
|
+
get() {
|
|
22
|
+
return _.get(property);
|
|
23
|
+
},
|
|
24
|
+
set: trueFn,
|
|
25
|
+
deleteProperty: trueFn
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
ownKeys(_) {
|
|
29
|
+
return _.keys();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
export function splitProps<T>(props: T, ...keys: (keyof T)[]): [T, T] {
|
|
33
|
+
const blocked = new Set(keys.flat());
|
|
34
|
+
const descriptors = Object.getOwnPropertyDescriptors(props);
|
|
35
|
+
const isProxy = $PROXY in props;
|
|
36
|
+
if (!isProxy) keys.push(Object.keys(descriptors).filter(k => !blocked.has(k)));
|
|
37
|
+
|
|
38
|
+
const res = keys.map(k => {
|
|
39
|
+
const clone = {};
|
|
40
|
+
for (let i = 0; i < k.length; i++) {
|
|
41
|
+
const key = k[i];
|
|
42
|
+
let cache;
|
|
43
|
+
Object.defineProperty(clone, key, {
|
|
44
|
+
enumerable: descriptors[key]?.enumerable ?? false,
|
|
45
|
+
configurable: true,
|
|
46
|
+
get() {
|
|
47
|
+
if (cache) {
|
|
48
|
+
return cache;
|
|
49
|
+
}
|
|
50
|
+
let val = props[key];
|
|
51
|
+
if (val?.t) {
|
|
52
|
+
val.t = `<solid-children>${val.t}</solid-children>`;
|
|
53
|
+
} else if (Array.isArray(val) && val.every(v => v?.t)) {
|
|
54
|
+
val = {
|
|
55
|
+
t: `<solid-children>${val.map(v => `${v.t}`).join("\n")}</solid-children>`
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
cache = val;
|
|
59
|
+
return val;
|
|
60
|
+
},
|
|
61
|
+
set() {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return clone;
|
|
67
|
+
});
|
|
68
|
+
if (isProxy) {
|
|
69
|
+
res.push(
|
|
70
|
+
new Proxy(
|
|
71
|
+
{
|
|
72
|
+
get(property) {
|
|
73
|
+
return blocked.has(property) ? undefined : props[property];
|
|
74
|
+
},
|
|
75
|
+
has(property) {
|
|
76
|
+
return blocked.has(property) ? false : property in props;
|
|
77
|
+
},
|
|
78
|
+
keys() {
|
|
79
|
+
return Object.keys(props).filter(k => !blocked.has(k));
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
propTraps
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return res;
|
|
87
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendResponseHeader,
|
|
3
|
+
defineMiddleware,
|
|
4
|
+
EventHandlerRequest,
|
|
5
|
+
getRequestIP,
|
|
6
|
+
getResponseHeader,
|
|
7
|
+
getResponseStatus,
|
|
8
|
+
H3Event,
|
|
9
|
+
removeResponseHeader,
|
|
10
|
+
sendRedirect,
|
|
11
|
+
sendWebResponse,
|
|
12
|
+
setResponseHeader,
|
|
13
|
+
setResponseStatus,
|
|
14
|
+
toWebRequest
|
|
15
|
+
} from "vinxi/server";
|
|
16
|
+
import { FetchEvent } from "./types";
|
|
17
|
+
|
|
18
|
+
export * from "vinxi/server";
|
|
19
|
+
|
|
20
|
+
const h3EventSymbol = Symbol("h3Event");
|
|
21
|
+
const fetchEventSymbol = Symbol("fetchEvent");
|
|
22
|
+
|
|
23
|
+
export function createFetchEvent(event: H3Event<EventHandlerRequest>): FetchEvent {
|
|
24
|
+
return {
|
|
25
|
+
request: toWebRequest(event),
|
|
26
|
+
clientAddress: getRequestIP(event),
|
|
27
|
+
locals: {},
|
|
28
|
+
redirect: (url, status) => sendRedirect(event, url, status),
|
|
29
|
+
getResponseStatus: () => getResponseStatus(event),
|
|
30
|
+
setResponseStatus: (code, text) => setResponseStatus(event, code, text),
|
|
31
|
+
getResponseHeader: name => getResponseHeader(event, name),
|
|
32
|
+
setResponseHeader: (name, value) => setResponseHeader(event, name, value),
|
|
33
|
+
appendResponseHeader: (name, value) => appendResponseHeader(event, name, value),
|
|
34
|
+
removeResponseHeader: name => removeResponseHeader(event, name),
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
[h3EventSymbol]: event
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getH3Event(fetchEvent: FetchEvent): H3Event<EventHandlerRequest> {
|
|
41
|
+
// @ts-ignore
|
|
42
|
+
return fetchEvent[h3EventSymbol];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getFetchEvent(h3Event: H3Event): FetchEvent {
|
|
46
|
+
if (!h3Event[fetchEventSymbol]) {
|
|
47
|
+
const fetchEvent = createFetchEvent(h3Event);
|
|
48
|
+
h3Event[fetchEventSymbol] = fetchEvent;
|
|
49
|
+
// @ts-ignore
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return h3Event[fetchEventSymbol];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type Middleware = (input: MiddlewareInput) => MiddlewareFn;
|
|
56
|
+
/** Input parameters for to an Exchange factory function. */
|
|
57
|
+
|
|
58
|
+
export interface MiddlewareInput {
|
|
59
|
+
forward: MiddlewareFn;
|
|
60
|
+
}
|
|
61
|
+
/** Function responsible for receiving an observable [operation]{@link Operation} and returning a [result]{@link OperationResult}. */
|
|
62
|
+
|
|
63
|
+
export type MiddlewareFn = (event: FetchEvent) => Promise<unknown> | unknown;
|
|
64
|
+
/** This composes an array of Exchanges into a single ExchangeIO function */
|
|
65
|
+
|
|
66
|
+
export const composeMiddleware =
|
|
67
|
+
(exchanges: Middleware[]) =>
|
|
68
|
+
({ forward }: MiddlewareInput) =>
|
|
69
|
+
exchanges.reduceRight(
|
|
70
|
+
(forward, exchange) =>
|
|
71
|
+
exchange({
|
|
72
|
+
forward
|
|
73
|
+
}),
|
|
74
|
+
forward
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
type RequestMiddleware = (event: FetchEvent) => Response | Promise<Response> | void | Promise<void>;
|
|
78
|
+
|
|
79
|
+
type ResponseMiddleware = (
|
|
80
|
+
event: FetchEvent,
|
|
81
|
+
response: Response
|
|
82
|
+
) => Response | Promise<Response> | void | Promise<void>;
|
|
83
|
+
|
|
84
|
+
function wrapRequestMiddleware(onRequest: RequestMiddleware) {
|
|
85
|
+
return async (h3Event: H3Event) => {
|
|
86
|
+
const fetchEvent = getFetchEvent(h3Event);
|
|
87
|
+
const response = await onRequest(fetchEvent);
|
|
88
|
+
if (!response) {
|
|
89
|
+
return;
|
|
90
|
+
} else {
|
|
91
|
+
sendWebResponse(h3Event, response);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function wrapResponseMiddleware(onBeforeResponse: ResponseMiddleware) {
|
|
97
|
+
return async (h3Event: H3Event, response: Response) => {
|
|
98
|
+
const fetchEvent = getFetchEvent(h3Event);
|
|
99
|
+
const mwResponse = await onBeforeResponse(fetchEvent, response);
|
|
100
|
+
if (!mwResponse) {
|
|
101
|
+
return;
|
|
102
|
+
} else {
|
|
103
|
+
sendWebResponse(h3Event, mwResponse);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function createMiddleware({
|
|
109
|
+
onRequest,
|
|
110
|
+
onBeforeResponse
|
|
111
|
+
}: {
|
|
112
|
+
onRequest?: RequestMiddleware | RequestMiddleware[] | undefined;
|
|
113
|
+
onBeforeResponse?: ResponseMiddleware | ResponseMiddleware[] | undefined;
|
|
114
|
+
}) {
|
|
115
|
+
return defineMiddleware({
|
|
116
|
+
onRequest:
|
|
117
|
+
typeof onRequest === "function"
|
|
118
|
+
? wrapRequestMiddleware(onRequest)
|
|
119
|
+
: Array.isArray(onRequest)
|
|
120
|
+
? onRequest.map(wrapRequestMiddleware)
|
|
121
|
+
: undefined,
|
|
122
|
+
onBeforeResponse:
|
|
123
|
+
typeof onBeforeResponse === "function"
|
|
124
|
+
? wrapResponseMiddleware(onBeforeResponse)
|
|
125
|
+
: Array.isArray(onBeforeResponse)
|
|
126
|
+
? onBeforeResponse.map(wrapResponseMiddleware)
|
|
127
|
+
: undefined
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const assetMap = {
|
|
2
|
+
style: (props) => <style {...props.attrs}>{props.children}</style>,
|
|
3
|
+
link: (props) => <link {...props.attrs} />,
|
|
4
|
+
script: (props) => {
|
|
5
|
+
return props.attrs.src ? (
|
|
6
|
+
<script {...props.attrs} id={props.key}>
|
|
7
|
+
{" "}
|
|
8
|
+
</script>
|
|
9
|
+
) : null;
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function renderAsset(asset) {
|
|
14
|
+
let { tag, attrs: { key, ...attrs } = { key: undefined }, children } = asset;
|
|
15
|
+
return assetMap[tag]({ attrs, key, children });
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import { NoHydration, ssr } from "solid-js/web";
|
|
3
|
+
import { renderAsset } from "../renderAsset";
|
|
4
|
+
|
|
5
|
+
const docType = ssr("<!DOCTYPE html>");
|
|
6
|
+
|
|
7
|
+
export function StartServer(props) {
|
|
8
|
+
const context = props.context;
|
|
9
|
+
return (
|
|
10
|
+
<NoHydration>
|
|
11
|
+
{docType as unknown as any}
|
|
12
|
+
<props.document
|
|
13
|
+
assets={<>{context.assets.map(m => renderAsset(m))}</>}
|
|
14
|
+
scripts={
|
|
15
|
+
<>
|
|
16
|
+
<script innerHTML={`window.manifest = ${JSON.stringify(context.manifest)}`} />
|
|
17
|
+
<script
|
|
18
|
+
type="module"
|
|
19
|
+
src={
|
|
20
|
+
import.meta.env.MANIFEST["client"].inputs[
|
|
21
|
+
import.meta.env.MANIFEST["client"].handler
|
|
22
|
+
].output.path
|
|
23
|
+
}
|
|
24
|
+
/>
|
|
25
|
+
</>
|
|
26
|
+
}
|
|
27
|
+
></props.document>
|
|
28
|
+
</NoHydration>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createHandler as createBaseHandler } from "../handler";
|
|
2
|
+
import { FetchEvent, FETCH_EVENT, PageEvent } from "../types";
|
|
3
|
+
|
|
4
|
+
export function createHandler(
|
|
5
|
+
fn: (context: PageEvent) => unknown,
|
|
6
|
+
options?: { nonce?: string; renderId?: string; timeoutMs?: number }
|
|
7
|
+
) {
|
|
8
|
+
return createBaseHandler(fn, { ...options, createPageEvent });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function createPageEvent(ctx: FetchEvent) {
|
|
12
|
+
const clientManifest = import.meta.env.MANIFEST["client"];
|
|
13
|
+
const pageEvent: PageEvent = Object.assign(ctx, {
|
|
14
|
+
manifest: await clientManifest.json(),
|
|
15
|
+
assets: [...(await clientManifest.inputs[clientManifest.handler].assets())],
|
|
16
|
+
routes: [],
|
|
17
|
+
prevUrl: "",
|
|
18
|
+
mutation: false,
|
|
19
|
+
$type: FETCH_EVENT,
|
|
20
|
+
$islands: new Set<string>()
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return pageEvent;
|
|
24
|
+
}
|
package/server/types.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { JSX } from "solid-js";
|
|
2
|
+
|
|
3
|
+
export const FETCH_EVENT = "$FETCH";
|
|
4
|
+
|
|
5
|
+
export type DocumentComponentProps = {
|
|
6
|
+
assets: any;
|
|
7
|
+
scripts: any;
|
|
8
|
+
children: JSX.Element;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ContextMatches = {
|
|
12
|
+
originalPath: string;
|
|
13
|
+
pattern: string;
|
|
14
|
+
path: string;
|
|
15
|
+
params: unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type TagDescription = {
|
|
19
|
+
tag: string;
|
|
20
|
+
props: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type RouterContext = {
|
|
24
|
+
// router matches;
|
|
25
|
+
matches: ContextMatches[][];
|
|
26
|
+
// redirected url
|
|
27
|
+
url: string;
|
|
28
|
+
|
|
29
|
+
// server route fragments
|
|
30
|
+
replaceOutletId: string;
|
|
31
|
+
newOutletId: string;
|
|
32
|
+
partial: boolean;
|
|
33
|
+
nextRoute: any;
|
|
34
|
+
prevRoute: any;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export interface FetchEvent {
|
|
38
|
+
request: Request;
|
|
39
|
+
clientAddress: string;
|
|
40
|
+
locals: Record<string, unknown>;
|
|
41
|
+
redirect(url: string, status?: number): void;
|
|
42
|
+
getResponseStatus(): number;
|
|
43
|
+
setResponseStatus(code: number, text?: string): void;
|
|
44
|
+
getResponseHeader(name: string): string | number | string[];
|
|
45
|
+
setResponseHeader(name: string, value: string): void;
|
|
46
|
+
appendResponseHeader(name: string, value: string): void;
|
|
47
|
+
removeResponseHeader(name: string): void;
|
|
48
|
+
}
|
|
49
|
+
export interface PageEvent extends FetchEvent {
|
|
50
|
+
manifest: any;
|
|
51
|
+
assets: any;
|
|
52
|
+
routes: any[];
|
|
53
|
+
prevUrl: string | null;
|
|
54
|
+
$type: typeof FETCH_EVENT;
|
|
55
|
+
$islands: Set<string>;
|
|
56
|
+
mutation: boolean;
|
|
57
|
+
response?: Response;
|
|
58
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEffect,
|
|
3
|
+
ErrorBoundary as ErrorBoundaryBase,
|
|
4
|
+
ParentProps,
|
|
5
|
+
resetErrorBoundaries
|
|
6
|
+
} from "solid-js";
|
|
7
|
+
|
|
8
|
+
export function ErrorBoundary(props: ParentProps) {
|
|
9
|
+
return (
|
|
10
|
+
<ErrorBoundaryBase fallback={e => <ErrorMessage error={e} />}>
|
|
11
|
+
{props.children}
|
|
12
|
+
</ErrorBoundaryBase>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function ErrorMessage(props: { error: any }) {
|
|
17
|
+
createEffect(() => console.error(props.error));
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div style="padding: 16px">
|
|
21
|
+
<div
|
|
22
|
+
style={`
|
|
23
|
+
background-color: rgba(252, 165, 165);
|
|
24
|
+
color: rgb(153, 27, 27);
|
|
25
|
+
border-radius": 5px;
|
|
26
|
+
overflow: scroll;
|
|
27
|
+
padding: 16px;
|
|
28
|
+
margin-bottom: 8px;
|
|
29
|
+
`}
|
|
30
|
+
>
|
|
31
|
+
<p style="font-weight: bold" id="error-message">
|
|
32
|
+
{props.error.message}
|
|
33
|
+
</p>
|
|
34
|
+
<button
|
|
35
|
+
id="reset-errors"
|
|
36
|
+
onClick={resetErrorBoundaries}
|
|
37
|
+
style={`color: rgba(252, 165, 165);
|
|
38
|
+
background-color: rgb(153, 27, 27);
|
|
39
|
+
border-radius: 5px;
|
|
40
|
+
padding: 4px 8px`}
|
|
41
|
+
>
|
|
42
|
+
Clear errors and retry
|
|
43
|
+
</button>
|
|
44
|
+
<pre style={"margin-top: 8px;width:100%"}>{props.error.stack}</pre>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getRequestEvent, isServer } from "solid-js/web";
|
|
2
|
+
import lazyRoute from "./lazyRoute";
|
|
3
|
+
|
|
4
|
+
import { pageRoutes as routeConfigs } from "./routes";
|
|
5
|
+
|
|
6
|
+
export function createRoutes() {
|
|
7
|
+
function createRoute(route) {
|
|
8
|
+
return {
|
|
9
|
+
...route,
|
|
10
|
+
...(route.$$route ? route.$$route.require().route : undefined),
|
|
11
|
+
component: lazyRoute(
|
|
12
|
+
route.$component,
|
|
13
|
+
import.meta.env.START_ISLANDS
|
|
14
|
+
? import.meta.env.MANIFEST["ssr"]
|
|
15
|
+
: import.meta.env.MANIFEST["client"],
|
|
16
|
+
import.meta.env.MANIFEST["ssr"]
|
|
17
|
+
),
|
|
18
|
+
children: route.children ? route.children.map(createRoute) : undefined
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const routes = routeConfigs.map(createRoute);
|
|
22
|
+
return routes;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let routes;
|
|
26
|
+
export const FileRoutes = () => {
|
|
27
|
+
return isServer ? getRequestEvent().routes : (routes || (routes = createRoutes()));
|
|
28
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { onCleanup } from "solid-js";
|
|
2
|
+
import { getRequestEvent, isServer } from "solid-js/web";
|
|
3
|
+
|
|
4
|
+
export function HttpHeader(props: { name: string; value: string; append?: boolean }) {
|
|
5
|
+
if (isServer) {
|
|
6
|
+
const pageContext = getRequestEvent();
|
|
7
|
+
if (props.append) {
|
|
8
|
+
pageContext!.appendResponseHeader(props.name, props.value);
|
|
9
|
+
} else {
|
|
10
|
+
pageContext!.setResponseHeader(props.name, props.value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
onCleanup(() => {
|
|
14
|
+
const value = pageContext!.getResponseHeader(props.name);
|
|
15
|
+
|
|
16
|
+
// Todo: review this logic still the same in H3
|
|
17
|
+
// H3 supports arrays so its possible there is no need to split..
|
|
18
|
+
// typeof check here is to guard but ultimately may be unneeded
|
|
19
|
+
if (value && typeof value === "string") {
|
|
20
|
+
const values = value.split(", ");
|
|
21
|
+
const index = values.indexOf(props.value);
|
|
22
|
+
index !== -1 && values.splice(index, 1);
|
|
23
|
+
if (values.length) pageContext!.setResponseHeader(props.name, values.join(", "));
|
|
24
|
+
else pageContext!.removeResponseHeader(props.name);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return null;
|
|
30
|
+
}
|