@solidjs/start 0.4.0-alpha.1 → 0.4.0-alpha.11
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 +12 -0
- package/config/index.d.ts +14 -0
- package/config/index.js +34 -18
- package/config/server-fns-runtime.jsx +11 -1
- package/config/server-handler.js +15 -14
- package/config/server-runtime.jsx +1 -1
- package/package.json +7 -4
- package/server/StartServer.tsx +34 -1
- package/server/handler.tsx +17 -12
- package/server/index.tsx +3 -2
- package/server/middleware.ts +18 -26
- package/server/page-event.tsx +53 -1
- package/server/spa/StartServer.tsx +1 -0
- package/server/spa/handler.tsx +4 -4
- package/server/spa/index.tsx +2 -0
- package/server/types.ts +16 -8
- package/shared/FileRoutes.tsx +3 -1
- package/shared/HttpHeader.tsx +2 -21
- package/shared/HttpStatusCode.tsx +2 -5
- package/shared/lazyRoute.tsx +2 -1
- package/shared/routes.ts +139 -11
package/README.md
ADDED
|
@@ -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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { serverFunctions } from "@vinxi/server-functions/plugin";
|
|
2
2
|
import { serverTransform } from "@vinxi/server-functions/server";
|
|
3
3
|
import defu from "defu";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { createApp, resolve } from "vinxi";
|
|
@@ -37,20 +38,29 @@ function solidStartServerFsRouter(config) {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export function defineConfig(baseConfig = {}) {
|
|
40
|
-
let { plugins = [],
|
|
41
|
+
let { plugins = [], start = {}, ...userConfig } = baseConfig;
|
|
41
42
|
const extensions = [...DEFAULT_EXTENSIONS, ...(start.extensions || [])];
|
|
42
43
|
start = defu(start, {
|
|
43
44
|
appRoot: "./src",
|
|
44
|
-
ssr,
|
|
45
|
+
ssr: true,
|
|
45
46
|
islands: false,
|
|
46
|
-
|
|
47
|
+
solid: {}
|
|
47
48
|
});
|
|
49
|
+
let server = start.server;
|
|
50
|
+
if (!start.ssr) {
|
|
51
|
+
server = { ...server, prerender: { routes: ["/"] } };
|
|
52
|
+
}
|
|
53
|
+
let entryExtension = ".tsx"
|
|
54
|
+
if (existsSync(join(process.cwd(), start.appRoot, "app.jsx"))) {
|
|
55
|
+
entryExtension = ".jsx"
|
|
56
|
+
}
|
|
48
57
|
|
|
49
58
|
return createApp({
|
|
50
59
|
server: {
|
|
51
60
|
compressPublicAssets: {
|
|
52
61
|
brotli: true
|
|
53
|
-
}
|
|
62
|
+
},
|
|
63
|
+
...server
|
|
54
64
|
},
|
|
55
65
|
routers: [
|
|
56
66
|
{
|
|
@@ -63,7 +73,7 @@ export function defineConfig(baseConfig = {}) {
|
|
|
63
73
|
{
|
|
64
74
|
name: "ssr",
|
|
65
75
|
mode: "handler",
|
|
66
|
-
handler: `${start.appRoot}/entry-server
|
|
76
|
+
handler: `${start.appRoot}/entry-server${entryExtension}`,
|
|
67
77
|
middleware: start.middleware,
|
|
68
78
|
routes: solidStartServerFsRouter({ dir: `${start.appRoot}/routes`, extensions }),
|
|
69
79
|
extensions,
|
|
@@ -75,23 +85,25 @@ export function defineConfig(baseConfig = {}) {
|
|
|
75
85
|
runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.jsx", import.meta.url)))
|
|
76
86
|
}),
|
|
77
87
|
start.islands ? serverComponents.server() : null,
|
|
78
|
-
solid({ ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
|
|
88
|
+
solid({ ...start.solid, ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
|
|
79
89
|
config("app-server", {
|
|
80
90
|
resolve: {
|
|
81
91
|
alias: {
|
|
82
|
-
"#start/app": join(process.cwd(), start.appRoot,
|
|
92
|
+
"#start/app": join(process.cwd(), start.appRoot, `app${entryExtension}`),
|
|
83
93
|
"~": join(process.cwd(), start.appRoot),
|
|
84
94
|
...(!start.ssr
|
|
85
95
|
? {
|
|
86
96
|
"@solidjs/start/server": "@solidjs/start/server/spa"
|
|
87
97
|
}
|
|
88
|
-
: {})
|
|
98
|
+
: {}),
|
|
99
|
+
...userConfig.resolve?.alias
|
|
89
100
|
}
|
|
90
101
|
},
|
|
91
102
|
define: {
|
|
92
103
|
"import.meta.env.START_ISLANDS": JSON.stringify(start.islands),
|
|
93
104
|
"import.meta.env.SSR": JSON.stringify(true),
|
|
94
|
-
"import.meta.env.START_SSR": JSON.stringify(start.ssr)
|
|
105
|
+
"import.meta.env.START_SSR": JSON.stringify(start.ssr),
|
|
106
|
+
...userConfig.define
|
|
95
107
|
}
|
|
96
108
|
})
|
|
97
109
|
]
|
|
@@ -99,7 +111,7 @@ export function defineConfig(baseConfig = {}) {
|
|
|
99
111
|
{
|
|
100
112
|
name: "client",
|
|
101
113
|
mode: "build",
|
|
102
|
-
handler: `${start.appRoot}/entry-client
|
|
114
|
+
handler: `${start.appRoot}/entry-client${entryExtension}`,
|
|
103
115
|
...(start.islands
|
|
104
116
|
? {}
|
|
105
117
|
: {
|
|
@@ -114,11 +126,11 @@ export function defineConfig(baseConfig = {}) {
|
|
|
114
126
|
runtime: normalize(fileURLToPath(new URL("./server-runtime.jsx", import.meta.url)))
|
|
115
127
|
}),
|
|
116
128
|
start.islands ? serverComponents.client() : null,
|
|
117
|
-
solid({ ssr: start.ssr, extensions: extensions.map(ext => `.${ext}`) }),
|
|
129
|
+
solid({ ...start.solid, ssr: start.ssr, extensions: extensions.map(ext => `.${ext}`) }),
|
|
118
130
|
config("app-client", {
|
|
119
131
|
resolve: {
|
|
120
132
|
alias: {
|
|
121
|
-
"#start/app": join(process.cwd(), start.appRoot,
|
|
133
|
+
"#start/app": join(process.cwd(), start.appRoot, `app${entryExtension}`),
|
|
122
134
|
"~": join(process.cwd(), start.appRoot),
|
|
123
135
|
...(start.islands
|
|
124
136
|
? {
|
|
@@ -129,13 +141,15 @@ export function defineConfig(baseConfig = {}) {
|
|
|
129
141
|
? {
|
|
130
142
|
"@solidjs/start/client": "@solidjs/start/client/spa"
|
|
131
143
|
}
|
|
132
|
-
: {})
|
|
144
|
+
: {}),
|
|
145
|
+
...userConfig.resolve?.alias
|
|
133
146
|
}
|
|
134
147
|
},
|
|
135
148
|
define: {
|
|
136
149
|
"import.meta.env.START_ISLANDS": JSON.stringify(start.islands),
|
|
137
150
|
"import.meta.env.SSR": JSON.stringify(false),
|
|
138
|
-
"import.meta.env.START_SSR": JSON.stringify(start.ssr)
|
|
151
|
+
"import.meta.env.START_SSR": JSON.stringify(start.ssr),
|
|
152
|
+
...userConfig.define
|
|
139
153
|
}
|
|
140
154
|
})
|
|
141
155
|
],
|
|
@@ -148,23 +162,25 @@ export function defineConfig(baseConfig = {}) {
|
|
|
148
162
|
plugins: () => [
|
|
149
163
|
config("user", userConfig),
|
|
150
164
|
...plugins,
|
|
151
|
-
solid({ ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
|
|
165
|
+
solid({ ...start.solid, ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
|
|
152
166
|
config("app-server", {
|
|
153
167
|
resolve: {
|
|
154
168
|
alias: {
|
|
155
|
-
"#start/app": join(process.cwd(), start.appRoot,
|
|
169
|
+
"#start/app": join(process.cwd(), start.appRoot, `app${entryExtension}`),
|
|
156
170
|
"~": join(process.cwd(), start.appRoot),
|
|
157
171
|
...(!start.ssr
|
|
158
172
|
? {
|
|
159
173
|
"@solidjs/start/server": "@solidjs/start/server/spa"
|
|
160
174
|
}
|
|
161
|
-
: {})
|
|
175
|
+
: {}),
|
|
176
|
+
...userConfig.resolve?.alias
|
|
162
177
|
}
|
|
163
178
|
},
|
|
164
179
|
define: {
|
|
165
180
|
"import.meta.env.START_ISLANDS": JSON.stringify(start.islands),
|
|
166
181
|
"import.meta.env.SSR": JSON.stringify(true),
|
|
167
|
-
"import.meta.env.START_SSR": JSON.stringify(start.ssr)
|
|
182
|
+
"import.meta.env.START_SSR": JSON.stringify(start.ssr),
|
|
183
|
+
...userConfig.define
|
|
168
184
|
}
|
|
169
185
|
})
|
|
170
186
|
],
|
|
@@ -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
|
-
|
|
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
|
}
|
package/config/server-handler.js
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
3
|
+
"version": "0.4.0-alpha.11",
|
|
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.
|
|
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.
|
|
51
|
+
"vite-plugin-solid": "^2.8.0"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"typecheck": "tsc --noEmit --checkJs false --skipLibCheck"
|
|
52
55
|
}
|
|
53
56
|
}
|
package/server/StartServer.tsx
CHANGED
|
@@ -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 {
|
|
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}
|
package/server/handler.tsx
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
|
42
|
+
const mod = await match.handler.import();
|
|
43
43
|
const fn = mod[event.request.method];
|
|
44
|
-
|
|
45
|
-
return
|
|
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
|
|
68
|
+
return sendRedirect(event, context.response.headers.get("Location"));
|
|
68
69
|
}
|
|
69
|
-
|
|
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
|
|
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(
|
package/server/middleware.ts
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
41
|
-
|
|
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 {
|
package/server/page-event.tsx
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/server/spa/handler.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHandler as createBaseHandler } from "../handler";
|
|
2
|
-
import { FetchEvent,
|
|
2
|
+
import { FetchEvent, PageEvent } from "../types";
|
|
3
3
|
|
|
4
4
|
export function createHandler(
|
|
5
5
|
fn: (context: PageEvent) => unknown,
|
|
@@ -14,9 +14,9 @@ export async function createPageEvent(ctx: FetchEvent) {
|
|
|
14
14
|
manifest: await clientManifest.json(),
|
|
15
15
|
assets: [...(await clientManifest.inputs[clientManifest.handler].assets())],
|
|
16
16
|
routes: [],
|
|
17
|
-
prevUrl: "",
|
|
18
|
-
mutation: false,
|
|
19
|
-
$type: FETCH_EVENT,
|
|
17
|
+
// prevUrl: "",
|
|
18
|
+
// mutation: false,
|
|
19
|
+
// $type: FETCH_EVENT,
|
|
20
20
|
$islands: new Set<string>()
|
|
21
21
|
});
|
|
22
22
|
|
package/server/spa/index.tsx
CHANGED
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
|
+
}
|
package/shared/FileRoutes.tsx
CHANGED
|
@@ -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 :
|
|
30
|
+
return isServer ? (getRequestEvent() as PageEvent).routes : routes || (routes = createRoutes());
|
|
29
31
|
};
|
package/shared/HttpHeader.tsx
CHANGED
|
@@ -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
|
|
7
|
-
if (props
|
|
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
|
|
7
|
-
|
|
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
|
}
|
package/shared/lazyRoute.tsx
CHANGED
|
@@ -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
|
-
...
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
50
|
-
|
|
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
|
+
}
|