react-router-effect 2.1.0 → 2.2.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/dist/vite.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.mjs";
2
- import { Effect } from "effect";
3
- import { HttpServerRequest, HttpServerResponse } from "effect/http";
2
+ import { Effect, Exit, Layer, Option, Predicate, Scope, SynchronizedRef, Tuple } from "effect";
3
+ import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
4
4
  import * as NodeHttpServerRequest from "@effect/platform-node/NodeHttpServerRequest";
5
5
  // src/vite.ts
6
- var vite_exports = /* @__PURE__ */ __exportAll({ devMiddleware: () => devMiddleware });
6
+ var vite_exports = /* @__PURE__ */ __exportAll({ layer: () => layer });
7
7
  /**
8
8
  * Runs Vite's middleware on the Node request. Succeeds with `true` when Vite
9
9
  * passes the request on, and `false` when Vite answered it, or the client left
@@ -28,19 +28,53 @@ const passThroughVite = (vite, request, response) => Effect.callback((resume) =>
28
28
  }
29
29
  });
30
30
  /**
31
- * Passes each request to Vite first, and the requests Vite passes on to the
32
- * app. Use it in development only, with Vite in middleware mode:
33
- *
34
- * ```ts
35
- * import { ReactRouterVite } from "react-router-effect/vite";
31
+ * Serves the app in development, as the Express template does. Give it to
32
+ * `HttpRouter.serve` in place of the app's `App`:
36
33
  *
34
+ * ```js
37
35
  * const vite = await createServer({ server: { middlewareMode: true } });
38
- * HttpServer.serve(Effect.flatten(reactRouterApp).pipe(ReactRouterVite.devMiddleware(vite)));
36
+ * const App = ReactRouterVite.layer(vite, () =>
37
+ * vite.ssrLoadModule("./server/app.ts").then((module) => module.App),
38
+ * );
39
39
  * ```
40
40
  *
41
- * A request Vite answers gets an empty `HttpServerResponse`, which Effect's
42
- * Node server does not write, since Vite has ended the Node response. A Vite
43
- * error fails the request as a defect.
41
+ * The layer registers one catch-all route on the server's router. Its
42
+ * handler passes each request to Vite first. For a request that Vite passes
43
+ * on, it calls `loadApp`. When `loadApp` returns a new `App` value, such as
44
+ * after an edit of the app's server code, the layer builds the new `App` with
45
+ * its own router and closes the old one. Concurrent requests share one build
46
+ * of each `App`. Then the request goes through the current `App`.
47
+ *
48
+ * A request that Vite answers gets an empty `HttpServerResponse`, which
49
+ * Effect's Node server does not write, since Vite has ended the Node
50
+ * response. A defect, such as a Vite error, a `loadApp` that rejects or an
51
+ * `App` that fails to build, goes to the app's loggers, after
52
+ * `ssrFixStacktrace` maps its stack, and the client gets an empty 500.
53
+ *
54
+ * @stability unstable
44
55
  */
