@solidjs/start 0.4.11 → 0.5.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,64 @@
1
+ /// <reference types="vinxi/types/server" />
2
+ import { createRouter } from "radix3";
3
+ import { sharedConfig } from "solid-js";
4
+ import fileRoutes from "vinxi/routes";
5
+
6
+ // @ts-ignore
7
+ import { provideRequestEvent } from "solid-js/web/storage";
8
+ import { eventHandler } from "vinxi/server";
9
+ import { getFetchEvent } from "../server/fetchEvent";
10
+ import { APIEvent } from "../server/types";
11
+
12
+ export default eventHandler(h3Event => {
13
+ const event = getFetchEvent(h3Event);
14
+ const match = matchAPIRoute(new URL(event.request.url).pathname, event.request.method);
15
+ if (match) {
16
+ return provideRequestEvent(event, async () => {
17
+ const mod = await match.handler.import();
18
+ const fn = mod[event.request.method];
19
+ (event as APIEvent).params = match.params;
20
+ // @ts-ignore
21
+ sharedConfig.context = { event };
22
+ const res = await fn(event);
23
+ if (res === undefined && event.request.method !== "GET") {
24
+ throw new Error(
25
+ `API handler for ${event.request.method} "${event.request.url}" did not return a response.`
26
+ );
27
+ }
28
+ return res;
29
+ });
30
+ }
31
+ });
32
+
33
+ interface Route {
34
+ path: string;
35
+ id: string;
36
+ type: "api" | "page";
37
+ children?: Route[];
38
+ }
39
+
40
+ const router = createRouter({
41
+ routes: (fileRoutes as unknown as Route[]).reduce((memo, route) => {
42
+ if (route.type !== "api") return memo;
43
+ const path = route.path.replace(/\(.*\)\/?/g, "");
44
+ if (memo[path]) {
45
+ throw new Error(
46
+ `Duplicate API routes for "${path}" found at "${memo[path].route.path}" and "${route.path}"`
47
+ );
48
+ }
49
+ memo[path] = { route };
50
+ return memo;
51
+ }, {} as Record<string, { route: Route }>)
52
+ });
53
+
54
+ function matchAPIRoute(path: string, method: string) {
55
+ const match = router.lookup(path);
56
+ if (match) {
57
+ const handler = match.route[`$${method}`];
58
+ if (handler === "skip" || handler === undefined) return;
59
+ return {
60
+ handler,
61
+ params: match.params
62
+ };
63
+ }
64
+ }
@@ -57,19 +57,7 @@ export class SolidStartClientFileRouter extends BaseFileSystemRouter {
57
57
  }
58
58
  }
59
59
  }
