@solidjs/start 1.2.1 → 2.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +2 -2
- package/dist/client/StartClient.d.ts +0 -1
- package/dist/client/StartClient.jsx +2 -4
- package/dist/client/index.d.ts +2 -3
- package/dist/client/index.jsx +2 -4
- package/dist/client/mount.d.ts +1 -7
- package/dist/client/mount.js +1 -91
- package/dist/client/spa/index.d.ts +1 -2
- package/dist/client/spa/index.jsx +1 -2
- package/dist/config/constants.d.ts +16 -0
- package/dist/config/constants.js +16 -0
- package/dist/config/dev-server.d.ts +2 -0
- package/dist/config/dev-server.js +108 -0
- package/dist/config/fs-router.d.ts +37 -0
- package/dist/config/fs-router.js +141 -0
- package/dist/config/fs-routes/fs-watcher.d.ts +3 -0
- package/dist/config/fs-routes/fs-watcher.js +49 -0
- package/dist/config/fs-routes/index.d.ts +7 -0
- package/dist/config/fs-routes/index.js +107 -0
- package/dist/config/fs-routes/router.d.ts +37 -0
- package/dist/config/fs-routes/router.js +136 -0
- package/dist/config/fs-routes/tree-shake.d.ts +2 -0
- package/dist/config/fs-routes/tree-shake.js +348 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +214 -0
- package/dist/config/lazy.d.ts +3 -0
- package/dist/config/lazy.js +82 -0
- package/dist/config/manifest.d.ts +3 -0
- package/dist/config/manifest.js +74 -0
- package/dist/config/utils.d.ts +4 -0
- package/dist/config/utils.js +6 -0
- package/dist/config/vite-utils.d.ts +16 -0
- package/dist/config/vite-utils.js +81 -0
- package/dist/http/index.d.ts +81 -0
- package/dist/http/index.js +129 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.jsx +5 -5
- package/dist/middleware/index.d.ts +6 -6
- package/dist/middleware/index.js +39 -0
- package/dist/{router/FileRoutes.js → router.jsx} +7 -8
- package/dist/server/StartServer.d.ts +3 -3
- package/dist/server/StartServer.jsx +9 -63
- package/dist/server/assets/index.d.ts +2 -0
- package/dist/server/assets/index.js +52 -0
- package/dist/server/assets/render.d.ts +19 -0
- package/dist/server/{renderAsset.jsx → assets/render.jsx} +1 -1
- package/dist/server/collect-styles.d.ts +3 -0
- package/dist/server/collect-styles.js +69 -0
- package/dist/server/fetchEvent.d.ts +4 -3
- package/dist/server/fetchEvent.js +12 -74
- package/dist/server/fetchEvent.spec.js +18 -197
- package/dist/server/handler.d.ts +6 -12
- package/dist/server/handler.js +193 -96
- package/dist/server/index.d.ts +9 -6
- package/dist/server/index.jsx +14 -6
- package/dist/server/manifest/client-manifest.d.ts +9 -0
- package/dist/server/manifest/client-manifest.js +6 -0
- package/dist/server/manifest/dev-client-manifest.d.ts +4 -0
- package/dist/server/manifest/dev-client-manifest.js +24 -0
- package/dist/server/manifest/dev-ssr-manifest.d.ts +5 -0
- package/dist/server/manifest/dev-ssr-manifest.js +23 -0
- package/dist/server/manifest/prod-client-manifest.d.ts +5 -0
- package/dist/server/manifest/prod-client-manifest.js +26 -0
- package/dist/server/manifest/prod-ssr-manifest.d.ts +6 -0
- package/dist/server/manifest/prod-ssr-manifest.js +85 -0
- package/dist/server/manifest/ssr-manifest.d.ts +4 -0
- package/dist/server/manifest/ssr-manifest.js +6 -0
- package/dist/{router → server}/routes.d.ts +5 -10
- package/dist/{router → server}/routes.js +32 -33
- package/dist/{runtime → server}/server-fns-runtime.d.ts +1 -1
- package/dist/{runtime → server}/server-fns-runtime.js +8 -7
- package/dist/server/server-functions-handler.d.ts +2 -0
- package/dist/{runtime/server-handler.js → server/server-functions-handler.js} +57 -97
- package/dist/server/server-runtime.d.ts +3 -0
- package/dist/{runtime → server}/server-runtime.js +17 -24
- package/dist/server/spa/StartServer.d.ts +1 -1
- package/dist/server/spa/StartServer.jsx +7 -9
- package/dist/server/spa/handler.d.ts +3 -3
- package/dist/server/spa/handler.js +9 -11
- package/dist/server/spa/index.d.ts +4 -3
- package/dist/server/spa/index.jsx +4 -3
- package/dist/server/types.d.ts +7 -9
- package/dist/server/util.d.ts +2 -0
- package/dist/server/util.js +8 -0
- package/dist/shared/ErrorBoundary.jsx +2 -2
- package/dist/shared/HttpHeader.jsx +4 -2
- package/dist/shared/{HttpStatusCode.jsx → HttpStatusCode.js} +3 -1
- package/dist/shared/dev-overlay/CodeView.d.ts +1 -1
- package/dist/shared/dev-overlay/CodeView.jsx +23 -34
- package/dist/shared/dev-overlay/DevOverlayDialog.d.ts +2 -2
- package/dist/shared/dev-overlay/DevOverlayDialog.jsx +73 -64
- package/dist/shared/dev-overlay/createStackFrame.js +3 -3
- package/dist/shared/dev-overlay/get-source-map.d.ts +1 -1
- package/dist/shared/dev-overlay/get-source-map.js +5 -5
- package/dist/shared/dev-overlay/icons.d.ts +10 -10
- package/dist/shared/dev-overlay/icons.jsx +91 -91
- package/dist/shared/dev-overlay/index.jsx +6 -9
- package/dist/shared/lazy.d.ts +3 -0
- package/dist/shared/lazy.js +42 -0
- package/dist/shared/serverFunction.d.ts +1 -1
- package/env.d.ts +2 -19
- package/package.json +44 -65
- package/scripts/build.js +15 -0
- package/scripts/validate-imports.js +316 -0
- package/src/client/StartClient.tsx +34 -0
- package/src/client/index.tsx +2 -0
- package/src/client/mount.ts +1 -0
- package/src/client/spa/index.tsx +9 -0
- package/src/config/constants.ts +19 -0
- package/src/config/dev-server.ts +145 -0
- package/{config/fs-router.js → src/config/fs-router.ts} +49 -29
- package/src/config/fs-routes/fs-watcher.ts +77 -0
- package/src/config/fs-routes/index.ts +136 -0
- package/src/config/fs-routes/router.ts +169 -0
- package/src/config/fs-routes/tree-shake.ts +371 -0
- package/src/config/index.ts +233 -0
- package/src/config/lazy.ts +100 -0
- package/src/config/manifest.ts +84 -0
- package/src/config/utils.ts +5 -0
- package/src/config/vite-utils.ts +109 -0
- package/src/env.d.ts +17 -0
- package/src/http/index.ts +216 -0
- package/src/index.tsx +7 -0
- package/src/internal.d.ts +14 -0
- package/src/middleware/index.ts +72 -0
- package/src/router.tsx +37 -0
- package/src/server/StartServer.tsx +56 -0
- package/src/server/assets/index.ts +70 -0
- package/src/server/assets/render.tsx +46 -0
- package/src/server/collect-styles.ts +87 -0
- package/src/server/fetchEvent.spec.ts +85 -0
- package/src/server/fetchEvent.ts +36 -0
- package/src/server/handler.ts +257 -0
- package/src/server/index.tsx +29 -0
- package/src/server/manifest/client-manifest.ts +8 -0
- package/src/server/manifest/dev-client-manifest.ts +21 -0
- package/src/server/manifest/dev-ssr-manifest.ts +23 -0
- package/src/server/manifest/prod-client-manifest.ts +21 -0
- package/src/server/manifest/prod-ssr-manifest.ts +109 -0
- package/src/server/manifest/ssr-manifest.ts +10 -0
- package/src/server/routes.ts +126 -0
- package/src/server/server-fns-runtime.ts +31 -0
- package/src/server/server-functions-handler.ts +339 -0
- package/src/server/server-runtime.ts +240 -0
- package/src/server/spa/StartServer.tsx +40 -0
- package/src/server/spa/handler.ts +29 -0
- package/src/server/spa/index.tsx +5 -0
- package/src/server/types.ts +82 -0
- package/src/server/util.ts +12 -0
- package/src/shared/ErrorBoundary.tsx +52 -0
- package/src/shared/GET.ts +8 -0
- package/src/shared/HttpHeader.tsx +42 -0
- package/src/shared/HttpStatusCode.ts +27 -0
- package/src/shared/clientOnly.ts +44 -0
- package/src/shared/dev-overlay/CodeView.tsx +92 -0
- package/src/shared/dev-overlay/DevOverlayDialog.tsx +316 -0
- package/src/shared/dev-overlay/createStackFrame.ts +74 -0
- package/src/shared/dev-overlay/download.ts +6 -0
- package/src/shared/dev-overlay/env.d.ts +4 -0
- package/src/shared/dev-overlay/get-source-map.ts +33 -0
- package/src/shared/dev-overlay/icons.tsx +456 -0
- package/src/shared/dev-overlay/index.tsx +63 -0
- package/src/shared/dev-overlay/styles.css +292 -0
- package/src/shared/lazy.ts +52 -0
- package/src/shared/serverFunction.ts +10 -0
- package/src/virtual.d.ts +27 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +9 -0
- package/config/index.d.ts +0 -40
- package/config/index.js +0 -304
- package/config/server-components.js +0 -106
- package/dist/client/islands.d.ts +0 -7
- package/dist/client/islands.jsx +0 -10
- package/dist/middleware/index.jsx +0 -35
- package/dist/router/index.d.ts +0 -1
- package/dist/router/index.jsx +0 -2
- package/dist/router/lazyRoute.d.ts +0 -6
- package/dist/router/lazyRoute.js +0 -53
- package/dist/runtime/server-handler.d.ts +0 -2
- package/dist/runtime/server-runtime.d.ts +0 -3
- package/dist/server/islands/index.d.ts +0 -19
- package/dist/server/islands/index.jsx +0 -62
- package/dist/server/islands/utils.d.ts +0 -1
- package/dist/server/islands/utils.jsx +0 -85
- package/dist/server/pageEvent.d.ts +0 -2
- package/dist/server/pageEvent.js +0 -56
- package/dist/server/renderAsset.d.ts +0 -2
- /package/dist/{router/FileRoutes.d.ts → router.d.ts} +0 -0
- /package/dist/shared/{clientOnly.jsx → clientOnly.js} +0 -0
- /package/dist/shared/{serverFunction.jsx → serverFunction.js} +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { clientViteManifest } from "solid-start:client-vite-manifest";
|
|
2
|
+
import { join } from "pathe";
|
|
3
|
+
import { Manifest } from "vite";
|
|
4
|
+
import type { Asset } from "../assets/render.tsx";
|
|
5
|
+
|
|
6
|
+
// Only reads from client manifest atm, might need server support for islands
|
|
7
|
+
export function getSsrProdManifest() {
|
|
8
|
+
const viteManifest = clientViteManifest;
|
|
9
|
+
return {
|
|
10
|
+
path(id: string) {
|
|
11
|
+
if (id.startsWith("./")) id = id.slice(2);
|
|
12
|
+
|
|
13
|
+
const viteManifestEntry = clientViteManifest[id /*import.meta.env.START_CLIENT_ENTRY*/];
|
|
14
|
+
if (!viteManifestEntry) throw new Error(`No entry found in vite manifest for '${id}'`);
|
|
15
|
+
|
|
16
|
+
return join("/", viteManifestEntry.file);
|
|
17
|
+
},
|
|
18
|
+
async getAssets(id) {
|
|
19
|
+
if (id.startsWith("./")) id = id.slice(2);
|
|
20
|
+
|
|
21
|
+
return createHtmlTagsForAssets(findAssetsInViteManifest(clientViteManifest, id));
|
|
22
|
+
},
|
|
23
|
+
async json() {
|
|
24
|
+
const json: Record<string, any> = {};
|
|
25
|
+
|
|
26
|
+
const entryKeys = Object.keys(viteManifest)
|
|
27
|
+
.filter(id => viteManifest[id]?.isEntry || viteManifest[id]?.isDynamicEntry)
|
|
28
|
+
.map(id => id);
|
|
29
|
+
|
|
30
|
+
for (const entryKey of entryKeys) {
|
|
31
|
+
json[entryKey] = {
|
|
32
|
+
output: join("/", viteManifest[entryKey]!.file),
|
|
33
|
+
assets: await this.getAssets(entryKey),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return json;
|
|
38
|
+
},
|
|
39
|
+
} satisfies StartManifest & {
|
|
40
|
+
json(): Promise<Record<string, any>>;
|
|
41
|
+
path(id: string): string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createHtmlTagsForAssets(assets: string[]) {
|
|
46
|
+
return assets
|
|
47
|
+
.filter(
|
|
48
|
+
asset =>
|
|
49
|
+
asset.endsWith(".css") ||
|
|
50
|
+
asset.endsWith(".js") ||
|
|
51
|
+
asset.endsWith(".ts") ||
|
|
52
|
+
asset.endsWith(".mjs"),
|
|
53
|
+
)
|
|
54
|
+
.map<Asset>(asset => ({
|
|
55
|
+
tag: "link",
|
|
56
|
+
attrs: {
|
|
57
|
+
href: "/" + asset,
|
|
58
|
+
key: asset,
|
|
59
|
+
...(asset.endsWith(".css") ? { rel: "stylesheet" } : { rel: "modulepreload" }),
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const entryId = import.meta.env.START_CLIENT_ENTRY.slice(2);
|
|
65
|
+
let entryImports: string[] | undefined = undefined;
|
|
66
|
+
|
|
67
|
+
function findAssetsInViteManifest(
|
|
68
|
+
manifest: Manifest,
|
|
69
|
+
id: string,
|
|
70
|
+
assetMap = new Map(),
|
|
71
|
+
stack: string[] = [],
|
|
72
|
+
) {
|
|
73
|
+
if (stack.includes(id)) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const cached = assetMap.get(id);
|
|
78
|
+
if (cached) {
|
|
79
|
+
return cached;
|
|
80
|
+
}
|
|
81
|
+
const chunk = manifest[id];
|
|
82
|
+
if (!chunk) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!entryImports) {
|
|
87
|
+
entryImports = [entryId, ...(manifest[entryId]?.imports ?? [])];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Only include entry imports, if we are specifically crawling the entry
|
|
91
|
+
// Chunks (e.g. routes) that import something from entry, should not render entry css redundantly
|
|
92
|
+
const excludeEntryImports = id !== entryId;
|
|
93
|
+
|
|
94
|
+
const assets = chunk.css?.filter(Boolean) || [];
|
|
95
|
+
if (chunk.imports) {
|
|
96
|
+
stack.push(id);
|
|
97
|
+
for (let i = 0, l = chunk.imports.length; i < l; i++) {
|
|
98
|
+
const importId = chunk.imports[i];
|
|
99
|
+
if (!importId || (excludeEntryImports && entryImports.includes(importId))) continue;
|
|
100
|
+
assets.push(...findAssetsInViteManifest(manifest, importId, assetMap, stack));
|
|
101
|
+
}
|
|
102
|
+
stack.pop();
|
|
103
|
+
}
|
|
104
|
+
assets.push(chunk.file);
|
|
105
|
+
const all = Array.from(new Set(assets));
|
|
106
|
+
assetMap.set(id, all);
|
|
107
|
+
|
|
108
|
+
return all;
|
|
109
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { getSsrDevManifest } from "./dev-ssr-manifest.ts";
|
|
2
|
+
import { getSsrProdManifest } from "./prod-ssr-manifest.ts";
|
|
3
|
+
|
|
4
|
+
export function getSsrManifest(
|
|
5
|
+
target: "client" | "ssr",
|
|
6
|
+
): ReturnType<typeof getSsrDevManifest> | ReturnType<typeof getSsrProdManifest> {
|
|
7
|
+
return import.meta.env.DEV ? getSsrDevManifest(target) : getSsrProdManifest();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export { getSsrManifest as getManifest };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// @ts-expect-error
|
|
2
|
+
import fileRoutes from "solid-start:routes";
|
|
3
|
+
import { createRouter } from "radix3";
|
|
4
|
+
|
|
5
|
+
import type { FetchEvent } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
interface Route {
|
|
8
|
+
path: string;
|
|
9
|
+
id: string;
|
|
10
|
+
children?: Route[];
|
|
11
|
+
page?: boolean;
|
|
12
|
+
$component?: any;
|
|
13
|
+
$HEAD?: any;
|
|
14
|
+
$GET?: any;
|
|
15
|
+
$POST?: any;
|
|
16
|
+
$PUT?: any;
|
|
17
|
+
$PATCH?: any;
|
|
18
|
+
$DELETE?: any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const pageRoutes = defineRoutes((fileRoutes as unknown as Route[]).filter(o => o.page));
|
|
22
|
+
|
|
23
|
+
function defineRoutes(fileRoutes: Route[]) {
|
|
24
|
+
function processRoute(routes: Route[], route: Route, id: string, full: string) {
|
|
25
|
+
const parentRoute = Object.values(routes).find(o => {
|
|
26
|
+
return id.startsWith(o.id + "/");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!parentRoute) {
|
|
30
|
+
routes.push({
|
|
31
|
+
...route,
|
|
32
|
+
id,
|
|
33
|
+
path: id.replace(/\([^)/]+\)/g, "").replace(/\/+/g, "/"),
|
|
34
|
+
});
|
|
35
|
+
return routes;
|
|
36
|
+
}
|
|
37
|
+
processRoute(
|
|
38
|
+
parentRoute.children || (parentRoute.children = []),
|
|
39
|
+
route,
|
|
40
|
+
id.slice(parentRoute.id.length),
|
|
41
|
+
full,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return routes;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return fileRoutes
|
|
48
|
+
.sort((a, b) => a.path.length - b.path.length)
|
|
49
|
+
.reduce((prevRoutes: Route[], route) => {
|
|
50
|
+
return processRoute(prevRoutes, route, route.path, route.path);
|
|
51
|
+
}, []);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const router = createRouter({
|
|
55
|
+
routes: (fileRoutes as unknown as Route[]).reduce(
|
|
56
|
+
(memo, route) => {
|
|
57
|
+
if (!containsHTTP(route)) return memo;
|
|
58
|
+
const path = route.path
|
|
59
|
+
.replace(/\([^)/]+\)/g, "")
|
|
60
|
+
.replace(/\/+/g, "/")
|
|
61
|
+
.replace(/\*([^/]*)/g, (_, m) => `**:${m}`)
|
|
62
|
+
.split("/")
|
|
63
|
+
.map(s => (s.startsWith(":") || s.startsWith("*") ? s : encodeURIComponent(s)))
|
|
64
|
+
.join("/");
|
|
65
|
+
if (/:[^/]*\?/g.test(path)) {
|
|
66
|
+
throw new Error(`Optional parameters are not supported in API routes: ${path}`);
|
|
67
|
+
}
|
|
68
|
+
if (memo[path]) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Duplicate API routes for "${path}" found at "${memo[path]!.route.path}" and "${
|
|
71
|
+
route.path
|
|
72
|
+
}"`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
memo[path] = { route };
|
|
76
|
+
return memo;
|
|
77
|
+
},
|
|
78
|
+
{} as Record<string, { route: Route }>,
|
|
79
|
+
),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
function containsHTTP(route: Route) {
|
|
83
|
+
return (
|
|
84
|
+
route["$HEAD"] ||
|
|
85
|
+
route["$GET"] ||
|
|
86
|
+
route["$POST"] ||
|
|
87
|
+
route["$PUT"] ||
|
|
88
|
+
route["$PATCH"] ||
|
|
89
|
+
route["$DELETE"]
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function matchAPIRoute(
|
|
94
|
+
path: string,
|
|
95
|
+
method: string,
|
|
96
|
+
):
|
|
97
|
+
| {
|
|
98
|
+
params?: Record<string, any>;
|
|
99
|
+
handler: {
|
|
100
|
+
import: () => Promise<Record<string, (e: FetchEvent) => Promise<any>>>;
|
|
101
|
+
};
|
|
102
|
+
isPage: boolean;
|
|
103
|
+
}
|
|
104
|
+
| undefined {
|
|
105
|
+
const match = router.lookup(path);
|
|
106
|
+
if (match && match.route) {
|
|
107
|
+
const route = match.route;
|
|
108
|
+
|
|
109
|
+
// Find the appropriate handler for the HTTP method
|
|
110
|
+
const handler = method === "HEAD" ? route.$HEAD || route.$GET : route[`$${method}`];
|
|
111
|
+
|
|
112
|
+
if (handler === undefined) return;
|
|
113
|
+
|
|
114
|
+
// Check if this is a page route
|
|
115
|
+
const isPage = route.page === true && route.$component !== undefined;
|
|
116
|
+
|
|
117
|
+
// Return comprehensive route information
|
|
118
|
+
return {
|
|
119
|
+
handler,
|
|
120
|
+
params: match.params,
|
|
121
|
+
isPage,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getRequestEvent } from "solid-js/web";
|
|
2
|
+
import { provideRequestEvent } from "solid-js/web/storage";
|
|
3
|
+
|
|
4
|
+
export function createServerReference(fn: Function, id: string) {
|
|
5
|
+
if (typeof fn !== "function")
|
|
6
|
+
throw new Error("Export from a 'use server' module must be a function");
|
|
7
|
+
let baseURL = import.meta.env.BASE_URL ?? "/";
|
|
8
|
+
if (!baseURL.endsWith("/")) baseURL += "/";
|
|
9
|
+
|
|
10
|
+
return new Proxy(fn, {
|
|
11
|
+
get(target, prop, receiver) {
|
|
12
|
+
if (prop === "url") {
|
|
13
|
+
return `${baseURL}_server?id=${encodeURIComponent(id)}`;
|
|
14
|
+
}
|
|
15
|
+
if (prop === "GET") return receiver;
|
|
16
|
+
return (target as any)[prop];
|
|
17
|
+
},
|
|
18
|
+
apply(target, thisArg, args) {
|
|
19
|
+
const ogEvt = getRequestEvent();
|
|
20
|
+
if (!ogEvt) throw new Error("Cannot call server function outside of a request");
|
|
21
|
+
const evt = { ...ogEvt };
|
|
22
|
+
evt.locals.serverFunctionMeta = {
|
|
23
|
+
id,
|
|
24
|
+
};
|
|
25
|
+
evt.serverOnly = true;
|
|
26
|
+
return provideRequestEvent(evt, () => {
|
|
27
|
+
return fn.apply(thisArg, args);
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { getServerFnById } from "solidstart:server-fn-manifest";
|
|
2
|
+
import { parseSetCookie } from "cookie-es";
|
|
3
|
+
import { type H3Event, parseCookies } from "h3";
|
|
4
|
+
import { crossSerializeStream, fromJSON, getCrossReferenceHeader } from "seroval";
|
|
5
|
+
import {
|
|
6
|
+
CustomEventPlugin,
|
|
7
|
+
DOMExceptionPlugin,
|
|
8
|
+
EventPlugin,
|
|
9
|
+
FormDataPlugin,
|
|
10
|
+
HeadersPlugin,
|
|
11
|
+
ReadableStreamPlugin,
|
|
12
|
+
RequestPlugin,
|
|
13
|
+
ResponsePlugin,
|
|
14
|
+
URLPlugin,
|
|
15
|
+
URLSearchParamsPlugin,
|
|
16
|
+
} from "seroval-plugins/web";
|
|
17
|
+
import { sharedConfig } from "solid-js";
|
|
18
|
+
import { renderToString } from "solid-js/web";
|
|
19
|
+
import { provideRequestEvent } from "solid-js/web/storage";
|
|
20
|
+
|
|
21
|
+
import { getFetchEvent, mergeResponseHeaders } from "./fetchEvent.ts";
|
|
22
|
+
import { createPageEvent } from "./handler.ts";
|
|
23
|
+
import type { FetchEvent, PageEvent } from "./types.ts";
|
|
24
|
+
import { getExpectedRedirectStatus } from "./util.ts";
|
|
25
|
+
|
|
26
|
+
function createChunk(data: string) {
|
|
27
|
+
const encodeData = new TextEncoder().encode(data);
|
|
28
|
+
const bytes = encodeData.length;
|
|
29
|
+
const baseHex = bytes.toString(16);
|
|
30
|
+
const totalHex = "00000000".substring(0, 8 - baseHex.length) + baseHex; // 32-bit
|
|
31
|
+
const head = new TextEncoder().encode(`;0x${totalHex};`);
|
|
32
|
+
|
|
33
|
+
const chunk = new Uint8Array(12 + bytes);
|
|
34
|
+
chunk.set(head);
|
|
35
|
+
chunk.set(encodeData, 12);
|
|
36
|
+
return chunk;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function serializeToStream(id: string, value: any) {
|
|
40
|
+
return new ReadableStream({
|
|
41
|
+
start(controller) {
|
|
42
|
+
crossSerializeStream(value, {
|
|
43
|
+
scopeId: id,
|
|
44
|
+
plugins: [
|
|
45
|
+
CustomEventPlugin,
|
|
46
|
+
DOMExceptionPlugin,
|
|
47
|
+
EventPlugin,
|
|
48
|
+
FormDataPlugin,
|
|
49
|
+
HeadersPlugin,
|
|
50
|
+
ReadableStreamPlugin,
|
|
51
|
+
RequestPlugin,
|
|
52
|
+
ResponsePlugin,
|
|
53
|
+
URLSearchParamsPlugin,
|
|
54
|
+
URLPlugin,
|
|
55
|
+
],
|
|
56
|
+
onSerialize(data: string, initial: boolean) {
|
|
57
|
+
controller.enqueue(
|
|
58
|
+
createChunk(initial ? `(${getCrossReferenceHeader(id)},${data})` : data),
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
onDone() {
|
|
62
|
+
controller.close();
|
|
63
|
+
},
|
|
64
|
+
onError(error: any) {
|
|
65
|
+
controller.error(error);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function handleServerFunction(h3Event: H3Event) {
|
|
73
|
+
const event = getFetchEvent(h3Event);
|
|
74
|
+
const request = event.request;
|
|
75
|
+
|
|
76
|
+
const serverReference = request.headers.get("X-Server-Id");
|
|
77
|
+
const instance = request.headers.get("X-Server-Instance");
|
|
78
|
+
const singleFlight = request.headers.has("X-Single-Flight");
|
|
79
|
+
const url = new URL(request.url);
|
|
80
|
+
let functionId: string | undefined | null;
|
|
81
|
+
if (serverReference) {
|
|
82
|
+
// invariant(typeof serverReference === "string", "Invalid server function");
|
|
83
|
+
[functionId] = serverReference.split("#");
|
|
84
|
+
} else {
|
|
85
|
+
functionId = url.searchParams.get("id");
|
|
86
|
+
|
|
87
|
+
if (!functionId) {
|
|
88
|
+
return process.env.NODE_ENV === "development"
|
|
89
|
+
? new Response("Server function not found", { status: 404 })
|
|
90
|
+
: new Response(null, { status: 404 });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const serverFunction = await getServerFnById(functionId!);
|
|
95
|
+
|
|
96
|
+
let parsed: any[] = [];
|
|
97
|
+
|
|
98
|
+
// grab bound arguments from url when no JS
|
|
99
|
+
if (!instance || h3Event.method === "GET") {
|
|
100
|
+
const args = url.searchParams.get("args");
|
|
101
|
+
if (args) {
|
|
102
|
+
const json = JSON.parse(args);
|
|
103
|
+
(json.t
|
|
104
|
+
? (fromJSON(json, {
|
|
105
|
+
plugins: [
|
|
106
|
+
CustomEventPlugin,
|
|
107
|
+
DOMExceptionPlugin,
|
|
108
|
+
EventPlugin,
|
|
109
|
+
FormDataPlugin,
|
|
110
|
+
HeadersPlugin,
|
|
111
|
+
ReadableStreamPlugin,
|
|
112
|
+
RequestPlugin,
|
|
113
|
+
ResponsePlugin,
|
|
114
|
+
URLSearchParamsPlugin,
|
|
115
|
+
URLPlugin,
|
|
116
|
+
],
|
|
117
|
+
}) as any)
|
|
118
|
+
: json
|
|
119
|
+
).forEach((arg: any) => {
|
|
120
|
+
parsed.push(arg);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (h3Event.method === "POST") {
|
|
125
|
+
const contentType = request.headers.get("content-type");
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
contentType?.startsWith("multipart/form-data") ||
|
|
129
|
+
contentType?.startsWith("application/x-www-form-urlencoded")
|
|
130
|
+
) {
|
|
131
|
+
parsed.push(await event.request.formData());
|
|
132
|
+
} else if (contentType?.startsWith("application/json")) {
|
|
133
|
+
parsed = fromJSON(await event.request.json(), {
|
|
134
|
+
plugins: [
|
|
135
|
+
CustomEventPlugin,
|
|
136
|
+
DOMExceptionPlugin,
|
|
137
|
+
EventPlugin,
|
|
138
|
+
FormDataPlugin,
|
|
139
|
+
HeadersPlugin,
|
|
140
|
+
ReadableStreamPlugin,
|
|
141
|
+
RequestPlugin,
|
|
142
|
+
ResponsePlugin,
|
|
143
|
+
URLSearchParamsPlugin,
|
|
144
|
+
URLPlugin,
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
let result = await provideRequestEvent(event, async () => {
|
|
151
|
+
/* @ts-expect-error */
|
|
152
|
+
sharedConfig.context = { event };
|
|
153
|
+
event.locals.serverFunctionMeta = {
|
|
154
|
+
id: functionId,
|
|
155
|
+
};
|
|
156
|
+
return serverFunction(...parsed);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (singleFlight && instance) {
|
|
160
|
+
result = await handleSingleFlight(event, result);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// handle responses
|
|
164
|
+
if (result instanceof Response) {
|
|
165
|
+
if (result.headers && result.headers.has("X-Content-Raw")) return result;
|
|
166
|
+
if (instance) {
|
|
167
|
+
// forward headers
|
|
168
|
+
if (result.headers) mergeResponseHeaders(h3Event, result.headers);
|
|
169
|
+
// forward non-redirect statuses
|
|
170
|
+
if (result.status && (result.status < 300 || result.status >= 400))
|
|
171
|
+
h3Event.res.status = result.status;
|
|
172
|
+
if ((result as any).customBody) {
|
|
173
|
+
result = await (result as any).customBody();
|
|
174
|
+
} else if (result.body == undefined) result = null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// handle no JS success case
|
|
179
|
+
if (!instance) return handleNoJS(result, request, parsed);
|
|
180
|
+
|
|
181
|
+
h3Event.res.headers.set("content-type", "text/javascript");
|
|
182
|
+
|
|
183
|
+
return serializeToStream(instance, result);
|
|
184
|
+
} catch (x) {
|
|
185
|
+
if (x instanceof Response) {
|
|
186
|
+
if (singleFlight && instance) {
|
|
187
|
+
x = await handleSingleFlight(event, x);
|
|
188
|
+
}
|
|
189
|
+
// forward headers
|
|
190
|
+
if ((x as any).headers) mergeResponseHeaders(h3Event, (x as any).headers);
|
|
191
|
+
// forward non-redirect statuses
|
|
192
|
+
if ((x as any).status && (!instance || (x as any).status < 300 || (x as any).status >= 400))
|
|
193
|
+
h3Event.res.status = (x as any).status;
|
|
194
|
+
if ((x as any).customBody) {
|
|
195
|
+
x = (x as any).customBody();
|
|
196
|
+
} else if ((x as any).body === undefined) x = null;
|
|
197
|
+
h3Event.res.headers.set("X-Error", "true");
|
|
198
|
+
} else if (instance) {
|
|
199
|
+
const error = x instanceof Error ? x.message : typeof x === "string" ? x : "true";
|
|
200
|
+
|
|
201
|
+
h3Event.res.headers.set("X-Error", error.replace(/[\r\n]+/g, ""));
|
|
202
|
+
} else {
|
|
203
|
+
x = handleNoJS(x, request, parsed, true);
|
|
204
|
+
}
|
|
205
|
+
if (instance) {
|
|
206
|
+
h3Event.res.headers.set("content-type", "text/javascript");
|
|
207
|
+
return serializeToStream(instance, x);
|
|
208
|
+
}
|
|
209
|
+
return x;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function handleNoJS(result: any, request: Request, parsed: any[], thrown?: boolean) {
|
|
214
|
+
const url = new URL(request.url);
|
|
215
|
+
const isError = result instanceof Error;
|
|
216
|
+
let statusCode = 302;
|
|
217
|
+
let headers: Headers;
|
|
218
|
+
if (result instanceof Response) {
|
|
219
|
+
headers = new Headers(result.headers);
|
|
220
|
+
if (result.headers.has("Location")) {
|
|
221
|
+
headers.set(
|
|
222
|
+
`Location`,
|
|
223
|
+
new URL(result.headers.get("Location")!, url.origin + import.meta.env.BASE_URL).toString(),
|
|
224
|
+
);
|
|
225
|
+
statusCode = getExpectedRedirectStatus(result);
|
|
226
|
+
}
|
|
227
|
+
} else
|
|
228
|
+
headers = new Headers({
|
|
229
|
+
Location: new URL(request.headers.get("referer")!).toString(),
|
|
230
|
+
});
|
|
231
|
+
if (result) {
|
|
232
|
+
headers.append(
|
|
233
|
+
"Set-Cookie",
|
|
234
|
+
`flash=${encodeURIComponent(
|
|
235
|
+
JSON.stringify({
|
|
236
|
+
url: url.pathname + url.search,
|
|
237
|
+
result: isError ? result.message : result,
|
|
238
|
+
thrown: thrown,
|
|
239
|
+
error: isError,
|
|
240
|
+
input: [...parsed.slice(0, -1), [...parsed[parsed.length - 1].entries()]],
|
|
241
|
+
}),
|
|
242
|
+
)}; Secure; HttpOnly;`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
return new Response(null, {
|
|
246
|
+
status: statusCode,
|
|
247
|
+
headers,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let App: any;
|
|
252
|
+
function createSingleFlightHeaders(sourceEvent: FetchEvent) {
|
|
253
|
+
// cookie handling logic is pretty simplistic so this might be imperfect
|
|
254
|
+
// unclear if h3 internals are available on all platforms but we need a way to
|
|
255
|
+
// update request headers on the underlying H3 event.
|
|
256
|
+
|
|
257
|
+
const headers = sourceEvent.request.headers;
|
|
258
|
+
const cookies = parseCookies(sourceEvent.nativeEvent);
|
|
259
|
+
const SetCookies = sourceEvent.response.headers.getSetCookie();
|
|
260
|
+
headers.delete("cookie");
|
|
261
|
+
// let useH3Internals = false;
|
|
262
|
+
// if (sourceEvent.nativeEvent.node?.req) {
|
|
263
|
+
// useH3Internals = true;
|
|
264
|
+
// sourceEvent.nativeEvent.node.req.headers.cookie = "";
|
|
265
|
+
// }
|
|
266
|
+
SetCookies.forEach(cookie => {
|
|
267
|
+
if (!cookie) return;
|
|
268
|
+
const { maxAge, expires, name, value } = parseSetCookie(cookie);
|
|
269
|
+
if (maxAge != null && maxAge <= 0) {
|
|
270
|
+
delete cookies[name];
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (expires != null && expires.getTime() <= Date.now()) {
|
|
274
|
+
delete cookies[name];
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
cookies[name] = value;
|
|
278
|
+
});
|
|
279
|
+
Object.entries(cookies).forEach(([key, value]) => {
|
|
280
|
+
headers.append("cookie", `${key}=${value}`);
|
|
281
|
+
// useH3Internals &&
|
|
282
|
+
// (sourceEvent.nativeEvent.node.req.headers.cookie += `${key}=${value};`);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
return headers;
|
|
286
|
+
}
|
|
287
|
+
async function handleSingleFlight(sourceEvent: FetchEvent, result: any): Promise<Response> {
|
|
288
|
+
let revalidate: string[];
|
|
289
|
+
let url = new URL(sourceEvent.request.headers.get("referer")!).toString();
|
|
290
|
+
if (result instanceof Response) {
|
|
291
|
+
if (result.headers.has("X-Revalidate"))
|
|
292
|
+
revalidate = result.headers.get("X-Revalidate")!.split(",");
|
|
293
|
+
if (result.headers.has("Location"))
|
|
294
|
+
url = new URL(
|
|
295
|
+
result.headers.get("Location")!,
|
|
296
|
+
new URL(sourceEvent.request.url).origin + import.meta.env.BASE_URL,
|
|
297
|
+
).toString();
|
|
298
|
+
}
|
|
299
|
+
const event = { ...sourceEvent } as PageEvent;
|
|
300
|
+
event.request = new Request(url, {
|
|
301
|
+
headers: createSingleFlightHeaders(sourceEvent),
|
|
302
|
+
});
|
|
303
|
+
return await provideRequestEvent(event, async () => {
|
|
304
|
+
await createPageEvent(event);
|
|
305
|
+
App || (App = (await import("solid-start:app")).default);
|
|
306
|
+
/* @ts-expect-error */
|
|
307
|
+
event.router.dataOnly = revalidate || true;
|
|
308
|
+
/* @ts-expect-error */
|
|
309
|
+
event.router.previousUrl = sourceEvent.request.headers.get("referer");
|
|
310
|
+
try {
|
|
311
|
+
renderToString(() => {
|
|
312
|
+
/* @ts-expect-error */
|
|
313
|
+
sharedConfig.context.event = event;
|
|
314
|
+
App();
|
|
315
|
+
});
|
|
316
|
+
} catch (e) {
|
|
317
|
+
console.log(e);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* @ts-expect-error */
|
|
321
|
+
const body = event.router.data;
|
|
322
|
+
if (!body) return result;
|
|
323
|
+
let containsKey = false;
|
|
324
|
+
for (const key in body) {
|
|
325
|
+
if (body[key] === undefined) delete body[key];
|
|
326
|
+
else containsKey = true;
|
|
327
|
+
}
|
|
328
|
+
if (!containsKey) return result;
|
|
329
|
+
if (!(result instanceof Response)) {
|
|
330
|
+
body["_$value"] = result;
|
|
331
|
+
result = new Response(null, { status: 200 });
|
|
332
|
+
} else if ((result as any).customBody) {
|
|
333
|
+
body["_$value"] = (result as any).customBody();
|
|
334
|
+
}
|
|
335
|
+
result.customBody = () => body;
|
|
336
|
+
result.headers.set("X-Single-Flight", "true");
|
|
337
|
+
return result;
|
|
338
|
+
});
|
|
339
|
+
}
|