45
- const devMiddleware = (vite) => (app) => HttpServerRequest.HttpServerRequest.use((request) => passThroughVite(vite, NodeHttpServerRequest.toIncomingMessage(request), NodeHttpServerRequest.toServerResponse(request)).pipe(Effect.flatMap((passed) => passed ? app : Effect.succeed(HttpServerResponse.empty()))));
56
+ const layer = (vite, loadApp) => HttpRouter.use((router) => Effect.gen(function* () {
57
+ const layerScope = yield* Effect.scope;
58
+ const services = yield* Effect.context();
59
+ /** Builds `app` in `scope`. The services of this layer do not replace either. */
60
+ const build = (app, scope) => HttpRouter.toHttpEffect(app).pipe(Scope.provide(scope), Effect.provideServiceEffect(Layer.CurrentMemoMap, Layer.makeMemoMap), Effect.provideContext(services));
61
+ const state = yield* SynchronizedRef.make(Option.none());
62
+ /** The handler of `app`, built once for each `App` value. */
63
+ const handlerOf = (app) => SynchronizedRef.modifyEffect(state, (previous) => Effect.gen(function* () {
64
+ if (Option.isSome(previous) && previous.value.app === app) return Tuple.make(previous.value.handler, previous);
65
+ const scope = yield* Scope.fork(layerScope);
66
+ const handler = yield* build(app, scope).pipe(Effect.onExit((exit) => Exit.isSuccess(exit) ? Effect.void : Scope.close(scope, exit)), Effect.orDie);
67
+ if (Option.isSome(previous)) yield* Scope.close(previous.value.scope, Exit.void);
68
+ return Tuple.make(handler, Option.some({
69
+ app,
70
+ handler,
71
+ scope
72
+ }));
73
+ }).pipe(Effect.uninterruptible));
74
+ const handler = HttpServerRequest.HttpServerRequest.use((request) => passThroughVite(vite, NodeHttpServerRequest.toIncomingMessage(request), NodeHttpServerRequest.toServerResponse(request)).pipe(Effect.flatMap((passed) => passed ? Effect.promise(loadApp).pipe(Effect.flatMap(handlerOf), Effect.flatten) : Effect.succeed(HttpServerResponse.empty())), Effect.catchDefect((defect) => {
75
+ if (Predicate.isError(defect)) vite.ssrFixStacktrace?.(defect);
76
+ return Effect.logError("Request failed in development", defect).pipe(Effect.as(HttpServerResponse.empty({ status: 500 })));
77
+ })));
78
+ yield* router.add("*", "*", handler);
79
+ }));
46
80
  export { vite_exports as ReactRouterVite };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schemastore.org/package.json",
3
3
  "name": "react-router-effect",
4
- "version": "2.1.0",
4
+ "version": "2.2.0",
5
5
  "description": "Wrap React Router framework-mode route modules with Effect, with typed recover/throw/redirect error handling.",
6
6
  "keywords": [
7
7
  "action",
@@ -40,22 +40,24 @@
40
40
  "devDependencies": {
41
41
  "@changesets/changelog-github": "^1.0.1",
42
42
  "@changesets/cli": "^3.0.3",
43
- "@effect/platform-node": "4.1.1",
43
+ "@effect/platform-node": "4.1.2",
44
+ "@effect/tsgo": "0.67.5",
44
45
  "@react-router/node": "8.4.0",
45
46
  "@types/node": "^25.6.2",
46
47
  "@types/react": "^19.2.17",
47
48
  "@types/react-dom": "^19.2.3",
48
49
  "@vitejs/plugin-react": "^6.0.4",
49
- "@vitest/browser": "5.0.5",
50
- "@vitest/browser-playwright": "5.0.5",
51
- "effect": "4.1.1",
50
+ "@vitest/browser": "5.1.0",
51
+ "@vitest/browser-playwright": "5.1.0",
52
+ "effect": "4.1.2",
53
+ "miniflare": "5.20261104.0-beta",
52
54
  "playwright": "1.64.1",
53
55
  "react": "^19.2.8",
54
56
  "react-dom": "^19.2.8",
55
57
  "react-router": "^8.3.0",
56
58
  "typescript": "^7.0.2",
57
59
  "vite": "npm:@voidzero-dev/vite-plus-core@latest",
58
- "vite-plus": "^1.4.0",
60
+ "vite-plus": "^1.5.0",
59
61
  "vitest": "5.0.5",
60
62
  "vitest-browser-react": "^2.2.0"
61
63
  },
@@ -76,6 +78,9 @@
76
78
  "onFail": "warn"
77
79
  }
78
80
  },
