@solidjs/start 0.5.0 → 0.5.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.
@@ -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"]
@@ -58,6 +56,20 @@ export class SolidStartClientFileRouter extends BaseFileSystemRouter {
58
56
  }
59
57
  }
60
58
 
59
+ const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"];
60
+ function createHTTPHandlers(src, exports) {
61
+ const handlers = {};
62
+ for (const exp of exports) {
63
+ if (HTTP_METHODS.includes(exp.n)) {
64
+ handlers[`$${exp.n}`] = {
65
+ src: src,
66
+ pick: [exp.n]
67
+ };
68
+ }
69
+ }
70
+ return handlers;
71
+ }
72
+
61
73
  export class SolidStartServerFileRouter extends BaseFileSystemRouter {
62
74
  toPath(src) {
63
75
  const routePath = cleanPath(src, this.config)
@@ -79,10 +91,8 @@ export class SolidStartServerFileRouter extends BaseFileSystemRouter {
79
91
 
80
92
  toRoute(src) {
81
93
  let path = this.toPath(src);
82
-
83
94
  if (src.endsWith(".md") || src.endsWith(".mdx")) {
84
95
  return {
85
- type: "page",
86
96
  $component: {
87
97
  src: src,
88
98
  pick: ["$css"]
@@ -95,62 +105,22 @@ export class SolidStartServerFileRouter extends BaseFileSystemRouter {
95
105
 
96
106
  const [_, exports] = analyzeModule(src);
97
107
  const hasRouteConfig = exports.find(e => e.n === "route");
98
- 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) {
99
111
  return {
100
- type: "page",
101
- $component: {
102
- src: src,
103
- pick: ["default", "$css"]
104
- },
112
+ $component: hasDefault
113
+ ? {
114
+ src: src,
115
+ pick: ["default", "$css"]
116
+ }
117
+ : undefined,
105
118
  $$route: hasRouteConfig
106
119
  ? {
107
120
  src: src,
108
121
  pick: ["route"]
109
122
  }
110
123
  : undefined,
111
- path,
112
- filePath: src
113
- };
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))) {
152
- return {
153
- type: "api",
154
124
  ...createHTTPHandlers(src, exports),
155
125
  path,
156
126
  filePath: src
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 { SolidStartAPIFileRouter, 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"];
@@ -37,18 +40,6 @@ function solidStartServerFsRouter(config) {
37
40
  );
38
41
  }
39
42
 
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
46
- },
47
- router,
48
- app
49
- );
50
- }
51
-
52
43
  export function defineConfig(baseConfig = {}) {
53
44
  let { plugins = [], start = {}, ...userConfig } = baseConfig;
54
45
  const extensions = [...DEFAULT_EXTENSIONS, ...(start.extensions || [])];
@@ -86,15 +77,6 @@ export function defineConfig(baseConfig = {}) {
86
77
  dir: "./public",
87
78
  base: "/"
88
79
  },
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
- },
98
80
  {
99
81
  name: "ssr",
100
82
  type: "http",
@@ -209,7 +191,7 @@ export function defineConfig(baseConfig = {}) {
209
191
  serverFunctions.router({
210
192
  handler: normalize(fileURLToPath(new URL("./server-handler.ts", import.meta.url))),
211
193
  runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.ts", import.meta.url))),
212
- // routes: solidStartServerFsRouter({ dir: `${start.appRoot}/routes`, extensions }),
194
+ routes: solidStartServerFsRouter({ dir: `${start.appRoot}/routes`, extensions }),
213
195
  plugins: async () => [
214
196
  config("user", {
215
197
  ...userConfig,
@@ -1,5 +1,4 @@
1
1
  import { getRequestEvent } from "solid-js/web";
2
- //@ts-ignore
3
2
  import { provideRequestEvent } from "solid-js/web/storage";
4
3
  import { cloneEvent } from "../server/fetchEvent";
5
4
 
@@ -9,7 +8,7 @@ export function createServerReference(fn, id, name) {
9
8
  return new Proxy(fn, {
10
9
  get(target, prop, receiver) {
11
10
  if (prop === "url") {
12
- return `${baseURL}/_server?id=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}`;
11
+ return `${baseURL}/_server/?id=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}`;
13
12
  }
14
13
  if (prop === "GET") return receiver;
15
14
  },
@@ -1,5 +1,6 @@
1
1
  /// <reference types="vinxi/types/server" />
2
2
  import { crossSerializeStream, fromJSON, getCrossReferenceHeader } from "seroval";
3
+ // @ts-ignore
3
4
  import {
4
5
  CustomEventPlugin,
5
6
  DOMExceptionPlugin,
@@ -13,11 +14,15 @@ import {
13
14
  URLSearchParamsPlugin
14
15
  } from "seroval-plugins/web";
15
16
  import { sharedConfig } from "solid-js";
16
- /* @ts-ignore */
17
+ import { renderToStringAsync } from "solid-js/web";
17
18
  import { provideRequestEvent } from "solid-js/web/storage";
19
+ import { eventHandler, setHeader, setResponseStatus } from "vinxi/http";
18
20
  import invariant from "vinxi/lib/invariant";
19
- import { eventHandler, setHeader } from "vinxi/server";
20
- import { getFetchEvent, mergeResponseHeaders } from "../server/fetchEvent";
21
+ import { cloneEvent, getFetchEvent, mergeResponseHeaders } from "../server/fetchEvent";
22
+ import { createPageEvent } from "../server/pageEvent";
23
+ // @ts-ignore
24
+ import App from "#start/app";
25
+ import { FetchEvent, PageEvent } from "../server";
21
26
 
22
27
  function createChunk(data) {
23
28
  const bytes = data.length;
@@ -63,8 +68,9 @@ async function handleServerFunction(h3Event) {
63
68
  const event = getFetchEvent(h3Event);
64
69
  const request = event.request;
65
70
 
66
- const serverReference = request.headers.get("x-server-id");
67
- const instance = request.headers.get("x-server-instance");
71
+ const serverReference = request.headers.get("X-Server-Id");
72
+ const instance = request.headers.get("X-Server-Instance");
73
+ const singleFlight = request.headers.has("X-Single-Flight");
68
74
  const url = new URL(request.url);
69
75
  let filepath, name;
70
76
  if (serverReference) {
@@ -76,7 +82,7 @@ async function handleServerFunction(h3Event) {
76
82
  if (!filepath || !name) throw new Error("Invalid request");
77
83
  }
78
84
 
79
- const action = (
85
+ const serverFunction = (
80
86
  await import.meta.env.MANIFEST[import.meta.env.ROUTER_NAME].chunks[filepath].import()
81
87
  )[name];
82
88
  let parsed = [];
@@ -94,13 +100,15 @@ async function handleServerFunction(h3Event) {
94
100
  ) {
95
101
  // workaround for https://github.com/unjs/nitro/issues/1721
96
102
  // (issue only in edge runtimes)
97
- parsed.push(await new Request(request, { ...request, body: h3Event.node.req.body }).formData());
98
- // what should work when #1721 is fixed
103
+ parsed.push(
104
+ await new Request(request, { ...request, body: h3Event.node.req.body }).formData()
105
+ );
106
+ // what should work when #1721 is fixed
99
107
  // parsed.push(await request.formData);
100
108
  } else {
101
109
  // workaround for https://github.com/unjs/nitro/issues/1721
102
110
  // (issue only in edge runtimes)
103
- const tmpReq = new Request(request, { ...request, body: h3Event.node.req.body })
111
+ const tmpReq = new Request(request, { ...request, body: h3Event.node.req.body });
104
112
  // what should work when #1721 is fixed
105
113
  // just use request.json() here
106
114
  parsed = fromJSON(await tmpReq.json(), {
@@ -120,27 +128,23 @@ async function handleServerFunction(h3Event) {
120
128
  }
121
129
  }
122
130
  try {
123
- let result = await provideRequestEvent(event, () => {
131
+ let result = await provideRequestEvent(event, async () => {
124
132
  /* @ts-ignore */
125
133
  sharedConfig.context = { event };
126
- return action(...parsed);
134
+ return serverFunction(...parsed);
127
135
  });
128
136
 
137
+ if (singleFlight && instance) {
138
+ result = await handleSingleFlight(event, result);
139
+ }
140
+
129
141
  // handle responses
130
- if (result instanceof Response) {
131
- if (result.status === 302) {
132
- return new Response(null, {
133
- status: instance ? 204 : 302,
134
- headers: {
135
- Location: result.headers.get("Location")
136
- }
137
- });
138
- }
142
+ if (result instanceof Response && instance) {
139
143
  // forward headers
140
144
  if (result.headers) mergeResponseHeaders(h3Event, result.headers);
141
145
  if ((result as any).customBody) {
142
146
  result = await (result as any).customBody();
143
- } else if (result.body == undefined) result = undefined;
147
+ } else if (result.body == undefined) result = null;
144
148
  }
145
149
 
146
150
  // handle no JS success case
@@ -164,27 +168,59 @@ async function handleServerFunction(h3Event) {
164
168
  }
165
169
  });
166
170
  }
167
- if (typeof result === "string") return new Response(result);
171
+ if (!result || typeof result === "string") return result || null;
168
172
  setHeader(h3Event, "content-type", "text/javascript");
169
173
  return serializeToStream(instance, result);
170
174
  } catch (x) {
171
175
  if (x instanceof Response) {
172
- if (x.status === 302) {
173
- return new Response(null, {
174
- status: instance ? 204 : 302,
175
- headers: {
176
- Location: x.headers.get("Location")
177
- }
178
- });
179
- }
176
+ if (x.status === 302 && !instance) setResponseStatus(h3Event, 302);
180
177
  // forward headers
181
178
  if (x.headers) mergeResponseHeaders(h3Event, x.headers);
182
179
  if ((x as any).customBody) {
183
180
  x = await (x as any).customBody();
184
- } else if (x.body == undefined) x = undefined;
181
+ } else if (x.body == undefined) x = null;
185
182
  }
186
183
  return x;
187
184
  }
188
185
  }
189
186
 
187
+ async function handleSingleFlight(sourceEvent: FetchEvent, result: any) {
188
+ let revalidate;
189
+ let url = new URL(sourceEvent.request.headers.get("referer")).toString();
190
+ if (result instanceof Response) {
191
+ if (result.headers.has("X-Revalidate"))
192
+ revalidate = result.headers.get("X-Revalidate").split(",");
193
+ if (result.headers.has("Location")) url = result.headers.get("Location");
194
+ }
195
+ const event = cloneEvent(sourceEvent) as PageEvent;
196
+ event.request = new Request(url);
197
+ return await provideRequestEvent(event, async () => {
198
+ await createPageEvent(event);
199
+ /* @ts-ignore */
200
+ sharedConfig.context = { event };
201
+ /* @ts-ignore */
202
+ event.router.dataOnly = revalidate || true;
203
+ /* @ts-ignore */
204
+ event.router.previousUrl = sourceEvent.request.headers.get("referer");
205
+ renderToStringAsync(App);
206
+ /* @ts-ignore */
207
+ const body = event.router.data;
208
+ if (body) {
209
+ let containsKey = false;
210
+ for (const key in body) {
211
+ if (body[key] === undefined) delete body[key];
212
+ else containsKey = true;
213
+ }
214
+ if (!containsKey) return result;
215
+ if (!(result instanceof Response)) {
216
+ body["_$value"] = result;
217
+ result = new Response(null, { status: 200 });
218
+ }
219
+ result.customBody = () => body;
220
+ result.headers.set("X-Single-Flight", "true");
221
+ return result;
222
+ }
223
+ });
224
+ }
225
+
190
226
  export default eventHandler(handleServerFunction);
@@ -109,33 +109,32 @@ async function deserializeStream(id, response) {
109
109
 
110
110
  let INSTANCE = 0;
111
111
 
112
- function createRequest(base: string, id: string, instance: string, body: any, contentType?: string) {
112
+ function createRequest(base: string, id: string, instance: string, options: RequestInit) {
113
113
  return fetch(base, {
114
114
  method: "POST",
115
+ ...options,
115
116
  headers: {
116
- "x-server-id": id,
117
- "x-server-instance": instance,
118
- ...(contentType ? { "Content-Type": contentType } : {})
119
- },
120
- body
117
+ ...options.headers,
118
+ "X-Server-Id": id,
119
+ "X-Server-Instance": instance
120
+ }
121
121
  });
122
122
  }
123
123
 
124
- async function fetchServerFunction(base, id, method, args) {
124
+ async function fetchServerFunction(
125
+ base: string,
126
+ id: string,
127
+ options: Omit<RequestInit, "body">,
128
+ args: any[]
129
+ ) {
125
130
  const instance = `server-fn:${INSTANCE++}`;
126
- const response = await (method === "GET"
127
- ? fetch(base + (args.length ? `&args=${JSON.stringify(args)}` : ""), {
128
- headers: {
129
- "x-server-instance": instance
130
- }
131
- })
131
+ const response = await (args.length === 0
132
+ ? createRequest(base, id, instance, options)
132
133
  : args.length === 1 && args[0] instanceof FormData
133
- ? createRequest(base, id, instance, args[0])
134
- : createRequest(
135
- base,
136
- id,
137
- instance,
138
- JSON.stringify(
134
+ ? createRequest(base, id, instance, { ...options, body: args[0] })
135
+ : createRequest(base, id, instance, {
136
+ ...options,
137
+ body: JSON.stringify(
139
138
  await Promise.resolve(
140
139
  toJSONAsync(args, {
141
140
  plugins: [
@@ -153,17 +152,23 @@ async function fetchServerFunction(base, id, method, args) {
153
152
  })
154
153
  )
155
154
  ),
156
- "application/json"
157
- ));
158
-
159
- if (response.headers.get("Location")) throw response;
160
- if (response.headers.get("X-Revalidate")) {
161
- /* @ts-ignore-next-line */
162
- response.customBody = () => {
163
- return deserializeStream(instance, response);
164
- };
165
- throw response;
155
+ headers: { ...options.headers, "Content-Type": "application/json" }
156
+ }));
157
+
158
+ if (
159
+ response.headers.get("Location") ||
160
+ response.headers.get("X-Revalidate") ||
161
+ response.headers.get("X-Single-Flight")
162
+ ) {
163
+ if (response.body) {
164
+ /* @ts-ignore-next-line */
165
+ response.customBody = () => {
166
+ return deserializeStream(instance, response);
167
+ };
168
+ }
169
+ return response;
166
170
  }
171
+
167
172
  const contentType = response.headers.get("Content-Type");
168
173
  let result;
169
174
  if (contentType && contentType.startsWith("text/plain")) {
@@ -184,14 +189,31 @@ export function createServerReference(fn, id, name) {
184
189
  return new Proxy(fn, {
185
190
  get(target, prop, receiver) {
186
191
  if (prop === "url") {
187
- return `${baseURL}/_server?id=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}`;
192
+ return `${baseURL}/_server/?id=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}`;
188
193
  }
189
194
  if (prop === "GET") {
190
- return (...args) => fetchServerFunction(`${baseURL}/_server/?id=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}`, `${id}#${name}`, "GET", args);
195
+ return receiver.withOptions({ method: "GET" });
196
+ }
197
+ if (prop === "withOptions") {
198
+ return (options: RequestInit) => {
199
+ const fn = (...args) => {
200
+ const encodeArgs = options.method && options.method.toUpperCase() === "GET";
201
+ return fetchServerFunction(
202
+ encodeArgs
203
+ ? receiver.url + (args.length ? `&args=${JSON.stringify(args)}` : "")
204
+ : `${baseURL}/_server`,
205
+ `${id}#${name}`,
206
+ options,
207
+ encodeArgs ? [] : args
208
+ );
209
+ };
210
+ fn.url = receiver.url;
211
+ return fn;
212
+ };
191
213
  }
192
214
  },
193
215
  apply(target, thisArg, args) {
194
- return fetchServerFunction(`${baseURL}/_server`, `${id}#${name}`, "POST", args);
216
+ return fetchServerFunction(`${baseURL}/_server`, `${id}#${name}`, {}, args);
195
217
  }
196
218
  });
197
219
  }
@@ -1,8 +1,8 @@
1
1
  import {
2
- H3Event,
3
- defineMiddleware,
4
- sendWebResponse
5
- } from "vinxi/server";
2
+ H3Event,
3
+ defineMiddleware,
4
+ sendWebResponse
5
+ } from "vinxi/http";
6
6
  import { getFetchEvent } from "../server/fetchEvent";
7
7
  import type { FetchEvent } from "../server/types";
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidjs/start",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "author": "Ryan Carniato",
6
6
  "publishConfig": {
@@ -42,7 +42,7 @@
42
42
  }
43
43
  },
44
44
  "devDependencies": {
45
- "solid-js": "^1.8.12",
45
+ "solid-js": "^1.8.14",
46
46
  "vinxi": "^0.2.1"
47
47
  },
48
48
  "dependencies": {
@@ -12,7 +12,7 @@ import {
12
12
  setResponseHeader,
13
13
  setResponseStatus,
14
14
  toWebRequest
15
- } from "vinxi/server";
15
+ } from "vinxi/http";
16
16
  import type { FetchEvent, ResponseStub } from "./types";
17
17
 
18
18
  const fetchEventSymbol = Symbol("fetchEvent");
@@ -40,7 +40,6 @@ export function getFetchEvent(h3Event: H3Event): FetchEvent {
40
40
  if (!h3Event[fetchEventSymbol]) {
41
41
  const fetchEvent = createFetchEvent(h3Event);
42
42
  h3Event[fetchEventSymbol] = fetchEvent;
43
- // @ts-ignore
44
43
  }
45
44
 
46
45
  return h3Event[fetchEventSymbol];
@@ -75,21 +74,21 @@ class HeaderProxy {
75
74
  return Array.isArray(cookies) ? cookies : [cookies];
76
75
  }
77
76
  forEach(fn: (value: string, key: string, object: Headers) => void) {
78
- return this.entries().forEach(([key, value]) => fn(value, key, this));
77
+ return Object.entries<string | string[]>(getResponseHeaders(this.event)).forEach(([key, value]) => fn(Array.isArray(value) ? value.join(", ") : value, key, this));
79
78
  }
80
79
  entries() {
81
- return getResponseHeaders(this.event).map(([key, value]) => [
80
+ return Object.entries<string | string[]>(getResponseHeaders(this.event)).map(([key, value]) => [
82
81
  key,
83
82
  Array.isArray(value) ? value.join(", ") : value
84
- ]);
83
+ ] as [string, string])[Symbol.iterator]();
85
84
  }
86
85
  keys() {
87
- return getResponseHeaders(this.event).map(([key]) => key);
86
+ return Object.keys(getResponseHeaders(this.event))[Symbol.iterator]();
88
87
  }
89
88
  values() {
90
- return getResponseHeaders(this.event).map(([key, value]) =>
89
+ return Object.values<string | string[]>(getResponseHeaders(this.event)).map((value) =>
91
90
  Array.isArray(value) ? value.join(", ") : value
92
- );
91
+ )[Symbol.iterator]();
93
92
  }
94
93
  [Symbol.iterator]() {
95
94
  return this.entries()[Symbol.iterator]();
package/server/handler.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { sharedConfig } from "solid-js";
2
2
  import { renderToStream, renderToString } from "solid-js/web";
3
- /* @ts-ignore */
4
3
  import { provideRequestEvent } from "solid-js/web/storage";
5
4
  import {
6
5
  EventHandlerObject,
@@ -10,10 +9,11 @@ import {
10
9
  sendRedirect,
11
10
  setHeader,
12
11
  setResponseStatus
13
- } from "vinxi/server";
12
+ } from "vinxi/http";
13
+ import { matchAPIRoute } from "../shared/routes";
14
14
  import { getFetchEvent } from "./fetchEvent";
15
15
  import { createPageEvent } from "./pageEvent";
16
- import { FetchEvent, PageEvent } from "./types";
16
+ import type { APIEvent, FetchEvent, PageEvent } from "./types";
17
17
 
18
18
  export function createBaseHandler(
19
19
  fn: (context: PageEvent) => unknown,
@@ -36,6 +36,23 @@ export function createBaseHandler(
36
36
  const mode = import.meta.env.START_SSR;
37
37
 
38
38
  return provideRequestEvent(event, async () => {
39
+ // api
40
+ const match = matchAPIRoute(new URL(event.request.url).pathname, event.request.method);
41
+ if (match) {
42
+ const mod = await match.handler.import();
43
+ const fn = mod[event.request.method];
44
+ (event as APIEvent).params = match.params;
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
+ }
54
+ }
55
+
39
56
  // render
40
57
  const context = await createPageEvent(event);
41
58
  if (mode === "sync") {
@@ -65,7 +82,7 @@ export function createBaseHandler(
65
82
  } else cloned.onCompleteShell = handleShellCompleteRedirect(context, e);
66
83
  const stream = renderToStream(() => {
67
84
  (sharedConfig.context as any).event = context;
68
- return fn(context)
85
+ return fn(context);
69
86
  }, cloned);
70
87
  if (context.response && context.response.headers.get("Location")) {
71
88
  return sendRedirect(event, context.response.headers.get("Location"));
@@ -86,7 +103,7 @@ function handleShellCompleteRedirect(context: PageEvent, e: H3Event<EventHandler
86
103
  setResponseStatus(e, 302);
87
104
  setHeader(e, "Location", context.response.headers.get("Location"));
88
105
  }
89
- }
106
+ };
90
107
  }
91
108
 
92
109
  function handleStreamCompleteRedirect(context: PageEvent) {
@@ -101,4 +118,4 @@ export function createHandler(
101
118
  options: Parameters<typeof createBaseHandler>[1] = {}
102
119
  ) {
103
120
  return createBaseHandler(fn, { ...options, createPageEvent });
104
- }
121
+ }
@@ -1,7 +1,7 @@
1
1
  import {
2
- getCookie,
3
- setCookie
4
- } from "vinxi/server";
2
+ getCookie,
3
+ setCookie
4
+ } from "vinxi/http";
5
5
  import { createRoutes } from "../shared/FileRoutes";
6
6
  import { FetchEvent, PageEvent } from "./types";
7
7
 
package/server/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { JSX } from "solid-js";
2
- import { HTTPEventSymbol, type EventHandlerRequest, type 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
 
@@ -1,24 +1,13 @@
1
- import {
2
- ErrorBoundary as DefaultErrorBoundary,
3
- ParentProps,
4
- } from "solid-js";
1
+ import { ErrorBoundary as DefaultErrorBoundary, type ParentProps } from "solid-js";
5
2
  import { HttpStatusCode } from "./HttpStatusCode";
6
3
  import { DevOverlay } from "./dev-overlay";
7
4
 
8
5
  export function ErrorBoundary(props: ParentProps) {
9
6
  if (import.meta.env.DEV) {
10
- return (
11
- <DevOverlay>
12
- {props.children}
13
- </DevOverlay>
14
- );
7
+ return <DevOverlay>{props.children}</DevOverlay>;
15
8
  }
16
9
  return (
17
- <DefaultErrorBoundary
18
- fallback={(
19
- <HttpStatusCode code={500} />
20
- )}
21
- >
10
+ <DefaultErrorBoundary fallback={<HttpStatusCode code={500} />}>
22
11
  {props.children}
23
12
  </DefaultErrorBoundary>
24
13
  );
package/shared/routes.ts CHANGED
@@ -1,23 +1,25 @@
1
+ import { createRouter } from "radix3";
1
2
  import fileRoutes from "vinxi/routes";
2
3
 
3
4
  interface Route {
4
5
  path: string;
5
6
  id: string;
6
- type: "page";
7
7
  children?: Route[];
8
+ $component?: any;
8
9
  }
9
10
 
10
11
  declare module "vinxi/routes" {
11
12
  export interface Register {
12
13
  route: {
13
14
  path: string;
14
- type: "api" | "page";
15
15
  children?: Route[];
16
16
  };
17
17
  }
18
18
  }
19
19
 
20
- export const pageRoutes = defineRoutes((fileRoutes as unknown as Route[]).filter(o => o.type === "page"));
20
+ export const pageRoutes = defineRoutes(
21
+ (fileRoutes as unknown as Route[]).filter(o => o.$component)
22
+ );
21
23
 
22
24
  function defineRoutes(fileRoutes: Route[]) {
23
25
  function processRoute(routes: Route[], route: Route, id: string, full: string) {
@@ -46,3 +48,35 @@ function defineRoutes(fileRoutes: Route[]) {
46
48
  }, []);
47
49
  }
48
50
 
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
+ };
60
+ }
61
+ }
62
+
63
+ function containsHTTP(route: Route) {
64
+ return route["$GET"] || route["$POST"] || route["$PUT"] || route["$PATCH"] || route["$DELETE"];
65
+ }
66
+
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}`);
73
+ }
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
+ });
@@ -1,64 +0,0 @@
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
- }