@kasperrt/smolter 0.2.0 → 0.3.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.
Files changed (270) hide show
  1. package/.generated/client/deployment.ts +161 -0
  2. package/{src/internal → .generated}/client/index.ts +3 -0
  3. package/.generated/core/environment.ts +43 -0
  4. package/{src/internal → .generated}/core/errors.ts +1 -1
  5. package/{src/internal → .generated}/core/index.ts +1 -0
  6. package/.generated/core/navigation-router.ts +107 -0
  7. package/.generated/core/navigation.ts +147 -0
  8. package/.generated/core/presentation.ts +108 -0
  9. package/.generated/core/resource-types.ts +44 -0
  10. package/.generated/core/resources.ts +444 -0
  11. package/.generated/core/responses.ts +86 -0
  12. package/{src/internal → .generated}/core/router.ts +136 -63
  13. package/{src/internal → .generated}/core/serialize.ts +7 -2
  14. package/.generated/core/store.ts +25 -0
  15. package/.generated/core/submission.ts +116 -0
  16. package/{src/internal → .generated}/core/types.ts +18 -1
  17. package/.generated/core/url.ts +29 -0
  18. package/.generated/core/utils.ts +5 -0
  19. package/.generated/preact/await.tsx +41 -0
  20. package/.generated/preact/client.tsx +93 -0
  21. package/.generated/preact/context.ts +59 -0
  22. package/.generated/preact/document.tsx +198 -0
  23. package/{src/internal → .generated}/preact/fetcher.ts +43 -23
  24. package/.generated/preact/form.ts +58 -0
  25. package/{src/internal → .generated}/preact/hooks.ts +25 -28
  26. package/.generated/preact/index.ts +34 -0
  27. package/.generated/preact/lazy-route.ts +100 -0
  28. package/.generated/preact/navigation-state.ts +9 -0
  29. package/.generated/preact/navigation.tsx +147 -0
  30. package/.generated/preact/root-context.ts +21 -0
  31. package/.generated/preact/root-data.ts +45 -0
  32. package/.generated/preact/route-hooks.tsx +170 -0
  33. package/.generated/preact/route-types.ts +76 -0
  34. package/.generated/preact/router.tsx +107 -0
  35. package/{src/internal → .generated}/preact/routes.ts +17 -9
  36. package/.generated/react/context.ts +59 -0
  37. package/.generated/react/data.ts +25 -0
  38. package/{src/internal → .generated}/react/fetcher.ts +13 -11
  39. package/.generated/react/form.ts +58 -0
  40. package/{src/internal → .generated}/react/hooks.ts +9 -14
  41. package/{src/internal/preact → .generated/react}/index.ts +5 -1
  42. package/{src/internal → .generated}/react/link.ts +13 -21
  43. package/.generated/react/resources/components.tsx +404 -0
  44. package/{src/internal → .generated}/react/routes.ts +17 -9
  45. package/.generated/server/application.tsx +185 -0
  46. package/.generated/server/assets.ts +19 -0
  47. package/.generated/server/handler.ts +69 -0
  48. package/.generated/server/http.ts +55 -0
  49. package/{src/internal → .generated}/server/index.ts +3 -3
  50. package/.generated/server/node.ts +4 -0
  51. package/.generated/server/preact.ts +7 -0
  52. package/.generated/vite/application/browser-boundary.ts +42 -0
  53. package/.generated/vite/application/lazy-files.ts +138 -0
  54. package/.generated/vite/application/vite.ts +144 -0
  55. package/.generated/vite/deployment.ts +46 -0
  56. package/{src/internal → .generated}/vite/index.ts +11 -4
  57. package/{src/preact-vite.ts → .generated/vite/preact.ts} +14 -3
  58. package/{src/react-vite.ts → .generated/vite/react.ts} +8 -3
  59. package/CHANGELOG.md +63 -0
  60. package/README.md +277 -35
  61. package/dist/client/deployment.d.ts +12 -0
  62. package/dist/client/deployment.js +102 -0
  63. package/dist/client/deserialize.js +10 -0
  64. package/dist/{internal/client → client}/index.d.ts +2 -0
  65. package/dist/client/index.js +47 -0
  66. package/dist/core/environment.d.ts +12 -0
  67. package/dist/core/environment.js +23 -0
  68. package/dist/core/errors.js +7 -0
  69. package/dist/{internal/core → core}/index.d.ts +1 -0
  70. package/dist/core/match.js +33 -0
  71. package/dist/core/navigation-router.d.ts +15 -0
  72. package/dist/core/navigation-router.js +64 -0
  73. package/dist/core/navigation.d.ts +15 -0
  74. package/dist/core/navigation.js +92 -0
  75. package/dist/core/presentation.d.ts +5 -0
  76. package/dist/core/presentation.js +69 -0
  77. package/dist/core/resource-types.d.ts +37 -0
  78. package/dist/core/resources.d.ts +19 -0
  79. package/dist/core/resources.js +345 -0
  80. package/dist/core/responses.d.ts +15 -0
  81. package/dist/core/responses.js +48 -0
  82. package/dist/{internal/core → core}/router.d.ts +1 -1
  83. package/dist/core/router.js +204 -0
  84. package/dist/{internal/core → core}/serialize.d.ts +2 -0
  85. package/dist/core/serialize.js +17 -0
  86. package/dist/core/store.d.ts +6 -0
  87. package/dist/core/store.js +21 -0
  88. package/dist/core/submission.d.ts +30 -0
  89. package/dist/core/submission.js +41 -0
  90. package/dist/{internal/core → core}/types.d.ts +16 -1
  91. package/dist/core/url.d.ts +7 -0
  92. package/dist/core/url.js +12 -0
  93. package/dist/core/utils.d.ts +5 -0
  94. package/dist/core/utils.js +5 -0
  95. package/dist/files/index.js +64 -0
  96. package/dist/preact/await.d.ts +7 -0
  97. package/dist/preact/await.js +19 -0
  98. package/dist/preact/client.d.ts +7 -0
  99. package/dist/preact/client.js +68 -0
  100. package/dist/preact/context.d.ts +18 -0
  101. package/dist/preact/context.js +23 -0
  102. package/dist/preact/document.d.ts +15 -0
  103. package/dist/preact/document.js +155 -0
  104. package/dist/{internal/preact → preact}/fetcher.d.ts +9 -5
  105. package/dist/preact/fetcher.js +45 -0
  106. package/dist/{internal/preact → preact}/form.d.ts +5 -6
  107. package/dist/preact/form.js +21 -0
  108. package/dist/{internal/preact → preact}/hooks.d.ts +7 -6
  109. package/dist/preact/hooks.js +25 -0
  110. package/dist/preact/index.d.ts +18 -0
  111. package/dist/preact/index.js +15 -0
  112. package/dist/preact/lazy-route.d.ts +19 -0
  113. package/dist/preact/lazy-route.js +51 -0
  114. package/dist/preact/navigation-state.d.ts +2 -0
  115. package/dist/preact/navigation-state.js +9 -0
  116. package/dist/preact/navigation.d.ts +28 -0
  117. package/dist/preact/navigation.js +60 -0
  118. package/dist/preact/root-context.d.ts +13 -0
  119. package/dist/preact/root-context.js +9 -0
  120. package/dist/preact/root-data.d.ts +8 -0
  121. package/dist/preact/root-data.js +28 -0
  122. package/dist/preact/route-hooks.d.ts +32 -0
  123. package/dist/preact/route-hooks.js +94 -0
  124. package/dist/preact/route-types.d.ts +54 -0
  125. package/dist/preact/router.d.ts +16 -0
  126. package/dist/preact/router.js +63 -0
  127. package/dist/{internal/preact → preact}/routes.d.ts +1 -1
  128. package/dist/preact/routes.js +12 -0
  129. package/dist/{internal/preact → preact}/runtime.js +1 -1
  130. package/dist/react/context.d.ts +18 -0
  131. package/dist/react/context.js +23 -0
  132. package/dist/react/data.d.ts +5 -0
  133. package/dist/react/data.js +3 -0
  134. package/dist/{internal/react → react}/fetcher.d.ts +2 -2
  135. package/dist/react/fetcher.js +34 -0
  136. package/dist/{internal/react → react}/form.d.ts +5 -6
  137. package/dist/react/form.js +21 -0
  138. package/dist/react/hooks.js +25 -0
  139. package/dist/{internal/react → react}/index.d.ts +1 -1
  140. package/dist/react/index.js +10 -0
  141. package/dist/{internal/react → react}/link.d.ts +2 -2
  142. package/dist/react/link.js +27 -0
  143. package/dist/react/resources/components.d.ts +64 -0
  144. package/dist/react/resources/components.js +239 -0
  145. package/dist/{internal/react → react}/routes.d.ts +1 -1
  146. package/dist/react/routes.js +12 -0
  147. package/dist/react/runtime.js +1 -0
  148. package/dist/server/application.d.ts +4 -0
  149. package/dist/server/application.js +119 -0
  150. package/dist/server/assets.d.ts +5 -0
  151. package/dist/server/assets.js +7 -0
  152. package/dist/server/handler.d.ts +19 -0
  153. package/dist/server/handler.js +22 -0
  154. package/dist/server/http.d.ts +4 -0
  155. package/dist/server/http.js +27 -0
  156. package/dist/server/index.js +76 -0
  157. package/dist/server/node.d.ts +3 -0
  158. package/dist/server/node.js +3 -0
  159. package/dist/server/preact.d.ts +3 -0
  160. package/dist/server/preact.js +4 -0
  161. package/dist/vite/application/browser-boundary.d.ts +3 -0
  162. package/dist/vite/application/browser-boundary.js +35 -0
  163. package/dist/vite/application/lazy-files.d.ts +2 -0
  164. package/dist/vite/application/lazy-files.js +88 -0
  165. package/dist/vite/application/vite.d.ts +9 -0
  166. package/dist/vite/application/vite.js +101 -0
  167. package/dist/vite/deployment.d.ts +8 -0
  168. package/dist/vite/deployment.js +36 -0
  169. package/dist/vite/index.js +109 -0
  170. package/dist/{preact-vite.d.ts → vite/preact.d.ts} +6 -3
  171. package/dist/vite/preact.js +16 -0
  172. package/dist/{react-vite.d.ts → vite/react.d.ts} +4 -3
  173. package/dist/vite/react.js +15 -0
  174. package/dist/vite/transport.js +35 -0
  175. package/jsr.json +28 -16
  176. package/package.json +115 -35
  177. package/dist/client.d.ts +0 -2
  178. package/dist/client.js +0 -2
  179. package/dist/core.d.ts +0 -2
  180. package/dist/core.js +0 -2
  181. package/dist/files.d.ts +0 -2
  182. package/dist/files.js +0 -2
  183. package/dist/internal/client/deserialize.js +0 -11
  184. package/dist/internal/client/index.js +0 -44
  185. package/dist/internal/core/errors.js +0 -6
  186. package/dist/internal/core/index.js +0 -7
  187. package/dist/internal/core/match.js +0 -27
  188. package/dist/internal/core/router.js +0 -189
  189. package/dist/internal/core/serialize.js +0 -13
  190. package/dist/internal/core/types.js +0 -5
  191. package/dist/internal/core/url.d.ts +0 -2
  192. package/dist/internal/core/url.js +0 -6
  193. package/dist/internal/files/index.js +0 -77
  194. package/dist/internal/preact/context.d.ts +0 -18
  195. package/dist/internal/preact/context.js +0 -20
  196. package/dist/internal/preact/fetcher.js +0 -28
  197. package/dist/internal/preact/form.js +0 -41
  198. package/dist/internal/preact/hooks.js +0 -35
  199. package/dist/internal/preact/index.d.ts +0 -9
  200. package/dist/internal/preact/index.js +0 -7
  201. package/dist/internal/preact/link.d.ts +0 -12
  202. package/dist/internal/preact/link.js +0 -28
  203. package/dist/internal/preact/routes.js +0 -22
  204. package/dist/internal/react/context.d.ts +0 -18
  205. package/dist/internal/react/context.js +0 -20
  206. package/dist/internal/react/fetcher.js +0 -28
  207. package/dist/internal/react/form.js +0 -41
  208. package/dist/internal/react/hooks.js +0 -35
  209. package/dist/internal/react/index.js +0 -7
  210. package/dist/internal/react/link.js +0 -28
  211. package/dist/internal/react/routes.js +0 -22
  212. package/dist/internal/react/runtime.js +0 -2
  213. package/dist/internal/server/index.js +0 -69
  214. package/dist/internal/vite/index.js +0 -132
  215. package/dist/internal/vite/transport.js +0 -42
  216. package/dist/preact-client.d.ts +0 -2
  217. package/dist/preact-client.js +0 -2
  218. package/dist/preact-server.d.ts +0 -2
  219. package/dist/preact-server.js +0 -2
  220. package/dist/preact-vite.js +0 -9
  221. package/dist/preact.d.ts +0 -2
  222. package/dist/preact.js +0 -2
  223. package/dist/react-client.d.ts +0 -2
  224. package/dist/react-client.js +0 -2
  225. package/dist/react-server.d.ts +0 -2
  226. package/dist/react-server.js +0 -2
  227. package/dist/react-vite.js +0 -9
  228. package/dist/react.d.ts +0 -2
  229. package/dist/react.js +0 -2
  230. package/dist/server.d.ts +0 -2
  231. package/dist/server.js +0 -2
  232. package/dist/utils.d.ts +0 -5
  233. package/dist/utils.js +0 -5
  234. package/dist/vite.d.ts +0 -2
  235. package/dist/vite.js +0 -2
  236. package/src/client.ts +0 -2
  237. package/src/core.ts +0 -2
  238. package/src/files.ts +0 -2
  239. package/src/internal/core/url.ts +0 -6
  240. package/src/internal/preact/context.ts +0 -46
  241. package/src/internal/preact/form.ts +0 -79
  242. package/src/internal/preact/link.ts +0 -46
  243. package/src/internal/react/context.ts +0 -46
  244. package/src/internal/react/form.ts +0 -79
  245. package/src/internal/react/index.ts +0 -13
  246. package/src/preact-client.ts +0 -2
  247. package/src/preact-server.ts +0 -2
  248. package/src/preact.ts +0 -2
  249. package/src/react-client.ts +0 -2
  250. package/src/react-server.ts +0 -2
  251. package/src/react.ts +0 -2
  252. package/src/server.ts +0 -2
  253. package/src/utils.ts +0 -5
  254. package/src/vite.ts +0 -2
  255. /package/{src/internal → .generated}/client/deserialize.ts +0 -0
  256. /package/{src/internal → .generated}/core/match.ts +0 -0
  257. /package/{src/internal → .generated}/files/index.ts +0 -0
  258. /package/{src/internal → .generated}/preact/runtime.ts +0 -0
  259. /package/{src/internal → .generated}/react/runtime.ts +0 -0
  260. /package/{src/internal → .generated}/vite/transport.ts +0 -0
  261. /package/dist/{internal/client → client}/deserialize.d.ts +0 -0
  262. /package/dist/{internal/core → core}/errors.d.ts +0 -0
  263. /package/dist/{internal/core → core}/match.d.ts +0 -0
  264. /package/dist/{internal/files → files}/index.d.ts +0 -0
  265. /package/dist/{internal/preact → preact}/runtime.d.ts +0 -0
  266. /package/dist/{internal/react → react}/hooks.d.ts +0 -0
  267. /package/dist/{internal/react → react}/runtime.d.ts +0 -0
  268. /package/dist/{internal/server → server}/index.d.ts +0 -0
  269. /package/dist/{internal/vite → vite}/index.d.ts +0 -0
  270. /package/dist/{internal/vite → vite}/transport.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,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import {
7
- createRouter,
7
+ createDataRouter,
8
8
  type DataFunction,
9
9
  matchRoutes,
10
10
  type Route,
@@ -38,7 +38,7 @@ export async function loadRouter(
38
38
  loader: wrap(route.loader),
39
39
  action: wrap(route.action),
40
40
  }));
