@solidjs/start 0.4.0-alpha.0 → 0.4.0-alpha.2

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/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # Solid Start
2
+
3
+ This is the SolidStart framework and CLI.
4
+
5
+ The quickest way to get started:
6
+
7
+ ```bash
8
+ npm init solid@latest my-app
9
+ cd my-app
10
+ npm install
11
+ npm run dev
12
+ ```
@@ -1,5 +1,6 @@
1
1
  // @ts-ignore
2
2
  import App from "#start/app";
3
+ import { ErrorBoundary } from "../shared/ErrorBoundary";
3
4
  import "./mount";
4
5
 
5
6
  function Dummy(props) {
@@ -10,7 +11,9 @@ export function StartClient() {
10
11
  return (
11
12
  <Dummy>
12
13
  <Dummy>
13
- <App />
14
+ <ErrorBoundary>
15
+ <App />
16
+ </ErrorBoundary>
14
17
  </Dummy>
15
18
  </Dummy>
16
19
  );
package/config/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { serverFunctions } from "@vinxi/server-functions/plugin";
2
+ import { serverTransform } from "@vinxi/server-functions/server";
2
3
  import defu from "defu";
3
4
  import { join } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
@@ -42,13 +43,18 @@ export function defineConfig(baseConfig = {}) {
42
43
  appRoot: "./src",
43
44
  ssr: true,
44
45
  islands: false,
45
- serverPlugins: []
46
46
  });
47
+ let server = start.server;
48
+ if (!start.ssr) {
49
+ server = { ...server, prerender: { routes: ["/"] } };
50
+ }
51
+
47
52
  return createApp({
48
53
  server: {
49
54
  compressPublicAssets: {
50
55
  brotli: true
51
- }
56
+ },
57
+ ...server
52
58
  },
53
59
  routers: [
54
60
  {
@@ -63,14 +69,15 @@ export function defineConfig(baseConfig = {}) {
63
69
  mode: "handler",
64
70
  handler: `${start.appRoot}/entry-server.tsx`,
65
71
  middleware: start.middleware,
66
- ...(start.ssr
67
- ? { routes: solidStartServerFsRouter({ dir: `${start.appRoot}/routes`, extensions }) }
68
- : {}),
72
+ routes: solidStartServerFsRouter({ dir: `${start.appRoot}/routes`, extensions }),
69
73
  extensions,
70
74
  target: "server",
71
75
  plugins: () => [
72
76
  config("user", userConfig),
73
77
  ...plugins,
78
+ serverTransform({
79
+ runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.jsx", import.meta.url)))
80
+ }),
74
81
  start.islands ? serverComponents.server() : null,
75
82
  solid({ ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
76
83
  config("app-server", {
@@ -138,16 +145,10 @@ export function defineConfig(baseConfig = {}) {
138
145
  ],
139
146
  base: "/_build"
140
147
  },
141
- // {
142
- // name: "server",
143
- // mode: "handler",
144
- // base: "/_server",
145
- // handler: serverFunctions.handler,
146
- // target: "server",
147
- // ...(overrides ?? {}),
148
- // plugins: () => [serverserver(), ...(overrides?.plugins?.() ?? [])],
149
- // },
150
148
  serverFunctions.router({
149
+ handler: normalize(fileURLToPath(new URL("./server-handler.js", import.meta.url))),
150
+ runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.jsx", import.meta.url))),
151
+ // routes: solidStartServerFsRouter({ dir: `${start.appRoot}/routes`, extensions }),
151
152
  plugins: () => [
152
153
  config("user", userConfig),
153
154
  ...plugins,
@@ -0,0 +1,20 @@
1
+ import { getRequestEvent } from "solid-js/web";
2
+ import { provideRequestEvent } from "solid-js/web/storage";
3
+ import { cloneEvent } from "../server";
4
+
5
+ export function createServerReference(fn, id, name) {
6
+ return new Proxy(fn, {
7
+ get(target, prop, receiver) {
8
+ if (prop === "url") {
9
+ return `/_server?id=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}`;
10
+ }
11
+ },
12
+ apply(target, thisArg, args) {
13
+ const evt = cloneEvent(getRequestEvent());
14
+ evt.serverOnly = true;
15
+ return provideRequestEvent(evt, () => {
16
+ return fn.apply(thisArg, args);
17
+ });
18
+ }
19
+ });
20
+ }
@@ -0,0 +1,113 @@
1
+ /// <reference types="vinxi/types/server" />
2
+ import { crossSerializeStream, fromJSON } from "seroval";
3
+ import { provideRequestEvent } from "solid-js/web/storage";
4
+ import invariant from "vinxi/lib/invariant";
5
+ import {
6
+ eventHandler,
7
+ getHeader,
8
+ getRequestURL,
9
+ readBody,
10
+ readFormData,
11
+ setHeader
12
+ } from "vinxi/server";
13
+ import { getFetchEvent } from "../server/middleware";
14
+
15
+ function serializeToStream(id, value) {
16
+ return new ReadableStream({
17
+ start(controller) {
18
+ crossSerializeStream(value, {
19
+ scopeId: id,
20
+ onSerialize(data, initial) {
21
+ const result = initial ? `($R["${id}"]=[],${data})` : data;
22
+ controller.enqueue(new TextEncoder().encode(`${result};\n`));
23
+ },
24
+ onDone() {
25
+ // controller.enqueue(`delete $R["${id}"];\n`);
26
+ controller.close();
27
+ },
28
+ onError(error) {
29
+ // controller.enqueue(`delete $R["${id}"];\n`);
30
+ controller.error(error);
31
+ }
32
+ });
33
+ }
34
+ });
35
+ }
36
+
37
+ async function handleServerFunction(event) {
38
+ invariant(event.method === "POST", `Invalid method ${event.method}. Expected POST.`);
39
+
40
+ const serverReference = getHeader(event, "x-server-id");
41
+ const instance = getHeader(event, "x-server-instance");
42
+ const url = getRequestURL(event);
43
+ let filepath, name;
44
+ if (serverReference) {
45
+ invariant(typeof serverReference === "string", "Invalid server function");
46
+ [filepath, name] = serverReference.split("#");
47
+ } else {
48
+ filepath = url.searchParams.get("id");
49
+ name = url.searchParams.get("name");
50
+ if (!filepath || !name) throw new Error("Invalid request");
51
+ }
52
+
53
+ const action = (
54
+ await import.meta.env.MANIFEST[import.meta.env.ROUTER_NAME].chunks[filepath].import()
55
+ )[name];
56
+ let parsed = [];
57
+
58
+ // grab bound arguments from url when no JS
59
+ if (!instance) {
60
+ const args = url.searchParams.get("args");
61
+ if (args) JSON.parse(args).forEach(arg => parsed.push(arg));
62
+ }
63
+ const contentType = getHeader(event, "content-type");
64
+ if (
65
+ contentType.startsWith("multipart/form-data") ||
66
+ contentType.startsWith("application/x-www-form-urlencoded")
67
+ ) {
68
+ parsed.push(await readFormData(event));
69
+ } else {
70
+ parsed = fromJSON(await readBody(event));
71
+ }
72
+ try {
73
+ const result = await provideRequestEvent(getFetchEvent(event), () => action(...parsed));
74
+
75
+ // handle no JS success case
76
+ if (!instance) {
77
+ const isError = result instanceof Error;
78
+ const refererUrl = new URL(getHeader(event, "referer"));
79
+ return new Response(null, {
80
+ status: 302,
81
+ headers: {
82
+ Location: refererUrl.toString(),
83
+ ...(result ? { "Set-Cookie": `flash=${JSON.stringify({
84
+ url: url.pathname + encodeURIComponent(url.search),
85
+ result: isError ? result.message : result,
86
+ error: isError,
87
+ input: [...parsed.slice(0, -1), [...parsed[parsed.length-1].entries()]]
88
+ })}; Secure; HttpOnly;`} : {})
89
+ }
90
+ });
91
+ }
92
+
93
+ setHeader(event, "content-type", "text/javascript");
94
+ return serializeToStream(instance, result);
95
+ } catch (x) {
96
+ if (x instanceof Response && x.status === 302) {
97
+ return new Response(null, {
98
+ status: instance ? 204 : 302,
99
+ headers: {
100
+ Location: x.headers.get("Location")
101
+ }
102
+ });
103
+ }
104
+ return new Response(serializeToStream(instance, x), {
105
+ status: 500,
106
+ headers: {
107
+ "Content-Type": "text/javascript"
108
+ }
109
+ });
110
+ }
111
+ }
112
+
113
+ export default eventHandler(handleServerFunction);
@@ -1,25 +1,89 @@
1
+ import { deserialize, toJSONAsync } from "seroval";
1
2
  import { createIslandReference } from "../server/islands";
2
3
 
3
- async function fetchServerAction(base, id, args) {
4
- console.log("fetching serevr action");
5
- const response = await fetch(base, {
4
+ async function deserializeStream(id, response) {
5
+ if (!response.body) {
6
+ throw new Error("missing body");
7
+ }
8
+ const reader = response.body.getReader();
9
+
10
+ async function pop() {
11
+ const result = await reader.read();
12
+ if (!result.done) {
13
+ const serialized = new TextDecoder().decode(result.value);
14
+ const splits = serialized.split("\n");
15
+ for (const split of splits) {
16
+ if (split !== "") {
17
+ deserialize(split);
18
+ }
19
+ }
20
+ await pop();
21
+ }
22
+ }
23
+
24
+ const result = await reader.read();
25
+ if (result.done) {
26
+ throw new Error("Unexpected end of body");
27
+ }
28
+ const serialized = new TextDecoder().decode(result.value);
29
+ let pending = true;
30
+ let revived;
31
+ const splits = serialized.split("\n");
32
+ for (const split of splits) {
33
+ if (split !== "") {
34
+ const current = deserialize(split);
35
+ if (pending) {
36
+ revived = current;
37
+ pending = false;
38
+ }
39
+ }
40
+ }
41
+
42
+ pop().then(
43
+ () => {
44
+ delete self.$R[id];
45
+ },
46
+ () => {
47
+ // no-op
48
+ }
49
+ );
50
+
51
+ return revived;
52
+ }
53
+
54
+ let INSTANCE = 0;
55
+
56
+ function createRequest(base, id, instance, body, contentType) {
57
+ return fetch(base, {
6
58
  method: "POST",
7
59
  headers: {
8
- Accept: "application/json",
9
- "Content-Type": "application/json",
10
- "server-action": id
60
+ "x-server-id": id,
61
+ "x-server-instance": instance,
62
+ ...(contentType ? { "Content-Type": contentType } : {})
11
63
  },
12
- body: JSON.stringify(args)
64
+ body
13
65
  });
66
+ }
14
67
 
15
- const json = await response.json();
68
+ async function fetchServerFunction(base, id, args) {
69
+ const instance = `server-fn:${INSTANCE++}`;
70
+ const response = await (args.length === 1 && args[0] instanceof FormData
71
+ ? createRequest(base, id, instance, args[0])
72
+ : createRequest(base, id, instance, JSON.stringify(await toJSONAsync(args)), "application/json"));
16
73
 
17
- if (response.status === 200) {
18
- return json;
19
- } else {
20
- console.log(json);
21
- throw { message: json.error };
74
+ if (response.headers.get("Location")) throw response;
75
+ if (response.headers.get("X-Revalidate")) {
76
+ /* ts-ignore-next-line */
77
+ response.customBody = () => {
78
+ return deserializeStream(instance, response);
79
+ }
80
+ throw response;
81
+ }
82
+ const result = deserializeStream(instance, response);
83
+ if (response.ok) {
84
+ return result;
22
85
  }
86
+ throw result;
23
87
  }
24
88
 
25
89
  export function createServerReference(fn, id, name) {
@@ -30,7 +94,7 @@ export function createServerReference(fn, id, name) {
30
94
  }
31
95
  },
32
96
  apply(target, thisArg, args) {
33
- return fetchServerAction("/_server", `${id}#${name}`, args);
97
+ return fetchServerFunction("/_server", `${id}#${name}`, args);
34
98
  }
35
99
  });
36
100
  }
package/env.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ // This file is an augmentation to the built-in ImportMeta interface
2
+ // Thus cannot contain any top-level imports
3
+ // <https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation>
4
+
5
+ /* eslint-disable @typescript-eslint/consistent-type-imports */
6
+
7
+ interface ImportMetaEnv extends Record<`VITE_${string}`, any>, SolidStartMetaEnv {
8
+ BASE_URL: string;
9
+ MODE: string;
10
+ DEV: boolean;
11
+ PROD: boolean;
12
+ SSR: boolean;
13
+ }
14
+
15
+ interface SolidStartMetaEnv {
16
+ START_ISLANDS_ROUTER: string;
17
+ START_INDEX_HTML: string;
18
+ START_ENTRY_CLIENT: string;
19
+ START_SSR: string;
20
+ START_ISLANDS: string;
21
+ START_ENTRY_SERVER: string;
22
+ DEBUG: string;
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidjs/start",
3
- "version": "0.4.0-alpha.0",
3
+ "version": "0.4.0-alpha.2",
4
4
  "type": "module",
5
5
  "author": "Ryan Carniato",
6
6
  "publishConfig": {
@@ -38,15 +38,19 @@
38
38
  }
39
39
  },
40
40
  "devDependencies": {
41
- "solid-js": "^1.8.4",
42
- "vinxi": "0.0.47"
41
+ "solid-js": "^1.8.7",
42
+ "vinxi": "0.0.48"
43
43
  },
44
44
  "dependencies": {
45
- "@vinxi/plugin-directives": "0.0.39",
46
- "@vinxi/server-functions": "0.0.39",
47
- "@vinxi/server-components": "0.0.39",
45
+ "@vinxi/plugin-directives": "0.0.44",
46
+ "@vinxi/server-components": "0.0.44",
47
+ "@vinxi/server-functions": "0.0.45",
48
48
  "defu": "^6.1.2",
49
+ "seroval": "^0.14.1",
49
50
  "vite-plugin-inspect": "^0.7.33",
50
- "vite-plugin-solid": "^2.7.0"
51
+ "vite-plugin-solid": "^2.8.0"
52
+ },
53
+ "scripts": {
54
+ "typecheck": "tsc --noEmit --checkJs false --skipLibCheck"
51
55
  }
52
56
  }
@@ -1,14 +1,15 @@
1
1
  // @ts-ignore
2
2
  import App from "#start/app";
3
- import { Hydration, HydrationScript, NoHydration, ssr } from "solid-js/web";
3
+ import type { Component } from "solid-js";
4
+ import { Hydration, HydrationScript, NoHydration, getRequestEvent, ssr } from "solid-js/web";
5
+ import { ErrorBoundary } from "../shared/ErrorBoundary";
4
6
  import { renderAsset } from "./renderAsset";
7
+ import type { DocumentComponentProps } from "./types";
5
8
 
6
9
  const docType = ssr("<!DOCTYPE html>");
7
10
 
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;
11
+ export function StartServer(props: { document: Component<DocumentComponentProps> }) {
12
+ const context = getRequestEvent() as any;
12
13
  return (
13
14
  <NoHydration>
14
15
  {docType as unknown as any}
@@ -31,10 +32,14 @@ export function StartServer(props) {
31
32
  >
32
33
  {!import.meta.env.START_ISLANDS ? (
33
34
  <Hydration>
34
- <App />
35
+ <ErrorBoundary>
36
+ <App />
37
+ </ErrorBoundary>
35
38
  </Hydration>
36
39
  ) : (
37
- <App />
40
+ <ErrorBoundary>
41
+ <App />
42
+ </ErrorBoundary>
38
43
  )}
39
44
  </props.document>
40
45
  </NoHydration>
@@ -1,10 +1,20 @@
1
1
  import { renderToStream } from "solid-js/web";
2
+ /* @ts-ignore */
2
3
  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";
4
+ import {
5
+ EventHandlerObject,
6
+ EventHandlerRequest,
7
+ H3Event,
8
+ HTTPMethod,
9
+ eventHandler,
10
+ sendRedirect,
11
+ setHeader,
12
+ setResponseStatus
13
+ } from "vinxi/server";
14
+ import { matchAPIRoute } from "../shared/routes";
6
15
  import { getFetchEvent } from "./middleware";
7
- import { FetchEvent, FETCH_EVENT, PageEvent } from "./types";
16
+ import { createPageEvent } from "./page-event";
17
+ import { APIEvent, FetchEvent, PageEvent } from "./types";
8
18
 
9
19
  export function createHandler(
10
20
  fn: (context: PageEvent) => unknown,
@@ -12,6 +22,8 @@ export function createHandler(
12
22
  nonce?: string;
13
23
  renderId?: string;
14
24
  timeoutMs?: number;
25
+ onCompleteAll?: (options: { write: (v: any) => void }) => void;
26
+ onCompleteShell?: (options: { write: (v: any) => void }) => void;
15
27
  createPageEvent?: (event: FetchEvent) => Promise<PageEvent>;
16
28
  onRequest?: EventHandlerObject["onRequest"];
17
29
  onBeforeResponse?: EventHandlerObject["onBeforeResponse"];
@@ -25,22 +37,34 @@ export function createHandler(
25
37
 
26
38
  return provideRequestEvent(event, async () => {
27
39
  // api
28
- const match = apiRoutes.find(
29
- route =>
30
- route[`$${event.request.method}`] && new URL(event.request.url).pathname === route.path
31
- );
40
+ const match = matchAPIRoute(new URL(event.request.url).pathname, event.request.method as HTTPMethod)
32
41
  if (match) {
33
- const mod = await match[`$${event.request.method}`].import();
42
+ const mod = await match.handler.import();
34
43
  const fn = mod[event.request.method];
35
- const result = await fn(event);
36
- return result;
44
+ (event as APIEvent).params = match.params;
45
+ return await fn(event);
37
46
  }
38
47
 
39
48
  // render stream
40
49
  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);
50
+ let cloned = { ...options };
51
+ if (cloned.onCompleteAll) {
52
+ const og = cloned.onCompleteAll;
53
+ cloned.onCompleteAll = options => {
54
+ handleStreamCompleteRedirect(context)(options);
55
+ og(options);
56
+ };
57
+ } else cloned.onCompleteAll = handleStreamCompleteRedirect(context);
58
+ if (cloned.onCompleteShell) {
59
+ const og = cloned.onCompleteShell;
60
+ cloned.onCompleteShell = options => {
61
+ handleShellCompleteRedirect(context, e)();
62
+ og(options);
63
+ };
64
+ } else cloned.onCompleteShell = handleShellCompleteRedirect(context, e);
65
+ const stream = renderToStream(() => fn(context), cloned);
66
+ if (context.response && context.response.headers.get("Location")) {
67
+ return sendRedirect(event, context.response.headers.get("Location"));
44
68
  }
45
69
  return { pipeTo: stream.pipeTo };
46
70
  });
@@ -48,25 +72,18 @@ export function createHandler(
48
72
  });
49
73
  }
50
74
 
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
- });
75
+ function handleShellCompleteRedirect(context: PageEvent, e: H3Event<EventHandlerRequest>) {
76
+ return () => {
77
+ if (context.response && context.response.headers.get("Location")) {
78
+ setResponseStatus(e, 302);
79
+ setHeader(e, "Location", context.response.headers.get("Location"));
80
+ }
81
+ }
82
+ }
70
83
 
71
- return pageEvent;
84
+ function handleStreamCompleteRedirect(context: PageEvent) {
85
+ return ({ write }: { write: (html: string) => void }) => {
86
+ const to = context.response && context.response.headers.get("Location");
87
+ to && write(`<script>window.location="${to}"</script>`);
88
+ };
72
89
  }
package/server/index.tsx CHANGED
@@ -1,4 +1,14 @@
1
+ export * from "vinxi/server";
1
2
  export { StartServer } from "./StartServer";
2
- export { createHandler } from "./handler";
3
3
  export * from "./middleware";
4
- export type { DocumentComponentProps } from "./types";
4
+ export * from "./types";
5
+ import { createHandler as createBaseHandler } from "./handler";
6
+ import { createPageEvent } from "./page-event";
7
+ import type { PageEvent } from "./types";
8
+
9
+ export function createHandler(
10
+ fn: (context: PageEvent) => unknown,
11
+ options: Parameters<typeof createBaseHandler>[1] = {}
12
+ ) {
13
+ return createBaseHandler(fn, { ...options, createPageEvent });
14
+ }
@@ -1,45 +1,37 @@
1
1
  import {
2
- appendResponseHeader,
3
2
  defineMiddleware,
4
3
  EventHandlerRequest,
5
4
  getRequestIP,
6
- getResponseHeader,
7
- getResponseStatus,
8
5
  H3Event,
9
- removeResponseHeader,
10
- sendRedirect,
11
6
  sendWebResponse,
12
- setResponseHeader,
13
- setResponseStatus,
14
7
  toWebRequest
15
8
  } from "vinxi/server";
16
9
  import { FetchEvent } from "./types";
17
10
 
18
- export * from "vinxi/server";
19
-
20
11
  const h3EventSymbol = Symbol("h3Event");
21
12
  const fetchEventSymbol = Symbol("fetchEvent");
13
+ const eventTraps = {
14
+ get(target, prop) {
15
+ if (prop === fetchEventSymbol) return target;
16
+ return target[prop] ?? target[h3EventSymbol][prop];
17
+ }
18
+ };
22
19
 
23
20
  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
- };
21
+ return new Proxy(
22
+ {
23
+ request: toWebRequest(event),
24
+ clientAddress: getRequestIP(event),
25
+ locals: {},
26
+ // @ts-ignore
27
+ [h3EventSymbol]: event
28
+ },
29
+ eventTraps
30
+ ) as unknown as FetchEvent;
38
31
  }
39
32
 
40
- export function getH3Event(fetchEvent: FetchEvent): H3Event<EventHandlerRequest> {
41
- // @ts-ignore
42
- return fetchEvent[h3EventSymbol];
33
+ export function cloneEvent<T extends FetchEvent>(fetchEvent: T): T {
34
+ return new Proxy({ ...fetchEvent[fetchEventSymbol] }, eventTraps);
43
35
  }
44
36
 
45
37
  export function getFetchEvent(h3Event: H3Event): FetchEvent {
@@ -52,28 +44,11 @@ export function getFetchEvent(h3Event: H3Event): FetchEvent {
52
44
  return h3Event[fetchEventSymbol];
53
45
  }
54
46
 
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
47
  /** Function responsible for receiving an observable [operation]{@link Operation} and returning a [result]{@link OperationResult}. */
62
48
 
63
49
  export type MiddlewareFn = (event: FetchEvent) => Promise<unknown> | unknown;
64
50
  /** This composes an array of Exchanges into a single ExchangeIO function */
65
51
 
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
52
  type RequestMiddleware = (event: FetchEvent) => Response | Promise<Response> | void | Promise<void>;
78
53
 
79
54
  type ResponseMiddleware = (
@@ -0,0 +1,42 @@
1
+ import { getCookie, setCookie, setResponseHeader } from "vinxi/server";
2
+ import { createRoutes } from "../shared/FileRoutes";
3
+ import { FetchEvent, PageEvent } from "./types";
4
+
5
+ function initFromFlash(ctx: FetchEvent) {
6
+ const flash = getCookie(ctx, "flash");
7
+ if (!flash) return;
8
+ let param = JSON.parse(flash);
9
+ if (!param || !param.result) return [];
10
+ const input = [...param.input.slice(0, -1), new Map(param.input[param.input.length - 1])];
11
+ setCookie(ctx, "flash", "", { maxAge: 0 });
12
+ return {
13
+ url: param.url,
14
+ result: param.error ? new Error(param.result) : param.result,
15
+ input
16
+ };
17
+ }
18
+
19
+ export async function createPageEvent(ctx: FetchEvent) {
20
+ const clientManifest = import.meta.env.MANIFEST["client"];
21
+ const serverManifest = import.meta.env.MANIFEST["ssr"];
22
+ setResponseHeader(ctx, "Content-Type", "text/html");
23
+ // const prevPath = ctx.request.headers.get("x-solid-referrer");
24
+ // const mutation = ctx.request.headers.get("x-solid-mutation") === "true";
25
+ const pageEvent: PageEvent = Object.assign(ctx, {
26
+ manifest: await clientManifest.json(),
27
+ assets: [
28
+ ...(await clientManifest.inputs[clientManifest.handler].assets()),
29
+ ...(import.meta.env.START_ISLANDS
30
+ ? await serverManifest.inputs[serverManifest.handler].assets()
31
+ : [])
32
+ ],
33
+ initialSubmission: initFromFlash(ctx),
34
+ routes: createRoutes(),
35
+ // prevUrl: prevPath || "",
36
+ // mutation: mutation,
37
+ // $type: FETCH_EVENT,
38
+ $islands: new Set<string>()
39
+ });
40
+
41
+ return pageEvent;
42
+ }
@@ -1,11 +1,11 @@
1
1
  // @ts-ignore
2
- import { NoHydration, ssr } from "solid-js/web";
2
+ import { NoHydration, getRequestEvent, ssr } from "solid-js/web";
3
3
  import { renderAsset } from "../renderAsset";
4
4
 
5
5
  const docType = ssr("<!DOCTYPE html>");
6
6
 
7
7
  export function StartServer(props) {
8
- const context = props.context;
8
+ const context = getRequestEvent() as any;
9
9
  return (
10
10
  <NoHydration>
11
11
  {docType as unknown as any}
@@ -14,6 +14,7 @@ export function StartServer(props) {
14
14
  scripts={
15
15
  <>
16
16
  <script innerHTML={`window.manifest = ${JSON.stringify(context.manifest)}`} />
17
+ <script>$R = [];</script>
17
18
  <script
18
19
  type="module"
19
20
  src={
@@ -1,5 +1,5 @@
1
1
  import { createHandler as createBaseHandler } from "../handler";
2
- import { FetchEvent, FETCH_EVENT, PageEvent } from "../types";
2
+ import { FetchEvent, PageEvent } from "../types";
3
3
 
4
4
  export function createHandler(
5
5
  fn: (context: PageEvent) => unknown,
@@ -14,9 +14,9 @@ export async function createPageEvent(ctx: FetchEvent) {
14
14
  manifest: await clientManifest.json(),
15
15
  assets: [...(await clientManifest.inputs[clientManifest.handler].assets())],
16
16
  routes: [],
17
- prevUrl: "",
18
- mutation: false,
19
- $type: FETCH_EVENT,
17
+ // prevUrl: "",
18
+ // mutation: false,
19
+ // $type: FETCH_EVENT,
20
20
  $islands: new Set<string>()
21
21
  });
22
22
 
package/server/types.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { JSX } from "solid-js";
2
+ import { EventHandlerRequest, H3Event } from "vinxi/server";
2
3
 
3
- export const FETCH_EVENT = "$FETCH";
4
+ // export const FETCH_EVENT = "$FETCH";
4
5
 
5
6
  export type DocumentComponentProps = {
6
- assets: any;
7
- scripts: any;
7
+ assets: JSX.Element;
8
+ scripts: JSX.Element;
8
9
  children: JSX.Element;
9
10
  }
10
11
 
@@ -15,44 +16,32 @@ export type ContextMatches = {
15
16
  params: unknown;
16
17
  };
17
18
 
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 {
19
+ export interface FetchEvent extends H3Event<EventHandlerRequest> {
38
20
  request: Request;
39
21
  clientAddress: string;
40
22
  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
23
  }
49
24
  export interface PageEvent extends FetchEvent {
50
25
  manifest: any;
51
26
  assets: any;
52
27
  routes: any[];
53
- prevUrl: string | null;
54
- $type: typeof FETCH_EVENT;
28
+ // prevUrl: string | null;
29
+ // $type: typeof FETCH_EVENT;
55
30
  $islands: Set<string>;
56
- mutation: boolean;
31
+ // mutation: boolean;
57
32
  response?: Response;
58
33
  }
34
+
35
+ export interface APIEvent extends FetchEvent {
36
+ params: { [key: string]: string };
37
+ }
38
+
39
+ export interface APIHandler {
40
+ (event: APIEvent): Promise<any>;
41
+ }
42
+
43
+ declare module "solid-js/web" {
44
+ interface RequestEvent extends FetchEvent {
45
+ serverOnly?: boolean;
46
+ }
47
+ }
@@ -1,6 +1,8 @@
1
+ /* @refresh skip */
1
2
  import { getRequestEvent, isServer } from "solid-js/web";
2
3
  import lazyRoute from "./lazyRoute";
3
4
 
5
+ import { PageEvent } from "../server";
4
6
  import { pageRoutes as routeConfigs } from "./routes";
5
7
 
6
8
  export function createRoutes() {
@@ -24,5 +26,5 @@ export function createRoutes() {
24
26
 
25
27
  let routes;
26
28
  export const FileRoutes = () => {
27
- return isServer ? getRequestEvent().routes : (routes || (routes = createRoutes()));
29
+ return isServer ? (getRequestEvent() as PageEvent).routes : (routes || (routes = createRoutes()));
28
30
  };
@@ -1,17 +1,18 @@
1
1
  import { onCleanup } from "solid-js";
2
2
  import { getRequestEvent, isServer } from "solid-js/web";
3
+ import { appendResponseHeader, getResponseHeader, removeResponseHeader, setResponseHeader } from "vinxi/server";
3
4
 
4
5
  export function HttpHeader(props: { name: string; value: string; append?: boolean }) {
5
6
  if (isServer) {
6
- const pageContext = getRequestEvent();
7
+ const event = getRequestEvent();
7
8
  if (props.append) {
8
- pageContext!.appendResponseHeader(props.name, props.value);
9
+ appendResponseHeader(event, props.name, props.value);
9
10
  } else {
10
- pageContext!.setResponseHeader(props.name, props.value);
11
+ setResponseHeader(event, props.name, props.value);
11
12
  }
12
13
 
13
14
  onCleanup(() => {
14
- const value = pageContext!.getResponseHeader(props.name);
15
+ const value = getResponseHeader(event, props.name);
15
16
 
16
17
  // Todo: review this logic still the same in H3
17
18
  // H3 supports arrays so its possible there is no need to split..
@@ -20,8 +21,8 @@ export function HttpHeader(props: { name: string; value: string; append?: boolea
20
21
  const values = value.split(", ");
21
22
  const index = values.indexOf(props.value);
22
23
  index !== -1 && values.splice(index, 1);
23
- if (values.length) pageContext!.setResponseHeader(props.name, values.join(", "));
24
- else pageContext!.removeResponseHeader(props.name);
24
+ if (values.length) setResponseHeader(event, props.name, values.join(", "));
25
+ else removeResponseHeader(event, props.name);
25
26
  }
26
27
  });
27
28
  }
@@ -1,12 +1,13 @@
1
1
  import { onCleanup } from "solid-js";
2
2
  import { getRequestEvent, isServer } from "solid-js/web";
3
+ import { setResponseStatus } from "vinxi/server";
3
4
 
4
5
  export function HttpStatusCode(props: { code: number, text?: string }) {
5
6
  if (isServer) {
6
- const context = getRequestEvent();
7
- context!.setResponseStatus(props.code, props.text);
7
+ const context = getRequestEvent()!;
8
+ setResponseStatus(context, props.code, props.text);
8
9
  onCleanup(() => {
9
- context!.setResponseStatus(200);
10
+ setResponseStatus(context, 200);
10
11
  });
11
12
  }
12
13
  return null;
package/shared/index.tsx CHANGED
@@ -1,5 +1,4 @@
1
1
  export { default as clientOnly } from "./clientOnly";
2
- export { ErrorBoundary as DefaultErrorBoundary } from "./ErrorBoundary";
3
2
  export { FileRoutes } from "./FileRoutes";
4
3
  export { HttpHeader } from "./HttpHeader";
5
4
  export { HttpStatusCode } from "./HttpStatusCode";
package/shared/routes.ts CHANGED
@@ -1,4 +1,22 @@
1
- import fileRoutes, { Route } from "vinxi/routes";
1
+ import fileRoutes from "vinxi/routes";
2
+ import { HTTPMethod } from "vinxi/server";
3
+
4
+ interface Route {
5
+ path: string;
6
+ type: "api" | "page";
7
+ children?: Route[];
8
+ }
9
+
10
+ type MatchRoute = Route & {
11
+ score: number;
12
+ params: {
13
+ type: "*" | ":";
14
+ name: string;
15
+ index: number;
16
+ }[];
17
+ matchSegments: (string | null)[];
18
+ wildcard: boolean;
19
+ };
2
20
 
3
21
  declare module "vinxi/routes" {
4
22
  export interface Register {
@@ -10,7 +28,52 @@ declare module "vinxi/routes" {
10
28
  }
11
29
  }
12
30
 
13
- const defineRoutes = (fileRoutes: Route[]) => {
31
+ export const pageRoutes = defineRoutes((fileRoutes as unknown as Route[]).filter(o => o.type === "page"));
32
+ export const apiRoutes = defineAPIRoutes((fileRoutes as unknown as Route[]).filter(o => o.type === "api"));
33
+
34
+ export function matchAPIRoute(path: string, method: HTTPMethod) {
35
+ const segments = path.split("/").filter(Boolean);
36
+
37
+ routeLoop: for (const route of apiRoutes) {
38
+ const matchSegments = route.matchSegments;
39
+
40
+ if (
41
+ segments.length < matchSegments.length ||
42
+ (!route.wildcard && segments.length > matchSegments.length)
43
+ ) {
44
+ continue;
45
+ }
46
+
47
+ for (let index = 0; index < matchSegments.length; index++) {
48
+ const match = matchSegments[index];
49
+ if (!match) {
50
+ continue;
51
+ }
52
+
53
+ if (segments[index] !== match) {
54
+ continue routeLoop;
55
+ }
56
+ }
57
+
58
+ const handler = route[`$${method}`];
59
+ if (handler === "skip" || handler === undefined) {
60
+ return;
61
+ }
62
+
63
+ const params: Record<string, string> = {};
64
+ for (const { type, name, index } of route.params) {
65
+ if (type === ":") {
66
+ params[name] = segments[index];
67
+ } else {
68
+ params[name] = segments.slice(index).join("/");
69
+ }
70
+ }
71
+
72
+ return { handler, params };
73
+ }
74
+ }
75
+
76
+ function defineRoutes(fileRoutes: Route[]) {
14
77
  function processRoute(routes: Route[], route: Route, id: string, full: string) {
15
78
  const parentRoute = Object.values(routes).find(o => {
16
79
  // if (o.id.endsWith("/index")) {
@@ -40,5 +103,78 @@ const defineRoutes = (fileRoutes: Route[]) => {
40
103
  }, []);
41
104
  };
42
105
 
43
- export const pageRoutes = defineRoutes(fileRoutes.filter(o => o.type === "page"));
44
- export const apiRoutes = fileRoutes.filter(o => o.type === "api");
106
+ function defineAPIRoutes(routes: Route[]) {
107
+ return routes
108
+ .flatMap(route => {
109
+ const paths = expandOptionals(route.path);
110
+ return paths.map(path => ({ ...route, path }));
111
+ })
112
+ .map(routeToMatchRoute)
113
+ .sort((a, b) => b.score - a.score);
114
+ }
115
+
116
+ // This is copied from https://github.com/solidjs/solid-router/blob/main/src/utils.ts
117
+ function expandOptionals(pattern: string): string[] {
118
+ let match = /(\/?\:[^\/]+)\?/.exec(pattern);
119
+ if (!match) return [pattern];
120
+
121
+ let prefix = pattern.slice(0, match.index);
122
+ let suffix = pattern.slice(match.index + match[0].length);
123
+ const prefixes: string[] = [prefix, (prefix += match[1])];
124
+
125
+ // This section handles adjacent optional params. We don't actually want all permuations since
126
+ // that will lead to equivalent apiRwhich have the same number of params. For example
127
+ // `/:a?/:b?/:c`? only has the unique expansion: `/`, `/:a`, `/:a/:b`, `/:a/:b/:c` and we can
128
+ // discard `/:b`, `/:c`, `/:b/:c` by building them up in order and not recursing. This also helps
129
+ // ensure predictability where earlier params have precidence.
130
+ while ((match = /^(\/\:[^\/]+)\?/.exec(suffix))) {
131
+ prefixes.push((prefix += match[1]));
132
+ suffix = suffix.slice(match[0].length);
133
+ }
134
+
135
+ return expandOptionals(suffix).reduce<string[]>(
136
+ (results, expansion) => [...results, ...prefixes.map(p => p + expansion)],
137
+ []
138
+ );
139
+ }
140
+
141
+ function routeToMatchRoute(route: Route): MatchRoute {
142
+ const segments = route.path.split("/").filter(Boolean);
143
+
144
+ const params: { type: "*" | ":"; name: string; index: number }[] = [];
145
+ const matchSegments: (string | null)[] = [];
146
+ let score = 0;
147
+ let wildcard = false;
148
+
149
+ for (const [index, segment] of segments.entries()) {
150
+ if (segment[0] === ":") {
151
+ const name = segment.slice(1);
152
+ score += 3;
153
+ params.push({
154
+ type: ":",
155
+ name,
156
+ index
157
+ });
158
+ matchSegments.push(null);
159
+ } else if (segment[0] === "*") {
160
+ score -= 1;
161
+ params.push({
162
+ type: "*",
163
+ name: segment.slice(1),
164
+ index
165
+ });
166
+ wildcard = true;
167
+ } else {
168
+ score += 4;
169
+ matchSegments.push(segment);
170
+ }
171
+ }
172
+
173
+ return {
174
+ ...route,
175
+ score,
176
+ params,
177
+ matchSegments,
178
+ wildcard
179
+ };
180
+ }