@kasperrt/smolter 0.0.1 → 0.3.0
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/{src/internal → .generated}/client/index.ts +1 -0
- package/.generated/core/environment.ts +43 -0
- package/{src/internal → .generated}/core/errors.ts +1 -1
- package/{src/internal → .generated}/core/index.ts +2 -0
- package/.generated/core/navigation-router.ts +107 -0
- package/.generated/core/navigation.ts +147 -0
- package/.generated/core/presentation.ts +108 -0
- package/.generated/core/resource-types.ts +44 -0
- package/.generated/core/resources.ts +444 -0
- package/.generated/core/responses.ts +86 -0
- package/{src/internal → .generated}/core/router.ts +136 -63
- package/.generated/core/serialize.ts +20 -0
- package/.generated/core/store.ts +25 -0
- package/.generated/core/submission.ts +116 -0
- package/{src/internal → .generated}/core/types.ts +18 -1
- package/.generated/core/url.ts +29 -0
- package/.generated/core/utils.ts +5 -0
- package/.generated/preact/await.tsx +41 -0
- package/.generated/preact/client.tsx +93 -0
- package/.generated/preact/context.ts +59 -0
- package/.generated/preact/document.tsx +198 -0
- package/{src/internal → .generated}/preact/fetcher.ts +43 -23
- package/.generated/preact/form.ts +58 -0
- package/{src/internal → .generated}/preact/hooks.ts +25 -28
- package/.generated/preact/index.ts +34 -0
- package/.generated/preact/lazy-route.ts +100 -0
- package/.generated/preact/navigation-state.ts +9 -0
- package/.generated/preact/navigation.tsx +147 -0
- package/.generated/preact/root-context.ts +21 -0
- package/.generated/preact/root-data.ts +45 -0
- package/.generated/preact/route-hooks.tsx +170 -0
- package/.generated/preact/route-types.ts +76 -0
- package/.generated/preact/router.tsx +107 -0
- package/{src/internal → .generated}/preact/routes.ts +17 -9
- package/.generated/react/context.ts +59 -0
- package/.generated/react/data.ts +25 -0
- package/{src/internal → .generated}/react/fetcher.ts +13 -11
- package/.generated/react/form.ts +58 -0
- package/{src/internal → .generated}/react/hooks.ts +9 -14
- package/{src/internal/preact → .generated/react}/index.ts +5 -1
- package/{src/internal → .generated}/react/link.ts +13 -21
- package/.generated/react/resources/components.tsx +404 -0
- package/{src/internal → .generated}/react/routes.ts +17 -9
- package/.generated/server/application.tsx +185 -0
- package/.generated/server/assets.ts +19 -0
- package/.generated/server/handler.ts +69 -0
- package/.generated/server/http.ts +55 -0
- package/{src/internal → .generated}/server/index.ts +4 -17
- package/.generated/server/node.ts +4 -0
- package/.generated/server/preact.ts +7 -0
- package/.generated/vite/application/browser-boundary.ts +42 -0
- package/.generated/vite/application/lazy-files.ts +136 -0
- package/.generated/vite/application/vite.ts +144 -0
- package/{src/internal → .generated}/vite/index.ts +4 -2
- package/.generated/vite/preact.ts +24 -0
- package/.generated/vite/react.ts +18 -0
- package/{src/internal → .generated}/vite/transport.ts +2 -1
- package/CHANGELOG.md +54 -0
- package/README.md +269 -55
- package/dist/client/deserialize.js +10 -0
- package/dist/{internal/client → client}/index.d.ts +1 -0
- package/dist/client/index.js +46 -0
- package/dist/core/environment.d.ts +12 -0
- package/dist/core/environment.js +23 -0
- package/dist/core/errors.js +7 -0
- package/dist/{internal/core → core}/index.d.ts +2 -0
- package/dist/core/match.js +33 -0
- package/dist/core/navigation-router.d.ts +15 -0
- package/dist/core/navigation-router.js +64 -0
- package/dist/core/navigation.d.ts +15 -0
- package/dist/core/navigation.js +92 -0
- package/dist/core/presentation.d.ts +5 -0
- package/dist/core/presentation.js +69 -0
- package/dist/core/resource-types.d.ts +37 -0
- package/dist/core/resources.d.ts +19 -0
- package/dist/core/resources.js +345 -0
- package/dist/core/responses.d.ts +15 -0
- package/dist/core/responses.js +48 -0
- package/dist/{internal/core → core}/router.d.ts +1 -1
- package/dist/core/router.js +204 -0
- package/dist/core/serialize.d.ts +5 -0
- package/dist/core/serialize.js +17 -0
- package/dist/core/store.d.ts +6 -0
- package/dist/core/store.js +21 -0
- package/dist/core/submission.d.ts +30 -0
- package/dist/core/submission.js +41 -0
- package/dist/{internal/core → core}/types.d.ts +16 -1
- package/dist/core/url.d.ts +7 -0
- package/dist/core/url.js +12 -0
- package/dist/core/utils.d.ts +5 -0
- package/dist/core/utils.js +5 -0
- package/dist/files/index.js +64 -0
- package/dist/preact/await.d.ts +7 -0
- package/dist/preact/await.js +19 -0
- package/dist/preact/client.d.ts +7 -0
- package/dist/preact/client.js +67 -0
- package/dist/preact/context.d.ts +18 -0
- package/dist/preact/context.js +23 -0
- package/dist/preact/document.d.ts +15 -0
- package/dist/preact/document.js +155 -0
- package/dist/{internal/preact → preact}/fetcher.d.ts +9 -5
- package/dist/preact/fetcher.js +45 -0
- package/dist/{internal/preact → preact}/form.d.ts +5 -6
- package/dist/preact/form.js +21 -0
- package/dist/{internal/preact → preact}/hooks.d.ts +7 -6
- package/dist/preact/hooks.js +25 -0
- package/dist/preact/index.d.ts +18 -0
- package/dist/preact/index.js +15 -0
- package/dist/preact/lazy-route.d.ts +19 -0
- package/dist/preact/lazy-route.js +51 -0
- package/dist/preact/navigation-state.d.ts +2 -0
- package/dist/preact/navigation-state.js +9 -0
- package/dist/preact/navigation.d.ts +28 -0
- package/dist/preact/navigation.js +60 -0
- package/dist/preact/root-context.d.ts +13 -0
- package/dist/preact/root-context.js +9 -0
- package/dist/preact/root-data.d.ts +8 -0
- package/dist/preact/root-data.js +28 -0
- package/dist/preact/route-hooks.d.ts +32 -0
- package/dist/preact/route-hooks.js +94 -0
- package/dist/preact/route-types.d.ts +54 -0
- package/dist/preact/router.d.ts +16 -0
- package/dist/preact/router.js +63 -0
- package/dist/{internal/preact → preact}/routes.d.ts +1 -1
- package/dist/preact/routes.js +12 -0
- package/dist/{internal/preact → preact}/runtime.js +1 -1
- package/dist/react/context.d.ts +18 -0
- package/dist/react/context.js +23 -0
- package/dist/react/data.d.ts +5 -0
- package/dist/react/data.js +3 -0
- package/dist/{internal/react → react}/fetcher.d.ts +2 -2
- package/dist/react/fetcher.js +34 -0
- package/dist/{internal/react → react}/form.d.ts +5 -6
- package/dist/react/form.js +21 -0
- package/dist/react/hooks.js +25 -0
- package/dist/{internal/react → react}/index.d.ts +1 -1
- package/dist/react/index.js +10 -0
- package/dist/{internal/react → react}/link.d.ts +2 -2
- package/dist/react/link.js +27 -0
- package/dist/react/resources/components.d.ts +64 -0
- package/dist/react/resources/components.js +239 -0
- package/dist/{internal/react → react}/routes.d.ts +1 -1
- package/dist/react/routes.js +12 -0
- package/dist/react/runtime.js +1 -0
- package/dist/server/application.d.ts +4 -0
- package/dist/server/application.js +119 -0
- package/dist/server/assets.d.ts +5 -0
- package/dist/server/assets.js +7 -0
- package/dist/server/handler.d.ts +19 -0
- package/dist/server/handler.js +22 -0
- package/dist/server/http.d.ts +4 -0
- package/dist/server/http.js +27 -0
- package/dist/{internal/server → server}/index.d.ts +2 -3
- package/dist/server/index.js +76 -0
- package/dist/server/node.d.ts +3 -0
- package/dist/server/node.js +3 -0
- package/dist/server/preact.d.ts +3 -0
- package/dist/server/preact.js +4 -0
- package/dist/vite/application/browser-boundary.d.ts +3 -0
- package/dist/vite/application/browser-boundary.js +35 -0
- package/dist/vite/application/lazy-files.d.ts +2 -0
- package/dist/vite/application/lazy-files.js +87 -0
- package/dist/vite/application/vite.d.ts +9 -0
- package/dist/vite/application/vite.js +101 -0
- package/dist/{internal/vite → vite}/index.d.ts +2 -1
- package/dist/vite/index.js +104 -0
- package/dist/vite/preact.d.ts +11 -0
- package/dist/vite/preact.js +15 -0
- package/dist/vite/react.d.ts +9 -0
- package/dist/vite/react.js +14 -0
- package/dist/{internal/vite → vite}/transport.d.ts +1 -1
- package/dist/vite/transport.js +35 -0
- package/jsr.json +28 -11
- package/package.json +119 -38
- package/dist/client.d.ts +0 -2
- package/dist/client.js +0 -2
- package/dist/core.d.ts +0 -2
- package/dist/core.js +0 -2
- package/dist/files.d.ts +0 -2
- package/dist/files.js +0 -2
- package/dist/internal/client/deserialize.js +0 -11
- package/dist/internal/client/index.js +0 -44
- package/dist/internal/core/errors.js +0 -6
- package/dist/internal/core/index.js +0 -6
- package/dist/internal/core/match.js +0 -27
- package/dist/internal/core/router.js +0 -189
- package/dist/internal/core/types.js +0 -5
- package/dist/internal/core/url.d.ts +0 -2
- package/dist/internal/core/url.js +0 -6
- package/dist/internal/files/index.js +0 -77
- package/dist/internal/preact/context.d.ts +0 -18
- package/dist/internal/preact/context.js +0 -20
- package/dist/internal/preact/fetcher.js +0 -28
- package/dist/internal/preact/form.js +0 -41
- package/dist/internal/preact/hooks.js +0 -35
- package/dist/internal/preact/index.d.ts +0 -9
- package/dist/internal/preact/index.js +0 -7
- package/dist/internal/preact/link.d.ts +0 -12
- package/dist/internal/preact/link.js +0 -28
- package/dist/internal/preact/routes.js +0 -22
- package/dist/internal/react/context.d.ts +0 -18
- package/dist/internal/react/context.js +0 -20
- package/dist/internal/react/fetcher.js +0 -28
- package/dist/internal/react/form.js +0 -41
- package/dist/internal/react/hooks.js +0 -35
- package/dist/internal/react/index.js +0 -7
- package/dist/internal/react/link.js +0 -28
- package/dist/internal/react/routes.js +0 -22
- package/dist/internal/react/runtime.js +0 -2
- package/dist/internal/server/index.js +0 -81
- package/dist/internal/vite/index.js +0 -132
- package/dist/internal/vite/transport.js +0 -42
- package/dist/preact.d.ts +0 -2
- package/dist/preact.js +0 -2
- package/dist/react.d.ts +0 -2
- package/dist/react.js +0 -2
- package/dist/server.d.ts +0 -2
- package/dist/server.js +0 -2
- package/dist/vite.d.ts +0 -2
- package/dist/vite.js +0 -2
- package/src/client.ts +0 -2
- package/src/core.ts +0 -2
- package/src/files.ts +0 -2
- package/src/internal/core/url.ts +0 -6
- package/src/internal/preact/context.ts +0 -46
- package/src/internal/preact/form.ts +0 -79
- package/src/internal/preact/link.ts +0 -46
- package/src/internal/react/context.ts +0 -46
- package/src/internal/react/form.ts +0 -79
- package/src/internal/react/index.ts +0 -13
- package/src/preact.ts +0 -2
- package/src/react.ts +0 -2
- package/src/server.ts +0 -2
- package/src/vite.ts +0 -2
- /package/{src/internal → .generated}/client/deserialize.ts +0 -0
- /package/{src/internal → .generated}/core/match.ts +0 -0
- /package/{src/internal → .generated}/files/index.ts +0 -0
- /package/{src/internal → .generated}/preact/runtime.ts +0 -0
- /package/{src/internal → .generated}/react/runtime.ts +0 -0
- /package/dist/{internal/client → client}/deserialize.d.ts +0 -0
- /package/dist/{internal/core → core}/errors.d.ts +0 -0
- /package/dist/{internal/core → core}/match.d.ts +0 -0
- /package/dist/{internal/files → files}/index.d.ts +0 -0
- /package/dist/{internal/preact → preact}/runtime.d.ts +0 -0
- /package/dist/{internal/react → react}/hooks.d.ts +0 -0
- /package/dist/{internal/react → react}/runtime.d.ts +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { send, webRequest } from "./http.ts";
|
|
3
|
+
|
|
4
|
+
/** Server module produced by an application's SSR entrypoint. */
|
|
5
|
+
export interface ServerBuild {
|
|
6
|
+
default: (request: Request, entry: string) => Response | Promise<Response>;
|
|
7
|
+
handleDataRequest: (request: Request) => Response | Promise<Response>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RequestHandlerOptions {
|
|
11
|
+
build: ServerBuild;
|
|
12
|
+
/** Browser entry URL resolved from Vite's production manifest. */
|
|
13
|
+
entry: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ServerIncomingMessage extends IncomingMessage {
|
|
17
|
+
/** Express supplies this using the application's trust proxy policy. */
|
|
18
|
+
protocol?: string;
|
|
19
|
+
originalUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type NextHandler = (error?: unknown) => void;
|
|
23
|
+
|
|
24
|
+
/** Node/Express middleware; mount after metrics, tracing and static assets. */
|
|
25
|
+
export function createRequestHandler({
|
|
26
|
+
build,
|
|
27
|
+
entry,
|
|
28
|
+
}: RequestHandlerOptions): (
|
|
29
|
+
incoming: ServerIncomingMessage,
|
|
30
|
+
outgoing: ServerResponse,
|
|
31
|
+
next: NextHandler,
|
|
32
|
+
) => Promise<void> {
|
|
33
|
+
return (
|
|
34
|
+
incoming: ServerIncomingMessage,
|
|
35
|
+
outgoing: ServerResponse,
|
|
36
|
+
next: NextHandler,
|
|
37
|
+
): Promise<void> => {
|
|
38
|
+
const handle = async () => {
|
|
39
|
+
const url = new URL(
|
|
40
|
+
incoming.originalUrl ?? incoming.url ?? "/",
|
|
41
|
+
"http://local",
|
|
42
|
+
);
|
|
43
|
+
const dataRequest = url.pathname === "/_smolter";
|
|
44
|
+
const target = dataRequest ? url.searchParams.get("url") : undefined;
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
dataRequest &&
|
|
48
|
+
(!target?.startsWith("/") ||
|
|
49
|
+
new URL(target, "http://local").origin !== "http://local")
|
|
50
|
+
) {
|
|
51
|
+
await send(
|
|
52
|
+
new Response("Invalid route URL", { status: 400 }),
|
|
53
|
+
outgoing,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const request = await webRequest(incoming, outgoing, target ?? undefined);
|
|
60
|
+
const response = dataRequest
|
|
61
|
+
? await build.handleDataRequest(request)
|
|
62
|
+
: await build.default(request, entry);
|
|
63
|
+
|
|
64
|
+
await send(response, outgoing);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return handle().catch(next);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
import type { ServerIncomingMessage } from "./handler.ts";
|
|
3
|
+
|
|
4
|
+
export async function webRequest(
|
|
5
|
+
incoming: ServerIncomingMessage,
|
|
6
|
+
outgoing: ServerResponse,
|
|
7
|
+
target?: string,
|
|
8
|
+
): Promise<Request> {
|
|
9
|
+
const origin = `${incoming.protocol ?? "http"}://${incoming.headers.host}`;
|
|
10
|
+
const headers = new Headers();
|
|
11
|
+
|
|
12
|
+
for (const [name, value] of Object.entries(incoming.headers)) {
|
|
13
|
+
if (value)
|
|
14
|
+
headers.set(name, Array.isArray(value) ? value.join(", ") : value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
outgoing.once("close", () => controller.abort());
|
|
19
|
+
const chunks: Uint8Array[] = [];
|
|
20
|
+
|
|
21
|
+
for await (const chunk of incoming) chunks.push(chunk);
|
|
22
|
+
|
|
23
|
+
return new Request(
|
|
24
|
+
new URL(target ?? incoming.originalUrl ?? incoming.url ?? "/", origin),
|
|
25
|
+
{
|
|
26
|
+
method: incoming.method,
|
|
27
|
+
headers,
|
|
28
|
+
signal: controller.signal,
|
|
29
|
+
...(incoming.method !== "GET" &&
|
|
30
|
+
incoming.method !== "HEAD" && {
|
|
31
|
+
body: new Uint8Array(Buffer.concat(chunks)),
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function send(response: Response, outgoing: ServerResponse) {
|
|
38
|
+
outgoing.statusCode = response.status;
|
|
39
|
+
response.headers.forEach((value, name) => {
|
|
40
|
+
if (name !== "set-cookie") outgoing.setHeader(name, value);
|
|
41
|
+
});
|
|
42
|
+
const cookies = response.headers.getSetCookie();
|
|
43
|
+
|
|
44
|
+
if (cookies.length)
|
|
45
|
+
outgoing.setHeader(
|
|
46
|
+
"set-cookie",
|
|
47
|
+
cookies.map((cookie) =>
|
|
48
|
+
process.env.NODE_ENV === "production"
|
|
49
|
+
? cookie
|
|
50
|
+
: cookie.replace(/; Secure/gi, ""),
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
outgoing.end(new Uint8Array(await response.arrayBuffer()));
|
|
55
|
+
}
|
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
createDataRouter,
|
|
8
8
|
type DataFunction,
|
|
9
9
|
matchRoutes,
|
|
10
10
|
type Route,
|
|
11
11
|
type Router,
|
|
12
|
-
type Snapshot,
|
|
13
12
|
safeWrapAsync,
|
|
14
13
|
} from "../core/index.ts";
|
|
15
14
|
/** Create one router per server request, await this, then render the provider. */
|
|
@@ -39,7 +38,7 @@ export async function loadRouter(
|
|
|
39
38
|
loader: wrap(route.loader),
|
|
40
39
|
action: wrap(route.action),
|
|
41
40
|
}));
|
|
42
|
-
const router =
|
|
41
|
+
const router = createDataRouter(serverRoutes, { url });
|
|
43
42
|
|
|
44
43
|
await router.navigate(url);
|
|
45
44
|
|
|
@@ -53,7 +52,7 @@ export async function loadRouter(
|
|
|
53
52
|
route.path === snapshot.path && route.clientLoader && !route.loader,
|
|
54
53
|
)
|
|
55
54
|
) {
|
|
56
|
-
return
|
|
55
|
+
return createDataRouter(serverRoutes, {
|
|
57
56
|
url,
|
|
58
57
|
hydrationData: { ...snapshot, loaded: false, state: "loading" },
|
|
59
58
|
});
|
|
@@ -61,19 +60,7 @@ export async function loadRouter(
|
|
|
61
60
|
|
|
62
61
|
return router;
|
|
63
62
|
}
|
|
64
|
-
|
|
65
|
-
export function serialize(snapshot: Snapshot): string {
|
|
66
|
-
return JSON.stringify({
|
|
67
|
-
...snapshot,
|
|
68
|
-
error: snapshot.error && {
|
|
69
|
-
name: snapshot.error.name,
|
|
70
|
-
message: snapshot.error.message,
|
|
71
|
-
},
|
|
72
|
-
})
|
|
73
|
-
.replace(/</g, "\\u003c")
|
|
74
|
-
.replace(/\u2028/g, "\\u2028")
|
|
75
|
-
.replace(/\u2029/g, "\\u2029");
|
|
76
|
-
}
|
|
63
|
+
export { serialize } from "../core/index.ts";
|
|
77
64
|
|
|
78
65
|
/** Dispatch an HTTP data request to a registered server loader or action. */
|
|
79
66
|
export async function handleRequest(
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { builtinModules } from "node:module";
|
|
2
|
+
import type { Plugin } from "vite";
|
|
3
|
+
|
|
4
|
+
const builtins = new Set(builtinModules.flatMap((id) => [id, `node:${id}`]));
|
|
5
|
+
|
|
6
|
+
const forbiddenModules = [
|
|
7
|
+
/(?:vite-browser-external|__vite-browser-external)/,
|
|
8
|
+
/\/node_modules\/(?:react|react-dom|react-router|react-router-dom)\//,
|
|
9
|
+
/\/smolter\/dist\/(?:internal\/)?(?:server|vite|files)\//,
|
|
10
|
+
/\/smolter\/packages\/(?:server|vite)\/src\//,
|
|
11
|
+
/\/vite\/dist\/node\//,
|
|
12
|
+
/\/src\/[^?]*\.server\.[cm]?[jt]sx?(?:\?|$)/,
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
/** Check emitted modules, after tree shaking, rather than import strings. */
|
|
16
|
+
export function browserBoundary(): Plugin {
|
|
17
|
+
return {
|
|
18
|
+
name: "smolter-browser-boundary",
|
|
19
|
+
apply: "build",
|
|
20
|
+
applyToEnvironment: (environment) => environment.name === "client",
|
|
21
|
+
generateBundle(_options, bundle) {
|
|
22
|
+
for (const output of Object.values(bundle)) {
|
|
23
|
+
if (output.type !== "chunk") continue;
|
|
24
|
+
|
|
25
|
+
for (const id of [...output.imports, ...output.dynamicImports]) {
|
|
26
|
+
if (!builtins.has(id)) continue;
|
|
27
|
+
|
|
28
|
+
this.error(`Node builtin in browser chunk ${output.fileName}: ${id}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const id of Object.keys(output.modules)) {
|
|
32
|
+
const normalized = id.replaceAll("\\", "/");
|
|
33
|
+
|
|
34
|
+
if (!forbiddenModules.some((pattern) => pattern.test(normalized)))
|
|
35
|
+
continue;
|
|
36
|
+
|
|
37
|
+
this.error(`Unexpected browser module in ${output.fileName}: ${id}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { flatRoutes } from "../../files/index.ts";
|
|
4
|
+
import { type Plugin, transformWithOxc } from "vite";
|
|
5
|
+
|
|
6
|
+
// Reuse Smolter's file discovery and server-export stripping. Only the browser
|
|
7
|
+
// route manifest changes: matched routes load before their data is committed.
|
|
8
|
+
export function lazyFiles(directory: string): Plugin {
|
|
9
|
+
let root: string;
|
|
10
|
+
const modules = new Map<string, string>();
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
name: "smolter-lazy-routes",
|
|
14
|
+
enforce: "pre",
|
|
15
|
+
configResolved(config) {
|
|
16
|
+
root = resolve(config.root, directory);
|
|
17
|
+
},
|
|
18
|
+
generateBundle(_options, bundle) {
|
|
19
|
+
if (!modules.size) return;
|
|
20
|
+
|
|
21
|
+
const routes = [];
|
|
22
|
+
|
|
23
|
+
for (const chunk of Object.values(bundle)) {
|
|
24
|
+
if (chunk.type !== "chunk" || !chunk.facadeModuleId) continue;
|
|
25
|
+
|
|
26
|
+
const path = modules.get(chunk.facadeModuleId);
|
|
27
|
+
|
|
28
|
+
if (!path) continue;
|
|
29
|
+
|
|
30
|
+
const files = new Set<string>();
|
|
31
|
+
|
|
32
|
+
function visit(file: string) {
|
|
33
|
+
if (files.has(file)) return;
|
|
34
|
+
|
|
35
|
+
files.add(file);
|
|
36
|
+
|
|
37
|
+
const dependency = bundle[file];
|
|
38
|
+
|
|
39
|
+
if (dependency?.type !== "chunk") return;
|
|
40
|
+
|
|
41
|
+
for (const imported of dependency.imports) visit(imported);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
visit(chunk.fileName);
|
|
45
|
+
routes.push({ path, files: [...files] });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const paths = [...modules.values()];
|
|
49
|
+
|
|
50
|
+
routes.sort(
|
|
51
|
+
(left, right) => paths.indexOf(left.path) - paths.indexOf(right.path),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
this.emitFile({
|
|
55
|
+
type: "asset",
|
|
56
|
+
fileName: ".vite/route-assets.json",
|
|
57
|
+
source: JSON.stringify(routes),
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
async load(id, options) {
|
|
61
|
+
if (id !== "\0virtual:smolter/routes" || options?.ssr) return;
|
|
62
|
+
|
|
63
|
+
const routes = await flatRoutes(root);
|
|
64
|
+
|
|
65
|
+
if (routes instanceof Error) return this.error(routes.message);
|
|
66
|
+
|
|
67
|
+
const definitions: string[] = [];
|
|
68
|
+
|
|
69
|
+
for (const route of routes) {
|
|
70
|
+
this.addWatchFile(route.file);
|
|
71
|
+
|
|
72
|
+
const source = await readFile(route.file, "utf8");
|
|
73
|
+
const transformed = await transformWithOxc(source, route.file);
|
|
74
|
+
const ast = this.parse(transformed.code);
|
|
75
|
+
const fields: Record<string, string | boolean> = { path: route.path };
|
|
76
|
+
|
|
77
|
+
for (const statement of ast.body) {
|
|
78
|
+
if (statement.type === "ExportDefaultDeclaration") {
|
|
79
|
+
fields.default = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (statement.type !== "ExportNamedDeclaration") continue;
|
|
84
|
+
|
|
85
|
+
if (statement.declaration?.type === "FunctionDeclaration") {
|
|
86
|
+
const name = statement.declaration.id?.name;
|
|
87
|
+
|
|
88
|
+
if (name) fields[name] = true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const specifier of statement.specifiers) {
|
|
92
|
+
const name =
|
|
93
|
+
specifier.exported.type === "Identifier"
|
|
94
|
+
? specifier.exported.name
|
|
95
|
+
: specifier.exported.value;
|
|
96
|
+
|
|
97
|
+
fields[name] = true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (statement.declaration?.type !== "VariableDeclaration") continue;
|
|
101
|
+
|
|
102
|
+
for (const declaration of statement.declaration.declarations) {
|
|
103
|
+
if (declaration.id.type === "Identifier")
|
|
104
|
+
fields[declaration.id.name] = true;
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
declaration.id.type !== "Identifier" ||
|
|
108
|
+
declaration.init?.type !== "Literal" ||
|
|
109
|
+
typeof declaration.init.value !== "string"
|
|
110
|
+
)
|
|
111
|
+
continue;
|
|
112
|
+
|
|
113
|
+
const name = declaration.id.name;
|
|
114
|
+
|
|
115
|
+
if (name === "id") fields.id = declaration.init.value;
|
|
116
|
+
|
|
117
|
+
if (name === "routePath") fields.path = declaration.init.value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const file = `${route.file.replaceAll("\\", "/")}?smolter-client`;
|
|
122
|
+
|
|
123
|
+
if (fields.default) modules.set(file, String(fields.path));
|
|
124
|
+
|
|
125
|
+
definitions.push(
|
|
126
|
+
`lazyRoute(${JSON.stringify(fields)}, () => import(${JSON.stringify(file)}))`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return [
|
|
131
|
+
'import { lazyRoute } from "@kasperrt/smolter/preact/lazy-route";',
|
|
132
|
+
`export default [${definitions.join(",\n")}];`,
|
|
133
|
+
].join("\n");
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { send, webRequest } from "../../server/http.ts";
|
|
2
|
+
import { routerPlugin } from "../index.ts";
|
|
3
|
+
import type { Plugin, UserConfig } from "vite";
|
|
4
|
+
import { browserBoundary } from "./browser-boundary.ts";
|
|
5
|
+
import { lazyFiles } from "./lazy-files.ts";
|
|
6
|
+
|
|
7
|
+
export interface ApplicationOptions {
|
|
8
|
+
directory?: string;
|
|
9
|
+
ssr?: boolean;
|
|
10
|
+
splitRoutes?: boolean;
|
|
11
|
+
resourceRequest?: (url: string) => boolean;
|
|
12
|
+
}
|
|
13
|
+
export const preactConfig: UserConfig = {
|
|
14
|
+
resolve: {
|
|
15
|
+
alias: [
|
|
16
|
+
{
|
|
17
|
+
find: /^react\/jsx-dev-runtime$/,
|
|
18
|
+
replacement: "preact/jsx-dev-runtime",
|
|
19
|
+
},
|
|
20
|
+
{ find: /^react\/jsx-runtime$/, replacement: "preact/jsx-runtime" },
|
|
21
|
+
{ find: /^react-dom\/client$/, replacement: "preact/compat/client" },
|
|
22
|
+
{ find: /^react-dom\/server$/, replacement: "preact-render-to-string" },
|
|
23
|
+
{ find: /^react-dom$/, replacement: "preact/compat" },
|
|
24
|
+
{ find: /^react$/, replacement: "preact/compat" },
|
|
25
|
+
],
|
|
26
|
+
dedupe: ["preact", "@kasperrt/smolter"],
|
|
27
|
+
},
|
|
28
|
+
ssr: {
|
|
29
|
+
noExternal: ["@kasperrt/smolter"],
|
|
30
|
+
},
|
|
31
|
+
optimizeDeps: {
|
|
32
|
+
include: [
|
|
33
|
+
"preact",
|
|
34
|
+
"preact/hooks",
|
|
35
|
+
"preact/compat",
|
|
36
|
+
"preact/jsx-runtime",
|
|
37
|
+
"@kasperrt/smolter/preact",
|
|
38
|
+
"@kasperrt/smolter/preact/lazy-route",
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function applicationPlugins({
|
|
44
|
+
directory = "src/routes",
|
|
45
|
+
ssr = true,
|
|
46
|
+
splitRoutes = true,
|
|
47
|
+
resourceRequest,
|
|
48
|
+
}: ApplicationOptions = {}): Plugin[] {
|
|
49
|
+
const application: Plugin = {
|
|
50
|
+
name: "smolter-application",
|
|
51
|
+
enforce: "pre",
|
|
52
|
+
config: () => preactConfig,
|
|
53
|
+
transformIndexHtml: {
|
|
54
|
+
order: "post",
|
|
55
|
+
handler(html, context) {
|
|
56
|
+
if (!context.server || !ssr) return html;
|
|
57
|
+
|
|
58
|
+
// Vite prepends its client ahead of the SSR head. That shifts Preact's
|
|
59
|
+
// hydration order and detaches loaded stylesheets while moving nodes.
|
|
60
|
+
// Keep development-only scripts after the application document instead.
|
|
61
|
+
const client = html.match(
|
|
62
|
+
/\s*<script\b[^>]*src="[^"]*\/@vite\/client"[^>]*><\/script>\s*/,
|
|
63
|
+
)?.[0];
|
|
64
|
+
|
|
65
|
+
if (!client) return html;
|
|
66
|
+
|
|
67
|
+
return html
|
|
68
|
+
.replace(client, "")
|
|
69
|
+
.replace("</body>", `${client.trim()}</body>`);
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
configureServer(server) {
|
|
73
|
+
// Own the HTTP adapter so redirects retain authentication cookies.
|
|
74
|
+
server.middlewares.use((incoming, outgoing, next) => {
|
|
75
|
+
if (!incoming.url?.startsWith("/_smolter?")) return next();
|
|
76
|
+
|
|
77
|
+
const run = async () => {
|
|
78
|
+
const target = new URL(
|
|
79
|
+
incoming.url!,
|
|
80
|
+
"http://local",
|
|
81
|
+
).searchParams.get("url");
|
|
82
|
+
|
|
83
|
+
if (!target?.startsWith("/") || target.startsWith("//"))
|
|
84
|
+
return send(
|
|
85
|
+
new Response("Invalid route URL", { status: 400 }),
|
|
86
|
+
outgoing,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const request = await webRequest(incoming, outgoing, target);
|
|
90
|
+
const [{ default: routes }, { dispatchRoute }] = await Promise.all([
|
|
91
|
+
server.ssrLoadModule("virtual:smolter/routes"),
|
|
92
|
+
server.ssrLoadModule("@kasperrt/smolter/preact/server"),
|
|
93
|
+
]);
|
|
94
|
+
await send(await dispatchRoute(request, routes), outgoing);
|
|
95
|
+
};
|
|
96
|
+
void run().catch(next);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!ssr) return;
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
server.middlewares.use((incoming, outgoing, next) => {
|
|
103
|
+
if (
|
|
104
|
+
!incoming.headers.accept?.includes("text/html") &&
|
|
105
|
+
!resourceRequest?.(incoming.originalUrl ?? incoming.url ?? "")
|
|
106
|
+
)
|
|
107
|
+
return next();
|
|
108
|
+
|
|
109
|
+
const run = async () => {
|
|
110
|
+
const request = await webRequest(incoming, outgoing);
|
|
111
|
+
const entry = await server.ssrLoadModule("/src/entry.server.tsx");
|
|
112
|
+
const response: Response = await entry.default(
|
|
113
|
+
request,
|
|
114
|
+
"/src/entry.client.tsx",
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (!response.headers.get("Content-Type")?.includes("text/html"))
|
|
118
|
+
return send(response, outgoing);
|
|
119
|
+
|
|
120
|
+
const html = await server.transformIndexHtml(
|
|
121
|
+
incoming.url!,
|
|
122
|
+
await response.text(),
|
|
123
|
+
);
|
|
124
|
+
await send(
|
|
125
|
+
new Response(html, {
|
|
126
|
+
status: response.status,
|
|
127
|
+
headers: response.headers,
|
|
128
|
+
}),
|
|
129
|
+
outgoing,
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
void run().catch(next);
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return [
|
|
139
|
+
application,
|
|
140
|
+
browserBoundary(),
|
|
141
|
+
...(splitRoutes ? [lazyFiles(directory)] : []),
|
|
142
|
+
routerPlugin({ directory, framework: "preact" }),
|
|
143
|
+
];
|
|
144
|
+
}
|
|
@@ -8,6 +8,7 @@ export interface FileRouterOptions {
|
|
|
8
8
|
directory?: string;
|
|
9
9
|
clientRoutes?: string;
|
|
10
10
|
serverRoutes?: string;
|
|
11
|
+
framework?: "react" | "preact";
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/** Build-time flat route discovery and the optional server data transport. */
|
|
@@ -15,6 +16,7 @@ export function routerPlugin({
|
|
|
15
16
|
directory = "src/routes",
|
|
16
17
|
clientRoutes,
|
|
17
18
|
serverRoutes,
|
|
19
|
+
framework = "react",
|
|
18
20
|
}: FileRouterOptions = {}): Plugin {
|
|
19
21
|
let root: string;
|
|
20
22
|
let projectRoot: string;
|
|
@@ -173,7 +175,7 @@ export function routerPlugin({
|
|
|
173
175
|
definitions.some((definition) => definition.startsWith("serverRoute("))
|
|
174
176
|
)
|
|
175
177
|
imports.unshift(
|
|
176
|
-
|
|
178
|
+
`import { serverRoute } from "@kasperrt/smolter/${framework}/client";`,
|
|
177
179
|
);
|
|
178
180
|
|
|
179
181
|
return `${imports.join("\n")}\nexport default [${definitions.join(",\n")}];\n`;
|
|
@@ -201,7 +203,7 @@ export function routerPlugin({
|
|
|
201
203
|
server.watcher.off("unlink", refresh);
|
|
202
204
|
});
|
|
203
205
|
|
|
204
|
-
configureTransport(server, publicId);
|
|
206
|
+
configureTransport(server, publicId, framework);
|
|
205
207
|
},
|
|
206
208
|
};
|
|
207
209
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Optional flat-file routing for Preact builds. @module */
|
|
2
|
+
|
|
3
|
+
import type { Plugin } from "vite";
|
|
4
|
+
import {
|
|
5
|
+
routerPlugin as createPlugin,
|
|
6
|
+
type FileRouterOptions,
|
|
7
|
+
} from "./index.ts";
|
|
8
|
+
|
|
9
|
+
export * from "../files/index.ts";
|
|
10
|
+
export type { FileRouterOptions } from "./index.ts";
|
|
11
|
+
|
|
12
|
+
/** Configure flat routes and server transport for Preact. */
|
|
13
|
+
export function routerPlugin(options: FileRouterOptions = {}): Plugin {
|
|
14
|
+
return createPlugin({ ...options, framework: "preact" });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** File routing is the default; this alias keeps configuration explicit. */
|
|
18
|
+
export const fileRouter = routerPlugin;
|
|
19
|
+
|
|
20
|
+
export type { ApplicationOptions } from "./application/vite.ts";
|
|
21
|
+
export {
|
|
22
|
+
applicationPlugins,
|
|
23
|
+
preactConfig,
|
|
24
|
+
} from "./application/vite.ts";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Optional flat-file routing for React builds. @module */
|
|
2
|
+
|
|
3
|
+
import type { Plugin } from "vite";
|
|
4
|
+
import {
|
|
5
|
+
routerPlugin as createPlugin,
|
|
6
|
+
type FileRouterOptions,
|
|
7
|
+
} from "./index.ts";
|
|
8
|
+
|
|
9
|
+
export * from "../files/index.ts";
|
|
10
|
+
export type { FileRouterOptions } from "./index.ts";
|
|
11
|
+
|
|
12
|
+
/** Configure flat routes and server transport for React. */
|
|
13
|
+
export function routerPlugin(options: FileRouterOptions = {}): Plugin {
|
|
14
|
+
return createPlugin({ ...options, framework: "react" });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** File routing is the default; this alias keeps configuration explicit. */
|
|
18
|
+
export const fileRouter = routerPlugin;
|
|
@@ -3,6 +3,7 @@ import type { ViteDevServer } from "vite";
|
|
|
3
3
|
export function configureTransport(
|
|
4
4
|
server: ViteDevServer,
|
|
5
5
|
publicId: string,
|
|
6
|
+
framework: "react" | "preact" = "react",
|
|
6
7
|
): void {
|
|
7
8
|
server.middlewares.use((incoming, outgoing, next) => {
|
|
8
9
|
if (!incoming.url?.startsWith("/_smolter?")) return next();
|
|
@@ -43,7 +44,7 @@ export function configureTransport(
|
|
|
43
44
|
});
|
|
44
45
|
const [{ default: routes }, { handleRequest }] = await Promise.all([
|
|
45
46
|
server.ssrLoadModule(publicId),
|
|
46
|
-
server.ssrLoadModule(
|
|
47
|
+
server.ssrLoadModule(`@kasperrt/smolter/${framework}/server`),
|
|
47
48
|
]);
|
|
48
49
|
const response: Response = await handleRequest(routes, request);
|
|
49
50
|
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Migration from 0.2.0
|
|
6
|
+
|
|
7
|
+
- **Breaking:** use `createDataRouter` for loaders, actions, fetchers and revalidation. `createRouter` now creates a navigation-only router. Framework imports remain `@kasperrt/smolter/react` and `@kasperrt/smolter/preact`.
|
|
8
|
+
- **Breaking:** the Preact adapter requires Preact 11.0.1 or newer.
|
|
9
|
+
- Optional Preact application helpers use explicit `/preact/client`, `/preact/document`, `/preact/lazy-route` and `/preact/server` entry points. Server code and Vite tooling remain outside browser bundles.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Navigation-only engines that tree-shake out data handling: 1,245 B React and 1,405 B Preact gzip with the documented routing subset.
|
|
14
|
+
- Preact application SSR and hydration, lazy flat routes, retained route content while loading, prefetching, and controlled view transitions.
|
|
15
|
+
- Express-compatible request/response helpers and documented Vite virtual-module and entry-file contracts.
|
|
16
|
+
- Route-targeted revalidation, including explicitly configured root data, and independent fetcher reads.
|
|
17
|
+
- Optional React resource hooks through `/react/data`.
|
|
18
|
+
- Detailed guides for routing, handlers, forms, fetchers, SSR, Express, bundle sizes and every public export.
|
|
19
|
+
|
|
20
|
+
### Fixed and improved
|
|
21
|
+
|
|
22
|
+
- Serialize Preact fetcher GET submissions into the destination query without navigating or invoking an action.
|
|
23
|
+
- Share form submission and navigation interception logic between adapters.
|
|
24
|
+
- Preserve cancellation, error hydration and stable data during navigation and revalidation.
|
|
25
|
+
- Separate example handler and boundary files; use route-appropriate loading placeholders.
|
|
26
|
+
- Build the publishable package and site through Vite with preserved module boundaries, production smoke tests and 100% implementation line coverage.
|
|
27
|
+
- Generate honest size comparisons for routing-only, data, transport and application entry points.
|
|
28
|
+
|
|
29
|
+
## 0.2.0
|
|
30
|
+
|
|
31
|
+
- Breaking: choose `@kasperrt/smolter/react` or `@kasperrt/smolter/preact` for routing APIs and types.
|
|
32
|
+
- Move optional transport, SSR, and build tools under the selected renderer’s `/client`, `/server`, and `/vite` subpaths. Remove root and top-level core/client/server/files/vite exports.
|
|
33
|
+
- Keep browser-safe helpers in `@kasperrt/smolter/utils`.
|
|
34
|
+
- Verify unused exports and server/build modules are removed from browser bundles.
|
|
35
|
+
|
|
36
|
+
## 0.0.1
|
|
37
|
+
|
|
38
|
+
- First stable release of `@kasperrt/smolter` for React and Preact.
|
|
39
|
+
- Typed loaders, actions, client handlers, fetchers, and controlled revalidation.
|
|
40
|
+
- Manual and flat file routes, server transport, SSR, and hydration.
|
|
41
|
+
- Tokenless npm and JSR publishing through tested GitHub Actions.
|
|
42
|
+
|
|
43
|
+
## 0.0.1-rc.2
|
|
44
|
+
|
|
45
|
+
- Use `@kasperrt/smolter` explicitly in every public import and generated route module.
|
|
46
|
+
- Remove the unscoped install alias from examples and documentation.
|
|
47
|
+
|
|
48
|
+
## 0.0.1-rc.1
|
|
49
|
+
|
|
50
|
+
- Typed React and Preact routing with loaders, actions, and independent fetchers.
|
|
51
|
+
- Server handlers with optional clientLoader and clientAction call-through.
|
|
52
|
+
- Manual route tables and optional flat file routing.
|
|
53
|
+
- Server rendering, hydration, and controlled loader revalidation.
|
|
54
|
+
- Four standalone examples and shared UI.
|