81
+ "engines": {
82
+ "node": ">=22"
83
+ },
79
84
  "scripts": {
80
85
  "build": "vp pack",
81
86
  "dev": "vp pack --watch",
@@ -83,7 +88,7 @@
83
88
  "test:unit": "vp test run --project unit",
84
89
  "test:browser": "vp test run --project browser",
85
90
  "test:browser:install": "vp exec playwright install chromium",
86
- "check": "vp check",
91
+ "check": "effect-tsgo vp check",
87
92
  "check:consumer": "tsc -p test/consumer",
88
93
  "changeset": "changeset",
89
94
  "changeset:version": "changeset version",
package/src/assets.ts ADDED
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Serves the client build of a React Router app from the app's Effect HTTP
3
+ * router, as the `express.static` lines of React Router's Express template
4
+ * do.
5
+ *
6
+ * Its API is unstable: a minor version can change it.
7
+ *
8
+ * @stability unstable
9
+ * @since 2.2.0
10
+ */
11
+ import { Array as Arr, Data, Effect, FileSystem, type Layer, Option, Path } from "effect";
12
+ import {
13
+ type HttpPlatform,
14
+ HttpRouter,
15
+ HttpServerRespondable,
16
+ HttpStaticServer,
17
+ } from "effect/http";
18
+ import type { ServerBuild } from "react-router";
19
+
20
+ import { ReactRouter } from "./server.ts";
21
+
22
+ /**
23
+ * The layer cannot serve the client build: it cannot read the directory, or
24
+ * a file name has a `*`, which the router cannot match. The build's
25
+ * `assetsBuildDirectory` is relative to the working directory of the server,
26
+ * so `directory` is the path that it resolved to.
27
+ *
28
+ * @stability unstable
29
+ * @since 2.2.0
30
+ */
31
+ export class ClientBuildError extends Data.TaggedError("ClientBuildError")<{
32
+ readonly message: string;
33
+ /** The resolved directory of the client build. */
34
+ readonly directory: string;
35
+ readonly cause: unknown;
36
+ }> {}
37
+
38
+ /** The `Cache-Control` of the files in the hashed directory, whose names include a content hash. */
39
+ const hashedCacheControl = "public, max-age=31536000, immutable";
40
+
41
+ /** The `Cache-Control` of the other files of the client build. */
42
+ const otherCacheControl = "public, max-age=3600";
43
+
44
+ /**
45
+ * The index file of a directory. React Router writes a prerendered page as
46
+ * the `index.html` of its path's directory, and it is the default index of
47
+ * `HttpStaticServer.make`.
48
+ */
49
+ const indexFile = "index.html";
50
+
51
+ /**
52
+ * The directory of the hashed files, relative to `publicPath`, from the URL
53
+ * of the manifest that the build plugin writes in it. `None` when the
54
+ * manifest is not under `publicPath`, or is at its root.
55
+ */
56
+ const hashedDirectory = (build: ServerBuild, path: Path.Path): Option.Option<string> => {
57
+ const manifest = build.assets.url;
58
+ if (!manifest.startsWith(build.publicPath)) return Option.none();
59
+ const directory = path.dirname(manifest.slice(build.publicPath.length - 1));
60
+ return directory === "/" ? Option.none() : Option.some(directory);
61
+ };
62
+
63
+ /**
64
+ * The handler of a static server. A missing file is a 404. Any other error,
65
+ * such as a file that the server cannot read, goes to the app's loggers
66
+ * before the 500.
67
+ */
68
+ const makeHandler = (options: {
69
+ readonly root: string;
70
+ readonly index: string | undefined;
71
+ readonly cacheControl: string;
72
+ }) =>
73
+ HttpStaticServer.make(options).pipe(
74
+ // Making a static server only resolves its root.
75
+ Effect.orDie,
76
+ Effect.map((files) =>
77
+ files.pipe(
78
+ Effect.catchReason("HttpServerError", "RouteNotFound", HttpServerRespondable.toResponse),
79
+ Effect.catch((error) =>
80
+ Effect.logError("Cannot serve a client file", error).pipe(
81
+ Effect.andThen(HttpServerRespondable.toResponse(error)),
82
+ ),
83
+ ),
84
+ ),
85
+ ),
86
+ );
87
+
88
+ /**
89
+ * Registers the routes of the client build on the app's router, for GET and
90
+ * HEAD, under the build's `publicPath`.
91
+ *
92
+ * The hashed directory is the directory of the build's manifest URL, such as
93
+ * `/assets`. The build plugin writes the files with a content hash in their
94
+ * names there. Its files are cached for a year as immutable, and a request
95
+ * under it for a file that the directory does not have gets a 404. The
96
+ * files are served from that directory only, so a path with `..` does not
97
+ * reach the other files of the client build.
98
+ *
99
+ * Each other file gets its own route, and is cached for an hour. A directory
100
+ * with an `index.html`, such as a prerendered page, gets a route that serves
101
+ * its `index.html`, and its path without the trailing slash redirects. The
102
+ * layer serves no path with a segment that starts with a dot, such as
103
+ * `.vite/`, as `express.static` ignores dotfiles. A root `index.html`, such
104
+ * as a prerendered home page, gets the route `/`, which wins over the
105
+ * catch-all of `ReactRouter.routes`. Any other request goes on to the app's
106
+ * other routes.
107
+ *
108
+ * The layer reads the client build when it builds, so a file that something
109
+ * adds outside the hashed directory later is served after a restart. It
110
+ * fails with a {@link ClientBuildError} when it cannot read the client build
111
+ * directory, or when a file name has a `*`, which the router cannot match.
112
+ * A route of the app with the path of a file fails the server's start.
113
+ *
114
+ * The layer registers nothing for a build that loads for each request, such
115
+ * as a development build, since Vite serves the client there. It also
116
+ * registers nothing for a `publicPath` on another origin.
117
+ *
118
+ * The routes of this layer get the middleware that the app provides to this
119
+ * layer. To leave the server's request logger out of file requests:
120
+ *
121
+ * ```ts
122
+ * ReactRouterAssets.layer.pipe(Layer.provide(HttpRouter.disableLogger))
123
+ * ```
124
+ *
125
+ * @stability unstable
126
+ */
127
+ export const layer: Layer.Layer<
128
+ never,
129
+ ClientBuildError,
130
+ | HttpRouter.HttpRouter
131
+ | ReactRouter
132
+ | FileSystem.FileSystem
133
+ | Path.Path
134
+ | HttpPlatform.HttpPlatform
135
+ > = HttpRouter.use((router) =>
136
+ Effect.gen(function* () {
137
+ const { build: loaded } = yield* ReactRouter;
138
+ if (Option.isNone(loaded)) return;
139
+ const build = loaded.value;
140
+ if (!build.publicPath.startsWith("/")) return;
141
+ const fileSystem = yield* FileSystem.FileSystem;
142
+ const path = yield* Path.Path;
143
+ const root = path.resolve(build.assetsBuildDirectory);
144
+ const readError = (cause: unknown) =>
145
+ new ClientBuildError({
146
+ message: `Cannot read the client build at ${root}`,
147
+ directory: root,
148
+ cause,
149
+ });
150
+ const entries = yield* fileSystem
151
+ .readDirectory(root, { recursive: true })
152
+ .pipe(Effect.mapError(readError));
153
+ const hashed = hashedDirectory(build, path);
154
+
155
+ if (Option.isSome(hashed)) {
156
+ const handler = yield* makeHandler({
157
+ root: path.join(root, hashed.value),
158
+ index: undefined,
159
+ cacheControl: hashedCacheControl,
160
+ });
161
+ yield* router
162
+ .prefixed(build.publicPath)
163
+ .prefixed(hashed.value)
164
+ .addAll([HttpRouter.route("GET", "/*", handler), HttpRouter.route("HEAD", "/*", handler)]);
165
+ }
166
+
167
+ /** The URL path of an entry of the client build, relative to `publicPath`. */
168
+ const urlPath = (entry: string): HttpRouter.PathInput => `/${entry.split(path.sep).join("/")}`;
169
+ const isServed = (url: string) =>
170
+ !url.split("/").some((segment) => segment.startsWith(".")) &&
171
+ Option.match(hashed, {
172
+ onNone: () => true,
173
+ onSome: (directory) => url !== directory && !url.startsWith(`${directory}/`),
174
+ });
175
+ const files = yield* Effect.filter(
176
+ entries.filter((entry) => isServed(urlPath(entry))),
177
+ (entry) =>
178
+ fileSystem.stat(path.join(root, entry)).pipe(
179
+ Effect.map((info) => info.type === "File"),
180
+ Effect.mapError(readError),
181
+ ),
182
+ );
183
+ const directories = files
184
+ .filter((file) => path.basename(file) === indexFile)
185
+ .map((file) => path.dirname(file));
186
+ const urls = Arr.appendAll(
187
+ files.map(urlPath),
188
+ // The root directory is `.`, whose URL path is `/`.
189
+ directories.map((directory) => (directory === "." ? "/" : urlPath(directory))),
190
+ );
191
+
192
+ const unmatchable = Arr.findFirst(urls, (url) => url.includes("*"));
193
+ if (Option.isSome(unmatchable)) {
194
+ return yield* new ClientBuildError({
195
+ message: `Cannot serve ${unmatchable.value} of the client build at ${root}: the router cannot match a * in a path`,
196
+ directory: root,
197
+ cause: undefined,
198
+ });
199
+ }
200
+
201
+ const handler = yield* makeHandler({ root, index: indexFile, cacheControl: otherCacheControl });
202
+ // A prefix of `/` changes the request URL on every request for nothing.
203
+ const underPublicPath = build.publicPath === "/" ? router : router.prefixed(build.publicPath);
204
+ yield* underPublicPath.addAll(
205
+ urls.flatMap((url) => {
206
+ // The router reads `:` as the start of a parameter, unless it is doubled.
207
+ const route: HttpRouter.PathInput = `/${url.slice(1).replaceAll(":", "::")}`;
208
+ return [HttpRouter.route("GET", route, handler), HttpRouter.route("HEAD", route, handler)];
209
+ }),
210
+ );
211
+ }),
212
+ );
package/src/check.ts CHANGED
@@ -30,6 +30,8 @@ type FailureMessage =
30
30
  *
31
31
  * It is an inline object type, not an interface, so the compiler prints its
32
32
  * fields in the error rather than an alias name.
33
+ *
34
+ * @stability unstable
33
35
  */
34
36
  export type RouteEffectFailure<Route, Export, Unhandled, Missing, Invalid> = {
35
37
  readonly "react-router-effect": FailureMessage;
@@ -112,6 +114,8 @@ type SameType<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false
112
114
  /**
113
115
  * A disagreement between the `Register` of `react-router-effect` and the
114
116
  * factory, which would type plain Effect routes' data wrongly.
117
+ *
118
+ * @stability unstable
115
119
  */
116
120
  export interface RegisterMismatch<Key, Factory, Registered> {
117
121
  readonly register: Key;
@@ -140,6 +144,8 @@ type Failures<Modules, Types> =
140
144
  * The failures of every Effect route function across the route modules React
141
145
  * Router's typegen registers, and the registration mismatches, as objects.
142
146
  * `never` when everything is handled. `RouteEffectCheck` reports them.
147
+ *
148
+ * @stability unstable
143
149
  */
144
150
  export type RouteEffectFailures<Factory, Modules = RegisteredRouteModules> = Failures<
145
151
  Modules,
@@ -199,6 +205,8 @@ type FailureLines<Failure> = Failure extends {
199
205
  * factory's runtime and request context. A middleware may also fail with what
200
206
  * its `next()` fails with. The check also fails when the `Register` of
201
207
  * `react-router-effect` disagrees with the factory.
208
+ *
209
+ * @stability unstable
202
210
  */
203
211
  export type RouteEffectCheck<Factory, Modules = RegisteredRouteModules> = {
204
212
  readonly [Line in FailureLines<RouteEffectFailures<Factory, Modules>>]: Line;
package/src/factory.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Context, Effect, type ManagedRuntime } from "effect";
1
+ import { Context, Effect, type ManagedRuntime, Predicate } from "effect";
2
2
  import { HttpServerRespondable, HttpServerResponse } from "effect/http";
3
3
  import {
4
4
  createContext,
@@ -244,6 +244,12 @@ const processRouteError = (
244
244
  * it in middleware and pass it to the factory's `requestContext` - the runner
245
245
  * reads it on every request and provides its services to the loader/action.
246
246
  *
247
+ * With the server adapter, do not set it to a new context. The adapter sets it
248
+ * to the request fiber's context before React Router runs, and a new context
249
+ * replaces those services, such as `HttpServerRequest` and the server span.
250
+ * Provide request services with a router middleware on `ReactRouter.routes`.
251
+ * The middleware below is for an app without the adapter.
252
+ *
247
253
  * It's just a `RouterContext`; create one with React Router's `createContext`:
248
254
  *
249
255
  * ```ts
@@ -257,7 +263,7 @@ const processRouteError = (
257
263
  *
258
264
  * export const requestContext: RequestContextKey<RequestContext> = createContext();
259
265
  *
260
- * // middleware:
266
+ * // middleware, in an app without the server adapter:
261
267
  * export const middleware: Route.MiddlewareFunction[] = [
262
268
  * ({ context, request }, next) => {
263
269
  * context.set(requestContext, Context.make(RequestContext, { userId: readUser(request) }));
@@ -511,11 +517,10 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
511
517
  // Defaults to an empty map when `errorHandlers` is omitted.
512
518
  const userHandlers = (config.errorHandlers ?? {}) as unknown as Record<
513
519
  string,
514
- ErrorHandler<DomainError>
520
+ ErrorHandler<Tagged>
515
521
  >;
516
-
517
- const isUserError = (e: unknown): e is DomainError =>
518
- typeof e === "object" && e !== null && "_tag" in e && (e as Tagged)._tag in userHandlers;
522
+ /** The tags of the registered domain errors. */
523
+ const userTags = Object.keys(userHandlers);
519
524
 
520
525
  /**
521
526
  * Runs a handler's effect to completion: dispatches its whole error channel
@@ -542,9 +547,11 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
542
547
  (e): Effect.Effect<unknown, FailureResponse> => {
543
548
  // Registered domain error → remap. A library-error return is processed
544
549
  // by the internal dispatch; an `Effect` return is used as-is.
545
- if (isUserError(e)) {
546
- const out = userHandlers[e._tag](e);
547
- return isRouteError(out) ? processRouteError(out) : out;
550
+ for (const tag of userTags) {
551
+ if (Predicate.isTagged(e, tag)) {
552
+ const out = userHandlers[tag](e);
553
+ return isRouteError(out) ? processRouteError(out) : out;
554
+ }
548
555
  }
549
556
  // A library route error raised directly in the handler → recover/throw.
550
557
  if (isRouteError(e)) return processRouteError(e);
@@ -570,6 +577,7 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
570
577
  // `provideContext` removes `ReqServices`, and when no `requestContext` is
571
578
  // configured `ReqServices` is `never` (nothing removed).
572
579
  const store = readRequestStore(context);
580
+ // oxlint-disable-next-line effecttsgo/unsafe-effect-type-assertion -- the cast is sound, as the comment above explains
573
581
  const provided = (
574
582
  store === undefined ? program : Effect.provideContext(program, store)
575
583
  ) as Effect.Effect<unknown, FailureResponse, RServices>;
@@ -580,7 +588,8 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
580
588
  // signal and discards its result, so the interrupt rejection is never rendered.
581
589
  return runtime
582
590
  ? runtime.runPromise(provided, { signal })
583
- : Effect.runPromise(provided as Effect.Effect<unknown, FailureResponse>, { signal });
591
+ : // oxlint-disable-next-line effecttsgo/unsafe-effect-type-assertion -- with no runtime `RServices` is `never`
592
+ Effect.runPromise(provided as Effect.Effect<unknown, FailureResponse>, { signal });
584
593
  }
585
594
 
586
595
  function makeLoaderOrAction<Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E, R>(
@@ -705,6 +714,7 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
705
714
  function runResult(result: unknown, args: RouteArgs): unknown {
706
715
  return Effect.isEffect(result)
707
716
  ? runProgram(
717
+ // oxlint-disable-next-line effecttsgo/any-unknown-in-error-context, effecttsgo/unsafe-effect-type-assertion -- a route function's Effect is only known to be an Effect here; `runProgram` dispatches its whole error channel
708
718
  result as Effect.Effect<unknown, unknown, RServices | ReqServices>,
709
719
  args.context,
710
720
  args.request.signal,
@@ -1,7 +1,8 @@
1
1
  /** `react-router-effect/server`: serve React Router from the Effect HTTP server. */
2
2
  import "./route-data.ts";
3
3
 
4
- export * as ReactRouterHttp from "./server.ts";
4
+ export { ReactRouter } from "./server.ts";
5
+ export * as ReactRouterAssets from "./assets.ts";
5
6
  export type {
6
7
  RegisterMismatch,
7
8
  RouteEffectCheck,