@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,70 @@
|
|
|
1
|
+
import { onCleanup, sharedConfig } from "solid-js";
|
|
2
|
+
import { getRequestEvent, useAssets as useAssets_ } from "solid-js/web";
|
|
3
|
+
import { renderAsset, type Asset } from "./render.tsx";
|
|
4
|
+
|
|
5
|
+
const REGISTRY = Symbol("assetRegistry");
|
|
6
|
+
const NOOP = () => "";
|
|
7
|
+
|
|
8
|
+
type AssetEntity = {
|
|
9
|
+
key: string;
|
|
10
|
+
consumers: number;
|
|
11
|
+
preloadEl?: HTMLLinkElement;
|
|
12
|
+
ssrIdx?: number;
|
|
13
|
+
};
|
|
14
|
+
type Registry = Record<string, AssetEntity>;
|
|
15
|
+
|
|
16
|
+
const keyAttrs = ["href", "rel", "data-vite-dev-id"] as const;
|
|
17
|
+
|
|
18
|
+
const getEntity = (registry: Registry, asset: Asset) => {
|
|
19
|
+
let key = asset.tag;
|
|
20
|
+
for (const k of keyAttrs) {
|
|
21
|
+
if (!(k in asset.attrs)) continue;
|
|
22
|
+
key += `[${k}='${asset.attrs[k as keyof Asset["attrs"]]}']`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const entity = (registry[key] ??= {
|
|
26
|
+
key,
|
|
27
|
+
consumers: 0,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return entity;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const useAssets = (assets: Asset[], nonce?: string) => {
|
|
34
|
+
if (!assets.length) return;
|
|
35
|
+
|
|
36
|
+
const registry: Registry = (getRequestEvent()!.locals[REGISTRY] ??= {});
|
|
37
|
+
const ssrRequestAssets: Function[] = (sharedConfig.context as any)?.assets;
|
|
38
|
+
const cssKeys: string[] = [];
|
|
39
|
+
|
|
40
|
+
for (const asset of assets) {
|
|
41
|
+
const entity = getEntity(registry, asset);
|
|
42
|
+
const isCSSLink = asset.tag === "link" && asset.attrs.rel === "stylesheet";
|
|
43
|
+
const isCSS = isCSSLink || asset.tag === "style";
|
|
44
|
+
if (isCSS) {
|
|
45
|
+
cssKeys.push(entity.key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
entity.consumers++;
|
|
49
|
+
if (entity.consumers > 1) continue;
|
|
50
|
+
|
|
51
|
+
// Mounting logic
|
|
52
|
+
useAssets_(() => renderAsset(asset, nonce));
|
|
53
|
+
entity.ssrIdx = ssrRequestAssets.length - 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
onCleanup(() => {
|
|
57
|
+
for (const key of cssKeys) {
|
|
58
|
+
const entity = registry[key]!;
|
|
59
|
+
entity.consumers--;
|
|
60
|
+
if (entity.consumers != 0) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Ideally this logic should be implemented directly in dom-expressions
|
|
65
|
+
ssrRequestAssets.splice(entity.ssrIdx!, 1, NOOP);
|
|
66
|
+
|
|
67
|
+
delete registry[key];
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// @refresh skip
|
|
2
|
+
import type { JSX } from "solid-js";
|
|
3
|
+
|
|
4
|
+
const assetMap = {
|
|
5
|
+
style: (props: { attrs: JSX.StyleHTMLAttributes<HTMLStyleElement>; children?: JSX.Element }) => (
|
|
6
|
+
<style {...props.attrs}>{props.children}</style>
|
|
7
|
+
),
|
|
8
|
+
link: (props: { attrs: JSX.LinkHTMLAttributes<HTMLLinkElement> }) => <link {...props.attrs} />,
|
|
9
|
+
script: (props: {
|
|
10
|
+
attrs: JSX.ScriptHTMLAttributes<HTMLScriptElement>;
|
|
11
|
+
key: string | undefined;
|
|
12
|
+
}) => {
|
|
13
|
+
return props.attrs.src ? (
|
|
14
|
+
<script {...props.attrs} id={props.key}>
|
|
15
|
+
{" "}
|
|
16
|
+
</script>
|
|
17
|
+
) : null;
|
|
18
|
+
},
|
|
19
|
+
noscript: (props: { attrs: JSX.HTMLAttributes<HTMLElement>; children: JSX.Element }) => (
|
|
20
|
+
<noscript {...props.attrs}>{props.children}</noscript>
|
|
21
|
+
),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function renderAsset(asset: Asset, nonce?: string) {
|
|
25
|
+
let {
|
|
26
|
+
tag,
|
|
27
|
+
attrs: { key, ...attrs } = { key: undefined },
|
|
28
|
+
children,
|
|
29
|
+
} = asset as any;
|
|
30
|
+
return (assetMap as any)[tag]({ attrs: { ...attrs, nonce }, key, children });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type Asset =
|
|
34
|
+
| {
|
|
35
|
+
tag: "style";
|
|
36
|
+
attrs: JSX.StyleHTMLAttributes<HTMLStyleElement> & { key?: string };
|
|
37
|
+
children?: JSX.Element;
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
tag: "script";
|
|
41
|
+
attrs: JSX.ScriptHTMLAttributes<HTMLScriptElement> & { key?: string };
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
tag: "link";
|
|
45
|
+
attrs: JSX.LinkHTMLAttributes<HTMLLinkElement> & { key?: string };
|
|
46
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolve } from "pathe";
|
|
3
|
+
import type { DevEnvironment, EnvironmentModuleNode } from "vite";
|
|
4
|
+
|
|
5
|
+
async function getViteModuleNode(vite: DevEnvironment, file: string) {
|
|
6
|
+
let nodePath = file;
|
|
7
|
+
let node = vite.moduleGraph.getModuleById(file);
|
|
8
|
+
|
|
9
|
+
if (!node) {
|
|
10
|
+
const resolvedId = await vite.pluginContainer.resolveId(file, undefined);
|
|
11
|
+
if (!resolvedId) return;
|
|
12
|
+
|
|
13
|
+
nodePath = resolvedId.id;
|
|
14
|
+
node = vite.moduleGraph.getModuleById(file);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!node) {
|
|
18
|
+
nodePath = resolve(nodePath);
|
|
19
|
+
node = await vite.moduleGraph.getModuleByUrl(file);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!node) {
|
|
23
|
+
await vite.moduleGraph.ensureEntryFromUrl(nodePath, false);
|
|
24
|
+
node = vite.moduleGraph.getModuleById(nodePath);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return node;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function findModuleDependencies(
|
|
31
|
+
vite: DevEnvironment,
|
|
32
|
+
file: string,
|
|
33
|
+
deps: Set<EnvironmentModuleNode>,
|
|
34
|
+
crawledFiles = new Set<string>(),
|
|
35
|
+
) {
|
|
36
|
+
crawledFiles.add(file);
|
|
37
|
+
const module = await getViteModuleNode(vite, file);
|
|
38
|
+
if (!module?.id || deps.has(module)) return;
|
|
39
|
+
|
|
40
|
+
deps.add(module);
|
|
41
|
+
|
|
42
|
+
if (module.url.endsWith(".css") || module.url.includes("node_modules")) return;
|
|
43
|
+
|
|
44
|
+
if (!module.transformResult) {
|
|
45
|
+
await vite.transformRequest(module.id).catch(() => {});
|
|
46
|
+
}
|
|
47
|
+
if (!module.transformResult?.deps) return;
|
|
48
|
+
|
|
49
|
+
// Relying on module.transformResult.deps instead of module.importedModules because:
|
|
50
|
+
// transformResult properly separates imports into deps and dynamicDeps, importedModules doesn't
|
|
51
|
+
// Style crawling has to skip dynamic imports as such modules load their styles themselves
|
|
52
|
+
for (const dep of module.transformResult.deps) {
|
|
53
|
+
if (crawledFiles.has(dep)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
await findModuleDependencies(vite, dep, deps, crawledFiles);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Vite doesn't expose these so we just copy the list for now
|
|
61
|
+
// https://github.com/vitejs/vite/blob/d6bde8b03d433778aaed62afc2be0630c8131908/packages/vite/src/node/constants.ts#L49C23-L50
|
|
62
|
+
const cssFileRegExp = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)$/;
|
|
63
|
+
// https://github.com/vitejs/vite/blob/d6bde8b03d433778aaed62afc2be0630c8131908/packages/vite/src/node/plugins/css.ts#L160
|
|
64
|
+
const cssModulesRegExp = new RegExp(`\\.module${cssFileRegExp.source}`);
|
|
65
|
+
|
|
66
|
+
const isCssFile = (file: string) => cssFileRegExp.test(file);
|
|
67
|
+
export const isCssModulesFile = (file: string) => cssModulesRegExp.test(file);
|
|
68
|
+
|
|
69
|
+
export async function findStylesInModuleGraph(vite: DevEnvironment, id: string) {
|
|
70
|
+
const absolute = path.resolve(process.cwd(), id);
|
|
71
|
+
const dependencies = new Set<EnvironmentModuleNode>();
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await findModuleDependencies(vite, absolute, dependencies);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
console.error(e);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const styles: Record<string, any> = {};
|
|
80
|
+
for (const dep of dependencies) {
|
|
81
|
+
if (dep.id && isCssFile(dep.url)) {
|
|
82
|
+
styles[dep.id] = dep.url;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return styles;
|
|
87
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as h3 from "h3";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { createFetchEvent, getFetchEvent, mergeResponseHeaders } from "./fetchEvent.ts";
|
|
5
|
+
|
|
6
|
+
vi.mock(import("h3"), async mod => {
|
|
7
|
+
return {
|
|
8
|
+
...(await mod()),
|
|
9
|
+
getRequestIP: vi.fn(),
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const mockedH3 = vi.mocked(h3);
|
|
14
|
+
|
|
15
|
+
const createMockH3Event = (): h3.H3Event => {
|
|
16
|
+
const event = new h3.H3Event(new Request("http://localhost/test"));
|
|
17
|
+
|
|
18
|
+
event.res.status = 200;
|
|
19
|
+
event.res.statusText = "OK";
|
|
20
|
+
|
|
21
|
+
return event;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe("fetchEvent", () => {
|
|
25
|
+
let mockH3Event: h3.H3Event;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
mockH3Event = createMockH3Event();
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
|
|
31
|
+
mockedH3.getRequestIP.mockReturnValue("127.0.0.1");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("createFetchEvent", () => {
|
|
35
|
+
it("should create a FetchEvent from H3Event", () => {
|
|
36
|
+
const fetchEvent = createFetchEvent(mockH3Event);
|
|
37
|
+
|
|
38
|
+
expect(fetchEvent).toEqual({
|
|
39
|
+
request: mockH3Event.req,
|
|
40
|
+
response: expect.any(Object),
|
|
41
|
+
clientAddress: "127.0.0.1",
|
|
42
|
+
locals: {},
|
|
43
|
+
nativeEvent: mockH3Event,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should create response stub with correct properties", () => {
|
|
48
|
+
const fetchEvent = createFetchEvent(mockH3Event);
|
|
49
|
+
|
|
50
|
+
expect(fetchEvent.response).toHaveProperty("status");
|
|
51
|
+
expect(fetchEvent.response).toHaveProperty("statusText");
|
|
52
|
+
expect(fetchEvent.response).toHaveProperty("headers");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("getFetchEvent", () => {
|
|
57
|
+
it("should create and cache FetchEvent on first call", () => {
|
|
58
|
+
const fetchEvent = getFetchEvent(mockH3Event);
|
|
59
|
+
|
|
60
|
+
expect(mockH3Event.context.solidFetchEvent).toBe(fetchEvent);
|
|
61
|
+
expect(fetchEvent.nativeEvent).toBe(mockH3Event);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should return cached FetchEvent on subsequent calls", () => {
|
|
65
|
+
const firstCall = getFetchEvent(mockH3Event);
|
|
66
|
+
const secondCall = getFetchEvent(mockH3Event);
|
|
67
|
+
|
|
68
|
+
expect(firstCall).toBe(secondCall);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("mergeResponseHeaders", () => {
|
|
73
|
+
it("should merge headers from Headers object to H3Event", () => {
|
|
74
|
+
const headers = new Headers({
|
|
75
|
+
"content-type": "application/json",
|
|
76
|
+
"x-custom": "value",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
mergeResponseHeaders(mockH3Event, headers);
|
|
80
|
+
|
|
81
|
+
expect(headers.get("content-type")).toBe("application/json");
|
|
82
|
+
expect(headers.get("x-custom")).toBe("value");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type EventHandler, getRequestIP, type H3Event, type Middleware } from "h3";
|
|
2
|
+
import { provideRequestEvent } from "solid-js/web/storage";
|
|
3
|
+
import type { FetchEvent } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
const FETCH_EVENT_CONTEXT = "solidFetchEvent";
|
|
6
|
+
|
|
7
|
+
export function createFetchEvent(event: H3Event): FetchEvent {
|
|
8
|
+
return {
|
|
9
|
+
request: event.req,
|
|
10
|
+
response: event.res,
|
|
11
|
+
clientAddress: getRequestIP(event),
|
|
12
|
+
locals: {},
|
|
13
|
+
nativeEvent: event,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getFetchEvent(h3Event: H3Event): FetchEvent {
|
|
18
|
+
if (!h3Event.context[FETCH_EVENT_CONTEXT]) {
|
|
19
|
+
const fetchEvent = createFetchEvent(h3Event);
|
|
20
|
+
h3Event.context[FETCH_EVENT_CONTEXT] = fetchEvent;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return h3Event.context[FETCH_EVENT_CONTEXT] as any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function mergeResponseHeaders(h3Event: H3Event, headers: Headers) {
|
|
27
|
+
for (const [key, value] of headers.entries()) {
|
|
28
|
+
h3Event.res.headers.append(key, value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const decorateHandler = <T extends EventHandler>(fn: T) =>
|
|
33
|
+
(event => provideRequestEvent(getFetchEvent(event), () => fn(event))) as T;
|
|
34
|
+
|
|
35
|
+
export const decorateMiddleware = <T extends Middleware>(fn: T) =>
|
|
36
|
+
((event, next) => provideRequestEvent(getFetchEvent(event), () => fn(event, next))) as T;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import middleware from "solid-start:middleware";
|
|
2
|
+
import { defineHandler, getCookie, H3, type H3Event, redirect, setCookie } from "h3";
|
|
3
|
+
import { join } from "pathe";
|
|
4
|
+
import type { JSX } from "solid-js";
|
|
5
|
+
import { sharedConfig } from "solid-js";
|
|
6
|
+
import { getRequestEvent, renderToStream, renderToString } from "solid-js/web";
|
|
7
|
+
|
|
8
|
+
import { createRoutes } from "../router.tsx";
|
|
9
|
+
import { decorateHandler, decorateMiddleware } from "./fetchEvent.ts";
|
|
10
|
+
import { getSsrManifest } from "./manifest/ssr-manifest.ts";
|
|
11
|
+
import { matchAPIRoute } from "./routes.ts";
|
|
12
|
+
import { handleServerFunction } from "./server-functions-handler.ts";
|
|
13
|
+
import type { APIEvent, FetchEvent, HandlerOptions, PageEvent } from "./types.ts";
|
|
14
|
+
import { getExpectedRedirectStatus } from "./util.ts";
|
|
15
|
+
|
|
16
|
+
const SERVER_FN_BASE = "/_server";
|
|
17
|
+
|
|
18
|
+
export function createBaseHandler(
|
|
19
|
+
createPageEvent: (e: FetchEvent) => Promise<PageEvent>,
|
|
20
|
+
fn: (context: PageEvent) => JSX.Element,
|
|
21
|
+
options: HandlerOptions | ((context: PageEvent) => HandlerOptions | Promise<HandlerOptions>) = {},
|
|
22
|
+
) {
|
|
23
|
+
const handler = defineHandler({
|
|
24
|
+
middleware: middleware.length ? middleware.map(decorateMiddleware) : undefined,
|
|
25
|
+
handler: decorateHandler(async (e: H3Event) => {
|
|
26
|
+
const event = getRequestEvent()!;
|
|
27
|
+
const url = new URL(event.request.url);
|
|
28
|
+
const pathname = stripBaseUrl(url.pathname);
|
|
29
|
+
|
|
30
|
+
if (pathname.startsWith(SERVER_FN_BASE)) {
|
|
31
|
+
const serverFnResponse = await handleServerFunction(e);
|
|
32
|
+
|
|
33
|
+
if (serverFnResponse instanceof Response)
|
|
34
|
+
return produceResponseWithEventHeaders(serverFnResponse);
|
|
35
|
+
|
|
36
|
+
return new Response(serverFnResponse as any, {
|
|
37
|
+
headers: e.res.headers,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const match = matchAPIRoute(pathname, event.request.method);
|
|
42
|
+
if (match) {
|
|
43
|
+
const mod = await match.handler.import();
|
|
44
|
+
const fn =
|
|
45
|
+
event.request.method === "HEAD" ? mod["HEAD"] || mod["GET"] : mod[event.request.method];
|
|
46
|
+
(event as APIEvent).params = match.params || {};
|
|
47
|
+
// @ts-expect-error
|
|
48
|
+
sharedConfig.context = { event };
|
|
49
|
+
const res = await fn!(event);
|
|
50
|
+
if (res !== undefined) {
|
|
51
|
+
if (res instanceof Response) return produceResponseWithEventHeaders(res);
|
|
52
|
+
|
|
53
|
+
return res;
|
|
54
|
+
}
|
|
55
|
+
if (event.request.method !== "GET") {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`API handler for ${event.request.method} "${event.request.url}" did not return a response.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (!match.isPage) return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const context = await createPageEvent(event);
|
|
64
|
+
|
|
65
|
+
const resolvedOptions =
|
|
66
|
+
typeof options === "function" ? await options(context) : { ...options };
|
|
67
|
+
const mode = resolvedOptions.mode || "stream";
|
|
68
|
+
if (resolvedOptions.nonce) context.nonce = resolvedOptions.nonce;
|
|
69
|
+
|
|
70
|
+
if (mode === "sync" || !import.meta.env.START_SSR) {
|
|
71
|
+
const html = renderToString(() => {
|
|
72
|
+
(sharedConfig.context as any).event = context;
|
|
73
|
+
return fn(context);
|
|
74
|
+
}, resolvedOptions);
|
|
75
|
+
context.complete = true;
|
|
76
|
+
|
|
77
|
+
if (context.response && context.response.headers.get("Location")) {
|
|
78
|
+
const status = getExpectedRedirectStatus(context.response);
|
|
79
|
+
return redirect(context.response.headers.get("Location")!, status);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return html;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (resolvedOptions.onCompleteAll) {
|
|
86
|
+
const og = resolvedOptions.onCompleteAll;
|
|
87
|
+
resolvedOptions.onCompleteAll = options => {
|
|
88
|
+
handleStreamCompleteRedirect(context)(options);
|
|
89
|
+
og(options);
|
|
90
|
+
};
|
|
91
|
+
} else resolvedOptions.onCompleteAll = handleStreamCompleteRedirect(context);
|
|
92
|
+
if (resolvedOptions.onCompleteShell) {
|
|
93
|
+
const og = resolvedOptions.onCompleteShell;
|
|
94
|
+
resolvedOptions.onCompleteShell = options => {
|
|
95
|
+
handleShellCompleteRedirect(context, e)();
|
|
96
|
+
og(options);
|
|
97
|
+
};
|
|
98
|
+
} else resolvedOptions.onCompleteShell = handleShellCompleteRedirect(context, e);
|
|
99
|
+
|
|
100
|
+
const _stream = renderToStream(() => {
|
|
101
|
+
(sharedConfig.context as any).event = context;
|
|
102
|
+
return fn(context);
|
|
103
|
+
}, resolvedOptions);
|
|
104
|
+
const stream = _stream as typeof _stream & PromiseLike<string>; // stream has a hidden 'then' method
|
|
105
|
+
|
|
106
|
+
if (context.response && context.response.headers.get("Location")) {
|
|
107
|
+
const status = getExpectedRedirectStatus(context.response);
|
|
108
|
+
return redirect(context.response.headers.get("Location")!, status);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (mode === "async") return await stream;
|
|
112
|
+
|
|
113
|
+
delete (stream as any).then;
|
|
114
|
+
|
|
115
|
+
// using TransformStream in dev can cause solid-start-dev-server to crash
|
|
116
|
+
// when stream is cancelled
|
|
117
|
+
if (globalThis.USING_SOLID_START_DEV_SERVER) return stream;
|
|
118
|
+
|
|
119
|
+
// returning stream directly breaks cloudflare workers
|
|
120
|
+
const { writable, readable } = new TransformStream();
|
|
121
|
+
stream.pipeTo(writable);
|
|
122
|
+
return readable;
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const app = new H3();
|
|
127
|
+
|
|
128
|
+
app.use(handler);
|
|
129
|
+
|
|
130
|
+
return app;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function createHandler(
|
|
134
|
+
fn: (context: PageEvent) => JSX.Element,
|
|
135
|
+
options: HandlerOptions | ((context: PageEvent) => HandlerOptions | Promise<HandlerOptions>) = {},
|
|
136
|
+
) {
|
|
137
|
+
return createBaseHandler(createPageEvent, fn, options);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function createPageEvent(ctx: FetchEvent) {
|
|
141
|
+
ctx.response.headers.set("Content-Type", "text/html");
|
|
142
|
+
// const prevPath = ctx.request.headers.get("x-solid-referrer");
|
|
143
|
+
// const mutation = ctx.request.headers.get("x-solid-mutation") === "true";
|
|
144
|
+
const manifest = getSsrManifest(import.meta.env.SSR && import.meta.env.DEV ? "ssr" : "client");
|
|
145
|
+
|
|
146
|
+
// Handle Vite build.cssCodeSplit
|
|
147
|
+
// When build.cssCodeSplit is false, a single CSS file is generated with the key style.css
|
|
148
|
+
const mergedCSS = import.meta.env.PROD ? await manifest.getAssets("style.css") : [];
|
|
149
|
+
|
|
150
|
+
const assets = [
|
|
151
|
+
...mergedCSS,
|
|
152
|
+
...(await manifest.getAssets(import.meta.env.START_CLIENT_ENTRY)),
|
|
153
|
+
...(await manifest.getAssets(import.meta.env.START_APP_ENTRY)),
|
|
154
|
+
// ...(import.meta.env.START_ISLANDS
|
|
155
|
+
// ? (await serverManifest.inputs[serverManifest.handler]!.assets()).filter(
|
|
156
|
+
// s => (s as any).attrs.rel !== "modulepreload"
|
|
157
|
+
// )
|
|
158
|
+
// : [])
|
|
159
|
+
];
|
|
160
|
+
const pageEvent: PageEvent = Object.assign(ctx, {
|
|
161
|
+
assets,
|
|
162
|
+
router: {
|
|
163
|
+
submission: initFromFlash(ctx) as any,
|
|
164
|
+
},
|
|
165
|
+
routes: createRoutes(),
|
|
166
|
+
// prevUrl: prevPath || "",
|
|
167
|
+
// mutation: mutation,
|
|
168
|
+
// $type: FETCH_EVENT,
|
|
169
|
+
complete: false,
|
|
170
|
+
$islands: new Set<string>(),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return pageEvent;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function initFromFlash(ctx: FetchEvent) {
|
|
177
|
+
const flash = getCookie(ctx.nativeEvent, "flash");
|
|
178
|
+
if (!flash) return;
|
|
179
|
+
try {
|
|
180
|
+
const param = JSON.parse(flash);
|
|
181
|
+
if (!param || !param.result) return;
|
|
182
|
+
const input = [...param.input.slice(0, -1), new Map(param.input[param.input.length - 1])];
|
|
183
|
+
const result = param.error ? new Error(param.result) : param.result;
|
|
184
|
+
return {
|
|
185
|
+
input,
|
|
186
|
+
url: param.url,
|
|
187
|
+
pending: false,
|
|
188
|
+
result: param.thrown ? undefined : result,
|
|
189
|
+
error: param.thrown ? result : undefined,
|
|
190
|
+
};
|
|
191
|
+
} catch (e) {
|
|
192
|
+
console.error(e);
|
|
193
|
+
} finally {
|
|
194
|
+
setCookie(ctx.nativeEvent, "flash", "", { maxAge: 0 });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function handleShellCompleteRedirect(context: PageEvent, e: H3Event) {
|
|
199
|
+
return () => {
|
|
200
|
+
if (context.response && context.response.headers.get("Location")) {
|
|
201
|
+
const status = getExpectedRedirectStatus(context.response);
|
|
202
|
+
e.res.status = status;
|
|
203
|
+
e.res.headers.set("Location", context.response.headers.get("Location")!);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function handleStreamCompleteRedirect(context: PageEvent) {
|
|
209
|
+
return ({ write }: { write: (html: string) => void }) => {
|
|
210
|
+
context.complete = true;
|
|
211
|
+
const to = context.response && context.response.headers.get("Location");
|
|
212
|
+
to && write(`<script>window.location="${to}"</script>`);
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function produceResponseWithEventHeaders(res: Response) {
|
|
217
|
+
const event = getRequestEvent()!;
|
|
218
|
+
|
|
219
|
+
let ret = res;
|
|
220
|
+
|
|
221
|
+
// Response.redirect returns an immutable value, so we clone on any redirect just in case
|
|
222
|
+
if (300 <= res.status && res.status < 400) {
|
|
223
|
+
const cookies = res.headers.getSetCookie?.() ?? [];
|
|
224
|
+
const headers = new Headers();
|
|
225
|
+
res.headers.forEach((value, key) => {
|
|
226
|
+
if (key.toLowerCase() !== "set-cookie") {
|
|
227
|
+
headers.set(key, value);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
for (const cookie of cookies) {
|
|
231
|
+
headers.append("Set-Cookie", cookie);
|
|
232
|
+
}
|
|
233
|
+
ret = new Response(res.body, {
|
|
234
|
+
status: res.status,
|
|
235
|
+
statusText: res.statusText,
|
|
236
|
+
headers,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const eventCookies = event.response.headers.getSetCookie?.() ?? [];
|
|
241
|
+
for (const cookie of eventCookies) {
|
|
242
|
+
ret.headers.append("Set-Cookie", cookie);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
for (const [name, value] of event.response.headers) {
|
|
246
|
+
if (name.toLowerCase() !== "set-cookie") {
|
|
247
|
+
ret.headers.set(name, value);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return ret;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function stripBaseUrl(path: string) {
|
|
255
|
+
if (import.meta.env.BASE_URL === "/" || import.meta.env.BASE_URL === "") return path;
|
|
256
|
+
return path.slice(import.meta.env.BASE_URL.length);
|
|
257
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export { default as lazy } from "../shared/lazy.ts";
|
|
2
|
+
export { getServerFunctionMeta } from "../shared/serverFunction.ts";
|
|
3
|
+
export { StartServer } from "./StartServer.tsx";
|
|
4
|
+
export { createHandler } from "./handler.ts";
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
APIEvent,
|
|
8
|
+
APIHandler,
|
|
9
|
+
Asset,
|
|
10
|
+
ContextMatches,
|
|
11
|
+
DocumentComponentProps,
|
|
12
|
+
FetchEvent,
|
|
13
|
+
HandlerOptions,
|
|
14
|
+
PageEvent,
|
|
15
|
+
ResponseStub,
|
|
16
|
+
ServerFunctionMeta,
|
|
17
|
+
} from "./types.ts";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks if user has set a redirect status in the response.
|
|
21
|
+
* If not, falls back to the 302 (temporary redirect)
|
|
22
|
+
*/
|
|
23
|
+
// export function getExpectedRedirectStatus(response: ResponseStub): number {
|
|
24
|
+
// if (response.status && validRedirectStatuses.has(response.status)) {
|
|
25
|
+
// return response.status;
|
|
26
|
+
// }
|
|
27
|
+
|
|
28
|
+
// return 302;
|
|
29
|
+
// }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { getClientDevManifest } from "./dev-client-manifest.ts";
|
|
2
|
+
import { getClientProdManifest } from "./prod-client-manifest.ts";
|
|
3
|
+
|
|
4
|
+
export function getClientManifest() {
|
|
5
|
+
return import.meta.env.DEV ? getClientDevManifest() : getClientProdManifest();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export { getClientManifest as getManifest };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { join } from "pathe";
|
|
2
|
+
|
|
3
|
+
export function getClientDevManifest() {
|
|
4
|
+
return {
|
|
5
|
+
import(id) {
|
|
6
|
+
return import(/* @vite-ignore */ join("/", id));
|
|
7
|
+
},
|
|
8
|
+
async getAssets(id) {
|
|
9
|
+
const assetsPath = `/@manifest/client/${Date.now()}/assets?id=${id}`;
|
|
10
|
+
|
|
11
|
+
const assets = (await import(/* @vite-ignore */ assetsPath)).default;
|
|
12
|
+
|
|
13
|
+
return await Promise.all(
|
|
14
|
+
assets.map(async (v: any) => ({
|
|
15
|
+
...v,
|
|
16
|
+
children: await v.children(),
|
|
17
|
+
})),
|
|
18
|
+
);
|
|
19
|
+
},
|
|
20
|
+
} satisfies StartManifest & { import(id: string): Promise<any> };
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { join, normalize } from "pathe";
|
|
2
|
+
|
|
3
|
+
export function getSsrDevManifest(environment: "client" | "ssr") {
|
|
4
|
+
return {
|
|
5
|
+
path: (id: string) => normalize(join(import.meta.env.BASE_URL, id)),
|
|
6
|
+
async getAssets(id) {
|
|
7
|
+
const assetsPath = `/@manifest/${environment}/${Date.now()}/assets?id=${id}`;
|
|
8
|
+
|
|
9
|
+
const assets = (await import(/* @vite-ignore */ assetsPath)).default;
|
|
10
|
+
|
|
11
|
+
return await Promise.all(
|
|
12
|
+
assets.map(async (v: any) => ({
|
|
13
|
+
...v,
|
|
14
|
+
children: await v.children(),
|
|
15
|
+
})),
|
|
16
|
+
);
|
|
17
|
+
},
|
|
18
|
+
} satisfies StartManifest & {
|
|
19
|
+
path(id: string): string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { getSsrDevManifest as getSsrManifest };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function getClientProdManifest() {
|
|
2
|
+
return {
|
|
3
|
+
import(id) {
|
|
4
|
+
// @ts-ignore
|
|
5
|
+
return import(/* @vite-ignore */ window.manifest[id].output);
|
|
6
|
+
},
|
|
7
|
+
async getAssets(id) {
|
|
8
|
+
if (id.startsWith("./")) id = id.slice(2);
|
|
9
|
+
|
|
10
|
+
// @ts-ignore
|
|
11
|
+
return window.manifest[id]?.assets ?? [];
|
|
12
|
+
},
|
|
13
|
+
async json() {
|
|
14
|
+
// @ts-ignore
|
|
15
|
+
return window.manifest;
|
|
16
|
+
},
|
|
17
|
+
} satisfies StartManifest & {
|
|
18
|
+
json(): Promise<Record<string, any>>;
|
|
19
|
+
import(id: string): Promise<any>;
|
|
20
|
+
};
|
|
21
|
+
}
|