41
- const router = createRouter(serverRoutes, { url });
41
+ const router = createDataRouter(serverRoutes, { url });
42
42
 
43
43
  await router.navigate(url);
44
44
 
@@ -52,7 +52,7 @@ export async function loadRouter(
52
52
  route.path === snapshot.path && route.clientLoader && !route.loader,
53
53
  )
54
54
  ) {
55
- return createRouter(serverRoutes, {
55
+ return createDataRouter(serverRoutes, {
56
56
  url,
57
57
  hydrationData: { ...snapshot, loaded: false, state: "loading" },
58
58
  });
@@ -0,0 +1,4 @@
1
+ /** Optional application integration. @module */
2
+
3
+ export * from "./handler.ts";
4
+ export * from "./http.ts";
@@ -0,0 +1,7 @@
1
+ /** Preact server rendering and HTTP data dispatch. @module */
2
+
3
+ export {
4
+ dispatchRoute,
5
+ renderApplication,
6
+ } from "./application.tsx";
7
+ export * from "./index.ts";
@@ -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,138 @@
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
+ modules.clear();
68
+
69
+ const definitions: string[] = [];
70
+
71
+ for (const route of routes) {
72
+ this.addWatchFile(route.file);
73
+
74
+ const source = await readFile(route.file, "utf8");
75
+ const transformed = await transformWithOxc(source, route.file);
76
+ const ast = this.parse(transformed.code);
77
+ const fields: Record<string, string | boolean> = { path: route.path };
78
+
79
+ for (const statement of ast.body) {
80
+ if (statement.type === "ExportDefaultDeclaration") {
81
+ fields.default = true;
82
+ continue;
83
+ }
84
+
85
+ if (statement.type !== "ExportNamedDeclaration") continue;
86
+
87
+ if (statement.declaration?.type === "FunctionDeclaration") {
88
+ const name = statement.declaration.id?.name;
89
+
90
+ if (name) fields[name] = true;
91
+ }
92
+
93
+ for (const specifier of statement.specifiers) {
94
+ const name =
95
+ specifier.exported.type === "Identifier"
96
+ ? specifier.exported.name
97
+ : specifier.exported.value;
98
+
99
+ fields[name] = true;
100
+ }
101
+
102
+ if (statement.declaration?.type !== "VariableDeclaration") continue;
103
+
104
+ for (const declaration of statement.declaration.declarations) {
105
+ if (declaration.id.type === "Identifier")
106
+ fields[declaration.id.name] = true;
107
+
108
+ if (
109
+ declaration.id.type !== "Identifier" ||
110
+ declaration.init?.type !== "Literal" ||
111
+ typeof declaration.init.value !== "string"
112
+ )
113
+ continue;
114
+
115
+ const name = declaration.id.name;
116
+
117
+ if (name === "id") fields.id = declaration.init.value;
118
+
119
+ if (name === "routePath") fields.path = declaration.init.value;
120
+ }
121
+ }
122
+
123
+ const file = `${route.file.replaceAll("\\", "/")}?smolter-client`;
124
+
125
+ if (fields.default) modules.set(file, String(fields.path));
126
+
127
+ definitions.push(
128
+ `lazyRoute(${JSON.stringify(fields)}, () => import(${JSON.stringify(file)}))`,
129
+ );
130
+ }
131
+
132
+ return [
133
+ 'import { lazyRoute } from "@kasperrt/smolter/preact/lazy-route";',
134
+ `export default [${definitions.join(",\n")}];`,
135
+ ].join("\n");
136
+ },
137
+ };
138
+ }
@@ -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
+ }
@@ -0,0 +1,46 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { Plugin } from "vite";
3
+
4
+ /** Deployment manifest configuration. Serve the emitted file with Cache-Control: no-store. */
5
+ export interface DeploymentPluginOptions {
6
+ /** Public URL, optionally on an asset host. Defaults to Vite base + smolter-manifest.json. */
7
+ manifestURL?: string;
8
+ }
9
+
10
+ /** Emit a build manifest and expose its identity through virtual:smolter/deployment. */
11
+ export function deploymentPlugin(
12
+ options: DeploymentPluginOptions = {},
13
+ ): Plugin {
14
+ const version = randomUUID();
15
+ const virtualId = "\0virtual:smolter/deployment";
16
+ let enabled = false;
17
+ let manifestURL = "";
18
+
19
+ return {
20
+ name: "smolter-deployment",
21
+ configResolved(config) {
22
+ enabled = config.command === "build" && !config.build.ssr;
23
+ manifestURL =
24
+ options.manifestURL ?? `${config.base}smolter-manifest.json`;
25
+ },
26
+ resolveId(id) {
27
+ if (id === "virtual:smolter/deployment") return virtualId;
28
+ },
29
+ load(id) {
30
+ if (id !== virtualId) return;
31
+
32
+ const deployment = enabled ? { version, manifestURL } : null;
33
+
34
+ return `export default ${JSON.stringify(deployment)};`;
35
+ },
36
+ generateBundle() {
37
+ if (!enabled) return;
38
+
39
+ this.emitFile({
40
+ type: "asset",
41
+ fileName: "smolter-manifest.json",
42
+ source: JSON.stringify({ version }),
43
+ });
44
+ },
45
+ };
46
+ }
@@ -1,5 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { resolve } from "node:path";
2
+ import { relative, resolve } from "node:path";
3
3
  import { flatRoutes } from "../files/index.ts";
