@solidjs/start 0.4.0-alpha.1 → 0.4.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # Solid Start
2
+
3
+ This is the SolidStart framework and CLI.
4
+
5
+ The quickest way to get started:
6
+
7
+ ```bash
8
+ npm init solid@latest my-app
9
+ cd my-app
10
+ npm install
11
+ npm run dev
12
+ ```
@@ -0,0 +1,14 @@
1
+ import { InlineConfig } from "vite";
2
+
3
+ type SolidStartInlineConfig = Omit<InlineConfig, "router"> & {
4
+ start?: {
5
+ ssr?: boolean | "async",
6
+ extensions?: string[],
7
+ server?: any,
8
+ appRoot?: string,
9
+ middleware?: string,
10
+ //islands?: boolean TODO: Get PROD bugs working probably missing release
11
+ }
12
+ }
13
+
14
+ export declare function defineConfig(baseConfig: SolidStartInlineConfig)
package/config/index.js CHANGED
@@ -37,20 +37,25 @@ function solidStartServerFsRouter(config) {
37
37
  }
38
38
 
39
39
  export function defineConfig(baseConfig = {}) {
40
- let { plugins = [], ssr = true, start = {}, ...userConfig } = baseConfig;
40
+ let { plugins = [], start = {}, ...userConfig } = baseConfig;
41
41
  const extensions = [...DEFAULT_EXTENSIONS, ...(start.extensions || [])];
42
42
  start = defu(start, {
43
43
  appRoot: "./src",
44
- ssr,
44
+ ssr: true,
45
45
  islands: false,
46
- serverPlugins: []
46
+ solid: {}
47
47
  });
48
+ let server = start.server;
49
+ if (!start.ssr) {
50
+ server = { ...server, prerender: { routes: ["/"] } };
51
+ }
48
52
 
49
53
  return createApp({
50
54
  server: {
51
55
  compressPublicAssets: {
52
56
  brotli: true
53
- }
57
+ },
58
+ ...server
54
59
  },
55
60
  routers: [
56
61
  {
@@ -75,7 +80,7 @@ export function defineConfig(baseConfig = {}) {
75
80
  runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.jsx", import.meta.url)))
76
81
  }),
77
82
  start.islands ? serverComponents.server() : null,
78
- solid({ ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
83
+ solid({ ...start.solid, ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
79
84
  config("app-server", {
80
85
  resolve: {
81
86
  alias: {
@@ -85,13 +90,15 @@ export function defineConfig(baseConfig = {}) {
85
90
  ? {
86
91
  "@solidjs/start/server": "@solidjs/start/server/spa"
87
92
  }
88
- : {})
93
+ : {}),
94
+ ...userConfig.resolve?.alias
89
95
  }
90
96
  },
91
97
  define: {
92
98
  "import.meta.env.START_ISLANDS": JSON.stringify(start.islands),
93
99
  "import.meta.env.SSR": JSON.stringify(true),
94
- "import.meta.env.START_SSR": JSON.stringify(start.ssr)
100
+ "import.meta.env.START_SSR": JSON.stringify(start.ssr),
101
+ ...userConfig.define
95
102
  }
96
103
  })
97
104
  ]
@@ -114,7 +121,7 @@ export function defineConfig(baseConfig = {}) {
114
121
  runtime: normalize(fileURLToPath(new URL("./server-runtime.jsx", import.meta.url)))
115
122
  }),
116
123
  start.islands ? serverComponents.client() : null,
117
- solid({ ssr: start.ssr, extensions: extensions.map(ext => `.${ext}`) }),
124
+ solid({ ...start.solid, ssr: start.ssr, extensions: extensions.map(ext => `.${ext}`) }),
118
125
  config("app-client", {
119
126
  resolve: {
120
127
  alias: {
@@ -129,13 +136,15 @@ export function defineConfig(baseConfig = {}) {
129
136
  ? {
130
137
  "@solidjs/start/client": "@solidjs/start/client/spa"
131
138
  }
132
- : {})
139
+ : {}),
140
+ ...userConfig.resolve?.alias
133
141
  }
134
142
  },
135
143
  define: {
136
144
  "import.meta.env.START_ISLANDS": JSON.stringify(start.islands),
137
145
  "import.meta.env.SSR": JSON.stringify(false),
138
- "import.meta.env.START_SSR": JSON.stringify(start.ssr)
146
+ "import.meta.env.START_SSR": JSON.stringify(start.ssr),
147
+ ...userConfig.define
139
148
  }
140
149
  })
141
150
  ],
@@ -148,7 +157,7 @@ export function defineConfig(baseConfig = {}) {
148
157
  plugins: () => [
149
158
  config("user", userConfig),
150
159
  ...plugins,
151
- solid({ ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
160
+ solid({ ...start.solid, ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
152
161
  config("app-server", {
153
162
  resolve: {
154
163
  alias: {
@@ -158,13 +167,15 @@ export function defineConfig(baseConfig = {}) {
158
167
  ? {
159
168
  "@solidjs/start/server": "@solidjs/start/server/spa"
160
169
  }
161
- : {})
170
+ : {}),
171
+ ...userConfig.resolve?.alias
162
172
  }
163
173
  },
164
174
  define: {
165
175
  "import.meta.env.START_ISLANDS": JSON.stringify(start.islands),
166
176
  "import.meta.env.SSR": JSON.stringify(true),
167
- "import.meta.env.START_SSR": JSON.stringify(start.ssr)
177
+ "import.meta.env.START_SSR": JSON.stringify(start.ssr),
178
+ ...userConfig.define
168
179
  }
169
180
  })
170
181
  ],
@@ -1,3 +1,7 @@
1
+ import { getRequestEvent } from "solid-js/web";
2
+ import { provideRequestEvent } from "solid-js/web/storage";
3
+ import { cloneEvent } from "../server";
4
+
1
5
  export function createServerReference(fn, id, name) {
2
6
  return new Proxy(fn, {
3
7
  get(target, prop, receiver) {
@@ -6,7 +10,13 @@ export function createServerReference(fn, id, name) {
6
10
  }
7
11
  },
8
12
  apply(target, thisArg, args) {
9
- return fn.apply(thisArg, args);
13
+ const ogEvt = getRequestEvent();
14
+ if (!ogEvt) throw new Error("Cannot call server function outside of a request");
15
+ const evt = cloneEvent(ogEvt);
16
+ evt.serverOnly = true;
17
+ return provideRequestEvent(evt, () => {
18
+ return fn.apply(thisArg, args);
19
+ });
10
20
  }
11
21
  });
12
22
  }
@@ -53,13 +53,19 @@ async function handleServerFunction(event) {
53
53
  const action = (
54
54
  await import.meta.env.MANIFEST[import.meta.env.ROUTER_NAME].chunks[filepath].import()
55
55
  )[name];
56
- let parsed;
56
+ let parsed = [];
57
+
58
+ // grab bound arguments from url when no JS
59
+ if (!instance) {
60
+ const args = url.searchParams.get("args");
61
+ if (args) JSON.parse(args).forEach(arg => parsed.push(arg));
62
+ }
57
63
  const contentType = getHeader(event, "content-type");
58
64
  if (
59
65
  contentType.startsWith("multipart/form-data") ||
60
66
  contentType.startsWith("application/x-www-form-urlencoded")
61
67
  ) {
62
- parsed = [await readFormData(event)];
68
+ parsed.push(await readFormData(event));
63
69
  } else {
64
70
  parsed = fromJSON(await readBody(event));
65
71
  }
@@ -70,21 +76,16 @@ async function handleServerFunction(event) {
70
76
  if (!instance) {
71
77
  const isError = result instanceof Error;
72
78
  const refererUrl = new URL(getHeader(event, "referer"));
73
- if (result !== undefined)
74
- refererUrl.searchParams.set(
75
- "form",
76
- JSON.stringify({
77
- url: url.pathname + url.search,
78
- result: isError ? result.message : result,
79
- error: isError,
80
- entries: [...parsed[0].entries()]
81
- })
82
- );
83
- else refererUrl.searchParams.delete("form");
84
79
  return new Response(null, {
85
80
  status: 302,
86
81
  headers: {
87
- Location: refererUrl.pathname + refererUrl.search
82
+ Location: refererUrl.toString(),
83
+ ...(result ? { "Set-Cookie": `flash=${JSON.stringify({
84
+ url: url.pathname + encodeURIComponent(url.search),
85
+ result: isError ? result.message : result,
86
+ error: isError,
87
+ input: [...parsed.slice(0, -1), [...parsed[parsed.length-1].entries()]]
88
+ })}; Secure; HttpOnly;`} : {})
88
89
  }
89
90
  });
90
91
  }
@@ -69,7 +69,7 @@ async function fetchServerFunction(base, id, args) {
69
69
  const instance = `server-fn:${INSTANCE++}`;
70
70
  const response = await (args.length === 1 && args[0] instanceof FormData
71
71
  ? createRequest(base, id, instance, args[0])
72
- : createRequest(base, id, instance, JSON.stringify(await toJSONAsync(args)), "application/json"));
72
+ : createRequest(base, id, instance, JSON.stringify(await Promise.resolve(toJSONAsync(args))), "application/json"));
73
73
 
74
74
  if (response.headers.get("Location")) throw response;
75
75
  if (response.headers.get("X-Revalidate")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidjs/start",
3
- "version": "0.4.0-alpha.1",
3
+ "version": "0.4.0-alpha.10",
4
4
  "type": "module",
5
5
  "author": "Ryan Carniato",
6
6
  "publishConfig": {
@@ -13,7 +13,7 @@
13
13
  "shared",
14
14
  "*.d.ts"
15
15
  ],
16
- "types": "./index.tsx",
16
+ "types": "./shared/index.tsx",
17
17
  "exports": {
18
18
  ".": "./shared/index.tsx",
19
19
  "./config": "./config/index.js",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "devDependencies": {
41
41
  "solid-js": "^1.8.7",
42
- "vinxi": "0.0.48"
42
+ "vinxi": "0.0.52"
43
43
  },
44
44
  "dependencies": {
45
45
  "@vinxi/plugin-directives": "0.0.44",
@@ -48,6 +48,9 @@
48
48
  "defu": "^6.1.2",
49
49
  "seroval": "^0.14.1",
50
50
  "vite-plugin-inspect": "^0.7.33",
51
- "vite-plugin-solid": "^2.7.0"
51
+ "vite-plugin-solid": "^2.8.0"
52
+ },
53
+ "scripts": {
54
+ "typecheck": "tsc --noEmit --checkJs false --skipLibCheck"
52
55
  }
53
56
  }
@@ -1,7 +1,14 @@
1
1
  // @ts-ignore
2
2
  import App from "#start/app";
3
3
  import type { Component } from "solid-js";
4
- import { Hydration, HydrationScript, NoHydration, getRequestEvent, ssr } from "solid-js/web";
4
+ import {
5
+ Hydration,
6
+ HydrationScript,
7
+ NoHydration,
8
+ getRequestEvent,
9
+ ssr,
10
+ useAssets
11
+ } from "solid-js/web";
5
12
  import { ErrorBoundary } from "../shared/ErrorBoundary";
6
13
  import { renderAsset } from "./renderAsset";
7
14
  import type { DocumentComponentProps } from "./types";
@@ -10,6 +17,32 @@ const docType = ssr("<!DOCTYPE html>");
10
17
 
11
18
  export function StartServer(props: { document: Component<DocumentComponentProps> }) {
12
19
  const context = getRequestEvent() as any;
20
+
21
+ let assets = [];
22
+ Promise.resolve().then(async () => {
23
+ let current = context.routes;
24
+ if (context.routerMatches && context.routerMatches[0]) {
25
+ for (let i = 0; i < context.routerMatches[0].length; i++) {
26
+ const match = context.routerMatches[0][i];
27
+ if (match.metadata && match.metadata.filesystem) {
28
+ const segment = current.find(r => r.path === match.path);
29
+ const part = import.meta.env.MANIFEST["client"].inputs[segment["$component"].src];
30
+ const asset = await part.assets();
31
+ assets.push.apply(assets, asset);
32
+ current = segment.children;
33
+ }
34
+ }
35
+ }
36
+ // dedupe assets
37
+ assets = [...new Map(assets.map(item => [item.attrs.key, item])).values()].filter(
38
+ asset =>
39
+ asset.attrs.rel === "modulepreload" &&
40
+ !context.assets.find(a => a.attrs.key === asset.attrs.key)
41
+ );
42
+ });
43
+
44
+ useAssets(() => assets.length ? assets.map(m => renderAsset(m)) : undefined);
45
+
13
46
  return (
14
47
  <NoHydration>
15
48
  {docType as unknown as any}
@@ -1,17 +1,20 @@
1
1
  import { renderToStream } from "solid-js/web";
2
+ /* @ts-ignore */
2
3
  import { provideRequestEvent } from "solid-js/web/storage";
3
4
  import {
4
- eventHandler,
5
5
  EventHandlerObject,
6
6
  EventHandlerRequest,
7
7
  H3Event,
8
+ HTTPMethod,
9
+ eventHandler,
10
+ sendRedirect,
8
11
  setHeader,
9
12
  setResponseStatus
10
13
  } from "vinxi/server";
11
- import { apiRoutes } from "../shared/routes";
14
+ import { matchAPIRoute } from "../shared/routes";
12
15
  import { getFetchEvent } from "./middleware";
13
16
  import { createPageEvent } from "./page-event";
14
- import { FetchEvent, PageEvent } from "./types";
17
+ import { APIEvent, FetchEvent, PageEvent } from "./types";
15
18
 
16
19
  export function createHandler(
17
20
  fn: (context: PageEvent) => unknown,
@@ -34,18 +37,16 @@ export function createHandler(
34
37
 
35
38
  return provideRequestEvent(event, async () => {
36
39
  // api
37
- const match = apiRoutes.find(
38
- route =>
39
- route[`$${event.request.method}`] && new URL(event.request.url).pathname === route.path
40
- );
40
+ const match = matchAPIRoute(new URL(event.request.url).pathname, event.request.method as HTTPMethod)
41
41
  if (match) {
42
- const mod = await match[`$${event.request.method}`].import();
42
+ const mod = await match.handler.import();
43
43
  const fn = mod[event.request.method];
44
- const result = await fn(event);
45
- return result;
44
+ (event as APIEvent).params = match.params;
45
+ return await fn(event);
46
46
  }
47
47
 
48
48
  // render stream
49
+ const doAsync = import.meta.env.START_SSR === "async";
49
50
  const context = await createPageEvent(event);
50
51
  let cloned = { ...options };
51
52
  if (cloned.onCompleteAll) {
@@ -64,9 +65,13 @@ export function createHandler(
64
65
  } else cloned.onCompleteShell = handleShellCompleteRedirect(context, e);
65
66
  const stream = renderToStream(() => fn(context), cloned);
66
67
  if (context.response && context.response.headers.get("Location")) {
67
- return event.redirect(context.response.headers.get("Location"));
68
+ return sendRedirect(event, context.response.headers.get("Location"));
68
69
  }
69
- return { pipeTo: stream.pipeTo };
70
+ if (doAsync) return stream;
71
+ // fix cloudflare streaming
72
+ const { writable, readable } = new TransformStream();
73
+ stream.pipeTo(writable);
74
+ return readable;
70
75
  });
71
76
  }
72
77
  });
package/server/index.tsx CHANGED
@@ -1,8 +1,9 @@
1
+ export * from "vinxi/server";
1
2
  export { StartServer } from "./StartServer";
2
3
  export * from "./middleware";
3
- export type { DocumentComponentProps } from "./types";
4
- import { createPageEvent } from "./page-event";
4
+ export * from "./types";
5
5
  import { createHandler as createBaseHandler } from "./handler";
6
+ import { createPageEvent } from "./page-event";
6
7
  import type { PageEvent } from "./types";
7
8
 
8
9
  export function createHandler(
@@ -1,45 +1,37 @@
1
1
  import {
2
- appendResponseHeader,
3
2
  defineMiddleware,
4
3
  EventHandlerRequest,
5
4
  getRequestIP,
6
- getResponseHeader,
7
- getResponseStatus,
8
5
  H3Event,
9
- removeResponseHeader,
10
- sendRedirect,
11
6
  sendWebResponse,
12
- setResponseHeader,
13
- setResponseStatus,
14
7
  toWebRequest
15
8
  } from "vinxi/server";
16
9
  import { FetchEvent } from "./types";
17
10
 
18
- export * from "vinxi/server";
19
-
20
11
  const h3EventSymbol = Symbol("h3Event");
21
12
  const fetchEventSymbol = Symbol("fetchEvent");
13
+ const eventTraps = {
14
+ get(target, prop) {
15
+ if (prop === fetchEventSymbol) return target;
16
+ return target[prop] ?? target[h3EventSymbol][prop];
17
+ }
18
+ };
22
19
 
23
20
  export function createFetchEvent(event: H3Event<EventHandlerRequest>): FetchEvent {
24
- return {
25
- request: toWebRequest(event),
26
- clientAddress: getRequestIP(event),
27
- locals: {},
28
- redirect: (url, status) => sendRedirect(event, url, status),
29
- getResponseStatus: () => getResponseStatus(event),
30
- setResponseStatus: (code, text) => setResponseStatus(event, code, text),
31
- getResponseHeader: name => getResponseHeader(event, name),
32
- setResponseHeader: (name, value) => setResponseHeader(event, name, value),
33
- appendResponseHeader: (name, value) => appendResponseHeader(event, name, value),
34
- removeResponseHeader: name => removeResponseHeader(event, name),
35
- // @ts-ignore
36
- [h3EventSymbol]: event
37
- };
21
+ return new Proxy(
22
+ {
23
+ request: toWebRequest(event),
24
+ clientAddress: getRequestIP(event),
25
+ locals: {},
26
+ // @ts-ignore
27
+ [h3EventSymbol]: event
28
+ },
29
+ eventTraps
30
+ ) as unknown as FetchEvent;
38
31
  }
39
32
 
40
- export function getH3Event(fetchEvent: FetchEvent): H3Event<EventHandlerRequest> {
41
- // @ts-ignore
42
- return fetchEvent[h3EventSymbol];
33
+ export function cloneEvent<T extends FetchEvent>(fetchEvent: T): T {
34
+ return new Proxy({ ...fetchEvent[fetchEventSymbol] }, eventTraps);
43
35
  }
44
36
 
45
37
  export function getFetchEvent(h3Event: H3Event): FetchEvent {
@@ -1,10 +1,33 @@
1
+ import {
2
+ appendResponseHeader,
3
+ getCookie,
4
+ getResponseHeader,
5
+ removeResponseHeader,
6
+ setCookie,
7
+ setResponseHeader,
8
+ setResponseStatus
9
+ } from "vinxi/server";
1
10
  import { createRoutes } from "../shared/FileRoutes";
2
11
  import { FetchEvent, PageEvent } from "./types";
3
12
 
13
+ function initFromFlash(ctx: FetchEvent) {
14
+ const flash = getCookie(ctx, "flash");
15
+ if (!flash) return;
16
+ let param = JSON.parse(flash);
17
+ if (!param || !param.result) return [];
18
+ const input = [...param.input.slice(0, -1), new Map(param.input[param.input.length - 1])];
19
+ setCookie(ctx, "flash", "", { maxAge: 0 });
20
+ return {
21
+ url: param.url,
22
+ result: param.error ? new Error(param.result) : param.result,
23
+ input
24
+ };
25
+ }
26
+
4
27
  export async function createPageEvent(ctx: FetchEvent) {
5
28
  const clientManifest = import.meta.env.MANIFEST["client"];
6
29
  const serverManifest = import.meta.env.MANIFEST["ssr"];
7
- ctx.setResponseHeader("Content-Type", "text/html");
30
+ setResponseHeader(ctx, "Content-Type", "text/html");
8
31
  // const prevPath = ctx.request.headers.get("x-solid-referrer");
9
32
  // const mutation = ctx.request.headers.get("x-solid-mutation") === "true";
10
33
  const pageEvent: PageEvent = Object.assign(ctx, {
@@ -15,7 +38,36 @@ export async function createPageEvent(ctx: FetchEvent) {
15
38
  ? await serverManifest.inputs[serverManifest.handler].assets()
16
39
  : [])
17
40
  ],
41
+ initialSubmission: initFromFlash(ctx),
18
42
  routes: createRoutes(),
43
+ components: {
44
+ status: props => {
45
+ setResponseStatus(ctx, props.code, props.text);
46
+ return () => setResponseStatus(ctx, 200);
47
+ },
48
+ header: props => {
49
+ if (props.append) {
50
+ appendResponseHeader(ctx, props.name, props.value);
51
+ } else {
52
+ setResponseHeader(ctx, props.name, props.value);
53
+ }
54
+
55
+ return () => {
56
+ const value = getResponseHeader(ctx, props.name);
57
+
58
+ // Todo: review this logic still the same in H3
59
+ // H3 supports arrays so its possible there is no need to split..
60
+ // typeof check here is to guard but ultimately may be unneeded
61
+ if (value && typeof value === "string") {
62
+ const values = value.split(", ");
63
+ const index = values.indexOf(props.value);
64
+ index !== -1 && values.splice(index, 1);
65
+ if (values.length) setResponseHeader(ctx, props.name, values.join(", "));
66
+ else removeResponseHeader(ctx, props.name);
67
+ }
68
+ };
69
+ }
70
+ },
19
71
  // prevUrl: prevPath || "",
20
72
  // mutation: mutation,
21
73
  // $type: FETCH_EVENT,
@@ -14,6 +14,7 @@ export function StartServer(props) {
14
14
  scripts={
15
15
  <>
16
16
  <script innerHTML={`window.manifest = ${JSON.stringify(context.manifest)}`} />
17
+ <script>$R = [];</script>
17
18
  <script
18
19
  type="module"
19
20
  src={
@@ -1,5 +1,5 @@
1
1
  import { createHandler as createBaseHandler } from "../handler";
2
- import { FetchEvent, FETCH_EVENT, PageEvent } from "../types";
2
+ import { FetchEvent, PageEvent } from "../types";
3
3
 
4
4
  export function createHandler(
5
5
  fn: (context: PageEvent) => unknown,
@@ -14,9 +14,9 @@ export async function createPageEvent(ctx: FetchEvent) {
14
14
  manifest: await clientManifest.json(),
15
15
  assets: [...(await clientManifest.inputs[clientManifest.handler].assets())],
16
16
  routes: [],
17
- prevUrl: "",
18
- mutation: false,
19
- $type: FETCH_EVENT,
17
+ // prevUrl: "",
18
+ // mutation: false,
19
+ // $type: FETCH_EVENT,
20
20
  $islands: new Set<string>()
21
21
  });
22
22
 
@@ -1,4 +1,6 @@
1
+ export * from "vinxi/server";
1
2
  export * from "../handler";
2
3
  export * from "../middleware";
4
+ export * from "../types";
3
5
  export { StartServer } from "./StartServer";
4
6
 
package/server/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { JSX } from "solid-js";
2
+ import { EventHandlerRequest, H3Event } from "vinxi/server";
2
3
 
3
4
  // export const FETCH_EVENT = "$FETCH";
4
5
 
@@ -15,17 +16,10 @@ export type ContextMatches = {
15
16
  params: unknown;
16
17
  };
17
18
 
18
- export interface FetchEvent {
19
+ export interface FetchEvent extends H3Event<EventHandlerRequest> {
19
20
  request: Request;
20
21
  clientAddress: string;
21
22
  locals: Record<string, unknown>;
22
- redirect(url: string, status?: number): void;
23
- getResponseStatus(): number;
24
- setResponseStatus(code: number, text?: string): void;
25
- getResponseHeader(name: string): string | number | string[];
26
- setResponseHeader(name: string, value: string): void;
27
- appendResponseHeader(name: string, value: string): void;
28
- removeResponseHeader(name: string): void;
29
23
  }
30
24
  export interface PageEvent extends FetchEvent {
31
25
  manifest: any;
@@ -37,3 +31,17 @@ export interface PageEvent extends FetchEvent {
37
31
  // mutation: boolean;
38
32
  response?: Response;
39
33
  }
34
+
35
+ export interface APIEvent extends FetchEvent {
36
+ params: { [key: string]: string };
37
+ }
38
+
39
+ export interface APIHandler {
40
+ (event: APIEvent): Promise<any>;
41
+ }
42
+
43
+ declare module "solid-js/web" {
44
+ interface RequestEvent extends FetchEvent {
45
+ serverOnly?: boolean;
46
+ }
47
+ }
@@ -2,6 +2,7 @@
2
2
  import { getRequestEvent, isServer } from "solid-js/web";
3
3
  import lazyRoute from "./lazyRoute";
4
4
 
5
+ import { PageEvent } from "../server";
5
6
  import { pageRoutes as routeConfigs } from "./routes";
6
7
 
7
8
  export function createRoutes() {
@@ -9,6 +10,7 @@ export function createRoutes() {
9
10
  return {
10
11
  ...route,
11
12
  ...(route.$$route ? route.$$route.require().route : undefined),
13
+ metadata: { ...(route.$$route ? route.$$route.require().route.metadata : {}), filesystem: true },
12
14
  component: lazyRoute(
13
15
  route.$component,
14
16
  import.meta.env.START_ISLANDS
@@ -25,5 +27,5 @@ export function createRoutes() {
25
27
 
26
28
  let routes;
27
29
  export const FileRoutes = () => {
28
- return isServer ? getRequestEvent().routes : (routes || (routes = createRoutes()));
30
+ return isServer ? (getRequestEvent() as PageEvent).routes : routes || (routes = createRoutes());
29
31
  };
@@ -3,27 +3,8 @@ import { getRequestEvent, isServer } from "solid-js/web";
3
3
 
4
4
  export function HttpHeader(props: { name: string; value: string; append?: boolean }) {
5
5
  if (isServer) {
6
- const pageContext = getRequestEvent();
7
- if (props.append) {
8
- pageContext!.appendResponseHeader(props.name, props.value);
9
- } else {
10
- pageContext!.setResponseHeader(props.name, props.value);
11
- }
12
-
13
- onCleanup(() => {
14
- const value = pageContext!.getResponseHeader(props.name);
15
-
16
- // Todo: review this logic still the same in H3
17
- // H3 supports arrays so its possible there is no need to split..
18
- // typeof check here is to guard but ultimately may be unneeded
19
- if (value && typeof value === "string") {
20
- const values = value.split(", ");
21
- const index = values.indexOf(props.value);
22
- index !== -1 && values.splice(index, 1);
23
- if (values.length) pageContext!.setResponseHeader(props.name, values.join(", "));
24
- else pageContext!.removeResponseHeader(props.name);
25
- }
26
- });
6
+ const event = getRequestEvent();
7
+ if (event) onCleanup((event as any).components.header(props));
27
8
  }
28
9
 
29
10
  return null;
@@ -3,11 +3,8 @@ import { getRequestEvent, isServer } from "solid-js/web";
3
3
 
4
4
  export function HttpStatusCode(props: { code: number, text?: string }) {
5
5
  if (isServer) {
6
- const context = getRequestEvent();
7
- context!.setResponseStatus(props.code, props.text);
8
- onCleanup(() => {
9
- context!.setResponseStatus(200);
10
- });
6
+ const event = getRequestEvent();
7
+ if (event) onCleanup((event as any).components.status(props));
11
8
  }
12
9
  return null;
13
10
  }
@@ -47,9 +47,10 @@ export default function lazyRoute(
47
47
  const mod = await component.import();
48
48
  const Component = mod[exported];
49
49
  let assets = await clientManifest.inputs?.[component.src].assets();
50
+ const styles = assets.filter((asset) => asset.tag === "style" || asset.attrs.rel === "stylesheet");
50
51
  const Comp = (props) => {
51
52
  return [
52
- ...assets.map((asset) => renderAsset(asset)),
53
+ ...styles.map((asset) => renderAsset(asset)),
53
54
  createComponent(Component, props),
54
55
  ];
55
56
  };
package/shared/routes.ts CHANGED
@@ -1,11 +1,24 @@
1
- import fileRoutes, { RouteModule } from "vinxi/routes";
1
+ import fileRoutes from "vinxi/routes";
2
+ import { HTTPMethod } from "vinxi/server";
2
3
 
3
4
  interface Route {
4
5
  path: string;
6
+ id: string;
5
7
  type: "api" | "page";
6
8
  children?: Route[];
7
9
  }
8
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
+
9
22
  declare module "vinxi/routes" {
10
23
  export interface Register {
11
24
  route: {
@@ -16,23 +29,65 @@ declare module "vinxi/routes" {
16
29
  }
17
30
  }
18
31
 
19
- const defineRoutes = (fileRoutes: RouteModule[]) => {
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.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
+
77
+ function defineRoutes(fileRoutes: Route[]) {
20
78
  function processRoute(routes: Route[], route: Route, id: string, full: string) {
21
79
  const parentRoute = Object.values(routes).find(o => {
22
- // if (o.id.endsWith("/index")) {
23
- // return false;
24
- // }
25
- return id.startsWith(o.path + "/");
80
+ return id.startsWith(o.id + "/");
26
81
  });
27
82
 
28
83
  if (!parentRoute) {
29
- routes.push({ ...route, path: id });
84
+ routes.push({ ...route, id, path: id.replace(/\/\([^)/]+\)/g, "") });
30
85
  return routes;
31
86
  }
32
87
  processRoute(
33
88
  parentRoute.children || (parentRoute.children = []),
34
89
  route,
35
- id.slice(parentRoute.path.length),
90
+ id.slice(parentRoute.id.length),
36
91
  full
37
92
  );
38
93
 
@@ -44,7 +99,80 @@ const defineRoutes = (fileRoutes: RouteModule[]) => {
44
99
  .reduce((prevRoutes, route) => {
45
100
  return processRoute(prevRoutes, route, route.path, route.path);
46
101
  }, []);
47
- };
102
+ }
103
+
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
+ }
48
132
 
49
- export const pageRoutes = defineRoutes(fileRoutes.filter(o => o.type === "page"));
50
- export const apiRoutes = fileRoutes.filter(o => o.type === "api");
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
+
142
+ const params: { type: "*" | ":"; name: string; index: number }[] = [];
143
+ const matchSegments: (string | null)[] = [];
144
+ let score = 0;
145
+ let wildcard = false;
146
+
147
+ for (const [index, segment] of segments.entries()) {
148
+ if (segment[0] === ":") {
149
+ const name = segment.slice(1);
150
+ score += 3;
151
+ params.push({
152
+ type: ":",
153
+ name,
154
+ index
155
+ });
156
+ matchSegments.push(null);
157
+ } else if (segment[0] === "*") {
158
+ score -= 1;
159
+ params.push({
160
+ type: "*",
161
+ name: segment.slice(1),
162
+ index
163
+ });
164
+ wildcard = true;
165
+ } else {
166
+ score += 4;
167
+ matchSegments.push(segment);
168
+ }
169
+ }
170
+
171
+ return {
172
+ ...route,
173
+ score,
174
+ params,
175
+ matchSegments,
176
+ wildcard
177
+ };
178
+ }