@solidjs/start 0.4.11 → 0.5.1

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.
@@ -24,7 +24,6 @@ export class SolidStartClientFileRouter extends BaseFileSystemRouter {
24
24
 
25
25
  if (src.endsWith(".md") || src.endsWith(".mdx")) {
26
26
  return {
27
- type: "page",
28
27
  $component: {
29
28
  src: src,
30
29
  pick: ["$css"]
@@ -40,7 +39,6 @@ export class SolidStartClientFileRouter extends BaseFileSystemRouter {
40
39
  const hasRouteConfig = exports.find(e => e.n === "route");
41
40
  if (hasDefault) {
42
41
  return {
43
- type: "page",
44
42
  $component: {
45
43
  src: src,
46
44
  pick: ["default", "$css"]
@@ -57,6 +55,7 @@ export class SolidStartClientFileRouter extends BaseFileSystemRouter {
57
55
  }
58
56
  }
59
57
  }
58
+
60
59
  const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"];
61
60
  function createHTTPHandlers(src, exports) {
62
61
  const handlers = {};
@@ -70,6 +69,7 @@ function createHTTPHandlers(src, exports) {
70
69
  }
71
70
  return handlers;
72
71
  }
72
+
73
73
  export class SolidStartServerFileRouter extends BaseFileSystemRouter {
74
74
  toPath(src) {
75
75
  const routePath = cleanPath(src, this.config)
@@ -91,10 +91,8 @@ export class SolidStartServerFileRouter extends BaseFileSystemRouter {
91
91
 
92
92
  toRoute(src) {
93
93
  let path = this.toPath(src);
94
-
95
94
  if (src.endsWith(".md") || src.endsWith(".mdx")) {
96
95
  return {
97
- type: "page",
98
96
  $component: {
99
97
  src: src,
100
98
  pick: ["$css"]
@@ -107,25 +105,22 @@ export class SolidStartServerFileRouter extends BaseFileSystemRouter {
107
105
 
108
106
  const [_, exports] = analyzeModule(src);
109
107
  const hasRouteConfig = exports.find(e => e.n === "route");
110
- if (exports.find(exp => exp.n === "default")) {
108
+ const hasDefault = exports.find(e => e.n === "default");
109
+ const hasAPIRoutes = exports.find(exp => HTTP_METHODS.includes(exp.n));
110
+ if (hasDefault || hasAPIRoutes) {
111
111
  return {
112
- type: "page",
113
- $component: {
114
- src: src,
115
- pick: ["default", "$css"]
116
- },
112
+ $component: hasDefault
113
+ ? {
114
+ src: src,
115
+ pick: ["default", "$css"]
116
+ }
117
+ : undefined,
117
118
  $$route: hasRouteConfig
118
119
  ? {
119
120
  src: src,
120
121
  pick: ["route"]
121
122
  }
122
123
  : undefined,
123
- path,
124
- filePath: src
125
- };
126
- } else if (exports.find(exp => HTTP_METHODS.includes(exp.n))) {
127
- return {
128
- type: "api",
129
124
  ...createHTTPHandlers(src, exports),
130
125
  path,
131
126
  filePath: src
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,10 @@ 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 {
12
+ SolidStartClientFileRouter,
13
+ SolidStartServerFileRouter
14
+ } from "./fs-router.js";
12
15
  import { serverComponents } from "./server-components.js";
13
16
 
14
17
  const DEFAULT_EXTENSIONS = ["js", "jsx", "ts", "tsx"];
@@ -18,7 +21,7 @@ function solidStartClientFsRouter(config) {
18
21
  new SolidStartClientFileRouter(
19
22
  {
20
23
  dir: resolve.absolute(config.dir, router.root),
21
- extensions: config.extensions ?? ["js", "jsx", "ts", "tsx"]
24
+ extensions: config.extensions ?? DEFAULT_EXTENSIONS
22
25
  },
23
26
  router,
24
27
  app
@@ -30,7 +33,7 @@ function solidStartServerFsRouter(config) {
30
33
  new SolidStartServerFileRouter(
31
34
  {
32
35
  dir: resolve.absolute(config.dir, router.root),
33
- extensions: config.extensions ?? ["js", "jsx", "ts", "tsx"]
36
+ extensions: config.extensions ?? DEFAULT_EXTENSIONS
34
37
  },
35
38
  router,
36
39
  app
@@ -44,7 +47,12 @@ export function defineConfig(baseConfig = {}) {
44
47
  appRoot: "./src",
45
48
  ssr: true,
46
49
  islands: false,
47
- solid: {}
50
+ solid: {},
51
+ server: {
52
+ experimental: {
53
+ asyncContext: true
54
+ }
55
+ }
48
56
  });
49
57
  let server = start.server;
50
58
  if (!start.ssr) {
@@ -65,16 +73,15 @@ export function defineConfig(baseConfig = {}) {
65
73
  routers: [
66
74
  {
67
75
  name: "public",
68
- mode: "static",
76
+ type: "static",
69
77
  dir: "./public",
70
78
  base: "/"
71
79
  },
72
-
73
80
  {
74
81
  name: "ssr",
75
- mode: "handler",
82
+ type: "http",
76
83
  link: {
77
- client: "client"
84
+ client: start.islands ? undefined : "client"
78
85
  },
79
86
  handler: `${start.appRoot}/entry-server${entryExtension}`,
80
87
  middleware: start.middleware,
@@ -96,7 +103,7 @@ export function defineConfig(baseConfig = {}) {
96
103
  ...(typeof plugins === "function" ? [...(await plugins())] : plugins),
97
104
 
98
105
  serverTransform({
99
- runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.jsx", import.meta.url)))
106
+ runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.ts", import.meta.url)))
100
107
  }),
101
108
  start.islands ? serverComponents.server() : null,
102
109
  solid({ ...start.solid, ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
@@ -125,7 +132,7 @@ export function defineConfig(baseConfig = {}) {
125
132
  },
126
133
  {
127
134
  name: "client",
128
- mode: "build",
135
+ type: "client",
129
136
  handler: `${start.appRoot}/entry-client${entryExtension}`,
130
137
  ...(start.islands
131
138
  ? {}
@@ -148,7 +155,7 @@ export function defineConfig(baseConfig = {}) {
148
155
  }),
149
156
  ...(typeof plugins === "function" ? [...(await plugins())] : plugins),
150
157
  serverFunctions.client({
151
- runtime: normalize(fileURLToPath(new URL("./server-runtime.jsx", import.meta.url)))
158
+ runtime: normalize(fileURLToPath(new URL("./server-runtime.ts", import.meta.url)))
152
159
  }),
153
160
  start.islands ? serverComponents.client() : null,
154
161
  solid({ ...start.solid, ssr: start.ssr, extensions: extensions.map(ext => `.${ext}`) }),
@@ -182,8 +189,8 @@ export function defineConfig(baseConfig = {}) {
182
189
  base: "/_build"
183
190
  },
184
191
  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))),
192
+ handler: normalize(fileURLToPath(new URL("./server-handler.ts", import.meta.url))),
193
+ runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.ts", import.meta.url))),
187
194
  // routes: solidStartServerFsRouter({ dir: `${start.appRoot}/routes`, extensions }),
188
195
  plugins: async () => [
189
196
  config("user", {
@@ -1,6 +1,6 @@
1
1
  import { getRequestEvent } from "solid-js/web";
2
2
  import { provideRequestEvent } from "solid-js/web/storage";
3
- import { cloneEvent } from "../server/middleware";
3
+ import { cloneEvent } from "../server/fetchEvent";
4
4
 
5
5
  export function createServerReference(fn, id, name) {
6
6
  if (typeof fn !== "function") throw new Error("Export from a 'use server' module must be a function");
@@ -13,11 +13,10 @@ import {
13
13
  URLSearchParamsPlugin
14
14
  } from "seroval-plugins/web";
15
15
  import { sharedConfig } from "solid-js";
16
- /* @ts-ignore */
17
16
  import { provideRequestEvent } from "solid-js/web/storage";
17
+ import { eventHandler, setHeader } from "vinxi/http";
18
18
  import invariant from "vinxi/lib/invariant";
19
- import { eventHandler, setHeader } from "vinxi/server";
20
- import { getFetchEvent } from "../server/middleware";
19
+ import { getFetchEvent, mergeResponseHeaders } from "../server/fetchEvent";
21
20
 
22
21
  function createChunk(data) {
23
22
  const bytes = data.length;
@@ -94,13 +93,13 @@ async function handleServerFunction(h3Event) {
94
93
  ) {
95
94
  // workaround for https://github.com/unjs/nitro/issues/1721
96
95
  // (issue only in edge runtimes)
97
- parsed.push(await new Request(request, { ...request, body: event.node.req.body }).formData());
96
+ parsed.push(await new Request(request, { ...request, body: h3Event.node.req.body }).formData());
98
97
  // what should work when #1721 is fixed
99
98
  // parsed.push(await request.formData);
100
99
  } else {
101
100
  // workaround for https://github.com/unjs/nitro/issues/1721
102
101
  // (issue only in edge runtimes)
103
- const tmpReq = new Request(request, { ...request, body: event.node.req.body })
102
+ const tmpReq = new Request(request, { ...request, body: h3Event.node.req.body })
104
103
  // what should work when #1721 is fixed
105
104
  // just use request.json() here
106
105
  parsed = fromJSON(await tmpReq.json(), {
@@ -137,13 +136,9 @@ async function handleServerFunction(h3Event) {
137
136
  });
138
137
  }
139
138
  // 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();
139
+ if (result.headers) mergeResponseHeaders(h3Event, result.headers);
140
+ if ((result as any).customBody) {
141
+ result = await (result as any).customBody();
147
142
  } else if (result.body == undefined) result = undefined;
148
143
  }
149
144
 
@@ -182,13 +177,9 @@ async function handleServerFunction(h3Event) {
182
177
  });
183
178
  }
184
179
  // 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();
180
+ if (x.headers) mergeResponseHeaders(h3Event, x.headers);
181
+ if ((x as any).customBody) {
182
+ x = await (x as any).customBody();
192
183
  } else if (x.body == undefined) x = undefined;
193
184
  }
194
185
  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
- H3Event,
5
- sendWebResponse,
6
- toWebRequest
7
- } 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
- }
2
+ H3Event,
3
+ defineMiddleware,
4
+ sendWebResponse
5
+ } from "vinxi/http";
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.1",
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"
@@ -43,16 +42,17 @@
43
42
  }
44
43
  },
45
44
  "devDependencies": {
46
- "solid-js": "^1.8.12",
47
- "vinxi": "^0.1.6"
45
+ "solid-js": "^1.8.13",
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,114 @@
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/http";
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
+ }
44
+
45
+ return h3Event[fetchEventSymbol];
46
+ }
47
+
48
+ export function mergeResponseHeaders(h3Event, headers) {
49
+ for (const [key, value] of headers.entries()) {
50
+ setHeader(h3Event, key, value);
51
+ }
52
+ }
53
+
54
+ class HeaderProxy {
55
+ constructor(private event: H3Event) {}
56
+ get(key: string) {
57
+ const h = getResponseHeader(this.event, key);
58
+ return Array.isArray(h) ? h.join(", ") : h;
59
+ }
60
+ has(key: string) {
61
+ return this.get(key) !== undefined;
62
+ }
63
+ set(key: string, value: string) {
64
+ return setResponseHeader(this.event, key, value);
65
+ }
66
+ delete(key: string) {
67
+ return removeResponseHeader(this.event, key);
68
+ }
69
+ append(key: string, value: string) {
70
+ appendResponseHeader(this.event, key, value);
71
+ }
72
+ getSetCookie() {
73
+ const cookies = getResponseHeader(this.event, "Set-Cookie");
74
+ return Array.isArray(cookies) ? cookies : [cookies];
75
+ }
76
+ forEach(fn: (value: string, key: string, object: Headers) => void) {
77
+ return this.entries().forEach(([key, value]) => fn(value, key, this));
78
+ }
79
+ entries() {
80
+ return getResponseHeaders(this.event).map(([key, value]) => [
81
+ key,
82
+ Array.isArray(value) ? value.join(", ") : value
83
+ ]);
84
+ }
85
+ keys() {
86
+ return getResponseHeaders(this.event).map(([key]) => key);
87
+ }
88
+ values() {
89
+ return getResponseHeaders(this.event).map(([key, value]) =>
90
+ Array.isArray(value) ? value.join(", ") : value
91
+ );
92
+ }
93
+ [Symbol.iterator]() {
94
+ return this.entries()[Symbol.iterator]();
95
+ }
96
+ }
97
+
98
+ function createResponseStub(event: H3Event): ResponseStub {
99
+ return {
100
+ get status() {
101
+ return getResponseStatus(event);
102
+ },
103
+ set status(v) {
104
+ setResponseStatus(event, v);
105
+ },
106
+ get statusText() {
107
+ return getResponseStatusText(event);
108
+ },
109
+ set statusText(v) {
110
+ setResponseStatus(event, getResponseStatus(), v);
111
+ },
112
+ headers: new HeaderProxy(event)
113
+ };
114
+ }
@@ -1,21 +1,19 @@
1
1
  import { sharedConfig } from "solid-js";
2
- import { renderToStream } from "solid-js/web";
3
- /* @ts-ignore */
2
+ import { renderToStream, renderToString } from "solid-js/web";
4
3
  import { provideRequestEvent } from "solid-js/web/storage";
5
4
  import {
6
5
  EventHandlerObject,
7
6
  EventHandlerRequest,
8
7
  H3Event,
9
- HTTPMethod,
10
8
  eventHandler,
11
9
  sendRedirect,
12
10
  setHeader,
13
11
  setResponseStatus
14
- } from "vinxi/server";
15
- import { getFetchEvent } from "../server/middleware";
16
- import { createPageEvent } from "../server/page-event";
17
- import { APIEvent, FetchEvent, PageEvent } from "../server/types";
12
+ } from "vinxi/http";
18
13
  import { matchAPIRoute } from "../shared/routes";
14
+ import { getFetchEvent } from "./fetchEvent";
15
+ import { createPageEvent } from "./pageEvent";
16
+ import type { APIEvent, FetchEvent, PageEvent } from "./types";
19
17
 
20
18
  export function createBaseHandler(
21
19
  fn: (context: PageEvent) => unknown,
@@ -35,21 +33,38 @@ 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
39
  // api
41
- const match = matchAPIRoute(new URL(event.request.url).pathname, event.request.method as HTTPMethod)
40
+ const match = matchAPIRoute(new URL(event.request.url).pathname, event.request.method);
42
41
  if (match) {
43
42
  const mod = await match.handler.import();
44
43
  const fn = mod[event.request.method];
45
44
  (event as APIEvent).params = match.params;
46
- sharedConfig.context = { event } as any;
47
- return await fn(event);
45
+ // @ts-ignore
46
+ sharedConfig.context = { event };
47
+ const res = await fn(event);
48
+ if (res !== undefined) return res;
49
+ if (event.request.method !== "GET") {
50
+ throw new Error(
51
+ `API handler for ${event.request.method} "${event.request.url}" did not return a response.`
52
+ );
53
+ }
48
54
  }
49
55
 
50
- // render stream
51
- const doAsync = import.meta.env.START_SSR === "async";
56
+ // render
52
57
  const context = await createPageEvent(event);
58
+ if (mode === "sync") {
59
+ const html = renderToString(() => {
60
+ (sharedConfig.context as any).event = context;
61
+ return fn(context);
62
+ }, options);
63
+ if (context.response && context.response.headers.get("Location")) {
64
+ return sendRedirect(event, context.response.headers.get("Location"));
65
+ }
66
+ return html;
67
+ }
53
68
  let cloned = { ...options };
54
69
  if (cloned.onCompleteAll) {
55
70
  const og = cloned.onCompleteAll;
@@ -67,12 +82,12 @@ export function createBaseHandler(
67
82
  } else cloned.onCompleteShell = handleShellCompleteRedirect(context, e);
68
83
  const stream = renderToStream(() => {
69
84
  (sharedConfig.context as any).event = context;
70
- return fn(context)
85
+ return fn(context);
71
86
  }, cloned);
72
87
  if (context.response && context.response.headers.get("Location")) {
73
88
  return sendRedirect(event, context.response.headers.get("Location"));
74
89
  }
75
- if (doAsync) return stream;
90
+ if (mode === "async") return stream;
76
91
  // fix cloudflare streaming
77
92
  const { writable, readable } = new TransformStream();
78
93
  stream.pipeTo(writable);
@@ -88,7 +103,7 @@ function handleShellCompleteRedirect(context: PageEvent, e: H3Event<EventHandler
88
103
  setResponseStatus(e, 302);
89
104
  setHeader(e, "Location", context.response.headers.get("Location"));
90
105
  }
91
- }
106
+ };
92
107
  }
93
108
 
94
109
  function handleStreamCompleteRedirect(context: PageEvent) {
@@ -103,4 +118,4 @@ export function createHandler(
103
118
  options: Parameters<typeof createBaseHandler>[1] = {}
104
119
  ) {
105
120
  return createBaseHandler(fn, { ...options, createPageEvent });
106
- }
121
+ }
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,12 +1,7 @@
1
1
  import {
2
- appendResponseHeader,
3
- getCookie,
4
- getResponseHeader,
5
- removeResponseHeader,
6
- setCookie,
7
- setResponseHeader,
8
- setResponseStatus
9
- } from "vinxi/server";
2
+ getCookie,
3
+ setCookie
4
+ } from "vinxi/http";
10
5
  import { createRoutes } from "../shared/FileRoutes";
11
6
  import { FetchEvent, PageEvent } from "./types";
12
7
 
@@ -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/http";
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,78 +1,25 @@
1
+ import { createRouter } from "radix3";
1
2
  import fileRoutes from "vinxi/routes";
2
- import { HTTPMethod } from "vinxi/server";
3
3
 
4
4
  interface Route {
5
5
  path: string;
6
6
  id: string;
7
- type: "api" | "page";
8
7
  children?: Route[];
8
+ $component?: any;
9
9
  }
10
10
 
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
11
  declare module "vinxi/routes" {
23
12
  export interface Register {
24
13
  route: {
25
14
  path: string;
26
- type: "api" | "page";
27
15
  children?: Route[];
28
16
  };
29
17
  }
30
18
  }
31
19
 
32
- 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
- }
20
+ export const pageRoutes = defineRoutes(
21
+ (fileRoutes as unknown as Route[]).filter(o => o.$component)
22
+ );
76
23
 
77
24
  function defineRoutes(fileRoutes: Route[]) {
78
25
  function processRoute(routes: Route[], route: Route, id: string, full: string) {
@@ -101,79 +48,35 @@ function defineRoutes(fileRoutes: Route[]) {
101
48
  }, []);
102
49
  }
103
50
 
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);
51
+ export function matchAPIRoute(path: string, method: string) {
52
+ const match = router.lookup(path);
53
+ if (match) {
54
+ const handler = match.route[`$${method}`];
55
+ if (handler === undefined) return;
56
+ return {
57
+ handler,
58
+ params: match.params
59
+ };
131
60
  }
132
-
133
- return expandOptionals(suffix).reduce<string[]>(
134
- (results, expansion) => [...results, ...prefixes.map(p => p + expansion)],
135
- []
136
- );
137
61
  }
138
62
 
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;
63
+ function containsHTTP(route: Route) {
64
+ return route["$GET"] || route["$POST"] || route["$PUT"] || route["$PATCH"] || route["$DELETE"];
65
+ }
145
66
 
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
- }
67
+ const router = createRouter({
68
+ routes: (fileRoutes as unknown as Route[]).reduce((memo, route) => {
69
+ if (!containsHTTP(route)) return memo;
70
+ let path = route.path.replace(/\(.*\)\/?/g, "").replace(/\*([^/]*)/g, (_, m) => `**:${m}`);
71
+ if (/:[^/]*\?/g.test(path)) {
72
+ throw new Error(`Optional parameters are not supported in API routes: ${path}`);
169
73
  }
170
- }
171
-
172
- return {
173
- ...route,
174
- score,
175
- params,
176
- matchSegments,
177
- wildcard
178
- };
179
- }
74
+ if (memo[path]) {
75
+ throw new Error(
76
+ `Duplicate API routes for "${path}" found at "${memo[path].route.path}" and "${route.path}"`
77
+ );
78
+ }
79
+ memo[path] = { route };
80
+ return memo;
81
+ }, {} as Record<string, { route: Route }>)
82
+ });