4
4
  import { type Plugin, transformWithOxc } from "vite";
5
5
  import { configureTransport } from "./transport.ts";
@@ -185,11 +185,18 @@ export function routerPlugin({
185
185
  server.watcher.add(root);
186
186
 
187
187
  const refresh = (file: string) => {
188
- if (!file.startsWith(`${root}/`)) return;
188
+ if (clientRoutes || serverRoutes) return;
189
189
 
190
- const module = server.moduleGraph.getModuleById(virtualId);
190
+ const path = relative(root, file).replaceAll("\\", "/");
191
191
 
192
- if (module) server.moduleGraph.invalidateModule(module);
192
+ if (!/^(?:[^/]+\.[jt]sx?|[^/]+\/route\.[jt]sx?)$/.test(path)) return;
193
+ if (path.endsWith(".d.ts")) return;
194
+
195
+ for (const environment of Object.values(server.environments)) {
196
+ const module = environment.moduleGraph.getModuleById(virtualId);
197
+
198
+ if (module) environment.moduleGraph.invalidateModule(module);
199
+ }
193
200
 
194
201
  server.ws.send({ type: "full-reload" });
195
202
  };
@@ -4,10 +4,10 @@ import type { Plugin } from "vite";
4
4
  import {
5
5
  routerPlugin as createPlugin,
6
6
  type FileRouterOptions,
7
- } from "./internal/vite/index.ts";
7
+ } from "./index.ts";
8
8
 
9
- export * from "./internal/files/index.ts";
10
- export type { FileRouterOptions } from "./internal/vite/index.ts";
9
+ export * from "../files/index.ts";
10
+ export type { FileRouterOptions } from "./index.ts";
11
11
 
12
12
  /** Configure flat routes and server transport for Preact. */
13
13
  export function routerPlugin(options: FileRouterOptions = {}): Plugin {
@@ -16,3 +16,14 @@ export function routerPlugin(options: FileRouterOptions = {}): Plugin {
16
16
 
17
17
  /** File routing is the default; this alias keeps configuration explicit. */
18
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";
25
+
26
+ export {
27
+ type DeploymentPluginOptions,
28
+ deploymentPlugin,
29
+ } from "./deployment.ts";
@@ -4,10 +4,10 @@ import type { Plugin } from "vite";
4
4
  import {
5
5
  routerPlugin as createPlugin,
6
6
  type FileRouterOptions,
7
- } from "./internal/vite/index.ts";
7
+ } from "./index.ts";
8
8
 
9
- export * from "./internal/files/index.ts";
10
- export type { FileRouterOptions } from "./internal/vite/index.ts";
9
+ export * from "../files/index.ts";
10
+ export type { FileRouterOptions } from "./index.ts";
11
11
 
12
12
  /** Configure flat routes and server transport for React. */
13
13
  export function routerPlugin(options: FileRouterOptions = {}): Plugin {
@@ -16,3 +16,8 @@ export function routerPlugin(options: FileRouterOptions = {}): Plugin {
16
16
 
17
17
  /** File routing is the default; this alias keeps configuration explicit. */
18
18
  export const fileRouter = routerPlugin;
19
+
20
+ export {
21
+ type DeploymentPluginOptions,
22
+ deploymentPlugin,
23
+ } from "./deployment.ts";
package/CHANGELOG.md ADDED
@@ -0,0 +1,63 @@
1
+ # Changelog
2
+
3
+ ## 0.3.1
4
+
5
+ - Refresh browser and SSR file-route discovery on additions, renames and removals; preserve normal Vite HMR for colocated helpers and components.
6
+
7
+ - Add optional production deployment manifests through `deploymentPlugin` for both React and Preact Vite integrations.
8
+ - Add `watchDeployment` to both client entrypoints: check before navigation and Back/Forward, recover stale Vite chunks after confirmed build changes, and preserve the requested URL.
9
+ - Skip hash-only links, development builds and SSR; bound failed checks and prevent repeated reloads. Keep routing-only bundles unchanged.
10
+ - Document production cache headers, CDN/application-specific manifest paths, unsaved state and rollout limitations.
11
+
12
+ ## 0.3.0
13
+
14
+ ### Migration from 0.2.0
15
+
16
+ - **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`.
17
+ - **Breaking:** the Preact adapter requires Preact 11.0.1 or newer.
18
+ - 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.
19
+
20
+ ### Added
21
+
22
+ - Navigation-only engines that tree-shake out data handling: 1,245 B React and 1,405 B Preact gzip with the documented routing subset.
23
+ - Preact application SSR and hydration, lazy flat routes, retained route content while loading, prefetching, and controlled view transitions.
24
+ - Express-compatible request/response helpers and documented Vite virtual-module and entry-file contracts.
25
+ - Route-targeted revalidation, including explicitly configured root data, and independent fetcher reads.
26
+ - Optional React resource hooks through `/react/data`.
27
+ - Detailed guides for routing, handlers, forms, fetchers, SSR, Express, bundle sizes and every public export.
28
+
29
+ ### Fixed and improved
30
+
31
+ - Serialize Preact fetcher GET submissions into the destination query without navigating or invoking an action.
32
+ - Share form submission and navigation interception logic between adapters.
33
+ - Preserve cancellation, error hydration and stable data during navigation and revalidation.
34
+ - Separate example handler and boundary files; use route-appropriate loading placeholders.
35
+ - Build the publishable package and site through Vite with preserved module boundaries, production smoke tests and 100% implementation line coverage.
36
+ - Generate honest size comparisons for routing-only, data, transport and application entry points.
37
+
38
+ ## 0.2.0
39
+
40
+ - Breaking: choose `@kasperrt/smolter/react` or `@kasperrt/smolter/preact` for routing APIs and types.
41
+ - 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.
42
+ - Keep browser-safe helpers in `@kasperrt/smolter/utils`.
43
+ - Verify unused exports and server/build modules are removed from browser bundles.
44
+
45
+ ## 0.0.1
46
+
47
+ - First stable release of `@kasperrt/smolter` for React and Preact.
48
+ - Typed loaders, actions, client handlers, fetchers, and controlled revalidation.
49
+ - Manual and flat file routes, server transport, SSR, and hydration.
50
+ - Tokenless npm and JSR publishing through tested GitHub Actions.
51
+
52
+ ## 0.0.1-rc.2
53
+
54
+ - Use `@kasperrt/smolter` explicitly in every public import and generated route module.
55
+ - Remove the unscoped install alias from examples and documentation.
56
+
57
+ ## 0.0.1-rc.1
58
+
59
+ - Typed React and Preact routing with loaders, actions, and independent fetchers.
60
+ - Server handlers with optional clientLoader and clientAction call-through.
61
+ - Manual route tables and optional flat file routing.
62
+ - Server rendering, hydration, and controlled loader revalidation.
63
+ - Four standalone examples and shared UI.