@kasperrt/smolter 0.0.0-stage → 0.0.1-rc.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 (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +470 -2
  3. package/dist/client.d.ts +2 -0
  4. package/dist/client.js +2 -0
  5. package/dist/core.d.ts +2 -0
  6. package/dist/core.js +2 -0
  7. package/dist/files.d.ts +2 -0
  8. package/dist/files.js +2 -0
  9. package/dist/internal/client/deserialize.d.ts +3 -0
  10. package/dist/internal/client/deserialize.js +11 -0
  11. package/dist/internal/client/index.d.ts +23 -0
  12. package/dist/internal/client/index.js +44 -0
  13. package/dist/internal/core/errors.d.ts +3 -0
  14. package/dist/internal/core/errors.js +6 -0
  15. package/dist/internal/core/index.d.ts +6 -0
  16. package/dist/internal/core/index.js +6 -0
  17. package/dist/internal/core/match.d.ts +5 -0
  18. package/dist/internal/core/match.js +27 -0
  19. package/dist/internal/core/router.d.ts +3 -0
  20. package/dist/internal/core/router.js +189 -0
  21. package/dist/internal/core/types.d.ts +119 -0
  22. package/dist/internal/core/types.js +5 -0
  23. package/dist/internal/core/url.d.ts +2 -0
  24. package/dist/internal/core/url.js +6 -0
  25. package/dist/internal/files/index.d.ts +11 -0
  26. package/dist/internal/files/index.js +77 -0
  27. package/dist/internal/preact/context.d.ts +18 -0
  28. package/dist/internal/preact/context.js +20 -0
  29. package/dist/internal/preact/fetcher.d.ts +22 -0
  30. package/dist/internal/preact/fetcher.js +28 -0
  31. package/dist/internal/preact/form.d.ts +17 -0
  32. package/dist/internal/preact/form.js +41 -0
  33. package/dist/internal/preact/hooks.d.ts +21 -0
  34. package/dist/internal/preact/hooks.js +35 -0
  35. package/dist/internal/preact/index.d.ts +9 -0
  36. package/dist/internal/preact/index.js +7 -0
  37. package/dist/internal/preact/link.d.ts +12 -0
  38. package/dist/internal/preact/link.js +28 -0
  39. package/dist/internal/preact/routes.d.ts +22 -0
  40. package/dist/internal/preact/routes.js +22 -0
  41. package/dist/internal/preact/runtime.d.ts +2 -0
  42. package/dist/internal/preact/runtime.js +1 -0
  43. package/dist/internal/react/context.d.ts +18 -0
  44. package/dist/internal/react/context.js +20 -0
  45. package/dist/internal/react/fetcher.d.ts +22 -0
  46. package/dist/internal/react/fetcher.js +28 -0
  47. package/dist/internal/react/form.d.ts +17 -0
  48. package/dist/internal/react/form.js +41 -0
  49. package/dist/internal/react/hooks.d.ts +21 -0
  50. package/dist/internal/react/hooks.js +35 -0
  51. package/dist/internal/react/index.d.ts +9 -0
  52. package/dist/internal/react/index.js +7 -0
  53. package/dist/internal/react/link.d.ts +12 -0
  54. package/dist/internal/react/link.js +28 -0
  55. package/dist/internal/react/routes.d.ts +22 -0
  56. package/dist/internal/react/routes.js +22 -0
  57. package/dist/internal/react/runtime.d.ts +2 -0
  58. package/dist/internal/react/runtime.js +2 -0
  59. package/dist/internal/server/index.d.ts +11 -0
  60. package/dist/internal/server/index.js +81 -0
  61. package/dist/internal/vite/index.d.ts +10 -0
  62. package/dist/internal/vite/index.js +132 -0
  63. package/dist/internal/vite/transport.d.ts +2 -0
  64. package/dist/internal/vite/transport.js +42 -0
  65. package/dist/preact.d.ts +2 -0
  66. package/dist/preact.js +2 -0
  67. package/dist/react.d.ts +2 -0
  68. package/dist/react.js +2 -0
  69. package/dist/server.d.ts +2 -0
  70. package/dist/server.js +2 -0
  71. package/dist/vite.d.ts +2 -0
  72. package/dist/vite.js +2 -0
  73. package/jsr.json +26 -0
  74. package/package.json +94 -4
  75. package/src/client.ts +2 -0
  76. package/src/core.ts +2 -0
  77. package/src/files.ts +2 -0
  78. package/src/internal/client/deserialize.ts +15 -0
  79. package/src/internal/client/index.ts +84 -0
  80. package/src/internal/core/errors.ts +13 -0
  81. package/src/internal/core/index.ts +7 -0
  82. package/src/internal/core/match.ts +45 -0
  83. package/src/internal/core/router.ts +251 -0
  84. package/src/internal/core/types.ts +151 -0
  85. package/src/internal/core/url.ts +6 -0
  86. package/src/internal/files/index.ts +112 -0
  87. package/src/internal/preact/context.ts +46 -0
  88. package/src/internal/preact/fetcher.ts +83 -0
  89. package/src/internal/preact/form.ts +79 -0
  90. package/src/internal/preact/hooks.ts +72 -0
  91. package/src/internal/preact/index.ts +13 -0
  92. package/src/internal/preact/link.ts +46 -0
  93. package/src/internal/preact/routes.ts +50 -0
  94. package/src/internal/preact/runtime.ts +15 -0
  95. package/src/internal/react/context.ts +46 -0
  96. package/src/internal/react/fetcher.ts +83 -0
  97. package/src/internal/react/form.ts +79 -0
  98. package/src/internal/react/hooks.ts +72 -0
  99. package/src/internal/react/index.ts +13 -0
  100. package/src/internal/react/link.ts +46 -0
  101. package/src/internal/react/routes.ts +50 -0
  102. package/src/internal/react/runtime.ts +18 -0
  103. package/src/internal/server/index.ts +120 -0
  104. package/src/internal/vite/index.ts +208 -0
  105. package/src/internal/vite/transport.ts +60 -0
  106. package/src/preact.ts +2 -0
  107. package/src/react.ts +2 -0
  108. package/src/server.ts +2 -0
  109. package/src/vite.ts +2 -0
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Preact components and typed data hooks. Import from smolter/preact.
3
+ * @module
4
+ */
5
+ import { internalURL } from "../core/index.js";
6
+ import { useRouter } from "./context.js";
7
+ import { createElement } from "./runtime.js";
8
+ /** Render an anchor that handles internal navigation and preserves native modified clicks. */
9
+ export function Link({ href, replace, onClick, ...props }) {
10
+ const router = useRouter();
11
+ const click = (event) => {
12
+ onClick?.(event);
13
+ if (event.defaultPrevented ||
14
+ event.button ||
15
+ event.metaKey ||
16
+ event.ctrlKey ||
17
+ event.altKey ||
18
+ event.shiftKey ||
19
+ props.download !== undefined ||
20
+ (props.target && props.target !== "_self"))
21
+ return;
22
+ if (!internalURL(router.getSnapshot().url, href))
23
+ return;
24
+ event.preventDefault();
25
+ void router.navigate(href, { replace });
26
+ };
27
+ return createElement("a", { ...props, href, onClick: click });
28
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Preact components and typed data hooks. Import from smolter/preact.
3
+ * @module
4
+ */
5
+ import type { RouteModule } from "../core/index.ts";
6
+ import type { ComponentType, ReactNode } from "./runtime.ts";
7
+ /** Renderable route module with optional loading and error components. */
8
+ export interface ViewModule extends RouteModule {
9
+ default?: ComponentType;
10
+ HydrateFallback?: ComponentType;
11
+ ErrorBoundary?: ComponentType<ErrorProps>;
12
+ }
13
+ /** Normalized route error supplied to an ErrorBoundary component. */
14
+ export interface ErrorProps {
15
+ error: Error;
16
+ }
17
+ /** Fallback content used when no matching component can be rendered. */
18
+ export interface RoutesProps {
19
+ fallback?: ReactNode;
20
+ }
21
+ /** Renders the first matching registered module. */
22
+ export declare function Routes({ fallback }: RoutesProps): ReactNode;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Preact components and typed data hooks. Import from smolter/preact.
3
+ * @module
4
+ */
5
+ import { useRouter, useSnapshot } from "./context.js";
6
+ import { createElement, useMemo } from "./runtime.js";
7
+ /** Renders the first matching registered module. */
8
+ export function Routes({ fallback = null }) {
9
+ const router = useRouter();
10
+ const state = useSnapshot((snapshot) => snapshot);
11
+ const module = router.routes.find((route) => route.path === state.path);
12
+ const component = state.error
13
+ ? module?.ErrorBoundary
14
+ : state.state === "loading" && !state.loaded
15
+ ? module?.HydrateFallback
16
+ : module?.default;
17
+ const element = useMemo(() => component &&
18
+ createElement(component, {
19
+ error: state.error,
20
+ }), [component, state.error]);
21
+ return element ?? state.error?.message ?? fallback;
22
+ }
@@ -0,0 +1,2 @@
1
+ export type { AnchorHTMLAttributes, ComponentType, FormHTMLAttributes, MouseEvent, ReactNode, } from "preact/compat";
2
+ export { createContext, createElement, useContext, useEffect, useMemo, useSyncExternalStore, } from "preact/compat";
@@ -0,0 +1 @@
1
+ export { createContext, createElement, useContext, useEffect, useMemo, useSyncExternalStore, } from "preact/compat";
@@ -0,0 +1,18 @@
1
+ /**
2
+ * React components and typed data hooks. Import from smolter/react.
3
+ * @module
4
+ */
5
+ import type { Router, Snapshot } from "../core/index.ts";
6
+ import type { ReactNode } from "./runtime.ts";
7
+ /** Router instance and children rendered within its context. */
8
+ export interface RouterProviderProps {
9
+ router: Router;
10
+ children?: ReactNode;
11
+ }
12
+ /** Provide a router to descendant hooks and connect its browser lifecycle. */
13
+ export declare function RouterProvider({ router, children, }: RouterProviderProps): ReactNode;
14
+ /** Hooks require a RouterProvider. */
15
+ export declare function useRouter(): Router;
16
+ type SnapshotSelector<T> = (snapshot: Snapshot) => T;
17
+ export declare function useSnapshot<T>(select: SnapshotSelector<T>): T;
18
+ export {};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * React components and typed data hooks. Import from smolter/react.
3
+ * @module
4
+ */
5
+ import { createContext, createElement, useContext, useEffect, useSyncExternalStore, } from "./runtime.js";
6
+ const RouterContext = createContext(undefined);
7
+ /** Provide a router to descendant hooks and connect its browser lifecycle. */
8
+ export function RouterProvider({ router, children, }) {
9
+ useEffect(router.start, [router]);
10
+ return createElement(RouterContext.Provider, { value: router }, children);
11
+ }
12
+ /** Hooks require a RouterProvider. */
13
+ export function useRouter() {
14
+ return useContext(RouterContext);
15
+ }
16
+ export function useSnapshot(select) {
17
+ const router = useRouter();
18
+ const getSnapshot = () => select(router.getSnapshot());
19
+ return useSyncExternalStore(router.subscribe, getSnapshot, getSnapshot);
20
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * React components and typed data hooks. Import from smolter/react.
3
+ * @module
4
+ */
5
+ import type { AnyDataFunction, DataFunction, DataOf, FetcherSnapshot, Result, Route as RouteDefinition, RouteModule, SubmitOptions } from "../core/index.ts";
6
+ import { type FormProps } from "./form.ts";
7
+ import type { ComponentType } from "./runtime.ts";
8
+ /** Action method and optional explicit route URL for a fetcher submission. */
9
+ export interface FetcherSubmitOptions extends SubmitOptions {
10
+ action?: string;
11
+ }
12
+ /** Typed independent request state, methods, and a bound Form component. */
13
+ export interface TypedFetcher<T> extends Omit<FetcherSnapshot, "data"> {
14
+ data?: T;
15
+ Form: ComponentType<FormProps>;
16
+ load: (url?: string) => Promise<Result>;
17
+ submit: (body: FormData, options?: FetcherSubmitOptions) => Promise<Result>;
18
+ }
19
+ /** Create a fetcher for independent route reads and mutations; its data stays local. */
20
+ export declare function useFetcher<T extends RouteModule>(route: RouteDefinition<T>): TypedFetcher<DataOf<Extract<T["action"] | T["clientAction"], AnyDataFunction>> | DataOf<Extract<T["loader"] | T["clientLoader"], AnyDataFunction>>>;
21
+ /** Create a fetcher for independent route reads and mutations; its data stays local. */
22
+ export declare function useFetcher<T extends AnyDataFunction = DataFunction>(): TypedFetcher<DataOf<T>>;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * React components and typed data hooks. Import from smolter/react.
3
+ * @module
4
+ */
5
+ import { useRouter } from "./context.js";
6
+ import { Form } from "./form.js";
7
+ import { createElement, useEffect, useMemo, useSyncExternalStore, } from "./runtime.js";
8
+ /** Create a fetcher for independent route reads and mutations; its data stays local. */
9
+ export function useFetcher(route) {
10
+ const router = useRouter();
11
+ const path = route?.path;
12
+ const fetcher = useMemo(() => {
13
+ const store = router.createFetcher();
14
+ const target = () => {
15
+ const snapshot = router.getSnapshot();
16
+ return path === undefined || path === snapshot.path ? snapshot.url : path;
17
+ };
18
+ return {
19
+ ...store,
20
+ Form: (props) => createElement(Form, { action: target(), ...props, fetcher: store }),
21
+ load: (url) => store.load(url ?? target()),
22
+ submit: (body, options) => store.submit(options?.action ?? target(), body, options),
23
+ };
24
+ }, [router, path]);
25
+ useEffect(() => fetcher.dispose, [fetcher]);
26
+ const state = useSyncExternalStore(fetcher.subscribe, fetcher.getSnapshot, fetcher.getSnapshot);
27
+ return { ...fetcher, ...state };
28
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * React components and typed data hooks. Import from smolter/react.
3
+ * @module
4
+ */
5
+ import type { Fetcher } from "../core/index.ts";
6
+ import type { FormHTMLAttributes, ReactNode } from "./runtime.ts";
7
+ /** Form attributes with GET loader requests or POST, PUT, PATCH, and DELETE actions. */
8
+ export interface FormProps extends Omit<FormHTMLAttributes<HTMLFormElement>, "action" | "method"> {
9
+ action?: string;
10
+ method?: "get" | "post" | "put" | "patch" | "delete";
11
+ }
12
+ interface InternalFormProps extends FormProps {
13
+ fetcher?: Fetcher;
14
+ }
15
+ /** Submit form data to a registered route without a document reload. */
16
+ export declare function Form({ action, method, fetcher, onSubmit, ...props }: InternalFormProps): ReactNode;
17
+ export {};
@@ -0,0 +1,41 @@
1
+ /**
2
+ * React components and typed data hooks. Import from smolter/react.
3
+ * @module
4
+ */
5
+ import { internalURL } from "../core/index.js";
6
+ import { useRouter } from "./context.js";
7
+ import { createElement } from "./runtime.js";
8
+ /** Submit form data to a registered route without a document reload. */
9
+ export function Form({ action, method = "post", fetcher, onSubmit, ...props }) {
10
+ const router = useRouter();
11
+ const submit = (event) => {
12
+ onSubmit?.(event);
13
+ if (event.defaultPrevented)
14
+ return;
15
+ const submitter = event.nativeEvent.submitter;
16
+ const target = internalURL(router.getSnapshot().url, submitter?.getAttribute("formaction") ?? action);
17
+ if (!target)
18
+ return;
19
+ event.preventDefault();
20
+ const data = new FormData(event.currentTarget, submitter);
21
+ const verb = (submitter?.getAttribute("formmethod") ?? method).toUpperCase();
22
+ if (verb === "GET") {
23
+ const search = new URLSearchParams();
24
+ for (const [key, value] of data)
25
+ search.append(key, typeof value === "string" ? value : value.name);
26
+ target.search = String(search);
27
+ void (fetcher ? fetcher.load : router.navigate)(target.href);
28
+ return;
29
+ }
30
+ const options = {
31
+ method: verb,
32
+ };
33
+ void (fetcher ?? router).submit(target.href, data, options);
34
+ };
35
+ return createElement("form", {
36
+ ...props,
37
+ action,
38
+ method: method === "get" ? "get" : "post",
39
+ onSubmit: submit,
40
+ });
41
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * React components and typed data hooks. Import from smolter/react.
3
+ * @module
4
+ */
5
+ import type { AnyDataFunction, DataOf, RouteModule, Router, Snapshot } from "../core/index.ts";
6
+ /** A route module or loader function used for data inference. */
7
+ export type LoaderSource = RouteModule | AnyDataFunction;
8
+ /** Infer server and client loader data from a route module or function. */
9
+ export type LoaderData<T> = T extends AnyDataFunction ? DataOf<T> : T extends RouteModule ? DataOf<Extract<T["loader"] | T["clientLoader"], AnyDataFunction>> : unknown;
10
+ /** Infer server and client action data from a route module or function. */
11
+ export type ActionData<T> = T extends AnyDataFunction ? DataOf<T> : T extends RouteModule ? DataOf<Extract<T["action"] | T["clientAction"], AnyDataFunction>> : unknown;
12
+ /** Pass a route for inference, or use useLoaderData<typeof loader>() in its module. */
13
+ export declare function useLoaderData<T extends LoaderSource = RouteModule>(route?: T): LoaderData<T> | undefined;
14
+ /** Read the last page action result, optionally inferring its type from a route. */
15
+ export declare function useActionData<T extends LoaderSource = RouteModule>(route?: T): ActionData<T> | undefined;
16
+ /** Read path parameters for the active route. */
17
+ export declare function useParams(): Snapshot["params"];
18
+ /** Read the current path, search, and hash together with the navigate function. */
19
+ export declare function useLocation(): readonly [string, Router["navigate"]];
20
+ /** Get the function that revalidates the current loader subject to shouldRevalidate. */
21
+ export declare function useRevalidator(): Router["revalidate"];
@@ -0,0 +1,35 @@
1
+ /**
2
+ * React components and typed data hooks. Import from smolter/react.
3
+ * @module
4
+ */
5
+ import { useRouter, useSnapshot } from "./context.js";
6
+ function useData(key, route) {
7
+ const path = route?.path;
8
+ return useSnapshot((snapshot) => {
9
+ if (path !== undefined && path !== snapshot.path)
10
+ return undefined;
11
+ return snapshot[key];
12
+ });
13
+ }
14
+ /** Pass a route for inference, or use useLoaderData<typeof loader>() in its module. */
15
+ export function useLoaderData(route) {
16
+ return useData("data", route);
17
+ }
18
+ /** Read the last page action result, optionally inferring its type from a route. */
19
+ export function useActionData(route) {
20
+ return useData("actionData", route);
21
+ }
22
+ /** Read path parameters for the active route. */
23
+ export function useParams() {
24
+ return useSnapshot((snapshot) => snapshot.params);
25
+ }
26
+ /** Read the current path, search, and hash together with the navigate function. */
27
+ export function useLocation() {
28
+ const router = useRouter();
29
+ const url = useSnapshot((snapshot) => snapshot.url);
30
+ return [url.replace(/^\w+:\/\/[^/]+/, ""), router.navigate];
31
+ }
32
+ /** Get the function that revalidates the current loader subject to shouldRevalidate. */
33
+ export function useRevalidator() {
34
+ return useRouter().revalidate;
35
+ }
@@ -0,0 +1,9 @@
1
+ /** Framework components and typed data hooks. @module */
2
+ export type * from "../core/index.ts";
3
+ export { createRouter, defineRoute } from "../core/index.ts";
4
+ export { RouterProvider, type RouterProviderProps, useRouter, } from "./context.ts";
5
+ export * from "./fetcher.ts";
6
+ export * from "./form.ts";
7
+ export * from "./hooks.ts";
8
+ export * from "./link.ts";
9
+ export * from "./routes.ts";
@@ -0,0 +1,7 @@
1
+ export { createRouter, defineRoute } from "../core/index.js";
2
+ export { RouterProvider, useRouter, } from "./context.js";
3
+ export * from "./fetcher.js";
4
+ export * from "./form.js";
5
+ export * from "./hooks.js";
6
+ export * from "./link.js";
7
+ export * from "./routes.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * React components and typed data hooks. Import from smolter/react.
3
+ * @module
4
+ */
5
+ import type { AnchorHTMLAttributes, ReactNode } from "./runtime.ts";
6
+ /** Anchor attributes with internal navigation and optional history replacement. */
7
+ export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
8
+ href: string;
9
+ replace?: boolean;
10
+ }
11
+ /** Render an anchor that handles internal navigation and preserves native modified clicks. */
12
+ export declare function Link({ href, replace, onClick, ...props }: LinkProps): ReactNode;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * React components and typed data hooks. Import from smolter/react.
3
+ * @module
4
+ */
5
+ import { internalURL } from "../core/index.js";
6
+ import { useRouter } from "./context.js";
7
+ import { createElement } from "./runtime.js";
8
+ /** Render an anchor that handles internal navigation and preserves native modified clicks. */
9
+ export function Link({ href, replace, onClick, ...props }) {
10
+ const router = useRouter();
11
+ const click = (event) => {
12
+ onClick?.(event);
13
+ if (event.defaultPrevented ||
14
+ event.button ||
15
+ event.metaKey ||
16
+ event.ctrlKey ||
17
+ event.altKey ||
18
+ event.shiftKey ||
19
+ props.download !== undefined ||
20
+ (props.target && props.target !== "_self"))
21
+ return;
22
+ if (!internalURL(router.getSnapshot().url, href))
23
+ return;
24
+ event.preventDefault();
25
+ void router.navigate(href, { replace });
26
+ };
27
+ return createElement("a", { ...props, href, onClick: click });
28
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * React components and typed data hooks. Import from smolter/react.
3
+ * @module
4
+ */
5
+ import type { RouteModule } from "../core/index.ts";
6
+ import type { ComponentType, ReactNode } from "./runtime.ts";
7
+ /** Renderable route module with optional loading and error components. */
8
+ export interface ViewModule extends RouteModule {
9
+ default?: ComponentType;
10
+ HydrateFallback?: ComponentType;
11
+ ErrorBoundary?: ComponentType<ErrorProps>;
12
+ }
13
+ /** Normalized route error supplied to an ErrorBoundary component. */
14
+ export interface ErrorProps {
15
+ error: Error;
16
+ }
17
+ /** Fallback content used when no matching component can be rendered. */
18
+ export interface RoutesProps {
19
+ fallback?: ReactNode;
20
+ }
21
+ /** Renders the first matching registered module. */
22
+ export declare function Routes({ fallback }: RoutesProps): ReactNode;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * React components and typed data hooks. Import from smolter/react.
3
+ * @module
4
+ */
5
+ import { useRouter, useSnapshot } from "./context.js";
6
+ import { createElement, useMemo } from "./runtime.js";
7
+ /** Renders the first matching registered module. */
8
+ export function Routes({ fallback = null }) {
9
+ const router = useRouter();
10
+ const state = useSnapshot((snapshot) => snapshot);
11
+ const module = router.routes.find((route) => route.path === state.path);
12
+ const component = state.error
13
+ ? module?.ErrorBoundary
14
+ : state.state === "loading" && !state.loaded
15
+ ? module?.HydrateFallback
16
+ : module?.default;
17
+ const element = useMemo(() => component &&
18
+ createElement(component, {
19
+ error: state.error,
20
+ }), [component, state.error]);
21
+ return element ?? state.error?.message ?? fallback;
22
+ }
@@ -0,0 +1,2 @@
1
+ export type { AnchorHTMLAttributes, ComponentType, FormHTMLAttributes, MouseEvent, ReactNode, } from "react";
2
+ export { createContext, createElement, useContext, useEffect, useMemo, useSyncExternalStore, } from "react";
@@ -0,0 +1,2 @@
1
+ // @deno-types="npm:@types/react@19.3.0"
2
+ export { createContext, createElement, useContext, useEffect, useMemo, useSyncExternalStore, } from "react";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Request-scoped server rendering and HTTP data dispatch.
3
+ * @module
4
+ */
5
+ import { type Route, type Router, type Snapshot } from "../core/index.ts";
6
+ /** Create one router per server request, await this, then render the provider. */
7
+ export declare function loadRouter(routes: readonly Route[], input: string | Request): Promise<Router>;
8
+ /** Safe inside a script element. Data must be JSON serializable; error stacks and causes are omitted. */
9
+ export declare function serialize(snapshot: Snapshot): string;
10
+ /** Dispatch an HTTP data request to a registered server loader or action. */
11
+ export declare function handleRequest(routes: readonly Route[], request: Request): Promise<Response>;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Request-scoped server rendering and HTTP data dispatch.
3
+ * @module
4
+ */
5
+ import { createRouter, matchRoutes, safeWrapAsync, } from "../core/index.js";
6
+ /** Create one router per server request, await this, then render the provider. */
7
+ export async function loadRouter(routes, input) {
8
+ const url = typeof input === "string" ? input : input.url;
9
+ const wrap = (handler) => {
10
+ if (!handler || typeof input === "string")
11
+ return handler;
12
+ return (args) => {
13
+ const signal = AbortSignal.any([input.signal, args.signal]);
14
+ const request = new Request(args.request, { signal });
15
+ input.headers.forEach((value, name) => {
16
+ if (!request.headers.has(name))
17
+ request.headers.set(name, value);
18
+ });
19
+ return handler({ ...args, request, signal });
20
+ };
21
+ };
22
+ const serverRoutes = routes.map((route) => ({
23
+ ...route,
24
+ clientLoader: undefined,
25
+ clientAction: undefined,
26
+ loader: wrap(route.loader),
27
+ action: wrap(route.action),
28
+ }));
29
+ const router = createRouter(serverRoutes, { url });
30
+ await router.navigate(url);
31
+ const snapshot = router.getSnapshot();
32
+ if (!snapshot.error &&
33
+ snapshot.data === undefined &&
34
+ routes.some((route) => route.path === snapshot.path && route.clientLoader && !route.loader)) {
35
+ return createRouter(serverRoutes, {
36
+ url,
37
+ hydrationData: { ...snapshot, loaded: false, state: "loading" },
38
+ });
39
+ }
40
+ return router;
41
+ }
42
+ /** Safe inside a script element. Data must be JSON serializable; error stacks and causes are omitted. */
43
+ export function serialize(snapshot) {
44
+ return JSON.stringify({
45
+ ...snapshot,
46
+ error: snapshot.error && {
47
+ name: snapshot.error.name,
48
+ message: snapshot.error.message,
49
+ },
50
+ })
51
+ .replace(/</g, "\\u003c")
52
+ .replace(/\u2028/g, "\\u2028")
53
+ .replace(/\u2029/g, "\\u2029");
54
+ }
55
+ /** Dispatch an HTTP data request to a registered server loader or action. */
56
+ export async function handleRequest(routes, request) {
57
+ const match = matchRoutes(routes, new URL(request.url).pathname);
58
+ const reading = request.method === "GET";
59
+ if (!match)
60
+ return Response.json({ error: "No route" }, { status: 404 });
61
+ if (!reading && !["POST", "PUT", "PATCH", "DELETE"].includes(request.method))
62
+ return Response.json({ error: "Method not allowed" }, { status: 405 });
63
+ const handler = reading ? match.route.loader : match.route.action;
64
+ if (!handler)
65
+ return Response.json({ error: `No ${reading ? "loader" : "action"}` }, { status: 405 });
66
+ const result = await safeWrapAsync(() => handler({ request, params: match.params, signal: request.signal }));
67
+ const error = result.error ?? (result.data instanceof Error && result.data);
68
+ if (error)
69
+ return Response.json({ error: error.message }, { status: 500 });
70
+ if (result.data instanceof Response) {
71
+ const location = result.data.headers.get("Location");
72
+ if (location)
73
+ return new Response(null, {
74
+ headers: { "X-Smolter-Redirect": location },
75
+ });
76
+ return result.data;
77
+ }
78
+ return Response.json(result.data ?? null, {
79
+ headers: { "Cache-Control": "no-store" },
80
+ });
81
+ }
@@ -0,0 +1,10 @@
1
+ import { type Plugin } from "vite";
2
+ export interface FileRouterOptions {
3
+ directory?: string;
4
+ clientRoutes?: string;
5
+ serverRoutes?: string;
6
+ }
7
+ /** Build-time flat route discovery and the optional server data transport. */
8
+ export declare function routerPlugin({ directory, clientRoutes, serverRoutes, }?: FileRouterOptions): Plugin;
9
+ /** File routing is the default; this alias keeps configuration explicit. */
10
+ export declare const fileRouter: typeof routerPlugin;
@@ -0,0 +1,132 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { flatRoutes } from "../files/index.js";
4
+ import { transformWithOxc } from "vite";
5
+ import { configureTransport } from "./transport.js";
6
+ /** Build-time flat route discovery and the optional server data transport. */
7
+ export function routerPlugin({ directory = "src/routes", clientRoutes, serverRoutes, } = {}) {
8
+ let root;
9
+ let projectRoot;
10
+ const publicId = "virtual:smolter/routes";
11
+ const virtualId = `\0${publicId}`;
12
+ const suffix = "?smolter-client";
13
+ return {
14
+ name: "smolter-flat-routes",
15
+ enforce: "pre",
16
+ configResolved(config) {
17
+ projectRoot = config.root;
18
+ root = resolve(config.root, directory);
19
+ },
20
+ resolveId(id) {
21
+ if (id === publicId)
22
+ return virtualId;
23
+ },
24
+ async load(id, options) {
25
+ if (id.endsWith(suffix)) {
26
+ const file = id.slice(0, -suffix.length);
27
+ const transformed = await transformWithOxc(await readFile(file, "utf8"), file);
28
+ const ast = this.parse(transformed.code);
29
+ const edits = [];
30
+ for (const statement of ast.body) {
31
+ if (statement.type === "ExportAllDeclaration") {
32
+ return this.error(`Use explicit named exports in ${file}; export * cannot be safely split between server and browser.`);
33
+ }
34
+ if (statement.type !== "ExportNamedDeclaration")
35
+ continue;
36
+ const server = statement.specifiers.filter((specifier) => {
37
+ const name = specifier.exported.type === "Identifier"
38
+ ? specifier.exported.name
39
+ : specifier.exported.value;
40
+ return name === "loader" || name === "action";
41
+ });
42
+ if (!server.length) {
43
+ const declaration = statement.declaration;
44
+ const name = declaration?.type === "FunctionDeclaration"
45
+ ? declaration.id?.name
46
+ : undefined;
47
+ const variables = declaration?.type === "VariableDeclaration"
48
+ ? declaration.declarations
49
+ : [];
50
+ if (name === "loader" ||
51
+ name === "action" ||
52
+ variables.some((item) => item.id.type === "Identifier" &&
53
+ ["loader", "action"].includes(item.id.name))) {
54
+ return this.error(`Re-export loader/action from separate files in ${file}; inline server handlers cannot enter a browser module.`);
55
+ }
56
+ continue;
57
+ }
58
+ if (!statement.source ||
59
+ server.length !== statement.specifiers.length) {
60
+ return this.error(`Use separate export { loader } / export { action } statements in ${file}.`);
61
+ }
62
+ edits.push([statement.start, statement.end]);
63
+ }
64
+ let code = transformed.code;
65
+ for (const [start, end] of edits.reverse())
66
+ code = code.slice(0, start) + code.slice(end);
67
+ return { code, map: null };
68
+ }
69
+ if (id !== virtualId)
70
+ return;
71
+ if (clientRoutes || serverRoutes) {
72
+ if (!clientRoutes || !serverRoutes)
73
+ return this.error("Specify both clientRoutes and serverRoutes for manual routing.");
74
+ const entry = resolve(projectRoot, options?.ssr ? serverRoutes : clientRoutes).replaceAll("\\", "/");
75
+ return `export { routes as default } from ${JSON.stringify(entry)};`;
76
+ }
77
+ const routes = await flatRoutes(root);
78
+ if (routes instanceof Error)
79
+ return this.error(routes.message);
80
+ const imports = [];
81
+ const definitions = [];
82
+ for (const [index, route] of routes.entries()) {
83
+ this.addWatchFile(route.file);
84
+ const file = route.file.replaceAll("\\", "/");
85
+ const transformed = await transformWithOxc(await readFile(file, "utf8"), file);
86
+ const ast = this.parse(transformed.code);
87
+ const handlers = {};
88
+ for (const statement of ast.body) {
89
+ if (statement.type !== "ExportNamedDeclaration")
90
+ continue;
91
+ for (const specifier of statement.specifiers) {
92
+ const name = specifier.exported.type === "Identifier"
93
+ ? specifier.exported.name
94
+ : specifier.exported.value;
95
+ if (name === "loader" || name === "action")
96
+ handlers[name] = true;
97
+ }
98
+ }
99
+ imports.push(`import * as route${index} from ${JSON.stringify(file + (options?.ssr ? "" : suffix))};`);
100
+ definitions.push(options?.ssr || !Object.keys(handlers).length
101
+ ? `{ ...route${index}, path: ${JSON.stringify(route.path)} }`
102
+ : `serverRoute(${JSON.stringify(route.path)}, route${index}, ${JSON.stringify(handlers)})`);
103
+ }
104
+ if (!options?.ssr &&
105
+ definitions.some((definition) => definition.startsWith("serverRoute(")))
106
+ imports.unshift('import { serverRoute } from "smolter/client";');
107
+ return `${imports.join("\n")}\nexport default [${definitions.join(",\n")}];\n`;
108
+ },
109
+ configureServer(server) {
110
+ server.watcher.add(root);
111
+ const refresh = (file) => {
112
+ if (!file.startsWith(`${root}/`))
113
+ return;
114
+ const module = server.moduleGraph.getModuleById(virtualId);
115
+ if (module)
116
+ server.moduleGraph.invalidateModule(module);
117
+ server.ws.send({ type: "full-reload" });
118
+ };
119
+ server.watcher.on("add", refresh);
120
+ server.watcher.on("change", refresh);
121
+ server.watcher.on("unlink", refresh);
122
+ server.httpServer?.once("close", () => {
123
+ server.watcher.off("add", refresh);
124
+ server.watcher.off("change", refresh);
125
+ server.watcher.off("unlink", refresh);
126
+ });
127
+ configureTransport(server, publicId);
128
+ },
129
+ };
130
+ }
131
+ /** File routing is the default; this alias keeps configuration explicit. */
132
+ export const fileRouter = routerPlugin;