60
- const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"];
61
- function createHTTPHandlers(src, exports) {
62
- const handlers = {};
63
- for (const exp of exports) {
64
- if (HTTP_METHODS.includes(exp.n)) {
65
- handlers[`$${exp.n}`] = {
66
- src: src,
67
- pick: [exp.n]
68
- };
69
- }
70
- }
71
- return handlers;
72
- }
60
+
73
61
  export class SolidStartServerFileRouter extends BaseFileSystemRouter {
74
62
  toPath(src) {
75
63
  const routePath = cleanPath(src, this.config)
@@ -123,7 +111,44 @@ export class SolidStartServerFileRouter extends BaseFileSystemRouter {
123
111
  path,
124
112
  filePath: src
125
113
  };
126
- } else if (exports.find(exp => HTTP_METHODS.includes(exp.n))) {
114
+ }
115
+ }
116
+ }
117
+
118
+ const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"];
119
+ function createHTTPHandlers(src, exports) {
120
+ const handlers = {};
121
+ for (const exp of exports) {
122
+ if (HTTP_METHODS.includes(exp.n)) {
123
+ handlers[`$${exp.n}`] = {
124
+ src: src,
125
+ pick: [exp.n]
126
+ };
127
+ }
128
+ }
129
+ return handlers;
130
+ }
131
+
132
+ export class SolidStartAPIFileRouter extends BaseFileSystemRouter {
133
+ toPath(src) {
134
+ const routePath = cleanPath(src, this.config)
135
+ // remove the initial slash
136
+ .slice(1)
137
+ .replace(/index$/, "")
138
+ .replace(/\[([^\/]+)\]/g, (_, m) => {
139
+ if (m.length > 2 && m.startsWith("...")) {
140
+ return `**${m.length > 3 ? ":" + m.slice(3) : ""}`;
141
+ }
142
+ return `:${m}`;
143
+ });
144
+
145
+ return routePath?.length > 0 ? `/${routePath}` : "/";
146
+ }
147
+
148
+ toRoute(src) {
149
+ let path = this.toPath(src);
150
+ const [_, exports] = analyzeModule(src);
151
+ if (exports.find(exp => HTTP_METHODS.includes(exp.n))) {
127
152
  return {
128
153
  type: "api",
129
154
  ...createHTTPHandlers(src, exports),
package/config/index.d.ts CHANGED
@@ -9,8 +9,9 @@ type SolidStartInlineConfig = Omit<InlineConfig, "router"> & {
9
9
  * true: streaming mode
10
10
  * false: csr only
11
11
  * async: ssr is in async mode
12
+ * sync: ssr is in sync mode
12
13
  */
13
- ssr?: boolean | "async",
14
+ ssr?: boolean | "async" | "sync",
14
15
  solid?: Options,
15
16
  extensions?: string[],
16
17
  server?: AppOptions['server'],
package/config/index.js CHANGED
@@ -8,7 +8,7 @@ import { createApp, resolve } from "vinxi";
8
8
  import { normalize } from "vinxi/lib/path";
9
9
  import { config } from "vinxi/plugins/config";
10
10
  import solid from "vite-plugin-solid";
11
- import { SolidStartClientFileRouter, SolidStartServerFileRouter } from "./fs-router.js";
11
+ import { SolidStartAPIFileRouter, SolidStartClientFileRouter, SolidStartServerFileRouter } from "./fs-router.js";
12
12
  import { serverComponents } from "./server-components.js";
13
13
 
14
14
  const DEFAULT_EXTENSIONS = ["js", "jsx", "ts", "tsx"];
@@ -18,7 +18,7 @@ function solidStartClientFsRouter(config) {
18
18
  new SolidStartClientFileRouter(
19
19
  {
20
20
  dir: resolve.absolute(config.dir, router.root),
21
- extensions: config.extensions ?? ["js", "jsx", "ts", "tsx"]
21
+ extensions: config.extensions ?? DEFAULT_EXTENSIONS
22
22
  },
23
23
  router,
24
24
  app
@@ -30,7 +30,19 @@ function solidStartServerFsRouter(config) {
30
30
  new SolidStartServerFileRouter(
31
31
  {
32
32
  dir: resolve.absolute(config.dir, router.root),
33
- extensions: config.extensions ?? ["js", "jsx", "ts", "tsx"]
33
+ extensions: config.extensions ?? DEFAULT_EXTENSIONS
34
+ },
35
+ router,
36
+ app
37
+ );
38
+ }
39
+
40
+ function solidStartAPIFileRouter(config) {
41
+ return (router, app) =>
42
+ new SolidStartAPIFileRouter(
43
+ {
44
+ dir: resolve.absolute(config.dir, router.root),
45
+ extensions: config.extensions ?? DEFAULT_EXTENSIONS
34
46
  },
35
47
  router,
36
48
  app
@@ -44,7 +56,12 @@ export function defineConfig(baseConfig = {}) {
44
56
  appRoot: "./src",
45
57
  ssr: true,
46
58
  islands: false,
47
- solid: {}
59
+ solid: {},
60
+ server: {
61
+ experimental: {
62
+ asyncContext: true
63
+ }
64
+ }
48
65
  });
49
66
  let server = start.server;
50
67
  if (!start.ssr) {
@@ -65,16 +82,24 @@ export function defineConfig(baseConfig = {}) {
65
82
  routers: [
66
83
  {
67
84
  name: "public",
68
- mode: "static",
85
+ type: "static",
69
86
  dir: "./public",
70
87
  base: "/"
71
88
  },
72
-
89
+ {
90
+ name: "api",
91
+ type: "http",
92
+ target: "server",
93
+ base: "/",
94
+ handler: normalize(fileURLToPath(new URL("./api-handler.ts", import.meta.url))),
95
+ routes: solidStartAPIFileRouter({ dir: `${start.appRoot}/routes` }),
96
+ middleware: start.middleware
97
+ },
73
98
  {
74
99
  name: "ssr",
75
- mode: "handler",
100
+ type: "http",
76
101
  link: {
77
- client: "client"
102
+ client: start.islands ? undefined : "client"
78
103
  },
79
104
  handler: `${start.appRoot}/entry-server${entryExtension}`,
80
105
  middleware: start.middleware,
@@ -96,7 +121,7 @@ export function defineConfig(baseConfig = {}) {
96
121
  ...(typeof plugins === "function" ? [...(await plugins())] : plugins),
97
122
 
98
123
  serverTransform({
99
- runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.jsx", import.meta.url)))
124
+ runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.ts", import.meta.url)))
100
125
  }),
101
126
  start.islands ? serverComponents.server() : null,
102
127
  solid({ ...start.solid, ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
@@ -125,7 +150,7 @@ export function defineConfig(baseConfig = {}) {
125
150
  },
126
151
  {
127
152
  name: "client",
128
- mode: "build",
153
+ type: "client",
129
154
  handler: `${start.appRoot}/entry-client${entryExtension}`,
130
155
  ...(start.islands
131
156
  ? {}
@@ -148,7 +173,7 @@ export function defineConfig(baseConfig = {}) {
148
173
  }),
149
174
  ...(typeof plugins === "function" ? [...(await plugins())] : plugins),
150
175
  serverFunctions.client({
151
- runtime: normalize(fileURLToPath(new URL("./server-runtime.jsx", import.meta.url)))
176
+ runtime: normalize(fileURLToPath(new URL("./server-runtime.ts", import.meta.url)))
152
177
  }),
153
178
  start.islands ? serverComponents.client() : null,
154
179
  solid({ ...start.solid, ssr: start.ssr, extensions: extensions.map(ext => `.${ext}`) }),
@@ -182,8 +207,8 @@ export function defineConfig(baseConfig = {}) {
182
207
  base: "/_build"
183
208
  },
184
209
  serverFunctions.router({
185
- handler: normalize(fileURLToPath(new URL("./server-handler.js", import.meta.url))),
186
- runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.jsx", import.meta.url))),
210
+ handler: normalize(fileURLToPath(new URL("./server-handler.ts", import.meta.url))),
211
+ runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.ts", import.meta.url))),
187
212
  // routes: solidStartServerFsRouter({ dir: `${start.appRoot}/routes`, extensions }),
188
213
  plugins: async () => [
189
214
  config("user", {
@@ -1,6 +1,7 @@
1
1
  import { getRequestEvent } from "solid-js/web";
2
+ //@ts-ignore
2
3
  import { provideRequestEvent } from "solid-js/web/storage";
3
- import { cloneEvent } from "../server/middleware";
4
+ import { cloneEvent } from "../server/fetchEvent";
4
5
 
5
6
  export function createServerReference(fn, id, name) {
6
7
  if (typeof fn !== "function") throw new Error("Export from a 'use server' module must be a function");
@@ -17,7 +17,7 @@ import { sharedConfig } from "solid-js";
17
17
  import { provideRequestEvent } from "solid-js/web/storage";
18
18
  import invariant from "vinxi/lib/invariant";
19
19
  import { eventHandler, setHeader } from "vinxi/server";
20
- import { getFetchEvent } from "../server/middleware";
20
+ import { getFetchEvent, mergeResponseHeaders } from "../server/fetchEvent";
21
21
 
22
22
  function createChunk(data) {
23
23
  const bytes = data.length;
@@ -94,13 +94,13 @@ async function handleServerFunction(h3Event) {
94
94
  ) {
95
95
  // workaround for https://github.com/unjs/nitro/issues/1721
96
96
  // (issue only in edge runtimes)
97
- parsed.push(await new Request(request, { ...request, body: event.node.req.body }).formData());
97
+ parsed.push(await new Request(request, { ...request, body: h3Event.node.req.body }).formData());
98
98
  // what should work when #1721 is fixed
99
99
  // parsed.push(await request.formData);
100
100
  } else {
101
101
  // workaround for https://github.com/unjs/nitro/issues/1721
102
102
  // (issue only in edge runtimes)
103
- const tmpReq = new Request(request, { ...request, body: event.node.req.body })
103
+ const tmpReq = new Request(request, { ...request, body: h3Event.node.req.body })
104
104
  // what should work when #1721 is fixed
105
105
  // just use request.json() here
106
106
  parsed = fromJSON(await tmpReq.json(), {
@@ -137,13 +137,9 @@ async function handleServerFunction(h3Event) {
137
137
  });
138
138
  }
139
139
  // forward headers
140
- if (result.headers) {
141
- for (const [key, value] of result.headers.entries()) {
142
- setHeader(h3Event, key, value);
143
- }
144
- }
145
- if (result.customBody) {
146
- result = await result.customBody();
140
+ if (result.headers) mergeResponseHeaders(h3Event, result.headers);
141
+ if ((result as any).customBody) {
142
+ result = await (result as any).customBody();
147
143
  } else if (result.body == undefined) result = undefined;
148
144
  }
149
145
 
@@ -182,13 +178,9 @@ async function handleServerFunction(h3Event) {
182
178
  });
183
179
  }
184
180
  // forward headers
185
- if (x.headers) {
186
- for (const [key, value] of x.headers.entries()) {
187
- setHeader(h3Event, key, value);
188
- }
189
- }
190
- if (x.customBody) {
191
- x = await x.customBody();
181
+ if (x.headers) mergeResponseHeaders(h3Event, x.headers);
182
+ if ((x as any).customBody) {
183
+ x = await (x as any).customBody();
192
184
  } else if (x.body == undefined) x = undefined;
193
185
  }
194
186
  return x;
@@ -14,6 +14,9 @@ import {
14
14
  import { createIslandReference } from "../server/islands";
15
15
 
16
16
  class SerovalChunkReader {
17
+ reader: ReadableStreamDefaultReader<Uint8Array>;
18
+ buffer: string;
19
+ done: boolean;
17
20
  constructor(stream) {
18
21
  this.reader = stream.getReader();
19
22
  this.buffer = "";
@@ -92,6 +95,7 @@ async function deserializeStream(id, response) {
92
95
  if (!result.done) {
93
96
  reader.drain().then(
94
97
  () => {
98
+ // @ts-ignore
95
99
  delete $R[id];
96
100
  },
97
101
  () => {
@@ -105,7 +109,7 @@ async function deserializeStream(id, response) {
105
109
 
106
110
  let INSTANCE = 0;
107
111
 
108
- function createRequest(base, id, instance, body, contentType) {
112
+ function createRequest(base: string, id: string, instance: string, body: any, contentType?: string) {
109
113
  return fetch(base, {
110
114
  method: "POST",
111
115
  headers: {
@@ -154,7 +158,7 @@ async function fetchServerFunction(base, id, method, args) {
154
158
 
155
159
  if (response.headers.get("Location")) throw response;
156
160
  if (response.headers.get("X-Revalidate")) {
157
- /* ts-ignore-next-line */
161
+ /* @ts-ignore-next-line */
158
162
  response.customBody = () => {
159
163
  return deserializeStream(instance, response);
160
164
  };
@@ -1,59 +1,25 @@
1
1
  import {
2
- defineMiddleware,
3
- getRequestIP,
4
2
  H3Event,
5
- sendWebResponse,
6
- toWebRequest
3
+ defineMiddleware,
4
+ sendWebResponse
7
5
  } from "vinxi/server";
8
- import { FetchEvent } from "./types";
9
-
10
- const h3EventSymbol = Symbol("h3Event");
11
- const fetchEventSymbol = Symbol("fetchEvent");
12
- const eventTraps = {
13
- get(target, prop) {
14
- if (prop === fetchEventSymbol) return target;
15
- return target[prop] ?? target[h3EventSymbol][prop];
16
- }
17
- };
18
-
19
- export function createFetchEvent(event: H3Event): FetchEvent {
20
- const request = toWebRequest(event);
21
- return new Proxy(
22
- {
23
- request: request,
24
- clientAddress: getRequestIP(event),
25
- locals: {},
26
- // @ts-ignore
27
- [h3EventSymbol]: event
28
- },
29
- eventTraps
30
- ) as unknown as FetchEvent;
31
- }
32
-
33
- export function cloneEvent<T extends FetchEvent>(fetchEvent: T): T {
34
- return new Proxy({ ...fetchEvent[fetchEventSymbol] }, eventTraps);
35
- }
36
-
37
- export function getFetchEvent(h3Event: H3Event): FetchEvent {
38
- if (!h3Event[fetchEventSymbol]) {
39
- const fetchEvent = createFetchEvent(h3Event);
40
- h3Event[fetchEventSymbol] = fetchEvent;
41
- // @ts-ignore
42
- }
43
-
44
- return h3Event[fetchEventSymbol];
45
- }
6
+ import { getFetchEvent } from "../server/fetchEvent";
7
+ import type { FetchEvent } from "../server/types";
46
8
 
47
9
  /** Function responsible for receiving an observable [operation]{@link Operation} and returning a [result]{@link OperationResult}. */
48
10
 
49
11
  export type MiddlewareFn = (event: FetchEvent) => Promise<unknown> | unknown;
50
12
  /** This composes an array of Exchanges into a single ExchangeIO function */
51
13
 
52
- type RequestMiddleware = (event: FetchEvent) => Response | Promise<Response> | void | Promise<void>;
14
+ export type RequestMiddleware = (event: FetchEvent) => Response | Promise<Response> | void | Promise<void>;
15
+
16
+ // copy-pasted from h3/dist/index.d.ts
17
+ type EventHandlerResponse<T = any> = T | Promise<T>;
18
+ type ResponseMiddlewareResponseParam = { body?: Awaited<EventHandlerResponse> };
53
19
 
54
- type ResponseMiddleware = (
20
+ export type ResponseMiddleware = (
55
21
  event: FetchEvent,
56
- response: Response
22
+ response: ResponseMiddlewareResponseParam
57
23
  ) => Response | Promise<Response> | void | Promise<void>;
58
24
 
59
25
  function wrapRequestMiddleware(onRequest: RequestMiddleware) {
@@ -69,7 +35,7 @@ function wrapRequestMiddleware(onRequest: RequestMiddleware) {
69
35
  }
70
36
 
71
37
  function wrapResponseMiddleware(onBeforeResponse: ResponseMiddleware) {
72
- return async (h3Event: H3Event, response: Response) => {
38
+ return async (h3Event: H3Event, response: ResponseMiddlewareResponseParam) => {
73
39
  const fetchEvent = getFetchEvent(h3Event);
74
40
  const mwResponse = await onBeforeResponse(fetchEvent, response);
75
41
  if (!mwResponse) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidjs/start",
3
- "version": "0.4.11",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "author": "Ryan Carniato",
6
6
  "publishConfig": {
@@ -9,7 +9,7 @@
9
9
  "files": [
10
10
  "client",
11
11
  "config",
12
- "entry",
12
+ "middleware",
13
13
  "server",
14
14
  "shared",
15
15
  "*.d.ts"
@@ -18,8 +18,7 @@
18
18
  "exports": {
19
19
  ".": "./shared/index.tsx",
20
20
  "./config": "./config/index.js",
21
- "./entry": "./entry/index.ts",
22
- "./entry/spa": "./entry/spa/index.ts",
21
+ "./middleware": "./middleware/index.ts",
23
22
  "./server": "./server/index.tsx",
24
23
  "./server/spa": "./server/spa/index.tsx",
25
24
  "./client": "./client/index.tsx",
@@ -31,8 +30,8 @@
31
30
  ".": [
32
31
  "./shared/index.tsx"
33
32
  ],
34
- "./entry": [
35
- "./entry/index.ts"
33
+ "./middleware": [
34
+ "./middleware/index.ts"
36
35
  ],
37
36
  "./server": [
38
37
  "./server/index.tsx"
@@ -44,15 +43,16 @@
44
43
  },
45
44
  "devDependencies": {
46
45
  "solid-js": "^1.8.12",
47
- "vinxi": "^0.1.6"
46
+ "vinxi": "^0.2.1"
48
47
  },
49
48
  "dependencies": {
50
- "@vinxi/plugin-directives": "^0.1.3",
51
- "@vinxi/server-components": "^0.1.3",
52
- "@vinxi/server-functions": "^0.1.3",
49
+ "@vinxi/plugin-directives": "^0.2.0",
50
+ "@vinxi/server-components": "^0.2.0",
51
+ "@vinxi/server-functions": "^0.2.1",
53
52
  "defu": "^6.1.2",
54
53
  "error-stack-parser": "^2.1.4",
55
54
  "html-to-image": "^1.11.11",
55
+ "radix3": "^1.1.0",
56
56
  "seroval": "^1.0.2",
57
57
  "seroval-plugins": "^1.0.2",
58
58
  "shikiji": "^0.9.12",
@@ -35,9 +35,9 @@ export function StartServer(props: { document: Component<DocumentComponentProps>
35
35
 
36
36
  let assets = [];
37
37
  Promise.resolve().then(async () => {
38
- if (context.routerMatches && context.routerMatches[0]) {
39
- const matches = [...context.routerMatches[0]];
40
- while (matches.length && (!matches[0].metadata || !matches[0].metadata.filesystem))
38
+ if (context.router && context.router.matches) {
39
+ const matches = [...context.router.matches];
40
+ while (matches.length && (!matches[0].info || !matches[0].info.filesystem))
41
41
  matches.shift();
42
42
  const matched = matches.length && matchRoute(matches, context.routes);
43
43
  if (matched) {
@@ -0,0 +1,115 @@
1
+ import {
2
+ H3Event,
3
+ HTTPEventSymbol,
4
+ appendResponseHeader,
5
+ getRequestIP,
6
+ getResponseHeader,
7
+ getResponseHeaders,
8
+ getResponseStatus,
9
+ getResponseStatusText,
10
+ removeResponseHeader,
11
+ setHeader,
12
+ setResponseHeader,
13
+ setResponseStatus,
14
+ toWebRequest
15
+ } from "vinxi/server";
16
+ import type { FetchEvent, ResponseStub } from "./types";
17
+
18
+ const fetchEventSymbol = Symbol("fetchEvent");
19
+
20
+ export function createFetchEvent(event: H3Event): FetchEvent {
21
+ const request = toWebRequest(event);
22
+ return {
23
+ request: request,
24
+ response: createResponseStub(event),
25
+ clientAddress: getRequestIP(event),
26
+ locals: {},
27
+ nativeEvent: event,
28
+ [HTTPEventSymbol]: event
29
+ };
30
+ }
31
+
32
+ export function cloneEvent<T extends FetchEvent>(fetchEvent: T): T {
33
+ return {
34
+ ...fetchEvent,
35
+ [HTTPEventSymbol]: fetchEvent[HTTPEventSymbol]
36
+ };
37
+ }
38
+
39
+ export function getFetchEvent(h3Event: H3Event): FetchEvent {
40
+ if (!h3Event[fetchEventSymbol]) {
41
+ const fetchEvent = createFetchEvent(h3Event);
42
+ h3Event[fetchEventSymbol] = fetchEvent;
43
+ // @ts-ignore
44
+ }
45
+
46
+ return h3Event[fetchEventSymbol];
47
+ }
48
+
49
+ export function mergeResponseHeaders(h3Event, headers) {
50
+ for (const [key, value] of headers.entries()) {
51
+ setHeader(h3Event, key, value);
52
+ }
53
+ }
54
+
55
+ class HeaderProxy {
56
+ constructor(private event: H3Event) {}
57
+ get(key: string) {
58
+ const h = getResponseHeader(this.event, key);
59
+ return Array.isArray(h) ? h.join(", ") : h;
60
+ }
61
+ has(key: string) {
62
+ return this.get(key) !== undefined;
63
+ }
64
+ set(key: string, value: string) {
65
+ return setResponseHeader(this.event, key, value);
66
+ }
67
+ delete(key: string) {
68
+ return removeResponseHeader(this.event, key);
69
+ }
70
+ append(key: string, value: string) {
71
+ appendResponseHeader(this.event, key, value);
72
+ }
73
+ getSetCookie() {
74
+ const cookies = getResponseHeader(this.event, "Set-Cookie");
75
+ return Array.isArray(cookies) ? cookies : [cookies];
76
+ }
77
+ forEach(fn: (value: string, key: string, object: Headers) => void) {
78
+ return this.entries().forEach(([key, value]) => fn(value, key, this));
79
+ }
80
+ entries() {
81
+ return getResponseHeaders(this.event).map(([key, value]) => [
82
+ key,
83
+ Array.isArray(value) ? value.join(", ") : value
84
+ ]);
85
+ }
86
+ keys() {
87
+ return getResponseHeaders(this.event).map(([key]) => key);
88
+ }
89
+ values() {
90
+ return getResponseHeaders(this.event).map(([key, value]) =>
91
+ Array.isArray(value) ? value.join(", ") : value
92
+ );
93
+ }
94
+ [Symbol.iterator]() {
95
+ return this.entries()[Symbol.iterator]();
96
+ }
97
+ }
98
+
99
+ function createResponseStub(event: H3Event): ResponseStub {
100
+ return {
101
+ get status() {
102
+ return getResponseStatus(event);
103
+ },
104
+ set status(v) {
105
+ setResponseStatus(event, v);
106
+ },
107
+ get statusText() {
108
+ return getResponseStatusText(event);
109
+ },
110
+ set statusText(v) {
111
+ setResponseStatus(event, getResponseStatus(), v);
112
+ },
113
+ headers: new HeaderProxy(event)
114
+ };
115
+ }
@@ -1,21 +1,19 @@
1
1
  import { sharedConfig } from "solid-js";
2
- import { renderToStream } from "solid-js/web";
2
+ import { renderToStream, renderToString } from "solid-js/web";
3
3
  /* @ts-ignore */
4
4
  import { provideRequestEvent } from "solid-js/web/storage";
5
5
  import {
6
6
  EventHandlerObject,
7
7
  EventHandlerRequest,
8
8
  H3Event,
9
- HTTPMethod,
10
9
  eventHandler,
11
10
  sendRedirect,
12
11
  setHeader,
13
12
  setResponseStatus
14
13
  } from "vinxi/server";
15
- import { getFetchEvent } from "../server/middleware";
16
- import { createPageEvent } from "../server/page-event";
17
- import { APIEvent, FetchEvent, PageEvent } from "../server/types";
18
- import { matchAPIRoute } from "../shared/routes";
14
+ import { getFetchEvent } from "./fetchEvent";
15
+ import { createPageEvent } from "./pageEvent";
16
+ import { FetchEvent, PageEvent } from "./types";
19
17
 
20
18
  export function createBaseHandler(
21
19
  fn: (context: PageEvent) => unknown,
@@ -35,21 +33,21 @@ export function createBaseHandler(
35
33
  onBeforeResponse: options.onBeforeResponse,
36
34
  handler: (e: H3Event<EventHandlerRequest> & { startEvent: FetchEvent }) => {
37
35
  const event = getFetchEvent(e);
36
+ const mode = import.meta.env.START_SSR;
38
37
 
39
38
  return provideRequestEvent(event, async () => {
40
- // api
41
- const match = matchAPIRoute(new URL(event.request.url).pathname, event.request.method as HTTPMethod)
42
- if (match) {
43
- const mod = await match.handler.import();
44
- const fn = mod[event.request.method];
45
- (event as APIEvent).params = match.params;
46
- sharedConfig.context = { event } as any;
47
- return await fn(event);
48
- }
49
-
50
- // render stream
51
- const doAsync = import.meta.env.START_SSR === "async";
39
+ // render
52
40
  const context = await createPageEvent(event);
41
+ if (mode === "sync") {
42
+ const html = renderToString(() => {
43
+ (sharedConfig.context as any).event = context;
44
+ return fn(context);
45
+ }, options);
46
+ if (context.response && context.response.headers.get("Location")) {
47
+ return sendRedirect(event, context.response.headers.get("Location"));
48
+ }
49
+ return html;
50
+ }
53
51
  let cloned = { ...options };
54
52
  if (cloned.onCompleteAll) {
55
53
  const og = cloned.onCompleteAll;
@@ -72,7 +70,7 @@ export function createBaseHandler(
72
70
  if (context.response && context.response.headers.get("Location")) {
73
71
  return sendRedirect(event, context.response.headers.get("Location"));
74
72
  }
75
- if (doAsync) return stream;
73
+ if (mode === "async") return stream;
76
74
  // fix cloudflare streaming
77
75
  const { writable, readable } = new TransformStream();
78
76
  stream.pipeTo(writable);
package/server/index.tsx CHANGED
@@ -1,4 +1,4 @@
1
- export * from "vinxi/server";
2
1
  export { StartServer } from "./StartServer";
3
- export * from "./middleware";
2
+ export { createHandler } from "./handler";
4
3
  export * from "./types";
4
+
@@ -1,11 +1,6 @@
1
1
  import {
2
- appendResponseHeader,
3
2
  getCookie,
4
- getResponseHeader,
5
- removeResponseHeader,
6
- setCookie,
7
- setResponseHeader,
8
- setResponseStatus
3
+ setCookie
9
4
  } from "vinxi/server";
10
5
  import { createRoutes } from "../shared/FileRoutes";
11
6
  import { FetchEvent, PageEvent } from "./types";
@@ -27,7 +22,7 @@ function initFromFlash(ctx: FetchEvent) {
27
22
  export async function createPageEvent(ctx: FetchEvent) {
28
23
  const clientManifest = import.meta.env.MANIFEST["client"];
29
24
  const serverManifest = import.meta.env.MANIFEST["ssr"];
30
- setResponseHeader(ctx, "Content-Type", "text/html");
25
+ ctx.response.headers.set("Cache-Control", "no-cache");
31
26
  // const prevPath = ctx.request.headers.get("x-solid-referrer");
32
27
  // const mutation = ctx.request.headers.get("x-solid-mutation") === "true";
33
28
  const pageEvent: PageEvent = Object.assign(ctx, {
@@ -40,31 +35,10 @@ export async function createPageEvent(ctx: FetchEvent) {
40
35
  )
41
36
  : [])
42
37
  ],
43
- initialSubmission: initFromFlash(ctx),
44
- routes: createRoutes(),
45
- components: {
46
- status: props => {
47
- setResponseStatus(ctx, props.code, props.text);
48
- return () => !ctx.handled && setResponseStatus(ctx, 200);
49
- },
50
- header: props => {
51
- if (props.append) {
52
- appendResponseHeader(ctx, props.name, props.value);
53
- } else {
54
- setResponseHeader(ctx, props.name, props.value);
55
- }
56
-
57
- return () => {
58
- if (ctx.handled) return;
59
- let values = getResponseHeader(ctx, props.name);
60
- if (!Array.isArray(values)) values = [values as string];
61
- const index = values.indexOf(props.value);
62
- index !== -1 && values.splice(index, 1);
63
- if (values.length) setResponseHeader(ctx, props.name, values);
64
- else removeResponseHeader(ctx, props.name);
65
- };
66
- }
38
+ router: {
39
+ submission: initFromFlash(ctx)
67
40
  },
41
+ routes: createRoutes(),
68
42
  // prevUrl: prevPath || "",
69
43
  // mutation: mutation,
70
44
  // $type: FETCH_EVENT,
@@ -1,5 +1,5 @@
1
- import { createBaseHandler } from "..";
2
- import { FetchEvent, PageEvent } from "../../server/types";
1
+ import { createBaseHandler } from "../handler";
2
+ import { FetchEvent, PageEvent } from "../types";
3
3
 
4
4
  export function createHandler(
5
5
  fn: (context: PageEvent) => unknown,
@@ -1,5 +1,4 @@
1
- export * from "vinxi/server";
2
- export * from "../middleware";
3
1
  export * from "../types";
4
2
  export { StartServer } from "./StartServer";
3
+ export { createHandler } from "./handler";
5
4
 
package/server/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { JSX } from "solid-js";
2
- import type { EventHandlerRequest, H3Event } from "vinxi/server";
2
+ import { HTTPEventSymbol, type EventHandlerRequest, type H3Event } from "vinxi/server";
3
3
 
4
4
  // export const FETCH_EVENT = "$FETCH";
5
5
 
@@ -16,10 +16,21 @@ export type ContextMatches = {
16
16
  params: unknown;
17
17
  };
18
18
 
19
- export interface FetchEvent extends H3Event<EventHandlerRequest> {
19
+ export interface ResponseStub {
20
+ status?: number;
21
+ statusText?: string;
22
+ headers: Headers;
23
+ }
24
+ export interface FetchEvent {
20
25
  request: Request;
26
+ response: ResponseStub;
21
27
  clientAddress: string;
22
- locals: Record<string, unknown>;
28
+ locals: RequestEventLocals;
29
+ nativeEvent: H3Event<EventHandlerRequest>;
30
+ [HTTPEventSymbol]: H3Event<EventHandlerRequest>;
31
+ }
32
+ export interface RequestEventLocals {
33
+ [key: string | symbol]: any;
23
34
  }
24
35
  export interface PageEvent extends FetchEvent {
25
36
  manifest: any;
@@ -29,7 +40,6 @@ export interface PageEvent extends FetchEvent {
29
40
  // $type: typeof FETCH_EVENT;
30
41
  $islands: Set<string>;
31
42
  // mutation: boolean;
32
- response?: Response;
33
43
  }
34
44
 
35
45
  export interface APIEvent extends FetchEvent {
@@ -10,8 +10,8 @@ export function createRoutes() {
10
10
  return {
11
11
  ...route,
12
12
  ...(route.$$route ? route.$$route.require().route : undefined),
13
- metadata: {
14
- ...(route.$$route ? route.$$route.require().route.metadata : {}),
13
+ info: {
14
+ ...(route.$$route ? route.$$route.require().route.info : {}),
15
15
  filesystem: true
16
16
  },
17
17
  component: lazyRoute(
@@ -4,7 +4,24 @@ import { getRequestEvent, isServer } from "solid-js/web";
4
4
  export function HttpHeader(props: { name: string; value: string; append?: boolean }) {
5
5
  if (isServer) {
6
6
  const event = getRequestEvent();
7
- if (event) onCleanup((event as any).components.header(props));
7
+
8
+ if (props.append) event.response.headers.append(props.name, props.value);
9
+ else event.response.headers.set(props.name, props.value);
10
+
11
+ onCleanup(() => {
12
+ if (event.nativeEvent.handled) return;
13
+ const value = event.response.headers.get(props.name);
14
+ if (!value) return;
15
+ if (!value.includes(", ")) {
16
+ if (value === props.value) event.response.headers.delete(props.name);
17
+ return;
18
+ }
19
+ const values = value.split(", ");
20
+ const index = values.indexOf(props.value);
21
+ index !== -1 && values.splice(index, 1);
22
+ if (values.length) event.response.headers.set(props.name, values.join(","));
23
+ else event.response.headers.delete(props.name);
24
+ });
8
25
  }
9
26
 
10
27
  return null;
@@ -4,7 +4,9 @@ import { getRequestEvent, isServer } from "solid-js/web";
4
4
  export function HttpStatusCode(props: { code: number, text?: string }) {
5
5
  if (isServer) {
6
6
  const event = getRequestEvent();
7
- if (event) onCleanup((event as any).components.status(props));
7
+ event.response.status = props.code;
8
+ event.response.statusText = props.text;
9
+ onCleanup(() => !event.nativeEvent.handled && (event.response.status = 200));
8
10
  }
9
11
  return null;
10
12
  }
package/shared/routes.ts CHANGED
@@ -1,24 +1,12 @@
1
1
  import fileRoutes from "vinxi/routes";
2
- import { HTTPMethod } from "vinxi/server";
3
2
 
4
3
  interface Route {
5
4
  path: string;
6
5
  id: string;
7
- type: "api" | "page";
6
+ type: "page";
8
7
  children?: Route[];
9
8
  }
10
9
 
11
- type MatchRoute = Route & {
12
- score: number;
13
- params: {
14
- type: "*" | ":";
15
- name: string;
16
- index: number;
17
- }[];
18
- matchSegments: (string | null)[];
19
- wildcard: boolean;
20
- };
21
-
22
10
  declare module "vinxi/routes" {
23
11
  export interface Register {
24
12
  route: {
@@ -30,49 +18,6 @@ declare module "vinxi/routes" {
30
18
  }
31
19
 
32
20
  export const pageRoutes = defineRoutes((fileRoutes as unknown as Route[]).filter(o => o.type === "page"));
33
- const apiRoutes = defineAPIRoutes((fileRoutes as unknown as Route[]).filter(o => o.type === "api"));
34
-
35
- export function matchAPIRoute(path: string, method: HTTPMethod) {
36
- const segments = path.replace(import.meta.env.SERVER_BASE_URL, "").split("/").filter(Boolean);
37
-
38
- routeLoop: for (const route of apiRoutes) {
39
- const matchSegments = route.matchSegments;
40
-
41
- if (
42
- segments.length < matchSegments.length ||
43
- (!route.wildcard && segments.length > matchSegments.length)
44
- ) {
45
- continue;
46
- }
47
-
48
- for (let index = 0; index < matchSegments.length; index++) {
49
- const match = matchSegments[index];
50
- if (!match) {
51
- continue;
52
- }
53
-
54
- if (segments[index] !== match) {
55
- continue routeLoop;
56
- }
57
- }
58
-
59
- const handler = route[`$${method}`];
60
- if (handler === "skip" || handler === undefined) {
61
- return;
62
- }
63
-
64
- const params: Record<string, string> = {};
65
- for (const { type, name, index } of route.params) {
66
- if (type === ":") {
67
- params[name] = segments[index];
68
- } else {
69
- params[name] = segments.slice(index).join("/");
70
- }
71
- }
72
-
73
- return { handler, params };
74
- }
75
- }
76
21
 
77
22
  function defineRoutes(fileRoutes: Route[]) {
78
23
  function processRoute(routes: Route[], route: Route, id: string, full: string) {
@@ -101,79 +46,3 @@ function defineRoutes(fileRoutes: Route[]) {
101
46
  }, []);
102
47
  }
103
48
 
104
- function defineAPIRoutes(routes: Route[]) {
105
- return routes
106
- .flatMap(route => {
107
- const paths = expandOptionals(route.path);
108
- return paths.map(path => ({ ...route, path }));
109
- })
110
- .map(routeToMatchRoute)
111
- .sort((a, b) => b.score - a.score);
112
- }
113
-
114
- // This is copied from https://github.com/solidjs/solid-router/blob/main/src/utils.ts
115
- function expandOptionals(pattern: string): string[] {
116
- let match = /(\/?\:[^\/]+)\?/.exec(pattern);
117
- if (!match) return [pattern];
118
-
119
- let prefix = pattern.slice(0, match.index);
120
- let suffix = pattern.slice(match.index + match[0].length);
121
- const prefixes: string[] = [prefix, (prefix += match[1])];
122
-
123
- // This section handles adjacent optional params. We don't actually want all permuations since
124
- // that will lead to equivalent apiRwhich have the same number of params. For example
125
- // `/:a?/:b?/:c`? only has the unique expansion: `/`, `/:a`, `/:a/:b`, `/:a/:b/:c` and we can
126
- // discard `/:b`, `/:c`, `/:b/:c` by building them up in order and not recursing. This also helps
127
- // ensure predictability where earlier params have precidence.
128
- while ((match = /^(\/\:[^\/]+)\?/.exec(suffix))) {
129
- prefixes.push((prefix += match[1]));
130
- suffix = suffix.slice(match[0].length);
131
- }
132
-
133
- return expandOptionals(suffix).reduce<string[]>(
134
- (results, expansion) => [...results, ...prefixes.map(p => p + expansion)],
135
- []
136
- );
137
- }
138
-
139
- function routeToMatchRoute(route: Route): MatchRoute {
140
- const segments = route.path.split("/").filter(Boolean);
141
- const params: { type: "*" | ":"; name: string; index: number }[] = [];
142
- const matchSegments: (string | null)[] = [];
143
- let score = 0;
144
- let wildcard = false;
145
-
146
- for (const [index, segment] of segments.entries()) {
147
- if (segment[0] === ":") {
148
- const name = segment.slice(1);
149
- score += 3;
150
- params.push({
151
- type: ":",
152
- name,
153
- index
154
- });
155
- matchSegments.push(null);
156
- } else if (segment[0] === "*") {
157
- score -= 1;
158
- params.push({
159
- type: "*",
160
- name: segment.slice(1),
161
- index
162
- });
163
- wildcard = true;
164
- } else {
165
- score += 4;
166
- if (!segment.match(/^\(.+\)$/)) {
167
- matchSegments.push(segment);
168
- }
169
- }
170
- }
171
-
172
- return {
173
- ...route,
174
- score,
175
- params,
176
- matchSegments,
177
- wildcard
178
- };
179
- }