@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.
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ export { StartServer } from "./StartServer";
2
+ export { createHandler } from "./handler";
3
+ export * from "./middleware";
4
+ export type { DocumentComponentProps } from "./types";
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ export * from "../handler";
2
+ export * from "../middleware";
3
+ export { StartServer } from "./StartServer";
4
+
@@ -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
+ }