effective-rsc 0.1.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/LICENSE +21 -0
- package/LLMS.md +382 -0
- package/README.md +235 -0
- package/bin/ersc.js +3 -0
- package/dist/application/component.d.ts +15 -0
- package/dist/application/component.js +20 -0
- package/dist/application/component.js.map +1 -0
- package/dist/application/definition.d.ts +31 -0
- package/dist/application/definition.js +57 -0
- package/dist/application/definition.js.map +1 -0
- package/dist/application/ersc-identity.d.ts +25 -0
- package/dist/application/ersc-identity.js +23 -0
- package/dist/application/ersc-identity.js.map +1 -0
- package/dist/application/ersc.d.ts +28 -0
- package/dist/application/ersc.js +72 -0
- package/dist/application/ersc.js.map +1 -0
- package/dist/application/layout.d.ts +18 -0
- package/dist/application/layout.js +17 -0
- package/dist/application/layout.js.map +1 -0
- package/dist/application/loading.d.ts +17 -0
- package/dist/application/loading.js +14 -0
- package/dist/application/loading.js.map +1 -0
- package/dist/application/middleware.d.ts +36 -0
- package/dist/application/middleware.js +46 -0
- package/dist/application/middleware.js.map +1 -0
- package/dist/application/page.d.ts +58 -0
- package/dist/application/page.js +50 -0
- package/dist/application/page.js.map +1 -0
- package/dist/application/render-runtime.d.ts +9 -0
- package/dist/application/render-runtime.js +28 -0
- package/dist/application/render-runtime.js.map +1 -0
- package/dist/application/route-graph.d.ts +22 -0
- package/dist/application/route-graph.js +68 -0
- package/dist/application/route-graph.js.map +1 -0
- package/dist/application/route-path.d.ts +31 -0
- package/dist/application/route-path.js +67 -0
- package/dist/application/route-path.js.map +1 -0
- package/dist/application/routes.d.ts +66 -0
- package/dist/application/routes.js +159 -0
- package/dist/application/routes.js.map +1 -0
- package/dist/application/server-fn.d.ts +30 -0
- package/dist/application/server-fn.js +58 -0
- package/dist/application/server-fn.js.map +1 -0
- package/dist/build/build.d.ts +26 -0
- package/dist/build/build.js +59 -0
- package/dist/build/build.js.map +1 -0
- package/dist/build/compiled-server.d.ts +53 -0
- package/dist/build/compiled-server.js +77 -0
- package/dist/build/compiled-server.js.map +1 -0
- package/dist/build/contract.d.ts +23 -0
- package/dist/build/contract.js +41 -0
- package/dist/build/contract.js.map +1 -0
- package/dist/build/dev-channel.d.ts +20 -0
- package/dist/build/dev-channel.js +71 -0
- package/dist/build/dev-channel.js.map +1 -0
- package/dist/build/dev.d.ts +41 -0
- package/dist/build/dev.js +186 -0
- package/dist/build/dev.js.map +1 -0
- package/dist/build/rsc-entry.d.ts +4 -0
- package/dist/build/rsc-entry.js +17 -0
- package/dist/build/rsc-entry.js.map +1 -0
- package/dist/build/rspack-config.d.ts +17 -0
- package/dist/build/rspack-config.js +259 -0
- package/dist/build/rspack-config.js.map +1 -0
- package/dist/build/rspack.d.ts +45 -0
- package/dist/build/rspack.js +231 -0
- package/dist/build/rspack.js.map +1 -0
- package/dist/build/terminal.d.ts +9 -0
- package/dist/build/terminal.js +17 -0
- package/dist/build/terminal.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +116 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/application.d.ts +2 -0
- package/dist/client/application.js +73 -0
- package/dist/client/application.js.map +1 -0
- package/dist/client/browser-capabilities.d.ts +9 -0
- package/dist/client/browser-capabilities.js +20 -0
- package/dist/client/browser-capabilities.js.map +1 -0
- package/dist/client/browser-effect-runner.d.ts +13 -0
- package/dist/client/browser-effect-runner.js +13 -0
- package/dist/client/browser-effect-runner.js.map +1 -0
- package/dist/client/browser-renderer.d.ts +70 -0
- package/dist/client/browser-renderer.js +159 -0
- package/dist/client/browser-renderer.js.map +1 -0
- package/dist/client/browser-screen.d.ts +2 -0
- package/dist/client/browser-screen.js +215 -0
- package/dist/client/browser-screen.js.map +1 -0
- package/dist/client/call-server.d.ts +8 -0
- package/dist/client/call-server.js +151 -0
- package/dist/client/call-server.js.map +1 -0
- package/dist/client/client-router.d.ts +6 -0
- package/dist/client/client-router.js +588 -0
- package/dist/client/client-router.js.map +1 -0
- package/dist/client/entry.d.ts +1 -0
- package/dist/client/entry.js +12 -0
- package/dist/client/entry.js.map +1 -0
- package/dist/client/flight-client.d.ts +83 -0
- package/dist/client/flight-client.js +146 -0
- package/dist/client/flight-client.js.map +1 -0
- package/dist/client/initial-flight-stream.d.ts +18 -0
- package/dist/client/initial-flight-stream.js +60 -0
- package/dist/client/initial-flight-stream.js.map +1 -0
- package/dist/client/navigation-api.d.ts +34 -0
- package/dist/client/navigation-api.js +25 -0
- package/dist/client/navigation-api.js.map +1 -0
- package/dist/client/navigation-routing.d.ts +3 -0
- package/dist/client/navigation-routing.js +8 -0
- package/dist/client/navigation-routing.js.map +1 -0
- package/dist/client/react-dom-renderer.d.ts +20 -0
- package/dist/client/react-dom-renderer.js +101 -0
- package/dist/client/react-dom-renderer.js.map +1 -0
- package/dist/client/route-loader.d.ts +81 -0
- package/dist/client/route-loader.js +128 -0
- package/dist/client/route-loader.js.map +1 -0
- package/dist/client/route-refresh.d.ts +31 -0
- package/dist/client/route-refresh.js +132 -0
- package/dist/client/route-refresh.js.map +1 -0
- package/dist/client/route-tree.d.ts +7 -0
- package/dist/client/route-tree.js +34 -0
- package/dist/client/route-tree.js.map +1 -0
- package/dist/dev/channel.d.ts +18 -0
- package/dist/dev/channel.js +30 -0
- package/dist/dev/channel.js.map +1 -0
- package/dist/dev/client.d.ts +3 -0
- package/dist/dev/client.js +132 -0
- package/dist/dev/client.js.map +1 -0
- package/dist/dev/hmr-update.d.ts +20 -0
- package/dist/dev/hmr-update.js +28 -0
- package/dist/dev/hmr-update.js.map +1 -0
- package/dist/dev/panel.d.ts +41 -0
- package/dist/dev/panel.js +9946 -0
- package/dist/dev/panel.js.map +1 -0
- package/dist/dev/runtime-failure.d.ts +11 -0
- package/dist/dev/runtime-failure.js +32 -0
- package/dist/dev/runtime-failure.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/rsc/flight.d.ts +16 -0
- package/dist/rsc/flight.js +6 -0
- package/dist/rsc/flight.js.map +1 -0
- package/dist/rsc/render-route-tree.d.ts +11 -0
- package/dist/rsc/render-route-tree.js +52 -0
- package/dist/rsc/render-route-tree.js.map +1 -0
- package/dist/rsc/route-tree.d.ts +6 -0
- package/dist/rsc/route-tree.js +5 -0
- package/dist/rsc/route-tree.js.map +1 -0
- package/dist/server/application.d.ts +17 -0
- package/dist/server/application.js +178 -0
- package/dist/server/application.js.map +1 -0
- package/dist/server/flight-html-stream.d.ts +19 -0
- package/dist/server/flight-html-stream.js +193 -0
- package/dist/server/flight-html-stream.js.map +1 -0
- package/dist/server/flight-renderer.d.ts +31 -0
- package/dist/server/flight-renderer.js +47 -0
- package/dist/server/flight-renderer.js.map +1 -0
- package/dist/server/html-renderer.d.ts +28 -0
- package/dist/server/html-renderer.js +82 -0
- package/dist/server/html-renderer.js.map +1 -0
- package/dist/server/request-outcome.d.ts +8 -0
- package/dist/server/request-outcome.js +5 -0
- package/dist/server/request-outcome.js.map +1 -0
- package/dist/server/server-config.d.ts +17 -0
- package/dist/server/server-config.js +18 -0
- package/dist/server/server-config.js.map +1 -0
- package/dist/server/server-fn-outcome.d.ts +13 -0
- package/dist/server/server-fn-outcome.js +24 -0
- package/dist/server/server-fn-outcome.js.map +1 -0
- package/dist/server/server-fn-request.d.ts +24 -0
- package/dist/server/server-fn-request.js +160 -0
- package/dist/server/server-fn-request.js.map +1 -0
- package/dist/unsupported.d.ts +0 -0
- package/dist/unsupported.js +5 -0
- package/dist/unsupported.js.map +1 -0
- package/docs/01-getting-started/01_first-application.tsx +26 -0
- package/docs/01-getting-started/20_styling.tsx +6 -0
- package/docs/01-getting-started/index.md +27 -0
- package/docs/01-getting-started/styles.css +1 -0
- package/docs/02-guides/01-server-functions/10_ersc.ts +6 -0
- package/docs/02-guides/01-server-functions/20_follow-author.ts +15 -0
- package/docs/02-guides/01-server-functions/30_follow-author-button.tsx +19 -0
- package/docs/02-guides/01-server-functions/40_application.tsx +30 -0
- package/docs/02-guides/01-server-functions/index.md +19 -0
- package/docs/02-guides/02-services/10_catalog.ts +22 -0
- package/docs/02-guides/02-services/20_application.tsx +44 -0
- package/docs/02-guides/02-services/index.md +17 -0
- package/docs/02-guides/03-routing/10_ersc.ts +6 -0
- package/docs/02-guides/03-routing/10_layouts.tsx +36 -0
- package/docs/02-guides/03-routing/20_pages.tsx +23 -0
- package/docs/02-guides/03-routing/30_routes.tsx +17 -0
- package/docs/02-guides/03-routing/40_application.ts +7 -0
- package/docs/02-guides/03-routing/index.md +22 -0
- package/docs/02-guides/04-middleware/10_auth.ts +27 -0
- package/docs/02-guides/04-middleware/20_account-page.tsx +13 -0
- package/docs/02-guides/04-middleware/30_update-profile.ts +16 -0
- package/docs/02-guides/04-middleware/40_application.tsx +20 -0
- package/docs/02-guides/04-middleware/index.md +25 -0
- package/docs/02-guides/05-http/10_application-layer.tsx +60 -0
- package/docs/02-guides/05-http/index.md +23 -0
- package/docs/02-guides/index.md +11 -0
- package/docs/03-advanced/01-request-runtime-and-lifetimes/index.md +13 -0
- package/docs/03-advanced/02-client-navigation/index.md +61 -0
- package/docs/03-advanced/03-server-function-execution-and-refresh/index.md +17 -0
- package/docs/03-advanced/index.md +11 -0
- package/docs/04-api-reference/01-application/index.md +8 -0
- package/docs/04-api-reference/02-page/index.md +12 -0
- package/docs/04-api-reference/03-layout/index.md +4 -0
- package/docs/04-api-reference/04-loading/index.md +4 -0
- package/docs/04-api-reference/05-component/10_component.tsx +18 -0
- package/docs/04-api-reference/05-component/index.md +10 -0
- package/docs/04-api-reference/06-middleware/index.md +33 -0
- package/docs/04-api-reference/07-routes/index.md +20 -0
- package/docs/04-api-reference/08-server-fn/index.md +43 -0
- package/docs/04-api-reference/index.md +17 -0
- package/docs/environment.d.ts +1 -0
- package/docs/index.md +15 -0
- package/logo-dark.svg +6 -0
- package/logo.svg +6 -0
- package/package.json +87 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
## Getting started
|
|
2
|
+
|
|
3
|
+
Create an application with compatible dependencies and Tailwind support:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
bunx create-ersc-app my-application
|
|
7
|
+
cd my-application
|
|
8
|
+
bun run dev
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Open `http://localhost:18193`.
|
|
12
|
+
|
|
13
|
+
In `src/application.tsx`, create one ERSC identity, define a root Layout and Page, compose Routes,
|
|
14
|
+
and export `ERSC.make(...)`.
|
|
15
|
+
|
|
16
|
+
For a production check, run `bun run check`, `bun run build`, and `bun run start`. Both
|
|
17
|
+
`ersc dev` and `ersc start` accept `--hostname` and `--port`; flags take precedence over
|
|
18
|
+
`HOST` and `PORT`. See the package README for requirements and manual installation.
|
|
19
|
+
|
|
20
|
+
Files in `public/` are served from `/` with `Cache-Control: public, max-age=0`.
|
|
21
|
+
|
|
22
|
+
<!-- source-navigation -->
|
|
23
|
+
|
|
24
|
+
### Examples
|
|
25
|
+
|
|
26
|
+
- [Minimal application](./01_first-application.tsx)
|
|
27
|
+
- [Stylesheet import](./20_styling.tsx)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Defining a Server Function
|
|
3
|
+
*
|
|
4
|
+
* ERSC decodes FormData before running the Effect handler.
|
|
5
|
+
*/
|
|
6
|
+
'use server';
|
|
7
|
+
|
|
8
|
+
import { Effect, Schema } from 'effect';
|
|
9
|
+
|
|
10
|
+
import { ERSC } from './10_ersc';
|
|
11
|
+
|
|
12
|
+
export const followAuthor = ERSC.ServerFn.make({
|
|
13
|
+
input: Schema.fromFormData(Schema.Struct({ authorId: Schema.NonEmptyString })),
|
|
14
|
+
handler: ({ authorId }) => Effect.logInfo('Followed author', { authorId }),
|
|
15
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Rendering a direct form action
|
|
3
|
+
*
|
|
4
|
+
* A FormData Server Function can be passed directly to form action.
|
|
5
|
+
*/
|
|
6
|
+
import { Effect } from 'effect';
|
|
7
|
+
|
|
8
|
+
import { ERSC } from './10_ersc';
|
|
9
|
+
import { followAuthor } from './20_follow-author';
|
|
10
|
+
|
|
11
|
+
export const FollowAuthorButton = ERSC.Component.make({
|
|
12
|
+
render: ({ authorId }: { readonly authorId: string }) =>
|
|
13
|
+
Effect.succeed(
|
|
14
|
+
<form action={followAuthor}>
|
|
15
|
+
<input name='authorId' type='hidden' value={authorId} />
|
|
16
|
+
<button type='submit'>Follow author</button>
|
|
17
|
+
</form>,
|
|
18
|
+
),
|
|
19
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Closing the Server Function application
|
|
3
|
+
*/
|
|
4
|
+
import { Effect } from 'effect';
|
|
5
|
+
|
|
6
|
+
import { ERSC } from './10_ersc';
|
|
7
|
+
import { FollowAuthorButton } from './30_follow-author-button';
|
|
8
|
+
|
|
9
|
+
const RootLayout = ERSC.Layout.make({
|
|
10
|
+
render: ({ children }) =>
|
|
11
|
+
Effect.succeed(
|
|
12
|
+
<html lang='en'>
|
|
13
|
+
<body>{children}</body>
|
|
14
|
+
</html>,
|
|
15
|
+
),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const AuthorPage = ERSC.Page.make({
|
|
19
|
+
render: () =>
|
|
20
|
+
Effect.succeed(
|
|
21
|
+
<main>
|
|
22
|
+
<h1>Grace Hopper</h1>
|
|
23
|
+
<FollowAuthorButton authorId='grace-hopper' />
|
|
24
|
+
</main>,
|
|
25
|
+
),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export default ERSC.make({
|
|
29
|
+
routes: ERSC.Routes.make({ layout: RootLayout }).page('/', AuthorPage),
|
|
30
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
## Server Functions
|
|
2
|
+
|
|
3
|
+
`ERSC.ServerFn.make` decodes Schema input and runs an Effect handler with application services.
|
|
4
|
+
|
|
5
|
+
Callers pass the Schema's encoded type and the handler receives its decoded type. Use an ordinary
|
|
6
|
+
`Schema.Struct(...)` for object input. Use `Schema.fromFormData(...)` when a native form supplies the
|
|
7
|
+
input; a function returning `void` can then be passed directly to `<form action>`. Let the Schema
|
|
8
|
+
infer the handler parameter.
|
|
9
|
+
|
|
10
|
+
A successful invocation refreshes the current route.
|
|
11
|
+
|
|
12
|
+
<!-- source-navigation -->
|
|
13
|
+
|
|
14
|
+
### Examples
|
|
15
|
+
|
|
16
|
+
- [Create the ERSC identity](./10_ersc.ts)
|
|
17
|
+
- [Define a Server Function](./20_follow-author.ts)
|
|
18
|
+
- [Use a direct form action](./30_follow-author-button.tsx)
|
|
19
|
+
- [Compose the application](./40_application.tsx)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title An application-owned service
|
|
3
|
+
*
|
|
4
|
+
* ERSC consumes the service contract and Layer; their construction is ordinary Effect code.
|
|
5
|
+
*/
|
|
6
|
+
import { Context, Effect, Layer } from 'effect';
|
|
7
|
+
|
|
8
|
+
export type CatalogItem = {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly price: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export class Catalog extends Context.Service<Catalog>()('shop/services/Catalog', {
|
|
14
|
+
make: Effect.succeed({
|
|
15
|
+
featured: Effect.succeed([
|
|
16
|
+
{ name: 'Mechanical keyboard', price: 129 },
|
|
17
|
+
{ name: 'Studio headphones', price: 249 },
|
|
18
|
+
] satisfies ReadonlyArray<CatalogItem>),
|
|
19
|
+
}),
|
|
20
|
+
}) {
|
|
21
|
+
static readonly layer = Layer.effect(this, this.make);
|
|
22
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Providing services at the composition boundary
|
|
3
|
+
*
|
|
4
|
+
* Declare the service union on ERSC and provide its Layer once at ERSC.make.
|
|
5
|
+
*/
|
|
6
|
+
import { Effect } from 'effect';
|
|
7
|
+
import { Application } from 'effective-rsc';
|
|
8
|
+
|
|
9
|
+
import { Catalog } from './10_catalog';
|
|
10
|
+
|
|
11
|
+
const ERSC = Application.ersc<Catalog>();
|
|
12
|
+
|
|
13
|
+
const RootLayout = ERSC.Layout.make({
|
|
14
|
+
render: ({ children }) =>
|
|
15
|
+
Effect.succeed(
|
|
16
|
+
<html lang='en'>
|
|
17
|
+
<body>{children}</body>
|
|
18
|
+
</html>,
|
|
19
|
+
),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const FeaturedPage = ERSC.Page.make({
|
|
23
|
+
render: Effect.fn('FeaturedPage.render')(function* () {
|
|
24
|
+
const catalog = yield* Catalog;
|
|
25
|
+
const items = yield* catalog.featured;
|
|
26
|
+
return (
|
|
27
|
+
<main>
|
|
28
|
+
<h1>Featured products</h1>
|
|
29
|
+
<ul>
|
|
30
|
+
{items.map((item) => (
|
|
31
|
+
<li key={item.name}>
|
|
32
|
+
{item.name}: ${item.price}
|
|
33
|
+
</li>
|
|
34
|
+
))}
|
|
35
|
+
</ul>
|
|
36
|
+
</main>
|
|
37
|
+
);
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export default ERSC.make({
|
|
42
|
+
routes: ERSC.Routes.make({ layout: RootLayout }).page('/', FeaturedPage),
|
|
43
|
+
layer: Catalog.layer,
|
|
44
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
## Services
|
|
2
|
+
|
|
3
|
+
Define services with Effect, then follow the ERSC composition convention:
|
|
4
|
+
|
|
5
|
+
1. Declare the complete service union with `Application.ersc<Services>()`.
|
|
6
|
+
2. Let Pages, Layouts, Components, and Server Functions require members of that union.
|
|
7
|
+
3. Provide the complete Layer once with `ERSC.make({ layer })`.
|
|
8
|
+
|
|
9
|
+
This keeps implementations at the application composition boundary while preserving each
|
|
10
|
+
renderer's inferred service requirements.
|
|
11
|
+
|
|
12
|
+
<!-- source-navigation -->
|
|
13
|
+
|
|
14
|
+
### Examples
|
|
15
|
+
|
|
16
|
+
- [Define a service](./10_catalog.ts)
|
|
17
|
+
- [Provide services](./20_application.tsx)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Layout and Loading concerns
|
|
3
|
+
*
|
|
4
|
+
* Layout is Effectful; Loading is synchronous and service-free.
|
|
5
|
+
*/
|
|
6
|
+
import { Effect } from 'effect';
|
|
7
|
+
|
|
8
|
+
import { ERSC } from './10_ersc';
|
|
9
|
+
|
|
10
|
+
export const RootLayout = ERSC.Layout.make({
|
|
11
|
+
render: ({ children }) =>
|
|
12
|
+
Effect.succeed(
|
|
13
|
+
<html lang='en'>
|
|
14
|
+
<body>
|
|
15
|
+
<nav>
|
|
16
|
+
<a href='/'>Home</a> · <a href='/articles/effectful-rsc'>Article</a>
|
|
17
|
+
</nav>
|
|
18
|
+
{children}
|
|
19
|
+
</body>
|
|
20
|
+
</html>,
|
|
21
|
+
),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const ArticleLayout = ERSC.Layout.make({
|
|
25
|
+
render: ({ children }) =>
|
|
26
|
+
Effect.succeed(
|
|
27
|
+
<section>
|
|
28
|
+
<h1>Articles</h1>
|
|
29
|
+
{children}
|
|
30
|
+
</section>,
|
|
31
|
+
),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const ArticleLoading = ERSC.Loading.make({
|
|
35
|
+
render: () => <p>Loading article…</p>,
|
|
36
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Static and parameterized Pages
|
|
3
|
+
*
|
|
4
|
+
* A Page Schema decodes captured path strings for render.
|
|
5
|
+
*/
|
|
6
|
+
import { Effect, Schema } from 'effect';
|
|
7
|
+
|
|
8
|
+
import { ERSC } from './10_ersc';
|
|
9
|
+
|
|
10
|
+
export const HomePage = ERSC.Page.make({
|
|
11
|
+
render: () => Effect.succeed(<h1>Publishing home</h1>),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const ArticlePage = ERSC.Page.make({
|
|
15
|
+
params: Schema.Struct({ slug: Schema.NonEmptyString }),
|
|
16
|
+
render: ({ params }) =>
|
|
17
|
+
Effect.succeed(
|
|
18
|
+
<article>
|
|
19
|
+
<h2>{params.slug}</h2>
|
|
20
|
+
<p>This article belongs to the mounted article route scope.</p>
|
|
21
|
+
</article>,
|
|
22
|
+
),
|
|
23
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Composing and mounting Routes
|
|
3
|
+
*
|
|
4
|
+
* Mounting retains the child graph's Layout and Loading ancestry.
|
|
5
|
+
*/
|
|
6
|
+
import { ERSC } from './10_ersc';
|
|
7
|
+
import { ArticleLayout, ArticleLoading, RootLayout } from './10_layouts';
|
|
8
|
+
import { ArticlePage, HomePage } from './20_pages';
|
|
9
|
+
|
|
10
|
+
const articleRoutes = ERSC.Routes.make({
|
|
11
|
+
layout: ArticleLayout,
|
|
12
|
+
loading: ArticleLoading,
|
|
13
|
+
}).page('/:slug', ArticlePage);
|
|
14
|
+
|
|
15
|
+
export const applicationRoutes = ERSC.Routes.make({ layout: RootLayout })
|
|
16
|
+
.page('/', HomePage)
|
|
17
|
+
.mount('/articles', articleRoutes);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
## Routing, parameters, and loading
|
|
2
|
+
|
|
3
|
+
- Routes are immutable and belong to one ERSC identity.
|
|
4
|
+
- `page(path, page)` attaches a Page; `mount(prefix, routes)` nests a route scope.
|
|
5
|
+
- Mounted scopes retain their Layout and Loading ancestry.
|
|
6
|
+
- Page parameter Schemas decode Effect HTTP path captures before rendering.
|
|
7
|
+
- Effect HTTP owns route matching; ERSC rejects duplicate shapes and invalid composition while
|
|
8
|
+
building the graph.
|
|
9
|
+
|
|
10
|
+
<!-- source-navigation -->
|
|
11
|
+
|
|
12
|
+
### Examples
|
|
13
|
+
|
|
14
|
+
- [Create the ERSC identity](./10_ersc.ts)
|
|
15
|
+
- [Define Layout and Loading concerns](./10_layouts.tsx)
|
|
16
|
+
- [Define Pages](./20_pages.tsx)
|
|
17
|
+
- [Compose and mount Routes](./30_routes.tsx)
|
|
18
|
+
- [Compose the application](./40_application.ts)
|
|
19
|
+
|
|
20
|
+
### Related
|
|
21
|
+
|
|
22
|
+
- [Middleware](../04-middleware/index.md)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Defining an authenticated view
|
|
3
|
+
*
|
|
4
|
+
* Middleware can short-circuit a request and provide typed services downstream.
|
|
5
|
+
*/
|
|
6
|
+
import { Context, Effect } from 'effect';
|
|
7
|
+
import { HttpServerRequest, HttpServerResponse } from 'effect/unstable/http';
|
|
8
|
+
import { Application } from 'effective-rsc';
|
|
9
|
+
|
|
10
|
+
export class CurrentUser extends Context.Service<CurrentUser, { readonly name: string }>()(
|
|
11
|
+
'docs/auth/CurrentUser',
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
export const ERSC = Application.ersc();
|
|
15
|
+
|
|
16
|
+
const RequireCurrentUser = ERSC.Middleware.make<{ provides: CurrentUser }>(
|
|
17
|
+
Effect.fnUntraced(function* (httpEffect) {
|
|
18
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
19
|
+
const name = request.cookies['user']?.trim();
|
|
20
|
+
if (name === undefined || name === '') {
|
|
21
|
+
return HttpServerResponse.text('Unauthorized', { status: 401 });
|
|
22
|
+
}
|
|
23
|
+
return yield* httpEffect.pipe(Effect.provideService(CurrentUser, { name }));
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
export const AuthenticatedERSC = ERSC.withMiddleware(RequireCurrentUser);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Consuming middleware data in a Page
|
|
3
|
+
*/
|
|
4
|
+
import { Effect } from 'effect';
|
|
5
|
+
|
|
6
|
+
import { AuthenticatedERSC, CurrentUser } from './10_auth';
|
|
7
|
+
|
|
8
|
+
export const AccountPage = AuthenticatedERSC.Page.make({
|
|
9
|
+
render: Effect.fn('AccountPage.render')(function* () {
|
|
10
|
+
const user = yield* CurrentUser;
|
|
11
|
+
return <h1>Welcome, {user.name}</h1>;
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Consuming middleware data in a Server Function
|
|
3
|
+
*/
|
|
4
|
+
'use server';
|
|
5
|
+
|
|
6
|
+
import { Effect, Schema } from 'effect';
|
|
7
|
+
|
|
8
|
+
import { AuthenticatedERSC, CurrentUser } from './10_auth';
|
|
9
|
+
|
|
10
|
+
export const updateDisplayName = AuthenticatedERSC.ServerFn.make({
|
|
11
|
+
input: Schema.Struct({ name: Schema.NonEmptyString }),
|
|
12
|
+
handler: Effect.fn('updateDisplayName')(function* ({ name }) {
|
|
13
|
+
const user = yield* CurrentUser;
|
|
14
|
+
yield* Effect.logInfo('Updating profile', { from: user.name, to: name });
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Activating middleware with Routes
|
|
3
|
+
*/
|
|
4
|
+
import { Effect } from 'effect';
|
|
5
|
+
|
|
6
|
+
import { ERSC, AuthenticatedERSC } from './10_auth';
|
|
7
|
+
import { AccountPage } from './20_account-page';
|
|
8
|
+
|
|
9
|
+
const RootLayout = ERSC.Layout.make({
|
|
10
|
+
render: ({ children }) =>
|
|
11
|
+
Effect.succeed(
|
|
12
|
+
<html lang='en'>
|
|
13
|
+
<body>{children}</body>
|
|
14
|
+
</html>,
|
|
15
|
+
),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const routes = AuthenticatedERSC.Routes.make({ layout: RootLayout }).page('/account', AccountPage);
|
|
19
|
+
|
|
20
|
+
export default ERSC.make({ routes });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## Middleware
|
|
2
|
+
|
|
3
|
+
Create middleware from the base ERSC view, then derive a view with
|
|
4
|
+
`ERSC.withMiddleware(middleware)`. The derived view has the same ERSC identity and retains the
|
|
5
|
+
middleware scope.
|
|
6
|
+
|
|
7
|
+
Routes and Server Functions created from the derived view activate that scope. Pages, Layouts, and
|
|
8
|
+
Components created from it may require the services declared by the middleware and consume them only
|
|
9
|
+
while rendered inside an active scope.
|
|
10
|
+
|
|
11
|
+
Use `ERSC.Middleware.make<{ provides: CurrentUser }>(handler)` when a middleware provides a
|
|
12
|
+
request-scoped service. The handler must provide that service to the downstream Effect. Chain
|
|
13
|
+
`withMiddleware` in request order; responses unwind in reverse.
|
|
14
|
+
|
|
15
|
+
Scoped middleware does not wrap userland HTTP, assets, or unmatched requests. Put server-wide policy
|
|
16
|
+
in native global Effect HTTP middleware supplied through the application Layer.
|
|
17
|
+
|
|
18
|
+
<!-- source-navigation -->
|
|
19
|
+
|
|
20
|
+
### Examples
|
|
21
|
+
|
|
22
|
+
- [Define an authenticated view](./10_auth.ts)
|
|
23
|
+
- [Consume the service in a Page](./20_account-page.tsx)
|
|
24
|
+
- [Consume the service in a Server Function](./30_update-profile.ts)
|
|
25
|
+
- [Activate the scope with Routes](./40_application.tsx)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Composing ERSC and userland HTTP
|
|
3
|
+
*
|
|
4
|
+
* ERSC concerns and native HTTP routes share one application Layer.
|
|
5
|
+
*/
|
|
6
|
+
import { Effect, Layer } from 'effect';
|
|
7
|
+
import { HttpRouter, HttpServerResponse } from 'effect/unstable/http';
|
|
8
|
+
import { Application } from 'effective-rsc';
|
|
9
|
+
|
|
10
|
+
import { Catalog } from '../02-services/10_catalog';
|
|
11
|
+
|
|
12
|
+
const CatalogApi = HttpRouter.use(
|
|
13
|
+
Effect.fnUntraced(function* (router) {
|
|
14
|
+
const catalog = yield* Catalog;
|
|
15
|
+
const response = Effect.map(catalog.featured, HttpServerResponse.jsonUnsafe);
|
|
16
|
+
yield* router.add('GET', '/api/catalog', response);
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const GlobalHeaders = HttpRouter.middleware(
|
|
21
|
+
(httpEffect) =>
|
|
22
|
+
Effect.map(httpEffect, HttpServerResponse.setHeader('x-content-type-options', 'nosniff')),
|
|
23
|
+
{ global: true },
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const ApplicationLayer = Layer.mergeAll(CatalogApi, GlobalHeaders).pipe(
|
|
27
|
+
Layer.provideMerge(Catalog.layer),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const ERSC = Application.ersc<Catalog>();
|
|
31
|
+
|
|
32
|
+
const RootLayout = ERSC.Layout.make({
|
|
33
|
+
render: ({ children }) =>
|
|
34
|
+
Effect.succeed(
|
|
35
|
+
<html lang='en'>
|
|
36
|
+
<body>{children}</body>
|
|
37
|
+
</html>,
|
|
38
|
+
),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const CatalogPage = ERSC.Page.make({
|
|
42
|
+
render: Effect.fn('CatalogPage.render')(function* () {
|
|
43
|
+
const catalog = yield* Catalog;
|
|
44
|
+
const items = yield* catalog.featured;
|
|
45
|
+
return (
|
|
46
|
+
<ul>
|
|
47
|
+
{items.map((item) => (
|
|
48
|
+
<li key={item.name}>
|
|
49
|
+
{item.name}: ${item.price}
|
|
50
|
+
</li>
|
|
51
|
+
))}
|
|
52
|
+
</ul>
|
|
53
|
+
);
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export default ERSC.make({
|
|
58
|
+
routes: ERSC.Routes.make({ layout: RootLayout }).page('/catalog', CatalogPage),
|
|
59
|
+
layer: ApplicationLayer,
|
|
60
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
## Userland HTTP
|
|
2
|
+
|
|
3
|
+
Register native Effect `HttpRouter`, `HttpApi`, or RPC layers in the Layer passed to
|
|
4
|
+
`ERSC.make({ layer })`. They share the framework's HTTP server, application services, and shutdown
|
|
5
|
+
scope.
|
|
6
|
+
|
|
7
|
+
Register routes that require application services with `HttpRouter.use`, then retain those services
|
|
8
|
+
with `Layer.provideMerge`.
|
|
9
|
+
|
|
10
|
+
Native global middleware belongs in the same application Layer. It observes Page requests, Server
|
|
11
|
+
Function requests, userland HTTP, assets, and unmatched requests. ERSC-scoped middleware has narrower
|
|
12
|
+
reach.
|
|
13
|
+
|
|
14
|
+
<!-- source-navigation -->
|
|
15
|
+
|
|
16
|
+
### Examples
|
|
17
|
+
|
|
18
|
+
- [Compose ERSC and userland HTTP](./10_application-layer.tsx)
|
|
19
|
+
|
|
20
|
+
### Related
|
|
21
|
+
|
|
22
|
+
- [Services](../02-services/index.md)
|
|
23
|
+
- [Middleware](../04-middleware/index.md)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## Guides
|
|
2
|
+
|
|
3
|
+
Familiarity with React Server Components and Effect is assumed.
|
|
4
|
+
|
|
5
|
+
<!-- source-navigation -->
|
|
6
|
+
|
|
7
|
+
- [Server Functions](./01-server-functions/index.md)
|
|
8
|
+
- [Services](./02-services/index.md)
|
|
9
|
+
- [Routing](./03-routing/index.md)
|
|
10
|
+
- [Middleware](./04-middleware/index.md)
|
|
11
|
+
- [Userland HTTP](./05-http/index.md)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## Request runtime and lifetimes
|
|
2
|
+
|
|
3
|
+
The server builds the Layer passed to `ERSC.make` once and releases it at shutdown. Its services have
|
|
4
|
+
application lifetime.
|
|
5
|
+
|
|
6
|
+
Each HTTP request has an independent Effect scope. Server Function handlers run in the HTTP request
|
|
7
|
+
fiber. Page, Layout, and Component render Effects run in a request-owned render scope.
|
|
8
|
+
|
|
9
|
+
Closing the response interrupts unfinished request work and runs its finalizers. Acquire
|
|
10
|
+
request-local resources inside the request Effect so their lifetime follows the request
|
|
11
|
+
automatically.
|
|
12
|
+
|
|
13
|
+
Give work that must outlive a request an explicit application-owned scope.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
## Client navigation
|
|
2
|
+
|
|
3
|
+
ERSC handles eligible document navigations through the browser Navigation API and
|
|
4
|
+
`NavigationPrecommitController`. There is no History API fallback. A browser missing either one
|
|
5
|
+
never hydrates: the streamed document stays as served and the application behaves as a plain
|
|
6
|
+
multi-page application, with document navigations and natively submitted forms but no interactive
|
|
7
|
+
Client Components.
|
|
8
|
+
|
|
9
|
+
An intercepted Page navigation has two milestones:
|
|
10
|
+
|
|
11
|
+
- **Native commit:** ERSC starts the Flight request in a React Transition, retains the common Layout
|
|
12
|
+
prefix, and publishes the destination in another Transition after the asynchronous load. The
|
|
13
|
+
precommit handler settles at the destination's first UI commit. The Navigation API can
|
|
14
|
+
then commit the URL and history entry, apply focus and default scroll, and finish any React View
|
|
15
|
+
Transition without waiting for Flight EOF.
|
|
16
|
+
- **Stream completion:** after native commit, the client router owns any remaining Flight stream
|
|
17
|
+
until EOF or until React confirms that another render retired it. A completed tree is cached for
|
|
18
|
+
the exact Navigation API history-entry id that committed for that navigation.
|
|
19
|
+
|
|
20
|
+
Canceling or superseding before commit interrupts the client transport and server request Effects.
|
|
21
|
+
A scheduled destination is discarded before its stream is released, so no rollback is needed. The
|
|
22
|
+
current UI and stream remain live while a successor prepares and retire only after React confirms a
|
|
23
|
+
different render. After native commit, Browser Stop no longer owns the stream; later Flight failures
|
|
24
|
+
use React's Error Boundary handling.
|
|
25
|
+
|
|
26
|
+
Back/Forward traversal reuses a completed cached payload. Push, replace, and uncached traversal
|
|
27
|
+
fetch fresh Flight. Disposing a history entry evicts its payload; a Server Function refresh clears
|
|
28
|
+
the traversal cache because a mutation may affect any route.
|
|
29
|
+
|
|
30
|
+
Flight redirects use the response's final URL; a non-success or non-Flight response becomes a
|
|
31
|
+
full-document navigation. Native focus and scroll remain enabled. Because Suspense content may
|
|
32
|
+
continue after native commit, history can remember an intermediate fallback's scroll position;
|
|
33
|
+
stream-aware restoration is not yet implemented.
|
|
34
|
+
|
|
35
|
+
### React View Transitions
|
|
36
|
+
|
|
37
|
+
Applications own React `<ViewTransition>` boundaries and all animation CSS. ERSC does not wrap the
|
|
38
|
+
route tree or call `document.startViewTransition()`. It calls React's `addTransitionType()` inside
|
|
39
|
+
the same Transition that publishes an initial navigation or refresh render, so application
|
|
40
|
+
boundaries can select animation policy without delaying native navigation until Flight EOF.
|
|
41
|
+
|
|
42
|
+
The types are additive:
|
|
43
|
+
|
|
44
|
+
| Publication | Added types |
|
|
45
|
+
| ------------------------------------------------------ | -------------------------------------------------- |
|
|
46
|
+
| Every routed navigation | `navigation`, `navigation-${event.navigationType}` |
|
|
47
|
+
| Push navigation | `navigation-forward` |
|
|
48
|
+
| Backward traversal | `navigation-backward` |
|
|
49
|
+
| Forward traversal | `navigation-forward` |
|
|
50
|
+
| Navigation with `event.hasUAVisualTransition` | `navigation-ua-visual-transition` |
|
|
51
|
+
| Server Function response tree or current-route refresh | `server-function` |
|
|
52
|
+
| HMR current-route refresh | `hmr-refresh` |
|
|
53
|
+
|
|
54
|
+
`event.navigationType` is `push`, `replace`, or `traverse`. Replace has no direction type. A
|
|
55
|
+
traversal has no direction type when either history index is unavailable or the indices are equal.
|
|
56
|
+
Applications may suppress author animation for `navigation-ua-visual-transition` and `hmr-refresh`,
|
|
57
|
+
but ERSC does not impose that policy.
|
|
58
|
+
|
|
59
|
+
These types describe only the first publication. Suspense content that resolves later renders in a
|
|
60
|
+
separate, untyped React Transition. Applications should use their own Suspense-specific
|
|
61
|
+
`<ViewTransition>` boundaries and styling for those reveals.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
## Server Function execution and refresh
|
|
2
|
+
|
|
3
|
+
Hydrated invocations and progressively enhanced forms execute the same request-scoped Effect
|
|
4
|
+
handler. A hydrated response contains the Server Function result and a refreshed route tree; a
|
|
5
|
+
progressively enhanced response contains a complete document with the refreshed tree and form state.
|
|
6
|
+
|
|
7
|
+
For hydrated calls, the result Promise settles independently from the route refresh. ERSC commits
|
|
8
|
+
the refreshed tree in a React transition and keeps the request active through React commit and
|
|
9
|
+
Flight EOF. Disconnecting interrupts unfinished request work and the response stream.
|
|
10
|
+
|
|
11
|
+
Hydrated invocations may execute concurrently. Only the latest invocation may apply its response's
|
|
12
|
+
route tree while its original history entry remains current and no navigation is active. Other
|
|
13
|
+
responses trigger a fresh current-route refresh. A response tree interrupts any older current-route
|
|
14
|
+
refresh before rendering.
|
|
15
|
+
|
|
16
|
+
After a successful mutation, ERSC clears the Back/Forward traversal cache because any route may have
|
|
17
|
+
changed.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## Advanced
|
|
2
|
+
|
|
3
|
+
These guides describe ERSC's runtime guarantees. See the
|
|
4
|
+
[current limitations](https://github.com/nikhilsnayak/effective-rsc/blob/main/docs/ARCHITECTURE.md#known-limitations)
|
|
5
|
+
before adopting them.
|
|
6
|
+
|
|
7
|
+
<!-- source-navigation -->
|
|
8
|
+
|
|
9
|
+
- [Request runtime and lifetimes](./01-request-runtime-and-lifetimes/index.md)
|
|
10
|
+
- [Client navigation](./02-client-navigation/index.md)
|
|
11
|
+
- [Server Function execution and refresh](./03-server-function-execution-and-refresh/index.md)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## Application
|
|
2
|
+
|
|
3
|
+
`Application.ersc<Services>()` creates one application-scoped ERSC identity and its base authoring
|
|
4
|
+
view. `Services` is the complete server-service union; omit it for a service-free application.
|
|
5
|
+
|
|
6
|
+
`ERSC.make({ routes, layer })` closes the route graph and application runtime. Export its result from
|
|
7
|
+
`src/application.tsx`. `layer` is required unless `Services` is `never`; it may provide the declared
|
|
8
|
+
services and register native Effect HTTP on the framework router.
|